-
-
Notifications
You must be signed in to change notification settings - Fork 1.6k
[DataGridPremium] Reordering support for row grouping #18251
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 62 commits
93bc77a
9ee3222
f731de5
578d189
870ed69
1bc38ef
511e47f
e91ecab
4f0f835
f67dd82
78b546f
048462e
fef3ef0
f9c1652
1577fd3
d2b49f9
8eeb048
d084c86
935dd13
56b7dc8
b4b8343
a9dec8c
14f5f0b
e4b548a
06719d5
609c128
54caf27
a723172
e538c2b
416cc3b
21f65ff
9a22d20
1afe0a6
1b26322
d133cda
e88810d
7c32d4e
9cb0673
e59106f
9217f70
225b65a
21746db
1146783
5866e32
9ecd8a5
1f7be68
06f3a49
93c203b
a863742
fcf5d17
12db498
8c847ed
453f143
fd5c78f
ab10a29
4a32394
8adf89f
eb666bc
e9f192d
346749f
7ed2268
a97e02b
0a24f22
3637d97
790db7f
2e1255a
34a8b60
7ea989e
142503e
28de358
30ce5ee
4dfd83a
7bf8c2a
71abb0d
1d0658d
0878ea1
cf5e47b
03d218e
901cb86
fe93004
743685d
6c16ad9
dfba357
8e11472
7656ec4
1da5aef
3ab02c0
0eaaa25
fccaf20
e010145
07af9c8
9ec8bdb
2cdb036
7bd8a9d
218558e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,71 @@ | ||
import * as React from 'react'; | ||
import { | ||
DataGridPremium, | ||
useGridApiRef, | ||
useKeepGroupedColumnsHidden, | ||
} from '@mui/x-data-grid-premium'; | ||
import { useMovieData } from '@mui/x-data-grid-generator'; | ||
|
||
export default function RowGroupingGroupingValueSetter() { | ||
const data = useMovieData(); | ||
const apiRef = useGridApiRef(); | ||
|
||
const columnsWithComposer = React.useMemo( | ||
() => [ | ||
...data.columns, | ||
{ | ||
field: 'composer', | ||
headerName: 'Composer', | ||
valueGetter: (value) => value.name, | ||
groupingValueGetter: (value) => value.name, | ||
groupingValueSetter: (value, row) => { | ||
return { | ||
...row, | ||
composer: { name: value }, | ||
}; | ||
}, | ||
width: 200, | ||
}, | ||
{ | ||
field: 'decade', | ||
headerName: 'Decade', | ||
valueGetter: (value, row) => Math.floor(row.year / 10) * 10, | ||
groupingValueGetter: (value, row) => Math.floor(row.year / 10) * 10, | ||
groupingValueSetter: (value, row) => ({ | ||
...row, | ||
// Since converting to decade is a lossy operation, directly using the decade value should be sufficient here | ||
year: value, | ||
}), | ||
renderCell: (params) => { | ||
if (params.value == null) { | ||
return ''; | ||
} | ||
|
||
return `${params.value.toString().slice(-2)}'s`; | ||
}, | ||
}, | ||
], | ||
[data.columns], | ||
); | ||
|
||
const initialState = useKeepGroupedColumnsHidden({ | ||
apiRef, | ||
initialState: { | ||
rowGrouping: { | ||
model: ['composer', 'decade'], | ||
}, | ||
}, | ||
}); | ||
|
||
return ( | ||
<div style={{ height: 400, width: '100%' }}> | ||
<DataGridPremium | ||
{...data} | ||
columns={columnsWithComposer} | ||
apiRef={apiRef} | ||
initialState={initialState} | ||
rowReordering | ||
/> | ||
</div> | ||
); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,72 @@ | ||
import * as React from 'react'; | ||
import { | ||
DataGridPremium, | ||
useGridApiRef, | ||
GridColDef, | ||
useKeepGroupedColumnsHidden, | ||
} from '@mui/x-data-grid-premium'; | ||
import { useMovieData, Movie } from '@mui/x-data-grid-generator'; | ||
|
||
export default function RowGroupingGroupingValueSetter() { | ||
const data = useMovieData(); | ||
const apiRef = useGridApiRef(); | ||
|
||
const columnsWithComposer = React.useMemo( | ||
() => [ | ||
...data.columns, | ||
{ | ||
field: 'composer', | ||
headerName: 'Composer', | ||
valueGetter: (value: { name: string }) => value.name, | ||
groupingValueGetter: (value: { name: string }) => value.name, | ||
MBilalShafi marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
groupingValueSetter: (value, row) => { | ||
return { | ||
...row, | ||
composer: { name: value }, | ||
}; | ||
}, | ||
width: 200, | ||
} as GridColDef<Movie, string>, | ||
{ | ||
field: 'decade', | ||
headerName: 'Decade', | ||
valueGetter: (value, row) => Math.floor(row.year / 10) * 10, | ||
MBilalShafi marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
groupingValueGetter: (value, row) => Math.floor(row.year / 10) * 10, | ||
MBilalShafi marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
groupingValueSetter: (value, row) => ({ | ||
...row, | ||
// Since converting to decade is a lossy operation, directly using the decade value should be sufficient here | ||
year: value, | ||
}), | ||
renderCell: (params) => { | ||
if (params.value == null) { | ||
return ''; | ||
} | ||
|
||
return `${params.value.toString().slice(-2)}'s`; | ||
}, | ||
} as GridColDef<Movie, number>, | ||
MBilalShafi marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
], | ||
[data.columns], | ||
); | ||
|
||
const initialState = useKeepGroupedColumnsHidden({ | ||
apiRef, | ||
initialState: { | ||
rowGrouping: { | ||
model: ['composer', 'decade'], | ||
}, | ||
}, | ||
}); | ||
|
||
return ( | ||
<div style={{ height: 400, width: '100%' }}> | ||
<DataGridPremium | ||
{...data} | ||
columns={columnsWithComposer} | ||
apiRef={apiRef} | ||
initialState={initialState} | ||
rowReordering | ||
/> | ||
</div> | ||
); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
<DataGridPremium | ||
{...data} | ||
columns={columnsWithComposer} | ||
apiRef={apiRef} | ||
initialState={initialState} | ||
rowReordering | ||
/> |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
import * as React from 'react'; | ||
import { | ||
DataGridPremium, | ||
useGridApiRef, | ||
useKeepGroupedColumnsHidden, | ||
} from '@mui/x-data-grid-premium'; | ||
import { useMovieData } from '@mui/x-data-grid-generator'; | ||
|
||
export default function RowGroupingReordering() { | ||
const data = useMovieData(); | ||
const apiRef = useGridApiRef(); | ||
|
||
const initialState = useKeepGroupedColumnsHidden({ | ||
apiRef, | ||
initialState: { | ||
rowGrouping: { | ||
model: ['company'], | ||
}, | ||
}, | ||
}); | ||
|
||
return ( | ||
<div style={{ height: 400, width: '100%' }}> | ||
<DataGridPremium | ||
{...data} | ||
apiRef={apiRef} | ||
initialState={initialState} | ||
rowReordering | ||
/> | ||
</div> | ||
); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
import * as React from 'react'; | ||
import { | ||
DataGridPremium, | ||
useGridApiRef, | ||
useKeepGroupedColumnsHidden, | ||
} from '@mui/x-data-grid-premium'; | ||
import { useMovieData } from '@mui/x-data-grid-generator'; | ||
|
||
export default function RowGroupingReordering() { | ||
const data = useMovieData(); | ||
const apiRef = useGridApiRef(); | ||
|
||
const initialState = useKeepGroupedColumnsHidden({ | ||
apiRef, | ||
initialState: { | ||
rowGrouping: { | ||
model: ['company'], | ||
MBilalShafi marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
}, | ||
}, | ||
}); | ||
|
||
return ( | ||
<div style={{ height: 400, width: '100%' }}> | ||
<DataGridPremium | ||
{...data} | ||
apiRef={apiRef} | ||
initialState={initialState} | ||
rowReordering | ||
/> | ||
</div> | ||
); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
<DataGridPremium | ||
{...data} | ||
apiRef={apiRef} | ||
initialState={initialState} | ||
rowReordering | ||
/> |
Original file line number | Diff line number | Diff line change | ||
---|---|---|---|---|
|
@@ -215,10 +215,10 @@ | |||
|
||||
{{"demo": "RowGroupingReadOnly.js", "bg": "inline", "defaultCodeOpen": false}} | ||||
|
||||
## Using groupingValueGetter for complex grouping value | ||||
## Using groupingValueGetter() for complex grouping value | ||||
|
||||
The grouping value must be either a string, a number, `null`, or `undefined`. | ||||
If your cell value is more complex, pass a `groupingValueGetter` property to the column definition to convert it into a valid value. | ||||
If your cell value is more complex, pass a `groupingValueGetter()` property to the column definition to convert it into a valid value. | ||||
|
||||
```ts | ||||
const columns: GridColDef[] = [ | ||||
|
@@ -377,6 +377,78 @@ | |||
|
||||
::: | ||||
|
||||
## Reorder groups with drag and drop | ||||
MBilalShafi marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||||
|
||||
Row reordering allows the users to reorder row groups, or move rows from one group to another. | ||||
MBilalShafi marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||||
To enable this feature with the row grouping, pass the `rowReordering` prop to the Data Grid Premium component: | ||||
MBilalShafi marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||||
|
||||
```tsx | ||||
<DataGridPremium rowGroupingModel={['category']} rowReordering /> | ||||
``` | ||||
|
||||
{{"demo": "RowGroupingReordering.js", "bg": "inline", "defaultCodeOpen": false}} | ||||
MBilalShafi marked this conversation as resolved.
Show resolved
Hide resolved
|
||||
|
||||
### Reacting to group updates | ||||
|
||||
Whenever a row is moved from one group to another, it warrants a row update, the row data value that was used to group this row must now be updated to maintain the row grouping data integrity. | ||||
MBilalShafi marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||||
|
||||
For example, on a Data Grid having movies grouped by companies, if a row "Avatar" is moved from "20th Century Fox" to "Disney Studios" group, apart from the row being updated in the row tree, the row data must be updated to reflect this change. | ||||
MBilalShafi marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||||
|
||||
```diff | ||||
// "Avatar" row | ||||
{ | ||||
title: 'Avatar', | ||||
- company: '20th Century Fox', | ||||
+ company: 'Disney Studios', | ||||
... | ||||
} | ||||
``` | ||||
|
||||
The Data Grid updates the internal row data, but to persist the change on the server, the [`processRowUpdate()`](/x/react-data-grid/editing/persistence/#the-processrowupdate-callback) callback must be used. | ||||
MBilalShafi marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||||
|
||||
### Usage with groupingValueSetter() | ||||
|
||||
If you use [`colDef.groupingValueGetter()`](#using-groupingvaluegetter-for-complex-grouping-value) to handle complex grouping values, in order for grouping across rows to work, you must use the `colDef.groupingValueSetter()` to properly convert back the simple value to the complex one. | ||||
MBilalShafi marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||||
|
||||
It should return the updated row based on the groupKey (`value`) corresponding to the target group. | ||||
MBilalShafi marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||||
|
||||
```ts | ||||
const columns: GridColDef[] = [ | ||||
{ | ||||
field: 'composer', | ||||
groupingValueGetter: (value) => value.name, | ||||
groupingValueSetter: (value, row) => ({ | ||||
...row, | ||||
composer: { name: value }, | ||||
MBilalShafi marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||||
}), | ||||
}, | ||||
// ... | ||||
]; | ||||
``` | ||||
|
||||
{{"demo": "RowGroupingGroupingValueSetter.js", "bg": "inline", "defaultCodeOpen": false}} | ||||
|
||||
:::warning | ||||
|
||||
There are some limitations when reordering grouped rows: | ||||
|
||||
- Leaf rows (the lowest level) can only be moved within their current group or another group at the same level—they cannot become parents. | ||||
- Parent rows can only be reordered among other parents at the same level; they cannot be moved to a different level or group. | ||||
|
handleRowUpdate(rowId).then(resolve).catch(resolve); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@cherniavskii Thanks for linking the clipboard paste logic.
IIUC, clipboard paste is usually a client-side operation. Even when it’s backed by a server update, partial updates (i.e., some rows succeed, others fail) are acceptable by simply discarding the rows that failed.
However, row reordering seems to be a bit different. For example, if a grouped row “X” is moved from parent group B to A, and only 5 out of 10 child rows update successfully, the group could end up appearing in both groups. This leads to a confusing and inconsistent state.
Take this demo as an example when moving “Anthony & Joe Russo” to “20th Century Fox”.
If some rows are updated and others aren’t, the group could appear under both “20th Century Fox” and “Disney Studios”.
As you mentioned, one option is to revert the update entirely if there’s a failure. But by that time, the backend will have already committed partial changes for the succeeded rows, and a revert may cause inconsistent client and server states.
So I’m curious — how would you feel about a scenario where, after a reorder, the group “Anthony & Joe Russo” ends up shown under both “20th Century Fox” and “Disney Studios”?
Would you consider that an acceptable edge case or a broken state from a user’s perspective?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
However, row reordering seems to be a bit different. For example, if a grouped row “X” is moved from parent group B to A, and only 5 out of 10 child rows update successfully, the group could end up appearing in both groups. This leads to a confusing and inconsistent state.
From what I can tell, it's the same for clipboard paste. If the pasted data impacts 10 rows, processRowUpdate
will be called 10 times (for each row individually), and the failed server updates will mean that these rows will not be changed.
Take this demo as an example when moving “Anthony & Joe Russo” to “20th Century Fox”.
If some rows are updated and others aren’t, the group could appear under both “20th Century Fox” and “Disney Studios”.
So I’m curious — how would you feel about a scenario where, after a reorder, the group “Anthony & Joe Russo” ends up shown under both “20th Century Fox” and “Disney Studios”?
Would you consider that an acceptable edge case or a broken state from a user’s perspective?
This behavior looks good to me.
I think we can assume 2 categories of server update failures here:
- You don't have permission to update specific rows, or there's a bug in the backend and it throws error.
In this case, the row should stay in the original position. - Connectivity issue (network, server) – if you have a mechanism to retry failed requests – this will be transparent from the grid point of view. If not – the row will stay at the original positions.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
From what I can tell, it's the same for clipboard paste. If the pasted data impacts 10 rows, processRowUpdate will be called 10 times (for each row individually), and the failed server updates will mean that these rows will not be changed.
@cherniavskii Thank you for your feedback. It made me realize we have to do it inevitably for the tree data feature so I did it for row grouping. (Updated the PR description a bit too)
Partial movement of nested groups (potential problem I raised above) is supported now as we could have same director working for multiple companies. (May be part time or on project basis 😄)
I'd still like to change the signature of processRowUpdate()
to handle multiple rows at a time in v9.
This behavior looks good to me.
I think we can assume 2 categories of server update failures here:You don't have permission to update specific rows, or there's a bug in the backend and it throws error.
In this case, the row should stay in the original position.
Connectivity issue (network, server) – if you have a mechanism to retry failed requests – this will be transparent from the grid point of view. If not – the row will stay at the original positions.
I didn't classify the types of errors with the current PR, aiming to give it a shot with #18947, would be a nice behavior if we are able to handle connectivity issues.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I played with demos a bit – seems to work like a charm, nice!
I'll review the code a bit later 👍🏻
MBilalShafi marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
Check failure on line 444 in docs/data/data-grid/row-grouping/row-grouping.md
GitHub Actions / runner / vale
[vale] reported by reviewdog 🐶
[MUI.GoogleLatin] Use 'that is' instead of 'i.e.'
Raw Output:
{"message": "[MUI.GoogleLatin] Use 'that is' instead of 'i.e.'", "location": {"path": "docs/data/data-grid/row-grouping/row-grouping.md", "range": {"start": {"line": 444, "column": 122}}}, "severity": "ERROR"}
Check failure on line 444 in docs/data/data-grid/row-grouping/row-grouping.md
GitHub Actions / runner / vale
[vale] reported by reviewdog 🐶
[MUI.CorrectRererenceCased] Use 'i.e.' instead of 'i.e'
Raw Output:
{"message": "[MUI.CorrectRererenceCased] Use 'i.e.' instead of 'i.e'", "location": {"path": "docs/data/data-grid/row-grouping/row-grouping.md", "range": {"start": {"line": 444, "column": 122}}}, "severity": "ERROR"}
MBilalShafi marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
Check failure on line 448 in docs/data/data-grid/row-grouping/row-grouping.md
GitHub Actions / runner / vale
[vale] reported by reviewdog 🐶
[MUI.CorrectReferenceAllCases] Use 'use case' instead of 'use-case'
Raw Output:
{"message": "[MUI.CorrectReferenceAllCases] Use 'use case' instead of 'use-case'", "location": {"path": "docs/data/data-grid/row-grouping/row-grouping.md", "range": {"start": {"line": 448, "column": 88}}}, "severity": "ERROR"}
Uh oh!
There was an error while loading. Please reload this page.