diff --git a/docs/data/data-grid/row-grouping/row-grouping.md b/docs/data/data-grid/row-grouping/row-grouping.md index b70899fb38b4..13f56a2ebac8 100644 --- a/docs/data/data-grid/row-grouping/row-grouping.md +++ b/docs/data/data-grid/row-grouping/row-grouping.md @@ -11,6 +11,10 @@ In the following example, movies are grouped based on their production `company` {{"demo": "RowGroupingBasicExample.js", "bg": "inline", "defaultCodeOpen": false}} +:::info +If you are looking for row grouping on the server-side, see [server-side row grouping](/x/react-data-grid/server-side-data/row-grouping/). +::: + ## Grouping criteria ### Initialize the row grouping @@ -252,6 +256,10 @@ Use the `setRowChildrenExpansion` method on `apiRef` to programmatically set the {{"demo": "RowGroupingSetChildrenExpansion.js", "bg": "inline", "defaultCodeOpen": false}} +:::warning +The `apiRef.current.setRowChildrenExpansion` method is not compatible with the [server-side tree data](/x/react-data-grid/server-side-data/tree-data/) and [server-side row grouping](/x/react-data-grid/server-side-data/row-grouping/). Use `apiRef.current.unstable_dataSource.fetchRows` instead. +::: + ### Customize grouping cell indent To change the default cell indent, you can use the `--DataGrid-cellOffsetMultiplier` CSS variable: @@ -280,10 +288,6 @@ If you are rendering leaves with the `leafField` property of `groupingColDef`, t You can force the filtering to be applied on another grouping criteria with the `mainGroupingCriteria` property of `groupingColDef` -:::warning -This feature is not yet compatible with `sortingMode = "server"` and `filteringMode = "server"`. -::: - {{"demo": "RowGroupingFilteringSingleGroupingColDef.js", "bg": "inline", "defaultCodeOpen": false}} ### Multiple grouping columns @@ -376,6 +380,10 @@ const rows = apiRef.current.getRowGroupChildren({ {{"demo": "RowGroupingGetRowGroupChildren.js", "bg": "inline", "defaultCodeOpen": false}} +:::warning +The `apiRef.current.getRowGroupChildren` method is not compatible with the [server-side row grouping](/x/react-data-grid/server-side-data/row-grouping/) since all the rows might not be available to get at a given instance. +::: + ## Row group panel 🚧 :::warning diff --git a/docs/data/data-grid/server-side-data/ServerSideRowGroupingDataGrid.js b/docs/data/data-grid/server-side-data/ServerSideRowGroupingDataGrid.js new file mode 100644 index 000000000000..fc75932d136b --- /dev/null +++ b/docs/data/data-grid/server-side-data/ServerSideRowGroupingDataGrid.js @@ -0,0 +1,69 @@ +import * as React from 'react'; +import { + DataGridPremium, + useGridApiRef, + useKeepGroupedColumnsHidden, +} from '@mui/x-data-grid-premium'; +import { useMockServer } from '@mui/x-data-grid-generator'; +import Button from '@mui/material/Button'; + +export default function ServerSideRowGroupingDataGrid() { + const apiRef = useGridApiRef(); + + const { fetchRows, columns } = useMockServer({ + rowGrouping: true, + }); + + const dataSource = React.useMemo(() => { + return { + getRows: async (params) => { + const urlParams = new URLSearchParams({ + paginationModel: JSON.stringify(params.paginationModel), + filterModel: JSON.stringify(params.filterModel), + sortModel: JSON.stringify(params.sortModel), + groupKeys: JSON.stringify(params.groupKeys), + groupFields: JSON.stringify(params.groupFields), + }); + const getRowsResponse = await fetchRows( + `https://mui.com/x/api/data-grid?${urlParams.toString()}`, + ); + return { + rows: getRowsResponse.rows, + rowCount: getRowsResponse.rowCount, + }; + }, + getGroupKey: (row) => row.group, + getChildrenCount: (row) => row.descendantCount, + }; + }, [fetchRows]); + + const initialState = useKeepGroupedColumnsHidden({ + apiRef, + initialState: { + rowGrouping: { + model: ['company', 'director'], + }, + }, + }); + + return ( +
+ + +
+ +
+
+ ); +} diff --git a/docs/data/data-grid/server-side-data/ServerSideRowGroupingDataGrid.tsx b/docs/data/data-grid/server-side-data/ServerSideRowGroupingDataGrid.tsx new file mode 100644 index 000000000000..91c7f66a6d99 --- /dev/null +++ b/docs/data/data-grid/server-side-data/ServerSideRowGroupingDataGrid.tsx @@ -0,0 +1,70 @@ +import * as React from 'react'; +import { + DataGridPremium, + GridDataSource, + useGridApiRef, + useKeepGroupedColumnsHidden, +} from '@mui/x-data-grid-premium'; +import { useMockServer } from '@mui/x-data-grid-generator'; +import Button from '@mui/material/Button'; + +export default function ServerSideRowGroupingDataGrid() { + const apiRef = useGridApiRef(); + + const { fetchRows, columns } = useMockServer({ + rowGrouping: true, + }); + + const dataSource: GridDataSource = React.useMemo(() => { + return { + getRows: async (params) => { + const urlParams = new URLSearchParams({ + paginationModel: JSON.stringify(params.paginationModel), + filterModel: JSON.stringify(params.filterModel), + sortModel: JSON.stringify(params.sortModel), + groupKeys: JSON.stringify(params.groupKeys), + groupFields: JSON.stringify(params.groupFields), + }); + const getRowsResponse = await fetchRows( + `https://mui.com/x/api/data-grid?${urlParams.toString()}`, + ); + return { + rows: getRowsResponse.rows, + rowCount: getRowsResponse.rowCount, + }; + }, + getGroupKey: (row) => row.group, + getChildrenCount: (row) => row.descendantCount, + }; + }, [fetchRows]); + + const initialState = useKeepGroupedColumnsHidden({ + apiRef, + initialState: { + rowGrouping: { + model: ['company', 'director'], + }, + }, + }); + + return ( +
+ + +
+ +
+
+ ); +} diff --git a/docs/data/data-grid/server-side-data/ServerSideRowGroupingDataGrid.tsx.preview b/docs/data/data-grid/server-side-data/ServerSideRowGroupingDataGrid.tsx.preview new file mode 100644 index 000000000000..920c80a41342 --- /dev/null +++ b/docs/data/data-grid/server-side-data/ServerSideRowGroupingDataGrid.tsx.preview @@ -0,0 +1,16 @@ + + +
+ +
\ No newline at end of file diff --git a/docs/data/data-grid/server-side-data/ServerSideRowGroupingErrorHandling.js b/docs/data/data-grid/server-side-data/ServerSideRowGroupingErrorHandling.js new file mode 100644 index 000000000000..793347a26301 --- /dev/null +++ b/docs/data/data-grid/server-side-data/ServerSideRowGroupingErrorHandling.js @@ -0,0 +1,137 @@ +import * as React from 'react'; +import { + DataGridPremium, + useGridApiRef, + useKeepGroupedColumnsHidden, +} from '@mui/x-data-grid-premium'; +import { useMockServer } from '@mui/x-data-grid-generator'; +import Snackbar from '@mui/material/Snackbar'; +import Button from '@mui/material/Button'; +import Checkbox from '@mui/material/Checkbox'; +import FormControlLabel from '@mui/material/FormControlLabel'; +import { alpha, styled, darken, lighten } from '@mui/material/styles'; + +export default function ServerSideRowGroupingErrorHandling() { + const apiRef = useGridApiRef(); + const [rootError, setRootError] = React.useState(); + const [childrenError, setChildrenError] = React.useState(); + const [shouldRequestsFail, setShouldRequestsFail] = React.useState(false); + + const { fetchRows, columns } = useMockServer( + { + rowGrouping: true, + }, + {}, + shouldRequestsFail, + ); + + const dataSource = React.useMemo(() => { + return { + getRows: async (params) => { + const urlParams = new URLSearchParams({ + paginationModel: JSON.stringify(params.paginationModel), + filterModel: JSON.stringify(params.filterModel), + sortModel: JSON.stringify(params.sortModel), + groupKeys: JSON.stringify(params.groupKeys), + groupFields: JSON.stringify(params.groupFields), + }); + const getRowsResponse = await fetchRows( + `https://mui.com/x/api/data-grid?${urlParams.toString()}`, + ); + return { + rows: getRowsResponse.rows, + rowCount: getRowsResponse.rowCount, + }; + }, + getGroupKey: (row) => row.group, + getChildrenCount: (row) => row.descendantCount, + }; + }, [fetchRows]); + + const initialState = useKeepGroupedColumnsHidden({ + apiRef, + initialState: { + rowGrouping: { + model: ['company', 'director'], + }, + }, + }); + + return ( +
+
+ + setShouldRequestsFail(event.target.checked)} + /> + } + label="Make the requests fail" + /> +
+
+ { + if (!params.groupKeys || params.groupKeys.length === 0) { + setRootError(error.message); + } else { + setChildrenError( + `${error.message} (Requested level: ${params.groupKeys.join(' > ')})`, + ); + } + }} + unstable_dataSourceCache={null} + apiRef={apiRef} + initialState={initialState} + /> + {rootError && } + setChildrenError('')} + message={childrenError} + /> +
+
+ ); +} + +function getBorderColor(theme) { + if (theme.palette.mode === 'light') { + return lighten(alpha(theme.palette.divider, 1), 0.88); + } + return darken(alpha(theme.palette.divider, 1), 0.68); +} + +const StyledDiv = styled('div')(({ theme: t }) => ({ + position: 'absolute', + zIndex: 10, + fontSize: '0.875em', + top: 0, + height: '100%', + width: '100%', + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + borderRadius: '4px', + border: `1px solid ${getBorderColor(t)}`, + backgroundColor: t.palette.background.default, +})); + +function ErrorOverlay({ error }) { + if (!error) { + return null; + } + return {error}; +} diff --git a/docs/data/data-grid/server-side-data/ServerSideRowGroupingErrorHandling.tsx b/docs/data/data-grid/server-side-data/ServerSideRowGroupingErrorHandling.tsx new file mode 100644 index 000000000000..621b74b052f4 --- /dev/null +++ b/docs/data/data-grid/server-side-data/ServerSideRowGroupingErrorHandling.tsx @@ -0,0 +1,138 @@ +import * as React from 'react'; +import { + DataGridPremium, + GridDataSource, + useGridApiRef, + useKeepGroupedColumnsHidden, +} from '@mui/x-data-grid-premium'; +import { useMockServer } from '@mui/x-data-grid-generator'; +import Snackbar from '@mui/material/Snackbar'; +import Button from '@mui/material/Button'; +import Checkbox from '@mui/material/Checkbox'; +import FormControlLabel from '@mui/material/FormControlLabel'; +import { alpha, styled, darken, lighten, Theme } from '@mui/material/styles'; + +export default function ServerSideRowGroupingErrorHandling() { + const apiRef = useGridApiRef(); + const [rootError, setRootError] = React.useState(); + const [childrenError, setChildrenError] = React.useState(); + const [shouldRequestsFail, setShouldRequestsFail] = React.useState(false); + + const { fetchRows, columns } = useMockServer( + { + rowGrouping: true, + }, + {}, + shouldRequestsFail, + ); + + const dataSource: GridDataSource = React.useMemo(() => { + return { + getRows: async (params) => { + const urlParams = new URLSearchParams({ + paginationModel: JSON.stringify(params.paginationModel), + filterModel: JSON.stringify(params.filterModel), + sortModel: JSON.stringify(params.sortModel), + groupKeys: JSON.stringify(params.groupKeys), + groupFields: JSON.stringify(params.groupFields), + }); + const getRowsResponse = await fetchRows( + `https://mui.com/x/api/data-grid?${urlParams.toString()}`, + ); + return { + rows: getRowsResponse.rows, + rowCount: getRowsResponse.rowCount, + }; + }, + getGroupKey: (row) => row.group, + getChildrenCount: (row) => row.descendantCount, + }; + }, [fetchRows]); + + const initialState = useKeepGroupedColumnsHidden({ + apiRef, + initialState: { + rowGrouping: { + model: ['company', 'director'], + }, + }, + }); + + return ( +
+
+ + setShouldRequestsFail(event.target.checked)} + /> + } + label="Make the requests fail" + /> +
+
+ { + if (!params.groupKeys || params.groupKeys.length === 0) { + setRootError(error.message); + } else { + setChildrenError( + `${error.message} (Requested level: ${params.groupKeys.join(' > ')})`, + ); + } + }} + unstable_dataSourceCache={null} + apiRef={apiRef} + initialState={initialState} + /> + {rootError && } + setChildrenError('')} + message={childrenError} + /> +
+
+ ); +} + +function getBorderColor(theme: Theme) { + if (theme.palette.mode === 'light') { + return lighten(alpha(theme.palette.divider, 1), 0.88); + } + return darken(alpha(theme.palette.divider, 1), 0.68); +} + +const StyledDiv = styled('div')(({ theme: t }) => ({ + position: 'absolute', + zIndex: 10, + fontSize: '0.875em', + top: 0, + height: '100%', + width: '100%', + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + borderRadius: '4px', + border: `1px solid ${getBorderColor(t)}`, + backgroundColor: t.palette.background.default, +})); + +function ErrorOverlay({ error }: { error: string }) { + if (!error) { + return null; + } + return {error}; +} diff --git a/docs/data/data-grid/server-side-data/ServerSideRowGroupingFullDataGrid.js b/docs/data/data-grid/server-side-data/ServerSideRowGroupingFullDataGrid.js new file mode 100644 index 000000000000..7db1ea09c9c3 --- /dev/null +++ b/docs/data/data-grid/server-side-data/ServerSideRowGroupingFullDataGrid.js @@ -0,0 +1,83 @@ +import * as React from 'react'; +import { + DataGridPremium, + useGridApiRef, + useKeepGroupedColumnsHidden, + GridToolbar, +} from '@mui/x-data-grid-premium'; +import { useMockServer } from '@mui/x-data-grid-generator'; +import Button from '@mui/material/Button'; + +export default function ServerSideRowGroupingFullDataGrid() { + const apiRef = useGridApiRef(); + + const { fetchRows, columns, loadNewData } = useMockServer({ + rowGrouping: true, + rowLength: 1000, + dataSet: 'Commodity', + maxColumns: 20, + }); + + const dataSource = React.useMemo(() => { + return { + getRows: async (params) => { + const urlParams = new URLSearchParams({ + paginationModel: JSON.stringify(params.paginationModel), + filterModel: JSON.stringify(params.filterModel), + sortModel: JSON.stringify(params.sortModel), + groupKeys: JSON.stringify(params.groupKeys), + groupFields: JSON.stringify(params.groupFields), + }); + const getRowsResponse = await fetchRows( + `https://mui.com/x/api/data-grid?${urlParams.toString()}`, + ); + return { + rows: getRowsResponse.rows, + rowCount: getRowsResponse.rowCount, + }; + }, + getGroupKey: (row) => row.group, + getChildrenCount: (row) => row.descendantCount, + }; + }, [fetchRows]); + + const initialState = useKeepGroupedColumnsHidden({ + apiRef, + initialState: { + rowGrouping: { + model: ['commodity', 'status'], + }, + columns: { + columnVisibilityModel: { + id: false, + }, + }, + }, + }); + + return ( +
+ + +
+ +
+
+ ); +} diff --git a/docs/data/data-grid/server-side-data/ServerSideRowGroupingFullDataGrid.tsx b/docs/data/data-grid/server-side-data/ServerSideRowGroupingFullDataGrid.tsx new file mode 100644 index 000000000000..4545cf49f9e7 --- /dev/null +++ b/docs/data/data-grid/server-side-data/ServerSideRowGroupingFullDataGrid.tsx @@ -0,0 +1,84 @@ +import * as React from 'react'; +import { + DataGridPremium, + GridDataSource, + useGridApiRef, + useKeepGroupedColumnsHidden, + GridToolbar, +} from '@mui/x-data-grid-premium'; +import { useMockServer } from '@mui/x-data-grid-generator'; +import Button from '@mui/material/Button'; + +export default function ServerSideRowGroupingFullDataGrid() { + const apiRef = useGridApiRef(); + + const { fetchRows, columns, loadNewData } = useMockServer({ + rowGrouping: true, + rowLength: 1000, + dataSet: 'Commodity', + maxColumns: 20, + }); + + const dataSource: GridDataSource = React.useMemo(() => { + return { + getRows: async (params) => { + const urlParams = new URLSearchParams({ + paginationModel: JSON.stringify(params.paginationModel), + filterModel: JSON.stringify(params.filterModel), + sortModel: JSON.stringify(params.sortModel), + groupKeys: JSON.stringify(params.groupKeys), + groupFields: JSON.stringify(params.groupFields), + }); + const getRowsResponse = await fetchRows( + `https://mui.com/x/api/data-grid?${urlParams.toString()}`, + ); + return { + rows: getRowsResponse.rows, + rowCount: getRowsResponse.rowCount, + }; + }, + getGroupKey: (row) => row.group, + getChildrenCount: (row) => row.descendantCount, + }; + }, [fetchRows]); + + const initialState = useKeepGroupedColumnsHidden({ + apiRef, + initialState: { + rowGrouping: { + model: ['commodity', 'status'], + }, + columns: { + columnVisibilityModel: { + id: false, + }, + }, + }, + }); + + return ( +
+ + +
+ +
+
+ ); +} diff --git a/docs/data/data-grid/server-side-data/ServerSideRowGroupingGroupExpansion.js b/docs/data/data-grid/server-side-data/ServerSideRowGroupingGroupExpansion.js new file mode 100644 index 000000000000..bdb42747fc07 --- /dev/null +++ b/docs/data/data-grid/server-side-data/ServerSideRowGroupingGroupExpansion.js @@ -0,0 +1,70 @@ +import * as React from 'react'; +import { + DataGridPremium, + useGridApiRef, + useKeepGroupedColumnsHidden, +} from '@mui/x-data-grid-premium'; +import { useMockServer } from '@mui/x-data-grid-generator'; +import Button from '@mui/material/Button'; + +export default function ServerSideRowGroupingGroupExpansion() { + const apiRef = useGridApiRef(); + + const { fetchRows, columns } = useMockServer({ + rowGrouping: true, + }); + + const dataSource = React.useMemo(() => { + return { + getRows: async (params) => { + const urlParams = new URLSearchParams({ + paginationModel: JSON.stringify(params.paginationModel), + filterModel: JSON.stringify(params.filterModel), + sortModel: JSON.stringify(params.sortModel), + groupKeys: JSON.stringify(params.groupKeys), + groupFields: JSON.stringify(params.groupFields), + }); + const getRowsResponse = await fetchRows( + `https://mui.com/x/api/data-grid?${urlParams.toString()}`, + ); + return { + rows: getRowsResponse.rows, + rowCount: getRowsResponse.rowCount, + }; + }, + getGroupKey: (row) => row.group, + getChildrenCount: (row) => row.descendantCount, + }; + }, [fetchRows]); + + const initialState = useKeepGroupedColumnsHidden({ + apiRef, + initialState: { + rowGrouping: { + model: ['company'], + }, + }, + }); + + return ( +
+ + +
+ +
+
+ ); +} diff --git a/docs/data/data-grid/server-side-data/ServerSideRowGroupingGroupExpansion.tsx b/docs/data/data-grid/server-side-data/ServerSideRowGroupingGroupExpansion.tsx new file mode 100644 index 000000000000..64aa58a45d8e --- /dev/null +++ b/docs/data/data-grid/server-side-data/ServerSideRowGroupingGroupExpansion.tsx @@ -0,0 +1,71 @@ +import * as React from 'react'; +import { + DataGridPremium, + GridDataSource, + useGridApiRef, + useKeepGroupedColumnsHidden, +} from '@mui/x-data-grid-premium'; +import { useMockServer } from '@mui/x-data-grid-generator'; +import Button from '@mui/material/Button'; + +export default function ServerSideRowGroupingGroupExpansion() { + const apiRef = useGridApiRef(); + + const { fetchRows, columns } = useMockServer({ + rowGrouping: true, + }); + + const dataSource: GridDataSource = React.useMemo(() => { + return { + getRows: async (params) => { + const urlParams = new URLSearchParams({ + paginationModel: JSON.stringify(params.paginationModel), + filterModel: JSON.stringify(params.filterModel), + sortModel: JSON.stringify(params.sortModel), + groupKeys: JSON.stringify(params.groupKeys), + groupFields: JSON.stringify(params.groupFields), + }); + const getRowsResponse = await fetchRows( + `https://mui.com/x/api/data-grid?${urlParams.toString()}`, + ); + return { + rows: getRowsResponse.rows, + rowCount: getRowsResponse.rowCount, + }; + }, + getGroupKey: (row) => row.group, + getChildrenCount: (row) => row.descendantCount, + }; + }, [fetchRows]); + + const initialState = useKeepGroupedColumnsHidden({ + apiRef, + initialState: { + rowGrouping: { + model: ['company'], + }, + }, + }); + + return ( +
+ + +
+ +
+
+ ); +} diff --git a/docs/data/data-grid/server-side-data/row-grouping.md b/docs/data/data-grid/server-side-data/row-grouping.md index 537ef8ad0d54..25b6044f3f43 100644 --- a/docs/data/data-grid/server-side-data/row-grouping.md +++ b/docs/data/data-grid/server-side-data/row-grouping.md @@ -2,14 +2,90 @@ title: React Server-side row grouping --- -# Data Grid - Server-side row grouping [](/x/introduction/licensing/#pro-plan 'Pro plan')🚧 +# Data Grid - Server-side row grouping [](/x/introduction/licensing/#pro-plan 'Pro plan')

Lazy-loaded row grouping with server-side data source.

-:::warning -This feature isn't implemented yet. It's coming. +To dynamically load row grouping data from the server, including lazy-loading of children, create a data source and pass the `unstable_dataSource` prop to the Data Grid, as mentioned in the [overview](/x/react-data-grid/server-side-data/) section. + +:::info +If you are looking for row grouping on the client-side, see [client-side row grouping](/x/react-data-grid/row-grouping/). +::: + +Similar to the [tree data](/x/react-data-grid/server-side-data/tree-data/), you need to pass some additional properties to enable the data source row grouping feature: + +- `getGroupKey()`: Returns the group key for the row. +- `getChildrenCount()`: Returns the number of children for the row. If the children count is not available for some reason, but there are some children, returns `-1`. + +```tsx +const customDataSource: GridDataSource = { + getRows: async (params) => { + // Fetch the data from the server + }, + getGroupKey: (row) => { + // Return the group key for the row, e.g. `name` + return row.name; + }, + getChildrenCount: (row) => { + // Return the number of children for the row + return row.childrenCount; + }, +}; +``` -👍 Upvote [issue #10859](https://github.com/mui/mui-x/issues/10859) if you want to see it land faster. +In addition to `groupKeys`, the `getRows()` callback receives a `groupFields` parameter. This corresponds to the current `rowGroupingModel`. Use `groupFields` on the server to group the data for each `getRows()` call. -Don't hesitate to leave a comment on the same issue to influence what gets built. Especially if you already have a use case for this component, or if you are facing a pain point with your current solution. +```tsx +const getRows: async (params) => { + const urlParams = new URLSearchParams({ + // Example: JSON.stringify(['20th Century Fox', 'James Cameron']) + groupKeys: JSON.stringify(params.groupKeys), + // Example: JSON.stringify(['company', 'director']) + groupFields: JSON.stringify(params.groupFields), + }); + const getRowsResponse = await fetchRows( + // Server should group the data based on `groupFields` and + // extract the rows for the nested level based on `groupKeys` + `https://mui.com/x/api/data-grid?${urlParams.toString()}`, + ); + return { + rows: getRowsResponse.rows, + rowCount: getRowsResponse.rowCount, + }; +} +``` + +{{"demo": "ServerSideRowGroupingDataGrid.js", "bg": "inline"}} + +:::warning +For complex data, consider using `colDef.groupingValueGetter` to extract the grouping value. This value is passed in the `groupKeys` parameter when `getRows` is called. + +Ensure your backend can interpret the `groupKeys` parameter generated by `colDef.groupingValueGetter` to retrieve grouping values for child rows. ::: + +## Error handling + +If an error occurs during a `getRows` call, the Data Grid displays an error message in the row group cell. `unstable_onDataSourceError` is also triggered with the error and the fetch params. + +This example shows error handling with toast notifications and default error messages in grouping cells. Caching is disabled for simplicity. + +{{"demo": "ServerSideRowGroupingErrorHandling.js", "bg": "inline"}} + +## Group expansion + +The group expansion works similar to the [data source tree data](/x/react-data-grid/server-side-data/tree-data/#group-expansion). +The following demo uses `defaultGroupingExpansionDepth='-1'` to expand all the groups. + +{{"demo": "ServerSideRowGroupingGroupExpansion.js", "bg": "inline"}} + +## Demo + +In the following demo, use the auto generated data based on the `Commodities` dataset to simulate the server-side row grouping. + +{{"demo": "ServerSideRowGroupingFullDataGrid.js", "bg": "inline"}} + +## API + +- [DataGrid](/x/api/data-grid/data-grid/) +- [DataGridPro](/x/api/data-grid/data-grid-pro/) +- [DataGridPremium](/x/api/data-grid/data-grid-premium/) diff --git a/docs/data/data-grid/server-side-data/tree-data.md b/docs/data/data-grid/server-side-data/tree-data.md index 8e77fe0542bf..d5c725ac456e 100644 --- a/docs/data/data-grid/server-side-data/tree-data.md +++ b/docs/data/data-grid/server-side-data/tree-data.md @@ -8,8 +8,14 @@ title: React Server-side tree data To dynamically load tree data from the server, including lazy-loading of children, you must create a data source and pass the `unstable_dataSource` prop to the Data Grid, as detailed in the [overview section](/x/react-data-grid/server-side-data/). -The data source also requires some additional props to handle tree data, namely `getGroupKey` and `getChildrenCount`. -If the children count is not available for some reason, but there are some children, `getChildrenCount` should return `-1`. +:::info +If you are looking for tree data on the client-side, see [client-side tree data](/x/react-data-grid/tree-data/). +::: + +The data source also requires some additional props to handle tree data: + +- `getGroupKey()`: Returns the group key for the row. +- `getChildrenCount()`: Returns the number of children for the row. If the children count is not available for some reason, but there are some children, returns `-1`. ```tsx const customDataSource: GridDataSource = { @@ -27,6 +33,26 @@ const customDataSource: GridDataSource = { }; ``` +Like the other parameters such as `filterModel`, `sortModel`, and `paginationModel`, the `getRows()` callback receives a `groupKeys` parameter that corresponds to the keys provided for each nested level in `getGroupKey()`. +Use `groupKeys` on the server to extract the rows for a given nested level. + +```tsx +const getRows: async (params) => { + const urlParams = new URLSearchParams({ + // Example: JSON.stringify(['Billy Houston', 'Lora Dean']) + groupKeys: JSON.stringify(params.groupKeys), + }); + const getRowsResponse = await fetchRows( + // Server should extract the rows for the nested level based on `groupKeys` + `https://mui.com/x/api/data-grid?${urlParams.toString()}`, + ); + return { + rows: getRowsResponse.rows, + rowCount: getRowsResponse.rowCount, + }; +} +``` + The following tree data example supports filtering, sorting, and pagination on the server. It also caches the data by default. diff --git a/docs/data/pages.ts b/docs/data/pages.ts index b952b8a5b722..557be8daaba8 100644 --- a/docs/data/pages.ts +++ b/docs/data/pages.ts @@ -120,8 +120,9 @@ const pages: MuiPage[] = [ pathname: '/x/react-data-grid/server-side-data-group', title: 'Server-side data', plan: 'pro', + newFeature: true, children: [ - { pathname: '/x/react-data-grid/server-side-data', title: 'Overview' }, + { pathname: '/x/react-data-grid/server-side-data', title: 'Overview', plan: 'pro' }, { pathname: '/x/react-data-grid/server-side-data/tree-data', plan: 'pro' }, { pathname: '/x/react-data-grid/server-side-data/lazy-loading', @@ -135,8 +136,7 @@ const pages: MuiPage[] = [ }, { pathname: '/x/react-data-grid/server-side-data/row-grouping', - plan: 'pro', - planned: true, + plan: 'premium', }, { pathname: '/x/react-data-grid/server-side-data/aggregation', diff --git a/packages/x-data-grid-generator/src/hooks/index.ts b/packages/x-data-grid-generator/src/hooks/index.ts index 84dd7368aea4..223d6170c94b 100644 --- a/packages/x-data-grid-generator/src/hooks/index.ts +++ b/packages/x-data-grid-generator/src/hooks/index.ts @@ -1,6 +1,7 @@ export * from './useDemoData'; export * from './useBasicDemoData'; -export * from './useMovieData'; +export { useMovieData } from './useMovieData'; +export type { Movie } from './useMovieData'; export * from './useQuery'; export * from './useMockServer'; export { loadServerRows } from './serverUtils'; diff --git a/packages/x-data-grid-generator/src/hooks/serverUtils.ts b/packages/x-data-grid-generator/src/hooks/serverUtils.ts index 095893126643..80651b133af5 100644 --- a/packages/x-data-grid-generator/src/hooks/serverUtils.ts +++ b/packages/x-data-grid-generator/src/hooks/serverUtils.ts @@ -10,7 +10,6 @@ import { GridValidRowModel, } from '@mui/x-data-grid-pro'; import { GridStateColDef } from '@mui/x-data-grid-pro/internals'; -import { UseDemoDataOptions } from './useDemoData'; import { randomInt } from '../services/random-generator'; export interface FakeServerResponse { @@ -53,14 +52,9 @@ export interface ServerSideQueryOptions { sortModel?: GridSortModel; firstRowToRender?: number; lastRowToRender?: number; + groupFields?: string[]; } -export const DEFAULT_DATASET_OPTIONS: UseDemoDataOptions = { - dataSet: 'Commodity', - rowLength: 100, - maxColumns: 6, -}; - declare const DISABLE_CHANCE_RANDOM: any; export const disableDelay = typeof DISABLE_CHANCE_RANDOM !== 'undefined' && DISABLE_CHANCE_RANDOM; @@ -323,7 +317,7 @@ export const loadServerRows = ( }); }; -interface ProcessTreeDataRowsResponse { +interface NestedDataRowsResponse { rows: GridRowModel[]; rootRowCount: number; } @@ -333,6 +327,7 @@ const findTreeDataRowChildren = ( parentPath: string[], pathKey: string = 'path', depth: number = 1, // the depth of the children to find relative to parentDepth, `-1` to find all + rowQualifier?: (row: GridRowModel) => boolean, ) => { const parentDepth = parentPath.length; const children = []; @@ -346,7 +341,9 @@ const findTreeDataRowChildren = ( ((depth < 0 && rowPath.length > parentDepth) || rowPath.length === parentDepth + depth) && parentPath.every((value, index) => value === rowPath[index]) ) { - children.push(row); + if (!rowQualifier || rowQualifier(row)) { + children.push(row); + } } } return children; @@ -427,14 +424,14 @@ const getTreeDataFilteredRows: GetTreeDataFilteredRows = ( }; /** - * Simulates server data loading + * Simulates server data for tree-data feature */ export const processTreeDataRows = ( rows: GridRowModel[], queryOptions: ServerSideQueryOptions, serverOptions: ServerOptions, columnsWithDefaultColDef: GridColDef[], -): Promise => { +): Promise => { const { minDelay = 100, maxDelay = 300 } = serverOptions; const pathKey = 'path'; // TODO: Support filtering and cursor based pagination @@ -490,3 +487,124 @@ export const processTreeDataRows = ( }, delay); // simulate network latency }); }; + +/** + * Simulates server data for row grouping feature + */ +export const processRowGroupingRows = ( + rows: GridValidRowModel[], + queryOptions: ServerSideQueryOptions, + serverOptions: ServerOptions, + columnsWithDefaultColDef: GridColDef[], +): Promise => { + const { minDelay = 100, maxDelay = 300 } = serverOptions; + const pathKey = 'path'; + + if (maxDelay < minDelay) { + throw new Error('serverOptions.minDelay is larger than serverOptions.maxDelay '); + } + + if (queryOptions.groupKeys == null) { + throw new Error('serverOptions.groupKeys must be defined to compute row grouping data'); + } + + if (queryOptions.groupFields == null) { + throw new Error('serverOptions.groupFields must be defined to compute row grouping data'); + } + + const delay = randomInt(minDelay, maxDelay); + + const pathsToAutogenerate = new Set(); + let rowsWithPaths = rows; + const rowsWithMissingGroups: GridValidRowModel[] = []; + + // add paths and generate parent rows based on `groupFields` + const groupFields = queryOptions.groupFields; + if (groupFields.length > 0) { + rowsWithPaths = rows.reduce((acc, row) => { + const partialPath = groupFields.map((field) => String(row[field])); + for (let index = 0; index < partialPath.length; index += 1) { + const value = partialPath[index]; + if (value === undefined) { + if (index === 0) { + rowsWithMissingGroups.push({ ...row, group: false }); + } + return acc; + } + const parentPath = partialPath.slice(0, index + 1); + const strigifiedPath = parentPath.join(','); + if (!pathsToAutogenerate.has(strigifiedPath)) { + pathsToAutogenerate.add(strigifiedPath); + } + } + acc.push({ ...row, path: [...partialPath, ''] }); + return acc; + }, []); + } else { + rowsWithPaths = rows.map((row) => ({ ...row, path: [''] })); + } + + const autogeneratedRows = Array.from(pathsToAutogenerate).map((path) => { + const pathArray = path.split(','); + return { + id: `auto-generated-parent-${pathArray.join('-')}`, + path: pathArray.slice(0, pathArray.length), + group: pathArray.slice(-1)[0], + }; + }); + + // apply plain filtering + const filteredRows = getTreeDataFilteredRows( + [...autogeneratedRows, ...rowsWithPaths, ...rowsWithMissingGroups], + queryOptions.filterModel, + columnsWithDefaultColDef, + ) as GridValidRowModel[]; + + // get root row count + const rootRows = findTreeDataRowChildren(filteredRows, []); + const rootRowCount = rootRows.length; + + let filteredRowsWithMissingGroups: GridValidRowModel[] = []; + let childRows = rootRows; + if (queryOptions.groupKeys.length === 0) { + filteredRowsWithMissingGroups = filteredRows.filter(({ group }) => group === false); + } else { + childRows = findTreeDataRowChildren(filteredRows, queryOptions.groupKeys); + } + + let childRowsWithDescendantCounts = childRows.map((row) => { + const descendants = findTreeDataRowChildren( + filteredRows, + row[pathKey], + pathKey, + -1, + ({ id }) => typeof id !== 'string' || !id.startsWith('auto-generated-parent-'), + ); + const descendantCount = descendants.length; + return { ...row, descendantCount } as GridRowModel; + }); + + if (queryOptions.sortModel) { + const rowComparator = getRowComparator(queryOptions.sortModel, columnsWithDefaultColDef); + const sortedMissingGroups = [...filteredRowsWithMissingGroups].sort(rowComparator); + const sortedChildRows = [...childRowsWithDescendantCounts].sort(rowComparator); + childRowsWithDescendantCounts = [...sortedMissingGroups, ...sortedChildRows]; + } + + if (queryOptions.paginationModel && queryOptions.groupKeys.length === 0) { + // Only paginate root rows, grid should refetch root rows when `paginationModel` updates + const { pageSize, page } = queryOptions.paginationModel; + if (pageSize < childRowsWithDescendantCounts.length) { + childRowsWithDescendantCounts = childRowsWithDescendantCounts.slice( + page * pageSize, + (page + 1) * pageSize, + ); + } + } + + return new Promise((resolve) => { + setTimeout(() => { + resolve({ rows: childRowsWithDescendantCounts, rootRowCount }); + }, delay); // simulate network latency + }); +}; diff --git a/packages/x-data-grid-generator/src/hooks/useMockServer.ts b/packages/x-data-grid-generator/src/hooks/useMockServer.ts index 83c43fd30c05..e9c168f0ca08 100644 --- a/packages/x-data-grid-generator/src/hooks/useMockServer.ts +++ b/packages/x-data-grid-generator/src/hooks/useMockServer.ts @@ -9,23 +9,24 @@ import { GridInitialState, GridColumnVisibilityModel, } from '@mui/x-data-grid-pro'; -import { - UseDemoDataOptions, - getColumnsFromOptions, - extrapolateSeed, - deepFreeze, -} from './useDemoData'; +import { extrapolateSeed, deepFreeze } from './useDemoData'; +import { getCommodityColumns } from '../columns/commodities.columns'; +import { getEmployeeColumns } from '../columns/employees.columns'; import { GridColDefGenerator } from '../services/gridColDefGenerator'; import { getRealGridData, GridDemoData } from '../services/real-data-service'; -import { addTreeDataOptionsToDemoData } from '../services/tree-data-generator'; +import { + addTreeDataOptionsToDemoData, + AddPathToDemoDataOptions, +} from '../services/tree-data-generator'; import { loadServerRows, processTreeDataRows, - DEFAULT_DATASET_OPTIONS, + processRowGroupingRows, DEFAULT_SERVER_OPTIONS, } from './serverUtils'; import type { ServerOptions } from './serverUtils'; import { randomInt } from '../services'; +import { getMovieRows, getMovieColumns } from './useMovieData'; const dataCache = new LRUCache({ max: 10, @@ -43,6 +44,66 @@ type UseMockServerResponse = { loadNewData: () => void; }; +type DataSet = 'Commodity' | 'Employee' | 'Movies'; + +interface UseMockServerOptions { + dataSet: DataSet; + /** + * Has no effect when DataSet='Movies' + */ + rowLength: number; + maxColumns?: number; + visibleFields?: string[]; + editable?: boolean; + treeData?: AddPathToDemoDataOptions; + rowGrouping?: boolean; +} + +interface GridMockServerData { + rows: GridRowModel[]; + columns: GridColDefGenerator[] | GridColDef[]; + initialState?: GridInitialState; +} + +interface ColumnsOptions + extends Pick {} + +const GET_DEFAULT_DATASET_OPTIONS: (isRowGrouping: boolean) => UseMockServerOptions = ( + isRowGrouping, +) => ({ + dataSet: isRowGrouping ? 'Movies' : 'Commodity', + rowLength: isRowGrouping ? getMovieRows().length : 100, + maxColumns: 6, +}); + +const getColumnsFromOptions = (options: ColumnsOptions): GridColDefGenerator[] | GridColDef[] => { + let columns; + + switch (options.dataSet) { + case 'Commodity': + columns = getCommodityColumns(options.editable); + break; + case 'Employee': + columns = getEmployeeColumns(); + break; + case 'Movies': + columns = getMovieColumns(); + break; + default: + throw new Error('Unknown dataset'); + } + + if (options.visibleFields) { + columns = columns.map((col) => + options.visibleFields?.includes(col.field) ? col : { ...col, hide: true }, + ); + } + if (options.maxColumns) { + columns = columns.slice(0, options.maxColumns); + } + return columns; +}; + function decodeParams(url: string): GridGetRowsParams { const params = new URL(url).searchParams; const decodedParams = {} as any; @@ -76,12 +137,18 @@ const getInitialState = (columns: GridColDefGenerator[], groupingField?: string) const defaultColDef = getGridDefaultColumnTypes(); +function sendEmptyResponse() { + return new Promise((resolve) => { + resolve({ rows: [], rowCount: 0 }); + }); +} + export const useMockServer = ( - dataSetOptions?: Partial, + dataSetOptions?: Partial, serverOptions?: ServerOptions & { verbose?: boolean }, shouldRequestsFail?: boolean, ): UseMockServerResponse => { - const [data, setData] = React.useState(); + const [data, setData] = React.useState(); const [index, setIndex] = React.useState(0); const shouldRequestsFailRef = React.useRef(shouldRequestsFail ?? false); @@ -91,7 +158,11 @@ export const useMockServer = ( } }, [shouldRequestsFail]); - const options = { ...DEFAULT_DATASET_OPTIONS, ...dataSetOptions }; + const isRowGrouping = dataSetOptions?.rowGrouping ?? false; + + const options = { ...GET_DEFAULT_DATASET_OPTIONS(isRowGrouping), ...dataSetOptions }; + + const isTreeData = options.treeData?.groupingField != null; const columns = React.useMemo(() => { return getColumnsFromOptions({ @@ -116,8 +187,6 @@ export const useMockServer = ( [columns], ); - const isTreeData = options.treeData?.groupingField != null; - const getGroupKey = React.useMemo(() => { if (isTreeData) { return (row: GridRowModel): string => row[options.treeData!.groupingField!]; @@ -144,6 +213,13 @@ export const useMockServer = ( return undefined; } + if (options.dataSet === 'Movies') { + const rowsData = { rows: getMovieRows(), columns }; + setData(rowsData); + dataCache.set(cacheKey, rowsData); + return undefined; + } + let active = true; (async () => { @@ -193,10 +269,8 @@ export const useMockServer = ( const fetchRows = React.useCallback( async (requestUrl: string): Promise => { - if (!data || !requestUrl) { - return new Promise((resolve) => { - resolve({ rows: [], rowCount: 0 }); - }); + if (!requestUrl || !data?.rows) { + return sendEmptyResponse(); } const params = decodeParams(requestUrl); const verbose = serverOptions?.verbose ?? true; @@ -224,9 +298,21 @@ export const useMockServer = ( }); } - if (isTreeData /* || TODO: `isRowGrouping` */) { + if (isTreeData) { const { rows, rootRowCount } = await processTreeDataRows( - data.rows, + data?.rows ?? [], + params, + serverOptionsWithDefault, + columnsWithDefaultColDef, + ); + + getRowsResponse = { + rows: rows.slice().map((row) => ({ ...row, path: undefined })), + rowCount: rootRowCount, + }; + } else if (isRowGrouping) { + const { rows, rootRowCount } = await processRowGroupingRows( + data?.rows ?? [], params, serverOptionsWithDefault, columnsWithDefaultColDef, @@ -237,9 +323,8 @@ export const useMockServer = ( rowCount: rootRowCount, }; } else { - // plain data const { returnedRows, nextCursor, totalRowCount } = await loadServerRows( - data.rows, + data?.rows ?? [], { ...params, ...params.paginationModel }, serverOptionsWithDefault, columnsWithDefaultColDef, @@ -262,12 +347,13 @@ export const useMockServer = ( serverOptions?.useCursorPagination, isTreeData, columnsWithDefaultColDef, + isRowGrouping, ], ); return { columns: columnsWithDefaultColDef, - initialState, + initialState: options.dataSet === 'Movies' ? {} : initialState, getGroupKey, getChildrenCount, fetchRows, diff --git a/packages/x-data-grid-generator/src/hooks/useMovieData.ts b/packages/x-data-grid-generator/src/hooks/useMovieData.ts index 7820a7471fde..b9b196716839 100644 --- a/packages/x-data-grid-generator/src/hooks/useMovieData.ts +++ b/packages/x-data-grid-generator/src/hooks/useMovieData.ts @@ -546,6 +546,9 @@ const ROWS: GridRowModel[] = [ }, ]; +export const getMovieColumns = (): GridColDef[] => COLUMNS; +export const getMovieRows = (): GridRowModel[] => ROWS; + export const useMovieData = () => { return { rows: ROWS, diff --git a/packages/x-data-grid-generator/src/hooks/useQuery.ts b/packages/x-data-grid-generator/src/hooks/useQuery.ts index 62ae140bcdcd..5387a7e1f485 100644 --- a/packages/x-data-grid-generator/src/hooks/useQuery.ts +++ b/packages/x-data-grid-generator/src/hooks/useQuery.ts @@ -7,9 +7,15 @@ import { getColumnsFromOptions, getInitialState, } from './useDemoData'; -import { DEFAULT_DATASET_OPTIONS, DEFAULT_SERVER_OPTIONS, loadServerRows } from './serverUtils'; +import { DEFAULT_SERVER_OPTIONS, loadServerRows } from './serverUtils'; import type { ServerOptions, QueryOptions, PageInfo } from './serverUtils'; +const DEFAULT_DATASET_OPTIONS: UseDemoDataOptions = { + dataSet: 'Commodity', + rowLength: 100, + maxColumns: 6, +}; + export const createFakeServer = ( dataSetOptions?: Partial, serverOptions?: ServerOptions, diff --git a/packages/x-data-grid-premium/src/DataGridPremium/useDataGridPremiumComponent.tsx b/packages/x-data-grid-premium/src/DataGridPremium/useDataGridPremiumComponent.tsx index 8106dec99163..46c76914dda7 100644 --- a/packages/x-data-grid-premium/src/DataGridPremium/useDataGridPremiumComponent.tsx +++ b/packages/x-data-grid-premium/src/DataGridPremium/useDataGridPremiumComponent.tsx @@ -86,6 +86,7 @@ import { rowGroupingStateInitializer, } from '../hooks/features/rowGrouping/useGridRowGrouping'; import { useGridRowGroupingPreProcessors } from '../hooks/features/rowGrouping/useGridRowGroupingPreProcessors'; +import { useGridDataSourceRowGroupingPreProcessors } from '../hooks/features/rowGrouping/useGridDataSourceRowGroupingPreProcessors'; import { useGridExcelExport } from '../hooks/features/export/useGridExcelExport'; import { cellSelectionStateInitializer, @@ -105,6 +106,7 @@ export const useDataGridPremiumComponent = ( useGridRowSelectionPreProcessors(apiRef, props); useGridRowReorderPreProcessors(apiRef, props); useGridRowGroupingPreProcessors(apiRef, props); + useGridDataSourceRowGroupingPreProcessors(apiRef, props); useGridTreeDataPreProcessors(apiRef, props); useGridDataSourceTreeDataPreProcessors(apiRef, props); useGridLazyLoaderPreProcessors(apiRef, props); diff --git a/packages/x-data-grid-premium/src/components/GridDataSourceGroupingCriteriaCell.tsx b/packages/x-data-grid-premium/src/components/GridDataSourceGroupingCriteriaCell.tsx new file mode 100644 index 000000000000..0f7f159ee84c --- /dev/null +++ b/packages/x-data-grid-premium/src/components/GridDataSourceGroupingCriteriaCell.tsx @@ -0,0 +1,147 @@ +import * as React from 'react'; +import { unstable_composeClasses as composeClasses } from '@mui/utils'; +import Box from '@mui/material/Box'; +import CircularProgress from '@mui/material/CircularProgress'; +import { useGridPrivateApiContext } from '@mui/x-data-grid-pro/internals'; +import { + useGridSelector, + getDataGridUtilityClass, + GridRenderCellParams, + GridGroupNode, +} from '@mui/x-data-grid-pro'; +import { useGridApiContext } from '../hooks/utils/useGridApiContext'; +import { useGridRootProps } from '../hooks/utils/useGridRootProps'; +import { DataGridPremiumProcessedProps } from '../models/dataGridPremiumProps'; +import { GridPrivateApiPremium } from '../models/gridApiPremium'; +import { GridStatePremium } from '../models/gridStatePremium'; + +type OwnerState = DataGridPremiumProcessedProps; + +const useUtilityClasses = (ownerState: OwnerState) => { + const { classes } = ownerState; + + const slots = { + root: ['groupingCriteriaCell'], + toggle: ['groupingCriteriaCellToggle'], + loadingContainer: ['groupingCriteriaCellLoadingContainer'], + }; + + return composeClasses(slots, getDataGridUtilityClass, classes); +}; + +interface GridGroupingCriteriaCellProps extends GridRenderCellParams { + hideDescendantCount?: boolean; +} + +interface GridGroupingCriteriaCellIconProps + extends Pick { + descendantCount: number; +} + +function GridGroupingCriteriaCellIcon(props: GridGroupingCriteriaCellIconProps) { + const apiRef = useGridPrivateApiContext() as React.MutableRefObject; + const rootProps = useGridRootProps(); + const classes = useUtilityClasses(rootProps); + const { rowNode, id, field, descendantCount } = props; + + const loadingSelector = (state: GridStatePremium) => state.dataSource.loading[id] ?? false; + const errorSelector = (state: GridStatePremium) => state.dataSource.errors[id]; + const isDataLoading = useGridSelector(apiRef, loadingSelector); + const error = useGridSelector(apiRef, errorSelector); + + const handleClick = (event: React.MouseEvent) => { + if (!rowNode.childrenExpanded) { + // always fetch/get from cache the children when the node is expanded + apiRef.current.unstable_dataSource.fetchRows(id); + } else { + apiRef.current.setRowChildrenExpansion(id, !rowNode.childrenExpanded); + } + apiRef.current.setCellFocus(id, field); + event.stopPropagation(); + }; + + const Icon = rowNode.childrenExpanded + ? rootProps.slots.groupingCriteriaCollapseIcon + : rootProps.slots.groupingCriteriaExpandIcon; + + if (isDataLoading) { + return ( +
+ +
+ ); + } + + return descendantCount > 0 ? ( + + + + + + + + ) : null; +} + +export function GridDataSourceGroupingCriteriaCell(props: GridGroupingCriteriaCellProps) { + const { id, field, rowNode, hideDescendantCount, formattedValue } = props; + + const rootProps = useGridRootProps(); + const apiRef = useGridApiContext(); + const rowSelector = (state: GridStatePremium) => state.rows.dataRowIdToModelLookup[id]; + const row = useGridSelector(apiRef, rowSelector); + const classes = useUtilityClasses(rootProps); + + let descendantCount = 0; + if (row) { + descendantCount = Math.max(rootProps.unstable_dataSource?.getChildrenCount?.(row) ?? 0, 0); + } + + let cellContent: React.ReactNode; + + const colDef = apiRef.current.getColumn(rowNode.groupingField!); + if (typeof colDef?.renderCell === 'function') { + cellContent = colDef.renderCell(props); + } else if (typeof formattedValue !== 'undefined') { + cellContent = {formattedValue}; + } else { + cellContent = {rowNode.groupingKey}; + } + + return ( + + `calc(var(--DataGrid-cellOffsetMultiplier) * ${theme.spacing(rowNode.depth)})`, + }} + > +
+ +
+ {cellContent} + {!hideDescendantCount && descendantCount > 0 ? ( + ({descendantCount}) + ) : null} +
+ ); +} diff --git a/packages/x-data-grid-premium/src/hooks/features/rowGrouping/createGroupingColDef.tsx b/packages/x-data-grid-premium/src/hooks/features/rowGrouping/createGroupingColDef.tsx index caa1a42af8f5..194c62efa521 100644 --- a/packages/x-data-grid-premium/src/hooks/features/rowGrouping/createGroupingColDef.tsx +++ b/packages/x-data-grid-premium/src/hooks/features/rowGrouping/createGroupingColDef.tsx @@ -12,10 +12,12 @@ import { GridColumnRawLookup, isSingleSelectColDef } from '@mui/x-data-grid-pro/ import { GridApiPremium } from '../../../models/gridApiPremium'; import { GridGroupingColumnFooterCell } from '../../../components/GridGroupingColumnFooterCell'; import { GridGroupingCriteriaCell } from '../../../components/GridGroupingCriteriaCell'; +import { GridDataSourceGroupingCriteriaCell } from '../../../components/GridDataSourceGroupingCriteriaCell'; import { GridGroupingColumnLeafCell } from '../../../components/GridGroupingColumnLeafCell'; import { getRowGroupingFieldFromGroupingCriteria, GRID_ROW_GROUPING_SINGLE_GROUPING_FIELD, + RowGroupingStrategy, } from './gridRowGroupingUtils'; import { gridRowGroupingSanitizedModelSelector } from './gridRowGroupingSelector'; @@ -25,11 +27,24 @@ const GROUPING_COL_DEF_DEFAULT_PROPERTIES: Omit = { disableReorder: true, }; -const GROUPING_COL_DEF_FORCED_PROPERTIES: Pick = { +const GROUPING_COL_DEF_FORCED_PROPERTIES_DEFAULT: Pick< + GridColDef, + 'type' | 'editable' | 'groupable' +> = { editable: false, groupable: false, }; +const GROUPING_COL_DEF_FORCED_PROPERTIES_DATA_SOURCE: Pick< + GridColDef, + 'type' | 'editable' | 'groupable' | 'filterable' | 'sortable' | 'aggregable' +> = { + ...GROUPING_COL_DEF_FORCED_PROPERTIES_DEFAULT, + // TODO: Support these features on the grouping column(s) + filterable: false, + sortable: false, +}; + /** * When sorting two cells with different grouping criteria, we consider that the cell with the grouping criteria coming first in the model should be displayed below. * This can occur when some rows don't have all the fields. In which case we want the rows with the missing field to be displayed above. @@ -122,6 +137,7 @@ interface CreateGroupingColDefMonoCriteriaParams { * This value comes `prop.groupingColDef`. */ colDefOverride: GridGroupingColDefOverride | null | undefined; + strategy?: RowGroupingStrategy; } /** @@ -132,11 +148,17 @@ export const createGroupingColDefForOneGroupingCriteria = ({ groupedByColDef, groupingCriteria, colDefOverride, + strategy = RowGroupingStrategy.Default, }: CreateGroupingColDefMonoCriteriaParams): GridColDef => { const { leafField, mainGroupingCriteria, hideDescendantCount, ...colDefOverrideProperties } = colDefOverride ?? {}; const leafColDef = leafField ? columnsLookup[leafField] : null; + const CriteriaCell = + strategy === RowGroupingStrategy.Default + ? GridGroupingCriteriaCell + : GridDataSourceGroupingCriteriaCell; + // The properties that do not depend on the presence of a `leafColDef` and that can be overridden by `colDefOverride` const commonProperties: Partial = { width: Math.max( @@ -170,7 +192,7 @@ export const createGroupingColDefForOneGroupingCriteria = ({ // Render current grouping criteria groups if (params.rowNode.groupingField === groupingCriteria) { return ( - )} hideDescendantCount={hideDescendantCount} /> @@ -222,7 +244,7 @@ export const createGroupingColDefForOneGroupingCriteria = ({ // The properties that can't be overridden with `colDefOverride` const forcedProperties: Pick = { field: getRowGroupingFieldFromGroupingCriteria(groupingCriteria), - ...GROUPING_COL_DEF_FORCED_PROPERTIES, + ...GROUPING_COL_DEF_FORCED_PROPERTIES_DEFAULT, }; return { @@ -246,6 +268,7 @@ interface CreateGroupingColDefSeveralCriteriaParams { * This value comes `prop.groupingColDef`. */ colDefOverride: GridGroupingColDefOverride | null | undefined; + strategy?: RowGroupingStrategy; } /** @@ -256,11 +279,17 @@ export const createGroupingColDefForAllGroupingCriteria = ({ columnsLookup, rowGroupingModel, colDefOverride, + strategy = RowGroupingStrategy.Default, }: CreateGroupingColDefSeveralCriteriaParams): GridColDef => { const { leafField, mainGroupingCriteria, hideDescendantCount, ...colDefOverrideProperties } = colDefOverride ?? {}; const leafColDef = leafField ? columnsLookup[leafField] : null; + const CriteriaCell = + strategy === RowGroupingStrategy.Default + ? GridGroupingCriteriaCell + : GridDataSourceGroupingCriteriaCell; + // The properties that do not depend on the presence of a `leafColDef` and that can be overridden by `colDefOverride` const commonProperties: Partial = { headerName: apiRef.current.getLocaleText('groupingColumnHeaderName'), @@ -296,7 +325,7 @@ export const createGroupingColDefForAllGroupingCriteria = ({ // Render the groups return ( - )} hideDescendantCount={hideDescendantCount} /> @@ -344,7 +373,9 @@ export const createGroupingColDefForAllGroupingCriteria = ({ // The properties that can't be overridden with `colDefOverride` const forcedProperties: Pick = { field: GRID_ROW_GROUPING_SINGLE_GROUPING_FIELD, - ...GROUPING_COL_DEF_FORCED_PROPERTIES, + ...(strategy === RowGroupingStrategy.Default + ? GROUPING_COL_DEF_FORCED_PROPERTIES_DEFAULT + : GROUPING_COL_DEF_FORCED_PROPERTIES_DATA_SOURCE), }; return { diff --git a/packages/x-data-grid-premium/src/hooks/features/rowGrouping/gridRowGroupingUtils.ts b/packages/x-data-grid-premium/src/hooks/features/rowGrouping/gridRowGroupingUtils.ts index 1cdec6a7c663..9d1e3ddcafae 100644 --- a/packages/x-data-grid-premium/src/hooks/features/rowGrouping/gridRowGroupingUtils.ts +++ b/packages/x-data-grid-premium/src/hooks/features/rowGrouping/gridRowGroupingUtils.ts @@ -9,6 +9,7 @@ import { GridRowModel, GridColDef, GridKeyValue, + GridDataSource, } from '@mui/x-data-grid-pro'; import { passFilterLogic, @@ -28,7 +29,10 @@ import { GridPrivateApiPremium } from '../../../models/gridApiPremium'; export const GRID_ROW_GROUPING_SINGLE_GROUPING_FIELD = '__row_group_by_columns_group__'; -export const ROW_GROUPING_STRATEGY = 'grouping-columns'; +export enum RowGroupingStrategy { + Default = 'grouping-columns', + DataSource = 'grouping-columns-data-source', +} export const getRowGroupingFieldFromGroupingCriteria = (groupingCriteria: string | null) => { if (groupingCriteria === null) { @@ -178,10 +182,11 @@ export const filterRowTreeFromGroupingColumns = ( export const getColDefOverrides = ( groupingColDefProp: DataGridPremiumProcessedProps['groupingColDef'], fields: string[], + strategy?: RowGroupingStrategy, ) => { if (typeof groupingColDefProp === 'function') { return groupingColDefProp({ - groupingName: ROW_GROUPING_STRATEGY, + groupingName: strategy ?? RowGroupingStrategy.Default, fields, }); } @@ -199,6 +204,7 @@ export const mergeStateWithRowGroupingModel = export const setStrategyAvailability = ( privateApiRef: React.MutableRefObject, disableRowGrouping: boolean, + dataSource?: GridDataSource, ) => { let isAvailable: () => boolean; if (disableRowGrouping) { @@ -210,7 +216,9 @@ export const setStrategyAvailability = ( }; } - privateApiRef.current.setStrategyAvailability('rowTree', ROW_GROUPING_STRATEGY, isAvailable); + const strategy = dataSource ? RowGroupingStrategy.DataSource : RowGroupingStrategy.Default; + + privateApiRef.current.setStrategyAvailability('rowTree', strategy, isAvailable); }; export const getCellGroupingCriteria = ({ diff --git a/packages/x-data-grid-premium/src/hooks/features/rowGrouping/useGridDataSourceRowGroupingPreProcessors.ts b/packages/x-data-grid-premium/src/hooks/features/rowGrouping/useGridDataSourceRowGroupingPreProcessors.ts new file mode 100644 index 000000000000..07ad4690d059 --- /dev/null +++ b/packages/x-data-grid-premium/src/hooks/features/rowGrouping/useGridDataSourceRowGroupingPreProcessors.ts @@ -0,0 +1,134 @@ +import * as React from 'react'; +import { GridRowId, gridRowTreeSelector, gridColumnLookupSelector } from '@mui/x-data-grid-pro'; +import { + GridStrategyProcessor, + useGridRegisterStrategyProcessor, + createRowTree, + updateRowTree, + getVisibleRowsLookup, + skipSorting, + skipFiltering, + GridRowsPartialUpdates, +} from '@mui/x-data-grid-pro/internals'; +import { DataGridPremiumProcessedProps } from '../../../models/dataGridPremiumProps'; +import { getGroupingRules, RowGroupingStrategy } from './gridRowGroupingUtils'; +import { GridPrivateApiPremium } from '../../../models/gridApiPremium'; +import { gridRowGroupingSanitizedModelSelector } from './gridRowGroupingSelector'; + +export const useGridDataSourceRowGroupingPreProcessors = ( + apiRef: React.MutableRefObject, + props: Pick< + DataGridPremiumProcessedProps, + | 'disableRowGrouping' + | 'groupingColDef' + | 'rowGroupingColumnMode' + | 'defaultGroupingExpansionDepth' + | 'isGroupExpandedByDefault' + | 'unstable_dataSource' + >, +) => { + const createRowTreeForRowGrouping = React.useCallback>( + (params) => { + const getGroupKey = props.unstable_dataSource?.getGroupKey; + if (!getGroupKey) { + throw new Error('MUI X: No `getGroupKey` method provided with the dataSource.'); + } + + const getChildrenCount = props.unstable_dataSource?.getChildrenCount; + if (!getChildrenCount) { + throw new Error('MUI X: No `getChildrenCount` method provided with the dataSource.'); + } + + const sanitizedRowGroupingModel = gridRowGroupingSanitizedModelSelector(apiRef); + const columnsLookup = gridColumnLookupSelector(apiRef); + const groupingRules = getGroupingRules({ + sanitizedRowGroupingModel, + columnsLookup, + }); + apiRef.current.caches.rowGrouping.rulesOnLastRowTreeCreation = groupingRules; + + const getRowTreeBuilderNode = (rowId: GridRowId) => { + const parentPath = (params.updates as GridRowsPartialUpdates).groupKeys ?? []; + const row = params.dataRowIdToModelLookup[rowId]; + const groupingRule = groupingRules[parentPath.length]; + const groupingValueGetter = groupingRule?.groupingValueGetter; + const leafKey = + groupingValueGetter?.( + row[groupingRule.field] as never, + row, + columnsLookup[groupingRule.field], + apiRef, + ) ?? getGroupKey(params.dataRowIdToModelLookup[rowId]); + return { + id: rowId, + path: [...parentPath, leafKey ?? rowId.toString()].map((key, i) => ({ + key, + field: groupingRules[i]?.field ?? null, + })), + serverChildrenCount: getChildrenCount(params.dataRowIdToModelLookup[rowId]) ?? 0, + }; + }; + + if (params.updates.type === 'full') { + return createRowTree({ + previousTree: params.previousTree, + nodes: params.updates.rows.map(getRowTreeBuilderNode), + defaultGroupingExpansionDepth: props.defaultGroupingExpansionDepth, + isGroupExpandedByDefault: props.isGroupExpandedByDefault, + groupingName: RowGroupingStrategy.DataSource, + }); + } + + return updateRowTree({ + nodes: { + inserted: (params.updates as GridRowsPartialUpdates).actions.insert.map( + getRowTreeBuilderNode, + ), + modified: (params.updates as GridRowsPartialUpdates).actions.modify.map( + getRowTreeBuilderNode, + ), + removed: (params.updates as GridRowsPartialUpdates).actions.remove, + }, + previousTree: params.previousTree!, + previousGroupsToFetch: params.previousGroupsToFetch, + previousTreeDepth: params.previousTreeDepths!, + defaultGroupingExpansionDepth: props.defaultGroupingExpansionDepth, + isGroupExpandedByDefault: props.isGroupExpandedByDefault, + groupingName: RowGroupingStrategy.DataSource, + }); + }, + [ + apiRef, + props.unstable_dataSource, + props.defaultGroupingExpansionDepth, + props.isGroupExpandedByDefault, + ], + ); + + const filterRows = React.useCallback>(() => { + const rowTree = gridRowTreeSelector(apiRef); + + return skipFiltering(rowTree); + }, [apiRef]); + + const sortRows = React.useCallback>(() => { + const rowTree = gridRowTreeSelector(apiRef); + + return skipSorting(rowTree); + }, [apiRef]); + + useGridRegisterStrategyProcessor( + apiRef, + RowGroupingStrategy.DataSource, + 'rowTreeCreation', + createRowTreeForRowGrouping, + ); + useGridRegisterStrategyProcessor(apiRef, RowGroupingStrategy.DataSource, 'filtering', filterRows); + useGridRegisterStrategyProcessor(apiRef, RowGroupingStrategy.DataSource, 'sorting', sortRows); + useGridRegisterStrategyProcessor( + apiRef, + RowGroupingStrategy.DataSource, + 'visibleRowsLookupCreation', + getVisibleRowsLookup, + ); +}; diff --git a/packages/x-data-grid-premium/src/hooks/features/rowGrouping/useGridRowGrouping.tsx b/packages/x-data-grid-premium/src/hooks/features/rowGrouping/useGridRowGrouping.tsx index b779e23a2064..24bff5926a7b 100644 --- a/packages/x-data-grid-premium/src/hooks/features/rowGrouping/useGridRowGrouping.tsx +++ b/packages/x-data-grid-premium/src/hooks/features/rowGrouping/useGridRowGrouping.tsx @@ -19,7 +19,7 @@ import { import { DataGridPremiumProcessedProps } from '../../../models/dataGridPremiumProps'; import { getRowGroupingFieldFromGroupingCriteria, - ROW_GROUPING_STRATEGY, + RowGroupingStrategy, isGroupingColumn, mergeStateWithRowGroupingModel, setStrategyAvailability, @@ -63,6 +63,7 @@ export const useGridRowGrouping = ( | 'disableRowGrouping' | 'slotProps' | 'slots' + | 'unstable_dataSource' >, ) => { apiRef.current.registerControlState({ @@ -73,7 +74,7 @@ export const useGridRowGrouping = ( changeEvent: 'rowGroupingModelChange', }); - /** + /* * API METHODS */ const setRowGroupingModel = React.useCallback( @@ -165,6 +166,16 @@ export const useGridRowGrouping = ( [props.disableRowGrouping], ); + const addGetRowsParams = React.useCallback>( + (params) => { + return { + ...params, + groupFields: gridRowGroupingModelSelector(apiRef), + }; + }, + [apiRef], + ); + const stateExportPreProcessing = React.useCallback>( (prevState, context) => { const rowGroupingModelToExport = gridRowGroupingModelSelector(apiRef); @@ -209,10 +220,11 @@ export const useGridRowGrouping = ( ); useGridRegisterPipeProcessor(apiRef, 'columnMenu', addColumnMenuButtons); + useGridRegisterPipeProcessor(apiRef, 'getRowsParams', addGetRowsParams); useGridRegisterPipeProcessor(apiRef, 'exportState', stateExportPreProcessing); useGridRegisterPipeProcessor(apiRef, 'restoreState', stateRestorePreProcessing); - /** + /* * EVENTS */ const handleCellKeyDown = React.useCallback>( @@ -233,10 +245,15 @@ export const useGridRowGrouping = ( return; } + if (props.unstable_dataSource && !params.rowNode.childrenExpanded) { + apiRef.current.unstable_dataSource.fetchRows(params.id); + return; + } + apiRef.current.setRowChildrenExpansion(params.id, !params.rowNode.childrenExpanded); } }, - [apiRef, props.rowGroupingColumnMode], + [apiRef, props.rowGroupingColumnMode, props.unstable_dataSource], ); const checkGroupingColumnsModelDiff = React.useCallback< @@ -258,7 +275,7 @@ export const useGridRowGrouping = ( // Refresh the row tree creation strategy processing // TODO: Add a clean way to re-run a strategy processing without publishing a private event - if (apiRef.current.getActiveStrategy('rowTree') === ROW_GROUPING_STRATEGY) { + if (apiRef.current.getActiveStrategy('rowTree') === RowGroupingStrategy.Default) { apiRef.current.publishEvent('activeStrategyProcessorChange', 'rowTreeCreation'); } } @@ -268,7 +285,7 @@ export const useGridRowGrouping = ( useGridApiEventHandler(apiRef, 'columnsChange', checkGroupingColumnsModelDiff); useGridApiEventHandler(apiRef, 'rowGroupingModelChange', checkGroupingColumnsModelDiff); - /** + /* * EFFECTS */ React.useEffect(() => { diff --git a/packages/x-data-grid-premium/src/hooks/features/rowGrouping/useGridRowGroupingPreProcessors.ts b/packages/x-data-grid-premium/src/hooks/features/rowGrouping/useGridRowGroupingPreProcessors.ts index 2fa403ec72d5..ac43f038695a 100644 --- a/packages/x-data-grid-premium/src/hooks/features/rowGrouping/useGridRowGroupingPreProcessors.ts +++ b/packages/x-data-grid-premium/src/hooks/features/rowGrouping/useGridRowGroupingPreProcessors.ts @@ -31,7 +31,7 @@ import { import { filterRowTreeFromGroupingColumns, getColDefOverrides, - ROW_GROUPING_STRATEGY, + RowGroupingStrategy, isGroupingColumn, setStrategyAvailability, getCellGroupingCriteria, @@ -48,6 +48,7 @@ export const useGridRowGroupingPreProcessors = ( | 'rowGroupingColumnMode' | 'defaultGroupingExpansionDepth' | 'isGroupExpandedByDefault' + | 'unstable_dataSource' >, ) => { const getGroupingColDefs = React.useCallback( @@ -56,6 +57,10 @@ export const useGridRowGroupingPreProcessors = ( return []; } + const strategy = props.unstable_dataSource + ? RowGroupingStrategy.DataSource + : RowGroupingStrategy.Default; + const groupingColDefProp = props.groupingColDef; // We can't use `gridGroupingRowsSanitizedModelSelector` here because the new columns are not in the state yet @@ -73,8 +78,9 @@ export const useGridRowGroupingPreProcessors = ( createGroupingColDefForAllGroupingCriteria({ apiRef, rowGroupingModel, - colDefOverride: getColDefOverrides(groupingColDefProp, rowGroupingModel), + colDefOverride: getColDefOverrides(groupingColDefProp, rowGroupingModel, strategy), columnsLookup: columnsState.lookup, + strategy, }), ]; } @@ -86,6 +92,7 @@ export const useGridRowGroupingPreProcessors = ( colDefOverride: getColDefOverrides(groupingColDefProp, [groupingCriteria]), groupedByColDef: columnsState.lookup[groupingCriteria], columnsLookup: columnsState.lookup, + strategy, }), ); } @@ -95,7 +102,13 @@ export const useGridRowGroupingPreProcessors = ( } } }, - [apiRef, props.groupingColDef, props.rowGroupingColumnMode, props.disableRowGrouping], + [ + apiRef, + props.groupingColDef, + props.rowGroupingColumnMode, + props.disableRowGrouping, + props.unstable_dataSource, + ], ); const updateGroupingColumn = React.useCallback>( @@ -177,7 +190,7 @@ export const useGridRowGroupingPreProcessors = ( nodes: params.updates.rows.map(getRowTreeBuilderNode), defaultGroupingExpansionDepth: props.defaultGroupingExpansionDepth, isGroupExpandedByDefault: props.isGroupExpandedByDefault, - groupingName: ROW_GROUPING_STRATEGY, + groupingName: RowGroupingStrategy.Default, }); } @@ -191,7 +204,7 @@ export const useGridRowGroupingPreProcessors = ( previousTreeDepth: params.previousTreeDepths!, defaultGroupingExpansionDepth: props.defaultGroupingExpansionDepth, isGroupExpandedByDefault: props.isGroupExpandedByDefault, - groupingName: ROW_GROUPING_STRATEGY, + groupingName: RowGroupingStrategy.Default, }); }, [apiRef, props.defaultGroupingExpansionDepth, props.isGroupExpandedByDefault], @@ -228,35 +241,29 @@ export const useGridRowGroupingPreProcessors = ( useGridRegisterPipeProcessor(apiRef, 'hydrateColumns', updateGroupingColumn); useGridRegisterStrategyProcessor( apiRef, - ROW_GROUPING_STRATEGY, + RowGroupingStrategy.Default, 'rowTreeCreation', createRowTreeForRowGrouping, ); - useGridRegisterStrategyProcessor(apiRef, ROW_GROUPING_STRATEGY, 'filtering', filterRows); - useGridRegisterStrategyProcessor(apiRef, ROW_GROUPING_STRATEGY, 'sorting', sortRows); + useGridRegisterStrategyProcessor(apiRef, RowGroupingStrategy.Default, 'filtering', filterRows); + useGridRegisterStrategyProcessor(apiRef, RowGroupingStrategy.Default, 'sorting', sortRows); useGridRegisterStrategyProcessor( apiRef, - ROW_GROUPING_STRATEGY, + RowGroupingStrategy.Default, 'visibleRowsLookupCreation', getVisibleRowsLookup, ); - /** - * 1ST RENDER - */ useFirstRender(() => { - setStrategyAvailability(apiRef, props.disableRowGrouping); + setStrategyAvailability(apiRef, props.disableRowGrouping, props.unstable_dataSource); }); - /** - * EFFECTS - */ const isFirstRender = React.useRef(true); React.useEffect(() => { if (!isFirstRender.current) { - setStrategyAvailability(apiRef, props.disableRowGrouping); + setStrategyAvailability(apiRef, props.disableRowGrouping, props.unstable_dataSource); } else { isFirstRender.current = false; } - }, [apiRef, props.disableRowGrouping]); + }, [apiRef, props.disableRowGrouping, props.unstable_dataSource]); }; diff --git a/packages/x-data-grid-pro/src/hooks/features/dataSource/cache.ts b/packages/x-data-grid-pro/src/hooks/features/dataSource/cache.ts index dde8cad3d39f..5645235abf01 100644 --- a/packages/x-data-grid-pro/src/hooks/features/dataSource/cache.ts +++ b/packages/x-data-grid-pro/src/hooks/features/dataSource/cache.ts @@ -15,6 +15,7 @@ function getKey(params: GridGetRowsParams) { params.filterModel, params.sortModel, params.groupKeys, + params.groupFields, ]); } diff --git a/packages/x-data-grid-pro/src/hooks/features/dataSource/gridDataSourceSelector.ts b/packages/x-data-grid-pro/src/hooks/features/dataSource/gridDataSourceSelector.ts index 6b6aa5a9404d..09e2d34c9099 100644 --- a/packages/x-data-grid-pro/src/hooks/features/dataSource/gridDataSourceSelector.ts +++ b/packages/x-data-grid-pro/src/hooks/features/dataSource/gridDataSourceSelector.ts @@ -21,8 +21,6 @@ export const gridGetRowsParamsSelector = createSelector( (filterModel, sortModel, paginationModel) => { return { groupKeys: [], - // TODO: Implement with `rowGrouping` - groupFields: [], paginationModel, sortModel, filterModel, diff --git a/packages/x-data-grid-pro/src/hooks/features/dataSource/useGridDataSource.ts b/packages/x-data-grid-pro/src/hooks/features/dataSource/useGridDataSource.ts index 4e365d10d7ee..10da6df7c74a 100644 --- a/packages/x-data-grid-pro/src/hooks/features/dataSource/useGridDataSource.ts +++ b/packages/x-data-grid-pro/src/hooks/features/dataSource/useGridDataSource.ts @@ -85,7 +85,10 @@ export const useGridDataSource = ( apiRef.current.resetDataSourceState(); } - const fetchParams = gridGetRowsParamsSelector(apiRef); + const fetchParams = { + ...gridGetRowsParamsSelector(apiRef), + ...apiRef.current.unstable_applyPipeProcessors('getRowsParams', {}), + }; const cachedData = apiRef.current.unstable_dataSource.cache.get(fetchParams); @@ -122,7 +125,8 @@ export const useGridDataSource = ( const fetchRowChildren = React.useCallback( async (id) => { - if (!props.treeData) { + const pipedParams = apiRef.current.unstable_applyPipeProcessors('getRowsParams', {}); + if (!props.treeData && (pipedParams.groupFields?.length ?? 0) === 0) { nestedDataManager.clearPendingRequest(id); return; } @@ -138,7 +142,11 @@ export const useGridDataSource = ( return; } - const fetchParams = { ...gridGetRowsParamsSelector(apiRef), groupKeys: rowNode.path }; + const fetchParams = { + ...gridGetRowsParamsSelector(apiRef), + ...pipedParams, + groupKeys: rowNode.path, + }; const cachedData = apiRef.current.unstable_dataSource.cache.get(fetchParams); @@ -267,6 +275,7 @@ export const useGridDataSource = ( 'paginationModelChange', runIfServerMode(props.paginationMode, fetchRows), ); + useGridApiEventHandler(apiRef, 'rowGroupingModelChange', () => fetchRows()); const isFirstRender = React.useRef(true); React.useEffect(() => { diff --git a/packages/x-data-grid-pro/src/hooks/features/serverSideTreeData/useGridDataSourceTreeDataPreProcessors.tsx b/packages/x-data-grid-pro/src/hooks/features/serverSideTreeData/useGridDataSourceTreeDataPreProcessors.tsx index c09bc45b8ded..0c6b841f939d 100644 --- a/packages/x-data-grid-pro/src/hooks/features/serverSideTreeData/useGridDataSourceTreeDataPreProcessors.tsx +++ b/packages/x-data-grid-pro/src/hooks/features/serverSideTreeData/useGridDataSourceTreeDataPreProcessors.tsx @@ -34,8 +34,7 @@ import { } from '../../../utils/tree/models'; import { updateRowTree } from '../../../utils/tree/updateRowTree'; import { getVisibleRowsLookup } from '../../../utils/tree/utils'; - -const DATA_SOURCE_TREE_DATA_STRATEGY = 'dataSourceTreeData'; +import { TreeDataStrategy } from '../treeData/gridTreeDataUtils'; export const useGridDataSourceTreeDataPreProcessors = ( privateApiRef: React.MutableRefObject, @@ -53,7 +52,7 @@ export const useGridDataSourceTreeDataPreProcessors = ( const setStrategyAvailability = React.useCallback(() => { privateApiRef.current.setStrategyAvailability( 'rowTree', - DATA_SOURCE_TREE_DATA_STRATEGY, + TreeDataStrategy.DataSource, props.treeData && props.unstable_dataSource ? () => true : () => false, ); }, [privateApiRef, props.treeData, props.unstable_dataSource]); @@ -64,7 +63,7 @@ export const useGridDataSourceTreeDataPreProcessors = ( let colDefOverride: GridGroupingColDefOverride | null | undefined; if (typeof groupingColDefProp === 'function') { const params: GridGroupingColDefOverrideParams = { - groupingName: DATA_SOURCE_TREE_DATA_STRATEGY, + groupingName: TreeDataStrategy.DataSource, fields: [], }; @@ -171,7 +170,7 @@ export const useGridDataSourceTreeDataPreProcessors = ( nodes: params.updates.rows.map(getRowTreeBuilderNode), defaultGroupingExpansionDepth: props.defaultGroupingExpansionDepth, isGroupExpandedByDefault: props.isGroupExpandedByDefault, - groupingName: DATA_SOURCE_TREE_DATA_STRATEGY, + groupingName: TreeDataStrategy.DataSource, onDuplicatePath, }); } @@ -187,7 +186,7 @@ export const useGridDataSourceTreeDataPreProcessors = ( previousTreeDepth: params.previousTreeDepths!, defaultGroupingExpansionDepth: props.defaultGroupingExpansionDepth, isGroupExpandedByDefault: props.isGroupExpandedByDefault, - groupingName: DATA_SOURCE_TREE_DATA_STRATEGY, + groupingName: TreeDataStrategy.DataSource, }); }, [ @@ -212,25 +211,20 @@ export const useGridDataSourceTreeDataPreProcessors = ( useGridRegisterPipeProcessor(privateApiRef, 'hydrateColumns', updateGroupingColumn); useGridRegisterStrategyProcessor( privateApiRef, - DATA_SOURCE_TREE_DATA_STRATEGY, + TreeDataStrategy.DataSource, 'rowTreeCreation', createRowTreeForTreeData, ); useGridRegisterStrategyProcessor( privateApiRef, - DATA_SOURCE_TREE_DATA_STRATEGY, + TreeDataStrategy.DataSource, 'filtering', filterRows, ); + useGridRegisterStrategyProcessor(privateApiRef, TreeDataStrategy.DataSource, 'sorting', sortRows); useGridRegisterStrategyProcessor( privateApiRef, - DATA_SOURCE_TREE_DATA_STRATEGY, - 'sorting', - sortRows, - ); - useGridRegisterStrategyProcessor( - privateApiRef, - DATA_SOURCE_TREE_DATA_STRATEGY, + TreeDataStrategy.DataSource, 'visibleRowsLookupCreation', getVisibleRowsLookup, ); diff --git a/packages/x-data-grid-pro/src/hooks/features/treeData/gridTreeDataUtils.ts b/packages/x-data-grid-pro/src/hooks/features/treeData/gridTreeDataUtils.ts index ee8224e89b08..6161b317a427 100644 --- a/packages/x-data-grid-pro/src/hooks/features/treeData/gridTreeDataUtils.ts +++ b/packages/x-data-grid-pro/src/hooks/features/treeData/gridTreeDataUtils.ts @@ -20,7 +20,10 @@ interface FilterRowTreeFromTreeDataParams { apiRef: React.MutableRefObject; } -export const TREE_DATA_STRATEGY = 'tree-data'; +export enum TreeDataStrategy { + Default = 'tree-data', + DataSource = 'tree-data-source', +} /** * A node is visible if one of the following criteria is met: diff --git a/packages/x-data-grid-pro/src/hooks/features/treeData/useGridTreeDataPreProcessors.tsx b/packages/x-data-grid-pro/src/hooks/features/treeData/useGridTreeDataPreProcessors.tsx index 74c0fcfcda38..6a5753c9eb60 100644 --- a/packages/x-data-grid-pro/src/hooks/features/treeData/useGridTreeDataPreProcessors.tsx +++ b/packages/x-data-grid-pro/src/hooks/features/treeData/useGridTreeDataPreProcessors.tsx @@ -19,7 +19,7 @@ import { GRID_TREE_DATA_GROUPING_COL_DEF_FORCED_PROPERTIES, } from './gridTreeDataGroupColDef'; import { DataGridProProcessedProps } from '../../../models/dataGridProProps'; -import { filterRowTreeFromTreeData, TREE_DATA_STRATEGY } from './gridTreeDataUtils'; +import { filterRowTreeFromTreeData, TreeDataStrategy } from './gridTreeDataUtils'; import { GridPrivateApiPro } from '../../../models/gridApiPro'; import { GridGroupingColDefOverride, @@ -52,7 +52,7 @@ export const useGridTreeDataPreProcessors = ( const setStrategyAvailability = React.useCallback(() => { privateApiRef.current.setStrategyAvailability( 'rowTree', - TREE_DATA_STRATEGY, + TreeDataStrategy.Default, props.treeData && !props.unstable_dataSource ? () => true : () => false, ); }, [privateApiRef, props.treeData, props.unstable_dataSource]); @@ -63,7 +63,7 @@ export const useGridTreeDataPreProcessors = ( let colDefOverride: GridGroupingColDefOverride | null | undefined; if (typeof groupingColDefProp === 'function') { const params: GridGroupingColDefOverrideParams = { - groupingName: TREE_DATA_STRATEGY, + groupingName: TreeDataStrategy.Default, fields: [], }; @@ -158,7 +158,7 @@ export const useGridTreeDataPreProcessors = ( nodes: params.updates.rows.map(getRowTreeBuilderNode), defaultGroupingExpansionDepth: props.defaultGroupingExpansionDepth, isGroupExpandedByDefault: props.isGroupExpandedByDefault, - groupingName: TREE_DATA_STRATEGY, + groupingName: TreeDataStrategy.Default, onDuplicatePath, }); } @@ -173,7 +173,7 @@ export const useGridTreeDataPreProcessors = ( previousTreeDepth: params.previousTreeDepths!, defaultGroupingExpansionDepth: props.defaultGroupingExpansionDepth, isGroupExpandedByDefault: props.isGroupExpandedByDefault, - groupingName: TREE_DATA_STRATEGY, + groupingName: TreeDataStrategy.Default, }); }, [props.getTreeDataPath, props.defaultGroupingExpansionDepth, props.isGroupExpandedByDefault], @@ -211,15 +211,20 @@ export const useGridTreeDataPreProcessors = ( useGridRegisterPipeProcessor(privateApiRef, 'hydrateColumns', updateGroupingColumn); useGridRegisterStrategyProcessor( privateApiRef, - TREE_DATA_STRATEGY, + TreeDataStrategy.Default, 'rowTreeCreation', createRowTreeForTreeData, ); - useGridRegisterStrategyProcessor(privateApiRef, TREE_DATA_STRATEGY, 'filtering', filterRows); - useGridRegisterStrategyProcessor(privateApiRef, TREE_DATA_STRATEGY, 'sorting', sortRows); useGridRegisterStrategyProcessor( privateApiRef, - TREE_DATA_STRATEGY, + TreeDataStrategy.Default, + 'filtering', + filterRows, + ); + useGridRegisterStrategyProcessor(privateApiRef, TreeDataStrategy.Default, 'sorting', sortRows); + useGridRegisterStrategyProcessor( + privateApiRef, + TreeDataStrategy.Default, 'visibleRowsLookupCreation', getVisibleRowsLookup, ); diff --git a/packages/x-data-grid-pro/src/internals/index.ts b/packages/x-data-grid-pro/src/internals/index.ts index c0de9e27064b..ed619bb0cc9e 100644 --- a/packages/x-data-grid-pro/src/internals/index.ts +++ b/packages/x-data-grid-pro/src/internals/index.ts @@ -33,7 +33,6 @@ export { useGridRowReorder } from '../hooks/features/rowReorder/useGridRowReorde export { useGridRowReorderPreProcessors } from '../hooks/features/rowReorder/useGridRowReorderPreProcessors'; export { useGridTreeData } from '../hooks/features/treeData/useGridTreeData'; export { useGridTreeDataPreProcessors } from '../hooks/features/treeData/useGridTreeDataPreProcessors'; -export { TREE_DATA_STRATEGY } from '../hooks/features/treeData/gridTreeDataUtils'; export { useGridRowPinning, rowPinningStateInitializer, @@ -61,4 +60,6 @@ export { sortRowTree } from '../utils/tree/sortRowTree'; export { insertNodeInTree, removeNodeFromTree, getVisibleRowsLookup } from '../utils/tree/utils'; export type { RowTreeBuilderGroupingCriterion } from '../utils/tree/models'; +export { skipSorting, skipFiltering } from '../hooks/features/serverSideTreeData/utils'; + export * from './propValidation'; diff --git a/packages/x-data-grid/src/components/cell/GridBooleanCell.tsx b/packages/x-data-grid/src/components/cell/GridBooleanCell.tsx index 543e199229c2..69b7b58032a6 100644 --- a/packages/x-data-grid/src/components/cell/GridBooleanCell.tsx +++ b/packages/x-data-grid/src/components/cell/GridBooleanCell.tsx @@ -2,13 +2,15 @@ import * as React from 'react'; import PropTypes from 'prop-types'; import { SvgIconProps } from '@mui/material/SvgIcon'; import composeClasses from '@mui/utils/composeClasses'; +import { useGridSelector } from '../../hooks/utils/useGridSelector'; +import { gridRowMaximumTreeDepthSelector } from '../../hooks/features/rows/gridRowsSelector'; import { getDataGridUtilityClass } from '../../constants/gridClasses'; -import { GridRenderCellParams } from '../../models/params/gridCellParams'; import { useGridRootProps } from '../../hooks/utils/useGridRootProps'; import { useGridApiContext } from '../../hooks/utils/useGridApiContext'; -import { DataGridProcessedProps } from '../../models/props/DataGridProps'; -import { GridColDef } from '../../models/colDef/gridColDef'; import { isAutogeneratedRowNode } from '../../hooks/features/rows/gridRowsUtils'; +import type { DataGridProcessedProps } from '../../models/props/DataGridProps'; +import type { GridColDef } from '../../models/colDef/gridColDef'; +import type { GridRenderCellParams } from '../../models/params/gridCellParams'; type OwnerState = { classes: DataGridProcessedProps['classes'] }; @@ -49,11 +51,20 @@ function GridBooleanCellRaw(props: GridBooleanCellProps) { const ownerState = { classes: rootProps.classes }; const classes = useUtilityClasses(ownerState); + const maxDepth = useGridSelector(apiRef, gridRowMaximumTreeDepthSelector); + const isServerSideRowGroupingRow = + // @ts-expect-error - Access tree data prop + maxDepth > 0 && rowNode.type === 'group' && rootProps.treeData === false; + const Icon = React.useMemo( () => (value ? rootProps.slots.booleanCellTrueIcon : rootProps.slots.booleanCellFalseIcon), [rootProps.slots.booleanCellFalseIcon, rootProps.slots.booleanCellTrueIcon, value], ); + if (isServerSideRowGroupingRow && value === undefined) { + return null; + } + return ( ('MuiDataGrid', [ 'treeDataGroupingCellLoadingContainer', 'groupingCriteriaCell', 'groupingCriteriaCellToggle', + 'groupingCriteriaCellLoadingContainer', 'pinnedRows', 'pinnedRows--top', 'pinnedRows--bottom', diff --git a/packages/x-data-grid/src/hooks/core/pipeProcessing/gridPipeProcessingApi.ts b/packages/x-data-grid/src/hooks/core/pipeProcessing/gridPipeProcessingApi.ts index 7cd516a68c06..d47a8bbe98b0 100644 --- a/packages/x-data-grid/src/hooks/core/pipeProcessing/gridPipeProcessingApi.ts +++ b/packages/x-data-grid/src/hooks/core/pipeProcessing/gridPipeProcessingApi.ts @@ -20,6 +20,7 @@ import { import { GridRowEntry, GridRowId } from '../../../models/gridRows'; import { GridHydrateRowsValue } from '../../features/rows/gridRowsInterfaces'; import { GridPreferencePanelsValue } from '../../features/preferencesPanel'; +import { GridGetRowsParams } from '../../../models/gridDataSource'; import { HeightEntry } from '../../features/rows/gridRowsMetaInterfaces'; export type GridPipeProcessorGroup = keyof GridPipeProcessingLookup; @@ -30,6 +31,7 @@ export interface GridPipeProcessingLookup { context: GridColDef; }; exportState: { value: GridInitialStateCommunity; context: GridExportStateParams }; + getRowsParams: { value: Partial }; hydrateColumns: { value: GridHydrateColumnsValue; };