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 (
+
+
{
+ apiRef.current.unstable_dataSource.cache.clear();
+ }}
+ >
+ Clear cache
+
+
+
+
+
+
+ );
+}
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 (
+
+
{
+ apiRef.current.unstable_dataSource.cache.clear();
+ }}
+ >
+ Clear cache
+
+
+
+
+
+
+ );
+}
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 @@
+ {
+ apiRef.current.unstable_dataSource.cache.clear();
+ }}
+>
+ Clear cache
+
+
+
+
+
\ 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 (
+
+
+ {
+ setRootError('');
+ apiRef.current.unstable_dataSource.fetchRows();
+ }}
+ >
+ Refetch rows
+
+ 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 (
+
+
+ {
+ setRootError('');
+ apiRef.current.unstable_dataSource.fetchRows();
+ }}
+ >
+ Refetch rows
+
+ 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 (
+
+
Regenerate Data
+
+
+
+
+
+ );
+}
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 (
+
+
Regenerate Data
+
+
+
+
+
+ );
+}
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 (
+
+
{
+ apiRef.current.unstable_dataSource.cache.clear();
+ }}
+ >
+ Clear cache
+
+
+
+
+
+
+ );
+}
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 (
+
+
{
+ apiRef.current.unstable_dataSource.cache.clear();
+ }}
+ >
+ Clear cache
+
+
+
+
+
+
+ );
+}
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 bae512de2940..361b46837c94 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;
};