feat: group by (#13138)

Supports grouping documents by specific fields within the list view.

For example, imagine having a "posts" collection with a "categories"
field. To report on each specific category, you'd traditionally filter
for each category, one at a time. This can be quite inefficient,
especially with large datasets.

Now, you can interact with all categories simultaneously, grouped by
distinct values.

Here is a simple demonstration:


https://github.com/user-attachments/assets/0dcd19d2-e983-47e6-9ea2-cfdd2424d8b5

Enable on any collection by setting the `admin.groupBy` property:

```ts
import type { CollectionConfig } from 'payload'

const MyCollection: CollectionConfig = {
  // ...
  admin: {
    groupBy: true
  }
}
```

This is currently marked as beta to gather feedback while we reach full
stability, and to leave room for API changes and other modifications.
Use at your own risk.

Note: when using `groupBy`, bulk editing is done group-by-group. In the
future we may support cross-group bulk editing.

Dependent on #13102 (merged).

---
- To see the specific tasks where the Asana app for GitHub is being
used, see below:
  - https://app.asana.com/0/0/1210774523852467

---------

Co-authored-by: Paul Popus <paul@payloadcms.com>
This commit is contained in:
Jacob Fletcher
2025-07-24 14:00:52 -04:00
committed by GitHub
parent 14322a71bb
commit bccf6ab16f
124 changed files with 7181 additions and 447 deletions

View File

@@ -82,9 +82,9 @@ export interface Config {
collectionsJoins: {
'orderable-join': {
orderableJoinField1: 'orderable';
'group.orderableJoinField': 'orderable';
orderableJoinField2: 'orderable';
nonOrderableJoinField: 'orderable';
'group.orderableJoinField': 'orderable';
};
};
collectionsSelect: {
@@ -206,8 +206,8 @@ export interface Localized {
*/
export interface Orderable {
id: string;
_orderable_orderableJoinField2_order?: string | null;
_orderable_group_orderableJoinField_order?: string | null;
_orderable_orderableJoinField2_order?: string | null;
_orderable_orderableJoinField1_order?: string | null;
_order?: string | null;
title?: string | null;
@@ -227,13 +227,6 @@ export interface OrderableJoin {
hasNextPage?: boolean;
totalDocs?: number;
};
group?: {
orderableJoinField?: {
docs?: (string | Orderable)[];
hasNextPage?: boolean;
totalDocs?: number;
};
};
orderableJoinField2?: {
docs?: (string | Orderable)[];
hasNextPage?: boolean;
@@ -244,6 +237,13 @@ export interface OrderableJoin {
hasNextPage?: boolean;
totalDocs?: number;
};
group?: {
orderableJoinField?: {
docs?: (string | Orderable)[];
hasNextPage?: boolean;
totalDocs?: number;
};
};
updatedAt: string;
createdAt: string;
}
@@ -262,6 +262,13 @@ export interface User {
hash?: string | null;
loginAttempts?: number | null;
lockUntil?: string | null;
sessions?:
| {
id: string;
createdAt?: string | null;
expiresAt: string;
}[]
| null;
password?: string | null;
}
/**
@@ -417,8 +424,8 @@ export interface LocalizedSelect<T extends boolean = true> {
* via the `definition` "orderable_select".
*/
export interface OrderableSelect<T extends boolean = true> {
_orderable_orderableJoinField2_order?: T;
_orderable_group_orderableJoinField_order?: T;
_orderable_orderableJoinField2_order?: T;
_orderable_orderableJoinField1_order?: T;
_order?: T;
title?: T;
@@ -433,13 +440,13 @@ export interface OrderableSelect<T extends boolean = true> {
export interface OrderableJoinSelect<T extends boolean = true> {
title?: T;
orderableJoinField1?: T;
orderableJoinField2?: T;
nonOrderableJoinField?: T;
group?:
| T
| {
orderableJoinField?: T;
};
orderableJoinField2?: T;
nonOrderableJoinField?: T;
updatedAt?: T;
createdAt?: T;
}
@@ -457,6 +464,13 @@ export interface UsersSelect<T extends boolean = true> {
hash?: T;
loginAttempts?: T;
lockUntil?: T;
sessions?:
| T
| {
id?: T;
createdAt?: T;
expiresAt?: T;
};
}
/**
* This interface was referenced by `Config`'s JSON-Schema