From 14c8caa302bd65169d22d67f904882ee165a4234 Mon Sep 17 00:00:00 2001 From: Flavien DELANGLE Date: Fri, 21 Jan 2022 14:39:45 +0700 Subject: [PATCH] [DataGridPro] Allow to group rows based on column value (#3277) --- .../api-docs/data-grid/data-grid-pro.json | 22 +- docs/pages/api-docs/data-grid/data-grid.json | 12 +- docs/pages/api-docs/data-grid/grid-api.md | 182 +- docs/pages/api-docs/data-grid/grid-col-def.md | 2 + .../data-grid/grid-row-grouping-api.json | 26 + docs/pages/api-docs/data-grid/selectors.json | 11 + .../api/buildInterfacesDocumentation.ts | 1 + .../data-grid/accessibility/accessibility.md | 27 +- .../components/data-grid/events/events.json | 6 + .../DefaultGroupingExpansionDepthTreeData.js | 121 - .../DefaultGroupingExpansionDepthTreeData.tsx | 121 - ...GroupingExpansionDepthTreeData.tsx.preview | 7 - .../group-pivot/RowGroupingApiNoSnap.js | 7 + .../group-pivot/RowGroupingBasicExample.js | 65 + .../group-pivot/RowGroupingBasicExample.tsx | 76 + .../RowGroupingBasicExample.tsx.preview | 14 + .../RowGroupingColDefCanBeGrouped.js | 73 + .../RowGroupingColDefCanBeGrouped.tsx | 84 + .../RowGroupingColDefCanBeGrouped.tsx.preview | 14 + .../group-pivot/RowGroupingControlled.js | 28 + .../group-pivot/RowGroupingControlled.tsx | 31 + .../RowGroupingControlled.tsx.preview | 9 + ...RowGroupingCustomGroupingColDefCallback.js | 97 + ...owGroupingCustomGroupingColDefCallback.tsx | 108 + .../RowGroupingCustomGroupingColDefObject.js | 68 + .../RowGroupingCustomGroupingColDefObject.tsx | 79 + .../RowGroupingDefaultExpansionDepth.js | 66 + .../RowGroupingDefaultExpansionDepth.tsx | 77 + ...wGroupingDefaultExpansionDepth.tsx.preview | 15 + .../group-pivot/RowGroupingDisabled.js | 20 + .../group-pivot/RowGroupingDisabled.tsx | 20 + .../RowGroupingDisabled.tsx.preview | 8 + .../group-pivot/RowGroupingFullExample.js | 77 + .../group-pivot/RowGroupingFullExample.tsx | 87 + .../RowGroupingGroupingValueGetter.js | 85 + .../RowGroupingGroupingValueGetter.tsx | 103 + ...RowGroupingGroupingValueGetter.tsx.preview | 13 + .../RowGroupingHideDescendantCount.js | 68 + .../RowGroupingHideDescendantCount.tsx | 79 + .../group-pivot/RowGroupingInitialState.js | 28 + .../group-pivot/RowGroupingInitialState.tsx | 28 + .../RowGroupingInitialState.tsx.preview | 13 + .../RowGroupingIsGroupExpandedByDefault.js | 69 + .../RowGroupingIsGroupExpandedByDefault.tsx | 81 + ...oupingIsGroupExpandedByDefault.tsx.preview | 15 + .../group-pivot/RowGroupingLeafWithValue.js | 66 + .../group-pivot/RowGroupingLeafWithValue.tsx | 77 + .../RowGroupingLeafWithValue.tsx.preview | 14 + .../RowGroupingMultipleGroupingCol.js | 65 + .../RowGroupingMultipleGroupingCol.tsx | 76 + ...RowGroupingMultipleGroupingCol.tsx.preview | 14 + .../RowGroupingRowsWithMissingGroups.js | 65 + .../RowGroupingRowsWithMissingGroups.tsx | 76 + ...wGroupingRowsWithMissingGroups.tsx.preview | 14 + .../RowGroupingSetChildrenExpansion.js | 87 + .../RowGroupingSetChildrenExpansion.tsx | 94 + .../RowGroupingSingleGroupingCol.js | 65 + .../RowGroupingSingleGroupingCol.tsx | 76 + .../RowGroupingSingleGroupingCol.tsx.preview | 14 + ...owGroupingSortingMultipleGroupingColDef.js | 75 + ...wGroupingSortingMultipleGroupingColDef.tsx | 86 + .../RowGroupingSortingSingleGroupingColDef.js | 102 + ...RowGroupingSortingSingleGroupingColDef.tsx | 113 + .../group-pivot/SetRowExpansionTreeData.js | 145 -- .../group-pivot/SetRowExpansionTreeData.tsx | 147 -- .../SetRowExpansionTreeData.tsx.preview | 11 - ...ata.js => TreeDataCustomGroupingColumn.js} | 5 +- ...a.tsx => TreeDataCustomGroupingColumn.tsx} | 2 +- ... TreeDataCustomGroupingColumn.tsx.preview} | 0 ...js => TreeDataDisableChildrenFiltering.js} | 2 +- ...x => TreeDataDisableChildrenFiltering.tsx} | 2 +- ...a.js => TreeDataDisableChildrenSorting.js} | 2 +- ...tsx => TreeDataDisableChildrenSorting.tsx} | 2 +- .../{BasicTreeData.js => TreeDataSimple.js} | 2 +- .../{BasicTreeData.tsx => TreeDataSimple.tsx} | 2 +- ...tsx.preview => TreeDataSimple.tsx.preview} | 0 .../data-grid/group-pivot/group-pivot.md | 216 +- .../components/data-grid/overview/overview.md | 3 +- .../api-docs/data-grid/data-grid-pro-pt.json | 13 +- .../api-docs/data-grid/data-grid-pro-zh.json | 13 +- .../api-docs/data-grid/data-grid-pro.json | 13 +- .../api-docs/data-grid/data-grid-pt.json | 4 +- .../api-docs/data-grid/data-grid-zh.json | 4 +- .../api-docs/data-grid/data-grid.json | 4 +- .../grid/components/cell/GridBooleanCell.tsx | 14 +- .../cell/GridGroupingColumnLeafCell.tsx | 17 + .../cell/GridGroupingCriteriaCell.tsx | 161 ++ .../components/containers/GridRootStyles.ts | 10 + .../_modules_/grid/components/icons/index.tsx | 6 +- .../GridRowGroupableColumnMenuItems.tsx | 63 + .../GridRowGroupingColumnMenuItems.tsx | 63 + .../constants/defaultGridSlotsComponents.ts | 8 +- .../grid/constants/localeTextConstants.ts | 5 + packages/grid/_modules_/grid/gridClasses.ts | 2 + .../columnReorder/useGridColumnReorder.tsx | 36 +- .../hooks/features/filter/gridFilterState.ts | 13 +- .../hooks/features/filter/gridFilterUtils.ts | 24 +- .../_modules_/grid/hooks/features/index.ts | 1 + .../rowGrouping/createGroupingColDef.tsx | 360 +++ .../rowGrouping/gridRowGroupingInterfaces.ts | 34 + .../rowGrouping/gridRowGroupingSelector.ts | 17 + .../rowGrouping/gridRowGroupingUtils.ts | 145 ++ .../grid/hooks/features/rowGrouping/index.ts | 6 + .../rowGrouping/useGridRowGrouping.tsx | 493 ++++ .../hooks/features/rows/useGridParamsApi.ts | 4 +- .../features/sorting/gridSortingUtils.ts | 1 + .../treeData/gridTreeDataGroupColDef.ts | 3 +- .../features/treeData/gridTreeDataUtils.ts | 3 +- .../features/treeData/useGridTreeData.tsx | 8 +- .../grid/hooks/utils/useGridRootProps.ts | 8 +- packages/grid/_modules_/grid/locales/arSD.ts | 5 + packages/grid/_modules_/grid/locales/bgBG.ts | 5 + packages/grid/_modules_/grid/locales/deDE.ts | 5 + packages/grid/_modules_/grid/locales/elGR.ts | 5 + packages/grid/_modules_/grid/locales/esES.ts | 5 + packages/grid/_modules_/grid/locales/faIR.ts | 5 + packages/grid/_modules_/grid/locales/fiFI.ts | 5 + packages/grid/_modules_/grid/locales/frFR.ts | 5 + packages/grid/_modules_/grid/locales/heIL.ts | 5 + packages/grid/_modules_/grid/locales/itIT.ts | 5 + packages/grid/_modules_/grid/locales/jaJP.ts | 5 + packages/grid/_modules_/grid/locales/koKR.ts | 5 + packages/grid/_modules_/grid/locales/nlNL.ts | 5 + packages/grid/_modules_/grid/locales/plPL.ts | 5 + packages/grid/_modules_/grid/locales/ptBR.ts | 5 + packages/grid/_modules_/grid/locales/ruRU.ts | 5 + packages/grid/_modules_/grid/locales/skSK.ts | 5 + packages/grid/_modules_/grid/locales/trTR.ts | 5 + packages/grid/_modules_/grid/locales/ukUA.ts | 5 + packages/grid/_modules_/grid/locales/viVN.ts | 5 + packages/grid/_modules_/grid/locales/zhCN.ts | 5 + .../grid/_modules_/grid/models/api/gridApi.ts | 2 + .../grid/models/api/gridLocaleTextApi.ts | 5 + .../grid/models/colDef/gridColDef.ts | 34 +- .../grid/models/colDef/gridStringColDef.ts | 1 + .../grid/models/events/gridEventLookup.ts | 2 + .../grid/models/events/gridEvents.ts | 4 + .../grid/models/gridIconSlotsComponent.ts | 12 + .../grid/_modules_/grid/models/gridRows.ts | 3 + .../_modules_/grid/models/gridSortModel.ts | 3 +- .../grid/_modules_/grid/models/gridState.ts | 9 + .../grid/models/params/gridCellParams.ts | 25 +- .../grid/models/props/DataGridProProps.ts | 53 +- .../grid/utils/tree/buildRowTree.test.ts | 90 +- .../src/commodities.columns.tsx | 10 +- .../grid/x-data-grid-generator/src/index.ts | 1 + .../src/renderer/renderCountry.tsx | 4 + .../src/renderer/renderPnl.tsx | 4 + .../src/renderer/renderProgress.tsx | 4 + .../src/renderer/renderStatus.tsx | 6 +- .../src/renderer/renderTotalPrice.tsx | 3 + .../x-data-grid-generator/src/useMovieData.ts | 303 +++ .../grid/x-data-grid-pro/src/DataGridPro.tsx | 32 +- .../tests/rowGrouping.DataGridPro.test.tsx | 2187 +++++++++++++++++ .../src/useDataGridProComponent.tsx | 2 + .../src/useDataGridProProps.ts | 2 + scripts/x-data-grid-pro.exports.json | 17 +- scripts/x-data-grid.exports.json | 17 +- 158 files changed, 7684 insertions(+), 775 deletions(-) create mode 100644 docs/pages/api-docs/data-grid/grid-row-grouping-api.json delete mode 100644 docs/src/pages/components/data-grid/group-pivot/DefaultGroupingExpansionDepthTreeData.js delete mode 100644 docs/src/pages/components/data-grid/group-pivot/DefaultGroupingExpansionDepthTreeData.tsx delete mode 100644 docs/src/pages/components/data-grid/group-pivot/DefaultGroupingExpansionDepthTreeData.tsx.preview create mode 100644 docs/src/pages/components/data-grid/group-pivot/RowGroupingApiNoSnap.js create mode 100644 docs/src/pages/components/data-grid/group-pivot/RowGroupingBasicExample.js create mode 100644 docs/src/pages/components/data-grid/group-pivot/RowGroupingBasicExample.tsx create mode 100644 docs/src/pages/components/data-grid/group-pivot/RowGroupingBasicExample.tsx.preview create mode 100644 docs/src/pages/components/data-grid/group-pivot/RowGroupingColDefCanBeGrouped.js create mode 100644 docs/src/pages/components/data-grid/group-pivot/RowGroupingColDefCanBeGrouped.tsx create mode 100644 docs/src/pages/components/data-grid/group-pivot/RowGroupingColDefCanBeGrouped.tsx.preview create mode 100644 docs/src/pages/components/data-grid/group-pivot/RowGroupingControlled.js create mode 100644 docs/src/pages/components/data-grid/group-pivot/RowGroupingControlled.tsx create mode 100644 docs/src/pages/components/data-grid/group-pivot/RowGroupingControlled.tsx.preview create mode 100644 docs/src/pages/components/data-grid/group-pivot/RowGroupingCustomGroupingColDefCallback.js create mode 100644 docs/src/pages/components/data-grid/group-pivot/RowGroupingCustomGroupingColDefCallback.tsx create mode 100644 docs/src/pages/components/data-grid/group-pivot/RowGroupingCustomGroupingColDefObject.js create mode 100644 docs/src/pages/components/data-grid/group-pivot/RowGroupingCustomGroupingColDefObject.tsx create mode 100644 docs/src/pages/components/data-grid/group-pivot/RowGroupingDefaultExpansionDepth.js create mode 100644 docs/src/pages/components/data-grid/group-pivot/RowGroupingDefaultExpansionDepth.tsx create mode 100644 docs/src/pages/components/data-grid/group-pivot/RowGroupingDefaultExpansionDepth.tsx.preview create mode 100644 docs/src/pages/components/data-grid/group-pivot/RowGroupingDisabled.js create mode 100644 docs/src/pages/components/data-grid/group-pivot/RowGroupingDisabled.tsx create mode 100644 docs/src/pages/components/data-grid/group-pivot/RowGroupingDisabled.tsx.preview create mode 100644 docs/src/pages/components/data-grid/group-pivot/RowGroupingFullExample.js create mode 100644 docs/src/pages/components/data-grid/group-pivot/RowGroupingFullExample.tsx create mode 100644 docs/src/pages/components/data-grid/group-pivot/RowGroupingGroupingValueGetter.js create mode 100644 docs/src/pages/components/data-grid/group-pivot/RowGroupingGroupingValueGetter.tsx create mode 100644 docs/src/pages/components/data-grid/group-pivot/RowGroupingGroupingValueGetter.tsx.preview create mode 100644 docs/src/pages/components/data-grid/group-pivot/RowGroupingHideDescendantCount.js create mode 100644 docs/src/pages/components/data-grid/group-pivot/RowGroupingHideDescendantCount.tsx create mode 100644 docs/src/pages/components/data-grid/group-pivot/RowGroupingInitialState.js create mode 100644 docs/src/pages/components/data-grid/group-pivot/RowGroupingInitialState.tsx create mode 100644 docs/src/pages/components/data-grid/group-pivot/RowGroupingInitialState.tsx.preview create mode 100644 docs/src/pages/components/data-grid/group-pivot/RowGroupingIsGroupExpandedByDefault.js create mode 100644 docs/src/pages/components/data-grid/group-pivot/RowGroupingIsGroupExpandedByDefault.tsx create mode 100644 docs/src/pages/components/data-grid/group-pivot/RowGroupingIsGroupExpandedByDefault.tsx.preview create mode 100644 docs/src/pages/components/data-grid/group-pivot/RowGroupingLeafWithValue.js create mode 100644 docs/src/pages/components/data-grid/group-pivot/RowGroupingLeafWithValue.tsx create mode 100644 docs/src/pages/components/data-grid/group-pivot/RowGroupingLeafWithValue.tsx.preview create mode 100644 docs/src/pages/components/data-grid/group-pivot/RowGroupingMultipleGroupingCol.js create mode 100644 docs/src/pages/components/data-grid/group-pivot/RowGroupingMultipleGroupingCol.tsx create mode 100644 docs/src/pages/components/data-grid/group-pivot/RowGroupingMultipleGroupingCol.tsx.preview create mode 100644 docs/src/pages/components/data-grid/group-pivot/RowGroupingRowsWithMissingGroups.js create mode 100644 docs/src/pages/components/data-grid/group-pivot/RowGroupingRowsWithMissingGroups.tsx create mode 100644 docs/src/pages/components/data-grid/group-pivot/RowGroupingRowsWithMissingGroups.tsx.preview create mode 100644 docs/src/pages/components/data-grid/group-pivot/RowGroupingSetChildrenExpansion.js create mode 100644 docs/src/pages/components/data-grid/group-pivot/RowGroupingSetChildrenExpansion.tsx create mode 100644 docs/src/pages/components/data-grid/group-pivot/RowGroupingSingleGroupingCol.js create mode 100644 docs/src/pages/components/data-grid/group-pivot/RowGroupingSingleGroupingCol.tsx create mode 100644 docs/src/pages/components/data-grid/group-pivot/RowGroupingSingleGroupingCol.tsx.preview create mode 100644 docs/src/pages/components/data-grid/group-pivot/RowGroupingSortingMultipleGroupingColDef.js create mode 100644 docs/src/pages/components/data-grid/group-pivot/RowGroupingSortingMultipleGroupingColDef.tsx create mode 100644 docs/src/pages/components/data-grid/group-pivot/RowGroupingSortingSingleGroupingColDef.js create mode 100644 docs/src/pages/components/data-grid/group-pivot/RowGroupingSortingSingleGroupingColDef.tsx delete mode 100644 docs/src/pages/components/data-grid/group-pivot/SetRowExpansionTreeData.js delete mode 100644 docs/src/pages/components/data-grid/group-pivot/SetRowExpansionTreeData.tsx delete mode 100644 docs/src/pages/components/data-grid/group-pivot/SetRowExpansionTreeData.tsx.preview rename docs/src/pages/components/data-grid/group-pivot/{CustomGroupingColumnTreeData.js => TreeDataCustomGroupingColumn.js} (98%) rename docs/src/pages/components/data-grid/group-pivot/{CustomGroupingColumnTreeData.tsx => TreeDataCustomGroupingColumn.tsx} (98%) rename docs/src/pages/components/data-grid/group-pivot/{CustomGroupingColumnTreeData.tsx.preview => TreeDataCustomGroupingColumn.tsx.preview} (100%) rename docs/src/pages/components/data-grid/group-pivot/{DisableChildrenFilteringTreeData.js => TreeDataDisableChildrenFiltering.js} (98%) rename docs/src/pages/components/data-grid/group-pivot/{DisableChildrenFilteringTreeData.tsx => TreeDataDisableChildrenFiltering.tsx} (98%) rename docs/src/pages/components/data-grid/group-pivot/{DisableChildrenSortingTreeData.js => TreeDataDisableChildrenSorting.js} (98%) rename docs/src/pages/components/data-grid/group-pivot/{DisableChildrenSortingTreeData.tsx => TreeDataDisableChildrenSorting.tsx} (98%) rename docs/src/pages/components/data-grid/group-pivot/{BasicTreeData.js => TreeDataSimple.js} (98%) rename docs/src/pages/components/data-grid/group-pivot/{BasicTreeData.tsx => TreeDataSimple.tsx} (98%) rename docs/src/pages/components/data-grid/group-pivot/{BasicTreeData.tsx.preview => TreeDataSimple.tsx.preview} (100%) create mode 100644 packages/grid/_modules_/grid/components/cell/GridGroupingColumnLeafCell.tsx create mode 100644 packages/grid/_modules_/grid/components/cell/GridGroupingCriteriaCell.tsx create mode 100644 packages/grid/_modules_/grid/components/menu/columnMenu/GridRowGroupableColumnMenuItems.tsx create mode 100644 packages/grid/_modules_/grid/components/menu/columnMenu/GridRowGroupingColumnMenuItems.tsx create mode 100644 packages/grid/_modules_/grid/hooks/features/rowGrouping/createGroupingColDef.tsx create mode 100644 packages/grid/_modules_/grid/hooks/features/rowGrouping/gridRowGroupingInterfaces.ts create mode 100644 packages/grid/_modules_/grid/hooks/features/rowGrouping/gridRowGroupingSelector.ts create mode 100644 packages/grid/_modules_/grid/hooks/features/rowGrouping/gridRowGroupingUtils.ts create mode 100644 packages/grid/_modules_/grid/hooks/features/rowGrouping/index.ts create mode 100644 packages/grid/_modules_/grid/hooks/features/rowGrouping/useGridRowGrouping.tsx create mode 100644 packages/grid/x-data-grid-generator/src/useMovieData.ts create mode 100644 packages/grid/x-data-grid-pro/src/tests/rowGrouping.DataGridPro.test.tsx diff --git a/docs/pages/api-docs/data-grid/data-grid-pro.json b/docs/pages/api-docs/data-grid/data-grid-pro.json index ec1df12f609a2..92bb747ba8b32 100644 --- a/docs/pages/api-docs/data-grid/data-grid-pro.json +++ b/docs/pages/api-docs/data-grid/data-grid-pro.json @@ -45,6 +45,7 @@ "disableMultipleColumnsFiltering": { "type": { "name": "bool" } }, "disableMultipleColumnsSorting": { "type": { "name": "bool" } }, "disableMultipleSelection": { "type": { "name": "bool" } }, + "disableRowGrouping": { "type": { "name": "bool" } }, "disableSelectionOnClick": { "type": { "name": "bool" } }, "disableVirtualization": { "type": { "name": "bool" } }, "editMode": { @@ -53,6 +54,9 @@ }, "editRowsModel": { "type": { "name": "object" } }, "error": { "type": { "name": "any" } }, + "experimentalFeatures": { + "type": { "name": "shape", "description": "{ rowGrouping?: bool }" } + }, "filterMode": { "type": { "name": "custom", "description": "'client'
| 'server'" }, "default": "\"client\"" @@ -131,6 +135,7 @@ "onRowEditCommit": { "type": { "name": "func" } }, "onRowEditStart": { "type": { "name": "func" } }, "onRowEditStop": { "type": { "name": "func" } }, + "onRowGroupingModelChange": { "type": { "name": "func" } }, "onRowsScrollEnd": { "type": { "name": "func" } }, "onSelectionModelChange": { "type": { "name": "func" } }, "onSortModelChange": { "type": { "name": "func" } }, @@ -150,6 +155,11 @@ }, "rowBuffer": { "type": { "name": "number" }, "default": "3" }, "rowCount": { "type": { "name": "number" } }, + "rowGroupingColumnMode": { + "type": { "name": "enum", "description": "'multiple'
| 'single'" }, + "default": "'single'" + }, + "rowGroupingModel": { "type": { "name": "arrayOf", "description": "Array<string>" } }, "rowHeight": { "type": { "name": "number" }, "default": "52" }, "rowsPerPageOptions": { "type": { "name": "arrayOf", "description": "Array<number>" }, @@ -232,6 +242,14 @@ "ExportIcon": { "default": "GridSaveAltIcon", "type": { "name": "elementType" } }, "FilterPanel": { "default": "GridFilterPanel", "type": { "name": "elementType" } }, "Footer": { "default": "GridFooter", "type": { "name": "elementType" } }, + "GroupingCriteriaCollapseIcon": { + "default": "GridExpandMoreIcon", + "type": { "name": "elementType" } + }, + "GroupingCriteriaExpandIcon": { + "default": "GridKeyboardArrowRight", + "type": { "name": "elementType" } + }, "Header": { "default": "GridHeader", "type": { "name": "elementType" } }, "LoadingOverlay": { "default": "GridLoadingOverlay", "type": { "name": "elementType" } }, "MoreActionsIcon": { "default": "GridMoreVertIcon", "type": { "name": "elementType" } }, @@ -243,8 +261,8 @@ "PreferencesPanel": { "default": "GridPreferencesPanel", "type": { "name": "elementType" } }, "Row": { "type": { "name": "elementType" } }, "Toolbar": { "default": "null", "type": { "name": "elementType | null" } }, - "TreeDataCollapseIcon": { "type": { "name": "elementType" } }, - "TreeDataExpandIcon": { "type": { "name": "elementType" } } + "TreeDataCollapseIcon": { "default": "GridExpandMoreIcon", "type": { "name": "elementType" } }, + "TreeDataExpandIcon": { "default": "GridKeyboardArrowRight", "type": { "name": "elementType" } } }, "name": "DataGridPro", "styles": { diff --git a/docs/pages/api-docs/data-grid/data-grid.json b/docs/pages/api-docs/data-grid/data-grid.json index 201b021f028c0..17b7cd5ad5044 100644 --- a/docs/pages/api-docs/data-grid/data-grid.json +++ b/docs/pages/api-docs/data-grid/data-grid.json @@ -189,6 +189,14 @@ "ExportIcon": { "default": "GridSaveAltIcon", "type": { "name": "elementType" } }, "FilterPanel": { "default": "GridFilterPanel", "type": { "name": "elementType" } }, "Footer": { "default": "GridFooter", "type": { "name": "elementType" } }, + "GroupingCriteriaCollapseIcon": { + "default": "GridExpandMoreIcon", + "type": { "name": "elementType" } + }, + "GroupingCriteriaExpandIcon": { + "default": "GridKeyboardArrowRight", + "type": { "name": "elementType" } + }, "Header": { "default": "GridHeader", "type": { "name": "elementType" } }, "LoadingOverlay": { "default": "GridLoadingOverlay", "type": { "name": "elementType" } }, "MoreActionsIcon": { "default": "GridMoreVertIcon", "type": { "name": "elementType" } }, @@ -200,8 +208,8 @@ "PreferencesPanel": { "default": "GridPreferencesPanel", "type": { "name": "elementType" } }, "Row": { "type": { "name": "elementType" } }, "Toolbar": { "default": "null", "type": { "name": "elementType | null" } }, - "TreeDataCollapseIcon": { "type": { "name": "elementType" } }, - "TreeDataExpandIcon": { "type": { "name": "elementType" } } + "TreeDataCollapseIcon": { "default": "GridExpandMoreIcon", "type": { "name": "elementType" } }, + "TreeDataExpandIcon": { "default": "GridKeyboardArrowRight", "type": { "name": "elementType" } } }, "name": "DataGrid", "styles": { diff --git a/docs/pages/api-docs/data-grid/grid-api.md b/docs/pages/api-docs/data-grid/grid-api.md index 92aec1a29d26c..b1e5122570cb7 100644 --- a/docs/pages/api-docs/data-grid/grid-api.md +++ b/docs/pages/api-docs/data-grid/grid-api.md @@ -10,92 +10,96 @@ import { GridApi } from '@mui/x-data-grid-pro'; ## Properties -| Name | Type | Description | -| :----------------------------------------------------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| applySorting | () => void | Applies the current sort model to the rows. | -| commitCellChange | (params: GridCommitCellChangeParams, event?: MuiBaseEvent) => boolean \| Promise<boolean> | Updates the field at the given id with the value stored in the edit row model. | -| commitRowChange | (id: GridRowId, event?: MuiBaseEvent) => boolean \| Promise<boolean> | Updates the row at the given id with the values stored in the edit row model. | -| deleteFilterItem | (item: GridFilterItem) => void | Deletes a [GridFilterItem](/api/data-grid/grid-filter-item/). | -| exportDataAsCsv | (options?: GridCsvExportOptions) => void | Downloads and exports a CSV of the grid's data. | -| exportDataAsPrint | (options?: GridPrintExportOptions) => void | Print the grid's data. | -| forceUpdate | () => void | Forces the grid to rerender. It's often used after a state update. | -| getAllColumns | () => GridStateColDef[] | Returns an array of [GridColDef](/api/data-grid/grid-col-def/) containing all the column definitions. | -| getAllRowIds | () => GridRowId[] | Gets the list of row ids. | -| getCellElement | (id: GridRowId, field: string) => HTMLDivElement \| null | Gets the underlying DOM element for a cell at the given `id` and `field`. | -| getCellMode | (id: GridRowId, field: string) => GridCellMode | Gets the mode of a cell. | -| getCellParams | (id: GridRowId, field: string) => GridCellParams | Gets the [GridCellParams](/api/data-grid/grid-cell-params/) object that is passed as argument in events. | -| getCellValue | (id: GridRowId, field: string) => GridCellValue | Gets the value of a cell at the given `id` and `field`. | -| getColumn | (field: string) => GridStateColDef | Returns the [GridColDef](/api/data-grid/grid-col-def/) for the given `field`. | -| getColumnHeaderElement | (field: string) => HTMLDivElement \| null | Gets the underlying DOM element for the column header with the given `field`. | -| getColumnHeaderParams | (field: string) => GridColumnHeaderParams | Gets the GridColumnHeaderParams object that is passed as argument in events. | -| getColumnIndex | (field: string, useVisibleColumns?: boolean) => number | Returns the index position of a column. By default, only the visible columns are considered.
Pass `false` to `useVisibleColumns` to consider all columns. | -| getColumnPosition | (field: string) => number | Returns the left-position of a column relative to the inner border of the grid. | -| getColumnsMeta | () => GridColumnsMeta | Returns the GridColumnsMeta for each column. | -| getDataAsCsv | (options?: GridCsvExportOptions) => string | Returns the grid data as a CSV string.
This method is used internally by `exportDataAsCsv`. | -| getEditRowsModel | () => GridEditRowsModel | Gets the edit rows model of the grid. | -| getLocaleText | <T extends GridTranslationKeys>(key: T) => GridLocaleText[T] | Returns the translation for the `key`. | -| getPinnedColumns | () => GridPinnedColumns | Returns which columns are pinned. | -| getRootDimensions | () => GridDimensions \| null | Returns the dimensions of the grid | -| getRow | (id: GridRowId) => GridRowModel \| null | Gets the row data with a given id. | -| getRowElement | (id: GridRowId) => HTMLDivElement \| null | Gets the underlying DOM element for a row at the given `id`. | -| getRowIdFromRowIndex | (index: number) => GridRowId | Gets the `GridRowId` of a row at a specific index.
The index is based on the sorted but unfiltered row list. | -| getRowIndex | (id: GridRowId) => number | Gets the row index of a row with a given id.
The index is based on the sorted but unfiltered row list. | -| getRowMode | (id: GridRowId) => GridRowMode | Gets the mode of a row. | -| getRowModels | () => Map<GridRowId, GridRowModel> | Gets the full set of rows as Map<GridRowId, GridRowModel>. | -| getRowNode | (id: GridRowId) => GridRowTreeNodeConfig \| null | Gets the row node from the internal tree structure. | -| getRowParams | (id: GridRowId) => GridRowParams | Gets the [GridRowParams](/api/data-grid/grid-row-params/) object that is passed as argument in events. | -| getRowsCount | () => number | Gets the total number of rows in the grid. | -| getScrollPosition | () => GridScrollParams | Returns the current scroll position. | -| getSelectedRows | () => Map<GridRowId, GridRowModel> | Returns an array of the selected rows. | -| getSortedRowIds | () => GridRowId[] | Returns all row ids sorted according to the active sort model. | -| getSortedRows | () => GridRowModel[] | Returns all rows sorted according to the active sort model. | -| getSortModel | () => GridSortModel | Returns the sort model currently applied to the grid. | -| getVisibleColumns | () => GridStateColDef[] | Returns the currently visible columns. | -| getVisibleRowModels | () => Map<GridRowId, GridRowModel> | Returns a sorted `Map` containing only the visible rows. | -| hideColumnMenu | () => void | Hides the column menu that is open. | -| hideFilterPanel | () => void | Hides the filter panel. | -| hidePreferences | () => void | Hides the preferences panel. | -| isCellEditable | (params: GridCellParams) => boolean | Controls if a cell is editable. | -| isColumnPinned | (field: string) => GridPinnedPosition \| false | Returns which side a column is pinned to. | -| isRowSelected | (id: GridRowId) => boolean | Determines if a row is selected or not. | -| pinColumn | (field: string, side: GridPinnedPosition) => void | Pins a column to the left or right side of the grid. | -| publishEvent | GridEventPublisher | Emits an event. | -| resize | () => void | Triggers a resize of the component and recalculation of width and height. | -| scroll | (params: Partial<GridScrollParams>) => void | Triggers the viewport to scroll to the given positions (in pixels). | -| scrollToIndexes | (params: Partial<GridCellIndexCoordinates>) => boolean | Triggers the viewport to scroll to the cell at indexes given by `params`.
Returns `true` if the grid had to scroll to reach the target. | -| selectRow | (id: GridRowId, isSelected?: boolean, resetSelection?: boolean) => void | Change the selection state of a row. | -| selectRowRange | (range: { startId: GridRowId; endId: GridRowId }, isSelected?: boolean, resetSelection?: boolean) => void | Change the selection state of all the selectable rows in a range. | -| selectRows | (ids: GridRowId[], isSelected?: boolean, resetSelection?: boolean) => void | Change the selection state of multiple rows. | -| setCellFocus | (id: GridRowId, field: string) => void | Sets the focus to the cell at the given `id` and `field`. | -| setCellMode | (id: GridRowId, field: string, mode: GridCellMode) => void | Sets the mode of a cell. | -| setColumnHeaderFocus | (field: string, event?: MuiBaseEvent) => void | Sets the focus to the column header at the given `field`. | -| setColumnIndex | (field: string, targetIndexPosition: number) => void | Moves a column from its original position to the position given by `targetIndexPosition`. | -| setColumnVisibility | (field: string, isVisible: boolean) => void | Changes the visibility of the column referred by `field`. | -| setColumnWidth | (field: string, width: number) => void | Updates the width of a column. | -| setDensity | (density: GridDensity, headerHeight?: number, rowHeight?: number) => void | Sets the density of the grid. | -| setEditCellValue | (params: GridEditCellValueParams, event?: MuiBaseEvent) => void | Sets the value of the edit cell.
Commonly used inside the edit cell component. | -| setEditRowsModel | (model: GridEditRowsModel) => void | Set the edit rows model of the grid. | -| setFilterLinkOperator | (operator: GridLinkOperator) => void | Changes the GridLinkOperator used to connect the filters. | -| setFilterModel | (model: GridFilterModel) => void | Sets the filter model to the one given by `model`. | -| setPage | (page: number) => void | Sets the displayed page to the value given by `page`. | -| setPageSize | (pageSize: number) => void | Sets the number of displayed rows to the value given by `pageSize`. | -| setPinnedColumns | (pinnedColumns: GridPinnedColumns) => void | Changes the pinned columns. | -| setRowChildrenExpansion | (id: GridRowId, isExpanded: boolean) => void | Expand or collapse a row children. | -| setRowMode | (id: GridRowId, mode: GridRowMode) => void | Sets the mode of a row. | -| setRows | (rows: GridRowModel[]) => void | Sets a new set of rows. | -| setSelectionModel | (rowIds: GridRowId[]) => void | Updates the selected rows to be those passed to the `rowIds` argument.
Any row already selected will be unselected. | -| setSortModel | (model: GridSortModel) => void | Updates the sort model and triggers the sorting of rows. | -| setState | (state: GridState \| ((previousState: GridState) => GridState)) => boolean | Sets the whole state of the grid. | -| showColumnMenu | (field: string) => void | Display the column menu under the `field` column. | -| showError | (props: any) => void | Displays the error overlay component. | -| showFilterPanel | (targetColumnField?: string) => void | Shows the filter panel. If `targetColumnField` is given, a filter for this field is also added. | -| showPreferences | (newValue: GridPreferencePanelsValue) => void | Displays the preferences panel. The `newValue` argument controls the content of the panel. | -| sortColumn | (column: GridColDef, direction?: GridSortDirection, allowMultipleSorting?: boolean) => void | Sorts a column. | -| state | GridState | Property that contains the whole state of the grid. | -| subscribeEvent | <E extends GridEventsStr>(event: E, handler: GridEventListener<E>, options?: EventListenerOptions) => () => void | Registers a handler for an event. | -| toggleColumnMenu | (field: string) => void | Toggles the column menu under the `field` column. | -| unpinColumn | (field: string) => void | Unpins a column. | -| updateColumn | (col: GridColDef) => void | Updates the definition of a column. | -| updateColumns | (cols: GridColDef[]) => void | Updates the definition of multiple columns at the same time. | -| updateRows | (updates: GridRowModelUpdate[]) => void | Allows to updates, insert and delete rows in a single call. | -| upsertFilterItem | (item: GridFilterItem) => void | Updates or inserts a [GridFilterItem](/api/data-grid/grid-filter-item/). | +| Name | Type | Description | +| :--------------------------------------------------------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| addRowGroupingCriteria | (groupingCriteriaField: string, groupingIndex?: number) => void | Adds the field to the row grouping model. | +| applySorting | () => void | Applies the current sort model to the rows. | +| commitCellChange | (params: GridCommitCellChangeParams, event?: MuiBaseEvent) => boolean \| Promise<boolean> | Updates the field at the given id with the value stored in the edit row model. | +| commitRowChange | (id: GridRowId, event?: MuiBaseEvent) => boolean \| Promise<boolean> | Updates the row at the given id with the values stored in the edit row model. | +| deleteFilterItem | (item: GridFilterItem) => void | Deletes a [GridFilterItem](/api/data-grid/grid-filter-item/). | +| exportDataAsCsv | (options?: GridCsvExportOptions) => void | Downloads and exports a CSV of the grid's data. | +| exportDataAsPrint | (options?: GridPrintExportOptions) => void | Print the grid's data. | +| forceUpdate | () => void | Forces the grid to rerender. It's often used after a state update. | +| getAllColumns | () => GridStateColDef[] | Returns an array of [GridColDef](/api/data-grid/grid-col-def/) containing all the column definitions. | +| getAllRowIds | () => GridRowId[] | Gets the list of row ids. | +| getCellElement | (id: GridRowId, field: string) => HTMLDivElement \| null | Gets the underlying DOM element for a cell at the given `id` and `field`. | +| getCellMode | (id: GridRowId, field: string) => GridCellMode | Gets the mode of a cell. | +| getCellParams | (id: GridRowId, field: string) => GridCellParams | Gets the [GridCellParams](/api/data-grid/grid-cell-params/) object that is passed as argument in events. | +| getCellValue | (id: GridRowId, field: string) => GridCellValue | Gets the value of a cell at the given `id` and `field`. | +| getColumn | (field: string) => GridStateColDef | Returns the [GridColDef](/api/data-grid/grid-col-def/) for the given `field`. | +| getColumnHeaderElement | (field: string) => HTMLDivElement \| null | Gets the underlying DOM element for the column header with the given `field`. | +| getColumnHeaderParams | (field: string) => GridColumnHeaderParams | Gets the GridColumnHeaderParams object that is passed as argument in events. | +| getColumnIndex | (field: string, useVisibleColumns?: boolean) => number | Returns the index position of a column. By default, only the visible columns are considered.
Pass `false` to `useVisibleColumns` to consider all columns. | +| getColumnPosition | (field: string) => number | Returns the left-position of a column relative to the inner border of the grid. | +| getColumnsMeta | () => GridColumnsMeta | Returns the GridColumnsMeta for each column. | +| getDataAsCsv | (options?: GridCsvExportOptions) => string | Returns the grid data as a CSV string.
This method is used internally by `exportDataAsCsv`. | +| getEditRowsModel | () => GridEditRowsModel | Gets the edit rows model of the grid. | +| getLocaleText | <T extends GridTranslationKeys>(key: T) => GridLocaleText[T] | Returns the translation for the `key`. | +| getPinnedColumns | () => GridPinnedColumns | Returns which columns are pinned. | +| getRootDimensions | () => GridDimensions \| null | Returns the dimensions of the grid | +| getRow | (id: GridRowId) => GridRowModel \| null | Gets the row data with a given id. | +| getRowElement | (id: GridRowId) => HTMLDivElement \| null | Gets the underlying DOM element for a row at the given `id`. | +| getRowIdFromRowIndex | (index: number) => GridRowId | Gets the `GridRowId` of a row at a specific index.
The index is based on the sorted but unfiltered row list. | +| getRowIndex | (id: GridRowId) => number | Gets the row index of a row with a given id.
The index is based on the sorted but unfiltered row list. | +| getRowMode | (id: GridRowId) => GridRowMode | Gets the mode of a row. | +| getRowModels | () => Map<GridRowId, GridRowModel> | Gets the full set of rows as Map<GridRowId, GridRowModel>. | +| getRowNode | (id: GridRowId) => GridRowTreeNodeConfig \| null | Gets the row node from the internal tree structure. | +| getRowParams | (id: GridRowId) => GridRowParams | Gets the [GridRowParams](/api/data-grid/grid-row-params/) object that is passed as argument in events. | +| getRowsCount | () => number | Gets the total number of rows in the grid. | +| getScrollPosition | () => GridScrollParams | Returns the current scroll position. | +| getSelectedRows | () => Map<GridRowId, GridRowModel> | Returns an array of the selected rows. | +| getSortedRowIds | () => GridRowId[] | Returns all row ids sorted according to the active sort model. | +| getSortedRows | () => GridRowModel[] | Returns all rows sorted according to the active sort model. | +| getSortModel | () => GridSortModel | Returns the sort model currently applied to the grid. | +| getVisibleColumns | () => GridStateColDef[] | Returns the currently visible columns. | +| getVisibleRowModels | () => Map<GridRowId, GridRowModel> | Returns a sorted `Map` containing only the visible rows. | +| hideColumnMenu | () => void | Hides the column menu that is open. | +| hideFilterPanel | () => void | Hides the filter panel. | +| hidePreferences | () => void | Hides the preferences panel. | +| isCellEditable | (params: GridCellParams) => boolean | Controls if a cell is editable. | +| isColumnPinned | (field: string) => GridPinnedPosition \| false | Returns which side a column is pinned to. | +| isRowSelected | (id: GridRowId) => boolean | Determines if a row is selected or not. | +| pinColumn | (field: string, side: GridPinnedPosition) => void | Pins a column to the left or right side of the grid. | +| publishEvent | GridEventPublisher | Emits an event. | +| removeRowGroupingCriteria | (groupingCriteriaField: string) => void | sRemove the field from the row grouping model. | +| resize | () => void | Triggers a resize of the component and recalculation of width and height. | +| scroll | (params: Partial<GridScrollParams>) => void | Triggers the viewport to scroll to the given positions (in pixels). | +| scrollToIndexes | (params: Partial<GridCellIndexCoordinates>) => boolean | Triggers the viewport to scroll to the cell at indexes given by `params`.
Returns `true` if the grid had to scroll to reach the target. | +| selectRow | (id: GridRowId, isSelected?: boolean, resetSelection?: boolean) => void | Change the selection state of a row. | +| selectRowRange | (range: { startId: GridRowId; endId: GridRowId }, isSelected?: boolean, resetSelection?: boolean) => void | Change the selection state of all the selectable rows in a range. | +| selectRows | (ids: GridRowId[], isSelected?: boolean, resetSelection?: boolean) => void | Change the selection state of multiple rows. | +| setCellFocus | (id: GridRowId, field: string) => void | Sets the focus to the cell at the given `id` and `field`. | +| setCellMode | (id: GridRowId, field: string, mode: GridCellMode) => void | Sets the mode of a cell. | +| setColumnHeaderFocus | (field: string, event?: MuiBaseEvent) => void | Sets the focus to the column header at the given `field`. | +| setColumnIndex | (field: string, targetIndexPosition: number) => void | Moves a column from its original position to the position given by `targetIndexPosition`. | +| setColumnVisibility | (field: string, isVisible: boolean) => void | Changes the visibility of the column referred by `field`. | +| setColumnWidth | (field: string, width: number) => void | Updates the width of a column. | +| setDensity | (density: GridDensity, headerHeight?: number, rowHeight?: number) => void | Sets the density of the grid. | +| setEditCellValue | (params: GridEditCellValueParams, event?: MuiBaseEvent) => void | Sets the value of the edit cell.
Commonly used inside the edit cell component. | +| setEditRowsModel | (model: GridEditRowsModel) => void | Set the edit rows model of the grid. | +| setFilterLinkOperator | (operator: GridLinkOperator) => void | Changes the GridLinkOperator used to connect the filters. | +| setFilterModel | (model: GridFilterModel) => void | Sets the filter model to the one given by `model`. | +| setPage | (page: number) => void | Sets the displayed page to the value given by `page`. | +| setPageSize | (pageSize: number) => void | Sets the number of displayed rows to the value given by `pageSize`. | +| setPinnedColumns | (pinnedColumns: GridPinnedColumns) => void | Changes the pinned columns. | +| setRowChildrenExpansion | (id: GridRowId, isExpanded: boolean) => void | Expand or collapse a row children. | +| setRowGroupingCriteriaIndex | (groupingCriteriaField: string, groupingIndex: number) => void | Sets the grouping index of a grouping criteria. | +| setRowGroupingModel | (model: GridRowGroupingModel) => void | Sets the columns to use as grouping criteria. | +| setRowMode | (id: GridRowId, mode: GridRowMode) => void | Sets the mode of a row. | +| setRows | (rows: GridRowModel[]) => void | Sets a new set of rows. | +| setSelectionModel | (rowIds: GridRowId[]) => void | Updates the selected rows to be those passed to the `rowIds` argument.
Any row already selected will be unselected. | +| setSortModel | (model: GridSortModel) => void | Updates the sort model and triggers the sorting of rows. | +| setState | (state: GridState \| ((previousState: GridState) => GridState)) => boolean | Sets the whole state of the grid. | +| showColumnMenu | (field: string) => void | Display the column menu under the `field` column. | +| showError | (props: any) => void | Displays the error overlay component. | +| showFilterPanel | (targetColumnField?: string) => void | Shows the filter panel. If `targetColumnField` is given, a filter for this field is also added. | +| showPreferences | (newValue: GridPreferencePanelsValue) => void | Displays the preferences panel. The `newValue` argument controls the content of the panel. | +| sortColumn | (column: GridColDef, direction?: GridSortDirection, allowMultipleSorting?: boolean) => void | Sorts a column. | +| state | GridState | Property that contains the whole state of the grid. | +| subscribeEvent | <E extends GridEventsStr>(event: E, handler: GridEventListener<E>, options?: EventListenerOptions) => () => void | Registers a handler for an event. | +| toggleColumnMenu | (field: string) => void | Toggles the column menu under the `field` column. | +| unpinColumn | (field: string) => void | Unpins a column. | +| updateColumn | (col: GridColDef) => void | Updates the definition of a column. | +| updateColumns | (cols: GridColDef[]) => void | Updates the definition of multiple columns at the same time. | +| updateRows | (updates: GridRowModelUpdate[]) => void | Allows to updates, insert and delete rows in a single call. | +| upsertFilterItem | (item: GridFilterItem) => void | Updates or inserts a [GridFilterItem](/api/data-grid/grid-filter-item/). | diff --git a/docs/pages/api-docs/data-grid/grid-col-def.md b/docs/pages/api-docs/data-grid/grid-col-def.md index 6eedb2a8013ee..714ab227b56c8 100644 --- a/docs/pages/api-docs/data-grid/grid-col-def.md +++ b/docs/pages/api-docs/data-grid/grid-col-def.md @@ -25,6 +25,8 @@ import { GridColDef } from '@mui/x-data-grid'; | filterable? | boolean | true | If `true`, the column is filterable. | | filterOperators? | GridFilterOperator[] | | Allows setting the filter operators for this column. | | flex? | number | | If set, it indicates that a column has fluid width. Range [0, ∞). | +| groupable? | boolean | true | If `true`, the rows can be grouped based on this column values (pro-plan only). | +| groupingValueGetter? | (params: GridGroupingValueGetterParams) => GridKeyValue \| null \| undefined | | Function that transforms a complex cell value into a key that be used for grouping the rows. | | headerAlign? | GridAlignment | | Header cell element alignment. | | headerClassName? | GridColumnHeaderClassNamePropType | | Class name that will be added in the column header cell. | | headerName? | string | | The title of the column rendered in the column header cell. | diff --git a/docs/pages/api-docs/data-grid/grid-row-grouping-api.json b/docs/pages/api-docs/data-grid/grid-row-grouping-api.json new file mode 100644 index 0000000000000..7d0865fc802a2 --- /dev/null +++ b/docs/pages/api-docs/data-grid/grid-row-grouping-api.json @@ -0,0 +1,26 @@ +{ + "name": "GridRowGroupingApi", + "description": "", + "properties": [ + { + "name": "addRowGroupingCriteria", + "description": "Adds the field to the row grouping model.", + "type": "(groupingCriteriaField: string, groupingIndex?: number) => void" + }, + { + "name": "removeRowGroupingCriteria", + "description": "sRemove the field from the row grouping model.", + "type": "(groupingCriteriaField: string) => void" + }, + { + "name": "setRowGroupingCriteriaIndex", + "description": "Sets the grouping index of a grouping criteria.", + "type": "(groupingCriteriaField: string, groupingIndex: number) => void" + }, + { + "name": "setRowGroupingModel", + "description": "Sets the columns to use as grouping criteria.", + "type": "(model: GridRowGroupingModel) => void" + } + ] +} diff --git a/docs/pages/api-docs/data-grid/selectors.json b/docs/pages/api-docs/data-grid/selectors.json index f3b49372df0aa..fdc17bd8b1d63 100644 --- a/docs/pages/api-docs/data-grid/selectors.json +++ b/docs/pages/api-docs/data-grid/selectors.json @@ -81,7 +81,18 @@ }, { "name": "gridResizingColumnFieldSelector", "returnType": "string", "description": "" }, { "name": "gridRowCountSelector", "returnType": "number", "description": "" }, + { + "name": "gridRowGroupingModelSelector", + "returnType": "GridRowGroupingModel", + "description": "" + }, { "name": "gridRowGroupingNameSelector", "returnType": "string", "description": "" }, + { "name": "gridRowGroupingSanitizedModelSelector", "returnType": "string[]", "description": "" }, + { + "name": "gridRowGroupingStateSelector", + "returnType": "GridRowGroupingState", + "description": "" + }, { "name": "gridRowIdsSelector", "returnType": "GridRowId[]", "description": "" }, { "name": "gridRowTreeDepthSelector", "returnType": "number", "description": "" }, { "name": "gridRowTreeSelector", "returnType": "GridRowTreeConfig", "description": "" }, diff --git a/docs/scripts/api/buildInterfacesDocumentation.ts b/docs/scripts/api/buildInterfacesDocumentation.ts index 37c250618a70a..582b0ff425b7f 100644 --- a/docs/scripts/api/buildInterfacesDocumentation.ts +++ b/docs/scripts/api/buildInterfacesDocumentation.ts @@ -43,6 +43,7 @@ const INTERFACES_WITH_DEDICATED_PAGES = [ 'GridCsvExportApi', 'GridScrollApi', 'GridEditRowApi', + 'GridRowGroupingApi', 'GridColumnPinningApi', 'GridPrintExportApi', 'GridDisableVirtualizationApi', diff --git a/docs/src/pages/components/data-grid/accessibility/accessibility.md b/docs/src/pages/components/data-grid/accessibility/accessibility.md index bc38a4c3b0dfe..96cd7b33c9567 100644 --- a/docs/src/pages/components/data-grid/accessibility/accessibility.md +++ b/docs/src/pages/components/data-grid/accessibility/accessibility.md @@ -49,19 +49,20 @@ The grid responds to keyboard interactions from the user and emits events when k Use the arrow keys to move the focus. -| Keys | Description | -| -----------------------------------------------------------------: | :-------------------------------------------- | -| Arrow Left | Navigate between cell elements | -| Arrow Bottom | Navigate between cell elements | -| Arrow Right | Navigate between cell elements | -| Arrow Up | Navigate between cell elements | -| Home | Navigate to the first cell of the current row | -| End | Navigate to the last cell of the current row | -| CTRL+Home | Navigate to the first cell of the first row | -| CTRL+End | Navigate to the last cell of the last row | -| Space | Navigate to the next scrollable page | -| Page Up | Navigate to the next scrollable page | -| Page Down | Navigate to the previous scrollable page | +| Keys | Description | +| -----------------------------------------------------------------: | :---------------------------------------------------------- | +| Arrow Left | Navigate between cell elements | +| Arrow Bottom | Navigate between cell elements | +| Arrow Right | Navigate between cell elements | +| Arrow Up | Navigate between cell elements | +| Home | Navigate to the first cell of the current row | +| End | Navigate to the last cell of the current row | +| CTRL+Home | Navigate to the first cell of the first row | +| CTRL+End | Navigate to the last cell of the last row | +| Space | Navigate to the next scrollable page | +| Page Up | Navigate to the next scrollable page | +| Page Down | Navigate to the previous scrollable page | +| Space | Toggle row children expansion when grouping cell is focused | ### Selection diff --git a/docs/src/pages/components/data-grid/events/events.json b/docs/src/pages/components/data-grid/events/events.json index e95e418cfd967..89c07bb5f2119 100644 --- a/docs/src/pages/components/data-grid/events/events.json +++ b/docs/src/pages/components/data-grid/events/events.json @@ -203,6 +203,12 @@ "params": "GridRowParams", "event": "MuiEvent" }, + { + "name": "rowGroupingModelChange", + "description": "Fired when the row grouping model changes.", + "params": "GridRowGroupingModel", + "event": "MuiEvent<{}>" + }, { "name": "rowMouseEnter", "description": "Fired when the mouse enters the row. Called with a GridRowParams object.", diff --git a/docs/src/pages/components/data-grid/group-pivot/DefaultGroupingExpansionDepthTreeData.js b/docs/src/pages/components/data-grid/group-pivot/DefaultGroupingExpansionDepthTreeData.js deleted file mode 100644 index 61a48100c6b66..0000000000000 --- a/docs/src/pages/components/data-grid/group-pivot/DefaultGroupingExpansionDepthTreeData.js +++ /dev/null @@ -1,121 +0,0 @@ -import * as React from 'react'; -import { DataGridPro } from '@mui/x-data-grid-pro'; - -const rows = [ - { - hierarchy: ['Sarah'], - jobTitle: 'Head of Human Resources', - recruitmentDate: new Date(2020, 8, 12), - id: 0, - }, - { - hierarchy: ['Thomas'], - jobTitle: 'Head of Sales', - recruitmentDate: new Date(2017, 3, 4), - id: 1, - }, - { - hierarchy: ['Thomas', 'Robert'], - jobTitle: 'Sales Person', - recruitmentDate: new Date(2020, 11, 20), - id: 2, - }, - { - hierarchy: ['Thomas', 'Karen'], - jobTitle: 'Sales Person', - recruitmentDate: new Date(2020, 10, 14), - id: 3, - }, - { - hierarchy: ['Thomas', 'Nancy'], - jobTitle: 'Sales Person', - recruitmentDate: new Date(2017, 10, 29), - id: 4, - }, - { - hierarchy: ['Thomas', 'Daniel'], - jobTitle: 'Sales Person', - recruitmentDate: new Date(2020, 7, 21), - id: 5, - }, - { - hierarchy: ['Thomas', 'Christopher'], - jobTitle: 'Sales Person', - recruitmentDate: new Date(2020, 7, 20), - id: 6, - }, - { - hierarchy: ['Thomas', 'Donald'], - jobTitle: 'Sales Person', - recruitmentDate: new Date(2019, 6, 28), - id: 7, - }, - { - hierarchy: ['Mary'], - jobTitle: 'Head of Engineering', - recruitmentDate: new Date(2016, 3, 14), - id: 8, - }, - { - hierarchy: ['Mary', 'Jennifer'], - jobTitle: 'Tech lead front', - recruitmentDate: new Date(2016, 5, 17), - id: 9, - }, - { - hierarchy: ['Mary', 'Jennifer', 'Anna'], - jobTitle: 'Front-end developer', - recruitmentDate: new Date(2019, 11, 7), - id: 10, - }, - { - hierarchy: ['Mary', 'Michael'], - jobTitle: 'Tech lead devops', - recruitmentDate: new Date(2021, 7, 1), - id: 11, - }, - { - hierarchy: ['Mary', 'Linda'], - jobTitle: 'Tech lead back', - recruitmentDate: new Date(2017, 0, 12), - id: 12, - }, - { - hierarchy: ['Mary', 'Linda', 'Elizabeth'], - jobTitle: 'Back-end developer', - recruitmentDate: new Date(2019, 2, 22), - id: 13, - }, - { - hierarchy: ['Mary', 'Linda', 'William'], - jobTitle: 'Back-end developer', - recruitmentDate: new Date(2018, 4, 19), - id: 14, - }, -]; - -const columns = [ - { field: 'jobTitle', headerName: 'Job Title', width: 200 }, - { - field: 'recruitmentDate', - headerName: 'Recruitment Date', - type: 'date', - width: 150, - }, -]; - -const getTreeDataPath = (row) => row.hierarchy; - -export default function DefaultGroupingExpansionDepthTreeData() { - return ( -
- -
- ); -} diff --git a/docs/src/pages/components/data-grid/group-pivot/DefaultGroupingExpansionDepthTreeData.tsx b/docs/src/pages/components/data-grid/group-pivot/DefaultGroupingExpansionDepthTreeData.tsx deleted file mode 100644 index 4b43a12797a74..0000000000000 --- a/docs/src/pages/components/data-grid/group-pivot/DefaultGroupingExpansionDepthTreeData.tsx +++ /dev/null @@ -1,121 +0,0 @@ -import * as React from 'react'; -import { DataGridPro, GridColumns, GridRowsProp } from '@mui/x-data-grid-pro'; - -const rows: GridRowsProp = [ - { - hierarchy: ['Sarah'], - jobTitle: 'Head of Human Resources', - recruitmentDate: new Date(2020, 8, 12), - id: 0, - }, - { - hierarchy: ['Thomas'], - jobTitle: 'Head of Sales', - recruitmentDate: new Date(2017, 3, 4), - id: 1, - }, - { - hierarchy: ['Thomas', 'Robert'], - jobTitle: 'Sales Person', - recruitmentDate: new Date(2020, 11, 20), - id: 2, - }, - { - hierarchy: ['Thomas', 'Karen'], - jobTitle: 'Sales Person', - recruitmentDate: new Date(2020, 10, 14), - id: 3, - }, - { - hierarchy: ['Thomas', 'Nancy'], - jobTitle: 'Sales Person', - recruitmentDate: new Date(2017, 10, 29), - id: 4, - }, - { - hierarchy: ['Thomas', 'Daniel'], - jobTitle: 'Sales Person', - recruitmentDate: new Date(2020, 7, 21), - id: 5, - }, - { - hierarchy: ['Thomas', 'Christopher'], - jobTitle: 'Sales Person', - recruitmentDate: new Date(2020, 7, 20), - id: 6, - }, - { - hierarchy: ['Thomas', 'Donald'], - jobTitle: 'Sales Person', - recruitmentDate: new Date(2019, 6, 28), - id: 7, - }, - { - hierarchy: ['Mary'], - jobTitle: 'Head of Engineering', - recruitmentDate: new Date(2016, 3, 14), - id: 8, - }, - { - hierarchy: ['Mary', 'Jennifer'], - jobTitle: 'Tech lead front', - recruitmentDate: new Date(2016, 5, 17), - id: 9, - }, - { - hierarchy: ['Mary', 'Jennifer', 'Anna'], - jobTitle: 'Front-end developer', - recruitmentDate: new Date(2019, 11, 7), - id: 10, - }, - { - hierarchy: ['Mary', 'Michael'], - jobTitle: 'Tech lead devops', - recruitmentDate: new Date(2021, 7, 1), - id: 11, - }, - { - hierarchy: ['Mary', 'Linda'], - jobTitle: 'Tech lead back', - recruitmentDate: new Date(2017, 0, 12), - id: 12, - }, - { - hierarchy: ['Mary', 'Linda', 'Elizabeth'], - jobTitle: 'Back-end developer', - recruitmentDate: new Date(2019, 2, 22), - id: 13, - }, - { - hierarchy: ['Mary', 'Linda', 'William'], - jobTitle: 'Back-end developer', - recruitmentDate: new Date(2018, 4, 19), - id: 14, - }, -]; - -const columns: GridColumns = [ - { field: 'jobTitle', headerName: 'Job Title', width: 200 }, - { - field: 'recruitmentDate', - headerName: 'Recruitment Date', - type: 'date', - width: 150, - }, -]; - -const getTreeDataPath = (row) => row.hierarchy; - -export default function DefaultGroupingExpansionDepthTreeData() { - return ( -
- -
- ); -} diff --git a/docs/src/pages/components/data-grid/group-pivot/DefaultGroupingExpansionDepthTreeData.tsx.preview b/docs/src/pages/components/data-grid/group-pivot/DefaultGroupingExpansionDepthTreeData.tsx.preview deleted file mode 100644 index e907316a9015d..0000000000000 --- a/docs/src/pages/components/data-grid/group-pivot/DefaultGroupingExpansionDepthTreeData.tsx.preview +++ /dev/null @@ -1,7 +0,0 @@ - \ No newline at end of file diff --git a/docs/src/pages/components/data-grid/group-pivot/RowGroupingApiNoSnap.js b/docs/src/pages/components/data-grid/group-pivot/RowGroupingApiNoSnap.js new file mode 100644 index 0000000000000..f48767cda16e1 --- /dev/null +++ b/docs/src/pages/components/data-grid/group-pivot/RowGroupingApiNoSnap.js @@ -0,0 +1,7 @@ +import React from 'react'; +import ApiDocs from 'docsx/src/modules/components/ApiDocs'; +import api from 'docsx/pages/api-docs/data-grid/grid-row-grouping-api.json'; + +export default function RowGroupingApiNoSnap() { + return ; +} diff --git a/docs/src/pages/components/data-grid/group-pivot/RowGroupingBasicExample.js b/docs/src/pages/components/data-grid/group-pivot/RowGroupingBasicExample.js new file mode 100644 index 0000000000000..dbbb7bd4160ad --- /dev/null +++ b/docs/src/pages/components/data-grid/group-pivot/RowGroupingBasicExample.js @@ -0,0 +1,65 @@ +import * as React from 'react'; +import { DataGridPro, GridEvents, useGridApiRef } from '@mui/x-data-grid-pro'; +import { useMovieData } from '@mui/x-data-grid-generator'; + +const INITIAL_GROUPING_COLUMN_MODEL = ['company']; + +const useKeepGroupingColumnsHidden = (apiRef, columns, initialModel, leafField) => { + const prevModel = React.useRef(initialModel); + + React.useEffect(() => { + apiRef.current.subscribeEvent(GridEvents.rowGroupingModelChange, (newModel) => { + apiRef.current.updateColumns([ + ...newModel + .filter((field) => !prevModel.current.includes(field)) + .map((field) => ({ field, hide: true })), + ...prevModel.current + .filter((field) => !newModel.includes(field)) + .map((field) => ({ field, hide: false })), + ]); + + prevModel.current = initialModel; + }); + }, [apiRef, initialModel]); + + return React.useMemo( + () => + columns.map((colDef) => + initialModel.includes(colDef.field) || + (leafField && colDef.field === leafField) + ? { ...colDef, hide: true } + : colDef, + ), + [columns, initialModel, leafField], + ); +}; + +export default function RowGroupingBasicExample() { + const data = useMovieData(); + const apiRef = useGridApiRef(); + + const columns = useKeepGroupingColumnsHidden( + apiRef, + data.columns, + INITIAL_GROUPING_COLUMN_MODEL, + ); + + return ( +
+ +
+ ); +} diff --git a/docs/src/pages/components/data-grid/group-pivot/RowGroupingBasicExample.tsx b/docs/src/pages/components/data-grid/group-pivot/RowGroupingBasicExample.tsx new file mode 100644 index 0000000000000..fd3136270e91e --- /dev/null +++ b/docs/src/pages/components/data-grid/group-pivot/RowGroupingBasicExample.tsx @@ -0,0 +1,76 @@ +import * as React from 'react'; +import { + DataGridPro, + GridApiRef, + GridColumns, + GridEvents, + GridRowGroupingModel, + useGridApiRef, +} from '@mui/x-data-grid-pro'; +import { useMovieData } from '@mui/x-data-grid-generator'; + +const INITIAL_GROUPING_COLUMN_MODEL = ['company']; + +const useKeepGroupingColumnsHidden = ( + apiRef: GridApiRef, + columns: GridColumns, + initialModel: GridRowGroupingModel, + leafField?: string, +) => { + const prevModel = React.useRef(initialModel); + + React.useEffect(() => { + apiRef.current.subscribeEvent(GridEvents.rowGroupingModelChange, (newModel) => { + apiRef.current.updateColumns([ + ...newModel + .filter((field) => !prevModel.current.includes(field)) + .map((field) => ({ field, hide: true })), + ...prevModel.current + .filter((field) => !newModel.includes(field)) + .map((field) => ({ field, hide: false })), + ]); + prevModel.current = initialModel; + }); + }, [apiRef, initialModel]); + + return React.useMemo( + () => + columns.map((colDef) => + initialModel.includes(colDef.field) || + (leafField && colDef.field === leafField) + ? { ...colDef, hide: true } + : colDef, + ), + [columns, initialModel, leafField], + ); +}; + +export default function RowGroupingBasicExample() { + const data = useMovieData(); + const apiRef = useGridApiRef(); + + const columns = useKeepGroupingColumnsHidden( + apiRef, + data.columns, + INITIAL_GROUPING_COLUMN_MODEL, + ); + + return ( +
+ +
+ ); +} diff --git a/docs/src/pages/components/data-grid/group-pivot/RowGroupingBasicExample.tsx.preview b/docs/src/pages/components/data-grid/group-pivot/RowGroupingBasicExample.tsx.preview new file mode 100644 index 0000000000000..3ebe5d5b5a813 --- /dev/null +++ b/docs/src/pages/components/data-grid/group-pivot/RowGroupingBasicExample.tsx.preview @@ -0,0 +1,14 @@ + \ No newline at end of file diff --git a/docs/src/pages/components/data-grid/group-pivot/RowGroupingColDefCanBeGrouped.js b/docs/src/pages/components/data-grid/group-pivot/RowGroupingColDefCanBeGrouped.js new file mode 100644 index 0000000000000..6d7be1765826e --- /dev/null +++ b/docs/src/pages/components/data-grid/group-pivot/RowGroupingColDefCanBeGrouped.js @@ -0,0 +1,73 @@ +import * as React from 'react'; +import { DataGridPro, GridEvents, useGridApiRef } from '@mui/x-data-grid-pro'; +import { useMovieData } from '@mui/x-data-grid-generator'; + +const INITIAL_GROUPING_COLUMN_MODEL = ['company']; + +const useKeepGroupingColumnsHidden = (apiRef, columns, initialModel, leafField) => { + const prevModel = React.useRef(initialModel); + + React.useEffect(() => { + apiRef.current.subscribeEvent(GridEvents.rowGroupingModelChange, (newModel) => { + apiRef.current.updateColumns([ + ...newModel + .filter((field) => !prevModel.current.includes(field)) + .map((field) => ({ field, hide: true })), + ...prevModel.current + .filter((field) => !newModel.includes(field)) + .map((field) => ({ field, hide: false })), + ]); + + prevModel.current = initialModel; + }); + }, [apiRef, initialModel]); + + return React.useMemo( + () => + columns.map((colDef) => + initialModel.includes(colDef.field) || + (leafField && colDef.field === leafField) + ? { ...colDef, hide: true } + : colDef, + ), + [columns, initialModel, leafField], + ); +}; + +export default function RowGroupingColDefCanBeGrouped() { + const data = useMovieData(); + const apiRef = useGridApiRef(); + + const columns = useKeepGroupingColumnsHidden( + apiRef, + data.columns, + INITIAL_GROUPING_COLUMN_MODEL, + ); + + const columnWithNoDirectorGroup = React.useMemo( + () => + columns.map((colDef) => + colDef.field === 'director' ? { ...colDef, groupable: false } : colDef, + ), + [columns], + ); + + return ( +
+ +
+ ); +} diff --git a/docs/src/pages/components/data-grid/group-pivot/RowGroupingColDefCanBeGrouped.tsx b/docs/src/pages/components/data-grid/group-pivot/RowGroupingColDefCanBeGrouped.tsx new file mode 100644 index 0000000000000..69e627e9f6f48 --- /dev/null +++ b/docs/src/pages/components/data-grid/group-pivot/RowGroupingColDefCanBeGrouped.tsx @@ -0,0 +1,84 @@ +import * as React from 'react'; +import { + DataGridPro, + GridApiRef, + GridColumns, + GridEvents, + GridRowGroupingModel, + useGridApiRef, +} from '@mui/x-data-grid-pro'; +import { useMovieData } from '@mui/x-data-grid-generator'; + +const INITIAL_GROUPING_COLUMN_MODEL = ['company']; + +const useKeepGroupingColumnsHidden = ( + apiRef: GridApiRef, + columns: GridColumns, + initialModel: GridRowGroupingModel, + leafField?: string, +) => { + const prevModel = React.useRef(initialModel); + + React.useEffect(() => { + apiRef.current.subscribeEvent(GridEvents.rowGroupingModelChange, (newModel) => { + apiRef.current.updateColumns([ + ...newModel + .filter((field) => !prevModel.current.includes(field)) + .map((field) => ({ field, hide: true })), + ...prevModel.current + .filter((field) => !newModel.includes(field)) + .map((field) => ({ field, hide: false })), + ]); + prevModel.current = initialModel; + }); + }, [apiRef, initialModel]); + + return React.useMemo( + () => + columns.map((colDef) => + initialModel.includes(colDef.field) || + (leafField && colDef.field === leafField) + ? { ...colDef, hide: true } + : colDef, + ), + [columns, initialModel, leafField], + ); +}; + +export default function RowGroupingColDefCanBeGrouped() { + const data = useMovieData(); + const apiRef = useGridApiRef(); + + const columns = useKeepGroupingColumnsHidden( + apiRef, + data.columns, + INITIAL_GROUPING_COLUMN_MODEL, + ); + + const columnWithNoDirectorGroup = React.useMemo( + () => + columns.map((colDef) => + colDef.field === 'director' ? { ...colDef, groupable: false } : colDef, + ), + [columns], + ); + + return ( +
+ +
+ ); +} diff --git a/docs/src/pages/components/data-grid/group-pivot/RowGroupingColDefCanBeGrouped.tsx.preview b/docs/src/pages/components/data-grid/group-pivot/RowGroupingColDefCanBeGrouped.tsx.preview new file mode 100644 index 0000000000000..a26c7811624ce --- /dev/null +++ b/docs/src/pages/components/data-grid/group-pivot/RowGroupingColDefCanBeGrouped.tsx.preview @@ -0,0 +1,14 @@ + \ No newline at end of file diff --git a/docs/src/pages/components/data-grid/group-pivot/RowGroupingControlled.js b/docs/src/pages/components/data-grid/group-pivot/RowGroupingControlled.js new file mode 100644 index 0000000000000..ad652179f6119 --- /dev/null +++ b/docs/src/pages/components/data-grid/group-pivot/RowGroupingControlled.js @@ -0,0 +1,28 @@ +import * as React from 'react'; +import { DataGridPro, useGridApiRef } from '@mui/x-data-grid-pro'; +import { useMovieData } from '@mui/x-data-grid-generator'; + +const INITIAL_GROUPING_COLUMN_MODEL = ['company', 'director']; + +export default function RowGroupingControlled() { + const data = useMovieData(); + const apiRef = useGridApiRef(); + + const [rowGroupingModel, setRowGroupingModel] = React.useState( + INITIAL_GROUPING_COLUMN_MODEL, + ); + + return ( +
+ setRowGroupingModel(model)} + experimentalFeatures={{ + rowGrouping: true, + }} + /> +
+ ); +} diff --git a/docs/src/pages/components/data-grid/group-pivot/RowGroupingControlled.tsx b/docs/src/pages/components/data-grid/group-pivot/RowGroupingControlled.tsx new file mode 100644 index 0000000000000..85bebb441a339 --- /dev/null +++ b/docs/src/pages/components/data-grid/group-pivot/RowGroupingControlled.tsx @@ -0,0 +1,31 @@ +import * as React from 'react'; +import { + DataGridPro, + GridRowGroupingModel, + useGridApiRef, +} from '@mui/x-data-grid-pro'; +import { useMovieData } from '@mui/x-data-grid-generator'; + +const INITIAL_GROUPING_COLUMN_MODEL = ['company', 'director']; + +export default function RowGroupingControlled() { + const data = useMovieData(); + const apiRef = useGridApiRef(); + + const [rowGroupingModel, setRowGroupingModel] = + React.useState(INITIAL_GROUPING_COLUMN_MODEL); + + return ( +
+ setRowGroupingModel(model)} + experimentalFeatures={{ + rowGrouping: true, + }} + /> +
+ ); +} diff --git a/docs/src/pages/components/data-grid/group-pivot/RowGroupingControlled.tsx.preview b/docs/src/pages/components/data-grid/group-pivot/RowGroupingControlled.tsx.preview new file mode 100644 index 0000000000000..4c9424ee2393f --- /dev/null +++ b/docs/src/pages/components/data-grid/group-pivot/RowGroupingControlled.tsx.preview @@ -0,0 +1,9 @@ + setRowGroupingModel(model)} + experimentalFeatures={{ + rowGrouping: true, + }} +/> \ No newline at end of file diff --git a/docs/src/pages/components/data-grid/group-pivot/RowGroupingCustomGroupingColDefCallback.js b/docs/src/pages/components/data-grid/group-pivot/RowGroupingCustomGroupingColDefCallback.js new file mode 100644 index 0000000000000..cb01ebdcd90c9 --- /dev/null +++ b/docs/src/pages/components/data-grid/group-pivot/RowGroupingCustomGroupingColDefCallback.js @@ -0,0 +1,97 @@ +import * as React from 'react'; +import { DataGridPro, GridEvents, useGridApiRef } from '@mui/x-data-grid-pro'; +import { useMovieData } from '@mui/x-data-grid-generator'; +import Stack from '@mui/material/Stack'; +import Chip from '@mui/material/Chip'; +import Box from '@mui/material/Box'; + +const INITIAL_GROUPING_COLUMN_MODEL = ['company', 'director']; + +const useKeepGroupingColumnsHidden = (apiRef, columns, initialModel, leafField) => { + const prevModel = React.useRef(initialModel); + + React.useEffect(() => { + apiRef.current.subscribeEvent(GridEvents.rowGroupingModelChange, (newModel) => { + apiRef.current.updateColumns([ + ...newModel + .filter((field) => !prevModel.current.includes(field)) + .map((field) => ({ field, hide: true })), + ...prevModel.current + .filter((field) => !newModel.includes(field)) + .map((field) => ({ field, hide: false })), + ]); + + prevModel.current = initialModel; + }); + }, [apiRef, initialModel]); + + return React.useMemo( + () => + columns.map((colDef) => + initialModel.includes(colDef.field) || + (leafField && colDef.field === leafField) + ? { ...colDef, hide: true } + : colDef, + ), + [columns, initialModel, leafField], + ); +}; + +export default function RowGroupingCustomGroupingColDefCallback() { + const data = useMovieData(); + const apiRef = useGridApiRef(); + const [rowGroupingModel, setRowGroupingModel] = React.useState( + INITIAL_GROUPING_COLUMN_MODEL, + ); + + const columns = useKeepGroupingColumnsHidden( + apiRef, + data.columns, + INITIAL_GROUPING_COLUMN_MODEL, + ); + + const rowGroupingModelStr = rowGroupingModel.join('-'); + + return ( +
+ + setRowGroupingModel(['company'])} + variant="outlined" + color={rowGroupingModelStr === 'company' ? 'primary' : undefined} + /> + setRowGroupingModel(['company', 'director'])} + variant="outlined" + color={rowGroupingModelStr === 'company-director' ? 'primary' : undefined} + /> + + + + params.fields.includes('director') + ? { + headerName: 'Director', + } + : {} + } + experimentalFeatures={{ + rowGrouping: true, + }} + /> + +
+ ); +} diff --git a/docs/src/pages/components/data-grid/group-pivot/RowGroupingCustomGroupingColDefCallback.tsx b/docs/src/pages/components/data-grid/group-pivot/RowGroupingCustomGroupingColDefCallback.tsx new file mode 100644 index 0000000000000..5df29e6313175 --- /dev/null +++ b/docs/src/pages/components/data-grid/group-pivot/RowGroupingCustomGroupingColDefCallback.tsx @@ -0,0 +1,108 @@ +import * as React from 'react'; +import { + DataGridPro, + GridApiRef, + GridColumns, + GridEvents, + GridRowGroupingModel, + useGridApiRef, +} from '@mui/x-data-grid-pro'; +import { useMovieData } from '@mui/x-data-grid-generator'; +import Stack from '@mui/material/Stack'; +import Chip from '@mui/material/Chip'; +import Box from '@mui/material/Box'; + +const INITIAL_GROUPING_COLUMN_MODEL = ['company', 'director']; + +const useKeepGroupingColumnsHidden = ( + apiRef: GridApiRef, + columns: GridColumns, + initialModel: GridRowGroupingModel, + leafField?: string, +) => { + const prevModel = React.useRef(initialModel); + + React.useEffect(() => { + apiRef.current.subscribeEvent(GridEvents.rowGroupingModelChange, (newModel) => { + apiRef.current.updateColumns([ + ...newModel + .filter((field) => !prevModel.current.includes(field)) + .map((field) => ({ field, hide: true })), + ...prevModel.current + .filter((field) => !newModel.includes(field)) + .map((field) => ({ field, hide: false })), + ]); + prevModel.current = initialModel; + }); + }, [apiRef, initialModel]); + + return React.useMemo( + () => + columns.map((colDef) => + initialModel.includes(colDef.field) || + (leafField && colDef.field === leafField) + ? { ...colDef, hide: true } + : colDef, + ), + [columns, initialModel, leafField], + ); +}; + +export default function RowGroupingCustomGroupingColDefCallback() { + const data = useMovieData(); + const apiRef = useGridApiRef(); + const [rowGroupingModel, setRowGroupingModel] = React.useState( + INITIAL_GROUPING_COLUMN_MODEL, + ); + + const columns = useKeepGroupingColumnsHidden( + apiRef, + data.columns, + INITIAL_GROUPING_COLUMN_MODEL, + ); + + const rowGroupingModelStr = rowGroupingModel.join('-'); + + return ( +
+ + setRowGroupingModel(['company'])} + variant="outlined" + color={rowGroupingModelStr === 'company' ? 'primary' : undefined} + /> + setRowGroupingModel(['company', 'director'])} + variant="outlined" + color={rowGroupingModelStr === 'company-director' ? 'primary' : undefined} + /> + + + + params.fields.includes('director') + ? { + headerName: 'Director', + } + : {} + } + experimentalFeatures={{ + rowGrouping: true, + }} + /> + +
+ ); +} diff --git a/docs/src/pages/components/data-grid/group-pivot/RowGroupingCustomGroupingColDefObject.js b/docs/src/pages/components/data-grid/group-pivot/RowGroupingCustomGroupingColDefObject.js new file mode 100644 index 0000000000000..f246b7122a8c1 --- /dev/null +++ b/docs/src/pages/components/data-grid/group-pivot/RowGroupingCustomGroupingColDefObject.js @@ -0,0 +1,68 @@ +import * as React from 'react'; +import { DataGridPro, GridEvents, useGridApiRef } from '@mui/x-data-grid-pro'; +import { useMovieData } from '@mui/x-data-grid-generator'; + +const INITIAL_GROUPING_COLUMN_MODEL = ['company', 'director']; + +const useKeepGroupingColumnsHidden = (apiRef, columns, initialModel, leafField) => { + const prevModel = React.useRef(initialModel); + + React.useEffect(() => { + apiRef.current.subscribeEvent(GridEvents.rowGroupingModelChange, (newModel) => { + apiRef.current.updateColumns([ + ...newModel + .filter((field) => !prevModel.current.includes(field)) + .map((field) => ({ field, hide: true })), + ...prevModel.current + .filter((field) => !newModel.includes(field)) + .map((field) => ({ field, hide: false })), + ]); + + prevModel.current = initialModel; + }); + }, [apiRef, initialModel]); + + return React.useMemo( + () => + columns.map((colDef) => + initialModel.includes(colDef.field) || + (leafField && colDef.field === leafField) + ? { ...colDef, hide: true } + : colDef, + ), + [columns, initialModel, leafField], + ); +}; + +export default function RowGroupingCustomGroupingColDefObject() { + const data = useMovieData(); + const apiRef = useGridApiRef(); + + const columns = useKeepGroupingColumnsHidden( + apiRef, + data.columns, + INITIAL_GROUPING_COLUMN_MODEL, + ); + + return ( +
+ +
+ ); +} diff --git a/docs/src/pages/components/data-grid/group-pivot/RowGroupingCustomGroupingColDefObject.tsx b/docs/src/pages/components/data-grid/group-pivot/RowGroupingCustomGroupingColDefObject.tsx new file mode 100644 index 0000000000000..3d1bff2c2c297 --- /dev/null +++ b/docs/src/pages/components/data-grid/group-pivot/RowGroupingCustomGroupingColDefObject.tsx @@ -0,0 +1,79 @@ +import * as React from 'react'; +import { + DataGridPro, + GridApiRef, + GridColumns, + GridEvents, + GridRowGroupingModel, + useGridApiRef, +} from '@mui/x-data-grid-pro'; +import { useMovieData } from '@mui/x-data-grid-generator'; + +const INITIAL_GROUPING_COLUMN_MODEL = ['company', 'director']; + +const useKeepGroupingColumnsHidden = ( + apiRef: GridApiRef, + columns: GridColumns, + initialModel: GridRowGroupingModel, + leafField?: string, +) => { + const prevModel = React.useRef(initialModel); + + React.useEffect(() => { + apiRef.current.subscribeEvent(GridEvents.rowGroupingModelChange, (newModel) => { + apiRef.current.updateColumns([ + ...newModel + .filter((field) => !prevModel.current.includes(field)) + .map((field) => ({ field, hide: true })), + ...prevModel.current + .filter((field) => !newModel.includes(field)) + .map((field) => ({ field, hide: false })), + ]); + prevModel.current = initialModel; + }); + }, [apiRef, initialModel]); + + return React.useMemo( + () => + columns.map((colDef) => + initialModel.includes(colDef.field) || + (leafField && colDef.field === leafField) + ? { ...colDef, hide: true } + : colDef, + ), + [columns, initialModel, leafField], + ); +}; + +export default function RowGroupingCustomGroupingColDefObject() { + const data = useMovieData(); + const apiRef = useGridApiRef(); + + const columns = useKeepGroupingColumnsHidden( + apiRef, + data.columns, + INITIAL_GROUPING_COLUMN_MODEL, + ); + + return ( +
+ +
+ ); +} diff --git a/docs/src/pages/components/data-grid/group-pivot/RowGroupingDefaultExpansionDepth.js b/docs/src/pages/components/data-grid/group-pivot/RowGroupingDefaultExpansionDepth.js new file mode 100644 index 0000000000000..26e21f4e0002e --- /dev/null +++ b/docs/src/pages/components/data-grid/group-pivot/RowGroupingDefaultExpansionDepth.js @@ -0,0 +1,66 @@ +import * as React from 'react'; +import { DataGridPro, GridEvents, useGridApiRef } from '@mui/x-data-grid-pro'; +import { useMovieData } from '@mui/x-data-grid-generator'; + +const INITIAL_GROUPING_COLUMN_MODEL = ['company', 'director']; + +const useKeepGroupingColumnsHidden = (apiRef, columns, initialModel, leafField) => { + const prevModel = React.useRef(initialModel); + + React.useEffect(() => { + apiRef.current.subscribeEvent(GridEvents.rowGroupingModelChange, (newModel) => { + apiRef.current.updateColumns([ + ...newModel + .filter((field) => !prevModel.current.includes(field)) + .map((field) => ({ field, hide: true })), + ...prevModel.current + .filter((field) => !newModel.includes(field)) + .map((field) => ({ field, hide: false })), + ]); + + prevModel.current = initialModel; + }); + }, [apiRef, initialModel]); + + return React.useMemo( + () => + columns.map((colDef) => + initialModel.includes(colDef.field) || + (leafField && colDef.field === leafField) + ? { ...colDef, hide: true } + : colDef, + ), + [columns, initialModel, leafField], + ); +}; + +export default function RowGroupingDefaultExpansionDepth() { + const data = useMovieData(); + const apiRef = useGridApiRef(); + + const columns = useKeepGroupingColumnsHidden( + apiRef, + data.columns, + INITIAL_GROUPING_COLUMN_MODEL, + ); + + return ( +
+ +
+ ); +} diff --git a/docs/src/pages/components/data-grid/group-pivot/RowGroupingDefaultExpansionDepth.tsx b/docs/src/pages/components/data-grid/group-pivot/RowGroupingDefaultExpansionDepth.tsx new file mode 100644 index 0000000000000..c66763ef4f779 --- /dev/null +++ b/docs/src/pages/components/data-grid/group-pivot/RowGroupingDefaultExpansionDepth.tsx @@ -0,0 +1,77 @@ +import * as React from 'react'; +import { + DataGridPro, + GridApiRef, + GridColumns, + GridEvents, + GridRowGroupingModel, + useGridApiRef, +} from '@mui/x-data-grid-pro'; +import { useMovieData } from '@mui/x-data-grid-generator'; + +const INITIAL_GROUPING_COLUMN_MODEL = ['company', 'director']; + +const useKeepGroupingColumnsHidden = ( + apiRef: GridApiRef, + columns: GridColumns, + initialModel: GridRowGroupingModel, + leafField?: string, +) => { + const prevModel = React.useRef(initialModel); + + React.useEffect(() => { + apiRef.current.subscribeEvent(GridEvents.rowGroupingModelChange, (newModel) => { + apiRef.current.updateColumns([ + ...newModel + .filter((field) => !prevModel.current.includes(field)) + .map((field) => ({ field, hide: true })), + ...prevModel.current + .filter((field) => !newModel.includes(field)) + .map((field) => ({ field, hide: false })), + ]); + prevModel.current = initialModel; + }); + }, [apiRef, initialModel]); + + return React.useMemo( + () => + columns.map((colDef) => + initialModel.includes(colDef.field) || + (leafField && colDef.field === leafField) + ? { ...colDef, hide: true } + : colDef, + ), + [columns, initialModel, leafField], + ); +}; + +export default function RowGroupingDefaultExpansionDepth() { + const data = useMovieData(); + const apiRef = useGridApiRef(); + + const columns = useKeepGroupingColumnsHidden( + apiRef, + data.columns, + INITIAL_GROUPING_COLUMN_MODEL, + ); + + return ( +
+ +
+ ); +} diff --git a/docs/src/pages/components/data-grid/group-pivot/RowGroupingDefaultExpansionDepth.tsx.preview b/docs/src/pages/components/data-grid/group-pivot/RowGroupingDefaultExpansionDepth.tsx.preview new file mode 100644 index 0000000000000..c4d72475b08ce --- /dev/null +++ b/docs/src/pages/components/data-grid/group-pivot/RowGroupingDefaultExpansionDepth.tsx.preview @@ -0,0 +1,15 @@ + \ No newline at end of file diff --git a/docs/src/pages/components/data-grid/group-pivot/RowGroupingDisabled.js b/docs/src/pages/components/data-grid/group-pivot/RowGroupingDisabled.js new file mode 100644 index 0000000000000..6e63ab094d572 --- /dev/null +++ b/docs/src/pages/components/data-grid/group-pivot/RowGroupingDisabled.js @@ -0,0 +1,20 @@ +import * as React from 'react'; +import { DataGridPro } from '@mui/x-data-grid-pro'; +import { useMovieData } from '@mui/x-data-grid-generator'; + +export default function RowGroupingDisabled() { + const data = useMovieData(); + + return ( +
+ +
+ ); +} diff --git a/docs/src/pages/components/data-grid/group-pivot/RowGroupingDisabled.tsx b/docs/src/pages/components/data-grid/group-pivot/RowGroupingDisabled.tsx new file mode 100644 index 0000000000000..6e63ab094d572 --- /dev/null +++ b/docs/src/pages/components/data-grid/group-pivot/RowGroupingDisabled.tsx @@ -0,0 +1,20 @@ +import * as React from 'react'; +import { DataGridPro } from '@mui/x-data-grid-pro'; +import { useMovieData } from '@mui/x-data-grid-generator'; + +export default function RowGroupingDisabled() { + const data = useMovieData(); + + return ( +
+ +
+ ); +} diff --git a/docs/src/pages/components/data-grid/group-pivot/RowGroupingDisabled.tsx.preview b/docs/src/pages/components/data-grid/group-pivot/RowGroupingDisabled.tsx.preview new file mode 100644 index 0000000000000..fdada4b2514bb --- /dev/null +++ b/docs/src/pages/components/data-grid/group-pivot/RowGroupingDisabled.tsx.preview @@ -0,0 +1,8 @@ + \ No newline at end of file diff --git a/docs/src/pages/components/data-grid/group-pivot/RowGroupingFullExample.js b/docs/src/pages/components/data-grid/group-pivot/RowGroupingFullExample.js new file mode 100644 index 0000000000000..6c56567d23d61 --- /dev/null +++ b/docs/src/pages/components/data-grid/group-pivot/RowGroupingFullExample.js @@ -0,0 +1,77 @@ +import { useDemoData } from '@mui/x-data-grid-generator'; +import * as React from 'react'; +import { DataGridPro, GridEvents, useGridApiRef } from '@mui/x-data-grid-pro'; + +const INITIAL_GROUPING_COLUMN_MODEL = ['commodity']; + +const useKeepGroupingColumnsHidden = (apiRef, columns, initialModel, leafField) => { + const prevModel = React.useRef(initialModel); + + React.useEffect(() => { + apiRef.current.subscribeEvent(GridEvents.rowGroupingModelChange, (newModel) => { + apiRef.current.updateColumns([ + ...newModel + .filter((field) => !prevModel.current.includes(field)) + .map((field) => ({ field, hide: true })), + ...prevModel.current + .filter((field) => !newModel.includes(field)) + .map((field) => ({ field, hide: false })), + ]); + + prevModel.current = initialModel; + }); + }, [apiRef, initialModel]); + + return React.useMemo( + () => + columns.map((colDef) => + initialModel.includes(colDef.field) || + (leafField && colDef.field === leafField) + ? { ...colDef, hide: true } + : colDef, + ), + [columns, initialModel, leafField], + ); +}; + +export default function RowGroupingFullExample() { + const { data, loading } = useDemoData({ + dataSet: 'Commodity', + rowLength: 100, + maxColumns: 25, + }); + + const apiRef = useGridApiRef(); + + const columns = useKeepGroupingColumnsHidden( + apiRef, + data.columns, + INITIAL_GROUPING_COLUMN_MODEL, + ); + + return ( +
+ +
+ ); +} diff --git a/docs/src/pages/components/data-grid/group-pivot/RowGroupingFullExample.tsx b/docs/src/pages/components/data-grid/group-pivot/RowGroupingFullExample.tsx new file mode 100644 index 0000000000000..48a9c5d775909 --- /dev/null +++ b/docs/src/pages/components/data-grid/group-pivot/RowGroupingFullExample.tsx @@ -0,0 +1,87 @@ +import { useDemoData } from '@mui/x-data-grid-generator'; +import * as React from 'react'; +import { + DataGridPro, + GridApiRef, + GridColumns, + GridEvents, + GridRowGroupingModel, + useGridApiRef, +} from '@mui/x-data-grid-pro'; + +const INITIAL_GROUPING_COLUMN_MODEL = ['commodity']; + +const useKeepGroupingColumnsHidden = ( + apiRef: GridApiRef, + columns: GridColumns, + initialModel: GridRowGroupingModel, + leafField?: string, +) => { + const prevModel = React.useRef(initialModel); + + React.useEffect(() => { + apiRef.current.subscribeEvent(GridEvents.rowGroupingModelChange, (newModel) => { + apiRef.current.updateColumns([ + ...newModel + .filter((field) => !prevModel.current.includes(field)) + .map((field) => ({ field, hide: true })), + ...prevModel.current + .filter((field) => !newModel.includes(field)) + .map((field) => ({ field, hide: false })), + ]); + prevModel.current = initialModel; + }); + }, [apiRef, initialModel]); + + return React.useMemo( + () => + columns.map((colDef) => + initialModel.includes(colDef.field) || + (leafField && colDef.field === leafField) + ? { ...colDef, hide: true } + : colDef, + ), + [columns, initialModel, leafField], + ); +}; + +export default function RowGroupingFullExample() { + const { data, loading } = useDemoData({ + dataSet: 'Commodity', + rowLength: 100, + maxColumns: 25, + }); + const apiRef = useGridApiRef(); + + const columns = useKeepGroupingColumnsHidden( + apiRef, + data.columns, + INITIAL_GROUPING_COLUMN_MODEL, + ); + + return ( +
+ +
+ ); +} diff --git a/docs/src/pages/components/data-grid/group-pivot/RowGroupingGroupingValueGetter.js b/docs/src/pages/components/data-grid/group-pivot/RowGroupingGroupingValueGetter.js new file mode 100644 index 0000000000000..98c2ea6044117 --- /dev/null +++ b/docs/src/pages/components/data-grid/group-pivot/RowGroupingGroupingValueGetter.js @@ -0,0 +1,85 @@ +import * as React from 'react'; +import { DataGridPro, GridEvents, useGridApiRef } from '@mui/x-data-grid-pro'; +import { useMovieData } from '@mui/x-data-grid-generator'; + +const INITIAL_GROUPING_COLUMN_MODEL = ['composer', 'decade']; + +const useKeepGroupingColumnsHidden = (apiRef, columns, initialModel, leafField) => { + const prevModel = React.useRef(initialModel); + + React.useEffect(() => { + apiRef.current.subscribeEvent(GridEvents.rowGroupingModelChange, (newModel) => { + apiRef.current.updateColumns([ + ...newModel + .filter((field) => !prevModel.current.includes(field)) + .map((field) => ({ field, hide: true })), + ...prevModel.current + .filter((field) => !newModel.includes(field)) + .map((field) => ({ field, hide: false })), + ]); + + prevModel.current = initialModel; + }); + }, [apiRef, initialModel]); + + return React.useMemo( + () => + columns.map((colDef) => + initialModel.includes(colDef.field) || + (leafField && colDef.field === leafField) + ? { ...colDef, hide: true } + : colDef, + ), + [columns, initialModel, leafField], + ); +}; + +export default function RowGroupingGroupingValueGetter() { + const data = useMovieData(); + const apiRef = useGridApiRef(); + + const columnsWithComposer = React.useMemo( + () => [ + ...data.columns, + { + field: 'composer', + headerName: 'Composer', + renderCell: (params) => params.value?.name, + groupingValueGetter: (params) => params.value.name, + width: 200, + }, + { + field: 'decade', + headerName: 'Decade', + valueGetter: (params) => Math.floor(params.row.year / 10) * 10, + groupingValueGetter: (params) => Math.floor(params.row.year / 10) * 10, + renderCell: (params) => `${params.value.toString().slice(-2)}'s`, + }, + ], + [data.columns], + ); + + const columns = useKeepGroupingColumnsHidden( + apiRef, + columnsWithComposer, + INITIAL_GROUPING_COLUMN_MODEL, + ); + + return ( +
+ +
+ ); +} diff --git a/docs/src/pages/components/data-grid/group-pivot/RowGroupingGroupingValueGetter.tsx b/docs/src/pages/components/data-grid/group-pivot/RowGroupingGroupingValueGetter.tsx new file mode 100644 index 0000000000000..8cc0e1f76eb4e --- /dev/null +++ b/docs/src/pages/components/data-grid/group-pivot/RowGroupingGroupingValueGetter.tsx @@ -0,0 +1,103 @@ +import * as React from 'react'; +import { + DataGridPro, + GridApiRef, + GridColumns, + GridEvents, + GridRowGroupingModel, + GridGroupingValueGetterParams, + GridRenderCellParams, + useGridApiRef, +} from '@mui/x-data-grid-pro'; +import { useMovieData } from '@mui/x-data-grid-generator'; + +const INITIAL_GROUPING_COLUMN_MODEL = ['composer', 'decade']; + +const useKeepGroupingColumnsHidden = ( + apiRef: GridApiRef, + columns: GridColumns, + initialModel: GridRowGroupingModel, + leafField?: string, +) => { + const prevModel = React.useRef(initialModel); + + React.useEffect(() => { + apiRef.current.subscribeEvent(GridEvents.rowGroupingModelChange, (newModel) => { + apiRef.current.updateColumns([ + ...newModel + .filter((field) => !prevModel.current.includes(field)) + .map((field) => ({ field, hide: true })), + ...prevModel.current + .filter((field) => !newModel.includes(field)) + .map((field) => ({ field, hide: false })), + ]); + prevModel.current = initialModel; + }); + }, [apiRef, initialModel]); + + return React.useMemo( + () => + columns.map((colDef) => + initialModel.includes(colDef.field) || + (leafField && colDef.field === leafField) + ? { ...colDef, hide: true } + : colDef, + ), + [columns, initialModel, leafField], + ); +}; + +export default function RowGroupingGroupingValueGetter() { + const data = useMovieData(); + const apiRef = useGridApiRef(); + + const columnsWithComposer = React.useMemo( + () => [ + ...data.columns, + { + field: 'composer', + headerName: 'Composer', + renderCell: (params: GridRenderCellParams<{ name: string } | undefined>) => + params.value?.name, + groupingValueGetter: ( + params: GridGroupingValueGetterParams<{ name: string }>, + ) => params.value.name, + width: 200, + }, + { + field: 'decade', + headerName: 'Decade', + valueGetter: (params): number => Math.floor(params.row.year / 10) * 10, + groupingValueGetter: (params): number => + Math.floor(params.row.year / 10) * 10, + renderCell: (params: GridRenderCellParams) => + `${params.value.toString().slice(-2)}'s`, + }, + ], + [data.columns], + ); + + const columns = useKeepGroupingColumnsHidden( + apiRef, + columnsWithComposer, + INITIAL_GROUPING_COLUMN_MODEL, + ); + + return ( +
+ +
+ ); +} diff --git a/docs/src/pages/components/data-grid/group-pivot/RowGroupingGroupingValueGetter.tsx.preview b/docs/src/pages/components/data-grid/group-pivot/RowGroupingGroupingValueGetter.tsx.preview new file mode 100644 index 0000000000000..d99ba502224bd --- /dev/null +++ b/docs/src/pages/components/data-grid/group-pivot/RowGroupingGroupingValueGetter.tsx.preview @@ -0,0 +1,13 @@ + \ No newline at end of file diff --git a/docs/src/pages/components/data-grid/group-pivot/RowGroupingHideDescendantCount.js b/docs/src/pages/components/data-grid/group-pivot/RowGroupingHideDescendantCount.js new file mode 100644 index 0000000000000..55750bb9e9fd9 --- /dev/null +++ b/docs/src/pages/components/data-grid/group-pivot/RowGroupingHideDescendantCount.js @@ -0,0 +1,68 @@ +import * as React from 'react'; +import { DataGridPro, GridEvents, useGridApiRef } from '@mui/x-data-grid-pro'; +import { useMovieData } from '@mui/x-data-grid-generator'; + +const INITIAL_GROUPING_COLUMN_MODEL = ['company']; + +const useKeepGroupingColumnsHidden = (apiRef, columns, initialModel, leafField) => { + const prevModel = React.useRef(initialModel); + + React.useEffect(() => { + apiRef.current.subscribeEvent(GridEvents.rowGroupingModelChange, (newModel) => { + apiRef.current.updateColumns([ + ...newModel + .filter((field) => !prevModel.current.includes(field)) + .map((field) => ({ field, hide: true })), + ...prevModel.current + .filter((field) => !newModel.includes(field)) + .map((field) => ({ field, hide: false })), + ]); + + prevModel.current = initialModel; + }); + }, [apiRef, initialModel]); + + return React.useMemo( + () => + columns.map((colDef) => + initialModel.includes(colDef.field) || + (leafField && colDef.field === leafField) + ? { ...colDef, hide: true } + : colDef, + ), + [columns, initialModel, leafField], + ); +}; + +export default function RowGroupingHideDescendantCount() { + const data = useMovieData(); + const apiRef = useGridApiRef(); + + const columns = useKeepGroupingColumnsHidden( + apiRef, + data.columns, + INITIAL_GROUPING_COLUMN_MODEL, + ); + + return ( +
+ +
+ ); +} diff --git a/docs/src/pages/components/data-grid/group-pivot/RowGroupingHideDescendantCount.tsx b/docs/src/pages/components/data-grid/group-pivot/RowGroupingHideDescendantCount.tsx new file mode 100644 index 0000000000000..370027695b95a --- /dev/null +++ b/docs/src/pages/components/data-grid/group-pivot/RowGroupingHideDescendantCount.tsx @@ -0,0 +1,79 @@ +import * as React from 'react'; +import { + DataGridPro, + GridApiRef, + GridColumns, + GridEvents, + GridRowGroupingModel, + useGridApiRef, +} from '@mui/x-data-grid-pro'; +import { useMovieData } from '@mui/x-data-grid-generator'; + +const INITIAL_GROUPING_COLUMN_MODEL = ['company']; + +const useKeepGroupingColumnsHidden = ( + apiRef: GridApiRef, + columns: GridColumns, + initialModel: GridRowGroupingModel, + leafField?: string, +) => { + const prevModel = React.useRef(initialModel); + + React.useEffect(() => { + apiRef.current.subscribeEvent(GridEvents.rowGroupingModelChange, (newModel) => { + apiRef.current.updateColumns([ + ...newModel + .filter((field) => !prevModel.current.includes(field)) + .map((field) => ({ field, hide: true })), + ...prevModel.current + .filter((field) => !newModel.includes(field)) + .map((field) => ({ field, hide: false })), + ]); + prevModel.current = initialModel; + }); + }, [apiRef, initialModel]); + + return React.useMemo( + () => + columns.map((colDef) => + initialModel.includes(colDef.field) || + (leafField && colDef.field === leafField) + ? { ...colDef, hide: true } + : colDef, + ), + [columns, initialModel, leafField], + ); +}; + +export default function RowGroupingHideDescendantCount() { + const data = useMovieData(); + const apiRef = useGridApiRef(); + + const columns = useKeepGroupingColumnsHidden( + apiRef, + data.columns, + INITIAL_GROUPING_COLUMN_MODEL, + ); + + return ( +
+ +
+ ); +} diff --git a/docs/src/pages/components/data-grid/group-pivot/RowGroupingInitialState.js b/docs/src/pages/components/data-grid/group-pivot/RowGroupingInitialState.js new file mode 100644 index 0000000000000..70cbb5a571b40 --- /dev/null +++ b/docs/src/pages/components/data-grid/group-pivot/RowGroupingInitialState.js @@ -0,0 +1,28 @@ +import * as React from 'react'; +import { DataGridPro, useGridApiRef } from '@mui/x-data-grid-pro'; +import { useMovieData } from '@mui/x-data-grid-generator'; + +const INITIAL_GROUPING_COLUMN_MODEL = ['company', 'director']; + +export default function RowGroupingInitialState() { + const data = useMovieData(); + const apiRef = useGridApiRef(); + + return ( +
+ +
+ ); +} diff --git a/docs/src/pages/components/data-grid/group-pivot/RowGroupingInitialState.tsx b/docs/src/pages/components/data-grid/group-pivot/RowGroupingInitialState.tsx new file mode 100644 index 0000000000000..70cbb5a571b40 --- /dev/null +++ b/docs/src/pages/components/data-grid/group-pivot/RowGroupingInitialState.tsx @@ -0,0 +1,28 @@ +import * as React from 'react'; +import { DataGridPro, useGridApiRef } from '@mui/x-data-grid-pro'; +import { useMovieData } from '@mui/x-data-grid-generator'; + +const INITIAL_GROUPING_COLUMN_MODEL = ['company', 'director']; + +export default function RowGroupingInitialState() { + const data = useMovieData(); + const apiRef = useGridApiRef(); + + return ( +
+ +
+ ); +} diff --git a/docs/src/pages/components/data-grid/group-pivot/RowGroupingInitialState.tsx.preview b/docs/src/pages/components/data-grid/group-pivot/RowGroupingInitialState.tsx.preview new file mode 100644 index 0000000000000..97fdf5f5d8301 --- /dev/null +++ b/docs/src/pages/components/data-grid/group-pivot/RowGroupingInitialState.tsx.preview @@ -0,0 +1,13 @@ + \ No newline at end of file diff --git a/docs/src/pages/components/data-grid/group-pivot/RowGroupingIsGroupExpandedByDefault.js b/docs/src/pages/components/data-grid/group-pivot/RowGroupingIsGroupExpandedByDefault.js new file mode 100644 index 0000000000000..f7ec8e2a86b8e --- /dev/null +++ b/docs/src/pages/components/data-grid/group-pivot/RowGroupingIsGroupExpandedByDefault.js @@ -0,0 +1,69 @@ +import * as React from 'react'; +import { DataGridPro, GridEvents, useGridApiRef } from '@mui/x-data-grid-pro'; +import { useMovieData } from '@mui/x-data-grid-generator'; + +const INITIAL_GROUPING_COLUMN_MODEL = ['company', 'director']; + +const useKeepGroupingColumnsHidden = (apiRef, columns, initialModel, leafField) => { + const prevModel = React.useRef(initialModel); + + React.useEffect(() => { + apiRef.current.subscribeEvent(GridEvents.rowGroupingModelChange, (newModel) => { + apiRef.current.updateColumns([ + ...newModel + .filter((field) => !prevModel.current.includes(field)) + .map((field) => ({ field, hide: true })), + ...prevModel.current + .filter((field) => !newModel.includes(field)) + .map((field) => ({ field, hide: false })), + ]); + + prevModel.current = initialModel; + }); + }, [apiRef, initialModel]); + + return React.useMemo( + () => + columns.map((colDef) => + initialModel.includes(colDef.field) || + (leafField && colDef.field === leafField) + ? { ...colDef, hide: true } + : colDef, + ), + [columns, initialModel, leafField], + ); +}; + +const isGroupExpandedByDefault = (node) => + node.groupingField === 'company' && node.groupingKey === '20th Century Fox'; + +export default function RowGroupingIsGroupExpandedByDefault() { + const data = useMovieData(); + const apiRef = useGridApiRef(); + + const columns = useKeepGroupingColumnsHidden( + apiRef, + data.columns, + INITIAL_GROUPING_COLUMN_MODEL, + ); + + return ( +
+ +
+ ); +} diff --git a/docs/src/pages/components/data-grid/group-pivot/RowGroupingIsGroupExpandedByDefault.tsx b/docs/src/pages/components/data-grid/group-pivot/RowGroupingIsGroupExpandedByDefault.tsx new file mode 100644 index 0000000000000..f7083df510200 --- /dev/null +++ b/docs/src/pages/components/data-grid/group-pivot/RowGroupingIsGroupExpandedByDefault.tsx @@ -0,0 +1,81 @@ +import * as React from 'react'; +import { + DataGridPro, + GridApiRef, + GridColumns, + GridEvents, + GridRowGroupingModel, + GridRowTreeNodeConfig, + useGridApiRef, +} from '@mui/x-data-grid-pro'; +import { useMovieData } from '@mui/x-data-grid-generator'; + +const INITIAL_GROUPING_COLUMN_MODEL = ['company', 'director']; + +const useKeepGroupingColumnsHidden = ( + apiRef: GridApiRef, + columns: GridColumns, + initialModel: GridRowGroupingModel, + leafField?: string, +) => { + const prevModel = React.useRef(initialModel); + + React.useEffect(() => { + apiRef.current.subscribeEvent(GridEvents.rowGroupingModelChange, (newModel) => { + apiRef.current.updateColumns([ + ...newModel + .filter((field) => !prevModel.current.includes(field)) + .map((field) => ({ field, hide: true })), + ...prevModel.current + .filter((field) => !newModel.includes(field)) + .map((field) => ({ field, hide: false })), + ]); + prevModel.current = initialModel; + }); + }, [apiRef, initialModel]); + + return React.useMemo( + () => + columns.map((colDef) => + initialModel.includes(colDef.field) || + (leafField && colDef.field === leafField) + ? { ...colDef, hide: true } + : colDef, + ), + [columns, initialModel, leafField], + ); +}; + +const isGroupExpandedByDefault = (node: GridRowTreeNodeConfig) => + node.groupingField === 'company' && node.groupingKey === '20th Century Fox'; + +export default function RowGroupingIsGroupExpandedByDefault() { + const data = useMovieData(); + const apiRef = useGridApiRef(); + + const columns = useKeepGroupingColumnsHidden( + apiRef, + data.columns, + INITIAL_GROUPING_COLUMN_MODEL, + ); + + return ( +
+ +
+ ); +} diff --git a/docs/src/pages/components/data-grid/group-pivot/RowGroupingIsGroupExpandedByDefault.tsx.preview b/docs/src/pages/components/data-grid/group-pivot/RowGroupingIsGroupExpandedByDefault.tsx.preview new file mode 100644 index 0000000000000..6ae78aa78205d --- /dev/null +++ b/docs/src/pages/components/data-grid/group-pivot/RowGroupingIsGroupExpandedByDefault.tsx.preview @@ -0,0 +1,15 @@ + \ No newline at end of file diff --git a/docs/src/pages/components/data-grid/group-pivot/RowGroupingLeafWithValue.js b/docs/src/pages/components/data-grid/group-pivot/RowGroupingLeafWithValue.js new file mode 100644 index 0000000000000..305be779e1931 --- /dev/null +++ b/docs/src/pages/components/data-grid/group-pivot/RowGroupingLeafWithValue.js @@ -0,0 +1,66 @@ +import * as React from 'react'; +import { DataGridPro, GridEvents, useGridApiRef } from '@mui/x-data-grid-pro'; +import { useMovieData } from '@mui/x-data-grid-generator'; + +const INITIAL_GROUPING_COLUMN_MODEL = ['company']; + +const useKeepGroupingColumnsHidden = (apiRef, columns, initialModel, leafField) => { + const prevModel = React.useRef(initialModel); + + React.useEffect(() => { + apiRef.current.subscribeEvent(GridEvents.rowGroupingModelChange, (newModel) => { + apiRef.current.updateColumns([ + ...newModel + .filter((field) => !prevModel.current.includes(field)) + .map((field) => ({ field, hide: true })), + ...prevModel.current + .filter((field) => !newModel.includes(field)) + .map((field) => ({ field, hide: false })), + ]); + + prevModel.current = initialModel; + }); + }, [apiRef, initialModel]); + + return React.useMemo( + () => + columns.map((colDef) => + initialModel.includes(colDef.field) || + (leafField && colDef.field === leafField) + ? { ...colDef, hide: true } + : colDef, + ), + [columns, initialModel, leafField], + ); +}; + +export default function RowGroupingLeafWithValue() { + const data = useMovieData(); + const apiRef = useGridApiRef(); + + const columns = useKeepGroupingColumnsHidden( + apiRef, + data.columns, + INITIAL_GROUPING_COLUMN_MODEL, + 'title', + ); + + return ( +
+ +
+ ); +} diff --git a/docs/src/pages/components/data-grid/group-pivot/RowGroupingLeafWithValue.tsx b/docs/src/pages/components/data-grid/group-pivot/RowGroupingLeafWithValue.tsx new file mode 100644 index 0000000000000..98d9a2fa99deb --- /dev/null +++ b/docs/src/pages/components/data-grid/group-pivot/RowGroupingLeafWithValue.tsx @@ -0,0 +1,77 @@ +import * as React from 'react'; +import { + DataGridPro, + GridApiRef, + GridColumns, + GridEvents, + GridRowGroupingModel, + useGridApiRef, +} from '@mui/x-data-grid-pro'; +import { useMovieData } from '@mui/x-data-grid-generator'; + +const INITIAL_GROUPING_COLUMN_MODEL = ['company']; + +const useKeepGroupingColumnsHidden = ( + apiRef: GridApiRef, + columns: GridColumns, + initialModel: GridRowGroupingModel, + leafField?: string, +) => { + const prevModel = React.useRef(initialModel); + + React.useEffect(() => { + apiRef.current.subscribeEvent(GridEvents.rowGroupingModelChange, (newModel) => { + apiRef.current.updateColumns([ + ...newModel + .filter((field) => !prevModel.current.includes(field)) + .map((field) => ({ field, hide: true })), + ...prevModel.current + .filter((field) => !newModel.includes(field)) + .map((field) => ({ field, hide: false })), + ]); + prevModel.current = initialModel; + }); + }, [apiRef, initialModel]); + + return React.useMemo( + () => + columns.map((colDef) => + initialModel.includes(colDef.field) || + (leafField && colDef.field === leafField) + ? { ...colDef, hide: true } + : colDef, + ), + [columns, initialModel, leafField], + ); +}; + +export default function RowGroupingLeafWithValue() { + const data = useMovieData(); + const apiRef = useGridApiRef(); + + const columns = useKeepGroupingColumnsHidden( + apiRef, + data.columns, + INITIAL_GROUPING_COLUMN_MODEL, + 'title', + ); + + return ( +
+ +
+ ); +} diff --git a/docs/src/pages/components/data-grid/group-pivot/RowGroupingLeafWithValue.tsx.preview b/docs/src/pages/components/data-grid/group-pivot/RowGroupingLeafWithValue.tsx.preview new file mode 100644 index 0000000000000..409eb7b562312 --- /dev/null +++ b/docs/src/pages/components/data-grid/group-pivot/RowGroupingLeafWithValue.tsx.preview @@ -0,0 +1,14 @@ + \ No newline at end of file diff --git a/docs/src/pages/components/data-grid/group-pivot/RowGroupingMultipleGroupingCol.js b/docs/src/pages/components/data-grid/group-pivot/RowGroupingMultipleGroupingCol.js new file mode 100644 index 0000000000000..20fecd466dbc3 --- /dev/null +++ b/docs/src/pages/components/data-grid/group-pivot/RowGroupingMultipleGroupingCol.js @@ -0,0 +1,65 @@ +import * as React from 'react'; +import { DataGridPro, GridEvents, useGridApiRef } from '@mui/x-data-grid-pro'; +import { useMovieData } from '@mui/x-data-grid-generator'; + +const INITIAL_GROUPING_COLUMN_MODEL = ['company', 'director']; + +const useKeepGroupingColumnsHidden = (apiRef, columns, initialModel, leafField) => { + const prevModel = React.useRef(initialModel); + + React.useEffect(() => { + apiRef.current.subscribeEvent(GridEvents.rowGroupingModelChange, (newModel) => { + apiRef.current.updateColumns([ + ...newModel + .filter((field) => !prevModel.current.includes(field)) + .map((field) => ({ field, hide: true })), + ...prevModel.current + .filter((field) => !newModel.includes(field)) + .map((field) => ({ field, hide: false })), + ]); + + prevModel.current = initialModel; + }); + }, [apiRef, initialModel]); + + return React.useMemo( + () => + columns.map((colDef) => + initialModel.includes(colDef.field) || + (leafField && colDef.field === leafField) + ? { ...colDef, hide: true } + : colDef, + ), + [columns, initialModel, leafField], + ); +}; + +export default function RowGroupingMultipleGroupingCol() { + const data = useMovieData(); + const apiRef = useGridApiRef(); + + const columns = useKeepGroupingColumnsHidden( + apiRef, + data.columns, + INITIAL_GROUPING_COLUMN_MODEL, + ); + + return ( +
+ +
+ ); +} diff --git a/docs/src/pages/components/data-grid/group-pivot/RowGroupingMultipleGroupingCol.tsx b/docs/src/pages/components/data-grid/group-pivot/RowGroupingMultipleGroupingCol.tsx new file mode 100644 index 0000000000000..20513d7b80d6d --- /dev/null +++ b/docs/src/pages/components/data-grid/group-pivot/RowGroupingMultipleGroupingCol.tsx @@ -0,0 +1,76 @@ +import * as React from 'react'; +import { + DataGridPro, + GridApiRef, + GridColumns, + GridEvents, + GridRowGroupingModel, + useGridApiRef, +} from '@mui/x-data-grid-pro'; +import { useMovieData } from '@mui/x-data-grid-generator'; + +const INITIAL_GROUPING_COLUMN_MODEL = ['company', 'director']; + +const useKeepGroupingColumnsHidden = ( + apiRef: GridApiRef, + columns: GridColumns, + initialModel: GridRowGroupingModel, + leafField?: string, +) => { + const prevModel = React.useRef(initialModel); + + React.useEffect(() => { + apiRef.current.subscribeEvent(GridEvents.rowGroupingModelChange, (newModel) => { + apiRef.current.updateColumns([ + ...newModel + .filter((field) => !prevModel.current.includes(field)) + .map((field) => ({ field, hide: true })), + ...prevModel.current + .filter((field) => !newModel.includes(field)) + .map((field) => ({ field, hide: false })), + ]); + prevModel.current = initialModel; + }); + }, [apiRef, initialModel]); + + return React.useMemo( + () => + columns.map((colDef) => + initialModel.includes(colDef.field) || + (leafField && colDef.field === leafField) + ? { ...colDef, hide: true } + : colDef, + ), + [columns, initialModel, leafField], + ); +}; + +export default function RowGroupingMultipleGroupingCol() { + const data = useMovieData(); + const apiRef = useGridApiRef(); + + const columns = useKeepGroupingColumnsHidden( + apiRef, + data.columns, + INITIAL_GROUPING_COLUMN_MODEL, + ); + + return ( +
+ +
+ ); +} diff --git a/docs/src/pages/components/data-grid/group-pivot/RowGroupingMultipleGroupingCol.tsx.preview b/docs/src/pages/components/data-grid/group-pivot/RowGroupingMultipleGroupingCol.tsx.preview new file mode 100644 index 0000000000000..867f47cf95155 --- /dev/null +++ b/docs/src/pages/components/data-grid/group-pivot/RowGroupingMultipleGroupingCol.tsx.preview @@ -0,0 +1,14 @@ + \ No newline at end of file diff --git a/docs/src/pages/components/data-grid/group-pivot/RowGroupingRowsWithMissingGroups.js b/docs/src/pages/components/data-grid/group-pivot/RowGroupingRowsWithMissingGroups.js new file mode 100644 index 0000000000000..524f67f7afd01 --- /dev/null +++ b/docs/src/pages/components/data-grid/group-pivot/RowGroupingRowsWithMissingGroups.js @@ -0,0 +1,65 @@ +import * as React from 'react'; +import { DataGridPro, GridEvents, useGridApiRef } from '@mui/x-data-grid-pro'; +import { useMovieData } from '@mui/x-data-grid-generator'; + +const INITIAL_GROUPING_COLUMN_MODEL = ['cinematicUniverse']; + +const useKeepGroupingColumnsHidden = (apiRef, columns, initialModel, leafField) => { + const prevModel = React.useRef(initialModel); + + React.useEffect(() => { + apiRef.current.subscribeEvent(GridEvents.rowGroupingModelChange, (newModel) => { + apiRef.current.updateColumns([ + ...newModel + .filter((field) => !prevModel.current.includes(field)) + .map((field) => ({ field, hide: true })), + ...prevModel.current + .filter((field) => !newModel.includes(field)) + .map((field) => ({ field, hide: false })), + ]); + + prevModel.current = initialModel; + }); + }, [apiRef, initialModel]); + + return React.useMemo( + () => + columns.map((colDef) => + initialModel.includes(colDef.field) || + (leafField && colDef.field === leafField) + ? { ...colDef, hide: true } + : colDef, + ), + [columns, initialModel, leafField], + ); +}; + +export default function RowGroupingRowsWithMissingGroups() { + const data = useMovieData(); + const apiRef = useGridApiRef(); + + const columns = useKeepGroupingColumnsHidden( + apiRef, + data.columns, + INITIAL_GROUPING_COLUMN_MODEL, + ); + + return ( +
+ +
+ ); +} diff --git a/docs/src/pages/components/data-grid/group-pivot/RowGroupingRowsWithMissingGroups.tsx b/docs/src/pages/components/data-grid/group-pivot/RowGroupingRowsWithMissingGroups.tsx new file mode 100644 index 0000000000000..13e4bb0575b88 --- /dev/null +++ b/docs/src/pages/components/data-grid/group-pivot/RowGroupingRowsWithMissingGroups.tsx @@ -0,0 +1,76 @@ +import * as React from 'react'; +import { + DataGridPro, + GridApiRef, + GridColumns, + GridEvents, + GridRowGroupingModel, + useGridApiRef, +} from '@mui/x-data-grid-pro'; +import { useMovieData } from '@mui/x-data-grid-generator'; + +const INITIAL_GROUPING_COLUMN_MODEL = ['cinematicUniverse']; + +const useKeepGroupingColumnsHidden = ( + apiRef: GridApiRef, + columns: GridColumns, + initialModel: GridRowGroupingModel, + leafField?: string, +) => { + const prevModel = React.useRef(initialModel); + + React.useEffect(() => { + apiRef.current.subscribeEvent(GridEvents.rowGroupingModelChange, (newModel) => { + apiRef.current.updateColumns([ + ...newModel + .filter((field) => !prevModel.current.includes(field)) + .map((field) => ({ field, hide: true })), + ...prevModel.current + .filter((field) => !newModel.includes(field)) + .map((field) => ({ field, hide: false })), + ]); + prevModel.current = initialModel; + }); + }, [apiRef, initialModel]); + + return React.useMemo( + () => + columns.map((colDef) => + initialModel.includes(colDef.field) || + (leafField && colDef.field === leafField) + ? { ...colDef, hide: true } + : colDef, + ), + [columns, initialModel, leafField], + ); +}; + +export default function RowGroupingRowsWithMissingGroups() { + const data = useMovieData(); + const apiRef = useGridApiRef(); + + const columns = useKeepGroupingColumnsHidden( + apiRef, + data.columns, + INITIAL_GROUPING_COLUMN_MODEL, + ); + + return ( +
+ +
+ ); +} diff --git a/docs/src/pages/components/data-grid/group-pivot/RowGroupingRowsWithMissingGroups.tsx.preview b/docs/src/pages/components/data-grid/group-pivot/RowGroupingRowsWithMissingGroups.tsx.preview new file mode 100644 index 0000000000000..4239560aa49d4 --- /dev/null +++ b/docs/src/pages/components/data-grid/group-pivot/RowGroupingRowsWithMissingGroups.tsx.preview @@ -0,0 +1,14 @@ + \ No newline at end of file diff --git a/docs/src/pages/components/data-grid/group-pivot/RowGroupingSetChildrenExpansion.js b/docs/src/pages/components/data-grid/group-pivot/RowGroupingSetChildrenExpansion.js new file mode 100644 index 0000000000000..2ff6b08f1fda9 --- /dev/null +++ b/docs/src/pages/components/data-grid/group-pivot/RowGroupingSetChildrenExpansion.js @@ -0,0 +1,87 @@ +import * as React from 'react'; +import { + DataGridPro, + GridEvents, + gridVisibleSortedRowIdsSelector, + useGridApiRef, +} from '@mui/x-data-grid-pro'; +import { useMovieData } from '@mui/x-data-grid-generator'; +import Stack from '@mui/material/Stack'; +import Button from '@mui/material/Button'; + +const INITIAL_GROUPING_COLUMN_MODEL = ['company']; + +const useKeepGroupingColumnsHidden = (apiRef, columns, initialModel, leafField) => { + const prevModel = React.useRef(initialModel); + + React.useEffect(() => { + apiRef.current.subscribeEvent(GridEvents.rowGroupingModelChange, (newModel) => { + apiRef.current.updateColumns([ + ...newModel + .filter((field) => !prevModel.current.includes(field)) + .map((field) => ({ field, hide: true })), + ...prevModel.current + .filter((field) => !newModel.includes(field)) + .map((field) => ({ field, hide: false })), + ]); + + prevModel.current = initialModel; + }); + }, [apiRef, initialModel]); + + return React.useMemo( + () => + columns.map((colDef) => + initialModel.includes(colDef.field) || + (leafField && colDef.field === leafField) + ? { ...colDef, hide: true } + : colDef, + ), + [columns, initialModel, leafField], + ); +}; + +export default function RowGroupingSetChildrenExpansion() { + const data = useMovieData(); + const apiRef = useGridApiRef(); + + const columns = useKeepGroupingColumnsHidden( + apiRef, + data.columns, + INITIAL_GROUPING_COLUMN_MODEL, + ); + + const toggleSecondRow = () => { + const rowIds = gridVisibleSortedRowIdsSelector(apiRef.current.state); + + if (rowIds.length > 1) { + const rowId = rowIds[1]; + apiRef.current.setRowChildrenExpansion( + rowId, + !apiRef.current.getRowNode(rowId)?.childrenExpanded, + ); + } + }; + + return ( + + +
+ +
+
+ ); +} diff --git a/docs/src/pages/components/data-grid/group-pivot/RowGroupingSetChildrenExpansion.tsx b/docs/src/pages/components/data-grid/group-pivot/RowGroupingSetChildrenExpansion.tsx new file mode 100644 index 0000000000000..b5a24e16c4084 --- /dev/null +++ b/docs/src/pages/components/data-grid/group-pivot/RowGroupingSetChildrenExpansion.tsx @@ -0,0 +1,94 @@ +import * as React from 'react'; +import { + DataGridPro, + GridApiRef, + GridColumns, + GridEvents, + GridRowGroupingModel, + gridVisibleSortedRowIdsSelector, + useGridApiRef, +} from '@mui/x-data-grid-pro'; +import { useMovieData } from '@mui/x-data-grid-generator'; +import Stack from '@mui/material/Stack'; +import Button from '@mui/material/Button'; + +const INITIAL_GROUPING_COLUMN_MODEL = ['company']; + +const useKeepGroupingColumnsHidden = ( + apiRef: GridApiRef, + columns: GridColumns, + initialModel: GridRowGroupingModel, + leafField?: string, +) => { + const prevModel = React.useRef(initialModel); + + React.useEffect(() => { + apiRef.current.subscribeEvent(GridEvents.rowGroupingModelChange, (newModel) => { + apiRef.current.updateColumns([ + ...newModel + .filter((field) => !prevModel.current.includes(field)) + .map((field) => ({ field, hide: true })), + ...prevModel.current + .filter((field) => !newModel.includes(field)) + .map((field) => ({ field, hide: false })), + ]); + prevModel.current = initialModel; + }); + }, [apiRef, initialModel]); + + return React.useMemo( + () => + columns.map((colDef) => + initialModel.includes(colDef.field) || + (leafField && colDef.field === leafField) + ? { ...colDef, hide: true } + : colDef, + ), + [columns, initialModel, leafField], + ); +}; + +export default function RowGroupingSetChildrenExpansion() { + const data = useMovieData(); + const apiRef = useGridApiRef(); + + const columns = useKeepGroupingColumnsHidden( + apiRef, + data.columns, + INITIAL_GROUPING_COLUMN_MODEL, + ); + + const toggleSecondRow = () => { + const rowIds = gridVisibleSortedRowIdsSelector(apiRef.current.state); + + if (rowIds.length > 1) { + const rowId = rowIds[1]; + apiRef.current.setRowChildrenExpansion( + rowId, + !apiRef.current.getRowNode(rowId)?.childrenExpanded, + ); + } + }; + + return ( + + +
+ +
+
+ ); +} diff --git a/docs/src/pages/components/data-grid/group-pivot/RowGroupingSingleGroupingCol.js b/docs/src/pages/components/data-grid/group-pivot/RowGroupingSingleGroupingCol.js new file mode 100644 index 0000000000000..d1a287d20afdd --- /dev/null +++ b/docs/src/pages/components/data-grid/group-pivot/RowGroupingSingleGroupingCol.js @@ -0,0 +1,65 @@ +import * as React from 'react'; +import { DataGridPro, GridEvents, useGridApiRef } from '@mui/x-data-grid-pro'; +import { useMovieData } from '@mui/x-data-grid-generator'; + +const INITIAL_GROUPING_COLUMN_MODEL = ['company', 'director']; + +const useKeepGroupingColumnsHidden = (apiRef, columns, initialModel, leafField) => { + const prevModel = React.useRef(initialModel); + + React.useEffect(() => { + apiRef.current.subscribeEvent(GridEvents.rowGroupingModelChange, (newModel) => { + apiRef.current.updateColumns([ + ...newModel + .filter((field) => !prevModel.current.includes(field)) + .map((field) => ({ field, hide: true })), + ...prevModel.current + .filter((field) => !newModel.includes(field)) + .map((field) => ({ field, hide: false })), + ]); + + prevModel.current = initialModel; + }); + }, [apiRef, initialModel]); + + return React.useMemo( + () => + columns.map((colDef) => + initialModel.includes(colDef.field) || + (leafField && colDef.field === leafField) + ? { ...colDef, hide: true } + : colDef, + ), + [columns, initialModel, leafField], + ); +}; + +export default function RowGroupingSingleGroupingCol() { + const data = useMovieData(); + const apiRef = useGridApiRef(); + + const columns = useKeepGroupingColumnsHidden( + apiRef, + data.columns, + INITIAL_GROUPING_COLUMN_MODEL, + ); + + return ( +
+ +
+ ); +} diff --git a/docs/src/pages/components/data-grid/group-pivot/RowGroupingSingleGroupingCol.tsx b/docs/src/pages/components/data-grid/group-pivot/RowGroupingSingleGroupingCol.tsx new file mode 100644 index 0000000000000..2f2548da65b43 --- /dev/null +++ b/docs/src/pages/components/data-grid/group-pivot/RowGroupingSingleGroupingCol.tsx @@ -0,0 +1,76 @@ +import * as React from 'react'; +import { + DataGridPro, + GridApiRef, + GridColumns, + GridEvents, + GridRowGroupingModel, + useGridApiRef, +} from '@mui/x-data-grid-pro'; +import { useMovieData } from '@mui/x-data-grid-generator'; + +const INITIAL_GROUPING_COLUMN_MODEL = ['company', 'director']; + +const useKeepGroupingColumnsHidden = ( + apiRef: GridApiRef, + columns: GridColumns, + initialModel: GridRowGroupingModel, + leafField?: string, +) => { + const prevModel = React.useRef(initialModel); + + React.useEffect(() => { + apiRef.current.subscribeEvent(GridEvents.rowGroupingModelChange, (newModel) => { + apiRef.current.updateColumns([ + ...newModel + .filter((field) => !prevModel.current.includes(field)) + .map((field) => ({ field, hide: true })), + ...prevModel.current + .filter((field) => !newModel.includes(field)) + .map((field) => ({ field, hide: false })), + ]); + prevModel.current = initialModel; + }); + }, [apiRef, initialModel]); + + return React.useMemo( + () => + columns.map((colDef) => + initialModel.includes(colDef.field) || + (leafField && colDef.field === leafField) + ? { ...colDef, hide: true } + : colDef, + ), + [columns, initialModel, leafField], + ); +}; + +export default function RowGroupingSingleGroupingCol() { + const data = useMovieData(); + const apiRef = useGridApiRef(); + + const columns = useKeepGroupingColumnsHidden( + apiRef, + data.columns, + INITIAL_GROUPING_COLUMN_MODEL, + ); + + return ( +
+ +
+ ); +} diff --git a/docs/src/pages/components/data-grid/group-pivot/RowGroupingSingleGroupingCol.tsx.preview b/docs/src/pages/components/data-grid/group-pivot/RowGroupingSingleGroupingCol.tsx.preview new file mode 100644 index 0000000000000..3ebe5d5b5a813 --- /dev/null +++ b/docs/src/pages/components/data-grid/group-pivot/RowGroupingSingleGroupingCol.tsx.preview @@ -0,0 +1,14 @@ + \ No newline at end of file diff --git a/docs/src/pages/components/data-grid/group-pivot/RowGroupingSortingMultipleGroupingColDef.js b/docs/src/pages/components/data-grid/group-pivot/RowGroupingSortingMultipleGroupingColDef.js new file mode 100644 index 0000000000000..4f91a497ef1d8 --- /dev/null +++ b/docs/src/pages/components/data-grid/group-pivot/RowGroupingSortingMultipleGroupingColDef.js @@ -0,0 +1,75 @@ +import * as React from 'react'; +import { DataGridPro, GridEvents, useGridApiRef } from '@mui/x-data-grid-pro'; +import { useMovieData } from '@mui/x-data-grid-generator'; + +const INITIAL_GROUPING_COLUMN_MODEL = ['company', 'director']; + +const useKeepGroupingColumnsHidden = (apiRef, columns, initialModel, leafField) => { + const prevModel = React.useRef(initialModel); + + React.useEffect(() => { + apiRef.current.subscribeEvent(GridEvents.rowGroupingModelChange, (newModel) => { + apiRef.current.updateColumns([ + ...newModel + .filter((field) => !prevModel.current.includes(field)) + .map((field) => ({ field, hide: true })), + ...prevModel.current + .filter((field) => !newModel.includes(field)) + .map((field) => ({ field, hide: false })), + ]); + + prevModel.current = initialModel; + }); + }, [apiRef, initialModel]); + + return React.useMemo( + () => + columns.map((colDef) => + initialModel.includes(colDef.field) || + (leafField && colDef.field === leafField) + ? { ...colDef, hide: true } + : colDef, + ), + [columns, initialModel, leafField], + ); +}; + +export default function RowGroupingSortingMultipleGroupingColDef() { + const data = useMovieData(); + const apiRef = useGridApiRef(); + + const columns = useKeepGroupingColumnsHidden( + apiRef, + data.columns, + INITIAL_GROUPING_COLUMN_MODEL, + ); + + return ( +
+ + params.fields.includes('director') + ? { + leafField: 'title', + mainGroupingCriteria: 'director', + } + : {} + } + experimentalFeatures={{ + rowGrouping: true, + }} + /> +
+ ); +} diff --git a/docs/src/pages/components/data-grid/group-pivot/RowGroupingSortingMultipleGroupingColDef.tsx b/docs/src/pages/components/data-grid/group-pivot/RowGroupingSortingMultipleGroupingColDef.tsx new file mode 100644 index 0000000000000..eaed26ecd704a --- /dev/null +++ b/docs/src/pages/components/data-grid/group-pivot/RowGroupingSortingMultipleGroupingColDef.tsx @@ -0,0 +1,86 @@ +import * as React from 'react'; +import { + DataGridPro, + GridApiRef, + GridColumns, + GridEvents, + GridRowGroupingModel, + useGridApiRef, +} from '@mui/x-data-grid-pro'; +import { useMovieData } from '@mui/x-data-grid-generator'; + +const INITIAL_GROUPING_COLUMN_MODEL = ['company', 'director']; + +const useKeepGroupingColumnsHidden = ( + apiRef: GridApiRef, + columns: GridColumns, + initialModel: GridRowGroupingModel, + leafField?: string, +) => { + const prevModel = React.useRef(initialModel); + + React.useEffect(() => { + apiRef.current.subscribeEvent(GridEvents.rowGroupingModelChange, (newModel) => { + apiRef.current.updateColumns([ + ...newModel + .filter((field) => !prevModel.current.includes(field)) + .map((field) => ({ field, hide: true })), + ...prevModel.current + .filter((field) => !newModel.includes(field)) + .map((field) => ({ field, hide: false })), + ]); + prevModel.current = initialModel; + }); + }, [apiRef, initialModel]); + + return React.useMemo( + () => + columns.map((colDef) => + initialModel.includes(colDef.field) || + (leafField && colDef.field === leafField) + ? { ...colDef, hide: true } + : colDef, + ), + [columns, initialModel, leafField], + ); +}; + +export default function RowGroupingSortingMultipleGroupingColDef() { + const data = useMovieData(); + const apiRef = useGridApiRef(); + + const columns = useKeepGroupingColumnsHidden( + apiRef, + data.columns, + INITIAL_GROUPING_COLUMN_MODEL, + ); + + return ( +
+ + params.fields.includes('director') + ? { + leafField: 'title', + mainGroupingCriteria: 'director', + } + : {} + } + experimentalFeatures={{ + rowGrouping: true, + }} + /> +
+ ); +} diff --git a/docs/src/pages/components/data-grid/group-pivot/RowGroupingSortingSingleGroupingColDef.js b/docs/src/pages/components/data-grid/group-pivot/RowGroupingSortingSingleGroupingColDef.js new file mode 100644 index 0000000000000..8c90b0cf91077 --- /dev/null +++ b/docs/src/pages/components/data-grid/group-pivot/RowGroupingSortingSingleGroupingColDef.js @@ -0,0 +1,102 @@ +import * as React from 'react'; +import { DataGridPro, GridEvents, useGridApiRef } from '@mui/x-data-grid-pro'; +import { useMovieData } from '@mui/x-data-grid-generator'; +import Stack from '@mui/material/Stack'; +import MenuItem from '@mui/material/MenuItem'; +import FormControl from '@mui/material/FormControl'; +import InputLabel from '@mui/material/InputLabel'; +import Select from '@mui/material/Select'; + +const INITIAL_GROUPING_COLUMN_MODEL = ['company', 'director']; + +const useKeepGroupingColumnsHidden = (apiRef, columns, initialModel, leafField) => { + const prevModel = React.useRef(initialModel); + + React.useEffect(() => { + apiRef.current.subscribeEvent(GridEvents.rowGroupingModelChange, (newModel) => { + apiRef.current.updateColumns([ + ...newModel + .filter((field) => !prevModel.current.includes(field)) + .map((field) => ({ field, hide: true })), + ...prevModel.current + .filter((field) => !newModel.includes(field)) + .map((field) => ({ field, hide: false })), + ]); + + prevModel.current = initialModel; + }); + }, [apiRef, initialModel]); + + return React.useMemo( + () => + columns.map((colDef) => + initialModel.includes(colDef.field) || + (leafField && colDef.field === leafField) + ? { ...colDef, hide: true } + : colDef, + ), + [columns, initialModel, leafField], + ); +}; + +export default function RowGroupingSortingSingleGroupingColDef() { + const data = useMovieData(); + const [mainGroupingCriteria, setMainGroupingCriteria] = + React.useState('undefined'); + + const apiRef = useGridApiRef(); + + const columns = useKeepGroupingColumnsHidden( + apiRef, + data.columns, + INITIAL_GROUPING_COLUMN_MODEL, + ); + + return ( + + + + Main Grouping Criteria + + + +
+ +
+
+ ); +} diff --git a/docs/src/pages/components/data-grid/group-pivot/RowGroupingSortingSingleGroupingColDef.tsx b/docs/src/pages/components/data-grid/group-pivot/RowGroupingSortingSingleGroupingColDef.tsx new file mode 100644 index 0000000000000..726b06fe7aed7 --- /dev/null +++ b/docs/src/pages/components/data-grid/group-pivot/RowGroupingSortingSingleGroupingColDef.tsx @@ -0,0 +1,113 @@ +import * as React from 'react'; +import { + DataGridPro, + GridApiRef, + GridColumns, + GridEvents, + GridRowGroupingModel, + useGridApiRef, +} from '@mui/x-data-grid-pro'; +import { useMovieData } from '@mui/x-data-grid-generator'; +import Stack from '@mui/material/Stack'; +import MenuItem from '@mui/material/MenuItem'; +import FormControl from '@mui/material/FormControl'; +import InputLabel from '@mui/material/InputLabel'; +import Select from '@mui/material/Select'; + +const INITIAL_GROUPING_COLUMN_MODEL = ['company', 'director']; + +const useKeepGroupingColumnsHidden = ( + apiRef: GridApiRef, + columns: GridColumns, + initialModel: GridRowGroupingModel, + leafField?: string, +) => { + const prevModel = React.useRef(initialModel); + + React.useEffect(() => { + apiRef.current.subscribeEvent(GridEvents.rowGroupingModelChange, (newModel) => { + apiRef.current.updateColumns([ + ...newModel + .filter((field) => !prevModel.current.includes(field)) + .map((field) => ({ field, hide: true })), + ...prevModel.current + .filter((field) => !newModel.includes(field)) + .map((field) => ({ field, hide: false })), + ]); + prevModel.current = initialModel; + }); + }, [apiRef, initialModel]); + + return React.useMemo( + () => + columns.map((colDef) => + initialModel.includes(colDef.field) || + (leafField && colDef.field === leafField) + ? { ...colDef, hide: true } + : colDef, + ), + [columns, initialModel, leafField], + ); +}; + +export default function RowGroupingSortingSingleGroupingColDef() { + const data = useMovieData(); + const [mainGroupingCriteria, setMainGroupingCriteria] = + React.useState('undefined'); + + const apiRef = useGridApiRef(); + + const columns = useKeepGroupingColumnsHidden( + apiRef, + data.columns, + INITIAL_GROUPING_COLUMN_MODEL, + ); + + return ( + + + + Main Grouping Criteria + + + +
+ +
+
+ ); +} diff --git a/docs/src/pages/components/data-grid/group-pivot/SetRowExpansionTreeData.js b/docs/src/pages/components/data-grid/group-pivot/SetRowExpansionTreeData.js deleted file mode 100644 index 25a936898a2ec..0000000000000 --- a/docs/src/pages/components/data-grid/group-pivot/SetRowExpansionTreeData.js +++ /dev/null @@ -1,145 +0,0 @@ -import * as React from 'react'; -import { - DataGridPro, - useGridApiRef, - gridVisibleSortedRowIdsSelector, -} from '@mui/x-data-grid-pro'; -import Stack from '@mui/material/Stack'; -import Button from '@mui/material/Button'; - -const rows = [ - { - hierarchy: ['Sarah'], - jobTitle: 'Head of Human Resources', - recruitmentDate: new Date(2020, 8, 12), - id: 0, - }, - { - hierarchy: ['Thomas'], - jobTitle: 'Head of Sales', - recruitmentDate: new Date(2017, 3, 4), - id: 1, - }, - { - hierarchy: ['Thomas', 'Robert'], - jobTitle: 'Sales Person', - recruitmentDate: new Date(2020, 11, 20), - id: 2, - }, - { - hierarchy: ['Thomas', 'Karen'], - jobTitle: 'Sales Person', - recruitmentDate: new Date(2020, 10, 14), - id: 3, - }, - { - hierarchy: ['Thomas', 'Nancy'], - jobTitle: 'Sales Person', - recruitmentDate: new Date(2017, 10, 29), - id: 4, - }, - { - hierarchy: ['Thomas', 'Daniel'], - jobTitle: 'Sales Person', - recruitmentDate: new Date(2020, 7, 21), - id: 5, - }, - { - hierarchy: ['Thomas', 'Christopher'], - jobTitle: 'Sales Person', - recruitmentDate: new Date(2020, 7, 20), - id: 6, - }, - { - hierarchy: ['Thomas', 'Donald'], - jobTitle: 'Sales Person', - recruitmentDate: new Date(2019, 6, 28), - id: 7, - }, - { - hierarchy: ['Mary'], - jobTitle: 'Head of Engineering', - recruitmentDate: new Date(2016, 3, 14), - id: 8, - }, - { - hierarchy: ['Mary', 'Jennifer'], - jobTitle: 'Tech lead front', - recruitmentDate: new Date(2016, 5, 17), - id: 9, - }, - { - hierarchy: ['Mary', 'Jennifer', 'Anna'], - jobTitle: 'Front-end developer', - recruitmentDate: new Date(2019, 11, 7), - id: 10, - }, - { - hierarchy: ['Mary', 'Michael'], - jobTitle: 'Tech lead devops', - recruitmentDate: new Date(2021, 7, 1), - id: 11, - }, - { - hierarchy: ['Mary', 'Linda'], - jobTitle: 'Tech lead back', - recruitmentDate: new Date(2017, 0, 12), - id: 12, - }, - { - hierarchy: ['Mary', 'Linda', 'Elizabeth'], - jobTitle: 'Back-end developer', - recruitmentDate: new Date(2019, 2, 22), - id: 13, - }, - { - hierarchy: ['Mary', 'Linda', 'William'], - jobTitle: 'Back-end developer', - recruitmentDate: new Date(2018, 4, 19), - id: 14, - }, -]; - -const columns = [ - { field: 'jobTitle', headerName: 'Job Title', width: 200 }, - { - field: 'recruitmentDate', - headerName: 'Recruitment Date', - type: 'date', - width: 150, - }, -]; - -const getTreeDataPath = (row) => row.hierarchy; - -export default function SetRowExpansionTreeData() { - const apiRef = useGridApiRef(); - - const toggleSecondRow = () => { - const rowIds = gridVisibleSortedRowIdsSelector(apiRef.current.state); - - if (rows.length > 1) { - const rowId = rowIds[1]; - apiRef.current.setRowChildrenExpansion( - rowId, - !apiRef.current.getRowNode(rowId)?.childrenExpanded, - ); - } - }; - - return ( - - -
- -
-
- ); -} diff --git a/docs/src/pages/components/data-grid/group-pivot/SetRowExpansionTreeData.tsx b/docs/src/pages/components/data-grid/group-pivot/SetRowExpansionTreeData.tsx deleted file mode 100644 index 2512e502342c5..0000000000000 --- a/docs/src/pages/components/data-grid/group-pivot/SetRowExpansionTreeData.tsx +++ /dev/null @@ -1,147 +0,0 @@ -import * as React from 'react'; -import { - DataGridPro, - GridColumns, - GridRowsProp, - useGridApiRef, - gridVisibleSortedRowIdsSelector, -} from '@mui/x-data-grid-pro'; -import Stack from '@mui/material/Stack'; -import Button from '@mui/material/Button'; - -const rows: GridRowsProp = [ - { - hierarchy: ['Sarah'], - jobTitle: 'Head of Human Resources', - recruitmentDate: new Date(2020, 8, 12), - id: 0, - }, - { - hierarchy: ['Thomas'], - jobTitle: 'Head of Sales', - recruitmentDate: new Date(2017, 3, 4), - id: 1, - }, - { - hierarchy: ['Thomas', 'Robert'], - jobTitle: 'Sales Person', - recruitmentDate: new Date(2020, 11, 20), - id: 2, - }, - { - hierarchy: ['Thomas', 'Karen'], - jobTitle: 'Sales Person', - recruitmentDate: new Date(2020, 10, 14), - id: 3, - }, - { - hierarchy: ['Thomas', 'Nancy'], - jobTitle: 'Sales Person', - recruitmentDate: new Date(2017, 10, 29), - id: 4, - }, - { - hierarchy: ['Thomas', 'Daniel'], - jobTitle: 'Sales Person', - recruitmentDate: new Date(2020, 7, 21), - id: 5, - }, - { - hierarchy: ['Thomas', 'Christopher'], - jobTitle: 'Sales Person', - recruitmentDate: new Date(2020, 7, 20), - id: 6, - }, - { - hierarchy: ['Thomas', 'Donald'], - jobTitle: 'Sales Person', - recruitmentDate: new Date(2019, 6, 28), - id: 7, - }, - { - hierarchy: ['Mary'], - jobTitle: 'Head of Engineering', - recruitmentDate: new Date(2016, 3, 14), - id: 8, - }, - { - hierarchy: ['Mary', 'Jennifer'], - jobTitle: 'Tech lead front', - recruitmentDate: new Date(2016, 5, 17), - id: 9, - }, - { - hierarchy: ['Mary', 'Jennifer', 'Anna'], - jobTitle: 'Front-end developer', - recruitmentDate: new Date(2019, 11, 7), - id: 10, - }, - { - hierarchy: ['Mary', 'Michael'], - jobTitle: 'Tech lead devops', - recruitmentDate: new Date(2021, 7, 1), - id: 11, - }, - { - hierarchy: ['Mary', 'Linda'], - jobTitle: 'Tech lead back', - recruitmentDate: new Date(2017, 0, 12), - id: 12, - }, - { - hierarchy: ['Mary', 'Linda', 'Elizabeth'], - jobTitle: 'Back-end developer', - recruitmentDate: new Date(2019, 2, 22), - id: 13, - }, - { - hierarchy: ['Mary', 'Linda', 'William'], - jobTitle: 'Back-end developer', - recruitmentDate: new Date(2018, 4, 19), - id: 14, - }, -]; - -const columns: GridColumns = [ - { field: 'jobTitle', headerName: 'Job Title', width: 200 }, - { - field: 'recruitmentDate', - headerName: 'Recruitment Date', - type: 'date', - width: 150, - }, -]; - -const getTreeDataPath = (row) => row.hierarchy; - -export default function SetRowExpansionTreeData() { - const apiRef = useGridApiRef(); - - const toggleSecondRow = () => { - const rowIds = gridVisibleSortedRowIdsSelector(apiRef.current.state); - - if (rows.length > 1) { - const rowId = rowIds[1]; - apiRef.current.setRowChildrenExpansion( - rowId, - !apiRef.current.getRowNode(rowId)?.childrenExpanded, - ); - } - }; - - return ( - - -
- -
-
- ); -} diff --git a/docs/src/pages/components/data-grid/group-pivot/SetRowExpansionTreeData.tsx.preview b/docs/src/pages/components/data-grid/group-pivot/SetRowExpansionTreeData.tsx.preview deleted file mode 100644 index 68347f54d4626..0000000000000 --- a/docs/src/pages/components/data-grid/group-pivot/SetRowExpansionTreeData.tsx.preview +++ /dev/null @@ -1,11 +0,0 @@ - -
- -
\ No newline at end of file diff --git a/docs/src/pages/components/data-grid/group-pivot/CustomGroupingColumnTreeData.js b/docs/src/pages/components/data-grid/group-pivot/TreeDataCustomGroupingColumn.js similarity index 98% rename from docs/src/pages/components/data-grid/group-pivot/CustomGroupingColumnTreeData.js rename to docs/src/pages/components/data-grid/group-pivot/TreeDataCustomGroupingColumn.js index 55130045ce945..c802eb9eb7796 100644 --- a/docs/src/pages/components/data-grid/group-pivot/CustomGroupingColumnTreeData.js +++ b/docs/src/pages/components/data-grid/group-pivot/TreeDataCustomGroupingColumn.js @@ -77,12 +77,14 @@ CustomGridTreeDataGroupingCell.propTypes = { rowNode: PropTypes.shape({ /** * The id of the row children. + * @default [] */ children: PropTypes.arrayOf( PropTypes.oneOfType([PropTypes.number, PropTypes.string]), ), /** * Current expansion status of the row. + * @default false */ childrenExpanded: PropTypes.bool, /** @@ -109,6 +111,7 @@ CustomGridTreeDataGroupingCell.propTypes = { id: PropTypes.oneOfType([PropTypes.number, PropTypes.string]).isRequired, /** * If `true`, this node has been automatically added to fill a gap in the tree structure. + * @default false */ isAutoGenerated: PropTypes.bool, /** @@ -240,7 +243,7 @@ const groupingColDef = { renderCell: (params) => , }; -export default function CustomGroupingColumnTreeData() { +export default function TreeDataCustomGroupingColumn() { return (
, }; -export default function CustomGroupingColumnTreeData() { +export default function TreeDataCustomGroupingColumn() { return (
row.hierarchy; -export default function DisableChildrenFilteringTreeData() { +export default function TreeDataDisableChildrenFiltering() { const [disableChildrenFiltering, setDisableChildrenFiltering] = React.useState(true); const [filterModel, setFilterModel] = React.useState({ diff --git a/docs/src/pages/components/data-grid/group-pivot/DisableChildrenFilteringTreeData.tsx b/docs/src/pages/components/data-grid/group-pivot/TreeDataDisableChildrenFiltering.tsx similarity index 98% rename from docs/src/pages/components/data-grid/group-pivot/DisableChildrenFilteringTreeData.tsx rename to docs/src/pages/components/data-grid/group-pivot/TreeDataDisableChildrenFiltering.tsx index 256e8a37a8f8d..af50f02481177 100644 --- a/docs/src/pages/components/data-grid/group-pivot/DisableChildrenFilteringTreeData.tsx +++ b/docs/src/pages/components/data-grid/group-pivot/TreeDataDisableChildrenFiltering.tsx @@ -117,7 +117,7 @@ const columns: GridColumns = [ const getTreeDataPath = (row) => row.hierarchy; -export default function DisableChildrenFilteringTreeData() { +export default function TreeDataDisableChildrenFiltering() { const [disableChildrenFiltering, setDisableChildrenFiltering] = React.useState(true); const [filterModel, setFilterModel] = React.useState({ diff --git a/docs/src/pages/components/data-grid/group-pivot/DisableChildrenSortingTreeData.js b/docs/src/pages/components/data-grid/group-pivot/TreeDataDisableChildrenSorting.js similarity index 98% rename from docs/src/pages/components/data-grid/group-pivot/DisableChildrenSortingTreeData.js rename to docs/src/pages/components/data-grid/group-pivot/TreeDataDisableChildrenSorting.js index 19e5fadc359d8..871b4ed1748b9 100644 --- a/docs/src/pages/components/data-grid/group-pivot/DisableChildrenSortingTreeData.js +++ b/docs/src/pages/components/data-grid/group-pivot/TreeDataDisableChildrenSorting.js @@ -111,7 +111,7 @@ const columns = [ const getTreeDataPath = (row) => row.hierarchy; -export default function DisableChildrenSortingTreeData() { +export default function TreeDataDisableChildrenSorting() { const [disableChildrenSorting, setDisableChildrenSorting] = React.useState(true); const [sortModel, setSortModel] = React.useState([ { field: 'recruitmentDate', sort: 'asc' }, diff --git a/docs/src/pages/components/data-grid/group-pivot/DisableChildrenSortingTreeData.tsx b/docs/src/pages/components/data-grid/group-pivot/TreeDataDisableChildrenSorting.tsx similarity index 98% rename from docs/src/pages/components/data-grid/group-pivot/DisableChildrenSortingTreeData.tsx rename to docs/src/pages/components/data-grid/group-pivot/TreeDataDisableChildrenSorting.tsx index 012a19e36a291..fdd25abcfae7f 100644 --- a/docs/src/pages/components/data-grid/group-pivot/DisableChildrenSortingTreeData.tsx +++ b/docs/src/pages/components/data-grid/group-pivot/TreeDataDisableChildrenSorting.tsx @@ -116,7 +116,7 @@ const columns: GridColumns = [ const getTreeDataPath = (row) => row.hierarchy; -export default function DisableChildrenSortingTreeData() { +export default function TreeDataDisableChildrenSorting() { const [disableChildrenSorting, setDisableChildrenSorting] = React.useState(true); const [sortModel, setSortModel] = React.useState([ { field: 'recruitmentDate', sort: 'asc' }, diff --git a/docs/src/pages/components/data-grid/group-pivot/BasicTreeData.js b/docs/src/pages/components/data-grid/group-pivot/TreeDataSimple.js similarity index 98% rename from docs/src/pages/components/data-grid/group-pivot/BasicTreeData.js rename to docs/src/pages/components/data-grid/group-pivot/TreeDataSimple.js index b53e0acbb0e05..616037c4fcdae 100644 --- a/docs/src/pages/components/data-grid/group-pivot/BasicTreeData.js +++ b/docs/src/pages/components/data-grid/group-pivot/TreeDataSimple.js @@ -106,7 +106,7 @@ const columns = [ const getTreeDataPath = (row) => row.hierarchy; -export default function BasicTreeData() { +export default function TreeDataSimple() { return (
row.hierarchy; -export default function BasicTreeData() { +export default function TreeDataSimple() { return (
Use grouping, pivoting, and more to analyze the data in depth.

+## Row grouping [](https://mui.com/store/items/material-ui-pro/) + +For when you need to group rows based on repeated column values, and/or custom functions. +On the following example, we're grouping all movies based on their production `company` + +{{"demo": "pages/components/data-grid/group-pivot/RowGroupingBasicExample.js", "bg": "inline", "defaultCodeOpen": false}} + +> ⚠️ This feature is temporarily available on the Pro plan until the release of the Premium plan. +> +> To avoid future regression for users of the Pro plan, the feature needs to be explicitly activated using the `rowGrouping` experimental feature flag. +> +> ```tsx +> +> ``` +> +> The feature is stable in its current form, and we encourage users willing to migrate to the Premium plan once available to start using it. + +### Set grouping criteria + +#### Initialize the row grouping + +The easiest way to get started with the feature is to provide its model to the `initialState` prop: + +```ts +initialState={{ + rowGrouping: { + model: ['company', 'director'], + } +}} +``` + +The basic parameters are the columns you want to check for repeating values. +In this example, we want to group all the movies matching the same company name, followed by a second group matching the director's name. + +{{"demo": "pages/components/data-grid/group-pivot/RowGroupingInitialState.js", "bg": "inline", "defaultCodeOpen": false}} + +#### Controlled row grouping + +If you need to control the state of the criteria used for grouping, use the `rowGroupingModel` prop. +You can use the `onRowGroupingModelChange` prop to listen to changes to the page size and update the prop accordingly. + +{{"demo": "pages/components/data-grid/group-pivot/RowGroupingControlled.js", "bg": "inline", "defaultCodeOpen": false}} + +### Grouping columns + +#### Single grouping column + +By default, the grid will display a single column holding all grouped columns. +If you have multiple grouped columns, this column name will be set to "Group". + +{{"demo": "pages/components/data-grid/group-pivot/RowGroupingSingleGroupingCol.js", "bg": "inline", "defaultCodeOpen": false}} + +#### Multiple grouping column + +To display a column for each grouping criterion, set the `rowGroupingColumnMode` prop to `multiple`. + +{{"demo": "pages/components/data-grid/group-pivot/RowGroupingMultipleGroupingCol.js", "bg": "inline", "defaultCodeOpen": false}} + +#### Custom grouping column + +To customize the rendering of the grouping column, use the `groupingColDef` prop. +You can override the **headerName** or any property of the `GridColDef` interface, except the `field`, the `type` and the properties related to inline edition. + +{{"demo": "pages/components/data-grid/group-pivot/RowGroupingCustomGroupingColDefObject.js", "bg": "inline", "defaultCodeOpen": false}} + +By default, when using the object format, the properties will be applied to all Grouping columns. This means that if you have `rowGroupingColumnMode` set to `multiple`, all the columns will share the same `groupingColDef` properties. + +If you wish to override properties of specific grouping columns or to apply different overrides based on the current grouping criteria, you can pass a callback function to `groupingColDef`, instead of an object with its config. +The callback is called for each grouping column, and it receives the respective column's "fields" as parameter. + +{{"demo": "pages/components/data-grid/group-pivot/RowGroupingCustomGroupingColDefCallback.js", "bg": "inline", "defaultCodeOpen": false}} + +#### Show values for the leaves + +By default, the grouped rows display no value on their grouping columns' cells. We're calling those cells "leaves". + +If you want to display some value, you can provide a `leafField` property to the `groupingColDef`. + +{{"demo": "pages/components/data-grid/group-pivot/RowGroupingLeafWithValue.js", "bg": "inline", "defaultCodeOpen": false}} + +#### Hide the descendant count + +Use the `hideDescendantCount` property of the `groupingColDef` to hide the number of descendants of a grouping row. + +{{"demo": "pages/components/data-grid/group-pivot/RowGroupingHideDescendantCount.js", "bg": "inline", "defaultCodeOpen": false}} + +### Disable the row grouping + +#### For all columns + +You can disable row grouping by setting `disableRowGrouping` prop to true. + +It will disable all the features related to the row grouping, even if a model is provided. + +{{"demo": "pages/components/data-grid/group-pivot/RowGroupingDisabled.js", "bg": "inline", "defaultCodeOpen": false}} + +#### For some columns + +In case you need to disable grouping on specific column(s), set the `groupable` property on the respective column definition (`GridColDef`) to `false`. +In the example below, the `director` column can not be grouped. And in all example, the `title` and `gross` columns can not be grouped. + +{{"demo": "pages/components/data-grid/group-pivot/RowGroupingColDefCanBeGrouped.js", "bg": "inline", "defaultCodeOpen": false}} + +### Using `groupingValueGetter` for complex grouping value + +The grouping value has to be either a `string`, a `number`, `null` or `undefined`. +If your cell value is more complex, pass a `groupingValueGetter` property to the column definition to convert it into a valid value. + +```ts +const columns: GridColumns = [ + { + field: 'composer', + groupingValueGetter: (params) => params.value.name, + }, + // ... +]; +``` + +{{"demo": "pages/components/data-grid/group-pivot/RowGroupingGroupingValueGetter.js", "bg": "inline", "defaultCodeOpen": false}} + +**Note**: If your column also have a `valueGetter` property, the value passed to the `groupingValueGetter` method will still be the row value from the `row[field]`. + +### Rows with missing groups + +If the grouping key of a grouping criteria is `null` or `undefined` for a row, the grid will consider that this row does not have a value for this group. and will inline it for those groups. + +{{"demo": "pages/components/data-grid/group-pivot/RowGroupingRowsWithMissingGroups.js", "bg": "inline", "defaultCodeOpen": false}} + +### Group expansion + +By default, all groups are initially displayed collapsed. You can change this behaviour by setting the `defaultGroupingExpansionDepth` prop to expand all the groups up to a given depth when loading the data. +If you want to expand the whole tree, set `defaultGroupingExpansionDepth = -1` + +{{"demo": "pages/components/data-grid/group-pivot/RowGroupingDefaultExpansionDepth.js", "bg": "inline", "defaultCodeOpen": false}} + +If you want to expand groups by default according to a more complex logic, use the `isGroupExpandedByDefault` prop which is a callback receiving the node as an argument. +When defined, this callback will always have the priority over the `defaultGroupingExpansionDepth` prop. + +```tsx +isGroupExpandedByDefault={ + node => node.groupingField === 'company' && node.groupingKey === '20th Century Fox' +} +``` + +{{"demo": "pages/components/data-grid/group-pivot/RowGroupingIsGroupExpandedByDefault.js", "bg": "inline", "defaultCodeOpen": false}} + +Use the `setRowChildrenExpansion` method on `apiRef` to programmatically set the expansion of a row. + +{{"demo": "pages/components/data-grid/group-pivot/RowGroupingSetChildrenExpansion.js", "bg": "inline", "defaultCodeOpen": false}} + +### Sorting / Filtering + +#### Single grouping column + +When using `rowGroupingColumnMode = "single"`, the default behavior is to apply the `sortComparator` and `filterOperators` of the top level grouping criteria. + +If you are rendering leaves with the `leafField` property of `groupColDef`, the sorting and filtering will be applied on the leaves based on the `sortComparator` and `filterOperators` of their original column. + +In both cases, you can force the sorting and filtering to be applied on another grouping criteria with the `mainGroupingCriteria` property of `groupColDef` + +> ⚠️ This feature is not yet compatible with `sortingMode = "server` and `filteringMode = "server"` + +{{"demo": "pages/components/data-grid/group-pivot/RowGroupingSortingSingleGroupingColDef.js", "bg": "inline", "defaultCodeOpen": false}} + +#### Multiple grouping column + +When using `rowGroupingColumnMode = "multiple"`, the default behavior is to apply the `sortComparator` and `filterOperators` of the grouping criteria of each grouping column. + +If you are rendering leaves on one of those columns with the `leafField` property of `groupColDef`, the sorting and filtering will be applied on the leaves for this grouping column based on the `sortComparator` and `filterOperators` of the leave's original column. + +If you want to render leaves but apply the sorting and filtering on the grouping criteria of the column, you can force it by setting the `mainGroupingCriteria` property `groupColDef` to be equal to the grouping criteria. + +In the example below: + +- the sorting and filtering of the `company` grouping column is applied on the `company` field +- the sorting and filtering of the `director` grouping column is applied on the `director` field even though it has leaves + +{{"demo": "pages/components/data-grid/group-pivot/RowGroupingSortingMultipleGroupingColDef.js", "bg": "inline", "defaultCodeOpen": false}} + +> ⚠️ If you are dynamically switching the `leafField` or `mainGroupingCriteria`, the sorting and filtering models will not automatically be cleaned-up and the sorting / filtering will not be re-applied. + +### Full example + +{{"demo": "pages/components/data-grid/group-pivot/RowGroupingFullExample.js", "bg": "inline", "defaultCodeOpen": false}} + +### apiRef [](https://mui.com/store/items/material-ui-pro/) + +{{"demo": "pages/components/data-grid/group-pivot/RowGroupingApiNoSnap.js", "bg": "inline", "hideToolbar": true}} + ## Tree Data [](https://mui.com/store/items/material-ui-pro/) Tree Data allows to display data with parent/child relationships. @@ -53,13 +242,13 @@ const rows: GridRowsProp = [ />; ``` -{{"demo": "pages/components/data-grid/group-pivot/BasicTreeData.js", "bg": "inline", "defaultCodeOpen": false}} +{{"demo": "pages/components/data-grid/group-pivot/TreeDataSimple.js", "bg": "inline", "defaultCodeOpen": false}} ### Custom grouping column -Use the `groupingColDef` prop to customize the rendering of the grouping column. +Same behavior as for the [Row grouping](#grouping-columns) except for the `leafField` and `mainGroupingCriteria` which are not applicable for the Tree Data. -{{"demo": "pages/components/data-grid/group-pivot/CustomGroupingColumnTreeData.js", "bg": "inline", "defaultCodeOpen": false}} +{{"demo": "pages/components/data-grid/group-pivot/TreeDataCustomGroupingColumn.js", "bg": "inline", "defaultCodeOpen": false}} #### Accessing the grouping column field @@ -79,14 +268,7 @@ If you want to access the grouping column field, for instance, to use it with co ### Group expansion -Use the `defaultGroupingExpansionDepth` prop to expand all the groups up to a given depth when loading the data. -If you want to expand the whole tree, set `defaultGroupingExpansionDepth = -1` - -{{"demo": "pages/components/data-grid/group-pivot/DefaultGroupingExpansionDepthTreeData.js", "bg": "inline", "defaultCodeOpen": false}} - -Use the `setRowChildrenExpansion` method on `apiRef` to programmatically set the expansion of a row. - -{{"demo": "pages/components/data-grid/group-pivot/SetRowExpansionTreeData.js", "bg": "inline", "defaultCodeOpen": false}} +Same behavior as for the [Row grouping](#group-expansion). ### Gaps in the tree @@ -104,14 +286,14 @@ A node is included if one of the following criteria is met: By default, the filtering is applied to every depth of the tree. You can limit the filtering to the top-level rows with the `disableChildrenFiltering` prop. -{{"demo": "pages/components/data-grid/group-pivot/DisableChildrenFilteringTreeData.js", "bg": "inline", "defaultCodeOpen": false}} +{{"demo": "pages/components/data-grid/group-pivot/TreeDataDisableChildrenFiltering.js", "bg": "inline", "defaultCodeOpen": false}} ### Sorting By default, the sorting is applied to every depth of the tree. You can limit the sorting to the top level rows with the `disableChildrenSorting` prop. -{{"demo": "pages/components/data-grid/group-pivot/DisableChildrenSortingTreeData.js", "bg": "inline", "defaultCodeOpen": false}} +{{"demo": "pages/components/data-grid/group-pivot/TreeDataDisableChildrenSorting.js", "bg": "inline", "defaultCodeOpen": false}} > If you are using `sortingMode="server"`, you need to always put the children of a row after its parent. > For instance: @@ -136,14 +318,6 @@ You can limit the sorting to the top level rows with the `disableChildrenSorting The feature allows to display row details on an expandable pane. -## 🚧 Grouping [](https://mui.com/store/items/material-ui-pro/) - -> ⚠️ This feature isn't implemented yet. It's coming. -> -> 👍 Upvote [issue #212](https://github.com/mui-org/material-ui-x/issues/212) if you want to see it land faster. - -Group rows together that share a column value, this creates a visible header for each group and allows the end-user to collapse groups that they don't want to see. - ## 🚧 Aggregation [](https://mui.com/store/items/material-ui-pro/) > ⚠️ This feature isn't implemented yet. It's coming. diff --git a/docs/src/pages/components/data-grid/overview/overview.md b/docs/src/pages/components/data-grid/overview/overview.md index b09f5ba20d654..35f890d035856 100644 --- a/docs/src/pages/components/data-grid/overview/overview.md +++ b/docs/src/pages/components/data-grid/overview/overview.md @@ -82,6 +82,7 @@ We provide three options: - [Sorting](/components/data-grid/sorting) and [multi-sort](/components/data-grid/sorting/#multi-column-sorting) - [Selection](/components/data-grid/selection/) - [Column virtualization](/components/data-grid/virtualization/#column-virtualization) and [rows virtualization](/components/data-grid/virtualization/#row-virtualization) +- [Row grouping](/components/data-grid/group-pivot/#row-grouping) - [Tree data](/components/data-grid/group-pivot/#tree-data) - [Resizable columns](/components/data-grid/columns/#column-resizing) - [100% customizable](/components/data-grid/style/) @@ -98,7 +99,7 @@ While development of the data grid component is moving fast, there are still man - Headless (hooks only) - [Excel export](/components/data-grid/export/) - [Range selection](/components/data-grid/selection/#range-selection) -- [Group, Pivot, Aggregation](/components/data-grid/group-pivot/) +- [Pivot, Aggregation](/components/data-grid/group-pivot/) You can find more details on, the [feature comparison](/components/data-grid/getting-started/#feature-comparison), our living quarterly [roadmap](https://github.com/mui-org/material-ui-x/projects/1) as well as on the open [GitHub issues](https://github.com/mui-org/material-ui-x/issues?q=is%3Aopen+label%3A%22component%3A+DataGrid%22+label%3Aenhancement). diff --git a/docs/translations/api-docs/data-grid/data-grid-pro-pt.json b/docs/translations/api-docs/data-grid/data-grid-pro-pt.json index e59c3ce1a9d29..e6473ff1a8da4 100644 --- a/docs/translations/api-docs/data-grid/data-grid-pro-pt.json +++ b/docs/translations/api-docs/data-grid/data-grid-pro-pt.json @@ -17,8 +17,8 @@ "componentsProps": "Overrideable components props dynamically passed to the component at rendering.", "defaultGroupingExpansionDepth": "If above 0, the row children will be expanded up to this depth. If equal to -1, all the row children will be expanded.", "density": "Set the density of the grid.", - "disableChildrenFiltering": "If true, the filtering will only be applied to the top level rows.", - "disableChildrenSorting": "If true, the sorting will only be applied to the top level rows.", + "disableChildrenFiltering": "If true, the filtering will only be applied to the top level rows when grouping rows with the treeData prop.", + "disableChildrenSorting": "If true, the sorting will only be applied to the top level rows when grouping rows with the treeData prop.", "disableColumnFilter": "If true, column filters are disabled.", "disableColumnMenu": "If true, the column menu is disabled.", "disableColumnPinning": "If true, the column pinning is disabled.", @@ -30,11 +30,13 @@ "disableMultipleColumnsFiltering": "If true, filtering with multiple columns is disabled.", "disableMultipleColumnsSorting": "If true, sorting with multiple columns is disabled.", "disableMultipleSelection": "If true, multiple selection using the CTRL or CMD key is disabled.", + "disableRowGrouping": "If true, the row grouping is disabled.", "disableSelectionOnClick": "If true, the selection on click on a row or cell is disabled.", "disableVirtualization": "If true, the virtualization is disabled.", "editMode": "Controls whether to use the cell or row editing.", "editRowsModel": "Set the edit rows model of the grid.", "error": "An error that will turn the grid into its error state and display the error component.", + "experimentalFeatures": "Features under development. For each feature, if the flag is not explicitly set to true, the feature will be fully disabled and any property / method call will not have any effect.", "filterMode": "Filtering can be processed on the server or client-side. Set it to 'server' if you would like to handle filtering on the server-side.", "filterModel": "Set the filter model of the grid.", "getCellClassName": "Function that applies CSS classes dynamically on cells.

Signature:
function(params: GridCellParams) => string
params: With all properties from GridCellParams.
returns (string): The CSS class to apply to the cell.", @@ -86,6 +88,7 @@ "onRowEditCommit": "Callback fired when the row changes are committed.

Signature:
function(id: GridRowId, event: MuiEvent<MuiBaseEvent>) => void
id: The row id.
event: The event that caused this prop to be called.", "onRowEditStart": "Callback fired when the row turns to edit mode.

Signature:
function(params: GridRowParams, event: MuiEvent<React.KeyboardEvent | React.MouseEvent>) => void
params: With all properties from GridRowParams.
event: The event that caused this prop to be called.", "onRowEditStop": "Callback fired when the row turns to view mode.

Signature:
function(params: GridRowParams, event: MuiEvent<MuiBaseEvent>) => void
params: With all properties from GridRowParams.
event: The event that caused this prop to be called.", + "onRowGroupingModelChange": "Callback fired when the row grouping model changes.

Signature:
function(model: GridRowGroupingModel, details: GridCallbackDetails) => void
model: Columns used as grouping criteria.
details: Additional details for this callback.", "onRowsScrollEnd": "Callback fired when scrolling to the bottom of the grid viewport.

Signature:
function(params: GridRowScrollEndParams, event: MuiEvent<{}>, details: GridCallbackDetails) => void
params: With all properties from GridRowScrollEndParams.
event: The event object.
details: Additional details for this callback.", "onSelectionModelChange": "Callback fired when the selection state of one or multiple rows changes.

Signature:
function(selectionModel: GridSelectionModel, details: GridCallbackDetails) => void
selectionModel: With all the row ids GridSelectionModel.
details: Additional details for this callback.", "onSortModelChange": "Callback fired when the sort model changes before a column is sorted.

Signature:
function(model: GridSortModel, details: GridCallbackDetails) => void
model: With all properties from GridSortModel.
details: Additional details for this callback.", @@ -97,6 +100,8 @@ "pinnedColumns": "The column fields to display pinned to left or right.", "rowBuffer": "Number of extra rows to be rendered before/after the visible slice.", "rowCount": "Set the total number of rows, if it is different than the length of the value rows prop. If some of the rows have children (for instance in the tree data), this number represents the amount of top level rows.", + "rowGroupingColumnMode": "If single, all column we are grouping by will be represented in the same grouping the same column. If multiple, each column we are grouping by will be represented in its own column.", + "rowGroupingModel": "Set the row grouping model of the grid.", "rowHeight": "Set the height in pixel of a row in the grid.", "rows": "Set of rows of type GridRowsProp.", "rowsPerPageOptions": "Select the pageSize dynamically using the component UI.", @@ -463,6 +468,8 @@ "ExportIcon": "Icon displayed on the open export button present in the toolbar by default.", "MoreActionsIcon": "Icon displayed on the actions column type to open the menu.", "TreeDataExpandIcon": "Icon displayed on the tree data toggling column when the children are collapsed", - "TreeDataCollapseIcon": "Icon displayed on the tree data toggling column when the children are expanded" + "TreeDataCollapseIcon": "Icon displayed on the tree data toggling column when the children are expanded", + "GroupingCriteriaExpandIcon": "Icon displayed on the grouping column when the children are collapsed", + "GroupingCriteriaCollapseIcon": "Icon displayed on the grouping column when the children are expanded" } } diff --git a/docs/translations/api-docs/data-grid/data-grid-pro-zh.json b/docs/translations/api-docs/data-grid/data-grid-pro-zh.json index e59c3ce1a9d29..e6473ff1a8da4 100644 --- a/docs/translations/api-docs/data-grid/data-grid-pro-zh.json +++ b/docs/translations/api-docs/data-grid/data-grid-pro-zh.json @@ -17,8 +17,8 @@ "componentsProps": "Overrideable components props dynamically passed to the component at rendering.", "defaultGroupingExpansionDepth": "If above 0, the row children will be expanded up to this depth. If equal to -1, all the row children will be expanded.", "density": "Set the density of the grid.", - "disableChildrenFiltering": "If true, the filtering will only be applied to the top level rows.", - "disableChildrenSorting": "If true, the sorting will only be applied to the top level rows.", + "disableChildrenFiltering": "If true, the filtering will only be applied to the top level rows when grouping rows with the treeData prop.", + "disableChildrenSorting": "If true, the sorting will only be applied to the top level rows when grouping rows with the treeData prop.", "disableColumnFilter": "If true, column filters are disabled.", "disableColumnMenu": "If true, the column menu is disabled.", "disableColumnPinning": "If true, the column pinning is disabled.", @@ -30,11 +30,13 @@ "disableMultipleColumnsFiltering": "If true, filtering with multiple columns is disabled.", "disableMultipleColumnsSorting": "If true, sorting with multiple columns is disabled.", "disableMultipleSelection": "If true, multiple selection using the CTRL or CMD key is disabled.", + "disableRowGrouping": "If true, the row grouping is disabled.", "disableSelectionOnClick": "If true, the selection on click on a row or cell is disabled.", "disableVirtualization": "If true, the virtualization is disabled.", "editMode": "Controls whether to use the cell or row editing.", "editRowsModel": "Set the edit rows model of the grid.", "error": "An error that will turn the grid into its error state and display the error component.", + "experimentalFeatures": "Features under development. For each feature, if the flag is not explicitly set to true, the feature will be fully disabled and any property / method call will not have any effect.", "filterMode": "Filtering can be processed on the server or client-side. Set it to 'server' if you would like to handle filtering on the server-side.", "filterModel": "Set the filter model of the grid.", "getCellClassName": "Function that applies CSS classes dynamically on cells.

Signature:
function(params: GridCellParams) => string
params: With all properties from GridCellParams.
returns (string): The CSS class to apply to the cell.", @@ -86,6 +88,7 @@ "onRowEditCommit": "Callback fired when the row changes are committed.

Signature:
function(id: GridRowId, event: MuiEvent<MuiBaseEvent>) => void
id: The row id.
event: The event that caused this prop to be called.", "onRowEditStart": "Callback fired when the row turns to edit mode.

Signature:
function(params: GridRowParams, event: MuiEvent<React.KeyboardEvent | React.MouseEvent>) => void
params: With all properties from GridRowParams.
event: The event that caused this prop to be called.", "onRowEditStop": "Callback fired when the row turns to view mode.

Signature:
function(params: GridRowParams, event: MuiEvent<MuiBaseEvent>) => void
params: With all properties from GridRowParams.
event: The event that caused this prop to be called.", + "onRowGroupingModelChange": "Callback fired when the row grouping model changes.

Signature:
function(model: GridRowGroupingModel, details: GridCallbackDetails) => void
model: Columns used as grouping criteria.
details: Additional details for this callback.", "onRowsScrollEnd": "Callback fired when scrolling to the bottom of the grid viewport.

Signature:
function(params: GridRowScrollEndParams, event: MuiEvent<{}>, details: GridCallbackDetails) => void
params: With all properties from GridRowScrollEndParams.
event: The event object.
details: Additional details for this callback.", "onSelectionModelChange": "Callback fired when the selection state of one or multiple rows changes.

Signature:
function(selectionModel: GridSelectionModel, details: GridCallbackDetails) => void
selectionModel: With all the row ids GridSelectionModel.
details: Additional details for this callback.", "onSortModelChange": "Callback fired when the sort model changes before a column is sorted.

Signature:
function(model: GridSortModel, details: GridCallbackDetails) => void
model: With all properties from GridSortModel.
details: Additional details for this callback.", @@ -97,6 +100,8 @@ "pinnedColumns": "The column fields to display pinned to left or right.", "rowBuffer": "Number of extra rows to be rendered before/after the visible slice.", "rowCount": "Set the total number of rows, if it is different than the length of the value rows prop. If some of the rows have children (for instance in the tree data), this number represents the amount of top level rows.", + "rowGroupingColumnMode": "If single, all column we are grouping by will be represented in the same grouping the same column. If multiple, each column we are grouping by will be represented in its own column.", + "rowGroupingModel": "Set the row grouping model of the grid.", "rowHeight": "Set the height in pixel of a row in the grid.", "rows": "Set of rows of type GridRowsProp.", "rowsPerPageOptions": "Select the pageSize dynamically using the component UI.", @@ -463,6 +468,8 @@ "ExportIcon": "Icon displayed on the open export button present in the toolbar by default.", "MoreActionsIcon": "Icon displayed on the actions column type to open the menu.", "TreeDataExpandIcon": "Icon displayed on the tree data toggling column when the children are collapsed", - "TreeDataCollapseIcon": "Icon displayed on the tree data toggling column when the children are expanded" + "TreeDataCollapseIcon": "Icon displayed on the tree data toggling column when the children are expanded", + "GroupingCriteriaExpandIcon": "Icon displayed on the grouping column when the children are collapsed", + "GroupingCriteriaCollapseIcon": "Icon displayed on the grouping column when the children are expanded" } } diff --git a/docs/translations/api-docs/data-grid/data-grid-pro.json b/docs/translations/api-docs/data-grid/data-grid-pro.json index e59c3ce1a9d29..e6473ff1a8da4 100644 --- a/docs/translations/api-docs/data-grid/data-grid-pro.json +++ b/docs/translations/api-docs/data-grid/data-grid-pro.json @@ -17,8 +17,8 @@ "componentsProps": "Overrideable components props dynamically passed to the component at rendering.", "defaultGroupingExpansionDepth": "If above 0, the row children will be expanded up to this depth. If equal to -1, all the row children will be expanded.", "density": "Set the density of the grid.", - "disableChildrenFiltering": "If true, the filtering will only be applied to the top level rows.", - "disableChildrenSorting": "If true, the sorting will only be applied to the top level rows.", + "disableChildrenFiltering": "If true, the filtering will only be applied to the top level rows when grouping rows with the treeData prop.", + "disableChildrenSorting": "If true, the sorting will only be applied to the top level rows when grouping rows with the treeData prop.", "disableColumnFilter": "If true, column filters are disabled.", "disableColumnMenu": "If true, the column menu is disabled.", "disableColumnPinning": "If true, the column pinning is disabled.", @@ -30,11 +30,13 @@ "disableMultipleColumnsFiltering": "If true, filtering with multiple columns is disabled.", "disableMultipleColumnsSorting": "If true, sorting with multiple columns is disabled.", "disableMultipleSelection": "If true, multiple selection using the CTRL or CMD key is disabled.", + "disableRowGrouping": "If true, the row grouping is disabled.", "disableSelectionOnClick": "If true, the selection on click on a row or cell is disabled.", "disableVirtualization": "If true, the virtualization is disabled.", "editMode": "Controls whether to use the cell or row editing.", "editRowsModel": "Set the edit rows model of the grid.", "error": "An error that will turn the grid into its error state and display the error component.", + "experimentalFeatures": "Features under development. For each feature, if the flag is not explicitly set to true, the feature will be fully disabled and any property / method call will not have any effect.", "filterMode": "Filtering can be processed on the server or client-side. Set it to 'server' if you would like to handle filtering on the server-side.", "filterModel": "Set the filter model of the grid.", "getCellClassName": "Function that applies CSS classes dynamically on cells.

Signature:
function(params: GridCellParams) => string
params: With all properties from GridCellParams.
returns (string): The CSS class to apply to the cell.", @@ -86,6 +88,7 @@ "onRowEditCommit": "Callback fired when the row changes are committed.

Signature:
function(id: GridRowId, event: MuiEvent<MuiBaseEvent>) => void
id: The row id.
event: The event that caused this prop to be called.", "onRowEditStart": "Callback fired when the row turns to edit mode.

Signature:
function(params: GridRowParams, event: MuiEvent<React.KeyboardEvent | React.MouseEvent>) => void
params: With all properties from GridRowParams.
event: The event that caused this prop to be called.", "onRowEditStop": "Callback fired when the row turns to view mode.

Signature:
function(params: GridRowParams, event: MuiEvent<MuiBaseEvent>) => void
params: With all properties from GridRowParams.
event: The event that caused this prop to be called.", + "onRowGroupingModelChange": "Callback fired when the row grouping model changes.

Signature:
function(model: GridRowGroupingModel, details: GridCallbackDetails) => void
model: Columns used as grouping criteria.
details: Additional details for this callback.", "onRowsScrollEnd": "Callback fired when scrolling to the bottom of the grid viewport.

Signature:
function(params: GridRowScrollEndParams, event: MuiEvent<{}>, details: GridCallbackDetails) => void
params: With all properties from GridRowScrollEndParams.
event: The event object.
details: Additional details for this callback.", "onSelectionModelChange": "Callback fired when the selection state of one or multiple rows changes.

Signature:
function(selectionModel: GridSelectionModel, details: GridCallbackDetails) => void
selectionModel: With all the row ids GridSelectionModel.
details: Additional details for this callback.", "onSortModelChange": "Callback fired when the sort model changes before a column is sorted.

Signature:
function(model: GridSortModel, details: GridCallbackDetails) => void
model: With all properties from GridSortModel.
details: Additional details for this callback.", @@ -97,6 +100,8 @@ "pinnedColumns": "The column fields to display pinned to left or right.", "rowBuffer": "Number of extra rows to be rendered before/after the visible slice.", "rowCount": "Set the total number of rows, if it is different than the length of the value rows prop. If some of the rows have children (for instance in the tree data), this number represents the amount of top level rows.", + "rowGroupingColumnMode": "If single, all column we are grouping by will be represented in the same grouping the same column. If multiple, each column we are grouping by will be represented in its own column.", + "rowGroupingModel": "Set the row grouping model of the grid.", "rowHeight": "Set the height in pixel of a row in the grid.", "rows": "Set of rows of type GridRowsProp.", "rowsPerPageOptions": "Select the pageSize dynamically using the component UI.", @@ -463,6 +468,8 @@ "ExportIcon": "Icon displayed on the open export button present in the toolbar by default.", "MoreActionsIcon": "Icon displayed on the actions column type to open the menu.", "TreeDataExpandIcon": "Icon displayed on the tree data toggling column when the children are collapsed", - "TreeDataCollapseIcon": "Icon displayed on the tree data toggling column when the children are expanded" + "TreeDataCollapseIcon": "Icon displayed on the tree data toggling column when the children are expanded", + "GroupingCriteriaExpandIcon": "Icon displayed on the grouping column when the children are collapsed", + "GroupingCriteriaCollapseIcon": "Icon displayed on the grouping column when the children are expanded" } } diff --git a/docs/translations/api-docs/data-grid/data-grid-pt.json b/docs/translations/api-docs/data-grid/data-grid-pt.json index 09c12bd44d2c4..d04ef2c78e1d7 100644 --- a/docs/translations/api-docs/data-grid/data-grid-pt.json +++ b/docs/translations/api-docs/data-grid/data-grid-pt.json @@ -439,6 +439,8 @@ "ExportIcon": "Icon displayed on the open export button present in the toolbar by default.", "MoreActionsIcon": "Icon displayed on the actions column type to open the menu.", "TreeDataExpandIcon": "Icon displayed on the tree data toggling column when the children are collapsed", - "TreeDataCollapseIcon": "Icon displayed on the tree data toggling column when the children are expanded" + "TreeDataCollapseIcon": "Icon displayed on the tree data toggling column when the children are expanded", + "GroupingCriteriaExpandIcon": "Icon displayed on the grouping column when the children are collapsed", + "GroupingCriteriaCollapseIcon": "Icon displayed on the grouping column when the children are expanded" } } diff --git a/docs/translations/api-docs/data-grid/data-grid-zh.json b/docs/translations/api-docs/data-grid/data-grid-zh.json index 09c12bd44d2c4..d04ef2c78e1d7 100644 --- a/docs/translations/api-docs/data-grid/data-grid-zh.json +++ b/docs/translations/api-docs/data-grid/data-grid-zh.json @@ -439,6 +439,8 @@ "ExportIcon": "Icon displayed on the open export button present in the toolbar by default.", "MoreActionsIcon": "Icon displayed on the actions column type to open the menu.", "TreeDataExpandIcon": "Icon displayed on the tree data toggling column when the children are collapsed", - "TreeDataCollapseIcon": "Icon displayed on the tree data toggling column when the children are expanded" + "TreeDataCollapseIcon": "Icon displayed on the tree data toggling column when the children are expanded", + "GroupingCriteriaExpandIcon": "Icon displayed on the grouping column when the children are collapsed", + "GroupingCriteriaCollapseIcon": "Icon displayed on the grouping column when the children are expanded" } } diff --git a/docs/translations/api-docs/data-grid/data-grid.json b/docs/translations/api-docs/data-grid/data-grid.json index 09c12bd44d2c4..d04ef2c78e1d7 100644 --- a/docs/translations/api-docs/data-grid/data-grid.json +++ b/docs/translations/api-docs/data-grid/data-grid.json @@ -439,6 +439,8 @@ "ExportIcon": "Icon displayed on the open export button present in the toolbar by default.", "MoreActionsIcon": "Icon displayed on the actions column type to open the menu.", "TreeDataExpandIcon": "Icon displayed on the tree data toggling column when the children are collapsed", - "TreeDataCollapseIcon": "Icon displayed on the tree data toggling column when the children are expanded" + "TreeDataCollapseIcon": "Icon displayed on the tree data toggling column when the children are expanded", + "GroupingCriteriaExpandIcon": "Icon displayed on the grouping column when the children are collapsed", + "GroupingCriteriaCollapseIcon": "Icon displayed on the grouping column when the children are expanded" } } diff --git a/packages/grid/_modules_/grid/components/cell/GridBooleanCell.tsx b/packages/grid/_modules_/grid/components/cell/GridBooleanCell.tsx index 5dd830e83913b..ef072e82d3411 100644 --- a/packages/grid/_modules_/grid/components/cell/GridBooleanCell.tsx +++ b/packages/grid/_modules_/grid/components/cell/GridBooleanCell.tsx @@ -18,7 +18,11 @@ const useUtilityClasses = (ownerState: OwnerState) => { return composeClasses(slots, getDataGridUtilityClass, classes); }; -export const GridBooleanCell = React.memo((props: GridRenderCellParams & SvgIconProps) => { +interface GridBooleanCellProps + extends GridRenderCellParams, + Omit {} + +export const GridBooleanCell = React.memo((props: GridBooleanCellProps) => { const { id, value, @@ -56,4 +60,10 @@ export const GridBooleanCell = React.memo((props: GridRenderCellParams & SvgIcon ); }); -export const renderBooleanCell = (params) => ; +export const renderBooleanCell = (params: GridBooleanCellProps) => { + if (params.rowNode.isAutoGenerated) { + return ''; + } + + return ; +}; diff --git a/packages/grid/_modules_/grid/components/cell/GridGroupingColumnLeafCell.tsx b/packages/grid/_modules_/grid/components/cell/GridGroupingColumnLeafCell.tsx new file mode 100644 index 0000000000000..3fa4b93759084 --- /dev/null +++ b/packages/grid/_modules_/grid/components/cell/GridGroupingColumnLeafCell.tsx @@ -0,0 +1,17 @@ +import * as React from 'react'; +import Box from '@mui/material/Box'; +import { useGridRootProps } from '../../hooks/utils/useGridRootProps'; +import { GridRenderCellParams } from '../../models/params/gridCellParams'; +import { DataGridProProcessedProps } from '../../models/props/DataGridProProps'; + +const GridGroupingColumnLeafCell = (props: GridRenderCellParams) => { + const { rowNode } = props; + + const rootProps = useGridRootProps(); + + const marginLeft = rootProps.rowGroupingColumnMode === 'multiple' ? 1 : rowNode.depth * 2; + + return {props.formattedValue ?? props.value}; +}; + +export { GridGroupingColumnLeafCell }; diff --git a/packages/grid/_modules_/grid/components/cell/GridGroupingCriteriaCell.tsx b/packages/grid/_modules_/grid/components/cell/GridGroupingCriteriaCell.tsx new file mode 100644 index 0000000000000..61bb75a2d80e1 --- /dev/null +++ b/packages/grid/_modules_/grid/components/cell/GridGroupingCriteriaCell.tsx @@ -0,0 +1,161 @@ +import * as React from 'react'; +import PropTypes from 'prop-types'; +import { unstable_composeClasses as composeClasses } from '@mui/base'; +import IconButton from '@mui/material/IconButton'; +import Box from '@mui/material/Box'; +import { useGridRootProps } from '../../hooks/utils/useGridRootProps'; +import { useGridApiContext } from '../../hooks/utils/useGridApiContext'; +import { GridRenderCellParams } from '../../models/params/gridCellParams'; +import { useGridSelector } from '../../hooks/utils/useGridSelector'; +import { gridFilteredDescendantCountLookupSelector } from '../../hooks/features/filter/gridFilterSelector'; +import { GridEvents } from '../../models/events'; +import { getDataGridUtilityClass } from '../../gridClasses'; +import { DataGridProProcessedProps } from '../../models/props/DataGridProProps'; + +type OwnerState = { classes: DataGridProProcessedProps['classes'] }; + +const useUtilityClasses = (ownerState: OwnerState) => { + const { classes } = ownerState; + + const slots = { + root: ['groupingCriteriaCell'], + toggle: ['groupingCriteriaCellToggle'], + }; + + return composeClasses(slots, getDataGridUtilityClass, classes); +}; + +interface GridGroupingCriteriaCellProps extends GridRenderCellParams { + hideDescendantCount?: boolean; +} + +const GridGroupingCriteriaCell = (props: GridGroupingCriteriaCellProps) => { + const { id, field, rowNode, hideDescendantCount } = props; + + const rootProps = useGridRootProps(); + const apiRef = useGridApiContext(); + const ownerState: OwnerState = { classes: rootProps.classes }; + const classes = useUtilityClasses(ownerState); + const filteredDescendantCountLookup = useGridSelector( + apiRef, + gridFilteredDescendantCountLookupSelector, + ); + const filteredDescendantCount = filteredDescendantCountLookup[rowNode.id] ?? 0; + + const Icon = rowNode.childrenExpanded + ? rootProps.components.GroupingCriteriaCollapseIcon + : rootProps.components.GroupingCriteriaExpandIcon; + + const handleKeyDown = (event) => { + if (event.key === ' ') { + event.stopPropagation(); + } + apiRef.current.publishEvent(GridEvents.cellKeyDown, props, event); + }; + + const handleClick = (event: React.MouseEvent) => { + apiRef.current.setRowChildrenExpansion(id, !rowNode.childrenExpanded); + apiRef.current.setCellFocus(id, field); + event.stopPropagation(); + }; + + const marginLeft = rootProps.rowGroupingColumnMode === 'multiple' ? 0 : rowNode.depth * 2; + + return ( + +
+ {filteredDescendantCount > 0 && ( + + + + )} +
+ + {rowNode.groupingKey} + {!hideDescendantCount && filteredDescendantCount > 0 ? ` (${filteredDescendantCount})` : ''} + +
+ ); +}; + +GridGroupingCriteriaCell.propTypes = { + // ----------------------------- Warning -------------------------------- + // | These PropTypes are generated from the TypeScript type definitions | + // | To update them edit the TypeScript types and run "yarn proptypes" | + // ---------------------------------------------------------------------- + /** + * GridApi that let you manipulate the grid. + */ + api: PropTypes.any.isRequired, + /** + * The mode of the cell. + */ + cellMode: PropTypes.oneOf(['edit', 'view']).isRequired, + /** + * The column of the row that the current cell belongs to. + */ + colDef: PropTypes.object.isRequired, + /** + * The column field of the cell that triggered the event. + */ + field: PropTypes.string.isRequired, + /** + * The cell value formatted with the column valueFormatter. + */ + formattedValue: PropTypes.oneOfType([ + PropTypes.instanceOf(Date), + PropTypes.number, + PropTypes.object, + PropTypes.string, + PropTypes.bool, + ]), + /** + * Get the cell value of a row and field. + * @param {GridRowId} id The row id. + * @param {string} field The field. + * @returns {GridCellValue} The cell value. + */ + getValue: PropTypes.func.isRequired, + /** + * If true, the cell is the active element. + */ + hasFocus: PropTypes.bool.isRequired, + /** + * The grid row id. + */ + id: PropTypes.oneOfType([PropTypes.number, PropTypes.string]).isRequired, + /** + * If true, the cell is editable. + */ + isEditable: PropTypes.bool, + /** + * The row model of the row that the current cell belongs to. + */ + row: PropTypes.object.isRequired, + /** + * the tabIndex value. + */ + tabIndex: PropTypes.oneOf([-1, 0]).isRequired, + /** + * The cell value, but if the column has valueGetter, use getValue. + */ + value: PropTypes.oneOfType([ + PropTypes.instanceOf(Date), + PropTypes.number, + PropTypes.object, + PropTypes.string, + PropTypes.bool, + ]), +} as any; + +export { GridGroupingCriteriaCell }; diff --git a/packages/grid/_modules_/grid/components/containers/GridRootStyles.ts b/packages/grid/_modules_/grid/components/containers/GridRootStyles.ts index c03a03dcfbdc6..55f9bbf5ae821 100644 --- a/packages/grid/_modules_/grid/components/containers/GridRootStyles.ts +++ b/packages/grid/_modules_/grid/components/containers/GridRootStyles.ts @@ -293,6 +293,16 @@ export const GridRootStyles = styled('div', { alignSelf: 'stretch', marginRight: theme.spacing(2), }, + [`& .${gridClasses.groupingCriteriaCell}`]: { + display: 'flex', + alignItems: 'center', + width: '100%', + }, + [`& .${gridClasses.groupingCriteriaCellToggle}`]: { + flex: '0 0 28px', + alignSelf: 'stretch', + marginRight: theme.spacing(2), + }, }; return gridStyle; diff --git a/packages/grid/_modules_/grid/components/icons/index.tsx b/packages/grid/_modules_/grid/components/icons/index.tsx index 395644208d92c..2c117dfb864ef 100644 --- a/packages/grid/_modules_/grid/components/icons/index.tsx +++ b/packages/grid/_modules_/grid/components/icons/index.tsx @@ -11,13 +11,13 @@ export const GridArrowDownwardIcon = createSvgIcon( 'ArrowDownward', ); -export const GridExpandMoreIcon = createSvgIcon( +export const GridKeyboardArrowRight = createSvgIcon( , 'KeyboardArrowRight', ); -export const GridExpandLessIcon = createSvgIcon( - , +export const GridExpandMoreIcon = createSvgIcon( + , 'ExpandMore', ); diff --git a/packages/grid/_modules_/grid/components/menu/columnMenu/GridRowGroupableColumnMenuItems.tsx b/packages/grid/_modules_/grid/components/menu/columnMenu/GridRowGroupableColumnMenuItems.tsx new file mode 100644 index 0000000000000..1a064e2ad7a59 --- /dev/null +++ b/packages/grid/_modules_/grid/components/menu/columnMenu/GridRowGroupableColumnMenuItems.tsx @@ -0,0 +1,63 @@ +import * as React from 'react'; +import PropTypes from 'prop-types'; +import MenuItem from '@mui/material/MenuItem'; +import { useGridApiContext } from '../../../hooks/utils/useGridApiContext'; +import { GridColDef } from '../../../models/colDef/gridColDef'; +import { useGridSelector } from '../../../hooks/utils/useGridSelector'; +import { gridRowGroupingSanitizedModelSelector } from '../../../hooks/features/rowGrouping/gridRowGroupingSelector'; +import { gridColumnLookupSelector } from '../../../hooks/features/columns/gridColumnsSelector'; + +interface GridRowGroupableColumnMenuItemsProps { + column?: GridColDef; + onClick?: (event: React.MouseEvent) => void; +} + +const GridRowGroupableColumnMenuItems = (props: GridRowGroupableColumnMenuItemsProps) => { + const { column, onClick } = props; + const apiRef = useGridApiContext(); + const rowGroupingModel = useGridSelector(apiRef, gridRowGroupingSanitizedModelSelector); + const columnsLookup = useGridSelector(apiRef, gridColumnLookupSelector); + + if (!column?.groupable) { + return null; + } + + const ungroupColumn = (event: React.MouseEvent) => { + apiRef.current.removeRowGroupingCriteria(column.field); + if (onClick) { + onClick(event); + } + }; + + const groupColumn = (event: React.MouseEvent) => { + apiRef.current.addRowGroupingCriteria(column.field); + if (onClick) { + onClick(event); + } + }; + + const name = columnsLookup[column.field].headerName ?? column.field; + + if (rowGroupingModel.includes(column.field)) { + return ( + + {apiRef.current.getLocaleText('unGroupColumn')(name)} + + ); + } + + return ( + {apiRef.current.getLocaleText('groupColumn')(name)} + ); +}; + +GridRowGroupableColumnMenuItems.propTypes = { + // ----------------------------- Warning -------------------------------- + // | These PropTypes are generated from the TypeScript type definitions | + // | To update them edit the TypeScript types and run "yarn proptypes" | + // ---------------------------------------------------------------------- + column: PropTypes.object, + onClick: PropTypes.func, +} as any; + +export { GridRowGroupableColumnMenuItems }; diff --git a/packages/grid/_modules_/grid/components/menu/columnMenu/GridRowGroupingColumnMenuItems.tsx b/packages/grid/_modules_/grid/components/menu/columnMenu/GridRowGroupingColumnMenuItems.tsx new file mode 100644 index 0000000000000..01285b97b15a6 --- /dev/null +++ b/packages/grid/_modules_/grid/components/menu/columnMenu/GridRowGroupingColumnMenuItems.tsx @@ -0,0 +1,63 @@ +import * as React from 'react'; +import PropTypes from 'prop-types'; +import MenuItem from '@mui/material/MenuItem'; +import { useGridApiContext } from '../../../hooks/utils/useGridApiContext'; +import { GridColDef } from '../../../models/colDef/gridColDef'; +import { useGridSelector } from '../../../hooks/utils/useGridSelector'; +import { gridRowGroupingSanitizedModelSelector } from '../../../hooks/features/rowGrouping/gridRowGroupingSelector'; +import { + getRowGroupingCriteriaFromGroupingField, + GRID_ROW_GROUPING_SINGLE_GROUPING_FIELD, + isGroupingColumn, +} from '../../../hooks/features/rowGrouping/gridRowGroupingUtils'; +import { gridColumnLookupSelector } from '../../../hooks/features/columns/gridColumnsSelector'; + +interface GridRowGroupingColumnMenuItemsProps { + column?: GridColDef; + onClick?: (event: React.MouseEvent) => void; +} + +const GridRowGroupingColumnMenuItems = (props: GridRowGroupingColumnMenuItemsProps) => { + const { column, onClick } = props; + const apiRef = useGridApiContext(); + const rowGroupingModel = useGridSelector(apiRef, gridRowGroupingSanitizedModelSelector); + const columnsLookup = useGridSelector(apiRef, gridColumnLookupSelector); + + const renderUnGroupingMenuItem = (field: string) => { + const ungroupColumn = (event: React.MouseEvent) => { + apiRef.current.removeRowGroupingCriteria(field); + if (onClick) { + onClick(event); + } + }; + + const name = columnsLookup[field].headerName ?? field; + + return ( + + {apiRef.current.getLocaleText('unGroupColumn')(name)} + + ); + }; + + if (!column || !isGroupingColumn(column.field)) { + return null; + } + + if (column.field === GRID_ROW_GROUPING_SINGLE_GROUPING_FIELD) { + return {rowGroupingModel.map(renderUnGroupingMenuItem)}; + } + + return renderUnGroupingMenuItem(getRowGroupingCriteriaFromGroupingField(column.field)!); +}; + +GridRowGroupingColumnMenuItems.propTypes = { + // ----------------------------- Warning -------------------------------- + // | These PropTypes are generated from the TypeScript type definitions | + // | To update them edit the TypeScript types and run "yarn proptypes" | + // ---------------------------------------------------------------------- + column: PropTypes.object, + onClick: PropTypes.func, +} as any; + +export { GridRowGroupingColumnMenuItems }; diff --git a/packages/grid/_modules_/grid/constants/defaultGridSlotsComponents.ts b/packages/grid/_modules_/grid/constants/defaultGridSlotsComponents.ts index dc7f643a3f7ac..71e349d1bdb67 100644 --- a/packages/grid/_modules_/grid/constants/defaultGridSlotsComponents.ts +++ b/packages/grid/_modules_/grid/constants/defaultGridSlotsComponents.ts @@ -35,7 +35,7 @@ import { GridViewStreamIcon, GridMoreVertIcon, GridExpandMoreIcon, - GridExpandLessIcon, + GridKeyboardArrowRight, } from '../components'; import { GridColumnUnsortedIcon } from '../components/columnHeaders/GridColumnUnsortedIcon'; import { ErrorOverlay } from '../components/ErrorOverlay'; @@ -57,8 +57,10 @@ const DEFAULT_GRID_ICON_SLOTS_COMPONENTS: GridIconSlotsComponent = { DensityComfortableIcon: GridViewStreamIcon, ExportIcon: GridSaveAltIcon, MoreActionsIcon: GridMoreVertIcon, - TreeDataCollapseIcon: GridExpandLessIcon, - TreeDataExpandIcon: GridExpandMoreIcon, + TreeDataCollapseIcon: GridExpandMoreIcon, + TreeDataExpandIcon: GridKeyboardArrowRight, + GroupingCriteriaCollapseIcon: GridExpandMoreIcon, + GroupingCriteriaExpandIcon: GridKeyboardArrowRight, }; export const DEFAULT_GRID_SLOTS_COMPONENTS: GridSlotsComponent = { diff --git a/packages/grid/_modules_/grid/constants/localeTextConstants.ts b/packages/grid/_modules_/grid/constants/localeTextConstants.ts index 7072a05194439..f41727b7785ac 100644 --- a/packages/grid/_modules_/grid/constants/localeTextConstants.ts +++ b/packages/grid/_modules_/grid/constants/localeTextConstants.ts @@ -115,6 +115,11 @@ export const GRID_DEFAULT_LOCALE_TEXT: GridLocaleText = { treeDataExpand: 'see children', treeDataCollapse: 'hide children', + // Grouping columns + groupingColumnHeaderName: 'Group', + groupColumn: (name) => `Group by ${name}`, + unGroupColumn: (name) => `Stop grouping by ${name}`, + // Used core components translation keys MuiTablePagination: {}, }; diff --git a/packages/grid/_modules_/grid/gridClasses.ts b/packages/grid/_modules_/grid/gridClasses.ts index 118c700a57719..2e1611ec74f21 100644 --- a/packages/grid/_modules_/grid/gridClasses.ts +++ b/packages/grid/_modules_/grid/gridClasses.ts @@ -392,4 +392,6 @@ export const gridClasses = generateUtilityClasses('MuiDataGrid', [ 'withBorder', 'treeDataGroupingCell', 'treeDataGroupingCellToggle', + 'groupingCriteriaCell', + 'groupingCriteriaCellToggle', ]); diff --git a/packages/grid/_modules_/grid/hooks/features/columnReorder/useGridColumnReorder.tsx b/packages/grid/_modules_/grid/hooks/features/columnReorder/useGridColumnReorder.tsx index fd78aced48fe8..2f701e227cbc7 100644 --- a/packages/grid/_modules_/grid/hooks/features/columnReorder/useGridColumnReorder.tsx +++ b/packages/grid/_modules_/grid/hooks/features/columnReorder/useGridColumnReorder.tsx @@ -138,17 +138,7 @@ export const useGridColumnReorder = ( const targetColVisibleIndex = apiRef.current.getColumnIndex(params.field, true); const targetCol = apiRef.current.getColumn(params.field); const dragColIndex = apiRef.current.getColumnIndex(dragColField, false); - const visibleColumnAmount = apiRef.current.getVisibleColumns().length; - - const canBeReordered = - !targetCol.disableReorder || - (targetColVisibleIndex > 0 && targetColVisibleIndex < visibleColumnAmount - 1); - - const canBeReorderedProcessed = apiRef.current.unstable_applyPreProcessors( - GridPreProcessingGroup.canBeReordered, - canBeReordered, - { targetIndex: targetColVisibleIndex }, - ); + const visibleColumns = apiRef.current.getVisibleColumns(); const cursorMoveDirectionX = getCursorMoveDirectionX(cursorPosition.current, coordinates); const hasMovedLeft = @@ -156,8 +146,28 @@ export const useGridColumnReorder = ( const hasMovedRight = cursorMoveDirectionX === CURSOR_MOVE_DIRECTION_RIGHT && dragColIndex < targetColIndex; - if (canBeReorderedProcessed && (hasMovedLeft || hasMovedRight)) { - apiRef.current.setColumnIndex(dragColField, targetColIndex); + if (hasMovedLeft || hasMovedRight) { + let canBeReordered: boolean; + if (!targetCol.disableReorder) { + canBeReordered = true; + } else if (hasMovedLeft) { + canBeReordered = + targetColIndex > 0 && !visibleColumns[targetColIndex - 1].disableReorder; + } else { + canBeReordered = + targetColIndex < visibleColumns.length - 1 && + !visibleColumns[targetColIndex + 1].disableReorder; + } + + const canBeReorderedProcessed = apiRef.current.unstable_applyPreProcessors( + GridPreProcessingGroup.canBeReordered, + canBeReordered, + { targetIndex: targetColVisibleIndex }, + ); + + if (canBeReorderedProcessed) { + apiRef.current.setColumnIndex(dragColField, targetColIndex); + } } cursorPosition.current = coordinates; diff --git a/packages/grid/_modules_/grid/hooks/features/filter/gridFilterState.ts b/packages/grid/_modules_/grid/hooks/features/filter/gridFilterState.ts index 2963cba9ed1b1..036a164c501ac 100644 --- a/packages/grid/_modules_/grid/hooks/features/filter/gridFilterState.ts +++ b/packages/grid/_modules_/grid/hooks/features/filter/gridFilterState.ts @@ -1,4 +1,4 @@ -import { GridLinkOperator } from '../../../models/gridFilterItem'; +import { GridFilterItem, GridLinkOperator } from '../../../models/gridFilterItem'; import { GridFilterModel } from '../../../models/gridFilterModel'; import { GridRowId } from '../../../models/gridRows'; @@ -30,8 +30,17 @@ export interface GridFilterInitialState { filterModel?: GridFilterModel; } +/** + * @param {GridRowId} rowId The id of the row we want to filter. + * @param {(filterItem: GridFilterItem) => boolean} shouldApplyItem An optional callback to allow the filtering engine to only apply some items. + */ +export type GridAggregatedFilterItemApplier = ( + rowId: GridRowId, + shouldApplyItem?: (filterItem: GridFilterItem) => boolean, +) => boolean; + export interface GridFilteringParams { - isRowMatchingFilters: ((rowId: GridRowId) => boolean) | null; + isRowMatchingFilters: GridAggregatedFilterItemApplier | null; } export type GridFilteringMethod = ( diff --git a/packages/grid/_modules_/grid/hooks/features/filter/gridFilterUtils.ts b/packages/grid/_modules_/grid/hooks/features/filter/gridFilterUtils.ts index f139c1dddee4e..53f1a9d5014a7 100644 --- a/packages/grid/_modules_/grid/hooks/features/filter/gridFilterUtils.ts +++ b/packages/grid/_modules_/grid/hooks/features/filter/gridFilterUtils.ts @@ -5,8 +5,12 @@ import { GridLinkOperator, GridRowId, } from '../../../models'; +import { GridAggregatedFilterItemApplier } from './gridFilterState'; -type GridFilterItemApplier = (rowId: GridRowId) => boolean; +type GridFilterItemApplier = { + fn: (rowId: GridRowId) => boolean; + item: GridFilterItem; +}; /** * Adds default values to the optional fields of a filter items. @@ -35,12 +39,12 @@ export const cleanFilterItem = (item: GridFilterItem, apiRef: GridApiRef) => { * Generates a method to easily check if a row is matching the current filter model. * @param {GridFilterModel} filterModel The model with which we want to filter the rows. * @param {GridApiRef} apiRef The API of the grid. - * @returns {GridFilterItemApplier | null} A method that checks if a row is matching the current filter model. If `null`, we consider that all the rows are matching the filters. + * @returns {GridAggregatedFilterItemApplier | null} A method that checks if a row is matching the current filter model. If `null`, we consider that all the rows are matching the filters. */ export const buildAggregatedFilterApplier = ( filterModel: GridFilterModel, apiRef: GridApiRef, -): GridFilterItemApplier | null => { +): GridAggregatedFilterItemApplier | null => { const { items, linkOperator = GridLinkOperator.And } = filterModel; const getFilterCallbackFromItem = (filterItem: GridFilterItem): GridFilterItemApplier | null => { @@ -83,11 +87,13 @@ export const buildAggregatedFilterApplier = ( return null; } - return (rowId: GridRowId) => { + const fn = (rowId: GridRowId) => { const cellParams = apiRef.current.getCellParams(rowId, newFilterItem.columnField!); return applyFilterOnRow(cellParams); }; + + return { fn, item: newFilterItem }; }; const appliers = items @@ -98,13 +104,17 @@ export const buildAggregatedFilterApplier = ( return null; } - return (rowId: GridRowId) => { + return (rowId, shouldApplyFilter) => { + const filteredAppliers = shouldApplyFilter + ? appliers.filter((applier) => shouldApplyFilter(applier.item)) + : appliers; + // Return `false` as soon as we have a failing filter if (linkOperator === GridLinkOperator.And) { - return appliers.every((applier) => applier(rowId)); + return filteredAppliers.every((applier) => applier.fn(rowId)); } // Return `true` as soon as we have a passing filter - return appliers.some((applier) => applier(rowId)); + return filteredAppliers.some((applier) => applier.fn(rowId)); }; }; diff --git a/packages/grid/_modules_/grid/hooks/features/index.ts b/packages/grid/_modules_/grid/hooks/features/index.ts index b10be95b03a3e..456895c72454c 100644 --- a/packages/grid/_modules_/grid/hooks/features/index.ts +++ b/packages/grid/_modules_/grid/hooks/features/index.ts @@ -15,3 +15,4 @@ export * from './selection'; export * from './sorting'; export * from './dimensions'; export * from './treeData'; +export * from './rowGrouping'; diff --git a/packages/grid/_modules_/grid/hooks/features/rowGrouping/createGroupingColDef.tsx b/packages/grid/_modules_/grid/hooks/features/rowGrouping/createGroupingColDef.tsx new file mode 100644 index 0000000000000..ce4e976c595ea --- /dev/null +++ b/packages/grid/_modules_/grid/hooks/features/rowGrouping/createGroupingColDef.tsx @@ -0,0 +1,360 @@ +import * as React from 'react'; +import { + GRID_STRING_COL_DEF, + GridApiRef, + GridColDef, + GridGroupingColDefOverride, + GridRenderCellParams, + GridStateColDef, + GridValueGetterFullParams, + GridComparatorFn, +} from '../../../models'; +import { GridColumnRawLookup } from '../columns/gridColumnsState'; +import { GridGroupingCriteriaCell } from '../../../components/cell/GridGroupingCriteriaCell'; +import { GridGroupingColumnLeafCell } from '../../../components/cell/GridGroupingColumnLeafCell'; +import { + getRowGroupingFieldFromGroupingCriteria, + GRID_ROW_GROUPING_SINGLE_GROUPING_FIELD, +} from './gridRowGroupingUtils'; +import { gridRowGroupingSanitizedModelSelector } from './gridRowGroupingSelector'; + +const GROUPING_COL_DEF_DEFAULT_PROPERTIES: Omit = { + ...GRID_STRING_COL_DEF, + disableReorder: true, +}; + +const GROUPING_COL_DEF_FORCED_PROPERTIES: Pick = { + type: 'rowGroupByColumnsGroup', + editable: false, + groupable: 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. + * TODO: Make this index comparator depth invariant, the logic should not be inverted when sorting in the "desc" direction (but the current return format of `sortComparator` does not support this behavior). + */ +const groupingFieldIndexComparator: GridComparatorFn = (v1, v2, cellParams1, cellParams2) => { + const model = gridRowGroupingSanitizedModelSelector(cellParams1.api.state); + const groupingField1 = cellParams1.rowNode.groupingField; + const groupingField2 = cellParams2.rowNode.groupingField; + + if (groupingField1 === groupingField2) { + return 0; + } + + if (groupingField1 == null) { + return -1; + } + + if (groupingField2 == null) { + return 1; + } + + if (model.indexOf(groupingField1) < model.indexOf(groupingField2)) { + return -1; + } + + return 1; +}; + +const getLeafProperties = (leafColDef: GridColDef): Partial => ({ + headerName: leafColDef.headerName ?? leafColDef.field, + sortable: leafColDef.sortable, + filterable: leafColDef.filterable, + filterOperators: leafColDef.filterOperators?.map((operator) => ({ + ...operator, + getApplyFilterFn: (filterItem, column) => { + const originalFn = operator.getApplyFilterFn(filterItem, column); + if (!originalFn) { + return null; + } + + return (params) => { + // We only want to filter leaves + if (params.rowNode.groupingField != null) { + return true; + } + + return originalFn(params); + }; + }, + })), + sortComparator: (v1, v2, cellParams1, cellParams2) => { + // We only want to sort the leaves + if (cellParams1.rowNode.groupingField === null && cellParams2.rowNode.groupingField === null) { + return leafColDef.sortComparator!(v1, v2, cellParams1, cellParams2); + } + + return groupingFieldIndexComparator(v1, v2, cellParams1, cellParams2); + }, +}); + +const getGroupingCriteriaProperties = (groupedByColDef: GridColDef, applyHeaderName: boolean) => { + const properties: Partial = { + sortable: groupedByColDef.sortable, + filterable: groupedByColDef.filterable, + sortComparator: (v1, v2, cellParams1, cellParams2) => { + // We only want to sort the groups of the current grouping criteria + if ( + cellParams1.rowNode.groupingField === groupedByColDef.field && + cellParams2.rowNode.groupingField === groupedByColDef.field + ) { + return groupedByColDef.sortComparator!(v1, v2, cellParams1, cellParams2); + } + + return groupingFieldIndexComparator(v1, v2, cellParams1, cellParams2); + }, + filterOperators: groupedByColDef.filterOperators?.map((operator) => ({ + ...operator, + getApplyFilterFn: (filterItem, column) => { + const originalFn = operator.getApplyFilterFn(filterItem, column); + if (!originalFn) { + return null; + } + + return (params) => { + // We only want to filter the groups of the current grouping criteria + if (params.rowNode.groupingField !== groupedByColDef.field) { + return true; + } + + return originalFn(params); + }; + }, + })), + }; + + if (applyHeaderName) { + properties.headerName = groupedByColDef.headerName ?? groupedByColDef.field; + } + + return properties; +}; + +interface CreateGroupingColDefMonoCriteriaParams { + columnsLookup: GridColumnRawLookup; + /** + * The field from which we are grouping the rows. + */ + groupingCriteria: string; + /** + * The col def from which we are grouping the rows. + */ + groupedByColDef: GridColDef | GridStateColDef; + /** + * The col def properties the user wants to override. + * This value comes `prop.groupingColDef`. + */ + colDefOverride: GridGroupingColDefOverride | null | undefined; +} + +/** + * Creates the `GridColDef` for a grouping column that only takes care of a single grouping criteria + */ +export const createGroupingColDefForOneGroupingCriteria = ({ + columnsLookup, + groupedByColDef, + groupingCriteria, + colDefOverride, +}: CreateGroupingColDefMonoCriteriaParams): GridColDef => { + const { leafField, mainGroupingCriteria, hideDescendantCount, ...colDefOverrideProperties } = + colDefOverride ?? {}; + const leafColDef = leafField ? columnsLookup[leafField] : null; + + // 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( + (groupedByColDef.width ?? GRID_STRING_COL_DEF.width!) + 40, + leafColDef?.width ?? 0, + ), + renderCell: (params: GridRenderCellParams) => { + // Render leaves + if (params.rowNode.groupingField == null) { + if (leafColDef) { + const leafParams: GridRenderCellParams = { + ...params.api.getCellParams(params.id, leafField!), + api: params.api, + }; + if (leafColDef.renderCell) { + return leafColDef.renderCell(leafParams); + } + + return ; + } + + return null; + } + + // Render current grouping criteria groups + if (params.rowNode.groupingField === groupingCriteria) { + return ; + } + + return null; + }, + valueGetter: (params) => { + const fullParams = params as GridValueGetterFullParams; + if (!fullParams.rowNode) { + return undefined; + } + + if (fullParams.rowNode.groupingField == null) { + if (leafColDef) { + return fullParams.api.getCellValue(params.id, leafField!); + } + + return undefined; + } + + if (fullParams.rowNode.groupingField === groupingCriteria) { + return fullParams.rowNode.groupingKey; + } + + return undefined; + }, + }; + + // If we have a `mainGroupingCriteria` defined and matching the `groupingCriteria` + // Then we apply the sorting / filtering on the groups of this column's grouping criteria based on the properties of `groupedByColDef`. + // It can be useful to define a `leafField` for leaves rendering but still use the grouping criteria for the sorting / filtering + // + // If we have a `leafField` defined and matching an existing column + // Then we apply the sorting / filtering on the leaves based on the properties of `leavesColDef` + // + // By default, we apply the sorting / filtering on the groups of this column's grouping criteria based on the properties of `groupedColDef`. + let sourceProperties: Partial; + if (mainGroupingCriteria && mainGroupingCriteria === groupingCriteria) { + sourceProperties = getGroupingCriteriaProperties(groupedByColDef, true); + } else if (leafColDef) { + sourceProperties = getLeafProperties(leafColDef); + } else { + sourceProperties = getGroupingCriteriaProperties(groupedByColDef, true); + } + + // The properties that can't be overridden with `colDefOverride` + const forcedProperties: Pick = { + field: getRowGroupingFieldFromGroupingCriteria(groupingCriteria), + ...GROUPING_COL_DEF_FORCED_PROPERTIES, + }; + + return { + ...GROUPING_COL_DEF_DEFAULT_PROPERTIES, + ...commonProperties, + ...sourceProperties, + ...colDefOverrideProperties, + ...forcedProperties, + }; +}; + +interface CreateGroupingColDefSeveralCriteriaParams { + apiRef: GridApiRef; + columnsLookup: GridColumnRawLookup; + + /** + * The fields from which we are grouping the rows. + */ + rowGroupingModel: string[]; + + /** + * The col def properties the user wants to override. + * This value comes `prop.groupingColDef`. + */ + colDefOverride: GridGroupingColDefOverride | null | undefined; +} + +/** + * Creates the `GridColDef` for a grouping column that takes care of all the grouping criteria + */ +export const createGroupingColDefForAllGroupingCriteria = ({ + apiRef, + columnsLookup, + rowGroupingModel, + colDefOverride, +}: CreateGroupingColDefSeveralCriteriaParams): GridColDef => { + const { leafField, mainGroupingCriteria, hideDescendantCount, ...colDefOverrideProperties } = + colDefOverride ?? {}; + const leafColDef = leafField ? columnsLookup[leafField] : null; + + // 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'), + width: Math.max( + ...rowGroupingModel.map( + (field) => (columnsLookup[field].width ?? GRID_STRING_COL_DEF.width!) + 40, + ), + leafColDef?.width ?? 0, + ), + renderCell: (params) => { + // Render the leaves + if (params.rowNode.groupingField == null) { + if (leafColDef) { + const leafParams: GridRenderCellParams = { + ...params.api.getCellParams(params.id, leafField!), + api: params.api, + }; + if (leafColDef.renderCell) { + return leafColDef.renderCell(leafParams); + } + + return ; + } + + return null; + } + + // Render the groups + return ; + }, + valueGetter: (params) => { + const fullParams = params as GridValueGetterFullParams; + if (!fullParams.rowNode) { + return undefined; + } + + if (fullParams.rowNode.groupingField == null) { + if (leafColDef) { + return fullParams.api.getCellValue(params.id, leafField!); + } + + return undefined; + } + + return fullParams.rowNode.groupingKey; + }, + }; + + // If we have a `mainGroupingCriteria` defined and matching one of the `orderedGroupedByFields` + // Then we apply the sorting / filtering on the groups of this column's grouping criteria based on the properties of `columnsLookup[mainGroupingCriteria]`. + // It can be useful to use another grouping criteria than the top level one for the sorting / filtering + // + // If we have a `leafField` defined and matching an existing column + // Then we apply the sorting / filtering on the leaves based on the properties of `leavesColDef` + // + // By default, we apply the sorting / filtering on the groups of the top level grouping criteria based on the properties of `columnsLookup[orderedGroupedByFields[0]]`. + let sourceProperties: Partial; + if (mainGroupingCriteria && rowGroupingModel.includes(mainGroupingCriteria)) { + sourceProperties = getGroupingCriteriaProperties(columnsLookup[mainGroupingCriteria], true); + } else if (leafColDef) { + sourceProperties = getLeafProperties(leafColDef); + } else { + sourceProperties = getGroupingCriteriaProperties( + columnsLookup[rowGroupingModel[0]], + rowGroupingModel.length === 1, + ); + } + + // The properties that can't be overridden with `colDefOverride` + const forcedProperties: Pick = { + field: GRID_ROW_GROUPING_SINGLE_GROUPING_FIELD, + ...GROUPING_COL_DEF_FORCED_PROPERTIES, + }; + + return { + ...GROUPING_COL_DEF_DEFAULT_PROPERTIES, + ...commonProperties, + ...sourceProperties, + ...colDefOverrideProperties, + ...forcedProperties, + }; +}; diff --git a/packages/grid/_modules_/grid/hooks/features/rowGrouping/gridRowGroupingInterfaces.ts b/packages/grid/_modules_/grid/hooks/features/rowGrouping/gridRowGroupingInterfaces.ts new file mode 100644 index 0000000000000..0c78c47e49e05 --- /dev/null +++ b/packages/grid/_modules_/grid/hooks/features/rowGrouping/gridRowGroupingInterfaces.ts @@ -0,0 +1,34 @@ +export type GridRowGroupingModel = string[]; + +export interface GridRowGroupingState { + model: GridRowGroupingModel; +} + +export interface GridRowGroupingInitialState { + model?: GridRowGroupingModel; +} + +export interface GridRowGroupingApi { + /** + * Sets the columns to use as grouping criteria. + * @param {GridRowGroupingModel} model The columns to use as grouping criteria. + */ + setRowGroupingModel: (model: GridRowGroupingModel) => void; + /** + * Adds the field to the row grouping model. + * @param {string} groupingCriteriaField The field from which we want to group the rows. + * @param {number | undefined} groupingIndex The grouping index at which we want to insert the new grouping criteria. By default, it will be inserted at the end of the model. + */ + addRowGroupingCriteria: (groupingCriteriaField: string, groupingIndex?: number) => void; + /** + * sRemove the field from the row grouping model. + * @param {string} groupingCriteriaField The field from which we want to stop grouping the rows. + */ + removeRowGroupingCriteria: (groupingCriteriaField: string) => void; + /** + * Sets the grouping index of a grouping criteria. + * @param {string} groupingCriteriaField The field of the grouping criteria from which we want to change the grouping index. + * @param {number} groupingIndex The new grouping index of this grouping criteria. + */ + setRowGroupingCriteriaIndex: (groupingCriteriaField: string, groupingIndex: number) => void; +} diff --git a/packages/grid/_modules_/grid/hooks/features/rowGrouping/gridRowGroupingSelector.ts b/packages/grid/_modules_/grid/hooks/features/rowGrouping/gridRowGroupingSelector.ts new file mode 100644 index 0000000000000..72558ef4bc08d --- /dev/null +++ b/packages/grid/_modules_/grid/hooks/features/rowGrouping/gridRowGroupingSelector.ts @@ -0,0 +1,17 @@ +import { createSelector } from 'reselect'; +import { GridState } from '../../../models'; +import { gridColumnLookupSelector } from '../columns'; + +export const gridRowGroupingStateSelector = (state: GridState) => state.rowGrouping; + +export const gridRowGroupingModelSelector = createSelector( + gridRowGroupingStateSelector, + (rowGrouping) => rowGrouping.model, +); + +export const gridRowGroupingSanitizedModelSelector = createSelector( + gridRowGroupingModelSelector, + gridColumnLookupSelector, + (model, columnsLookup) => + model.filter((field) => !!columnsLookup[field] && columnsLookup[field].groupable), +); diff --git a/packages/grid/_modules_/grid/hooks/features/rowGrouping/gridRowGroupingUtils.ts b/packages/grid/_modules_/grid/hooks/features/rowGrouping/gridRowGroupingUtils.ts new file mode 100644 index 0000000000000..be89667aaf8b9 --- /dev/null +++ b/packages/grid/_modules_/grid/hooks/features/rowGrouping/gridRowGroupingUtils.ts @@ -0,0 +1,145 @@ +import { + GridFilterItem, + GridRowId, + GridRowTreeConfig, + GridRowTreeNodeConfig, +} from '../../../models'; +import { GridFilterState } from '../filter'; +import { DataGridProProcessedProps } from '../../../models/props/DataGridProProps'; +import { GridAggregatedFilterItemApplier } from '../filter/gridFilterState'; + +export const GRID_ROW_GROUPING_SINGLE_GROUPING_FIELD = '__row_group_by_columns_group__'; + +export const GROUPING_COLUMNS_FEATURE_NAME = 'grouping-columns'; + +export const getRowGroupingFieldFromGroupingCriteria = (groupingCriteria: string | null) => { + if (groupingCriteria === null) { + return GRID_ROW_GROUPING_SINGLE_GROUPING_FIELD; + } + + return `__row_group_by_columns_group_${groupingCriteria}__`; +}; + +export const getRowGroupingCriteriaFromGroupingField = (groupingColDefField: string) => { + const match = groupingColDefField.match(/^__row_group_by_columns_group_(.*)__$/); + + if (!match) { + return null; + } + + return match[1]; +}; + +export const isGroupingColumn = (field: string) => + field === GRID_ROW_GROUPING_SINGLE_GROUPING_FIELD || + getRowGroupingCriteriaFromGroupingField(field) !== null; + +interface FilterRowTreeFromTreeDataParams { + rowTree: GridRowTreeConfig; + isRowMatchingFilters: GridAggregatedFilterItemApplier | null; +} + +/** + * When filtering a group, we only want to filter according to the items related to this grouping column. + */ +const shouldApplyFilterItemOnGroup = (item: GridFilterItem, node: GridRowTreeNodeConfig) => { + if (item.columnField === GRID_ROW_GROUPING_SINGLE_GROUPING_FIELD) { + return true; + } + + const groupingCriteriaField = getRowGroupingCriteriaFromGroupingField(item.columnField); + + return groupingCriteriaField === node.groupingField; +}; + +/** + * A leaf is visible if it passed the filter + * A group is visible if all the following criteria are met: + * - One of its children is passing the filter + * - It is passing the filter + */ +export const filterRowTreeFromGroupingColumns = ( + params: FilterRowTreeFromTreeDataParams, +): Pick => { + const { rowTree, isRowMatchingFilters } = params; + const visibleRowsLookup: Record = {}; + const filteredDescendantCountLookup: Record = {}; + + const filterTreeNode = ( + node: GridRowTreeNodeConfig, + areAncestorsPassingChildren: boolean, + areAncestorsExpanded: boolean, + ): number => { + let isMatchingFilters: boolean; + if (!isRowMatchingFilters) { + isMatchingFilters = true; + } else { + const shouldApplyItem = node.isAutoGenerated + ? (item: GridFilterItem) => shouldApplyFilterItemOnGroup(item, node) + : undefined; + + isMatchingFilters = isRowMatchingFilters(node.id, shouldApplyItem); + } + + let filteredDescendantCount = 0; + node.children?.forEach((childId) => { + const childNode = rowTree[childId]; + const childSubTreeSize = filterTreeNode( + childNode, + areAncestorsPassingChildren && isMatchingFilters, + areAncestorsExpanded && !!node.childrenExpanded, + ); + filteredDescendantCount += childSubTreeSize; + }); + + let shouldPassFilters: boolean; + if (!areAncestorsPassingChildren) { + shouldPassFilters = false; + } else if (node.children?.length) { + shouldPassFilters = isMatchingFilters && filteredDescendantCount > 0; + } else { + shouldPassFilters = isMatchingFilters; + } + + visibleRowsLookup[node.id] = shouldPassFilters && areAncestorsExpanded; + + if (!shouldPassFilters) { + return 0; + } + + filteredDescendantCountLookup[node.id] = filteredDescendantCount; + + if (!node.children) { + return filteredDescendantCount + 1; + } + + return filteredDescendantCount; + }; + + const nodes = Object.values(rowTree); + for (let i = 0; i < nodes.length; i += 1) { + const node = nodes[i]; + if (node.depth === 0) { + filterTreeNode(node, true, true); + } + } + + return { + visibleRowsLookup, + filteredDescendantCountLookup, + }; +}; + +export const getColDefOverrides = ( + groupingColDefProp: DataGridProProcessedProps['groupingColDef'], + fields: string[], +) => { + if (typeof groupingColDefProp === 'function') { + return groupingColDefProp({ + groupingName: GROUPING_COLUMNS_FEATURE_NAME, + fields, + }); + } + + return groupingColDefProp; +}; diff --git a/packages/grid/_modules_/grid/hooks/features/rowGrouping/index.ts b/packages/grid/_modules_/grid/hooks/features/rowGrouping/index.ts new file mode 100644 index 0000000000000..4a716de788980 --- /dev/null +++ b/packages/grid/_modules_/grid/hooks/features/rowGrouping/index.ts @@ -0,0 +1,6 @@ +export * from './gridRowGroupingSelector'; +export * from './gridRowGroupingInterfaces'; +export { + GRID_ROW_GROUPING_SINGLE_GROUPING_FIELD, + getRowGroupingFieldFromGroupingCriteria, +} from './gridRowGroupingUtils'; diff --git a/packages/grid/_modules_/grid/hooks/features/rowGrouping/useGridRowGrouping.tsx b/packages/grid/_modules_/grid/hooks/features/rowGrouping/useGridRowGrouping.tsx new file mode 100644 index 0000000000000..d33343953475f --- /dev/null +++ b/packages/grid/_modules_/grid/hooks/features/rowGrouping/useGridRowGrouping.tsx @@ -0,0 +1,493 @@ +import * as React from 'react'; +import Divider from '@mui/material/Divider'; +import type { + GridApiRef, + GridRowModel, + GridRowId, + GridColDef, + GridKeyValue, + GridGroupingValueGetterParams, + GridStateColDef, +} from '../../../models'; +import { GridEvents, GridEventListener } from '../../../models/events'; +import { GridRowGroupingPreProcessing } from '../../core/rowGroupsPerProcessing'; +import { useFirstRender } from '../../utils/useFirstRender'; +import { buildRowTree, BuildRowTreeGroupingCriteria } from '../../../utils/tree/buildRowTree'; +import { useGridApiEventHandler } from '../../utils/useGridApiEventHandler'; +import { + gridRowGroupingModelSelector, + gridRowGroupingSanitizedModelSelector, +} from './gridRowGroupingSelector'; +import { DataGridProProcessedProps } from '../../../models/props/DataGridProProps'; +import { + filterRowTreeFromGroupingColumns, + getRowGroupingFieldFromGroupingCriteria, + getColDefOverrides, + GROUPING_COLUMNS_FEATURE_NAME, + isGroupingColumn, +} from './gridRowGroupingUtils'; +import { + createGroupingColDefForOneGroupingCriteria, + createGroupingColDefForAllGroupingCriteria, +} from './createGroupingColDef'; +import { isDeepEqual } from '../../../utils/utils'; +import { GridPreProcessingGroup, useGridRegisterPreProcessor } from '../../core/preProcessing'; +import { GridColumnRawLookup, GridColumnsRawState } from '../columns/gridColumnsState'; +import { useGridRegisterFilteringMethod } from '../filter/useGridRegisterFilteringMethod'; +import { GridFilteringMethod } from '../filter/gridFilterState'; +import { gridRowIdsSelector, gridRowTreeSelector } from '../rows'; +import { useGridRegisterSortingMethod } from '../sorting/useGridRegisterSortingMethod'; +import { GridSortingMethod } from '../sorting/gridSortingState'; +import { sortRowTree } from '../../../utils/tree/sortRowTree'; +import { gridFilteredDescendantCountLookupSelector } from '../filter'; +import { useGridStateInit } from '../../utils/useGridStateInit'; +import { GridRowGroupingApi, GridRowGroupingModel } from './gridRowGroupingInterfaces'; +import { useGridApiMethod } from '../../utils'; +import { gridColumnLookupSelector } from '../columns'; +import { GridRowGroupableColumnMenuItems } from '../../../components/menu/columnMenu/GridRowGroupableColumnMenuItems'; +import { GridRowGroupingColumnMenuItems } from '../../../components/menu/columnMenu/GridRowGroupingColumnMenuItems'; + +/** + * Only available in DataGridPro + * @requires useGridColumns (state, method) - can be after, async only + * @requires useGridRows (state, method) - can be after, async only + * @requires useGridParamsApi (method) - can be after, async only + * TODO: Move the the Premium plan once available and remove the `experimentalFeatures.rowGrouping` flag + */ +export const useGridRowGrouping = ( + apiRef: GridApiRef, + props: Pick< + DataGridProProcessedProps, + | 'initialState' + | 'rowGroupingModel' + | 'onRowGroupingModelChange' + | 'defaultGroupingExpansionDepth' + | 'isGroupExpandedByDefault' + | 'groupingColDef' + | 'rowGroupingColumnMode' + | 'disableRowGrouping' + >, +) => { + useGridStateInit(apiRef, (state) => ({ + ...state, + rowGrouping: { + model: props.rowGroupingModel ?? props.initialState?.rowGrouping?.model ?? [], + }, + })); + + apiRef.current.unstable_updateControlState({ + stateId: 'rowGrouping', + propModel: props.rowGroupingModel, + propOnChange: props.onRowGroupingModelChange, + stateSelector: gridRowGroupingModelSelector, + changeEvent: GridEvents.rowGroupingModelChange, + }); + + /** + * ROW GROUPING + */ + // Tracks the model on the last pre-processing to check if we need to re-build the grouping columns when the grid upserts a column. + const sanitizedModelOnLastRowPreProcessing = React.useRef([]); + + const updateRowGrouping = React.useCallback(() => { + const groupRows: GridRowGroupingPreProcessing = (params) => { + const rowGroupingModel = gridRowGroupingSanitizedModelSelector(apiRef.current.state); + const columnsLookup = gridColumnLookupSelector(apiRef.current.state); + sanitizedModelOnLastRowPreProcessing.current = rowGroupingModel; + + if (props.disableRowGrouping || rowGroupingModel.length === 0) { + return null; + } + + const distinctValues: { + [field: string]: { lookup: { [val: string]: boolean }; list: any[] }; + } = Object.fromEntries( + rowGroupingModel.map((groupingField) => [groupingField, { lookup: {}, list: [] }]), + ); + + const getCellGroupingCriteria = ({ + row, + id, + colDef, + }: { + row: GridRowModel; + id: GridRowId; + colDef: GridColDef; + }) => { + let key: GridKeyValue | null | undefined; + if (colDef.groupingValueGetter) { + const groupingValueGetterParams: GridGroupingValueGetterParams = { + colDef, + field: colDef.field, + value: row[colDef.field], + id, + row, + rowNode: { + isAutoGenerated: false, + id, + }, + }; + key = colDef.groupingValueGetter(groupingValueGetterParams); + } else { + key = row[colDef.field] as GridKeyValue | null | undefined; + } + + return { + key, + field: colDef.field, + }; + }; + + params.ids.forEach((rowId) => { + const row = params.idRowsLookup[rowId]; + + rowGroupingModel.forEach((groupingCriteria) => { + const { key } = getCellGroupingCriteria({ + row, + id: rowId, + colDef: columnsLookup[groupingCriteria], + }); + const groupingFieldsDistinctKeys = distinctValues[groupingCriteria]; + + if (key != null && !groupingFieldsDistinctKeys.lookup[key.toString()]) { + groupingFieldsDistinctKeys.lookup[key.toString()] = true; + groupingFieldsDistinctKeys.list.push(key); + } + }); + }); + + const rows = params.ids.map((rowId) => { + const row = params.idRowsLookup[rowId]; + const parentPath = rowGroupingModel + .map((groupingField) => + getCellGroupingCriteria({ + row, + id: rowId, + colDef: columnsLookup[groupingField], + }), + ) + .filter((cell) => cell.key != null) as BuildRowTreeGroupingCriteria[]; + + const leafGroupingCriteria: BuildRowTreeGroupingCriteria = { + key: rowId.toString(), + field: null, + }; + + return { + path: [...parentPath, leafGroupingCriteria], + id: rowId, + }; + }); + + return buildRowTree({ + ...params, + rows, + defaultGroupingExpansionDepth: props.defaultGroupingExpansionDepth, + isGroupExpandedByDefault: props.isGroupExpandedByDefault, + groupingName: GROUPING_COLUMNS_FEATURE_NAME, + }); + }; + + return apiRef.current.unstable_registerRowGroupsBuilder('rowGrouping', groupRows); + }, [ + apiRef, + props.defaultGroupingExpansionDepth, + props.isGroupExpandedByDefault, + props.disableRowGrouping, + ]); + + useFirstRender(() => { + updateRowGrouping(); + }); + + const isFirstRender = React.useRef(true); + React.useEffect(() => { + if (isFirstRender.current) { + isFirstRender.current = false; + return () => {}; + } + + return updateRowGrouping(); + }, [updateRowGrouping]); + + /** + * PRE-PROCESSING + */ + const getGroupingColDefs = React.useCallback( + (columnsState: GridColumnsRawState) => { + if (props.disableRowGrouping) { + return []; + } + + const groupingColDefProp = props.groupingColDef; + + // We can't use `gridGroupingRowsSanitizedModelSelector` here because the new columns are not in the state yet + const rowGroupingModel = gridRowGroupingModelSelector(apiRef.current.state).filter( + (field) => !!columnsState.lookup[field], + ); + + if (rowGroupingModel.length === 0) { + return []; + } + + switch (props.rowGroupingColumnMode) { + case 'single': { + return [ + createGroupingColDefForAllGroupingCriteria({ + apiRef, + rowGroupingModel, + colDefOverride: getColDefOverrides(groupingColDefProp, rowGroupingModel), + columnsLookup: columnsState.lookup, + }), + ]; + } + + case 'multiple': { + return rowGroupingModel.map((groupingCriteria) => + createGroupingColDefForOneGroupingCriteria({ + groupingCriteria, + colDefOverride: getColDefOverrides(groupingColDefProp, [groupingCriteria]), + groupedByColDef: columnsState.lookup[groupingCriteria], + columnsLookup: columnsState.lookup, + }), + ); + } + + default: { + return []; + } + } + }, + [apiRef, props.groupingColDef, props.rowGroupingColumnMode, props.disableRowGrouping], + ); + + const updateGroupingColumn = React.useCallback( + (columnsState: GridColumnsRawState) => { + const groupingColDefs = getGroupingColDefs(columnsState); + let newColumnFields: string[] = []; + const newColumnsLookup: GridColumnRawLookup = {}; + + // We only keep the non-grouping columns + columnsState.all.forEach((field) => { + if (!isGroupingColumn(field)) { + newColumnFields.push(field); + newColumnsLookup[field] = columnsState.lookup[field]; + } + }); + + // We add the grouping column + groupingColDefs.forEach((groupingColDef) => { + const matchingGroupingColDef = columnsState.lookup[groupingColDef.field]; + if (matchingGroupingColDef) { + groupingColDef.width = matchingGroupingColDef.width; + groupingColDef.flex = matchingGroupingColDef.flex; + } + + newColumnsLookup[groupingColDef.field] = groupingColDef; + }); + const startIndex = newColumnFields[0] === '__check__' ? 1 : 0; + newColumnFields = [ + ...newColumnFields.slice(0, startIndex), + ...groupingColDefs.map((colDef) => colDef.field), + ...newColumnFields.slice(startIndex), + ]; + + columnsState.all = newColumnFields; + columnsState.lookup = newColumnsLookup; + + return columnsState; + }, + [getGroupingColDefs], + ); + + const addColumnMenuButtons = React.useCallback( + (initialValue: JSX.Element[], columns: GridStateColDef) => { + if (props.disableRowGrouping) { + return initialValue; + } + + let menuItems: React.ReactNode; + if (isGroupingColumn(columns.field)) { + menuItems = ; + } else if (columns.groupable) { + menuItems = ; + } else { + menuItems = null; + } + + if (menuItems == null) { + return initialValue; + } + + return [...initialValue, , menuItems]; + }, + [props.disableRowGrouping], + ); + + const filteringMethod = React.useCallback( + (params) => { + const rowTree = gridRowTreeSelector(apiRef.current.state); + + return filterRowTreeFromGroupingColumns({ + rowTree, + isRowMatchingFilters: params.isRowMatchingFilters, + }); + }, + [apiRef], + ); + + const sortingMethod = React.useCallback( + (params) => { + const rowTree = gridRowTreeSelector(apiRef.current.state); + const rowIds = gridRowIdsSelector(apiRef.current.state); + + return sortRowTree({ + rowTree, + rowIds, + sortRowList: params.sortRowList, + disableChildrenSorting: false, + }); + }, + [apiRef], + ); + + useGridRegisterPreProcessor(apiRef, GridPreProcessingGroup.hydrateColumns, updateGroupingColumn); + useGridRegisterPreProcessor(apiRef, GridPreProcessingGroup.columnMenu, addColumnMenuButtons); + useGridRegisterFilteringMethod(apiRef, GROUPING_COLUMNS_FEATURE_NAME, filteringMethod); + useGridRegisterSortingMethod(apiRef, GROUPING_COLUMNS_FEATURE_NAME, sortingMethod); + + /** + * API METHODS + */ + const setRowGroupingModel = React.useCallback( + (model) => { + const currentModel = gridRowGroupingModelSelector(apiRef.current.state); + if (currentModel !== model) { + apiRef.current.setState((state) => ({ + ...state, + rowGrouping: { ...state.rowGrouping, model }, + })); + updateRowGrouping(); + apiRef.current.forceUpdate(); + } + }, + [apiRef, updateRowGrouping], + ); + + const addRowGroupingCriteria = React.useCallback( + (field, groupingIndex) => { + const currentModel = gridRowGroupingModelSelector(apiRef.current.state); + if (currentModel.includes(field)) { + return; + } + + const cleanGroupingIndex = groupingIndex ?? currentModel.length; + + const updatedModel = [ + ...currentModel.slice(0, cleanGroupingIndex), + field, + ...currentModel.slice(cleanGroupingIndex), + ]; + + apiRef.current.setRowGroupingModel(updatedModel); + }, + [apiRef], + ); + + const removeRowGroupingCriteria = React.useCallback< + GridRowGroupingApi['removeRowGroupingCriteria'] + >( + (field) => { + const currentModel = gridRowGroupingModelSelector(apiRef.current.state); + if (!currentModel.includes(field)) { + return; + } + apiRef.current.setRowGroupingModel(currentModel.filter((el) => el !== field)); + }, + [apiRef], + ); + + const setRowGroupingCriteriaIndex = React.useCallback< + GridRowGroupingApi['setRowGroupingCriteriaIndex'] + >( + (field, targetIndex) => { + const currentModel = gridRowGroupingModelSelector(apiRef.current.state); + const currentTargetIndex = currentModel.indexOf(field); + + if (currentTargetIndex === -1) { + return; + } + + const updatedModel = [...currentModel]; + updatedModel.splice(targetIndex, 0, updatedModel.splice(currentTargetIndex, 1)[0]); + + apiRef.current.setRowGroupingModel(updatedModel); + }, + [apiRef], + ); + + useGridApiMethod( + apiRef, + { + setRowGroupingModel, + addRowGroupingCriteria, + removeRowGroupingCriteria, + setRowGroupingCriteriaIndex, + }, + 'GridRowGroupingApi', + ); + + /** + * EVENTS + */ + const handleCellKeyDown = React.useCallback>( + (params, event) => { + const cellParams = apiRef.current.getCellParams(params.id, params.field); + if (isGroupingColumn(cellParams.field) && event.key === ' ' && !event.shiftKey) { + event.stopPropagation(); + event.preventDefault(); + + const filteredDescendantCount = + gridFilteredDescendantCountLookupSelector(apiRef.current.state)[params.id] ?? 0; + + const isOnGroupingCell = + props.rowGroupingColumnMode === 'single' || + getRowGroupingFieldFromGroupingCriteria(params.rowNode.groupingField) === params.field; + if (!isOnGroupingCell || filteredDescendantCount === 0) { + return; + } + + apiRef.current.setRowChildrenExpansion(params.id, !params.rowNode.childrenExpanded); + } + }, + [apiRef, props.rowGroupingColumnMode], + ); + + const checkGroupingColumnsModelDiff = React.useCallback< + GridEventListener + >(() => { + const rowGroupingModel = gridRowGroupingSanitizedModelSelector(apiRef.current.state); + const lastGroupingColumnsModelApplied = sanitizedModelOnLastRowPreProcessing.current; + + if (!isDeepEqual(lastGroupingColumnsModelApplied, rowGroupingModel)) { + sanitizedModelOnLastRowPreProcessing.current = rowGroupingModel; + + // Refresh the column pre-processing + apiRef.current.updateColumns([]); + updateRowGrouping(); + } + }, [apiRef, updateRowGrouping]); + + useGridApiEventHandler(apiRef, GridEvents.cellKeyDown, handleCellKeyDown); + useGridApiEventHandler(apiRef, GridEvents.columnsChange, checkGroupingColumnsModelDiff); + useGridApiEventHandler(apiRef, GridEvents.rowGroupingModelChange, checkGroupingColumnsModelDiff); + + /** + * EFFECTS + */ + React.useEffect(() => { + if (props.rowGroupingModel !== undefined) { + apiRef.current.setRowGroupingModel(props.rowGroupingModel); + } + }, [apiRef, props.rowGroupingModel]); +}; diff --git a/packages/grid/_modules_/grid/hooks/features/rows/useGridParamsApi.ts b/packages/grid/_modules_/grid/hooks/features/rows/useGridParamsApi.ts index b7705bf484dc5..f0682311f6805 100644 --- a/packages/grid/_modules_/grid/hooks/features/rows/useGridParamsApi.ts +++ b/packages/grid/_modules_/grid/hooks/features/rows/useGridParamsApi.ts @@ -2,7 +2,7 @@ import * as React from 'react'; import { GridApiRef } from '../../../models/api/gridApiRef'; import { GridParamsApi } from '../../../models/api/gridParamsApi'; import { GridRowId } from '../../../models/gridRows'; -import { GridCellParams, GridValueGetterFullParams } from '../../../models/params/gridCellParams'; +import { GridCellParams, GridValueGetterParams } from '../../../models/params/gridCellParams'; import { GridColumnHeaderParams } from '../../../models/params/gridColumnHeaderParams'; import { GridRowParams } from '../../../models/params/gridRowParams'; import { @@ -100,7 +100,7 @@ export function useGridParamsApi(apiRef: GridApiRef) { const cellFocus = gridFocusCellSelector(apiRef.current.state); const cellTabIndex = gridTabIndexCellSelector(apiRef.current.state); - const params: GridValueGetterFullParams = { + const params: GridValueGetterParams = { id, field, row, diff --git a/packages/grid/_modules_/grid/hooks/features/sorting/gridSortingUtils.ts b/packages/grid/_modules_/grid/hooks/features/sorting/gridSortingUtils.ts index 8d7ae8ad129bb..4b82215f3a095 100644 --- a/packages/grid/_modules_/grid/hooks/features/sorting/gridSortingUtils.ts +++ b/packages/grid/_modules_/grid/hooks/features/sorting/gridSortingUtils.ts @@ -39,6 +39,7 @@ const parseSortItem = (sortItem: GridSortItem, apiRef: GridApiRef): GridParsedSo const getSortCellParams = (id: GridRowId): GridSortCellParams => ({ id, field: column.field, + rowNode: apiRef.current.getRowNode(id)!, value: apiRef.current.getCellValue(id, column.field), api: apiRef.current, }); diff --git a/packages/grid/_modules_/grid/hooks/features/treeData/gridTreeDataGroupColDef.ts b/packages/grid/_modules_/grid/hooks/features/treeData/gridTreeDataGroupColDef.ts index 4e4ef0a874fbb..d69123155d34f 100644 --- a/packages/grid/_modules_/grid/hooks/features/treeData/gridTreeDataGroupColDef.ts +++ b/packages/grid/_modules_/grid/hooks/features/treeData/gridTreeDataGroupColDef.ts @@ -21,8 +21,9 @@ export const GRID_TREE_DATA_GROUPING_FIELD = '__tree_data_group__'; export const GRID_TREE_DATA_GROUPING_COL_DEF_FORCED_PROPERTIES: Pick< GridColDef, - 'field' | 'editable' + 'field' | 'editable' | 'groupable' > = { field: GRID_TREE_DATA_GROUPING_FIELD, editable: false, + groupable: false, }; diff --git a/packages/grid/_modules_/grid/hooks/features/treeData/gridTreeDataUtils.ts b/packages/grid/_modules_/grid/hooks/features/treeData/gridTreeDataUtils.ts index 7cd83e20d863b..41b4b163316f9 100644 --- a/packages/grid/_modules_/grid/hooks/features/treeData/gridTreeDataUtils.ts +++ b/packages/grid/_modules_/grid/hooks/features/treeData/gridTreeDataUtils.ts @@ -1,10 +1,11 @@ import { GridFilterState } from '../filter'; import { GridRowId, GridRowTreeConfig, GridRowTreeNodeConfig } from '../../../models'; +import { GridAggregatedFilterItemApplier } from '../filter/gridFilterState'; interface FilterRowTreeFromTreeDataParams { rowTree: GridRowTreeConfig; disableChildrenFiltering: boolean; - isRowMatchingFilters: ((rowId: GridRowId) => boolean) | null; + isRowMatchingFilters: GridAggregatedFilterItemApplier | null; } /** diff --git a/packages/grid/_modules_/grid/hooks/features/treeData/useGridTreeData.tsx b/packages/grid/_modules_/grid/hooks/features/treeData/useGridTreeData.tsx index 7a492d34e6f72..eeb53e048085e 100644 --- a/packages/grid/_modules_/grid/hooks/features/treeData/useGridTreeData.tsx +++ b/packages/grid/_modules_/grid/hooks/features/treeData/useGridTreeData.tsx @@ -109,18 +109,18 @@ export const useGridTreeData = ( * PRE-PROCESSING */ const getGroupingColDef = React.useCallback((): GridColDef => { - const propGroupingColDef = props.groupingColDef; + const groupingColDefProp = props.groupingColDef; let colDefOverride: GridGroupingColDefOverride | null | undefined; - if (typeof propGroupingColDef === 'function') { + if (typeof groupingColDefProp === 'function') { const params: GridGroupingColDefOverrideParams = { groupingName: TREE_DATA_GROUPING_NAME, fields: [], }; - colDefOverride = propGroupingColDef(params); + colDefOverride = groupingColDefProp(params); } else { - colDefOverride = propGroupingColDef; + colDefOverride = groupingColDefProp; } const { hideDescendantCount, ...colDefOverrideProperties } = colDefOverride ?? {}; diff --git a/packages/grid/_modules_/grid/hooks/utils/useGridRootProps.ts b/packages/grid/_modules_/grid/hooks/utils/useGridRootProps.ts index b941adac1034f..9a20b69e02e0a 100644 --- a/packages/grid/_modules_/grid/hooks/utils/useGridRootProps.ts +++ b/packages/grid/_modules_/grid/hooks/utils/useGridRootProps.ts @@ -1,7 +1,11 @@ import * as React from 'react'; import { GridRootPropsContext } from '../../context/GridRootPropsContext'; +import type { DataGridProcessedProps } from '../../models/props/DataGridProps'; +import type { DataGridProProcessedProps } from '../../models/props/DataGridProProps'; -export const useGridRootProps = () => { +export const useGridRootProps = < + Props extends DataGridProcessedProps | DataGridProProcessedProps, +>() => { const contextValue = React.useContext(GridRootPropsContext); if (!contextValue) { @@ -10,5 +14,5 @@ export const useGridRootProps = () => { ); } - return contextValue; + return contextValue as Props; }; diff --git a/packages/grid/_modules_/grid/locales/arSD.ts b/packages/grid/_modules_/grid/locales/arSD.ts index c30d6cc05b0bc..c89e64aa50f20 100644 --- a/packages/grid/_modules_/grid/locales/arSD.ts +++ b/packages/grid/_modules_/grid/locales/arSD.ts @@ -114,6 +114,11 @@ const arSDGrid: Partial = { // treeDataGroupingHeaderName: 'Group', // treeDataExpand: 'see children', // treeDataCollapse: 'hide children', + + // Grouping columns + // groupingColumnHeaderName: 'Group', + // groupColumn: name => `Group by ${name}`, + // unGroupColumn: name => `Stop grouping by ${name}`, }; export const arSD: Localization = getGridLocalization(arSDGrid, arSDCore); diff --git a/packages/grid/_modules_/grid/locales/bgBG.ts b/packages/grid/_modules_/grid/locales/bgBG.ts index 34d76fb70ad37..856a4995408ad 100644 --- a/packages/grid/_modules_/grid/locales/bgBG.ts +++ b/packages/grid/_modules_/grid/locales/bgBG.ts @@ -113,6 +113,11 @@ const bgBGGrid: Partial = { // treeDataGroupingHeaderName: 'Group', // treeDataExpand: 'see children', // treeDataCollapse: 'hide children', + + // Grouping columns + // groupingColumnHeaderName: 'Group', + // groupColumn: name => `Group by ${name}`, + // unGroupColumn: name => `Stop grouping by ${name}`, }; export const bgBG: Localization = getGridLocalization(bgBGGrid, bgBGCore); diff --git a/packages/grid/_modules_/grid/locales/deDE.ts b/packages/grid/_modules_/grid/locales/deDE.ts index 9d70db9d8da5b..5885a53581840 100644 --- a/packages/grid/_modules_/grid/locales/deDE.ts +++ b/packages/grid/_modules_/grid/locales/deDE.ts @@ -116,6 +116,11 @@ const deDEGrid: Partial = { treeDataGroupingHeaderName: 'Gruppe', treeDataExpand: 'Kinder einblenden', treeDataCollapse: 'Kinder ausblenden', + + // Grouping columns + // groupingColumnHeaderName: 'Group', + // groupColumn: name => `Group by ${name}`, + // unGroupColumn: name => `Stop grouping by ${name}`, }; export const deDE: Localization = getGridLocalization(deDEGrid, deDECore); diff --git a/packages/grid/_modules_/grid/locales/elGR.ts b/packages/grid/_modules_/grid/locales/elGR.ts index 7c0544a6b3cf7..1362aea43ca9e 100644 --- a/packages/grid/_modules_/grid/locales/elGR.ts +++ b/packages/grid/_modules_/grid/locales/elGR.ts @@ -115,6 +115,11 @@ const elGRGrid: Partial = { // treeDataGroupingHeaderName: 'Group', // treeDataExpand: 'see children', // treeDataCollapse: 'hide children', + + // Grouping columns + // groupingColumnHeaderName: 'Group', + // groupColumn: name => `Group by ${name}`, + // unGroupColumn: name => `Stop grouping by ${name}`, }; export const elGR: Localization = getGridLocalization(elGRGrid); diff --git a/packages/grid/_modules_/grid/locales/esES.ts b/packages/grid/_modules_/grid/locales/esES.ts index ff2513d58b51e..b98164faf4ba8 100644 --- a/packages/grid/_modules_/grid/locales/esES.ts +++ b/packages/grid/_modules_/grid/locales/esES.ts @@ -116,6 +116,11 @@ const esESGrid: Partial = { // treeDataGroupingHeaderName: 'Group', // treeDataExpand: 'see children', // treeDataCollapse: 'hide children', + + // Grouping columns + // groupingColumnHeaderName: 'Group', + // groupColumn: name => `Group by ${name}`, + // unGroupColumn: name => `Stop grouping by ${name}`, }; export const esES: Localization = getGridLocalization(esESGrid, esESCore); diff --git a/packages/grid/_modules_/grid/locales/faIR.ts b/packages/grid/_modules_/grid/locales/faIR.ts index 002b0a5c03cce..b737fcd834e92 100644 --- a/packages/grid/_modules_/grid/locales/faIR.ts +++ b/packages/grid/_modules_/grid/locales/faIR.ts @@ -116,6 +116,11 @@ const faIRGrid: Partial = { // treeDataGroupingHeaderName: 'Group', // treeDataExpand: 'see children', // treeDataCollapse: 'hide children', + + // Grouping columns + // groupingColumnHeaderName: 'Group', + // groupColumn: name => `Group by ${name}`, + // unGroupColumn: name => `Stop grouping by ${name}`, }; export const faIR: Localization = getGridLocalization(faIRGrid, faIRCore); diff --git a/packages/grid/_modules_/grid/locales/fiFI.ts b/packages/grid/_modules_/grid/locales/fiFI.ts index f70c01fedfad9..5672bd203715e 100644 --- a/packages/grid/_modules_/grid/locales/fiFI.ts +++ b/packages/grid/_modules_/grid/locales/fiFI.ts @@ -116,6 +116,11 @@ const fiFIGrid: Partial = { treeDataGroupingHeaderName: 'Ryhmä', treeDataExpand: 'Laajenna', treeDataCollapse: 'Supista', + + // Grouping columns + // groupingColumnHeaderName: 'Group', + // groupColumn: name => `Group by ${name}`, + // unGroupColumn: name => `Stop grouping by ${name}`, }; export const fiFI: Localization = getGridLocalization(fiFIGrid, fiFICore); diff --git a/packages/grid/_modules_/grid/locales/frFR.ts b/packages/grid/_modules_/grid/locales/frFR.ts index cccad917b74a1..4fd80cfef547c 100644 --- a/packages/grid/_modules_/grid/locales/frFR.ts +++ b/packages/grid/_modules_/grid/locales/frFR.ts @@ -116,6 +116,11 @@ const frFRGrid: Partial = { treeDataGroupingHeaderName: 'Groupe', treeDataExpand: 'afficher les enfants', treeDataCollapse: 'masquer les enfants', + + // Grouping columns + // groupingColumnHeaderName: 'Group', + // groupColumn: name => `Group by ${name}`, + // unGroupColumn: name => `Stop grouping by ${name}`, }; export const frFR: Localization = getGridLocalization(frFRGrid, frFRCore); diff --git a/packages/grid/_modules_/grid/locales/heIL.ts b/packages/grid/_modules_/grid/locales/heIL.ts index 94de246c08bdc..0a37b11e4bb4b 100644 --- a/packages/grid/_modules_/grid/locales/heIL.ts +++ b/packages/grid/_modules_/grid/locales/heIL.ts @@ -114,6 +114,11 @@ const heILGrid: Partial = { treeDataGroupingHeaderName: 'קבוצה', treeDataExpand: 'הרחב', treeDataCollapse: 'כווץ', + + // Grouping columns + // groupingColumnHeaderName: 'Group', + // groupColumn: name => `Group by ${name}`, + // unGroupColumn: name => `Stop grouping by ${name}`, }; export const heIL: Localization = getGridLocalization(heILGrid, heILCore); diff --git a/packages/grid/_modules_/grid/locales/itIT.ts b/packages/grid/_modules_/grid/locales/itIT.ts index 74916ec5350ee..be7c0d5a4be90 100644 --- a/packages/grid/_modules_/grid/locales/itIT.ts +++ b/packages/grid/_modules_/grid/locales/itIT.ts @@ -116,6 +116,11 @@ const itITGrid: Partial = { // treeDataGroupingHeaderName: 'Group', // treeDataExpand: 'see children', // treeDataCollapse: 'hide children', + + // Grouping columns + // groupingColumnHeaderName: 'Group', + // groupColumn: name => `Group by ${name}`, + // unGroupColumn: name => `Stop grouping by ${name}`, }; export const itIT: Localization = getGridLocalization(itITGrid, itITCore); diff --git a/packages/grid/_modules_/grid/locales/jaJP.ts b/packages/grid/_modules_/grid/locales/jaJP.ts index fa9c6060b8386..8be31a9420cde 100644 --- a/packages/grid/_modules_/grid/locales/jaJP.ts +++ b/packages/grid/_modules_/grid/locales/jaJP.ts @@ -111,6 +111,11 @@ const jaJPGrid: Partial = { // treeDataGroupingHeaderName: 'Group', // treeDataExpand: 'see children', // treeDataCollapse: 'hide children', + + // Grouping columns + // groupingColumnHeaderName: 'Group', + // groupColumn: name => `Group by ${name}`, + // unGroupColumn: name => `Stop grouping by ${name}`, }; export const jaJP: Localization = getGridLocalization(jaJPGrid, jaJPCore); diff --git a/packages/grid/_modules_/grid/locales/koKR.ts b/packages/grid/_modules_/grid/locales/koKR.ts index e3f26a765c8b9..4481bbe752f70 100644 --- a/packages/grid/_modules_/grid/locales/koKR.ts +++ b/packages/grid/_modules_/grid/locales/koKR.ts @@ -111,6 +111,11 @@ const koKRGrid: Partial = { treeDataGroupingHeaderName: '그룹', treeDataExpand: '하위노드 펼치기', treeDataCollapse: '하위노드 접기', + + // Grouping columns + // groupingColumnHeaderName: 'Group', + // groupColumn: name => `Group by ${name}`, + // unGroupColumn: name => `Stop grouping by ${name}`, }; export const koKR: Localization = getGridLocalization(koKRGrid, koKRCore); diff --git a/packages/grid/_modules_/grid/locales/nlNL.ts b/packages/grid/_modules_/grid/locales/nlNL.ts index 791053caebd9c..2db7c595cc2d8 100644 --- a/packages/grid/_modules_/grid/locales/nlNL.ts +++ b/packages/grid/_modules_/grid/locales/nlNL.ts @@ -116,6 +116,11 @@ const nlNLGrid: Partial = { treeDataGroupingHeaderName: 'Groep', treeDataExpand: 'Uitvouwen', treeDataCollapse: 'Inklappen', + + // Grouping columns + // groupingColumnHeaderName: 'Group', + // groupColumn: name => `Group by ${name}`, + // unGroupColumn: name => `Stop grouping by ${name}`, }; export const nlNL: Localization = getGridLocalization(nlNLGrid, nlNLCore); diff --git a/packages/grid/_modules_/grid/locales/plPL.ts b/packages/grid/_modules_/grid/locales/plPL.ts index aeda0abdbd4e5..9961d34b4a72a 100644 --- a/packages/grid/_modules_/grid/locales/plPL.ts +++ b/packages/grid/_modules_/grid/locales/plPL.ts @@ -111,6 +111,11 @@ const plPLGrid: Partial = { // treeDataGroupingHeaderName: 'Group', // treeDataExpand: 'see children', // treeDataCollapse: 'hide children', + + // Grouping columns + // groupingColumnHeaderName: 'Group', + // groupColumn: name => `Group by ${name}`, + // unGroupColumn: name => `Stop grouping by ${name}`, }; export const plPL: Localization = getGridLocalization(plPLGrid, plPLCore); diff --git a/packages/grid/_modules_/grid/locales/ptBR.ts b/packages/grid/_modules_/grid/locales/ptBR.ts index be0aa19fbea17..b78df114ba3af 100644 --- a/packages/grid/_modules_/grid/locales/ptBR.ts +++ b/packages/grid/_modules_/grid/locales/ptBR.ts @@ -116,6 +116,11 @@ const ptBRGrid: Partial = { // treeDataGroupingHeaderName: 'Group', // treeDataExpand: 'see children', // treeDataCollapse: 'hide children', + + // Grouping columns + groupingColumnHeaderName: 'Grupo', + groupColumn: (name) => `Agrupar por ${name}`, + unGroupColumn: (name) => `Parar agrupamento por ${name}`, }; export const ptBR: Localization = getGridLocalization(ptBRGrid, ptBRCore); diff --git a/packages/grid/_modules_/grid/locales/ruRU.ts b/packages/grid/_modules_/grid/locales/ruRU.ts index caf26da40d6c3..17d553301fd2a 100644 --- a/packages/grid/_modules_/grid/locales/ruRU.ts +++ b/packages/grid/_modules_/grid/locales/ruRU.ts @@ -144,6 +144,11 @@ const ruRUGrid: Partial = { treeDataGroupingHeaderName: 'Группа', treeDataExpand: 'показать дочерние элементы', treeDataCollapse: 'скрыть дочерние элементы', + + // Grouping columns + // groupingColumnHeaderName: 'Group', + // groupColumn: name => `Group by ${name}`, + // unGroupColumn: name => `Stop grouping by ${name}`, }; export const ruRU: Localization = getGridLocalization(ruRUGrid, ruRUCore); diff --git a/packages/grid/_modules_/grid/locales/skSK.ts b/packages/grid/_modules_/grid/locales/skSK.ts index 236c432cc483e..ba2d540ac5c44 100644 --- a/packages/grid/_modules_/grid/locales/skSK.ts +++ b/packages/grid/_modules_/grid/locales/skSK.ts @@ -140,6 +140,11 @@ const skSKGrid: Partial = { // treeDataGroupingHeaderName: 'Group', // treeDataExpand: 'see children', // treeDataCollapse: 'hide children', + + // Grouping columns + // groupingColumnHeaderName: 'Group', + // groupColumn: name => `Group by ${name}`, + // unGroupColumn: name => `Stop grouping by ${name}`, }; export const skSK: Localization = getGridLocalization(skSKGrid, skSKCore); diff --git a/packages/grid/_modules_/grid/locales/trTR.ts b/packages/grid/_modules_/grid/locales/trTR.ts index ad25e76030f33..83e2ffd9407ea 100644 --- a/packages/grid/_modules_/grid/locales/trTR.ts +++ b/packages/grid/_modules_/grid/locales/trTR.ts @@ -111,6 +111,11 @@ const trTRGrid: Partial = { // treeDataGroupingHeaderName: 'Group', // treeDataExpand: 'see children', // treeDataCollapse: 'hide children', + + // Grouping columns + // groupingColumnHeaderName: 'Group', + // groupColumn: name => `Group by ${name}`, + // unGroupColumn: name => `Stop grouping by ${name}`, }; export const trTR: Localization = getGridLocalization(trTRGrid, trTRCore); diff --git a/packages/grid/_modules_/grid/locales/ukUA.ts b/packages/grid/_modules_/grid/locales/ukUA.ts index b1c366022d84a..d541ff2207a05 100644 --- a/packages/grid/_modules_/grid/locales/ukUA.ts +++ b/packages/grid/_modules_/grid/locales/ukUA.ts @@ -145,6 +145,11 @@ const ukUAGrid: Partial = { treeDataGroupingHeaderName: 'Група', treeDataExpand: 'показати дочірні елементи', treeDataCollapse: 'приховати дочірні елементи', + + // Grouping columns + // groupingColumnHeaderName: 'Group', + // groupColumn: name => `Group by ${name}`, + // unGroupColumn: name => `Stop grouping by ${name}`, }; export const ukUA: Localization = getGridLocalization(ukUAGrid, ukUACore); diff --git a/packages/grid/_modules_/grid/locales/viVN.ts b/packages/grid/_modules_/grid/locales/viVN.ts index 68730467d766f..c96981feed0d3 100644 --- a/packages/grid/_modules_/grid/locales/viVN.ts +++ b/packages/grid/_modules_/grid/locales/viVN.ts @@ -114,6 +114,11 @@ const viVNGrid: Partial = { treeDataGroupingHeaderName: 'Nhóm', treeDataExpand: 'mở rộng', treeDataCollapse: 'ẩn đi', + + // Grouping columns + // groupingColumnHeaderName: 'Group', + // groupColumn: name => `Group by ${name}`, + // unGroupColumn: name => `Stop grouping by ${name}`, }; export const viVN: Localization = getGridLocalization(viVNGrid, viVNCore); diff --git a/packages/grid/_modules_/grid/locales/zhCN.ts b/packages/grid/_modules_/grid/locales/zhCN.ts index f13c85f18554e..081cd3b8065b8 100644 --- a/packages/grid/_modules_/grid/locales/zhCN.ts +++ b/packages/grid/_modules_/grid/locales/zhCN.ts @@ -112,6 +112,11 @@ const zhCNGrid: Partial = { // treeDataGroupingHeaderName: 'Group', // treeDataExpand: 'see children', // treeDataCollapse: 'hide children', + + // Grouping columns + // groupingColumnHeaderName: 'Group', + // groupColumn: name => `Group by ${name}`, + // unGroupColumn: name => `Stop grouping by ${name}`, }; export const zhCN: Localization = getGridLocalization(zhCNGrid, zhCNCore); diff --git a/packages/grid/_modules_/grid/models/api/gridApi.ts b/packages/grid/_modules_/grid/models/api/gridApi.ts index 02013968489ad..95084fa547ad2 100644 --- a/packages/grid/_modules_/grid/models/api/gridApi.ts +++ b/packages/grid/_modules_/grid/models/api/gridApi.ts @@ -22,6 +22,7 @@ import { GridColumnPinningApi } from './gridColumnPinningApi'; import type { GridPreProcessingApi } from '../../hooks/core/preProcessing'; import type { GridRowGroupsPreProcessingApi } from '../../hooks/core/rowGroupsPerProcessing'; import type { GridDimensionsApi } from '../../hooks/features/dimensions'; +import type { GridRowGroupingApi } from '../../hooks/features/rowGrouping'; import type { GridPaginationApi } from '../../hooks/features/pagination'; /** @@ -52,4 +53,5 @@ export interface GridApi GridLocaleTextApi, GridClipboardApi, GridScrollApi, + GridRowGroupingApi, GridColumnPinningApi {} diff --git a/packages/grid/_modules_/grid/models/api/gridLocaleTextApi.ts b/packages/grid/_modules_/grid/models/api/gridLocaleTextApi.ts index 8457c66238771..40aef43bff101 100644 --- a/packages/grid/_modules_/grid/models/api/gridLocaleTextApi.ts +++ b/packages/grid/_modules_/grid/models/api/gridLocaleTextApi.ts @@ -112,6 +112,11 @@ export interface GridLocaleText { treeDataExpand: string; treeDataCollapse: string; + // Grouping columns + groupingColumnHeaderName: string; + groupColumn: (name: string) => string; + unGroupColumn: (name: string) => string; + // Used core components translation keys MuiTablePagination: Omit< ComponentsPropsList['MuiTablePagination'], diff --git a/packages/grid/_modules_/grid/models/colDef/gridColDef.ts b/packages/grid/_modules_/grid/models/colDef/gridColDef.ts index 09d5e21397dae..fd268bffc2581 100644 --- a/packages/grid/_modules_/grid/models/colDef/gridColDef.ts +++ b/packages/grid/_modules_/grid/models/colDef/gridColDef.ts @@ -5,6 +5,7 @@ import { GridColumnHeaderClassNamePropType } from '../gridColumnHeaderClass'; import { GridFilterOperator } from '../gridFilterOperator'; import { GridCellParams, + GridGroupingValueGetterParams, GridRenderCellParams, GridRenderEditCellParams, GridValueFormatterParams, @@ -92,6 +93,11 @@ export interface GridColDef { * @default false */ editable?: boolean; + /** + * If `true`, the rows can be grouped based on this column values (pro-plan only). + * @default true + */ + groupable?: boolean; /** * If `false`, the menu items for column pinning menu will not be rendered. * Only available in DataGridPro. @@ -121,6 +127,12 @@ export interface GridColDef { * @returns {GridCellValue} The cell value. */ valueGetter?: (params: GridValueGetterParams) => GridCellValue; + /** + * Function that transforms a complex cell value into a key that be used for grouping the rows. + * @param {GridGroupingValueGetterParams} params Object containing parameters for the getter. + * @returns {GridKeyValue | null | undefined} The cell key. + */ + groupingValueGetter?: (params: GridGroupingValueGetterParams) => GridKeyValue | null | undefined; /** * Function that allows to customize how the entered value is stored in the row. * It only works with cell/row editing. @@ -244,8 +256,28 @@ export interface GridColumnsMeta { export interface GridGroupingColDefOverride extends Omit< GridColDef, - 'editable' | 'valueSetter' | 'field' | 'preProcessEditCellProps' | 'renderEditCell' + | 'editable' + | 'valueSetter' + | 'field' + | 'type' + | 'preProcessEditCellProps' + | 'renderEditCell' + | 'groupable' > { + /** + * The field from which we want to apply the sorting and the filtering for the grouping column. + * It is only useful when `props.rowGroupingColumnMode === "multiple"` to decide which grouping criteria should be used for sorting and filtering. + * Do not have any effect when building the tree with the `props.treeData` feature. + * @default: The sorting and filtering is applied based on the leaf field in any, otherwise based on top level grouping criteria. + */ + mainGroupingCriteria?: string; + + /** + * The field from which we want to render the leaves of the tree. + * Do not have any effect when building the tree with the `props.treeData` feature. + */ + leafField?: string; + /** * If `true`, the grouping cells will not render the amount of descendants. * @default: false diff --git a/packages/grid/_modules_/grid/models/colDef/gridStringColDef.ts b/packages/grid/_modules_/grid/models/colDef/gridStringColDef.ts index 450459e3dcf1d..a792461e4b778 100644 --- a/packages/grid/_modules_/grid/models/colDef/gridStringColDef.ts +++ b/packages/grid/_modules_/grid/models/colDef/gridStringColDef.ts @@ -11,6 +11,7 @@ export const GRID_STRING_COL_DEF: GridColTypeDef = { sortable: true, resizable: true, filterable: true, + groupable: true, pinnable: true, editable: false, sortComparator: gridStringNumberComparer, diff --git a/packages/grid/_modules_/grid/models/events/gridEventLookup.ts b/packages/grid/_modules_/grid/models/events/gridEventLookup.ts index 6daa745ba3c55..02f5941d0e97e 100644 --- a/packages/grid/_modules_/grid/models/events/gridEventLookup.ts +++ b/packages/grid/_modules_/grid/models/events/gridEventLookup.ts @@ -22,6 +22,7 @@ import type { ElementSize } from '../elementSize'; import type { MuiBaseEvent } from '../muiEvent'; import type { GridRowId, GridRowTreeNodeConfig } from '../gridRows'; import type { GridPreProcessingGroup } from '../../hooks/core/preProcessing'; +import type { GridRowGroupingModel } from '../../hooks/features/rowGrouping'; import type { GridPinnedColumns } from '../api/gridColumnPinningApi'; export interface GridRowEventLookup { @@ -128,6 +129,7 @@ export interface GridControlledStateEventLookup { sortModelChange: { params: GridSortModel }; editRowsModelChange: { params: GridEditRowsModel }; selectionChange: { params: GridSelectionModel }; + rowGroupingModelChange: { params: GridRowGroupingModel }; pinnedColumnsChange: { params: GridPinnedColumns }; } diff --git a/packages/grid/_modules_/grid/models/events/gridEvents.ts b/packages/grid/_modules_/grid/models/events/gridEvents.ts index 572390b2907bd..157518db82eb4 100644 --- a/packages/grid/_modules_/grid/models/events/gridEvents.ts +++ b/packages/grid/_modules_/grid/models/events/gridEvents.ts @@ -205,6 +205,10 @@ export enum GridEvents { * Fired when the page size changes. */ pageSizeChange = 'pageSizeChange', + /** + * Fired when the row grouping model changes. + */ + rowGroupingModelChange = 'rowGroupingModelChange', /** * Fired during the scroll of the grid viewport. */ diff --git a/packages/grid/_modules_/grid/models/gridIconSlotsComponent.ts b/packages/grid/_modules_/grid/models/gridIconSlotsComponent.ts index f2d73ddfd8737..b1d4b54d97c6e 100644 --- a/packages/grid/_modules_/grid/models/gridIconSlotsComponent.ts +++ b/packages/grid/_modules_/grid/models/gridIconSlotsComponent.ts @@ -82,10 +82,22 @@ export interface GridIconSlotsComponent { MoreActionsIcon: React.JSXElementConstructor; /** * Icon displayed on the tree data toggling column when the children are collapsed + * @default GridKeyboardArrowRight */ TreeDataExpandIcon: React.JSXElementConstructor; /** * Icon displayed on the tree data toggling column when the children are expanded + * @default GridExpandMoreIcon */ TreeDataCollapseIcon: React.JSXElementConstructor; + /** + * Icon displayed on the grouping column when the children are collapsed + * @default GridKeyboardArrowRight + */ + GroupingCriteriaExpandIcon: React.JSXElementConstructor; + /** + * Icon displayed on the grouping column when the children are expanded + * @default GridExpandMoreIcon + */ + GroupingCriteriaCollapseIcon: React.JSXElementConstructor; } diff --git a/packages/grid/_modules_/grid/models/gridRows.ts b/packages/grid/_modules_/grid/models/gridRows.ts index b55acfb425e50..b363e8f102caf 100644 --- a/packages/grid/_modules_/grid/models/gridRows.ts +++ b/packages/grid/_modules_/grid/models/gridRows.ts @@ -25,6 +25,7 @@ export interface GridRowTreeNodeConfig { id: GridRowId; /** * The id of the row children. + * @default [] */ children?: GridRowId[]; /** @@ -33,6 +34,7 @@ export interface GridRowTreeNodeConfig { parent: GridRowId | null; /** * Current expansion status of the row. + * @default false */ childrenExpanded?: boolean; /** @@ -50,6 +52,7 @@ export interface GridRowTreeNodeConfig { groupingField: string | null; /** * If `true`, this node has been automatically added to fill a gap in the tree structure. + * @default false */ isAutoGenerated?: boolean; } diff --git a/packages/grid/_modules_/grid/models/gridSortModel.ts b/packages/grid/_modules_/grid/models/gridSortModel.ts index d344b71a6b7dd..ed3e98caedfff 100644 --- a/packages/grid/_modules_/grid/models/gridSortModel.ts +++ b/packages/grid/_modules_/grid/models/gridSortModel.ts @@ -1,5 +1,5 @@ import { GridCellValue } from './gridCell'; -import { GridRowId } from './gridRows'; +import { GridRowId, GridRowTreeNodeConfig } from './gridRows'; import type { GridApi } from './api'; export type GridSortDirection = 'asc' | 'desc' | null | undefined; @@ -8,6 +8,7 @@ export interface GridSortCellParams { id: GridRowId; field: string; value: GridCellValue; + rowNode: GridRowTreeNodeConfig; api: GridApi; } diff --git a/packages/grid/_modules_/grid/models/gridState.ts b/packages/grid/_modules_/grid/models/gridState.ts index 82e070c9a4e70..b26b9afac6c24 100644 --- a/packages/grid/_modules_/grid/models/gridState.ts +++ b/packages/grid/_modules_/grid/models/gridState.ts @@ -20,8 +20,15 @@ import type { GridFilterState, GridFilterInitialState, } from '../hooks/features/filter/gridFilterState'; +import type { + GridRowGroupingState, + GridRowGroupingInitialState, +} from '../hooks/features/rowGrouping'; import { GridColumnPinningState } from '../hooks/features/columnPinning/gridColumnPinningState'; +/** + * TODO: Distinguish pro and community states + */ export interface GridState { rows: GridRowsState; editRows: GridEditRowsModel; @@ -37,6 +44,7 @@ export interface GridState { filter: GridFilterState; preferencePanel: GridPreferencePanelState; density: GridDensityState; + rowGrouping: GridRowGroupingState; error?: any; pinnedColumns: GridColumnPinningState; } @@ -46,5 +54,6 @@ export interface GridInitialState { sorting?: GridSortingInitialState; filter?: GridFilterInitialState; preferencePanel?: GridPreferencePanelInitialState; + rowGrouping?: GridRowGroupingInitialState; pinnedColumns?: GridColumnPinningState; } diff --git a/packages/grid/_modules_/grid/models/params/gridCellParams.ts b/packages/grid/_modules_/grid/models/params/gridCellParams.ts index 35abdbb296dc2..d6ad6b72a370c 100644 --- a/packages/grid/_modules_/grid/models/params/gridCellParams.ts +++ b/packages/grid/_modules_/grid/models/params/gridCellParams.ts @@ -83,9 +83,9 @@ export interface GridRenderEditCellParams extends GridEditCellProps { } /** - * Parameters passed when calling `colDef.valueGetter` in the rendering sequence. + * Parameters passed to `colDef.valueGetter`. */ -export interface GridValueGetterFullParams +export interface GridValueGetterParams extends Omit, 'formattedValue' | 'isEditable'> { /** * GridApi that let you manipulate the grid. @@ -94,17 +94,26 @@ export interface GridValueGetterFullParams } /** - * Parameters passed when calling `colDef.valueGetter` in the row grouping sequence. + * @deprecated Use `GridValueGetterParams` instead. */ -export interface GridValueGetterSimpleParams { +export type GridValueGetterFullParams = GridValueGetterParams; + +/** + * Parameters passed to `colDef.groupingValueGetter`. + */ +export interface GridGroupingValueGetterParams { /** * The grid row id. */ id: GridRowId; /** - * The column field of the cell that triggered the event + * The column field of the cell that triggered the event. */ field: string; + /** + * The cell value, does not take `valueGetter` into account. + */ + value: V; /** * The row model of the row that the current cell belongs to. */ @@ -120,10 +129,6 @@ export interface GridValueGetterSimpleParams { rowNode: Pick; } -export type GridValueGetterParams = - | GridValueGetterFullParams - | GridValueGetterSimpleParams; - /** * Object passed as parameter in the column [[GridColDef]] value setter callback. */ @@ -148,7 +153,7 @@ export interface GridValueFormatterParams { */ id?: GridRowId; /** - * The column field of the cell that triggered the event + * The column field of the cell that triggered the event. */ field: string; /** diff --git a/packages/grid/_modules_/grid/models/props/DataGridProProps.ts b/packages/grid/_modules_/grid/models/props/DataGridProProps.ts index d58cf0610d8a3..46d516cdc2524 100644 --- a/packages/grid/_modules_/grid/models/props/DataGridProProps.ts +++ b/packages/grid/_modules_/grid/models/props/DataGridProProps.ts @@ -10,16 +10,30 @@ import { DataGridPropsWithComplexDefaultValueBeforeProcessing, DATA_GRID_PROPS_DEFAULT_VALUES, } from './DataGridProps'; +import type { GridRowGroupingModel } from '../../hooks/features/rowGrouping'; + +export type GridExperimentalProFeatures = + /** + * Will be part of the premium-plan when fully ready. + */ + 'rowGrouping'; /** * The props users can give to the `DataGridProProps` component. */ -export type DataGridProProps = Omit< - Partial & - DataGridPropsWithComplexDefaultValueBeforeProcessing & - DataGridProPropsWithoutDefaultValue, - DataGridProForcedPropsKey ->; +export interface DataGridProProps + extends Omit< + Partial & + DataGridPropsWithComplexDefaultValueBeforeProcessing & + DataGridProPropsWithoutDefaultValue, + DataGridProForcedPropsKey + > { + /** + * Features under development. + * For each feature, if the flag is not explicitly set to `true`, the feature will be fully disabled and any property / method call will not have any effect. + */ + experimentalFeatures?: { [key in GridExperimentalProFeatures]?: boolean }; +} /** * The props of the `DataGridPro` component after the pre-processing phase. @@ -66,15 +80,26 @@ export interface DataGridProPropsWithDefaultValue extends DataGridPropsWithDefau */ disableColumnPinning: boolean; /** - * If `true`, the filtering will only be applied to the top level rows. + * If `true`, the filtering will only be applied to the top level rows when grouping rows with the `treeData` prop. * @default false */ disableChildrenFiltering: boolean; /** - * If `true`, the sorting will only be applied to the top level rows. + * If `true`, the sorting will only be applied to the top level rows when grouping rows with the `treeData` prop. * @default false */ disableChildrenSorting: boolean; + /** + * If `true`, the row grouping is disabled. + * @default false + */ + disableRowGrouping: boolean; + /** + * If `single`, all column we are grouping by will be represented in the same grouping the same column. + * If `multiple`, each column we are grouping by will be represented in its own column. + * @default 'single' + */ + rowGroupingColumnMode: 'single' | 'multiple'; } /** @@ -86,8 +111,10 @@ export const DATA_GRID_PRO_PROPS_DEFAULT_VALUES: DataGridProPropsWithDefaultValu treeData: false, defaultGroupingExpansionDepth: 0, disableColumnPinning: false, + disableRowGrouping: false, disableChildrenFiltering: false, disableChildrenSorting: false, + rowGroupingColumnMode: 'single', }; export interface DataGridProPropsWithoutDefaultValue extends DataGridPropsWithoutDefaultValue { @@ -130,6 +157,16 @@ export interface DataGridProPropsWithoutDefaultValue extends DataGridPropsWithou * @param {GridCallbackDetails} details Additional details for this callback. */ onPinnedColumnsChange?: (pinnedColumns: GridPinnedColumns, details: GridCallbackDetails) => void; + /** + * Set the row grouping model of the grid. + */ + rowGroupingModel?: GridRowGroupingModel; + /** + * Callback fired when the row grouping model changes. + * @param {GridRowGroupingModel} model Columns used as grouping criteria. + * @param {GridCallbackDetails} details Additional details for this callback. + */ + onRowGroupingModelChange?: (model: GridRowGroupingModel, details: GridCallbackDetails) => void; /** * The grouping column used by the tree data. */ diff --git a/packages/grid/_modules_/grid/utils/tree/buildRowTree.test.ts b/packages/grid/_modules_/grid/utils/tree/buildRowTree.test.ts index 6d8178b42558e..666b5a93d4dab 100644 --- a/packages/grid/_modules_/grid/utils/tree/buildRowTree.test.ts +++ b/packages/grid/_modules_/grid/utils/tree/buildRowTree.test.ts @@ -1,7 +1,6 @@ import { expect } from 'chai'; import { buildRowTree } from './buildRowTree'; -// TODO: Add tests for multi-field grouping describe('buildRowTree', () => { it('should not expand the rows when defaultGroupingExpansionDepth === 0', () => { const response = buildRowTree({ @@ -282,4 +281,93 @@ describe('buildRowTree', () => { }, }); }); + + it('should allow to have two rows with the same field at various depth', () => { + const response = buildRowTree({ + groupingName: '', + idRowsLookup: { + 0: {}, + 1: {}, + }, + ids: [0, 1], + rows: [ + { + id: 0, + path: [ + { key: 'value-1-1', field: 'field1' }, + { key: 'value-2-1', field: 'field2' }, + { key: 'value-leaf-1', field: null }, + ], + }, + { + id: 1, + path: [ + { key: 'value-2-1', field: 'field2' }, + { key: 'value-leaf-1', field: null }, + ], + }, + ], + defaultGroupingExpansionDepth: -1, + previousTree: null, + }); + + // The tree structure: + // "value-1-1" + // "value-2-1" + // "value-leaf-1" + // "value-2-1" + // "value-leaf-1" + expect(response.tree).to.deep.equal({ + '0': { + id: 0, + childrenExpanded: undefined, + isAutoGenerated: false, + parent: 'auto-generated-row-field1/value-1-1-field2/value-2-1', + groupingKey: 'value-leaf-1', + groupingField: null, + depth: 2, + children: undefined, + }, + '1': { + id: 1, + childrenExpanded: undefined, + isAutoGenerated: false, + parent: 'auto-generated-row-field2/value-2-1', + groupingKey: 'value-leaf-1', + groupingField: null, + depth: 1, + children: undefined, + }, + 'auto-generated-row-field1/value-1-1': { + id: 'auto-generated-row-field1/value-1-1', + isAutoGenerated: true, + childrenExpanded: true, + parent: null, + groupingKey: 'value-1-1', + groupingField: 'field1', + depth: 0, + children: ['auto-generated-row-field1/value-1-1-field2/value-2-1'], + }, + 'auto-generated-row-field1/value-1-1-field2/value-2-1': { + id: 'auto-generated-row-field1/value-1-1-field2/value-2-1', + isAutoGenerated: true, + childrenExpanded: true, + parent: 'auto-generated-row-field1/value-1-1', + groupingKey: 'value-2-1', + groupingField: 'field2', + depth: 1, + children: [0], + }, + 'auto-generated-row-field2/value-2-1': { + id: 'auto-generated-row-field2/value-2-1', + isAutoGenerated: true, + childrenExpanded: true, + parent: null, + groupingKey: 'value-2-1', + groupingField: 'field2', + depth: 0, + children: [1], + }, + }); + }); }); diff --git a/packages/grid/x-data-grid-generator/src/commodities.columns.tsx b/packages/grid/x-data-grid-generator/src/commodities.columns.tsx index 4fa58cefcd793..15c0d6f2cff64 100644 --- a/packages/grid/x-data-grid-generator/src/commodities.columns.tsx +++ b/packages/grid/x-data-grid-generator/src/commodities.columns.tsx @@ -146,7 +146,8 @@ export const getCommodityColumns = (editable = false): GridColDefGenerator[] => { field: 'subTotal', headerName: 'Sub Total', - valueGetter: ({ row }) => row.quantity * row.unitPrice, + valueGetter: ({ row, rowNode }) => + rowNode.isAutoGenerated ? null : row.quantity * row.unitPrice, type: 'number', width: 120, }, @@ -163,7 +164,8 @@ export const getCommodityColumns = (editable = false): GridColDefGenerator[] => { field: 'feeAmount', headerName: 'Fee Amount', - valueGetter: ({ row }) => row.feeRate * row.quantity * row.unitPrice, + valueGetter: ({ row, rowNode }) => + rowNode.isAutoGenerated ? null : row.feeRate * row.quantity * row.unitPrice, type: 'number', width: 120, }, @@ -179,7 +181,8 @@ export const getCommodityColumns = (editable = false): GridColDefGenerator[] => { field: 'totalPrice', headerName: 'Total in USD', - valueGetter: ({ row }) => row.feeRate + row.quantity * row.unitPrice, + valueGetter: ({ row, rowNode }) => + rowNode.isAutoGenerated ? null : row.feeRate + row.quantity * row.unitPrice, renderCell: renderTotalPrice, type: 'number', width: 160, @@ -239,6 +242,7 @@ export const getCommodityColumns = (editable = false): GridColDefGenerator[] => return value; }, + groupingValueGetter: (params) => params.value.code, type: 'singleSelect', valueOptions: COUNTRY_ISO_OPTIONS, editable, diff --git a/packages/grid/x-data-grid-generator/src/index.ts b/packages/grid/x-data-grid-generator/src/index.ts index a5590f5ed67e6..2adc86aa857a4 100644 --- a/packages/grid/x-data-grid-generator/src/index.ts +++ b/packages/grid/x-data-grid-generator/src/index.ts @@ -3,3 +3,4 @@ export * from './services'; export * from './commodities.columns'; export * from './employees.columns'; export * from './useDemoData'; +export * from './useMovieData'; diff --git a/packages/grid/x-data-grid-generator/src/renderer/renderCountry.tsx b/packages/grid/x-data-grid-generator/src/renderer/renderCountry.tsx index 7352962d87d60..a0a28c4971bee 100644 --- a/packages/grid/x-data-grid-generator/src/renderer/renderCountry.tsx +++ b/packages/grid/x-data-grid-generator/src/renderer/renderCountry.tsx @@ -49,5 +49,9 @@ const Country = React.memo(function Country(props: CountryProps) { }); export function renderCountry(params: GridRenderCellParams) { + if (params.rowNode.isAutoGenerated) { + return ''; + } + return ; } diff --git a/packages/grid/x-data-grid-generator/src/renderer/renderPnl.tsx b/packages/grid/x-data-grid-generator/src/renderer/renderPnl.tsx index 8f6f1cabab989..55fa5c49df371 100644 --- a/packages/grid/x-data-grid-generator/src/renderer/renderPnl.tsx +++ b/packages/grid/x-data-grid-generator/src/renderer/renderPnl.tsx @@ -49,5 +49,9 @@ const Pnl = React.memo(function Pnl(props: PnlProps) { }); export function renderPnl(params: GridCellParams) { + if (params.rowNode.isAutoGenerated) { + return ''; + } + return ; } diff --git a/packages/grid/x-data-grid-generator/src/renderer/renderProgress.tsx b/packages/grid/x-data-grid-generator/src/renderer/renderProgress.tsx index 8c235fa18e13a..c18647de9c15d 100644 --- a/packages/grid/x-data-grid-generator/src/renderer/renderProgress.tsx +++ b/packages/grid/x-data-grid-generator/src/renderer/renderProgress.tsx @@ -64,5 +64,9 @@ const ProgressBar = React.memo(function ProgressBar(props: ProgressBarProps) { }); export function renderProgress(params: GridCellParams) { + if (params.rowNode.isAutoGenerated) { + return ''; + } + return ; } diff --git a/packages/grid/x-data-grid-generator/src/renderer/renderStatus.tsx b/packages/grid/x-data-grid-generator/src/renderer/renderStatus.tsx index d9fd9dfd77c1e..11986b3c53464 100644 --- a/packages/grid/x-data-grid-generator/src/renderer/renderStatus.tsx +++ b/packages/grid/x-data-grid-generator/src/renderer/renderStatus.tsx @@ -74,5 +74,9 @@ const Status = React.memo((props: StatusProps) => { }); export function renderStatus(params: GridCellParams) { - return ; + if (params.rowNode.isAutoGenerated) { + return ''; + } + + return ; } diff --git a/packages/grid/x-data-grid-generator/src/renderer/renderTotalPrice.tsx b/packages/grid/x-data-grid-generator/src/renderer/renderTotalPrice.tsx index ad81fa698d25d..2861f2b03b22f 100644 --- a/packages/grid/x-data-grid-generator/src/renderer/renderTotalPrice.tsx +++ b/packages/grid/x-data-grid-generator/src/renderer/renderTotalPrice.tsx @@ -48,5 +48,8 @@ const TotalPrice = React.memo(function TotalPrice(props: TotalPriceProps) { }); export function renderTotalPrice(params: GridCellParams) { + if (params.rowNode.isAutoGenerated) { + return ''; + } return ; } diff --git a/packages/grid/x-data-grid-generator/src/useMovieData.ts b/packages/grid/x-data-grid-generator/src/useMovieData.ts new file mode 100644 index 0000000000000..9dbcf0ae44b91 --- /dev/null +++ b/packages/grid/x-data-grid-generator/src/useMovieData.ts @@ -0,0 +1,303 @@ +import { GridColumns, DataGridProps, GridRowModel } from '@mui/x-data-grid'; + +type Movie = { + id: number; + title: string; + gross: number; + director: string; + company: string; + year: number; + composer: { name: string }; + cinematicUniverse?: string; +}; + +const COLUMNS: GridColumns = [ + { field: 'title', headerName: 'Title', width: 200, groupable: false }, + { + field: 'gross', + headerName: 'Gross', + type: 'number', + width: 150, + groupable: false, + valueFormatter: ({ value }) => { + if (!value || typeof value !== 'number') { + return value; + } + return `${value.toLocaleString()}$`; + }, + }, + { + field: 'company', + headerName: 'Company', + width: 200, + }, + { + field: 'director', + headerName: 'Director', + width: 200, + }, + { + field: 'year', + headerName: 'Year', + }, + { + field: 'cinematicUniverse', + headerName: 'Cinematic Universe', + width: 220, + }, +]; + +const ROWS: GridRowModel[] = [ + { + id: 0, + title: 'Avatar', + gross: 2847246203, + director: 'James Cameron', + company: '20th Century Fox', + year: 2009, + composer: { + name: 'James Horner', + }, + }, + { + id: 1, + title: 'Avengers: Endgame', + gross: 2797501328, + director: 'Anthony & Joe Russo', + company: 'Disney Studios', + year: 2019, + cinematicUniverse: 'Marvel Cinematic Universe', + composer: { + name: 'Alan Silvestri', + }, + }, + { + id: 2, + title: 'Titanic', + gross: 2187425379, + director: 'James Cameron', + company: '20th Century Fox', + year: 1997, + composer: { + name: 'James Horner', + }, + }, + { + id: 3, + title: 'Star Wars: The Force Awakens', + gross: 2068223624, + director: 'J. J. Abrams', + company: 'Disney Studios', + year: 2015, + cinematicUniverse: 'Star Wars', + composer: { + name: 'John Williams', + }, + }, + { + id: 4, + title: 'Avengers: Infinity War', + gross: 2048359754, + director: 'Anthony & Joe Russo', + company: 'Disney Studios', + year: 2018, + cinematicUniverse: 'Star Wars', + composer: { + name: 'Alan Silvestri', + }, + }, + { + id: 5, + title: 'Jurassic World', + gross: 1671713208, + director: 'Colin Trevorrow', + company: 'Universal Pictures', + year: 2015, + cinematicUniverse: 'Jurassic Park', + composer: { + name: 'Michael Giacchino', + }, + }, + { + id: 6, + title: 'The Lion King', + gross: 1656943394, + director: 'Jon Favreau', + company: 'Disney Studios', + year: 2019, + composer: { + name: 'Hans Zimmer', + }, + }, + { + id: 7, + title: 'The Avengers', + gross: 1518812988, + director: 'Joss Whedon', + company: 'Disney Studios', + year: 2012, + cinematicUniverse: 'Marvel Cinematic Universe', + composer: { + name: 'Alan Silvestri', + }, + }, + { + id: 8, + title: 'Furious 7', + gross: 1516045911, + director: 'James Wan', + company: 'Universal Pictures', + year: 2015, + cinematicUniverse: 'Fast & Furious', + composer: { + name: 'Brian Tyler', + }, + }, + { + id: 9, + title: 'Frozen II', + gross: 1450026933, + director: 'Chris Buck & Jennifer Lee', + company: 'Disney Studios', + year: 2019, + cinematicUniverse: 'Frozen', + composer: { + name: 'Christophe Beck', + }, + }, + { + id: 10, + title: 'Avengers: Age of Ultron', + gross: 1402804868, + director: 'Joss Whedon', + company: 'Disney Studios', + year: 2015, + cinematicUniverse: 'Marvel Cinematic Universe', + composer: { + name: 'Danny Elfman', + }, + }, + { + id: 11, + title: 'Black Panther', + gross: 1347280838, + director: 'Ryan Coogler', + company: 'Disney Studios', + year: 2018, + cinematicUniverse: 'Marvel Cinematic Universe', + composer: { + name: 'Ludwig Göransson', + }, + }, + { + id: 12, + title: 'Harry Potter and the Deathly Hallows – Part 2', + gross: 1342025430, + director: 'David Yates', + company: 'Warner Bros. Pictures', + year: 2011, + composer: { + name: 'Alexandre Desplat', + }, + }, + { + id: 13, + title: 'Star Wars: The Last Jedi', + gross: 1332539889, + director: 'Rian Johnson', + company: 'Disney Studios', + year: 2017, + cinematicUniverse: 'Star Wars', + composer: { + name: 'John Williams', + }, + }, + { + id: 14, + title: 'Jurassic World: Fallen Kingdom', + gross: 1309484461, + director: 'J. A. Bayona', + company: 'Universal Pictures', + year: 2018, + cinematicUniverse: 'Jurassic Park', + composer: { + name: 'Michael Giacchino', + }, + }, + { + id: 15, + title: 'Frozen', + gross: 1290000000, + director: 'Chris Buck & Jennifer Lee', + company: 'Disney Studios', + year: 2013, + cinematicUniverse: 'Frozen', + composer: { + name: 'Christophe Beck', + }, + }, + { + id: 16, + title: 'Beauty and the Beast', + gross: 1263521136, + director: 'Bill Condon', + company: 'Disney Studios', + year: 2017, + composer: { + name: 'Alan Menken', + }, + }, + { + id: 17, + title: 'Incredibles 2', + gross: 1242805359, + director: 'Brad Bird', + company: 'Disney Studios', + year: 2018, + composer: { + name: 'Michael Giacchino', + }, + }, + { + id: 18, + title: 'The Fate of the Furious', + gross: 1238764765, + director: 'F. Gary Gray', + company: 'Universal Pictures', + year: 2017, + cinematicUniverse: 'Fast & Furious', + composer: { + name: 'Brian Tyler', + }, + }, + { + id: 19, + title: 'Iron Man 3', + gross: 1214811252, + director: 'Shane Black', + company: 'Disney Studios', + year: 2013, + cinematicUniverse: 'Marvel Cinematic Universe', + composer: { + name: 'Brian Tyler', + }, + }, + { + id: 20, + title: 'Minions', + gross: 11159398397, + director: 'Pierre Coffin & Kyle Balda', + company: 'Universal Pictures', + year: 2015, + composer: { + name: 'Heitor Pereira', + }, + }, +]; + +export const useMovieData = (): Pick => { + return { + rows: ROWS, + columns: COLUMNS, + }; +}; diff --git a/packages/grid/x-data-grid-pro/src/DataGridPro.tsx b/packages/grid/x-data-grid-pro/src/DataGridPro.tsx index 64ca878627fc4..e1856693ff720 100644 --- a/packages/grid/x-data-grid-pro/src/DataGridPro.tsx +++ b/packages/grid/x-data-grid-pro/src/DataGridPro.tsx @@ -147,12 +147,12 @@ DataGridProRaw.propTypes = { */ density: PropTypes.oneOf(['comfortable', 'compact', 'standard']), /** - * If `true`, the filtering will only be applied to the top level rows. + * If `true`, the filtering will only be applied to the top level rows when grouping rows with the `treeData` prop. * @default false */ disableChildrenFiltering: PropTypes.bool, /** - * If `true`, the sorting will only be applied to the top level rows. + * If `true`, the sorting will only be applied to the top level rows when grouping rows with the `treeData` prop. * @default false */ disableChildrenSorting: PropTypes.bool, @@ -211,6 +211,11 @@ DataGridProRaw.propTypes = { * @default false */ disableMultipleSelection: PropTypes.bool, + /** + * If `true`, the row grouping is disabled. + * @default false + */ + disableRowGrouping: PropTypes.bool, /** * If `true`, the selection on click on a row or cell is disabled. * @default false @@ -234,6 +239,13 @@ DataGridProRaw.propTypes = { * An error that will turn the grid into its error state and display the error component. */ error: PropTypes.any, + /** + * Features under development. + * For each feature, if the flag is not explicitly set to `true`, the feature will be fully disabled and any property / method call will not have any effect. + */ + experimentalFeatures: PropTypes.shape({ + rowGrouping: PropTypes.bool, + }), /** * Filtering can be processed on the server or client-side. * Set it to 'server' if you would like to handle filtering on the server-side. @@ -576,6 +588,12 @@ DataGridProRaw.propTypes = { * @param {MuiEvent} event The event that caused this prop to be called. */ onRowEditStop: PropTypes.func, + /** + * Callback fired when the row grouping model changes. + * @param {GridRowGroupingModel} model Columns used as grouping criteria. + * @param {GridCallbackDetails} details Additional details for this callback. + */ + onRowGroupingModelChange: PropTypes.func, /** * Callback fired when scrolling to the bottom of the grid viewport. * @param {GridRowScrollEndParams} params With all properties from [[GridRowScrollEndParams]]. @@ -643,6 +661,16 @@ DataGridProRaw.propTypes = { * If some of the rows have children (for instance in the tree data), this number represents the amount of top level rows. */ rowCount: PropTypes.number, + /** + * If `single`, all column we are grouping by will be represented in the same grouping the same column. + * If `multiple`, each column we are grouping by will be represented in its own column. + * @default 'single' + */ + rowGroupingColumnMode: PropTypes.oneOf(['multiple', 'single']), + /** + * Set the row grouping model of the grid. + */ + rowGroupingModel: PropTypes.arrayOf(PropTypes.string), /** * Set the height in pixel of a row in the grid. * @default 52 diff --git a/packages/grid/x-data-grid-pro/src/tests/rowGrouping.DataGridPro.test.tsx b/packages/grid/x-data-grid-pro/src/tests/rowGrouping.DataGridPro.test.tsx new file mode 100644 index 0000000000000..36e0dc01bc61c --- /dev/null +++ b/packages/grid/x-data-grid-pro/src/tests/rowGrouping.DataGridPro.test.tsx @@ -0,0 +1,2187 @@ +import { createRenderer, fireEvent, screen, act } from '@mui/monorepo/test/utils'; +import { + getColumnHeaderCell, + getColumnHeadersTextContent, + getColumnValues, +} from 'test/utils/helperFn'; +import * as React from 'react'; +import { expect } from 'chai'; +import { + DataGridPro, + DataGridProProps, + getRowGroupingFieldFromGroupingCriteria, + GRID_ROW_GROUPING_SINGLE_GROUPING_FIELD, + GridApiRef, + GridGroupingValueGetterParams, + GridPreferencePanelsValue, + GridRowsProp, + GridRowTreeNodeConfig, + useGridApiRef, + useGridRootProps, +} from '@mui/x-data-grid-pro'; +import { spy } from 'sinon'; +import { DataGridProProcessedProps } from '../../../_modules_/grid/models/props/DataGridProProps'; + +const isJSDOM = /jsdom/.test(window.navigator.userAgent); + +const rows: GridRowsProp = [ + { id: 0, category1: 'Cat A', category2: 'Cat 1' }, + { id: 1, category1: 'Cat A', category2: 'Cat 2' }, + { id: 2, category1: 'Cat A', category2: 'Cat 2' }, + { id: 3, category1: 'Cat B', category2: 'Cat 2' }, + { id: 4, category1: 'Cat B', category2: 'Cat 1' }, +]; + +const unbalancedRows: GridRowsProp = [ + { id: 0, category1: 'Cat A' }, + { id: 1, category1: 'Cat A' }, + { id: 2, category1: 'Cat B' }, + { id: 3, category1: 'Cat B' }, + { id: 4, category1: null }, + { id: 5, category1: null }, +]; + +const baselineProps: DataGridProProps = { + autoHeight: isJSDOM, + disableVirtualization: true, + rows, + columns: [ + { + field: 'id', + type: 'number', + }, + { + field: 'category1', + }, + { + field: 'category2', + }, + ], + experimentalFeatures: { + rowGrouping: true, + }, +}; + +describe(' - Group Rows By Column', () => { + const { render, clock } = createRenderer({ clock: 'fake' }); + + let apiRef: GridApiRef; + + const Test = (props: Partial) => { + apiRef = useGridApiRef(); + + return ( +
+ +
+ ); + }; + + describe('Setting grouping criteria', () => { + describe('initialState: rowGrouping.model', () => { + it('should allow to initialize the row grouping', () => { + render( + , + ); + expect(getColumnValues(0)).to.deep.equal(['Cat A (3)', '', '', '', 'Cat B (2)', '', '']); + }); + + it('should not react to initial state updates', () => { + const { setProps } = render( + , + ); + expect(getColumnValues(0)).to.deep.equal(['Cat A (3)', '', '', '', 'Cat B (2)', '', '']); + + setProps({ initialState: { rowGrouping: { model: ['category2'] } } }); + expect(getColumnValues(0)).to.deep.equal(['Cat A (3)', '', '', '', 'Cat B (2)', '', '']); + }); + }); + + describe('prop: rowGroupingModel', () => { + it('should not call onRowGroupingModelChange on initialisation or on rowGroupingModel prop change', () => { + const onRowGroupingModelChange = spy(); + + const { setProps } = render( + , + ); + + expect(onRowGroupingModelChange.callCount).to.equal(0); + setProps({ rowGroupingModel: ['category2'] }); + + expect(onRowGroupingModelChange.callCount).to.equal(0); + }); + + it('should allow to update the row grouping model from the outside', () => { + const { setProps } = render( + , + ); + expect(getColumnValues(0)).to.deep.equal(['Cat A (3)', '', '', '', 'Cat B (2)', '', '']); + setProps({ rowGroupingModel: ['category2'] }); + expect(getColumnValues(0)).to.deep.equal(['Cat 1 (2)', '', '', 'Cat 2 (3)', '', '', '']); + setProps({ rowGroupingModel: ['category1', 'category2'] }); + expect(getColumnValues()).to.deep.equal([ + 'Cat A (3)', + 'Cat 1 (1)', + '', + 'Cat 2 (2)', + '', + '', + 'Cat B (2)', + 'Cat 2 (1)', + '', + 'Cat 1 (1)', + '', + ]); + }); + }); + + it('should ignore grouping criteria that do not match any column', () => { + render( + , + ); + expect(getColumnValues(0)).to.deep.equal(['Cat A (3)', '', '', '', 'Cat B (2)', '', '']); + }); + + it('should ignore grouping criteria with colDef.groupable = false', () => { + render( + , + ); + expect(getColumnValues(0)).to.deep.equal(['Cat A (3)', '', '', '', 'Cat B (2)', '', '']); + }); + + it('should allow to use several time the same grouping criteria', () => { + render( + , + ); + + expect(getColumnValues(0)).to.deep.equal([ + 'Cat A (3)', + 'Cat A (3)', + '', + '', + '', + 'Cat B (2)', + 'Cat B (2)', + '', + '', + ]); + }); + }); + + describe('props: rowGroupingColumnMode', () => { + it('should gather all the grouping criteria into a single column when rowGroupingColumnMode is not defined', () => { + render( + , + ); + + expect(getColumnHeadersTextContent()).to.deep.equal([ + 'Group', + 'id', + 'category1', + 'category2', + ]); + expect(getColumnValues(0)).to.deep.equal([ + 'Cat A (3)', + 'Cat 1 (1)', + '', + 'Cat 2 (2)', + '', + '', + 'Cat B (2)', + 'Cat 2 (1)', + '', + 'Cat 1 (1)', + '', + ]); + }); + + it('should gather all the grouping criteria into a single column when rowGroupingColumnMode = "single"', () => { + render( + , + ); + + expect(getColumnHeadersTextContent()).to.deep.equal([ + 'Group', + 'id', + 'category1', + 'category2', + ]); + expect(getColumnValues(0)).to.deep.equal([ + 'Cat A (3)', + 'Cat 1 (1)', + '', + 'Cat 2 (2)', + '', + '', + 'Cat B (2)', + 'Cat 2 (1)', + '', + 'Cat 1 (1)', + '', + ]); + }); + + it('should create one grouping column per grouping criteria when rowGroupingColumnMode = "multiple"', () => { + render( + , + ); + + expect(getColumnHeadersTextContent()).to.deep.equal([ + 'category1', + 'category2', + 'id', + 'category1', + 'category2', + ]); + expect(getColumnValues(0)).to.deep.equal([ + 'Cat A (3)', + '', + '', + '', + '', + '', + 'Cat B (2)', + '', + '', + '', + '', + ]); + expect(getColumnValues(1)).to.deep.equal([ + '', + 'Cat 1 (1)', + '', + 'Cat 2 (2)', + '', + '', + '', + 'Cat 2 (1)', + '', + 'Cat 1 (1)', + '', + ]); + }); + + it('should support rowGroupingColumnMode switch', () => { + const { setProps } = render( + , + ); + + expect(getColumnHeadersTextContent()).to.deep.equal([ + 'category1', + 'category2', + 'id', + 'category1', + 'category2', + ]); + expect(getColumnValues(0)).to.deep.equal([ + 'Cat A (3)', + '', + '', + '', + '', + '', + 'Cat B (2)', + '', + '', + '', + '', + ]); + expect(getColumnValues(1)).to.deep.equal([ + '', + 'Cat 1 (1)', + '', + 'Cat 2 (2)', + '', + '', + '', + 'Cat 2 (1)', + '', + 'Cat 1 (1)', + '', + ]); + + setProps({ rowGroupingColumnMode: 'single' }); + expect(getColumnHeadersTextContent()).to.deep.equal([ + 'Group', + 'id', + 'category1', + 'category2', + ]); + expect(getColumnValues(0)).to.deep.equal([ + 'Cat A (3)', + 'Cat 1 (1)', + '', + 'Cat 2 (2)', + '', + '', + 'Cat B (2)', + 'Cat 2 (1)', + '', + 'Cat 1 (1)', + '', + ]); + + setProps({ rowGroupingColumnMode: 'multiple' }); + expect(getColumnHeadersTextContent()).to.deep.equal([ + 'category1', + 'category2', + 'id', + 'category1', + 'category2', + ]); + expect(getColumnValues(0)).to.deep.equal([ + 'Cat A (3)', + '', + '', + '', + '', + '', + 'Cat B (2)', + '', + '', + '', + '', + ]); + expect(getColumnValues(1)).to.deep.equal([ + '', + 'Cat 1 (1)', + '', + 'Cat 2 (2)', + '', + '', + '', + 'Cat 2 (1)', + '', + 'Cat 1 (1)', + '', + ]); + }); + + it('should respect the model grouping order when rowGroupingColumnMode = "single"', () => { + render( + , + ); + + expect(getColumnHeadersTextContent()).to.deep.equal([ + 'Group', + 'id', + 'category1', + 'category2', + ]); + expect(getColumnValues(0)).to.deep.equal([ + 'Cat 1 (2)', + 'Cat A (1)', + '', + 'Cat B (1)', + '', + 'Cat 2 (3)', + 'Cat A (2)', + '', + '', + 'Cat B (1)', + '', + ]); + }); + + it('should respect the model grouping order when rowGroupingColumnMode = "multiple"', () => { + render( + , + ); + + expect(getColumnHeadersTextContent()).to.deep.equal([ + 'category2', + 'category1', + 'id', + 'category1', + 'category2', + ]); + expect(getColumnValues(0)).to.deep.equal([ + 'Cat 1 (2)', + '', + '', + '', + '', + 'Cat 2 (3)', + '', + '', + '', + '', + '', + ]); + expect(getColumnValues(1)).to.deep.equal([ + '', + 'Cat A (1)', + '', + 'Cat B (1)', + '', + '', + 'Cat A (2)', + '', + '', + 'Cat B (1)', + '', + ]); + }); + }); + + describe('props: disableRowGrouping', () => { + // TODO: Remove once the feature is stable + it('should set `disableRowGrouping` to `true` if `experimentalFeatures.rowGrouping = false', () => { + const disableRowGroupingSpy = spy(); + + const CustomToolbar = () => { + const rootProps = useGridRootProps(); + disableRowGroupingSpy(rootProps.disableRowGrouping); + return null; + }; + + render( + , + ); + + expect(disableRowGroupingSpy.lastCall.firstArg).to.equal(true); + }); + + it('should disable the row grouping when `prop.disableRowGrouping = true`', () => { + render( + , + ); + + // No grouping applied on rows + expect(apiRef.current.state.rows.groupingName).to.equal('none'); + expect(getColumnValues(0)).to.deep.equal(['0', '1', '2', '3', '4']); + + // No grouping column rendered + expect(getColumnHeadersTextContent()).to.deep.equal(['id', 'category1', 'category2']); + + // No menu item on column menu to add / remove grouping criteria + apiRef.current.showColumnMenu('category1'); + clock.runToLast(); + expect(screen.queryByRole('menu')).not.to.equal(null); + const category1Menuitem = screen.queryByRole('menuitem', { + name: 'Stop grouping by category1', + }); + expect(category1Menuitem).to.equal(null); + + apiRef.current.hideColumnMenu(); + clock.runToLast(); + expect(screen.queryByRole('menu')).to.equal(null); + + apiRef.current.showColumnMenu('category2'); + clock.runToLast(); + expect(screen.queryByRole('menu')).not.to.equal(null); + const category2Menuitem = screen.queryByRole('menuitem', { name: 'Group by category2' }); + expect(category2Menuitem).to.equal(null); + }); + }); + + describe('prop: defaultGroupingExpansionDepth', () => { + it('should not expand any row if defaultGroupingExpansionDepth = 0', () => { + render( + , + ); + expect(getColumnValues(0)).to.deep.equal(['Cat A (3)', 'Cat B (2)']); + }); + + it('should expand all top level rows if defaultGroupingExpansionDepth = 1', () => { + render( + , + ); + expect(getColumnValues(0)).to.deep.equal([ + 'Cat A (3)', + 'Cat 1 (1)', + 'Cat 2 (2)', + 'Cat B (2)', + 'Cat 2 (1)', + 'Cat 1 (1)', + ]); + }); + + it('should expand all rows up to depth of 2 if defaultGroupingExpansionDepth = 2', () => { + render( + , + ); + expect(getColumnValues(0)).to.deep.equal([ + 'Cat A (3)', + 'Cat 1 (1)', + '', + 'Cat 2 (2)', + '', + '', + 'Cat B (2)', + 'Cat 2 (1)', + '', + 'Cat 1 (1)', + '', + ]); + }); + + it('should expand all rows if defaultGroupingExpansionDepth = -1', () => { + render( + , + ); + expect(getColumnValues(0)).to.deep.equal([ + 'Cat A (3)', + 'Cat 1 (1)', + '', + 'Cat 2 (2)', + '', + '', + 'Cat B (2)', + 'Cat 2 (1)', + '', + 'Cat 1 (1)', + '', + ]); + }); + + it('should not re-apply default expansion on rerender after expansion manually toggled', () => { + const { setProps } = render( + , + ); + expect(getColumnValues(0)).to.deep.equal(['Cat A (3)', 'Cat B (2)']); + act(() => { + apiRef.current.setRowChildrenExpansion('auto-generated-row-category1/Cat B', true); + }); + expect(getColumnValues(0)).to.deep.equal([ + 'Cat A (3)', + 'Cat B (2)', + 'Cat 2 (1)', + 'Cat 1 (1)', + ]); + setProps({ sortModel: [{ field: '__row_group_by_columns_group__', sort: 'desc' }] }); + expect(getColumnValues(0)).to.deep.equal([ + 'Cat B (2)', + 'Cat 2 (1)', + 'Cat 1 (1)', + 'Cat A (3)', + ]); + }); + }); + + describe('prop: isGroupExpandedByDefault', () => { + it('should expand groups according to isGroupExpandedByDefault when defined', () => { + const isGroupExpandedByDefault = spy( + (node: GridRowTreeNodeConfig) => + node.groupingKey === 'Cat A' && node.groupingField === 'category1', + ); + + render( + , + ); + expect(isGroupExpandedByDefault.callCount).to.equal(12); // Should not be called on leaves + const { childrenExpanded, ...node } = apiRef.current.state.rows.tree.A; + const callForNodeA = isGroupExpandedByDefault + .getCalls() + .find( + (call) => + call.firstArg.groupingKey === 'Cat A' && call.firstArg.groupingField === 'category1', + )!; + expect(callForNodeA.firstArg).to.deep.includes(node); + expect(getColumnValues(0)).to.deep.equal([ + 'Cat A (3)', + 'Cat 1 (1)', + 'Cat 2 (2)', + 'Cat B (2)', + ]); + }); + + it('should have priority over defaultGroupingExpansionDepth when both defined', () => { + const isGroupExpandedByDefault = (node: GridRowTreeNodeConfig) => + node.groupingKey === 'Cat A' && node.groupingField === 'category1'; + + render( + , + ); + expect(getColumnValues(0)).to.deep.equal([ + 'Cat A (3)', + 'Cat 1 (1)', + 'Cat 2 (2)', + 'Cat B (2)', + ]); + }); + }); + + describe('props: groupingColDef when groupingColumMode = "single"', () => { + it('should not allow to override the field', () => { + render( + , + ); + + expect(apiRef.current.getAllColumns()[0].field).to.equal('__row_group_by_columns_group__'); + }); + + it('should react to groupingColDef update', () => { + const { setProps } = render( + , + ); + + expect(getColumnHeadersTextContent()).to.deep.equal([ + 'category1', + 'id', + 'category1', + 'category2', + ]); + + setProps({ + groupingColDef: { + headerName: 'Custom group', + }, + }); + expect(getColumnHeadersTextContent()).to.deep.equal([ + 'Custom group', + 'id', + 'category1', + 'category2', + ]); + }); + + it('should keep the grouping column width between generations', () => { + render( + , + ); + + // @ts-expect-error need to migrate helpers to TypeScript + expect(getColumnHeaderCell(0)).toHaveInlineStyle({ width: '200px' }); + apiRef.current.updateColumns([ + { field: GRID_ROW_GROUPING_SINGLE_GROUPING_FIELD, width: 100 }, + ]); + // @ts-expect-error need to migrate helpers to TypeScript + expect(getColumnHeaderCell(0)).toHaveInlineStyle({ width: '100px' }); + apiRef.current.updateColumns([ + { + field: 'id', + headerName: 'New id', + }, + ]); + // @ts-expect-error need to migrate helpers to TypeScript + expect(getColumnHeaderCell(0)).toHaveInlineStyle({ width: '100px' }); + }); + + describe('prop: groupColDef.leafField', () => { + it('should render the leafField `value` on leaves', () => { + render( + , + ); + + expect(getColumnValues(0)).to.deep.equal([ + 'Cat A (3)', + '0', + '1', + '2', + 'Cat B (2)', + '3', + '4', + ]); + }); + + it('should render the leafField `formattedValue` on leaves if `valueFormatter` is defined on the leafColDef', () => { + render( + { + if (params.value == null) { + return null; + } + + return `#${params.value}`; + }, + }, + { + field: 'category1', + }, + ]} + initialState={{ rowGrouping: { model: ['category1'] } }} + rowGroupingColumnMode="single" + groupingColDef={{ leafField: 'id' }} + defaultGroupingExpansionDepth={-1} + />, + ); + + expect(getColumnValues(0)).to.deep.equal([ + 'Cat A (3)', + '#0', + '#1', + '#2', + 'Cat B (2)', + '#3', + '#4', + ]); + }); + + it('should render the leafField `renderCell` on leaves if `renderCell` is defined on the leafColDef', () => { + const renderIdCell = spy(() => 'Custom leaf'); + + render( + , + ); + + expect(renderIdCell.lastCall.firstArg.id).to.equal(4); + expect(renderIdCell.lastCall.firstArg.field).to.equal('id'); + expect(getColumnValues(0)).to.deep.equal([ + 'Cat A (3)', + 'Custom leaf', + 'Custom leaf', + 'Custom leaf', + 'Cat B (2)', + 'Custom leaf', + 'Custom leaf', + ]); + }); + }); + + describe('prop: groupColDef.headerName', () => { + it('should allow to override the headerName in object mode', () => { + render( + , + ); + + expect(getColumnHeadersTextContent()).to.deep.equal([ + 'Main category', + 'id', + 'category1', + 'category2', + ]); + }); + + it('should allow to override the headerName in callback mode', () => { + render( + + params.fields.includes('category1') + ? { + headerName: 'Main category', + } + : {} + } + />, + ); + + expect(getColumnHeadersTextContent()).to.deep.equal([ + 'Main category', + 'id', + 'category1', + 'category2', + ]); + }); + }); + + describe('prop: groupingColDef.hideDescendantCount', () => { + it('should render descendant count when hideDescendantCount = false', () => { + render( + , + ); + + expect(getColumnValues(0)).to.deep.equal([ + 'Cat A (3)', + 'Cat 1 (1)', + '', + 'Cat 2 (2)', + '', + '', + 'Cat B (2)', + 'Cat 2 (1)', + '', + 'Cat 1 (1)', + '', + ]); + }); + + it('should not render descendant count when hideDescendantCount = true', () => { + render( + , + ); + + expect(getColumnValues(0)).to.deep.equal([ + 'Cat A', + 'Cat 1', + '', + 'Cat 2', + '', + '', + 'Cat B', + 'Cat 2', + '', + 'Cat 1', + '', + ]); + }); + }); + }); + + describe('props: groupingColDef when groupingColumMode = "multiple"', () => { + it('should not allow to override the field', () => { + render( + , + ); + + expect(apiRef.current.getAllColumns()[0].field).to.equal( + '__row_group_by_columns_group_category1__', + ); + }); + + it('should react to groupingColDef update', () => { + const { setProps } = render( + + params.fields.includes('category1') + ? { + headerName: 'Custom group', + } + : {} + } + />, + ); + + expect(getColumnHeadersTextContent()).to.deep.equal([ + 'Custom group', + 'category2', + 'id', + 'category1', + 'category2', + ]); + + setProps({ + groupingColDef: (params) => + params.fields.includes('category2') + ? { + headerName: 'Custom group', + } + : {}, + }); + expect(getColumnHeadersTextContent()).to.deep.equal([ + 'category1', + 'Custom group', + 'id', + 'category1', + 'category2', + ]); + }); + + it('should keep the grouping column width between generations', () => { + render( + + params.fields.includes('category1') ? { width: 200 } : { width: 300 } + } + />, + ); + + // @ts-expect-error need to migrate helpers to TypeScript + expect(getColumnHeaderCell(0)).toHaveInlineStyle({ width: '200px' }); + // @ts-expect-error need to migrate helpers to TypeScript + expect(getColumnHeaderCell(1)).toHaveInlineStyle({ width: '300px' }); + apiRef.current.updateColumns([ + { field: getRowGroupingFieldFromGroupingCriteria('category1'), width: 100 }, + ]); + // @ts-expect-error need to migrate helpers to TypeScript + expect(getColumnHeaderCell(0)).toHaveInlineStyle({ width: '100px' }); + // @ts-expect-error need to migrate helpers to TypeScript + expect(getColumnHeaderCell(1)).toHaveInlineStyle({ width: '300px' }); + apiRef.current.updateColumns([ + { + field: 'id', + headerName: 'New id', + }, + ]); + // @ts-expect-error need to migrate helpers to TypeScript + expect(getColumnHeaderCell(0)).toHaveInlineStyle({ width: '100px' }); + // @ts-expect-error need to migrate helpers to TypeScript + expect(getColumnHeaderCell(1)).toHaveInlineStyle({ width: '300px' }); + }); + + describe('prop: groupColDef.leafField', () => { + it('should render the leafField `value` on leaves', () => { + render( + + params.fields.includes('category2') + ? { + leafField: 'id', + } + : {} + } + defaultGroupingExpansionDepth={-1} + />, + ); + expect(getColumnValues(0)).to.deep.equal([ + 'Cat A (3)', + '', + '', + '', + '', + '', + 'Cat B (2)', + '', + '', + '', + '', + ]); + expect(getColumnValues(1)).to.deep.equal([ + '', + 'Cat 1 (1)', + '0', + 'Cat 2 (2)', + '1', + '2', + '', + 'Cat 2 (1)', + '3', + 'Cat 1 (1)', + '4', + ]); + }); + + it('should render the leafField `formattedValue` on leaves if `valueFormatter` is defined on the leafColDef', () => { + render( + { + if (params.value == null) { + return null; + } + + return `#${params.value}`; + }, + }, + { + field: 'category1', + }, + { + field: 'category2', + }, + ]} + initialState={{ rowGrouping: { model: ['category1', 'category2'] } }} + rowGroupingColumnMode="multiple" + groupingColDef={(params) => + params.fields.includes('category2') + ? { + leafField: 'id', + } + : {} + } + defaultGroupingExpansionDepth={-1} + />, + ); + + expect(getColumnValues(0)).to.deep.equal([ + 'Cat A (3)', + '', + '', + '', + '', + '', + 'Cat B (2)', + '', + '', + '', + '', + ]); + expect(getColumnValues(1)).to.deep.equal([ + '', + 'Cat 1 (1)', + '#0', + 'Cat 2 (2)', + '#1', + '#2', + '', + 'Cat 2 (1)', + '#3', + 'Cat 1 (1)', + '#4', + ]); + }); + + it('should render the leafField `renderCell` on leaves if `renderCell` is defined on the leafColDef', () => { + const renderIdCell = spy(() => 'Custom leaf'); + + render( + + params.fields.includes('category2') + ? { + leafField: 'id', + } + : {} + } + defaultGroupingExpansionDepth={-1} + />, + ); + + expect(renderIdCell.lastCall.firstArg.id).to.equal(4); + expect(renderIdCell.lastCall.firstArg.field).to.equal('id'); + expect(getColumnValues(0)).to.deep.equal([ + 'Cat A (3)', + '', + '', + '', + '', + '', + 'Cat B (2)', + '', + '', + '', + '', + ]); + expect(getColumnValues(1)).to.deep.equal([ + '', + 'Cat 1 (1)', + 'Custom leaf', + 'Cat 2 (2)', + 'Custom leaf', + 'Custom leaf', + '', + 'Cat 2 (1)', + 'Custom leaf', + 'Cat 1 (1)', + 'Custom leaf', + ]); + }); + }); + + describe('prop: groupColDef.headerName', () => { + it('should allow to override the headerName in object mode', () => { + render( + , + ); + + expect(getColumnHeadersTextContent()).to.deep.equal([ + 'Main category', + 'Main category', + 'id', + 'category1', + 'category2', + ]); + }); + + it('should allow to override the headerName in callback mode', () => { + render( + + params.fields.includes('category1') + ? { + headerName: 'Main category', + } + : {} + } + />, + ); + + expect(getColumnHeadersTextContent()).to.deep.equal([ + 'Main category', + 'category2', + 'id', + 'category1', + 'category2', + ]); + }); + }); + + describe('prop: groupingColDef.hideDescendantCount', () => { + it('should render descendant count when hideDescendantCount = false', () => { + render( + , + ); + + expect(getColumnValues(0)).to.deep.equal([ + 'Cat A (3)', + '', + '', + '', + '', + '', + 'Cat B (2)', + '', + '', + '', + '', + ]); + expect(getColumnValues(1)).to.deep.equal([ + '', + 'Cat 1 (1)', + '', + 'Cat 2 (2)', + '', + '', + '', + 'Cat 2 (1)', + '', + 'Cat 1 (1)', + '', + ]); + }); + + it('should not render descendant count when hideDescendantCount = true', () => { + render( + , + ); + + expect(getColumnValues(0)).to.deep.equal([ + 'Cat A', + '', + '', + '', + '', + '', + 'Cat B', + '', + '', + '', + '', + ]); + expect(getColumnValues(1)).to.deep.equal([ + '', + 'Cat 1', + '', + 'Cat 2', + '', + '', + '', + 'Cat 2', + '', + 'Cat 1', + '', + ]); + }); + }); + }); + + describe('colDef: groupingValueGetter & valueGetter', () => { + it('should use groupingValueGetter to group rows when defined', () => { + render( + ) => + `groupingValue ${params.value}`, + }, + ]} + initialState={{ rowGrouping: { model: ['category1'] } }} + defaultGroupingExpansionDepth={-1} + />, + ); + expect(getColumnValues(0)).to.deep.equal([ + 'groupingValue Cat A (3)', + '', + '', + '', + 'groupingValue Cat B (2)', + '', + '', + ]); + expect(getColumnValues(1)).to.deep.equal(['', '0', '1', '2', '', '3', '4']); + }); + + it('should not use valueGetter to group the rows when defined', () => { + render( + `value ${params.row.category1}`, + }, + ]} + initialState={{ rowGrouping: { model: ['category1'] } }} + defaultGroupingExpansionDepth={-1} + />, + ); + expect(getColumnValues(0)).to.deep.equal(['Cat A (3)', '', '', '', 'Cat B (2)', '', '']); + expect(getColumnValues(1)).to.deep.equal(['', '0', '1', '2', '', '3', '4']); + }); + + it('should still pass the raw row value to the groupingValueGetter callback when valueGetter defined', () => { + render( + `value ${params.row.category1}`, + groupingValueGetter: (params: GridGroupingValueGetterParams) => + `groupingValue ${params.row.category1}`, + }, + ]} + defaultGroupingExpansionDepth={-1} + />, + ); + expect(getColumnValues(0)).to.deep.equal([ + 'groupingValue Cat A (3)', + '', + '', + '', + 'groupingValue Cat B (2)', + '', + '', + ]); + expect(getColumnValues(1)).to.deep.equal(['', '0', '1', '2', '', '3', '4']); + }); + }); + + describe('column menu', () => { + it('should add a "Group by {field}" menu item on ungrouped columns when coLDef.groupable is not defined', () => { + render( + , + ); + apiRef.current.showColumnMenu('category1'); + clock.runToLast(); + expect(screen.queryByRole('menu')).not.to.equal(null); + const menuItem = screen.queryByRole('menuitem', { name: 'Group by category1' }); + fireEvent.click(menuItem); + expect(apiRef.current.state.rowGrouping.model).to.deep.equal(['category1']); + }); + + it('should not add a "Group by {field}" menu item on ungrouped columns when coLDef.groupable = false', () => { + render( + , + ); + apiRef.current.showColumnMenu('category1'); + clock.runToLast(); + expect(screen.queryByRole('menu')).not.to.equal(null); + expect(screen.queryByRole('menuitem', { name: 'Group by category1' })).to.equal(null); + }); + + it('should add a "Stop grouping by {field}" menu item on grouped column', () => { + render( + , + ); + apiRef.current.showColumnMenu('category1'); + clock.runToLast(); + expect(screen.queryByRole('menu')).not.to.equal(null); + const menuItem = screen.queryByRole('menuitem', { name: 'Stop grouping by category1' }); + fireEvent.click(menuItem); + expect(apiRef.current.state.rowGrouping.model).to.deep.equal([]); + }); + + it('should add a "Stop grouping by {field} menu item on each grouping column when prop.rowGroupingColumnMode = "multiple"', () => { + render( + , + ); + + apiRef.current.showColumnMenu('__row_group_by_columns_group_category1__'); + clock.runToLast(); + expect(screen.queryByRole('menu')).not.to.equal(null); + const menuItemCategory1 = screen.queryByRole('menuitem', { + name: 'Stop grouping by category1', + }); + fireEvent.click(menuItemCategory1); + expect(apiRef.current.state.rowGrouping.model).to.deep.equal(['category2']); + + apiRef.current.hideColumnMenu(); + clock.runToLast(); + expect(screen.queryByRole('menu')).to.equal(null); + + apiRef.current.showColumnMenu('__row_group_by_columns_group_category2__'); + clock.runToLast(); + expect(screen.queryByRole('menu')).not.to.equal(null); + const menuItemCategory2 = screen.queryByRole('menuitem', { + name: 'Stop grouping by category2', + }); + fireEvent.click(menuItemCategory2); + expect(apiRef.current.state.rowGrouping.model).to.deep.equal([]); + }); + + it('should add a "Stop grouping {field} menu item for each grouping criteria on the grouping column when prop.rowGroupingColumnMode = "single"', () => { + render( + , + ); + + apiRef.current.showColumnMenu('__row_group_by_columns_group__'); + clock.runToLast(); + expect(screen.queryByRole('menu')).not.to.equal(null); + const menuItemCategory1 = screen.queryByRole('menuitem', { + name: 'Stop grouping by category1', + }); + fireEvent.click(menuItemCategory1); + expect(apiRef.current.state.rowGrouping.model).to.deep.equal(['category2']); + const menuItemCategory2 = screen.queryByRole('menuitem', { + name: 'Stop grouping by category2', + }); + fireEvent.click(menuItemCategory2); + expect(apiRef.current.state.rowGrouping.model).to.deep.equal([]); + }); + + it('should use the colDef.headerName property for grouping menu item label', () => { + render( + , + ); + apiRef.current.showColumnMenu('category1'); + clock.runToLast(); + expect(screen.queryByRole('menu')).not.to.equal(null); + expect(screen.queryByRole('menuitem', { name: 'Group by Category 1' })).not.to.equal(null); + }); + + it('should use the colDef.headerName property for ungrouping menu item label', () => { + render( + , + ); + apiRef.current.showColumnMenu('category1'); + clock.runToLast(); + expect(screen.queryByRole('menu')).not.to.equal(null); + expect(screen.queryByRole('menuitem', { name: 'Stop grouping by Category 1' })).not.to.equal( + null, + ); + }); + }); + + describe('sorting', () => { + describe('props: rowGroupingColumnMode = "single"', () => { + it('should use the top level grouping criteria for sorting if mainGroupingCriteria and leafField are not defined', () => { + render( + , + ); + + expect(getColumnValues(0)).to.deep.equal([ + 'Cat B (2)', + 'Cat 2 (1)', + '', + 'Cat 1 (1)', + '', + 'Cat A (3)', + 'Cat 1 (1)', + '', + 'Cat 2 (2)', + '', + '', + ]); + }); + + it('should use the column grouping criteria for sorting if mainGroupingCriteria is one of the grouping criteria and leaf field is defined', () => { + render( + , + ); + expect(getColumnValues(0)).to.deep.equal([ + 'Cat A (3)', + 'Cat 2 (2)', + '1', + '2', + 'Cat 1 (1)', + '0', + 'Cat B (2)', + 'Cat 2 (1)', + '3', + 'Cat 1 (1)', + '4', + ]); + }); + + it('should use the leaf field for sorting if mainGroupingCriteria is not defined and leaf field is defined', () => { + render( + , + ); + + expect(getColumnValues(0)).to.deep.equal([ + 'Cat A (3)', + 'Cat 1 (1)', + '0', + 'Cat 2 (2)', + '2', + '1', + 'Cat B (2)', + 'Cat 2 (1)', + '3', + 'Cat 1 (1)', + '4', + ]); + }); + + it('should use the leaf field for sorting if mainGroupingCriteria is not one of the grouping criteria and leaf field is defined', () => { + render( + , + ); + + expect(getColumnValues(0)).to.deep.equal([ + 'Cat A (3)', + 'Cat 1 (1)', + '0', + 'Cat 2 (2)', + '2', + '1', + 'Cat B (2)', + 'Cat 2 (1)', + '3', + 'Cat 1 (1)', + '4', + ]); + }); + + it('should sort unbalanced grouped by index of the grouping criteria in the model when sorting by a grouping criteria', () => { + render( + , + ); + + expect(getColumnValues(0)).to.deep.equal([ + 'Cat B (2)', + '2', + '3', + 'Cat A (2)', + '0', + '1', + '4', + '5', + ]); + }); + + it('should sort unbalanced grouped by index of the grouping criteria in the model when sorting by leaves', () => { + render( + , + ); + + expect(getColumnValues(0)).to.deep.equal([ + 'Cat A (2)', + '1', + '0', + 'Cat B (2)', + '3', + '2', + '5', + '4', + ]); + }); + }); + + describe('props: rowGroupingColumnMode = "multiple"', () => { + it('should use the column grouping criteria for sorting if mainGroupingCriteria and leafField are not defined', () => { + render( + , + ); + + expect(getColumnValues(0)).to.deep.equal(['Cat B (2)', '', '', 'Cat A (3)', '', '', '']); + }); + + it('should use the column grouping criteria for sorting if mainGroupingCriteria matches the column grouping criteria and leaf field is defined', () => { + render( + , + ); + + expect(getColumnValues(0)).to.deep.equal([ + 'Cat B (2)', + '3', + '4', + 'Cat A (3)', + '0', + '1', + '2', + ]); + }); + + it('should use the leaf field for sorting if mainGroupingCriteria is not defined and leaf field is defined', () => { + render( + , + ); + + expect(getColumnValues(0)).to.deep.equal([ + 'Cat A (3)', + '2', + '1', + '0', + 'Cat B (2)', + '4', + '3', + ]); + }); + + it("should use the leaf field for sorting if mainGroupingCriteria doesn't match the column grouping criteria and leaf field is defined", () => { + render( + , + ); + + expect(getColumnValues(0)).to.deep.equal([ + 'Cat A (3)', + '2', + '1', + '0', + 'Cat B (2)', + '4', + '3', + ]); + }); + }); + }); + + describe('filtering', () => { + clock.withFakeTimers(); + + describe('props: rowGroupingColumnMode = "single"', () => { + it('should use the top level grouping criteria for filtering if mainGroupingCriteria and leafField are not defined', () => { + render( + , + ); + + fireEvent.change(screen.getByRole('textbox', { name: 'Value' }), { + target: { value: 'Cat A' }, + }); + clock.tick(500); + + expect(getColumnValues(0)).to.deep.equal([ + 'Cat A (3)', + 'Cat 1 (1)', + '', + 'Cat 2 (2)', + '', + '', + ]); + }); + + it('should use the column grouping criteria for filtering if mainGroupingCriteria is one of the grouping criteria and leaf field is defined', () => { + render( + , + ); + + fireEvent.change(screen.getByRole('textbox', { name: 'Value' }), { + target: { value: 'Cat 1' }, + }); + clock.tick(500); + + expect(getColumnValues(0)).to.deep.equal([ + 'Cat A (1)', + 'Cat 1 (1)', + '0', + 'Cat B (1)', + 'Cat 1 (1)', + '4', + ]); + }); + + it('should use the leaf field for filtering if mainGroupingCriteria is not defined and leaf field is defined', () => { + render( + , + ); + + fireEvent.change(screen.getByRole('combobox', { name: 'Operators' }), { + target: { value: '>' }, + }); + fireEvent.change(screen.getByRole('spinbutton', { name: 'Value' }), { + target: { value: 2 }, + }); + clock.tick(500); + + expect(getColumnValues(0)).to.deep.equal(['Cat B (2)', 'Cat 2 (1)', '3', 'Cat 1 (1)', '4']); + }); + + it('should use the leaf field for filtering if mainGroupingCriteria is not one of the grouping criteria and leaf field is defined', () => { + render( + , + ); + + fireEvent.change(screen.getByRole('combobox', { name: 'Operators' }), { + target: { value: '>' }, + }); + fireEvent.change(screen.getByRole('spinbutton', { name: 'Value' }), { + target: { value: 2 }, + }); + clock.tick(500); + + expect(getColumnValues(0)).to.deep.equal(['Cat B (2)', 'Cat 2 (1)', '3', 'Cat 1 (1)', '4']); + }); + + it('should not filter the groups when filtering with an item that is not on the grouping column', () => { + render( + { + it('should use the column grouping criteria for filtering if mainGroupingCriteria and leafField are not defined', () => { + render( + , + ); + + fireEvent.change(screen.getByRole('textbox', { name: 'Value' }), { + target: { value: 'Cat A' }, + }); + clock.tick(500); + + expect(getColumnValues(0)).to.deep.equal(['Cat A (3)', '', '', '']); + expect(getColumnValues(1)).to.deep.equal(['', '0', '1', '2']); + }); + + it('should use the column grouping criteria for filtering if mainGroupingCriteria matches the column grouping criteria and leaf field is defined', () => { + render( + , + ); + + fireEvent.change(screen.getByRole('textbox', { name: 'Value' }), { + target: { value: 'Cat A' }, + }); + clock.tick(500); + + expect(getColumnValues(0)).to.deep.equal(['Cat A (3)', '0', '1', '2']); + }); + + it('should use the leaf field for filtering if mainGroupingCriteria is not defined and leaf field is defined', () => { + render( + , + ); + + fireEvent.change(screen.getByRole('combobox', { name: 'Operators' }), { + target: { value: '>' }, + }); + fireEvent.change(screen.getByRole('spinbutton', { name: 'Value' }), { + target: { value: 2 }, + }); + clock.tick(500); + + expect(getColumnValues(0)).to.deep.equal(['Cat B (2)', '3', '4']); + }); + + it("should use the leaf field for filtering if mainGroupingCriteria doesn't match the column grouping criteria and leaf field is defined", () => { + render( + , + ); + + fireEvent.change(screen.getByRole('combobox', { name: 'Operators' }), { + target: { value: '>' }, + }); + fireEvent.change(screen.getByRole('spinbutton', { name: 'Value' }), { + target: { value: 2 }, + }); + clock.tick(500); + + expect(getColumnValues(0)).to.deep.equal(['Cat B (2)', '3', '4']); + }); + + it('should not filter the groups when filtering with an item that is not on the grouping column', () => { + render( + { + render( + , + ); + + // "Cat A" is testing against the "__row_group_by_columns_group_category1__" filter item, but "Cat 1" and "Cat 2" are not + expect(getColumnValues(0)).to.deep.equal(['Cat A (3)', '', '', '', '', '']); + expect(getColumnValues(1)).to.deep.equal(['', 'Cat 1 (1)', '', 'Cat 2 (2)', '', '']); + }); + }); + }); + + describe('apiRef: addRowGroupingCriteria', () => { + it('should add grouping criteria to model', () => { + render(); + apiRef.current.addRowGroupingCriteria('category2'); + expect(apiRef.current.state.rowGrouping.model).to.deep.equal(['category1', 'category2']); + }); + + it('should add grouping criteria to model at the right position', () => { + render(); + apiRef.current.addRowGroupingCriteria('category2', 0); + expect(apiRef.current.state.rowGrouping.model).to.deep.equal(['category2', 'category1']); + }); + }); + + describe('apiRef: removeRowGroupingCriteria', () => { + it('should remove field from model', () => { + render(); + apiRef.current.removeRowGroupingCriteria('category1'); + expect(apiRef.current.state.rowGrouping.model).to.deep.equal([]); + }); + }); + + describe('apiRef: setRowGroupingCriteriaIndex', () => { + it('should change the grouping criteria order', () => { + render(); + apiRef.current.setRowGroupingCriteriaIndex('category1', 1); + expect(apiRef.current.state.rowGrouping.model).to.deep.equal(['category2', 'category1']); + }); + }); +}); diff --git a/packages/grid/x-data-grid-pro/src/useDataGridProComponent.tsx b/packages/grid/x-data-grid-pro/src/useDataGridProComponent.tsx index ee1229138f6e8..c2f5607d1dee8 100644 --- a/packages/grid/x-data-grid-pro/src/useDataGridProComponent.tsx +++ b/packages/grid/x-data-grid-pro/src/useDataGridProComponent.tsx @@ -28,11 +28,13 @@ import { useGridScroll } from '../../_modules_/grid/hooks/features/scroll/useGri import { useGridEvents } from '../../_modules_/grid/hooks/features/events/useGridEvents'; import { useGridDimensions } from '../../_modules_/grid/hooks/features/dimensions/useGridDimensions'; import { useGridTreeData } from '../../_modules_/grid/hooks/features/treeData/useGridTreeData'; +import { useGridRowGrouping } from '../../_modules_/grid/hooks/features/rowGrouping/useGridRowGrouping'; import { useGridColumnPinning } from '../../_modules_/grid/hooks/features/columnPinning/useGridColumnPinning'; export const useDataGridProComponent = (apiRef: GridApiRef, props: DataGridProProcessedProps) => { useGridInitialization(apiRef, props); useGridTreeData(apiRef, props); + useGridRowGrouping(apiRef, props); useGridSelection(apiRef, props); useGridColumns(apiRef, props); useGridRows(apiRef, props); diff --git a/packages/grid/x-data-grid-pro/src/useDataGridProProps.ts b/packages/grid/x-data-grid-pro/src/useDataGridProProps.ts index 80d071119896b..ff7972ac281e5 100644 --- a/packages/grid/x-data-grid-pro/src/useDataGridProProps.ts +++ b/packages/grid/x-data-grid-pro/src/useDataGridProProps.ts @@ -37,6 +37,8 @@ export const useDataGridProProps = (inProps: DataGridProProps) => { () => ({ ...DATA_GRID_PRO_PROPS_DEFAULT_VALUES, ...themedProps, + disableRowGrouping: + themedProps.disableRowGrouping || !themedProps.experimentalFeatures?.rowGrouping, localeText, components, signature: 'DataGridPro', diff --git a/scripts/x-data-grid-pro.exports.json b/scripts/x-data-grid-pro.exports.json index fcee47fa9d0d3..a3fcce5c2021e 100644 --- a/scripts/x-data-grid-pro.exports.json +++ b/scripts/x-data-grid-pro.exports.json @@ -32,6 +32,7 @@ { "name": "getGridNumericOperators", "kind": "Variable" }, { "name": "getGridSingleSelectOperators", "kind": "Variable" }, { "name": "getGridStringOperators", "kind": "Variable" }, + { "name": "getRowGroupingFieldFromGroupingCriteria", "kind": "ExportSpecifier" }, { "name": "GRID_ACTIONS_COL_DEF", "kind": "Variable" }, { "name": "GRID_BOOLEAN_COL_DEF", "kind": "Variable" }, { "name": "GRID_CHECKBOX_SELECTION_COL_DEF", "kind": "Variable" }, @@ -40,6 +41,7 @@ { "name": "GRID_DEFAULT_LOCALE_TEXT", "kind": "Variable" }, { "name": "GRID_EXPERIMENTAL_ENABLED", "kind": "Variable" }, { "name": "GRID_NUMERIC_COL_DEF", "kind": "Variable" }, + { "name": "GRID_ROW_GROUPING_SINGLE_GROUPING_FIELD", "kind": "ExportSpecifier" }, { "name": "GRID_SINGLE_SELECT_COL_DEF", "kind": "Variable" }, { "name": "GRID_STRING_COL_DEF", "kind": "Variable" }, { "name": "GRID_TREE_DATA_GROUPING_FIELD", "kind": "ExportSpecifier" }, @@ -166,7 +168,6 @@ { "name": "GridEventPublisher", "kind": "TypeAlias" }, { "name": "GridEvents", "kind": "Enum" }, { "name": "GridEventsStr", "kind": "TypeAlias" }, - { "name": "GridExpandLessIcon", "kind": "Variable" }, { "name": "GridExpandMoreIcon", "kind": "Variable" }, { "name": "GridExportFormat", "kind": "TypeAlias" }, { "name": "GridExportOptions", "kind": "Interface" }, @@ -212,6 +213,7 @@ { "name": "GridFooterPlaceholder", "kind": "Function" }, { "name": "GridGroupingColDefOverride", "kind": "Interface" }, { "name": "GridGroupingColDefOverrideParams", "kind": "Interface" }, + { "name": "GridGroupingValueGetterParams", "kind": "Interface" }, { "name": "GridHeader", "kind": "Variable" }, { "name": "GridHeaderCheckbox", "kind": "ExportSpecifier" }, { "name": "GridHeaderPlaceholder", "kind": "Function" }, @@ -219,6 +221,7 @@ { "name": "GridIconSlotsComponent", "kind": "Interface" }, { "name": "GridInitialState", "kind": "Interface" }, { "name": "GridInputSelectionModel", "kind": "TypeAlias" }, + { "name": "GridKeyboardArrowRight", "kind": "Variable" }, { "name": "GridKeyValue", "kind": "TypeAlias" }, { "name": "GridLinkOperator", "kind": "Enum" }, { "name": "GridLoadIcon", "kind": "Variable" }, @@ -284,7 +287,14 @@ { "name": "GridRowData", "kind": "TypeAlias" }, { "name": "GridRowEntry", "kind": "TypeAlias" }, { "name": "GridRowEventLookup", "kind": "Interface" }, + { "name": "GridRowGroupingApi", "kind": "Interface" }, + { "name": "GridRowGroupingInitialState", "kind": "Interface" }, + { "name": "GridRowGroupingModel", "kind": "TypeAlias" }, + { "name": "gridRowGroupingModelSelector", "kind": "Variable" }, { "name": "gridRowGroupingNameSelector", "kind": "Variable" }, + { "name": "gridRowGroupingSanitizedModelSelector", "kind": "Variable" }, + { "name": "GridRowGroupingState", "kind": "Interface" }, + { "name": "gridRowGroupingStateSelector", "kind": "Variable" }, { "name": "GridRowId", "kind": "TypeAlias" }, { "name": "GridRowIdGetter", "kind": "TypeAlias" }, { "name": "gridRowIdsSelector", "kind": "Variable" }, @@ -357,9 +367,8 @@ { "name": "GridTypeFilterInputValueProps", "kind": "Interface" }, { "name": "GridUpdateAction", "kind": "TypeAlias" }, { "name": "GridValueFormatterParams", "kind": "Interface" }, - { "name": "GridValueGetterFullParams", "kind": "Interface" }, - { "name": "GridValueGetterParams", "kind": "TypeAlias" }, - { "name": "GridValueGetterSimpleParams", "kind": "Interface" }, + { "name": "GridValueGetterFullParams", "kind": "TypeAlias" }, + { "name": "GridValueGetterParams", "kind": "Interface" }, { "name": "GridValueOptionsParams", "kind": "Interface" }, { "name": "GridValueSetterParams", "kind": "Interface" }, { "name": "GridViewHeadlineIcon", "kind": "Variable" }, diff --git a/scripts/x-data-grid.exports.json b/scripts/x-data-grid.exports.json index 8d2e4e1dd3ae5..eb9c706eda163 100644 --- a/scripts/x-data-grid.exports.json +++ b/scripts/x-data-grid.exports.json @@ -32,6 +32,7 @@ { "name": "getGridNumericOperators", "kind": "Variable" }, { "name": "getGridSingleSelectOperators", "kind": "Variable" }, { "name": "getGridStringOperators", "kind": "Variable" }, + { "name": "getRowGroupingFieldFromGroupingCriteria", "kind": "ExportSpecifier" }, { "name": "GRID_ACTIONS_COL_DEF", "kind": "Variable" }, { "name": "GRID_BOOLEAN_COL_DEF", "kind": "Variable" }, { "name": "GRID_CHECKBOX_SELECTION_COL_DEF", "kind": "Variable" }, @@ -40,6 +41,7 @@ { "name": "GRID_DEFAULT_LOCALE_TEXT", "kind": "Variable" }, { "name": "GRID_EXPERIMENTAL_ENABLED", "kind": "Variable" }, { "name": "GRID_NUMERIC_COL_DEF", "kind": "Variable" }, + { "name": "GRID_ROW_GROUPING_SINGLE_GROUPING_FIELD", "kind": "ExportSpecifier" }, { "name": "GRID_SINGLE_SELECT_COL_DEF", "kind": "Variable" }, { "name": "GRID_STRING_COL_DEF", "kind": "Variable" }, { "name": "GRID_TREE_DATA_GROUPING_FIELD", "kind": "ExportSpecifier" }, @@ -166,7 +168,6 @@ { "name": "GridEventPublisher", "kind": "TypeAlias" }, { "name": "GridEvents", "kind": "Enum" }, { "name": "GridEventsStr", "kind": "TypeAlias" }, - { "name": "GridExpandLessIcon", "kind": "Variable" }, { "name": "GridExpandMoreIcon", "kind": "Variable" }, { "name": "GridExportFormat", "kind": "TypeAlias" }, { "name": "GridExportOptions", "kind": "Interface" }, @@ -212,6 +213,7 @@ { "name": "GridFooterPlaceholder", "kind": "Function" }, { "name": "GridGroupingColDefOverride", "kind": "Interface" }, { "name": "GridGroupingColDefOverrideParams", "kind": "Interface" }, + { "name": "GridGroupingValueGetterParams", "kind": "Interface" }, { "name": "GridHeader", "kind": "Variable" }, { "name": "GridHeaderCheckbox", "kind": "ExportSpecifier" }, { "name": "GridHeaderPlaceholder", "kind": "Function" }, @@ -219,6 +221,7 @@ { "name": "GridIconSlotsComponent", "kind": "Interface" }, { "name": "GridInitialState", "kind": "Interface" }, { "name": "GridInputSelectionModel", "kind": "TypeAlias" }, + { "name": "GridKeyboardArrowRight", "kind": "Variable" }, { "name": "GridKeyValue", "kind": "TypeAlias" }, { "name": "GridLinkOperator", "kind": "Enum" }, { "name": "GridLoadIcon", "kind": "Variable" }, @@ -284,7 +287,14 @@ { "name": "GridRowData", "kind": "TypeAlias" }, { "name": "GridRowEntry", "kind": "TypeAlias" }, { "name": "GridRowEventLookup", "kind": "Interface" }, + { "name": "GridRowGroupingApi", "kind": "Interface" }, + { "name": "GridRowGroupingInitialState", "kind": "Interface" }, + { "name": "GridRowGroupingModel", "kind": "TypeAlias" }, + { "name": "gridRowGroupingModelSelector", "kind": "Variable" }, { "name": "gridRowGroupingNameSelector", "kind": "Variable" }, + { "name": "gridRowGroupingSanitizedModelSelector", "kind": "Variable" }, + { "name": "GridRowGroupingState", "kind": "Interface" }, + { "name": "gridRowGroupingStateSelector", "kind": "Variable" }, { "name": "GridRowId", "kind": "TypeAlias" }, { "name": "GridRowIdGetter", "kind": "TypeAlias" }, { "name": "gridRowIdsSelector", "kind": "Variable" }, @@ -357,9 +367,8 @@ { "name": "GridTypeFilterInputValueProps", "kind": "Interface" }, { "name": "GridUpdateAction", "kind": "TypeAlias" }, { "name": "GridValueFormatterParams", "kind": "Interface" }, - { "name": "GridValueGetterFullParams", "kind": "Interface" }, - { "name": "GridValueGetterParams", "kind": "TypeAlias" }, - { "name": "GridValueGetterSimpleParams", "kind": "Interface" }, + { "name": "GridValueGetterFullParams", "kind": "TypeAlias" }, + { "name": "GridValueGetterParams", "kind": "Interface" }, { "name": "GridValueOptionsParams", "kind": "Interface" }, { "name": "GridValueSetterParams", "kind": "Interface" }, { "name": "GridViewHeadlineIcon", "kind": "Variable" },