From 4c4ecf8eb0c09428d342206325f36b030049ea20 Mon Sep 17 00:00:00 2001 From: Bilal Shafi Date: Mon, 8 Jul 2024 12:45:27 +0500 Subject: [PATCH 01/45] [DataGridPro] Add option to propagate row selection for nested rows --- .../RowGroupingPropagateSelection.js | 33 +++++++ .../RowGroupingPropagateSelection.tsx | 33 +++++++ .../RowGroupingPropagateSelection.tsx.preview | 7 ++ .../data-grid/row-grouping/row-grouping.md | 16 ++++ docs/data/data-grid/tree-data/tree-data.md | 4 + .../x/api/data-grid/data-grid-premium.json | 1 + docs/pages/x/api/data-grid/data-grid-pro.json | 1 + .../data-grid-premium/data-grid-premium.json | 3 + .../data-grid-pro/data-grid-pro.json | 3 + .../src/DataGridPremium/DataGridPremium.tsx | 10 ++ .../src/DataGridPro/DataGridPro.tsx | 10 ++ .../src/DataGridPro/useDataGridProProps.ts | 1 + .../GridCellCheckboxRenderer.tsx | 21 ++++ .../rowSelection/useGridRowSelection.ts | 35 +++++-- .../src/hooks/features/rowSelection/utils.ts | 96 +++++++++++++++++++ .../src/models/props/DataGridProps.ts | 10 ++ 16 files changed, 276 insertions(+), 8 deletions(-) create mode 100644 docs/data/data-grid/row-grouping/RowGroupingPropagateSelection.js create mode 100644 docs/data/data-grid/row-grouping/RowGroupingPropagateSelection.tsx create mode 100644 docs/data/data-grid/row-grouping/RowGroupingPropagateSelection.tsx.preview diff --git a/docs/data/data-grid/row-grouping/RowGroupingPropagateSelection.js b/docs/data/data-grid/row-grouping/RowGroupingPropagateSelection.js new file mode 100644 index 000000000000..5a408ba6405b --- /dev/null +++ b/docs/data/data-grid/row-grouping/RowGroupingPropagateSelection.js @@ -0,0 +1,33 @@ +import * as React from 'react'; +import { + DataGridPremium, + useGridApiRef, + useKeepGroupedColumnsHidden, +} from '@mui/x-data-grid-premium'; +import { useMovieData } from '@mui/x-data-grid-generator'; + +export default function RowGroupingPropagateSelection() { + const data = useMovieData(); + const apiRef = useGridApiRef(); + + const initialState = useKeepGroupedColumnsHidden({ + apiRef, + initialState: { + rowGrouping: { + model: ['company'], + }, + }, + }); + + return ( +
+ +
+ ); +} diff --git a/docs/data/data-grid/row-grouping/RowGroupingPropagateSelection.tsx b/docs/data/data-grid/row-grouping/RowGroupingPropagateSelection.tsx new file mode 100644 index 000000000000..5a408ba6405b --- /dev/null +++ b/docs/data/data-grid/row-grouping/RowGroupingPropagateSelection.tsx @@ -0,0 +1,33 @@ +import * as React from 'react'; +import { + DataGridPremium, + useGridApiRef, + useKeepGroupedColumnsHidden, +} from '@mui/x-data-grid-premium'; +import { useMovieData } from '@mui/x-data-grid-generator'; + +export default function RowGroupingPropagateSelection() { + const data = useMovieData(); + const apiRef = useGridApiRef(); + + const initialState = useKeepGroupedColumnsHidden({ + apiRef, + initialState: { + rowGrouping: { + model: ['company'], + }, + }, + }); + + return ( +
+ +
+ ); +} diff --git a/docs/data/data-grid/row-grouping/RowGroupingPropagateSelection.tsx.preview b/docs/data/data-grid/row-grouping/RowGroupingPropagateSelection.tsx.preview new file mode 100644 index 000000000000..761de773b092 --- /dev/null +++ b/docs/data/data-grid/row-grouping/RowGroupingPropagateSelection.tsx.preview @@ -0,0 +1,7 @@ + \ No newline at end of file diff --git a/docs/data/data-grid/row-grouping/row-grouping.md b/docs/data/data-grid/row-grouping/row-grouping.md index 08ef4281d6cf..f7671a79abc8 100644 --- a/docs/data/data-grid/row-grouping/row-grouping.md +++ b/docs/data/data-grid/row-grouping/row-grouping.md @@ -305,6 +305,22 @@ In the example below: If you are dynamically switching the `leafField` or `mainGroupingCriteria`, the sorting and filtering models will not be cleaned up automatically, and the sorting/filtering will not be re-applied. ::: +## Propagate row selection + +By default, selecting a parent row will not select its children. +Set the `propagateRowSelection` prop to `true` to achieve the following behavior. + +1. Selecting/Deselecting a parent row would select/deselect all the children rows. +2. When all child rows are selected, the parent row will be auto selected. +3. When a child row is deselected, if one or more parent rows are already selected, they will be auto deselected. +4. Select All checkbox would select/deselect all the rows including child rows. + +{{"demo": "RowGroupingPropagateSelection.js", "bg": "inline", "defaultCodeOpen": false}} + +:::warning +The `propagateRowSelection` is a client-side feature and not recommended to be used with the [server-side data source](/x/react-data-grid/server-side-data/), since it will only work on the partially loaded data. +::: + ## Get the rows in a group You can use the `apiRef.current.getRowGroupChildren` method to get the id of all rows contained in a group. diff --git a/docs/data/data-grid/tree-data/tree-data.md b/docs/data/data-grid/tree-data/tree-data.md index 3af238f688a3..8bc4d2b97065 100644 --- a/docs/data/data-grid/tree-data/tree-data.md +++ b/docs/data/data-grid/tree-data/tree-data.md @@ -87,6 +87,10 @@ If you want to access the grouping column field, for instance, to use it with co Same behavior as for the [Row grouping](/x/react-data-grid/row-grouping/#group-expansion). +## Propagate row selection + +Same behavior as for the [Row grouping](/x/react-data-grid/row-grouping/#propagate-row-selection). + ## Gaps in the tree If some entries are missing to build the full tree, the `DataGridPro` will automatically create rows to fill those gaps. diff --git a/docs/pages/x/api/data-grid/data-grid-premium.json b/docs/pages/x/api/data-grid/data-grid-premium.json index bce75b9e5e66..fa1227f3bcae 100644 --- a/docs/pages/x/api/data-grid/data-grid-premium.json +++ b/docs/pages/x/api/data-grid/data-grid-premium.json @@ -558,6 +558,7 @@ "returned": "Promise | R" } }, + "propagateRowSelection": { "type": { "name": "bool" }, "default": "false" }, "resizeThrottleMs": { "type": { "name": "number" }, "default": "60" }, "rowBufferPx": { "type": { "name": "number" }, "default": "150" }, "rowCount": { "type": { "name": "number" } }, diff --git a/docs/pages/x/api/data-grid/data-grid-pro.json b/docs/pages/x/api/data-grid/data-grid-pro.json index 7767957b769d..c1b50a739a07 100644 --- a/docs/pages/x/api/data-grid/data-grid-pro.json +++ b/docs/pages/x/api/data-grid/data-grid-pro.json @@ -501,6 +501,7 @@ "returned": "Promise | R" } }, + "propagateRowSelection": { "type": { "name": "bool" }, "default": "false" }, "resizeThrottleMs": { "type": { "name": "number" }, "default": "60" }, "rowBufferPx": { "type": { "name": "number" }, "default": "150" }, "rowCount": { "type": { "name": "number" } }, diff --git a/docs/translations/api-docs/data-grid/data-grid-premium/data-grid-premium.json b/docs/translations/api-docs/data-grid/data-grid-premium/data-grid-premium.json index a8b79b49fb93..34c6faa2a402 100644 --- a/docs/translations/api-docs/data-grid/data-grid-premium/data-grid-premium.json +++ b/docs/translations/api-docs/data-grid/data-grid-premium/data-grid-premium.json @@ -586,6 +586,9 @@ "Promise | R": "The final values to update the row." } }, + "propagateRowSelection": { + "description": "If true, following behavior happens with nested data: 1. Selecting/Deselecting a parent row selects/deselects all the nested rows. 2. When all nested rows are selected, the parent row is also selected. 3. When a nested row is deselected, the parent row is also deselected, if already selected. 4. Select All checkbox selects/deselects all the rows including nested rows. Works with tree data and row grouping on the client-side only." + }, "resizeThrottleMs": { "description": "The milliseconds throttle delay for resizing the grid." }, "rowBufferPx": { "description": "Row region in pixels to render before/after the viewport" }, "rowCount": { diff --git a/docs/translations/api-docs/data-grid/data-grid-pro/data-grid-pro.json b/docs/translations/api-docs/data-grid/data-grid-pro/data-grid-pro.json index 914817291c87..5750236894c6 100644 --- a/docs/translations/api-docs/data-grid/data-grid-pro/data-grid-pro.json +++ b/docs/translations/api-docs/data-grid/data-grid-pro/data-grid-pro.json @@ -532,6 +532,9 @@ "Promise | R": "The final values to update the row." } }, + "propagateRowSelection": { + "description": "If true, following behavior happens with nested data: 1. Selecting/Deselecting a parent row selects/deselects all the nested rows. 2. When all nested rows are selected, the parent row is also selected. 3. When a nested row is deselected, the parent row is also deselected, if already selected. 4. Select All checkbox selects/deselects all the rows including nested rows. Works with tree data and row grouping on the client-side only." + }, "resizeThrottleMs": { "description": "The milliseconds throttle delay for resizing the grid." }, "rowBufferPx": { "description": "Row region in pixels to render before/after the viewport" }, "rowCount": { diff --git a/packages/x-data-grid-premium/src/DataGridPremium/DataGridPremium.tsx b/packages/x-data-grid-premium/src/DataGridPremium/DataGridPremium.tsx index 2cba34da09e7..5fce50df7cb7 100644 --- a/packages/x-data-grid-premium/src/DataGridPremium/DataGridPremium.tsx +++ b/packages/x-data-grid-premium/src/DataGridPremium/DataGridPremium.tsx @@ -893,6 +893,16 @@ DataGridPremiumRaw.propTypes = { * @returns {Promise | R} The final values to update the row. */ processRowUpdate: PropTypes.func, + /** + * If `true`, following behavior happens with nested data: + * 1. Selecting/Deselecting a parent row selects/deselects all the nested rows. + * 2. When all nested rows are selected, the parent row is also selected. + * 3. When a nested row is deselected, the parent row is also deselected, if already selected. + * 4. Select All checkbox selects/deselects all the rows including nested rows. + * Works with tree data and row grouping on the client-side only. + * @default false + */ + propagateRowSelection: PropTypes.bool, /** * The milliseconds throttle delay for resizing the grid. * @default 60 diff --git a/packages/x-data-grid-pro/src/DataGridPro/DataGridPro.tsx b/packages/x-data-grid-pro/src/DataGridPro/DataGridPro.tsx index 7f7c45fa1d5e..c8c9793c28f2 100644 --- a/packages/x-data-grid-pro/src/DataGridPro/DataGridPro.tsx +++ b/packages/x-data-grid-pro/src/DataGridPro/DataGridPro.tsx @@ -809,6 +809,16 @@ DataGridProRaw.propTypes = { * @returns {Promise | R} The final values to update the row. */ processRowUpdate: PropTypes.func, + /** + * If `true`, following behavior happens with nested data: + * 1. Selecting/Deselecting a parent row selects/deselects all the nested rows. + * 2. When all nested rows are selected, the parent row is also selected. + * 3. When a nested row is deselected, the parent row is also deselected, if already selected. + * 4. Select All checkbox selects/deselects all the rows including nested rows. + * Works with tree data and row grouping on the client-side only. + * @default false + */ + propagateRowSelection: PropTypes.bool, /** * The milliseconds throttle delay for resizing the grid. * @default 60 diff --git a/packages/x-data-grid-pro/src/DataGridPro/useDataGridProProps.ts b/packages/x-data-grid-pro/src/DataGridPro/useDataGridProProps.ts index b970dfdd4645..c1138477c866 100644 --- a/packages/x-data-grid-pro/src/DataGridPro/useDataGridProProps.ts +++ b/packages/x-data-grid-pro/src/DataGridPro/useDataGridProProps.ts @@ -50,6 +50,7 @@ export const DATA_GRID_PRO_PROPS_DEFAULT_VALUES: DataGridProPropsWithDefaultValu rowsLoadingMode: 'client', getDetailPanelHeight: () => 500, headerFilters: false, + propagateRowSelection: false, }; const defaultSlots = DATA_GRID_PRO_DEFAULT_SLOTS_COMPONENTS; diff --git a/packages/x-data-grid/src/components/columnSelection/GridCellCheckboxRenderer.tsx b/packages/x-data-grid/src/components/columnSelection/GridCellCheckboxRenderer.tsx index 377713f3bb3b..3e25fdec48df 100644 --- a/packages/x-data-grid/src/components/columnSelection/GridCellCheckboxRenderer.tsx +++ b/packages/x-data-grid/src/components/columnSelection/GridCellCheckboxRenderer.tsx @@ -10,6 +10,9 @@ import { useGridRootProps } from '../../hooks/utils/useGridRootProps'; import { getDataGridUtilityClass } from '../../constants/gridClasses'; import type { DataGridProcessedProps } from '../../models/props/DataGridProps'; import type { GridRowSelectionCheckboxParams } from '../../models/params/gridRowSelectionCheckboxParams'; +import { selectedIdsLookupSelector } from '../../hooks/features/rowSelection/gridRowSelectionSelector'; +import { useGridSelector } from '../../hooks/utils/useGridSelector'; +import { GridRowId } from '../../models'; type OwnerState = { classes: DataGridProcessedProps['classes'] }; @@ -50,6 +53,23 @@ const GridCellCheckboxForwardRef = React.forwardRef(null); + const rowSelectionLookup = useGridSelector(apiRef, selectedIdsLookupSelector); + const children: GridRowId[] = React.useMemo(() => { + if (rowNode.type === 'group') { + // TODO check if a selector could be used here + // @ts-expect-error Access `GridProApi` method + return apiRef.current.getRowGroupChildren({ groupId: id }); + } + return []; + }, [id, rowNode.type, apiRef]); + + const someChildrenSelected = React.useMemo(() => { + if (rowNode.type === 'group') { + return children.some((child) => rowSelectionLookup[child]); + } + return false; + }, [children, rowNode.type, rowSelectionLookup]); + const rippleRef = React.useRef(null); const handleRef = useForkRef(checkboxElement, ref); @@ -104,6 +124,7 @@ const GridCellCheckboxForwardRef = React.forwardRef, ): void => { @@ -119,6 +121,7 @@ export const useGridRowSelection = ( const canHaveMultipleSelection = isMultipleRowSelectionEnabled(props); const visibleRows = useGridVisibleRows(apiRef, props); + const tree = useGridSelector(apiRef, gridRowTreeSelector); const expandMouseRowRangeSelection = React.useCallback( (id: GridRowId) => { @@ -146,9 +149,6 @@ export const useGridRowSelection = ( [apiRef], ); - /** - * API METHODS - */ const setRowSelectionModel = React.useCallback( (model) => { if ( @@ -219,10 +219,17 @@ export const useGridRowSelection = ( logger.debug(`Toggling selection for row ${id}`); const selection = gridRowSelectionStateSelector(apiRef.current.state); - const newSelection: GridRowId[] = selection.filter((el) => el !== id); + let newSelection: GridRowId[] = selection.filter((el) => el !== id); if (isSelected) { newSelection.push(id); + if (props.propagateRowSelection) { + const rowsToSelect = findRowsToSelect(apiRef, tree, id); + newSelection.push(...rowsToSelect); + } + } else if (props.propagateRowSelection) { + const rowsToDeselect = findRowsToDeselect(apiRef, tree, id); + newSelection = newSelection.filter((el) => !rowsToDeselect.includes(el)); } const isSelectionValid = newSelection.length < 2 || canHaveMultipleSelection; @@ -231,7 +238,7 @@ export const useGridRowSelection = ( } } }, - [apiRef, logger, canHaveMultipleSelection], + [apiRef, logger, canHaveMultipleSelection, tree, props.propagateRowSelection], ); const selectRows = React.useCallback( @@ -252,8 +259,20 @@ export const useGridRowSelection = ( selectableIds.forEach((id) => { if (isSelected) { selectionLookup[id] = id; + if (props.propagateRowSelection) { + const rowsToSelect = findRowsToSelect(apiRef, tree, id); + rowsToSelect.forEach((rowId) => { + selectionLookup[rowId] = rowId; + }); + } } else { delete selectionLookup[id]; + if (props.propagateRowSelection) { + const rowsToDeselect = findRowsToDeselect(apiRef, tree, id); + rowsToDeselect.forEach((parentId) => { + delete selectionLookup[parentId]; + }); + } } }); @@ -265,7 +284,7 @@ export const useGridRowSelection = ( apiRef.current.setRowSelectionModel(newSelection); } }, - [apiRef, logger, canHaveMultipleSelection], + [apiRef, logger, canHaveMultipleSelection, props.propagateRowSelection, tree], ); const selectRowRange = React.useCallback( diff --git a/packages/x-data-grid/src/hooks/features/rowSelection/utils.ts b/packages/x-data-grid/src/hooks/features/rowSelection/utils.ts index 908dc4260d1e..55e47f6b29ab 100644 --- a/packages/x-data-grid/src/hooks/features/rowSelection/utils.ts +++ b/packages/x-data-grid/src/hooks/features/rowSelection/utils.ts @@ -1,5 +1,8 @@ import type { DataGridProcessedProps } from '../../../models/props/DataGridProps'; import { GridSignature } from '../../utils/useGridApiEventHandler'; +import { GRID_ROOT_GROUP_ID } from '../rows/gridRowsUtils'; +import type { GridGroupNode, GridRowId, GridRowTreeConfig } from '../../../models/gridRows'; +import type { GridPrivateApiCommunity } from '../../../models/api/gridApiCommunity'; export function isMultipleRowSelectionEnabled( props: Pick< @@ -13,3 +16,96 @@ export function isMultipleRowSelectionEnabled( } return !props.disableMultipleRowSelection; } + +const getRowNodeParents = (tree: GridRowTreeConfig, id: GridRowId) => { + const parents: GridRowId[] = []; + + let parent: GridRowId | null = id; + + while (parent && parent !== GRID_ROOT_GROUP_ID) { + const node = tree[parent] as GridGroupNode; + if (!node) { + return parents; + } + parents.push(parent); + + parent = node.parent; + } + return parents; +}; + +const getRowNodeSiblings = ( + apiRef: React.MutableRefObject, + tree: GridRowTreeConfig, + id: GridRowId, +) => { + const node = apiRef.current.getRowNode(id); + if (!node) { + return []; + } + + const parent = node.parent; + if (!parent) { + return []; + } + + const parentNode = tree[parent] as GridGroupNode; + + return [...parentNode.children].filter((childId) => childId !== id); +}; + +export const findRowsToSelect = ( + apiRef: React.MutableRefObject, + tree: GridRowTreeConfig, + selectedRow: GridRowId, +) => { + const rowsToSelect: GridRowId[] = []; + + const traverseParents = (rowId: GridRowId) => { + const siblings: GridRowId[] = getRowNodeSiblings(apiRef, tree, rowId); + if ( + siblings.length === 0 || + siblings.every((sibling) => apiRef.current.isRowSelected(sibling)) + ) { + const rowNode = apiRef.current.getRowNode(rowId) as GridGroupNode; + const parent = rowNode.parent; + if (parent && parent !== GRID_ROOT_GROUP_ID) { + rowsToSelect.push(parent); + traverseParents(parent); + } + } + }; + traverseParents(selectedRow); + + const rowNode = apiRef.current.getRowNode(selectedRow); + + if (rowNode?.type === 'group') { + rowsToSelect.push(...apiRef.current.getRowGroupChildren({ groupId: selectedRow })); + } + return rowsToSelect; +}; + +export const findRowsToDeselect = ( + apiRef: React.MutableRefObject, + tree: GridRowTreeConfig, + deselectedRow: GridRowId, +) => { + let rowsToDeselect: GridRowId[] = []; + + const allParents = getRowNodeParents(tree, deselectedRow); + allParents.forEach((parent) => { + const isSelected = apiRef.current.isRowSelected(parent); + if (isSelected) { + rowsToDeselect.push(parent); + } + }); + + const rowNode = apiRef.current.getRowNode(deselectedRow); + if (rowNode?.type === 'group') { + rowsToDeselect = [ + ...rowsToDeselect, + ...apiRef.current.getRowGroupChildren({ groupId: deselectedRow }), + ]; + } + return rowsToDeselect; +}; diff --git a/packages/x-data-grid/src/models/props/DataGridProps.ts b/packages/x-data-grid/src/models/props/DataGridProps.ts index 19fc5d2cf42a..e2681e0e0f7d 100644 --- a/packages/x-data-grid/src/models/props/DataGridProps.ts +++ b/packages/x-data-grid/src/models/props/DataGridProps.ts @@ -807,6 +807,16 @@ export interface DataGridProSharedPropsWithDefaultValue { * @default false */ headerFilters: boolean; + /** + * If `true`, following behavior happens with nested data: + * 1. Selecting/Deselecting a parent row selects/deselects all the nested rows. + * 2. When all nested rows are selected, the parent row is also selected. + * 3. When a nested row is deselected, the parent row is also deselected, if already selected. + * 4. Select All checkbox selects/deselects all the rows including nested rows. + * Works with tree data and row grouping on the client-side only. + * @default false + */ + propagateRowSelection: boolean; } export interface DataGridProSharedPropsWithoutDefaultValue { From 7fd93e76b3c3621fae6a9469df9aada0700528fd Mon Sep 17 00:00:00 2001 From: Bilal Shafi Date: Thu, 11 Jul 2024 14:27:52 +0500 Subject: [PATCH 02/45] Fix autogenerated parents not being affected by nested selection --- .../src/hooks/features/rowSelection/utils.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/packages/x-data-grid/src/hooks/features/rowSelection/utils.ts b/packages/x-data-grid/src/hooks/features/rowSelection/utils.ts index 55e47f6b29ab..3789fcb70840 100644 --- a/packages/x-data-grid/src/hooks/features/rowSelection/utils.ts +++ b/packages/x-data-grid/src/hooks/features/rowSelection/utils.ts @@ -28,7 +28,6 @@ const getRowNodeParents = (tree: GridRowTreeConfig, id: GridRowId) => { return parents; } parents.push(parent); - parent = node.parent; } return parents; @@ -80,7 +79,9 @@ export const findRowsToSelect = ( const rowNode = apiRef.current.getRowNode(selectedRow); if (rowNode?.type === 'group') { - rowsToSelect.push(...apiRef.current.getRowGroupChildren({ groupId: selectedRow })); + rowsToSelect.push( + ...apiRef.current.getRowGroupChildren({ groupId: selectedRow, skipAutoGeneratedRows: false }), + ); } return rowsToSelect; }; @@ -104,7 +105,10 @@ export const findRowsToDeselect = ( if (rowNode?.type === 'group') { rowsToDeselect = [ ...rowsToDeselect, - ...apiRef.current.getRowGroupChildren({ groupId: deselectedRow }), + ...apiRef.current.getRowGroupChildren({ + groupId: deselectedRow, + skipAutoGeneratedRows: false, + }), ]; } return rowsToDeselect; From 124ddbcb8c4f9696dbcdee69773ddca1b45d51a0 Mon Sep 17 00:00:00 2001 From: Bilal Shafi Date: Thu, 11 Jul 2024 14:32:39 +0500 Subject: [PATCH 03/45] Update docs/data/data-grid/row-grouping/row-grouping.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: José Rodolfo Freitas Signed-off-by: Bilal Shafi --- docs/data/data-grid/row-grouping/row-grouping.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/data/data-grid/row-grouping/row-grouping.md b/docs/data/data-grid/row-grouping/row-grouping.md index f7671a79abc8..fdfb8543c544 100644 --- a/docs/data/data-grid/row-grouping/row-grouping.md +++ b/docs/data/data-grid/row-grouping/row-grouping.md @@ -305,7 +305,7 @@ In the example below: If you are dynamically switching the `leafField` or `mainGroupingCriteria`, the sorting and filtering models will not be cleaned up automatically, and the sorting/filtering will not be re-applied. ::: -## Propagate row selection +## Automatic children selection | Selection propagation By default, selecting a parent row will not select its children. Set the `propagateRowSelection` prop to `true` to achieve the following behavior. From 3e9ad6e501a031413c6493ce7dfd42416e8cbfa9 Mon Sep 17 00:00:00 2001 From: Bilal Shafi Date: Thu, 11 Jul 2024 16:15:54 +0500 Subject: [PATCH 04/45] Fix the id 0 not being picked properly for selection --- .../components/columnSelection/GridCellCheckboxRenderer.tsx | 4 ++-- .../src/hooks/features/rowSelection/useGridRowSelection.ts | 6 ------ 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/packages/x-data-grid/src/components/columnSelection/GridCellCheckboxRenderer.tsx b/packages/x-data-grid/src/components/columnSelection/GridCellCheckboxRenderer.tsx index 3e25fdec48df..3e48dee10592 100644 --- a/packages/x-data-grid/src/components/columnSelection/GridCellCheckboxRenderer.tsx +++ b/packages/x-data-grid/src/components/columnSelection/GridCellCheckboxRenderer.tsx @@ -58,14 +58,14 @@ const GridCellCheckboxForwardRef = React.forwardRef { if (rowNode.type === 'group') { - return children.some((child) => rowSelectionLookup[child]); + return children.some((child) => rowSelectionLookup[child] !== undefined); } return false; }, [children, rowNode.type, rowSelectionLookup]); diff --git a/packages/x-data-grid/src/hooks/features/rowSelection/useGridRowSelection.ts b/packages/x-data-grid/src/hooks/features/rowSelection/useGridRowSelection.ts index 93a2bbebb94c..d4a3daafa074 100644 --- a/packages/x-data-grid/src/hooks/features/rowSelection/useGridRowSelection.ts +++ b/packages/x-data-grid/src/hooks/features/rowSelection/useGridRowSelection.ts @@ -336,9 +336,6 @@ export const useGridRowSelection = ( props.signature === GridSignature.DataGrid ? 'private' : 'public', ); - /** - * EVENTS - */ const removeOutdatedSelection = React.useCallback(() => { if (props.keepNonExistentRowsSelected) { return; @@ -572,9 +569,6 @@ export const useGridRowSelection = ( ); useGridApiEventHandler(apiRef, 'cellKeyDown', runIfRowSelectionIsEnabled(handleCellKeyDown)); - /** - * EFFECTS - */ React.useEffect(() => { if (propRowSelectionModel !== undefined) { apiRef.current.setRowSelectionModel(propRowSelectionModel); From c581f04f625f324e0973bd750d2a2e73a60bac3b Mon Sep 17 00:00:00 2001 From: Bilal Shafi Date: Thu, 11 Jul 2024 16:42:40 +0500 Subject: [PATCH 05/45] Update demo to use multi-level --- .../data-grid/row-grouping/RowGroupingPropagateSelection.js | 2 +- .../data-grid/row-grouping/RowGroupingPropagateSelection.tsx | 2 +- docs/data/data-grid/row-grouping/row-grouping.md | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/data/data-grid/row-grouping/RowGroupingPropagateSelection.js b/docs/data/data-grid/row-grouping/RowGroupingPropagateSelection.js index 5a408ba6405b..30d590c9d4c5 100644 --- a/docs/data/data-grid/row-grouping/RowGroupingPropagateSelection.js +++ b/docs/data/data-grid/row-grouping/RowGroupingPropagateSelection.js @@ -14,7 +14,7 @@ export default function RowGroupingPropagateSelection() { apiRef, initialState: { rowGrouping: { - model: ['company'], + model: ['company', 'director'], }, }, }); diff --git a/docs/data/data-grid/row-grouping/RowGroupingPropagateSelection.tsx b/docs/data/data-grid/row-grouping/RowGroupingPropagateSelection.tsx index 5a408ba6405b..30d590c9d4c5 100644 --- a/docs/data/data-grid/row-grouping/RowGroupingPropagateSelection.tsx +++ b/docs/data/data-grid/row-grouping/RowGroupingPropagateSelection.tsx @@ -14,7 +14,7 @@ export default function RowGroupingPropagateSelection() { apiRef, initialState: { rowGrouping: { - model: ['company'], + model: ['company', 'director'], }, }, }); diff --git a/docs/data/data-grid/row-grouping/row-grouping.md b/docs/data/data-grid/row-grouping/row-grouping.md index fdfb8543c544..38e568e6f64e 100644 --- a/docs/data/data-grid/row-grouping/row-grouping.md +++ b/docs/data/data-grid/row-grouping/row-grouping.md @@ -310,7 +310,7 @@ If you are dynamically switching the `leafField` or `mainGroupingCriteria`, the By default, selecting a parent row will not select its children. Set the `propagateRowSelection` prop to `true` to achieve the following behavior. -1. Selecting/Deselecting a parent row would select/deselect all the children rows. +1. Selecting/deselecting a parent row would select/deselect all the children rows. 2. When all child rows are selected, the parent row will be auto selected. 3. When a child row is deselected, if one or more parent rows are already selected, they will be auto deselected. 4. Select All checkbox would select/deselect all the rows including child rows. From 4af394b8ff731623d3fa58f38a6df0e2ef9bc878 Mon Sep 17 00:00:00 2001 From: Bilal Shafi Date: Tue, 16 Jul 2024 07:54:35 +0500 Subject: [PATCH 06/45] Reset selection on filtering --- .../data-grid/row-grouping/row-grouping.md | 12 +++-- docs/data/data-grid/tree-data/tree-data.md | 2 +- .../data-grid-premium/data-grid-premium.json | 4 +- .../data-grid-pro/data-grid-pro.json | 4 +- .../src/DataGridPremium/DataGridPremium.tsx | 10 ++-- .../src/DataGridPro/DataGridPro.tsx | 10 ++-- .../GridCellCheckboxRenderer.tsx | 14 ++++-- .../rowSelection/useGridRowSelection.ts | 49 +++++++++++++++++-- .../src/hooks/features/rowSelection/utils.ts | 16 ++++-- .../src/models/props/DataGridProps.ts | 10 ++-- 10 files changed, 96 insertions(+), 35 deletions(-) diff --git a/docs/data/data-grid/row-grouping/row-grouping.md b/docs/data/data-grid/row-grouping/row-grouping.md index 38e568e6f64e..b0c0954e884f 100644 --- a/docs/data/data-grid/row-grouping/row-grouping.md +++ b/docs/data/data-grid/row-grouping/row-grouping.md @@ -305,18 +305,22 @@ In the example below: If you are dynamically switching the `leafField` or `mainGroupingCriteria`, the sorting and filtering models will not be cleaned up automatically, and the sorting/filtering will not be re-applied. ::: -## Automatic children selection | Selection propagation +## Automatic parents and children selection By default, selecting a parent row will not select its children. Set the `propagateRowSelection` prop to `true` to achieve the following behavior. 1. Selecting/deselecting a parent row would select/deselect all the children rows. -2. When all child rows are selected, the parent row will be auto selected. -3. When a child row is deselected, if one or more parent rows are already selected, they will be auto deselected. -4. Select All checkbox would select/deselect all the rows including child rows. +2. When all the child rows are selected, the parent row will be auto selected. +3. When a child row is deselected, if one or more parent rows are already selected, they will be moved to an indeterminate state. +4. Select All checkbox in the header row would select/deselect all the rows including child rows. {{"demo": "RowGroupingPropagateSelection.js", "bg": "inline", "defaultCodeOpen": false}} +:::info +If the `propagateRowSelection` is enabled, only the filtered rows will be kept selected. If some rows were selected before filtering, they will be auto deselected if they are not among the newly filtered rows. +::: + :::warning The `propagateRowSelection` is a client-side feature and not recommended to be used with the [server-side data source](/x/react-data-grid/server-side-data/), since it will only work on the partially loaded data. ::: diff --git a/docs/data/data-grid/tree-data/tree-data.md b/docs/data/data-grid/tree-data/tree-data.md index 8bc4d2b97065..31ad8d277e19 100644 --- a/docs/data/data-grid/tree-data/tree-data.md +++ b/docs/data/data-grid/tree-data/tree-data.md @@ -87,7 +87,7 @@ If you want to access the grouping column field, for instance, to use it with co Same behavior as for the [Row grouping](/x/react-data-grid/row-grouping/#group-expansion). -## Propagate row selection +## Automatic parents and children selection Same behavior as for the [Row grouping](/x/react-data-grid/row-grouping/#propagate-row-selection). diff --git a/docs/translations/api-docs/data-grid/data-grid-premium/data-grid-premium.json b/docs/translations/api-docs/data-grid/data-grid-premium/data-grid-premium.json index 34c6faa2a402..87bb4fb2be6b 100644 --- a/docs/translations/api-docs/data-grid/data-grid-premium/data-grid-premium.json +++ b/docs/translations/api-docs/data-grid/data-grid-premium/data-grid-premium.json @@ -184,7 +184,7 @@ "groupingColDef": { "description": "The grouping column used by the tree data." }, "headerFilterHeight": { "description": "Override the height of the header filters." }, "headerFilters": { - "description": "If true, enables the data grid filtering on header feature." + "description": "If true, the filtering on the header feature is enabled." }, "hideFooter": { "description": "If true, the footer component is hidden." }, "hideFooterPagination": { @@ -587,7 +587,7 @@ } }, "propagateRowSelection": { - "description": "If true, following behavior happens with nested data: 1. Selecting/Deselecting a parent row selects/deselects all the nested rows. 2. When all nested rows are selected, the parent row is also selected. 3. When a nested row is deselected, the parent row is also deselected, if already selected. 4. Select All checkbox selects/deselects all the rows including nested rows. Works with tree data and row grouping on the client-side only." + "description": "If true, following behavior happens with nested data: 1. Selecting/deselecting a parent row would select/deselect all the children rows. 2. When all the child rows are selected, the parent row will be auto selected. 3. When a child row is deselected, if one or more parent rows are already selected, they will be moved to an indeterminate state. 4. Select All checkbox in the header row would select/deselect all the rows including child rows. Works with tree data and row grouping on the client-side only." }, "resizeThrottleMs": { "description": "The milliseconds throttle delay for resizing the grid." }, "rowBufferPx": { "description": "Row region in pixels to render before/after the viewport" }, diff --git a/docs/translations/api-docs/data-grid/data-grid-pro/data-grid-pro.json b/docs/translations/api-docs/data-grid/data-grid-pro/data-grid-pro.json index 5750236894c6..51cd1a55a35a 100644 --- a/docs/translations/api-docs/data-grid/data-grid-pro/data-grid-pro.json +++ b/docs/translations/api-docs/data-grid/data-grid-pro/data-grid-pro.json @@ -165,7 +165,7 @@ "groupingColDef": { "description": "The grouping column used by the tree data." }, "headerFilterHeight": { "description": "Override the height of the header filters." }, "headerFilters": { - "description": "If true, enables the data grid filtering on header feature." + "description": "If true, the filtering on the header feature is enabled." }, "hideFooter": { "description": "If true, the footer component is hidden." }, "hideFooterPagination": { @@ -533,7 +533,7 @@ } }, "propagateRowSelection": { - "description": "If true, following behavior happens with nested data: 1. Selecting/Deselecting a parent row selects/deselects all the nested rows. 2. When all nested rows are selected, the parent row is also selected. 3. When a nested row is deselected, the parent row is also deselected, if already selected. 4. Select All checkbox selects/deselects all the rows including nested rows. Works with tree data and row grouping on the client-side only." + "description": "If true, following behavior happens with nested data: 1. Selecting/deselecting a parent row would select/deselect all the children rows. 2. When all the child rows are selected, the parent row will be auto selected. 3. When a child row is deselected, if one or more parent rows are already selected, they will be moved to an indeterminate state. 4. Select All checkbox in the header row would select/deselect all the rows including child rows. Works with tree data and row grouping on the client-side only." }, "resizeThrottleMs": { "description": "The milliseconds throttle delay for resizing the grid." }, "rowBufferPx": { "description": "Row region in pixels to render before/after the viewport" }, diff --git a/packages/x-data-grid-premium/src/DataGridPremium/DataGridPremium.tsx b/packages/x-data-grid-premium/src/DataGridPremium/DataGridPremium.tsx index 7b417b9756d7..0844dc93a227 100644 --- a/packages/x-data-grid-premium/src/DataGridPremium/DataGridPremium.tsx +++ b/packages/x-data-grid-premium/src/DataGridPremium/DataGridPremium.tsx @@ -421,7 +421,7 @@ DataGridPremiumRaw.propTypes = { */ headerFilterHeight: PropTypes.number, /** - * If `true`, enables the data grid filtering on header feature. + * If `true`, the filtering on the header feature is enabled. * @default false */ headerFilters: PropTypes.bool, @@ -896,10 +896,10 @@ DataGridPremiumRaw.propTypes = { processRowUpdate: PropTypes.func, /** * If `true`, following behavior happens with nested data: - * 1. Selecting/Deselecting a parent row selects/deselects all the nested rows. - * 2. When all nested rows are selected, the parent row is also selected. - * 3. When a nested row is deselected, the parent row is also deselected, if already selected. - * 4. Select All checkbox selects/deselects all the rows including nested rows. + * 1. Selecting/deselecting a parent row would select/deselect all the children rows. + * 2. When all the child rows are selected, the parent row will be auto selected. + * 3. When a child row is deselected, if one or more parent rows are already selected, they will be moved to an indeterminate state. + * 4. Select All checkbox in the header row would select/deselect all the rows including child rows. * Works with tree data and row grouping on the client-side only. * @default false */ diff --git a/packages/x-data-grid-pro/src/DataGridPro/DataGridPro.tsx b/packages/x-data-grid-pro/src/DataGridPro/DataGridPro.tsx index 09fd483d3b67..fc31bb75cfdf 100644 --- a/packages/x-data-grid-pro/src/DataGridPro/DataGridPro.tsx +++ b/packages/x-data-grid-pro/src/DataGridPro/DataGridPro.tsx @@ -376,7 +376,7 @@ DataGridProRaw.propTypes = { */ headerFilterHeight: PropTypes.number, /** - * If `true`, enables the data grid filtering on header feature. + * If `true`, the filtering on the header feature is enabled. * @default false */ headerFilters: PropTypes.bool, @@ -812,10 +812,10 @@ DataGridProRaw.propTypes = { processRowUpdate: PropTypes.func, /** * If `true`, following behavior happens with nested data: - * 1. Selecting/Deselecting a parent row selects/deselects all the nested rows. - * 2. When all nested rows are selected, the parent row is also selected. - * 3. When a nested row is deselected, the parent row is also deselected, if already selected. - * 4. Select All checkbox selects/deselects all the rows including nested rows. + * 1. Selecting/deselecting a parent row would select/deselect all the children rows. + * 2. When all the child rows are selected, the parent row will be auto selected. + * 3. When a child row is deselected, if one or more parent rows are already selected, they will be moved to an indeterminate state. + * 4. Select All checkbox in the header row would select/deselect all the rows including child rows. * Works with tree data and row grouping on the client-side only. * @default false */ diff --git a/packages/x-data-grid/src/components/columnSelection/GridCellCheckboxRenderer.tsx b/packages/x-data-grid/src/components/columnSelection/GridCellCheckboxRenderer.tsx index 3e48dee10592..6bc1a699ca44 100644 --- a/packages/x-data-grid/src/components/columnSelection/GridCellCheckboxRenderer.tsx +++ b/packages/x-data-grid/src/components/columnSelection/GridCellCheckboxRenderer.tsx @@ -13,6 +13,7 @@ import type { GridRowSelectionCheckboxParams } from '../../models/params/gridRow import { selectedIdsLookupSelector } from '../../hooks/features/rowSelection/gridRowSelectionSelector'; import { useGridSelector } from '../../hooks/utils/useGridSelector'; import { GridRowId } from '../../models'; +import { gridFilteredRowsLookupSelector } from '../../hooks/features/filter/gridFilterSelector'; type OwnerState = { classes: DataGridProcessedProps['classes'] }; @@ -54,21 +55,26 @@ const GridCellCheckboxForwardRef = React.forwardRef(null); const rowSelectionLookup = useGridSelector(apiRef, selectedIdsLookupSelector); + const filteredRowsLookup = useGridSelector(apiRef, gridFilteredRowsLookupSelector); const children: GridRowId[] = React.useMemo(() => { if (rowNode.type === 'group') { - // TODO check if a selector could be used here // @ts-expect-error Access `GridProApi` method - return apiRef.current.getRowGroupChildren({ groupId: id, skipAutoGeneratedRows: false }); + return apiRef.current.getRowGroupChildren({ + groupId: id, + skipAutoGeneratedRows: false, + }); } return []; }, [id, rowNode.type, apiRef]); const someChildrenSelected = React.useMemo(() => { if (rowNode.type === 'group') { - return children.some((child) => rowSelectionLookup[child] !== undefined); + return children.some( + (child) => filteredRowsLookup[child] && rowSelectionLookup[child] !== undefined, + ); } return false; - }, [children, rowNode.type, rowSelectionLookup]); + }, [children, rowNode.type, rowSelectionLookup, filteredRowsLookup]); const rippleRef = React.useRef(null); const handleRef = useForkRef(checkboxElement, ref); diff --git a/packages/x-data-grid/src/hooks/features/rowSelection/useGridRowSelection.ts b/packages/x-data-grid/src/hooks/features/rowSelection/useGridRowSelection.ts index d4a3daafa074..8f7378e3d4b6 100644 --- a/packages/x-data-grid/src/hooks/features/rowSelection/useGridRowSelection.ts +++ b/packages/x-data-grid/src/hooks/features/rowSelection/useGridRowSelection.ts @@ -6,7 +6,7 @@ import { GridRowSelectionApi, GridRowMultiSelectionApi, } from '../../../models/api/gridRowSelectionApi'; -import { GridRowId } from '../../../models/gridRows'; +import { GridGroupNode, GridRowId } from '../../../models/gridRows'; import { GridSignature, useGridApiEventHandler } from '../../utils/useGridApiEventHandler'; import { useGridApiMethod } from '../../utils/useGridApiMethod'; import { useGridLogger } from '../../utils/useGridLogger'; @@ -214,7 +214,18 @@ export const useGridRowSelection = ( if (resetSelection) { logger.debug(`Setting selection for row ${id}`); - apiRef.current.setRowSelectionModel(isSelected ? [id] : []); + const newSelection: GridRowId[] = []; + if (isSelected) { + newSelection.push(id); + if (props.propagateRowSelection) { + const rowsToSelect = findRowsToSelect(apiRef, tree, id); + rowsToSelect.forEach((rowId) => { + newSelection.push(rowId); + }); + } + } + + apiRef.current.setRowSelectionModel(newSelection); } else { logger.debug(`Toggling selection for row ${id}`); @@ -249,7 +260,19 @@ export const useGridRowSelection = ( let newSelection: GridRowId[]; if (resetSelection) { - newSelection = isSelected ? selectableIds : []; + if (isSelected) { + newSelection = selectableIds; + if (props.propagateRowSelection) { + selectableIds.forEach((id) => { + const rowsToSelect = findRowsToSelect(apiRef, tree, id); + rowsToSelect.forEach((rowId) => { + newSelection.push(rowId); + }); + }); + } + } else { + newSelection = []; + } } else { // We clone the existing object to avoid mutating the same object returned by the selector to others part of the project const selectionLookup = { @@ -546,6 +569,21 @@ export const useGridRowSelection = ( [apiRef, handleSingleRowSelection, selectRows, visibleRows.rows, canHaveMultipleSelection], ); + const handleFilteredRowsSet = React.useCallback(() => { + if (!props.propagateRowSelection) { + return; + } + const filteredRows = gridExpandedSortedRowIdsSelector(apiRef); + const selectedRows = selectedGridRowsSelector(apiRef); + const newSelectedRows: GridRowId[] = []; + selectedRows.forEach((row) => { + if (filteredRows.includes(row?.id) && !(tree[row.id] as GridGroupNode)?.isAutoGenerated) { + newSelectedRows.push(row.id); + } + }); + apiRef.current.selectRows(newSelectedRows, true, true); + }, [apiRef, props.propagateRowSelection, tree]); + useGridApiEventHandler( apiRef, 'sortedRowsSet', @@ -568,6 +606,11 @@ export const useGridRowSelection = ( runIfRowSelectionIsEnabled(preventSelectionOnShift), ); useGridApiEventHandler(apiRef, 'cellKeyDown', runIfRowSelectionIsEnabled(handleCellKeyDown)); + useGridApiEventHandler( + apiRef, + 'filteredRowsSet', + runIfRowSelectionIsEnabled(handleFilteredRowsSet), + ); React.useEffect(() => { if (propRowSelectionModel !== undefined) { diff --git a/packages/x-data-grid/src/hooks/features/rowSelection/utils.ts b/packages/x-data-grid/src/hooks/features/rowSelection/utils.ts index 3789fcb70840..e9300868b18e 100644 --- a/packages/x-data-grid/src/hooks/features/rowSelection/utils.ts +++ b/packages/x-data-grid/src/hooks/features/rowSelection/utils.ts @@ -3,6 +3,7 @@ import { GridSignature } from '../../utils/useGridApiEventHandler'; import { GRID_ROOT_GROUP_ID } from '../rows/gridRowsUtils'; import type { GridGroupNode, GridRowId, GridRowTreeConfig } from '../../../models/gridRows'; import type { GridPrivateApiCommunity } from '../../../models/api/gridApiCommunity'; +import { gridFilteredRowsLookupSelector } from '../filter/gridFilterSelector'; export function isMultipleRowSelectionEnabled( props: Pick< @@ -33,9 +34,10 @@ const getRowNodeParents = (tree: GridRowTreeConfig, id: GridRowId) => { return parents; }; -const getRowNodeSiblings = ( +const getFilteredRowNodeSiblings = ( apiRef: React.MutableRefObject, tree: GridRowTreeConfig, + filteredRows: Record, id: GridRowId, ) => { const node = apiRef.current.getRowNode(id); @@ -50,7 +52,7 @@ const getRowNodeSiblings = ( const parentNode = tree[parent] as GridGroupNode; - return [...parentNode.children].filter((childId) => childId !== id); + return [...parentNode.children].filter((childId) => childId !== id && filteredRows[childId]); }; export const findRowsToSelect = ( @@ -58,10 +60,11 @@ export const findRowsToSelect = ( tree: GridRowTreeConfig, selectedRow: GridRowId, ) => { + const filteredRows = gridFilteredRowsLookupSelector(apiRef); const rowsToSelect: GridRowId[] = []; const traverseParents = (rowId: GridRowId) => { - const siblings: GridRowId[] = getRowNodeSiblings(apiRef, tree, rowId); + const siblings: GridRowId[] = getFilteredRowNodeSiblings(apiRef, tree, filteredRows, rowId); if ( siblings.length === 0 || siblings.every((sibling) => apiRef.current.isRowSelected(sibling)) @@ -80,7 +83,11 @@ export const findRowsToSelect = ( if (rowNode?.type === 'group') { rowsToSelect.push( - ...apiRef.current.getRowGroupChildren({ groupId: selectedRow, skipAutoGeneratedRows: false }), + ...apiRef.current.getRowGroupChildren({ + groupId: selectedRow, + skipAutoGeneratedRows: false, + applyFiltering: true, + }), ); } return rowsToSelect; @@ -108,6 +115,7 @@ export const findRowsToDeselect = ( ...apiRef.current.getRowGroupChildren({ groupId: deselectedRow, skipAutoGeneratedRows: false, + applyFiltering: true, }), ]; } diff --git a/packages/x-data-grid/src/models/props/DataGridProps.ts b/packages/x-data-grid/src/models/props/DataGridProps.ts index 0eeee7293a26..1e1644cfbb3b 100644 --- a/packages/x-data-grid/src/models/props/DataGridProps.ts +++ b/packages/x-data-grid/src/models/props/DataGridProps.ts @@ -804,16 +804,16 @@ export interface DataGridPropsWithoutDefaultValue Date: Tue, 16 Jul 2024 12:26:16 +0500 Subject: [PATCH 07/45] Add tests --- .../rowSelection.DataGridPremium.test.tsx | 178 +++++++++++++++++ .../tests/rowSelection.DataGridPro.test.tsx | 189 +++++++++++++++++- 2 files changed, 366 insertions(+), 1 deletion(-) create mode 100644 packages/x-data-grid-premium/src/tests/rowSelection.DataGridPremium.test.tsx diff --git a/packages/x-data-grid-premium/src/tests/rowSelection.DataGridPremium.test.tsx b/packages/x-data-grid-premium/src/tests/rowSelection.DataGridPremium.test.tsx new file mode 100644 index 000000000000..7133ee943288 --- /dev/null +++ b/packages/x-data-grid-premium/src/tests/rowSelection.DataGridPremium.test.tsx @@ -0,0 +1,178 @@ +import * as React from 'react'; +import { createRenderer, fireEvent, act } from '@mui/internal-test-utils'; +import { getCell } from 'test/utils/helperFn'; +import { expect } from 'chai'; +import { + DataGridPremium, + DataGridPremiumProps, + GridApi, + GridRowsProp, + useGridApiRef, +} from '@mui/x-data-grid-premium'; + +const isJSDOM = /jsdom/.test(window.navigator.userAgent); + +interface BaselineProps extends DataGridPremiumProps { + rows: GridRowsProp; +} + +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 baselineProps: BaselineProps = { + autoHeight: isJSDOM, + disableVirtualization: true, + rows, + columns: [ + { + field: 'id', + type: 'number', + }, + { + field: 'category1', + }, + { + field: 'category2', + }, + ], +}; + +describe(' - Row selection', () => { + const { render } = createRenderer(); + + let apiRef: React.MutableRefObject; + + function Test(props: Partial) { + apiRef = useGridApiRef(); + + return ( +
+ +
+ ); + } + + describe('prop: propagateRowSelection', () => { + it('should select all the children when selecting a parent', () => { + render( + , + ); + + fireEvent.click(getCell(1, 0).querySelector('input')!); + expect(apiRef.current.getSelectedRows()).to.have.keys([ + 'auto-generated-row-category1/Cat B', + 3, + 4, + ]); + }); + + it('should deselect all the children when deselecting a parent', () => { + render( + , + ); + + fireEvent.click(getCell(1, 0).querySelector('input')!); + expect(apiRef.current.getSelectedRows()).to.have.keys([ + 'auto-generated-row-category1/Cat B', + 3, + 4, + ]); + fireEvent.click(getCell(1, 0).querySelector('input')!); + expect(apiRef.current.getSelectedRows().size).to.equal(0); + }); + + it('should put the parent into indeterminate if some but not all the children are selected', () => { + render( + , + ); + + fireEvent.click(getCell(1, 0).querySelector('input')!); + expect(getCell(0, 0).querySelector('input')!).to.have.attr('data-indeterminate', 'true'); + }); + + it('should auto select the parent if all the children are selected', () => { + render( + , + ); + + fireEvent.click(getCell(1, 0).querySelector('input')!); + fireEvent.click(getCell(2, 0).querySelector('input')!); + fireEvent.click(getCell(3, 0).querySelector('input')!); + expect(apiRef.current.getSelectedRows()).to.have.keys([ + 0, + 1, + 2, + 'auto-generated-row-category1/Cat A', + ]); + }); + + it('should deselect auto selected parent if one of the children is deselected', () => { + render( + , + ); + + fireEvent.click(getCell(1, 0).querySelector('input')!); + fireEvent.click(getCell(2, 0).querySelector('input')!); + fireEvent.click(getCell(3, 0).querySelector('input')!); + expect(apiRef.current.getSelectedRows()).to.have.keys([ + 0, + 1, + 2, + 'auto-generated-row-category1/Cat A', + ]); + fireEvent.click(getCell(2, 0).querySelector('input')!); + expect(apiRef.current.getSelectedRows()).to.have.keys([0, 2]); + }); + + it('should deselect unfiltered rows after filtering', () => { + render( + , + ); + + fireEvent.click(getCell(1, 0).querySelector('input')!); + fireEvent.click(getCell(2, 0).querySelector('input')!); + fireEvent.click(getCell(3, 0).querySelector('input')!); + fireEvent.click(getCell(5, 0).querySelector('input')!); + + expect(apiRef.current.getSelectedRows()).to.have.keys([ + 0, + 1, + 2, + 'auto-generated-row-category1/Cat A', + 3, + ]); + act(() => { + apiRef.current.setFilterModel({ + items: [], + quickFilterValues: ['Cat B'], + }); + }); + expect(apiRef.current.getSelectedRows()).to.have.keys([3]); + }); + }); +}); diff --git a/packages/x-data-grid-pro/src/tests/rowSelection.DataGridPro.test.tsx b/packages/x-data-grid-pro/src/tests/rowSelection.DataGridPro.test.tsx index 3c5eda242358..a510497d70c5 100644 --- a/packages/x-data-grid-pro/src/tests/rowSelection.DataGridPro.test.tsx +++ b/packages/x-data-grid-pro/src/tests/rowSelection.DataGridPro.test.tsx @@ -2,13 +2,15 @@ import * as React from 'react'; import { expect } from 'chai'; import { spy } from 'sinon'; import { getCell, getColumnValues, getRows } from 'test/utils/helperFn'; -import { createRenderer, fireEvent, screen, act } from '@mui/internal-test-utils'; +import { createRenderer, fireEvent, screen, act, within } from '@mui/internal-test-utils'; import { GridApi, useGridApiRef, DataGridPro, DataGridProProps, GridRowSelectionModel, + GridRowsProp, + GridColDef, } from '@mui/x-data-grid-pro'; import { getBasicGridData } from '@mui/x-data-grid-generator'; @@ -212,6 +214,191 @@ describe(' - Row selection', () => { }); }); + describe('prop: propagateRowSelection', () => { + 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: GridColDef[] = [ + { field: 'jobTitle', headerName: 'Job Title', width: 200 }, + { + field: 'recruitmentDate', + headerName: 'Recruitment Date', + type: 'date', + width: 150, + }, + ]; + + const getTreeDataPath: DataGridProProps['getTreeDataPath'] = (row) => row.hierarchy; + + function TreeDataGrid(props: Partial) { + apiRef = useGridApiRef(); + return ( +
+ +
+ ); + } + it('should select all the children when selecting a parent', () => { + render(); + + fireEvent.click(getCell(1, 0).querySelector('input')!); + expect(apiRef.current.getSelectedRows()).to.have.keys([1, 2, 3, 4, 5, 6, 7]); + }); + + it('should deselect all the children when deselecting a parent', () => { + render(); + + fireEvent.click(getCell(1, 0).querySelector('input')!); + expect(apiRef.current.getSelectedRows()).to.have.keys([1, 2, 3, 4, 5, 6, 7]); + fireEvent.click(getCell(1, 0).querySelector('input')!); + expect(apiRef.current.getSelectedRows().size).to.equal(0); + }); + + it('should put the parent into indeterminate if some but not all the children are selected', () => { + render(); + + fireEvent.click(getCell(11, 0).querySelector('input')!); + expect(getCell(8, 0).querySelector('input')!).to.have.attr('data-indeterminate', 'true'); + }); + + it('should auto select the parent if all the children are selected', () => { + render(); + + fireEvent.click(getCell(9, 0).querySelector('input')!); + fireEvent.click(getCell(11, 0).querySelector('input')!); + fireEvent.click(getCell(12, 0).querySelector('input')!); + + // The parent row (Mary, id: 8) should be among the selected rows + expect(apiRef.current.getSelectedRows()).to.have.keys([9, 10, 11, 12, 8, 13, 14]); + }); + + it('should deselect auto selected parent if one of the children is deselected', () => { + render(); + + fireEvent.click(getCell(9, 0).querySelector('input')!); + fireEvent.click(getCell(11, 0).querySelector('input')!); + fireEvent.click(getCell(12, 0).querySelector('input')!); + expect(apiRef.current.getSelectedRows()).to.have.keys([9, 10, 11, 12, 8, 13, 14]); + fireEvent.click(getCell(9, 0).querySelector('input')!); + expect(apiRef.current.getSelectedRows()).to.have.keys([11, 12, 13, 14]); + }); + + it('should deselect unfiltered rows after filtering', () => { + render(); + + fireEvent.click(getCell(9, 0).querySelector('input')!); + fireEvent.click(getCell(11, 0).querySelector('input')!); + fireEvent.click(getCell(12, 0).querySelector('input')!); + expect(apiRef.current.getSelectedRows()).to.have.keys([9, 10, 11, 12, 8, 13, 14]); + act(() => { + apiRef.current.setFilterModel({ + items: [], + quickFilterValues: ['Linda'], + }); + }); + expect(apiRef.current.getSelectedRows()).to.have.keys([12, 8]); + }); + }); + describe('apiRef: getSelectedRows', () => { it('should handle the event internally before triggering onRowSelectionModelChange', () => { render( From 361658bb0f2948af482941d8e7b700e4929df2ad Mon Sep 17 00:00:00 2001 From: Bilal Shafi Date: Tue, 16 Jul 2024 12:42:13 +0500 Subject: [PATCH 08/45] Lint --- .../src/tests/rowSelection.DataGridPro.test.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/x-data-grid-pro/src/tests/rowSelection.DataGridPro.test.tsx b/packages/x-data-grid-pro/src/tests/rowSelection.DataGridPro.test.tsx index a510497d70c5..ea53c7be6910 100644 --- a/packages/x-data-grid-pro/src/tests/rowSelection.DataGridPro.test.tsx +++ b/packages/x-data-grid-pro/src/tests/rowSelection.DataGridPro.test.tsx @@ -2,7 +2,7 @@ import * as React from 'react'; import { expect } from 'chai'; import { spy } from 'sinon'; import { getCell, getColumnValues, getRows } from 'test/utils/helperFn'; -import { createRenderer, fireEvent, screen, act, within } from '@mui/internal-test-utils'; +import { createRenderer, fireEvent, screen, act } from '@mui/internal-test-utils'; import { GridApi, useGridApiRef, @@ -337,6 +337,7 @@ describe(' - Row selection', () => { ); } + it('should select all the children when selecting a parent', () => { render(); From f3124b0514cd8c3549e8c2b81566c527524cbbfe Mon Sep 17 00:00:00 2001 From: Bilal Shafi Date: Tue, 16 Jul 2024 12:50:45 +0500 Subject: [PATCH 09/45] Prettier --- .../x-data-grid-pro/src/tests/rowSelection.DataGridPro.test.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/x-data-grid-pro/src/tests/rowSelection.DataGridPro.test.tsx b/packages/x-data-grid-pro/src/tests/rowSelection.DataGridPro.test.tsx index ea53c7be6910..50566d91c0b9 100644 --- a/packages/x-data-grid-pro/src/tests/rowSelection.DataGridPro.test.tsx +++ b/packages/x-data-grid-pro/src/tests/rowSelection.DataGridPro.test.tsx @@ -337,7 +337,7 @@ describe(' - Row selection', () => { ); } - + it('should select all the children when selecting a parent', () => { render(); From 7e0f47523ba5e477d0362268ff08245cb6ba0509 Mon Sep 17 00:00:00 2001 From: Bilal Shafi Date: Tue, 16 Jul 2024 14:05:22 +0500 Subject: [PATCH 10/45] Tweak the performance a bit --- .../features/rowSelection/useGridRowSelection.ts | 15 +++++++++------ .../src/hooks/features/rowSelection/utils.ts | 13 ++++++------- 2 files changed, 15 insertions(+), 13 deletions(-) diff --git a/packages/x-data-grid/src/hooks/features/rowSelection/useGridRowSelection.ts b/packages/x-data-grid/src/hooks/features/rowSelection/useGridRowSelection.ts index 8f7378e3d4b6..54020b971046 100644 --- a/packages/x-data-grid/src/hooks/features/rowSelection/useGridRowSelection.ts +++ b/packages/x-data-grid/src/hooks/features/rowSelection/useGridRowSelection.ts @@ -230,22 +230,25 @@ export const useGridRowSelection = ( logger.debug(`Toggling selection for row ${id}`); const selection = gridRowSelectionStateSelector(apiRef.current.state); - let newSelection: GridRowId[] = selection.filter((el) => el !== id); + + let newSelection: Set = new Set(selection.filter((el) => el !== id)); if (isSelected) { - newSelection.push(id); + newSelection.add(id); if (props.propagateRowSelection) { const rowsToSelect = findRowsToSelect(apiRef, tree, id); - newSelection.push(...rowsToSelect); + rowsToSelect.forEach(newSelection.add, newSelection); } } else if (props.propagateRowSelection) { const rowsToDeselect = findRowsToDeselect(apiRef, tree, id); - newSelection = newSelection.filter((el) => !rowsToDeselect.includes(el)); + rowsToDeselect.forEach((parentId) => { + newSelection.delete(parentId); + }); } - const isSelectionValid = newSelection.length < 2 || canHaveMultipleSelection; + const isSelectionValid = newSelection.size < 2 || canHaveMultipleSelection; if (isSelectionValid) { - apiRef.current.setRowSelectionModel(newSelection); + apiRef.current.setRowSelectionModel(Array.from(newSelection)); } } }, diff --git a/packages/x-data-grid/src/hooks/features/rowSelection/utils.ts b/packages/x-data-grid/src/hooks/features/rowSelection/utils.ts index e9300868b18e..f849d0c8b1a0 100644 --- a/packages/x-data-grid/src/hooks/features/rowSelection/utils.ts +++ b/packages/x-data-grid/src/hooks/features/rowSelection/utils.ts @@ -82,13 +82,12 @@ export const findRowsToSelect = ( const rowNode = apiRef.current.getRowNode(selectedRow); if (rowNode?.type === 'group') { - rowsToSelect.push( - ...apiRef.current.getRowGroupChildren({ - groupId: selectedRow, - skipAutoGeneratedRows: false, - applyFiltering: true, - }), - ); + const children = apiRef.current.getRowGroupChildren({ + groupId: selectedRow, + skipAutoGeneratedRows: false, + applyFiltering: true, + }); + children.forEach((child) => rowsToSelect.push(child)); } return rowsToSelect; }; From c4f4d0cab59c980db9f508197b18e2d7794e3815 Mon Sep 17 00:00:00 2001 From: Bilal Shafi Date: Tue, 16 Jul 2024 14:44:12 +0500 Subject: [PATCH 11/45] Lint --- .../src/hooks/features/rowSelection/useGridRowSelection.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/x-data-grid/src/hooks/features/rowSelection/useGridRowSelection.ts b/packages/x-data-grid/src/hooks/features/rowSelection/useGridRowSelection.ts index 54020b971046..91d5f581b5eb 100644 --- a/packages/x-data-grid/src/hooks/features/rowSelection/useGridRowSelection.ts +++ b/packages/x-data-grid/src/hooks/features/rowSelection/useGridRowSelection.ts @@ -231,7 +231,7 @@ export const useGridRowSelection = ( const selection = gridRowSelectionStateSelector(apiRef.current.state); - let newSelection: Set = new Set(selection.filter((el) => el !== id)); + const newSelection: Set = new Set(selection.filter((el) => el !== id)); if (isSelected) { newSelection.add(id); From fbd2d4435ea504dce36bbc545f875690c9313ace Mon Sep 17 00:00:00 2001 From: Bilal Shafi Date: Tue, 16 Jul 2024 20:23:03 +0500 Subject: [PATCH 12/45] Michel's review addressed --- docs/data/data-grid/row-grouping/row-grouping.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/docs/data/data-grid/row-grouping/row-grouping.md b/docs/data/data-grid/row-grouping/row-grouping.md index b0c0954e884f..92fa4be7988c 100644 --- a/docs/data/data-grid/row-grouping/row-grouping.md +++ b/docs/data/data-grid/row-grouping/row-grouping.md @@ -313,16 +313,17 @@ Set the `propagateRowSelection` prop to `true` to achieve the following behavior 1. Selecting/deselecting a parent row would select/deselect all the children rows. 2. When all the child rows are selected, the parent row will be auto selected. 3. When a child row is deselected, if one or more parent rows are already selected, they will be moved to an indeterminate state. -4. Select All checkbox in the header row would select/deselect all the rows including child rows. +4. "Select All" checkbox in the header row would select/deselect all the rows including child rows. +5. After applying the filtering, the previously selected rows which are not in the newly filtered rows will be auto deselected. {{"demo": "RowGroupingPropagateSelection.js", "bg": "inline", "defaultCodeOpen": false}} :::info -If the `propagateRowSelection` is enabled, only the filtered rows will be kept selected. If some rows were selected before filtering, they will be auto deselected if they are not among the newly filtered rows. +If the row selection propagation feature enabled, only the filtered rows will be kept selected. If some rows were selected before filtering, they will be auto deselected if they are not among the newly filtered rows. ::: :::warning -The `propagateRowSelection` is a client-side feature and not recommended to be used with the [server-side data source](/x/react-data-grid/server-side-data/), since it will only work on the partially loaded data. +The row selection propagation is a client-side feature and not recommended to be used with the [server-side data source](/x/react-data-grid/server-side-data/), since it will only work on the partially loaded data. ::: ## Get the rows in a group From 1f50a41a4e5227dcf54f0bddaed9696fe250c360 Mon Sep 17 00:00:00 2001 From: Bilal Shafi Date: Tue, 16 Jul 2024 20:24:37 +0500 Subject: [PATCH 13/45] Minor docs update --- docs/data/data-grid/row-grouping/row-grouping.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/data/data-grid/row-grouping/row-grouping.md b/docs/data/data-grid/row-grouping/row-grouping.md index 92fa4be7988c..5a4fff6dece6 100644 --- a/docs/data/data-grid/row-grouping/row-grouping.md +++ b/docs/data/data-grid/row-grouping/row-grouping.md @@ -319,7 +319,8 @@ Set the `propagateRowSelection` prop to `true` to achieve the following behavior {{"demo": "RowGroupingPropagateSelection.js", "bg": "inline", "defaultCodeOpen": false}} :::info -If the row selection propagation feature enabled, only the filtered rows will be kept selected. If some rows were selected before filtering, they will be auto deselected if they are not among the newly filtered rows. +When the row selection propagation feature is enabled, only the filtered rows will be kept selected. +If some rows were selected before filtering, they will be auto deselected if they are not among the newly filtered rows. ::: :::warning From a328ea62893b99909a3573c585366b93c6e7a75a Mon Sep 17 00:00:00 2001 From: Bilal Shafi Date: Tue, 16 Jul 2024 20:43:29 +0500 Subject: [PATCH 14/45] useDemoData: Temporarily allow higher row size for testing --- .../x-data-grid-generator/src/hooks/useDemoData.ts | 12 ++++++------ .../src/services/tree-data-generator.ts | 6 +++--- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/packages/x-data-grid-generator/src/hooks/useDemoData.ts b/packages/x-data-grid-generator/src/hooks/useDemoData.ts index 2030e6db8240..e5e01d091076 100644 --- a/packages/x-data-grid-generator/src/hooks/useDemoData.ts +++ b/packages/x-data-grid-generator/src/hooks/useDemoData.ts @@ -167,12 +167,12 @@ export const useDemoData = (options: UseDemoDataOptions): DemoDataReturnType => setLoading(true); let newData: DemoTreeDataValue; - if (rowLength > 1000) { - newData = await getRealGridData(1000, columns); - newData = await extrapolateSeed(rowLength, newData); - } else { - newData = await getRealGridData(rowLength, columns); - } + // if (rowLength > 1000) { + // newData = await getRealGridData(1000, columns); + // newData = await extrapolateSeed(rowLength, newData); + // } else { + newData = await getRealGridData(rowLength, columns); + // } if (!active) { return; diff --git a/packages/x-data-grid-generator/src/services/tree-data-generator.ts b/packages/x-data-grid-generator/src/services/tree-data-generator.ts index 938537b95ba7..8cbf6db4691e 100644 --- a/packages/x-data-grid-generator/src/services/tree-data-generator.ts +++ b/packages/x-data-grid-generator/src/services/tree-data-generator.ts @@ -40,9 +40,9 @@ export const addTreeDataOptionsToDemoData = ( return data; } - if (data.rows.length > 1000) { - throw new Error('MUI X: useDemoData tree data mode only works up to 1000 rows.'); - } + // if (data.rows.length > 1000) { + // throw new Error('MUI X: useDemoData tree data mode only works up to 1000 rows.'); + // } const rowsByTreeDepth: Record< number, From 4f03788c466706e46548a3884b2088b1bd9011ff Mon Sep 17 00:00:00 2001 From: Bilal Shafi Date: Tue, 16 Jul 2024 21:02:41 +0500 Subject: [PATCH 15/45] Revert a328ea62893b99909a3573c585366b93c6e7a75a --- .../x-data-grid-generator/src/hooks/useDemoData.ts | 12 ++++++------ .../src/services/tree-data-generator.ts | 6 +++--- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/packages/x-data-grid-generator/src/hooks/useDemoData.ts b/packages/x-data-grid-generator/src/hooks/useDemoData.ts index e5e01d091076..2030e6db8240 100644 --- a/packages/x-data-grid-generator/src/hooks/useDemoData.ts +++ b/packages/x-data-grid-generator/src/hooks/useDemoData.ts @@ -167,12 +167,12 @@ export const useDemoData = (options: UseDemoDataOptions): DemoDataReturnType => setLoading(true); let newData: DemoTreeDataValue; - // if (rowLength > 1000) { - // newData = await getRealGridData(1000, columns); - // newData = await extrapolateSeed(rowLength, newData); - // } else { - newData = await getRealGridData(rowLength, columns); - // } + if (rowLength > 1000) { + newData = await getRealGridData(1000, columns); + newData = await extrapolateSeed(rowLength, newData); + } else { + newData = await getRealGridData(rowLength, columns); + } if (!active) { return; diff --git a/packages/x-data-grid-generator/src/services/tree-data-generator.ts b/packages/x-data-grid-generator/src/services/tree-data-generator.ts index 8cbf6db4691e..938537b95ba7 100644 --- a/packages/x-data-grid-generator/src/services/tree-data-generator.ts +++ b/packages/x-data-grid-generator/src/services/tree-data-generator.ts @@ -40,9 +40,9 @@ export const addTreeDataOptionsToDemoData = ( return data; } - // if (data.rows.length > 1000) { - // throw new Error('MUI X: useDemoData tree data mode only works up to 1000 rows.'); - // } + if (data.rows.length > 1000) { + throw new Error('MUI X: useDemoData tree data mode only works up to 1000 rows.'); + } const rowsByTreeDepth: Record< number, From 0c9fbaabd32625689f16f3e40837678115452f78 Mon Sep 17 00:00:00 2001 From: Bilal Shafi Date: Tue, 16 Jul 2024 23:38:20 +0500 Subject: [PATCH 16/45] Make row group fetch use loop instead of recursion --- .../src/components/columnSelection/GridCellCheckboxRenderer.tsx | 1 + packages/x-data-grid/src/hooks/features/rowSelection/utils.ts | 2 ++ 2 files changed, 3 insertions(+) diff --git a/packages/x-data-grid/src/components/columnSelection/GridCellCheckboxRenderer.tsx b/packages/x-data-grid/src/components/columnSelection/GridCellCheckboxRenderer.tsx index 6bc1a699ca44..cc6d118b2c18 100644 --- a/packages/x-data-grid/src/components/columnSelection/GridCellCheckboxRenderer.tsx +++ b/packages/x-data-grid/src/components/columnSelection/GridCellCheckboxRenderer.tsx @@ -62,6 +62,7 @@ const GridCellCheckboxForwardRef = React.forwardRef rowsToSelect.push(child)); } @@ -115,6 +116,7 @@ export const findRowsToDeselect = ( groupId: deselectedRow, skipAutoGeneratedRows: false, applyFiltering: true, + applySorting: true, }), ]; } From 27904e4d74e93ed702d32faf7a4216c1196bb200 Mon Sep 17 00:00:00 2001 From: Bilal Shafi Date: Thu, 18 Jul 2024 15:13:28 +0500 Subject: [PATCH 17/45] Desc update --- .../api-docs/data-grid/data-grid-premium/data-grid-premium.json | 2 +- .../api-docs/data-grid/data-grid-pro/data-grid-pro.json | 2 +- .../x-data-grid-premium/src/DataGridPremium/DataGridPremium.tsx | 2 +- packages/x-data-grid-pro/src/DataGridPro/DataGridPro.tsx | 2 +- packages/x-data-grid/src/models/props/DataGridProps.ts | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/translations/api-docs/data-grid/data-grid-premium/data-grid-premium.json b/docs/translations/api-docs/data-grid/data-grid-premium/data-grid-premium.json index 87bb4fb2be6b..95af5804f06f 100644 --- a/docs/translations/api-docs/data-grid/data-grid-premium/data-grid-premium.json +++ b/docs/translations/api-docs/data-grid/data-grid-premium/data-grid-premium.json @@ -184,7 +184,7 @@ "groupingColDef": { "description": "The grouping column used by the tree data." }, "headerFilterHeight": { "description": "Override the height of the header filters." }, "headerFilters": { - "description": "If true, the filtering on the header feature is enabled." + "description": "If true, the header filters feature is enabled." }, "hideFooter": { "description": "If true, the footer component is hidden." }, "hideFooterPagination": { diff --git a/docs/translations/api-docs/data-grid/data-grid-pro/data-grid-pro.json b/docs/translations/api-docs/data-grid/data-grid-pro/data-grid-pro.json index 51cd1a55a35a..7b642b62e542 100644 --- a/docs/translations/api-docs/data-grid/data-grid-pro/data-grid-pro.json +++ b/docs/translations/api-docs/data-grid/data-grid-pro/data-grid-pro.json @@ -165,7 +165,7 @@ "groupingColDef": { "description": "The grouping column used by the tree data." }, "headerFilterHeight": { "description": "Override the height of the header filters." }, "headerFilters": { - "description": "If true, the filtering on the header feature is enabled." + "description": "If true, the header filters feature is enabled." }, "hideFooter": { "description": "If true, the footer component is hidden." }, "hideFooterPagination": { diff --git a/packages/x-data-grid-premium/src/DataGridPremium/DataGridPremium.tsx b/packages/x-data-grid-premium/src/DataGridPremium/DataGridPremium.tsx index 0844dc93a227..4aac04e74998 100644 --- a/packages/x-data-grid-premium/src/DataGridPremium/DataGridPremium.tsx +++ b/packages/x-data-grid-premium/src/DataGridPremium/DataGridPremium.tsx @@ -421,7 +421,7 @@ DataGridPremiumRaw.propTypes = { */ headerFilterHeight: PropTypes.number, /** - * If `true`, the filtering on the header feature is enabled. + * If `true`, the header filters feature is enabled. * @default false */ headerFilters: PropTypes.bool, diff --git a/packages/x-data-grid-pro/src/DataGridPro/DataGridPro.tsx b/packages/x-data-grid-pro/src/DataGridPro/DataGridPro.tsx index fc31bb75cfdf..7e6259e2a136 100644 --- a/packages/x-data-grid-pro/src/DataGridPro/DataGridPro.tsx +++ b/packages/x-data-grid-pro/src/DataGridPro/DataGridPro.tsx @@ -376,7 +376,7 @@ DataGridProRaw.propTypes = { */ headerFilterHeight: PropTypes.number, /** - * If `true`, the filtering on the header feature is enabled. + * If `true`, the header filters feature is enabled. * @default false */ headerFilters: PropTypes.bool, diff --git a/packages/x-data-grid/src/models/props/DataGridProps.ts b/packages/x-data-grid/src/models/props/DataGridProps.ts index 1e1644cfbb3b..fe0f3aff13d1 100644 --- a/packages/x-data-grid/src/models/props/DataGridProps.ts +++ b/packages/x-data-grid/src/models/props/DataGridProps.ts @@ -804,7 +804,7 @@ export interface DataGridPropsWithoutDefaultValue Date: Thu, 18 Jul 2024 15:15:25 +0500 Subject: [PATCH 18/45] Romain's suggestion --- .../src/hooks/features/rowSelection/useGridRowSelection.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/x-data-grid/src/hooks/features/rowSelection/useGridRowSelection.ts b/packages/x-data-grid/src/hooks/features/rowSelection/useGridRowSelection.ts index 91d5f581b5eb..d36f9aef72a2 100644 --- a/packages/x-data-grid/src/hooks/features/rowSelection/useGridRowSelection.ts +++ b/packages/x-data-grid/src/hooks/features/rowSelection/useGridRowSelection.ts @@ -231,7 +231,8 @@ export const useGridRowSelection = ( const selection = gridRowSelectionStateSelector(apiRef.current.state); - const newSelection: Set = new Set(selection.filter((el) => el !== id)); + const newSelection: Set = new Set(selection); + newSelection.delete(id); if (isSelected) { newSelection.add(id); From ff2bea17314054e562f3186abc20be0d3efffabd Mon Sep 17 00:00:00 2001 From: Bilal Shafi Date: Thu, 18 Jul 2024 17:12:49 +0500 Subject: [PATCH 19/45] Use specific selectors and update the row propagation updation logic --- .../GridCellCheckboxRenderer.tsx | 28 +----- .../rowSelection/useGridRowSelection.ts | 32 ++++--- .../src/hooks/features/rowSelection/utils.ts | 85 +++++++++++++++---- 3 files changed, 92 insertions(+), 53 deletions(-) diff --git a/packages/x-data-grid/src/components/columnSelection/GridCellCheckboxRenderer.tsx b/packages/x-data-grid/src/components/columnSelection/GridCellCheckboxRenderer.tsx index cc6d118b2c18..3d8284edde95 100644 --- a/packages/x-data-grid/src/components/columnSelection/GridCellCheckboxRenderer.tsx +++ b/packages/x-data-grid/src/components/columnSelection/GridCellCheckboxRenderer.tsx @@ -10,10 +10,8 @@ import { useGridRootProps } from '../../hooks/utils/useGridRootProps'; import { getDataGridUtilityClass } from '../../constants/gridClasses'; import type { DataGridProcessedProps } from '../../models/props/DataGridProps'; import type { GridRowSelectionCheckboxParams } from '../../models/params/gridRowSelectionCheckboxParams'; -import { selectedIdsLookupSelector } from '../../hooks/features/rowSelection/gridRowSelectionSelector'; import { useGridSelector } from '../../hooks/utils/useGridSelector'; -import { GridRowId } from '../../models'; -import { gridFilteredRowsLookupSelector } from '../../hooks/features/filter/gridFilterSelector'; +import { getGridSomeChildrenSelectedSelector } from '../../hooks/features/rowSelection/utils'; type OwnerState = { classes: DataGridProcessedProps['classes'] }; @@ -54,28 +52,8 @@ const GridCellCheckboxForwardRef = React.forwardRef(null); - const rowSelectionLookup = useGridSelector(apiRef, selectedIdsLookupSelector); - const filteredRowsLookup = useGridSelector(apiRef, gridFilteredRowsLookupSelector); - const children: GridRowId[] = React.useMemo(() => { - if (rowNode.type === 'group') { - // @ts-expect-error Access `GridProApi` method - return apiRef.current.getRowGroupChildren({ - groupId: id, - skipAutoGeneratedRows: false, - applySorting: true, - }); - } - return []; - }, [id, rowNode.type, apiRef]); - - const someChildrenSelected = React.useMemo(() => { - if (rowNode.type === 'group') { - return children.some( - (child) => filteredRowsLookup[child] && rowSelectionLookup[child] !== undefined, - ); - } - return false; - }, [children, rowNode.type, rowSelectionLookup, filteredRowsLookup]); + const someChildrenSelectedSelector = getGridSomeChildrenSelectedSelector(id); + const someChildrenSelected = useGridSelector(apiRef, someChildrenSelectedSelector); const rippleRef = React.useRef(null); const handleRef = useForkRef(checkboxElement, ref); diff --git a/packages/x-data-grid/src/hooks/features/rowSelection/useGridRowSelection.ts b/packages/x-data-grid/src/hooks/features/rowSelection/useGridRowSelection.ts index d36f9aef72a2..99b9a6813c7b 100644 --- a/packages/x-data-grid/src/hooks/features/rowSelection/useGridRowSelection.ts +++ b/packages/x-data-grid/src/hooks/features/rowSelection/useGridRowSelection.ts @@ -6,7 +6,7 @@ import { GridRowSelectionApi, GridRowMultiSelectionApi, } from '../../../models/api/gridRowSelectionApi'; -import { GridGroupNode, GridRowId } from '../../../models/gridRows'; +import { GridRowId } from '../../../models/gridRows'; import { GridSignature, useGridApiEventHandler } from '../../utils/useGridApiEventHandler'; import { useGridApiMethod } from '../../utils/useGridApiMethod'; import { useGridLogger } from '../../utils/useGridLogger'; @@ -22,6 +22,7 @@ import { gridFocusCellSelector } from '../focus/gridFocusStateSelector'; import { gridExpandedSortedRowIdsSelector, gridFilterModelSelector, + gridFilteredSortedRowIdsSelector, } from '../filter/gridFilterSelector'; import { GRID_CHECKBOX_SELECTION_COL_DEF, GRID_ACTIONS_COLUMN_TYPE } from '../../../colDef'; import { GridCellModes } from '../../../models/gridEditRowModel'; @@ -88,13 +89,15 @@ export const useGridRowSelection = ( ): void => { const logger = useGridLogger(apiRef, 'useGridSelection'); - const runIfRowSelectionIsEnabled = + const runIfRowSelectionIsEnabled = React.useCallback( (callback: (...args: Args) => void) => - (...args: Args) => { - if (props.rowSelection) { - callback(...args); - } - }; + (...args: Args) => { + if (props.rowSelection) { + callback(...args); + } + }, + [props.rowSelection], + ); const propRowSelectionModel = React.useMemo(() => { return getSelectionModelPropValue( @@ -573,15 +576,15 @@ export const useGridRowSelection = ( [apiRef, handleSingleRowSelection, selectRows, visibleRows.rows, canHaveMultipleSelection], ); - const handleFilteredRowsSet = React.useCallback(() => { + const handleRowPropagation = React.useCallback(() => { if (!props.propagateRowSelection) { return; } - const filteredRows = gridExpandedSortedRowIdsSelector(apiRef); + const filteredRows = gridFilteredSortedRowIdsSelector(apiRef); const selectedRows = selectedGridRowsSelector(apiRef); const newSelectedRows: GridRowId[] = []; selectedRows.forEach((row) => { - if (filteredRows.includes(row?.id) && !(tree[row.id] as GridGroupNode)?.isAutoGenerated) { + if (filteredRows.includes(row?.id) && tree[row.id].type !== 'group') { newSelectedRows.push(row.id); } }); @@ -613,7 +616,7 @@ export const useGridRowSelection = ( useGridApiEventHandler( apiRef, 'filteredRowsSet', - runIfRowSelectionIsEnabled(handleFilteredRowsSet), + runIfRowSelectionIsEnabled(handleRowPropagation), ); React.useEffect(() => { @@ -657,4 +660,11 @@ export const useGridRowSelection = ( apiRef.current.setRowSelectionModel([]); } }, [apiRef, canHaveMultipleSelection, checkboxSelection, isStateControlled, props.rowSelection]); + + React.useEffect(() => { + // TODO v8: Remove when `propagateRowSelection` is the default behavior + if (props.propagateRowSelection) { + runIfRowSelectionIsEnabled(handleRowPropagation); + } + }, [handleRowPropagation, props.propagateRowSelection, runIfRowSelectionIsEnabled]); }; diff --git a/packages/x-data-grid/src/hooks/features/rowSelection/utils.ts b/packages/x-data-grid/src/hooks/features/rowSelection/utils.ts index 69d3f612129d..c94f19b3d538 100644 --- a/packages/x-data-grid/src/hooks/features/rowSelection/utils.ts +++ b/packages/x-data-grid/src/hooks/features/rowSelection/utils.ts @@ -4,6 +4,67 @@ import { GRID_ROOT_GROUP_ID } from '../rows/gridRowsUtils'; import type { GridGroupNode, GridRowId, GridRowTreeConfig } from '../../../models/gridRows'; import type { GridPrivateApiCommunity } from '../../../models/api/gridApiCommunity'; import { gridFilteredRowsLookupSelector } from '../filter/gridFilterSelector'; +import { gridSortedRowIdsSelector } from '../sorting/gridSortingSelector'; +import { selectedIdsLookupSelector } from './gridRowSelectionSelector'; +import { gridRowTreeSelector } from '../rows/gridRowsSelector'; +import { createSelector } from '../../../utils/createSelector'; + +function getGridRowGroupChildrenSelector(groupId: GridRowId) { + return createSelector( + gridRowTreeSelector, + gridSortedRowIdsSelector, + gridFilteredRowsLookupSelector, + (rowTree, sortedRowIds, filteredRowsLookup) => { + const groupNode = rowTree[groupId]; + if (!groupNode || groupNode.type !== 'group') { + return []; + } + + const children: GridRowId[] = []; + + const startIndex = sortedRowIds.findIndex((id) => id === groupId) + 1; + for ( + let index = startIndex; + index < sortedRowIds.length && rowTree[sortedRowIds[index]].depth > groupNode.depth; + index += 1 + ) { + const id = sortedRowIds[index]; + if (filteredRowsLookup[id] !== false) { + children.push(id); + } + } + return children; + }, + ); +} + +export function getGridSomeChildrenSelectedSelector(groupId: GridRowId) { + return createSelector( + gridRowTreeSelector, + gridSortedRowIdsSelector, + gridFilteredRowsLookupSelector, + selectedIdsLookupSelector, + (rowTree, sortedRowIds, filteredRowsLookup, rowSelectionLookup) => { + const groupNode = rowTree[groupId]; + if (!groupNode || groupNode.type !== 'group') { + return false; + } + + const startIndex = sortedRowIds.findIndex((id) => id === groupId) + 1; + for ( + let index = startIndex; + index < sortedRowIds.length && rowTree[sortedRowIds[index]].depth > groupNode.depth; + index += 1 + ) { + const id = sortedRowIds[index]; + if (filteredRowsLookup[id] !== false && rowSelectionLookup[id] !== undefined) { + return true; + } + } + return false; + }, + ); +} export function isMultipleRowSelectionEnabled( props: Pick< @@ -82,13 +143,9 @@ export const findRowsToSelect = ( const rowNode = apiRef.current.getRowNode(selectedRow); if (rowNode?.type === 'group') { - const children = apiRef.current.getRowGroupChildren({ - groupId: selectedRow, - skipAutoGeneratedRows: false, - applyFiltering: true, - applySorting: true, - }); - children.forEach((child) => rowsToSelect.push(child)); + const rowGroupChildrenSelector = getGridRowGroupChildrenSelector(selectedRow); + const children = rowGroupChildrenSelector(apiRef); + return rowsToSelect.concat(children); } return rowsToSelect; }; @@ -98,7 +155,7 @@ export const findRowsToDeselect = ( tree: GridRowTreeConfig, deselectedRow: GridRowId, ) => { - let rowsToDeselect: GridRowId[] = []; + const rowsToDeselect: GridRowId[] = []; const allParents = getRowNodeParents(tree, deselectedRow); allParents.forEach((parent) => { @@ -110,15 +167,9 @@ export const findRowsToDeselect = ( const rowNode = apiRef.current.getRowNode(deselectedRow); if (rowNode?.type === 'group') { - rowsToDeselect = [ - ...rowsToDeselect, - ...apiRef.current.getRowGroupChildren({ - groupId: deselectedRow, - skipAutoGeneratedRows: false, - applyFiltering: true, - applySorting: true, - }), - ]; + const rowGroupChildrenSelector = getGridRowGroupChildrenSelector(deselectedRow); + const children = rowGroupChildrenSelector(apiRef); + return rowsToDeselect.concat(children); } return rowsToDeselect; }; From 8d2d05902687b4a40022491d500854bf5a85ef0a Mon Sep 17 00:00:00 2001 From: Bilal Shafi Date: Thu, 18 Jul 2024 19:22:43 +0500 Subject: [PATCH 20/45] Fix failing tests --- .../features/filter/gridFilterSelector.ts | 16 +++++++++++++ .../rowSelection/useGridRowSelection.ts | 24 +++++++++++++------ .../src/hooks/features/rowSelection/utils.ts | 4 ++-- 3 files changed, 35 insertions(+), 9 deletions(-) diff --git a/packages/x-data-grid/src/hooks/features/filter/gridFilterSelector.ts b/packages/x-data-grid/src/hooks/features/filter/gridFilterSelector.ts index 9d376a2321f2..9a3208437e3d 100644 --- a/packages/x-data-grid/src/hooks/features/filter/gridFilterSelector.ts +++ b/packages/x-data-grid/src/hooks/features/filter/gridFilterSelector.ts @@ -4,6 +4,7 @@ import { GridStateCommunity } from '../../../models/gridStateCommunity'; import { gridSortedRowEntriesSelector } from '../sorting/gridSortingSelector'; import { gridColumnLookupSelector } from '../columns/gridColumnsSelector'; import { gridRowMaximumTreeDepthSelector, gridRowTreeSelector } from '../rows/gridRowsSelector'; +import { GridRowId } from '../../../models/gridRows'; /** * @category Filtering @@ -96,6 +97,21 @@ export const gridFilteredSortedRowIdsSelector = createSelectorMemoized( (filteredSortedRowEntries) => filteredSortedRowEntries.map((row) => row.id), ); +/** + * Get a `Set` containing the ids of the rows accessible after the filtering process. + * Contains the collapsed children. + * @category Filtering + * @ignore - Do not document. + */ +export const gridFilteredSortedRowIdsSetSelector = createSelectorMemoized( + gridFilteredSortedRowEntriesSelector, + (filteredSortedRowEntries) => { + const filteredSortedRowIdsSetSelector = new Set(); + filteredSortedRowEntries.map((row) => filteredSortedRowIdsSetSelector.add(row.id)); + return filteredSortedRowIdsSetSelector; + }, +); + /** * Get the id and the model of the top level rows accessible after the filtering process. * @category Filtering diff --git a/packages/x-data-grid/src/hooks/features/rowSelection/useGridRowSelection.ts b/packages/x-data-grid/src/hooks/features/rowSelection/useGridRowSelection.ts index 99b9a6813c7b..50cc6dc8c15d 100644 --- a/packages/x-data-grid/src/hooks/features/rowSelection/useGridRowSelection.ts +++ b/packages/x-data-grid/src/hooks/features/rowSelection/useGridRowSelection.ts @@ -6,7 +6,7 @@ import { GridRowSelectionApi, GridRowMultiSelectionApi, } from '../../../models/api/gridRowSelectionApi'; -import { GridRowId } from '../../../models/gridRows'; +import { GridGroupNode, GridRowId } from '../../../models/gridRows'; import { GridSignature, useGridApiEventHandler } from '../../utils/useGridApiEventHandler'; import { useGridApiMethod } from '../../utils/useGridApiMethod'; import { useGridLogger } from '../../utils/useGridLogger'; @@ -22,7 +22,7 @@ import { gridFocusCellSelector } from '../focus/gridFocusStateSelector'; import { gridExpandedSortedRowIdsSelector, gridFilterModelSelector, - gridFilteredSortedRowIdsSelector, + gridFilteredSortedRowIdsSetSelector, } from '../filter/gridFilterSelector'; import { GRID_CHECKBOX_SELECTION_COL_DEF, GRID_ACTIONS_COLUMN_TYPE } from '../../../colDef'; import { GridCellModes } from '../../../models/gridEditRowModel'; @@ -580,15 +580,25 @@ export const useGridRowSelection = ( if (!props.propagateRowSelection) { return; } - const filteredRows = gridFilteredSortedRowIdsSelector(apiRef); + const filteredRows = gridFilteredSortedRowIdsSetSelector(apiRef); const selectedRows = selectedGridRowsSelector(apiRef); - const newSelectedRows: GridRowId[] = []; + const newSelectedRows: Set = new Set(); selectedRows.forEach((row) => { - if (filteredRows.includes(row?.id) && tree[row.id].type !== 'group') { - newSelectedRows.push(row.id); + const id = apiRef.current.getRowId(row); + if (filteredRows.has(id)) { + const node = tree[id]; + if (node.type === 'group' && !(node as GridGroupNode).isAutoGenerated) { + // Keep those previously selected tree data parents whose all children are filtered + // out and they've become leaf node now in `newSelectedRows` so that they are not auto deselected + if (!node.children.some((childId) => (newSelectedRows.has(childId) ? true : false))) { + newSelectedRows.add(id); + } + } else { + newSelectedRows.add(id); + } } }); - apiRef.current.selectRows(newSelectedRows, true, true); + apiRef.current.selectRows(Array.from(newSelectedRows), true, true); }, [apiRef, props.propagateRowSelection, tree]); useGridApiEventHandler( diff --git a/packages/x-data-grid/src/hooks/features/rowSelection/utils.ts b/packages/x-data-grid/src/hooks/features/rowSelection/utils.ts index c94f19b3d538..0507f2a2450d 100644 --- a/packages/x-data-grid/src/hooks/features/rowSelection/utils.ts +++ b/packages/x-data-grid/src/hooks/features/rowSelection/utils.ts @@ -25,7 +25,7 @@ function getGridRowGroupChildrenSelector(groupId: GridRowId) { const startIndex = sortedRowIds.findIndex((id) => id === groupId) + 1; for ( let index = startIndex; - index < sortedRowIds.length && rowTree[sortedRowIds[index]].depth > groupNode.depth; + index < sortedRowIds.length && rowTree[sortedRowIds[index]]?.depth > groupNode.depth; index += 1 ) { const id = sortedRowIds[index]; @@ -53,7 +53,7 @@ export function getGridSomeChildrenSelectedSelector(groupId: GridRowId) { const startIndex = sortedRowIds.findIndex((id) => id === groupId) + 1; for ( let index = startIndex; - index < sortedRowIds.length && rowTree[sortedRowIds[index]].depth > groupNode.depth; + index < sortedRowIds.length && rowTree[sortedRowIds[index]]?.depth > groupNode.depth; index += 1 ) { const id = sortedRowIds[index]; From b6ca56733fb3d7be9eb3d15fd569cb8b7bf8c2f1 Mon Sep 17 00:00:00 2001 From: Bilal Shafi Date: Thu, 18 Jul 2024 20:04:47 +0500 Subject: [PATCH 21/45] Updates --- .../src/hooks/features/rowSelection/useGridRowSelection.ts | 5 ++--- scripts/x-data-grid-premium.exports.json | 1 + scripts/x-data-grid-pro.exports.json | 1 + scripts/x-data-grid.exports.json | 1 + 4 files changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/x-data-grid/src/hooks/features/rowSelection/useGridRowSelection.ts b/packages/x-data-grid/src/hooks/features/rowSelection/useGridRowSelection.ts index 50cc6dc8c15d..709f2b05ea5f 100644 --- a/packages/x-data-grid/src/hooks/features/rowSelection/useGridRowSelection.ts +++ b/packages/x-data-grid/src/hooks/features/rowSelection/useGridRowSelection.ts @@ -583,14 +583,13 @@ export const useGridRowSelection = ( const filteredRows = gridFilteredSortedRowIdsSetSelector(apiRef); const selectedRows = selectedGridRowsSelector(apiRef); const newSelectedRows: Set = new Set(); - selectedRows.forEach((row) => { - const id = apiRef.current.getRowId(row); + selectedRows.forEach((_, id) => { if (filteredRows.has(id)) { const node = tree[id]; if (node.type === 'group' && !(node as GridGroupNode).isAutoGenerated) { // Keep those previously selected tree data parents whose all children are filtered // out and they've become leaf node now in `newSelectedRows` so that they are not auto deselected - if (!node.children.some((childId) => (newSelectedRows.has(childId) ? true : false))) { + if (!node.children.some((childId) => newSelectedRows.has(childId))) { newSelectedRows.add(id); } } else { diff --git a/scripts/x-data-grid-premium.exports.json b/scripts/x-data-grid-premium.exports.json index 095b9e762608..693188eca45b 100644 --- a/scripts/x-data-grid-premium.exports.json +++ b/scripts/x-data-grid-premium.exports.json @@ -329,6 +329,7 @@ { "name": "gridFilteredRowsLookupSelector", "kind": "Variable" }, { "name": "gridFilteredSortedRowEntriesSelector", "kind": "Variable" }, { "name": "gridFilteredSortedRowIdsSelector", "kind": "Variable" }, + { "name": "gridFilteredSortedRowIdsSetSelector", "kind": "Variable" }, { "name": "gridFilteredSortedTopLevelRowEntriesSelector", "kind": "Variable" }, { "name": "gridFilteredTopLevelRowCountSelector", "kind": "Variable" }, { "name": "GridFilterForm", "kind": "Variable" }, diff --git a/scripts/x-data-grid-pro.exports.json b/scripts/x-data-grid-pro.exports.json index 1f61374d2036..9e2d93c3e46d 100644 --- a/scripts/x-data-grid-pro.exports.json +++ b/scripts/x-data-grid-pro.exports.json @@ -298,6 +298,7 @@ { "name": "gridFilteredRowsLookupSelector", "kind": "Variable" }, { "name": "gridFilteredSortedRowEntriesSelector", "kind": "Variable" }, { "name": "gridFilteredSortedRowIdsSelector", "kind": "Variable" }, + { "name": "gridFilteredSortedRowIdsSetSelector", "kind": "Variable" }, { "name": "gridFilteredSortedTopLevelRowEntriesSelector", "kind": "Variable" }, { "name": "gridFilteredTopLevelRowCountSelector", "kind": "Variable" }, { "name": "GridFilterForm", "kind": "Variable" }, diff --git a/scripts/x-data-grid.exports.json b/scripts/x-data-grid.exports.json index d6ba9efe3d01..7006290e6ad5 100644 --- a/scripts/x-data-grid.exports.json +++ b/scripts/x-data-grid.exports.json @@ -268,6 +268,7 @@ { "name": "gridFilteredRowsLookupSelector", "kind": "Variable" }, { "name": "gridFilteredSortedRowEntriesSelector", "kind": "Variable" }, { "name": "gridFilteredSortedRowIdsSelector", "kind": "Variable" }, + { "name": "gridFilteredSortedRowIdsSetSelector", "kind": "Variable" }, { "name": "gridFilteredSortedTopLevelRowEntriesSelector", "kind": "Variable" }, { "name": "gridFilteredTopLevelRowCountSelector", "kind": "Variable" }, { "name": "GridFilterForm", "kind": "Variable" }, From 5facfd3fd64160daf56af16476a70520004aa600 Mon Sep 17 00:00:00 2001 From: Bilal Shafi Date: Wed, 24 Jul 2024 13:25:39 +0500 Subject: [PATCH 22/45] Make the auto selection respect the isRowSelectable prop --- .../src/hooks/features/rowSelection/utils.ts | 21 +++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/packages/x-data-grid/src/hooks/features/rowSelection/utils.ts b/packages/x-data-grid/src/hooks/features/rowSelection/utils.ts index 0507f2a2450d..ed72f8abf1d9 100644 --- a/packages/x-data-grid/src/hooks/features/rowSelection/utils.ts +++ b/packages/x-data-grid/src/hooks/features/rowSelection/utils.ts @@ -2,14 +2,20 @@ import type { DataGridProcessedProps } from '../../../models/props/DataGridProps import { GridSignature } from '../../utils/useGridApiEventHandler'; import { GRID_ROOT_GROUP_ID } from '../rows/gridRowsUtils'; import type { GridGroupNode, GridRowId, GridRowTreeConfig } from '../../../models/gridRows'; -import type { GridPrivateApiCommunity } from '../../../models/api/gridApiCommunity'; +import type { + GridPrivateApiCommunity, + GridApiCommunity, +} from '../../../models/api/gridApiCommunity'; import { gridFilteredRowsLookupSelector } from '../filter/gridFilterSelector'; import { gridSortedRowIdsSelector } from '../sorting/gridSortingSelector'; import { selectedIdsLookupSelector } from './gridRowSelectionSelector'; import { gridRowTreeSelector } from '../rows/gridRowsSelector'; import { createSelector } from '../../../utils/createSelector'; -function getGridRowGroupChildrenSelector(groupId: GridRowId) { +function getGridRowGroupSelectableChildrenSelector( + apiRef: React.MutableRefObject, + groupId: GridRowId, +) { return createSelector( gridRowTreeSelector, gridSortedRowIdsSelector, @@ -29,7 +35,7 @@ function getGridRowGroupChildrenSelector(groupId: GridRowId) { index += 1 ) { const id = sortedRowIds[index]; - if (filteredRowsLookup[id] !== false) { + if (filteredRowsLookup[id] !== false && apiRef.current.isRowSelectable(id)) { children.push(id); } } @@ -132,7 +138,7 @@ export const findRowsToSelect = ( ) { const rowNode = apiRef.current.getRowNode(rowId) as GridGroupNode; const parent = rowNode.parent; - if (parent && parent !== GRID_ROOT_GROUP_ID) { + if (parent && parent !== GRID_ROOT_GROUP_ID && apiRef.current.isRowSelectable(parent)) { rowsToSelect.push(parent); traverseParents(parent); } @@ -143,7 +149,7 @@ export const findRowsToSelect = ( const rowNode = apiRef.current.getRowNode(selectedRow); if (rowNode?.type === 'group') { - const rowGroupChildrenSelector = getGridRowGroupChildrenSelector(selectedRow); + const rowGroupChildrenSelector = getGridRowGroupSelectableChildrenSelector(apiRef, selectedRow); const children = rowGroupChildrenSelector(apiRef); return rowsToSelect.concat(children); } @@ -167,7 +173,10 @@ export const findRowsToDeselect = ( const rowNode = apiRef.current.getRowNode(deselectedRow); if (rowNode?.type === 'group') { - const rowGroupChildrenSelector = getGridRowGroupChildrenSelector(deselectedRow); + const rowGroupChildrenSelector = getGridRowGroupSelectableChildrenSelector( + apiRef, + deselectedRow, + ); const children = rowGroupChildrenSelector(apiRef); return rowsToDeselect.concat(children); } From 49ed4e071d777c643c3986449d3b5b4ebf3b216e Mon Sep 17 00:00:00 2001 From: Bilal Shafi Date: Wed, 24 Jul 2024 21:12:35 +0500 Subject: [PATCH 23/45] Make select all checkbox in indeterminate state select all the rows --- .../src/tests/rowSelection.DataGridPro.test.tsx | 10 +++++----- .../components/columnSelection/GridHeaderCheckbox.tsx | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/x-data-grid-pro/src/tests/rowSelection.DataGridPro.test.tsx b/packages/x-data-grid-pro/src/tests/rowSelection.DataGridPro.test.tsx index 50566d91c0b9..3778a5985237 100644 --- a/packages/x-data-grid-pro/src/tests/rowSelection.DataGridPro.test.tsx +++ b/packages/x-data-grid-pro/src/tests/rowSelection.DataGridPro.test.tsx @@ -80,8 +80,8 @@ describe(' - Row selection', () => { name: /select all rows/i, }); fireEvent.click(selectAllCheckbox); - expect(apiRef.current.getSelectedRows()).to.have.length(0); - expect(selectAllCheckbox.checked).to.equal(false); + expect(apiRef.current.getSelectedRows()).to.have.length(4); + expect(selectAllCheckbox.checked).to.equal(true); }); it('should select all visible rows if pagination is not enabled', () => { @@ -170,7 +170,7 @@ describe(' - Row selection', () => { expect(selectAllCheckbox.checked).to.equal(true); }); - it('should unselect all the rows of the current page if 1 row of the current page is selected', () => { + it('should select all the rows of the current page if 1 row of the current page is selected', () => { render( - Row selection', () => { name: /select all rows/i, }); fireEvent.click(selectAllCheckbox); - expect(apiRef.current.getSelectedRows()).to.have.keys([0]); - expect(selectAllCheckbox.checked).to.equal(false); + expect(apiRef.current.getSelectedRows()).to.have.keys([0, 2, 3]); + expect(selectAllCheckbox.checked).to.equal(true); }); it('should not set the header checkbox in a indeterminate state when some rows of other pages are not selected', () => { diff --git a/packages/x-data-grid/src/components/columnSelection/GridHeaderCheckbox.tsx b/packages/x-data-grid/src/components/columnSelection/GridHeaderCheckbox.tsx index 47ee8b35b92b..e594490ad093 100644 --- a/packages/x-data-grid/src/components/columnSelection/GridHeaderCheckbox.tsx +++ b/packages/x-data-grid/src/components/columnSelection/GridHeaderCheckbox.tsx @@ -133,7 +133,7 @@ const GridHeaderCheckbox = React.forwardRef Date: Thu, 1 Aug 2024 22:31:50 +0500 Subject: [PATCH 24/45] Introduce selectors with arguments --- .../GridCellCheckboxRenderer.tsx | 9 +- .../src/hooks/features/rowSelection/utils.ts | 134 +++++++------ .../src/hooks/utils/useGridSelectorV8.ts | 83 ++++++++ .../x-data-grid/src/utils/createSelectorV8.ts | 181 ++++++++++++++++++ 4 files changed, 335 insertions(+), 72 deletions(-) create mode 100644 packages/x-data-grid/src/hooks/utils/useGridSelectorV8.ts create mode 100644 packages/x-data-grid/src/utils/createSelectorV8.ts diff --git a/packages/x-data-grid/src/components/columnSelection/GridCellCheckboxRenderer.tsx b/packages/x-data-grid/src/components/columnSelection/GridCellCheckboxRenderer.tsx index 3d8284edde95..5bbb0cf4322e 100644 --- a/packages/x-data-grid/src/components/columnSelection/GridCellCheckboxRenderer.tsx +++ b/packages/x-data-grid/src/components/columnSelection/GridCellCheckboxRenderer.tsx @@ -10,8 +10,8 @@ import { useGridRootProps } from '../../hooks/utils/useGridRootProps'; import { getDataGridUtilityClass } from '../../constants/gridClasses'; import type { DataGridProcessedProps } from '../../models/props/DataGridProps'; import type { GridRowSelectionCheckboxParams } from '../../models/params/gridRowSelectionCheckboxParams'; -import { useGridSelector } from '../../hooks/utils/useGridSelector'; -import { getGridSomeChildrenSelectedSelector } from '../../hooks/features/rowSelection/utils'; +import { useGridSelectorV8 } from '../../hooks/utils/useGridSelectorV8'; +import { gridSomeChildrenSelectedSelector } from '../../hooks/features/rowSelection/utils'; type OwnerState = { classes: DataGridProcessedProps['classes'] }; @@ -52,8 +52,9 @@ const GridCellCheckboxForwardRef = React.forwardRef(null); - const someChildrenSelectedSelector = getGridSomeChildrenSelectedSelector(id); - const someChildrenSelected = useGridSelector(apiRef, someChildrenSelectedSelector); + const someChildrenSelected = useGridSelectorV8(apiRef, gridSomeChildrenSelectedSelector, { + groupId: id, + }); const rippleRef = React.useRef(null); const handleRef = useForkRef(checkboxElement, ref); diff --git a/packages/x-data-grid/src/hooks/features/rowSelection/utils.ts b/packages/x-data-grid/src/hooks/features/rowSelection/utils.ts index ed72f8abf1d9..ab8260bf71a7 100644 --- a/packages/x-data-grid/src/hooks/features/rowSelection/utils.ts +++ b/packages/x-data-grid/src/hooks/features/rowSelection/utils.ts @@ -2,75 +2,72 @@ import type { DataGridProcessedProps } from '../../../models/props/DataGridProps import { GridSignature } from '../../utils/useGridApiEventHandler'; import { GRID_ROOT_GROUP_ID } from '../rows/gridRowsUtils'; import type { GridGroupNode, GridRowId, GridRowTreeConfig } from '../../../models/gridRows'; -import type { - GridPrivateApiCommunity, - GridApiCommunity, -} from '../../../models/api/gridApiCommunity'; +import type { GridPrivateApiCommunity } from '../../../models/api/gridApiCommunity'; import { gridFilteredRowsLookupSelector } from '../filter/gridFilterSelector'; import { gridSortedRowIdsSelector } from '../sorting/gridSortingSelector'; import { selectedIdsLookupSelector } from './gridRowSelectionSelector'; import { gridRowTreeSelector } from '../rows/gridRowsSelector'; -import { createSelector } from '../../../utils/createSelector'; - -function getGridRowGroupSelectableChildrenSelector( - apiRef: React.MutableRefObject, - groupId: GridRowId, -) { - return createSelector( - gridRowTreeSelector, - gridSortedRowIdsSelector, - gridFilteredRowsLookupSelector, - (rowTree, sortedRowIds, filteredRowsLookup) => { - const groupNode = rowTree[groupId]; - if (!groupNode || groupNode.type !== 'group') { - return []; - } - - const children: GridRowId[] = []; - - const startIndex = sortedRowIds.findIndex((id) => id === groupId) + 1; - for ( - let index = startIndex; - index < sortedRowIds.length && rowTree[sortedRowIds[index]]?.depth > groupNode.depth; - index += 1 - ) { - const id = sortedRowIds[index]; - if (filteredRowsLookup[id] !== false && apiRef.current.isRowSelectable(id)) { - children.push(id); - } - } +import { createSelectorV8 } from '../../../utils/createSelectorV8'; + +export const gridRowGroupSelectableChildrenSelector = createSelectorV8( + gridRowTreeSelector, + gridSortedRowIdsSelector, + gridFilteredRowsLookupSelector, + (rowTree, sortedRowIds, filteredRowsLookup, args) => { + const children = new Set(); + const { groupId, apiRef } = args; + if (groupId === undefined || apiRef === undefined) { return children; - }, - ); -} + } + const groupNode = rowTree[groupId]; + if (!groupNode || groupNode.type !== 'group') { + return children; + } -export function getGridSomeChildrenSelectedSelector(groupId: GridRowId) { - return createSelector( - gridRowTreeSelector, - gridSortedRowIdsSelector, - gridFilteredRowsLookupSelector, - selectedIdsLookupSelector, - (rowTree, sortedRowIds, filteredRowsLookup, rowSelectionLookup) => { - const groupNode = rowTree[groupId]; - if (!groupNode || groupNode.type !== 'group') { - return false; + const startIndex = sortedRowIds.findIndex((id) => id === groupId) + 1; + for ( + let index = startIndex; + index < sortedRowIds.length && rowTree[sortedRowIds[index]]?.depth > groupNode.depth; + index += 1 + ) { + const id = sortedRowIds[index]; + if (filteredRowsLookup[id] !== false && apiRef.current.isRowSelectable(id)) { + children.add(id); } + } + return children; + }, +); + +export const gridSomeChildrenSelectedSelector = createSelectorV8( + gridRowTreeSelector, + gridSortedRowIdsSelector, + gridFilteredRowsLookupSelector, + selectedIdsLookupSelector, + (rowTree, sortedRowIds, filteredRowsLookup, rowSelectionLookup, args) => { + const groupId = args.groupId; + if (groupId === undefined) { + return false; + } + const groupNode = rowTree[groupId]; + if (!groupNode || groupNode.type !== 'group') { + return false; + } - const startIndex = sortedRowIds.findIndex((id) => id === groupId) + 1; - for ( - let index = startIndex; - index < sortedRowIds.length && rowTree[sortedRowIds[index]]?.depth > groupNode.depth; - index += 1 - ) { - const id = sortedRowIds[index]; - if (filteredRowsLookup[id] !== false && rowSelectionLookup[id] !== undefined) { - return true; - } + const startIndex = sortedRowIds.findIndex((id) => id === groupId) + 1; + for ( + let index = startIndex; + index < sortedRowIds.length && rowTree[sortedRowIds[index]]?.depth > groupNode.depth; + index += 1 + ) { + const id = sortedRowIds[index]; + if (filteredRowsLookup[id] !== false && rowSelectionLookup[id] !== undefined) { + return true; } - return false; - }, - ); -} + } + return false; + }, +); export function isMultipleRowSelectionEnabled( props: Pick< @@ -149,9 +146,11 @@ export const findRowsToSelect = ( const rowNode = apiRef.current.getRowNode(selectedRow); if (rowNode?.type === 'group') { - const rowGroupChildrenSelector = getGridRowGroupSelectableChildrenSelector(apiRef, selectedRow); - const children = rowGroupChildrenSelector(apiRef); - return rowsToSelect.concat(children); + const children = gridRowGroupSelectableChildrenSelector(apiRef, { + groupId: selectedRow, + apiRef, + }); + return rowsToSelect.concat(Array.from(children)); } return rowsToSelect; }; @@ -173,12 +172,11 @@ export const findRowsToDeselect = ( const rowNode = apiRef.current.getRowNode(deselectedRow); if (rowNode?.type === 'group') { - const rowGroupChildrenSelector = getGridRowGroupSelectableChildrenSelector( + const children = gridRowGroupSelectableChildrenSelector(apiRef, { + groupId: deselectedRow, apiRef, - deselectedRow, - ); - const children = rowGroupChildrenSelector(apiRef); - return rowsToDeselect.concat(children); + }); + return rowsToDeselect.concat(Array.from(children)); } return rowsToDeselect; }; diff --git a/packages/x-data-grid/src/hooks/utils/useGridSelectorV8.ts b/packages/x-data-grid/src/hooks/utils/useGridSelectorV8.ts new file mode 100644 index 000000000000..7b7b8a7ee81e --- /dev/null +++ b/packages/x-data-grid/src/hooks/utils/useGridSelectorV8.ts @@ -0,0 +1,83 @@ +import * as React from 'react'; +import type { GridApiCommon } from '../../models/api/gridApiCommon'; +import { OutputSelectorV8 } from '../../utils/createSelectorV8'; +import { useLazyRef } from './useLazyRef'; +import { useOnMount } from './useOnMount'; +import { warnOnce } from '../../internals/utils/warning'; +import type { GridCoreApi } from '../../models/api/gridCoreApi'; +import { fastObjectShallowCompare } from '../../utils/fastObjectShallowCompare'; + +function isOutputSelector( + selector: any, +): selector is OutputSelectorV8 { + return selector.acceptsApiRef; +} + +function applySelectorV8( + apiRef: React.MutableRefObject, + selector: ((state: Api['state']) => T) | OutputSelectorV8, + args: Args, + instanceId: GridCoreApi['instanceId'], +) { + if (isOutputSelector(selector)) { + return selector(apiRef, args); + } + return selector(apiRef.current.state, instanceId); +} + +const defaultCompare = Object.is; +export const objectShallowCompare = fastObjectShallowCompare; + +const createRefs = () => ({ state: null, equals: null, selector: null }) as any; + +export const useGridSelectorV8 = ( + apiRef: React.MutableRefObject, + selector: ((state: Api['state']) => T) | OutputSelectorV8, + args: Args = {} as Args, + equals: (a: T, b: T) => boolean = defaultCompare, +) => { + if (process.env.NODE_ENV !== 'production') { + if (!apiRef.current.state) { + warnOnce([ + 'MUI X: `useGridSelector` has been called before the initialization of the state.', + 'This hook can only be used inside the context of the grid.', + ]); + } + } + + const refs = useLazyRef< + { + state: T; + equals: typeof equals; + selector: typeof selector; + }, + never + >(createRefs); + const didInit = refs.current.selector !== null; + + const [state, setState] = React.useState( + // We don't use an initialization function to avoid allocations + (didInit ? null : applySelectorV8(apiRef, selector, args, apiRef.current.instanceId)) as T, + ); + + refs.current.state = state; + refs.current.equals = equals; + refs.current.selector = selector; + + useOnMount(() => { + return apiRef.current.store.subscribe(() => { + const newState = applySelectorV8( + apiRef, + refs.current.selector, + args, + apiRef.current.instanceId, + ) as T; + if (!refs.current.equals(refs.current.state, newState)) { + refs.current.state = newState; + setState(newState); + } + }); + }); + + return state; +}; diff --git a/packages/x-data-grid/src/utils/createSelectorV8.ts b/packages/x-data-grid/src/utils/createSelectorV8.ts new file mode 100644 index 000000000000..b64c12c3c354 --- /dev/null +++ b/packages/x-data-grid/src/utils/createSelectorV8.ts @@ -0,0 +1,181 @@ +import * as React from 'react'; +import { createSelector as reselectCreateSelector, Selector, SelectorResultArray } from 'reselect'; +import type { GridCoreApi } from '../models/api/gridCoreApi'; +import { warnOnce } from '../internals/utils/warning'; + +type CacheKey = { id: number }; + +export interface OutputSelectorV8 { + ( + apiRef: React.MutableRefObject<{ state: State; instanceId: GridCoreApi['instanceId'] }>, + args: Args, + ): Result; + (state: State, instanceId: GridCoreApi['instanceId']): Result; + acceptsApiRef: boolean; +} + +type StateFromSelector = T extends (first: infer F, ...args: any[]) => any + ? F extends { state: infer F2 } + ? F2 + : F + : never; + +type StateFromSelectorList = Selectors extends [ + f: infer F, + ...other: infer R, +] + ? StateFromSelector extends StateFromSelectorList + ? StateFromSelector + : StateFromSelectorList + : {}; + +type SelectorResultArrayWithAdditionalArgs>> = [ + ...SelectorResultArray, + Record, +]; + +type SelectorArgs>, Result> = + // Input selectors as a separate array + | [ + selectors: [...Selectors], + combiner: (...args: SelectorResultArrayWithAdditionalArgs) => Result, + ] + // Input selectors as separate inline arguments + | [...Selectors, (...args: SelectorResultArrayWithAdditionalArgs) => Result]; + +type CreateSelectorFunction = >, Args, Result>( + ...items: SelectorArgs +) => OutputSelectorV8, Args, Result>; + +const cache = new WeakMap>(); + +function checkIsAPIRef(value: any) { + return 'current' in value && 'instanceId' in value.current; +} + +const DEFAULT_INSTANCE_ID = { id: 'default' }; + +export const createSelectorV8 = (( + a: Function, + b: Function, + c?: Function, + d?: Function, + e?: Function, + f?: Function, + ...other: any[] +) => { + if (other.length > 0) { + throw new Error('Unsupported number of selectors'); + } + + let selector: any; + + if (a && b && c && d && e && f) { + selector = (stateOrApiRef: any, args: any, instanceIdParam: any) => { + const isAPIRef = checkIsAPIRef(stateOrApiRef); + const instanceId = + instanceIdParam ?? (isAPIRef ? stateOrApiRef.current.instanceId : DEFAULT_INSTANCE_ID); + const state = isAPIRef ? stateOrApiRef.current.state : stateOrApiRef; + const va = a(state, args, instanceId); + const vb = b(state, args, instanceId); + const vc = c(state, args, instanceId); + const vd = d(state, args, instanceId); + const ve = e(state, args, instanceId); + return f(va, vb, vc, vd, ve, args); + }; + } else if (a && b && c && d && e) { + selector = (stateOrApiRef: any, args: any, instanceIdParam: any) => { + const isAPIRef = checkIsAPIRef(stateOrApiRef); + const instanceId = + instanceIdParam ?? (isAPIRef ? stateOrApiRef.current.instanceId : DEFAULT_INSTANCE_ID); + const state = isAPIRef ? stateOrApiRef.current.state : stateOrApiRef; + const va = a(state, args, instanceId); + const vb = b(state, args, instanceId); + const vc = c(state, args, instanceId); + const vd = d(state, args, instanceId); + return e(va, vb, vc, vd, args); + }; + } else if (a && b && c && d) { + selector = (stateOrApiRef: any, args: any, instanceIdParam: any) => { + const isAPIRef = checkIsAPIRef(stateOrApiRef); + const instanceId = + instanceIdParam ?? (isAPIRef ? stateOrApiRef.current.instanceId : DEFAULT_INSTANCE_ID); + const state = isAPIRef ? stateOrApiRef.current.state : stateOrApiRef; + const va = a(state, args, instanceId); + const vb = b(state, args, instanceId); + const vc = c(state, args, instanceId); + return d(va, vb, vc, args); + }; + } else if (a && b && c) { + selector = (stateOrApiRef: any, args: any, instanceIdParam: any) => { + const isAPIRef = checkIsAPIRef(stateOrApiRef); + const instanceId = + instanceIdParam ?? (isAPIRef ? stateOrApiRef.current.instanceId : DEFAULT_INSTANCE_ID); + const state = isAPIRef ? stateOrApiRef.current.state : stateOrApiRef; + const va = a(state, args, instanceId); + const vb = b(state, args, instanceId); + return c(va, vb, args); + }; + } else if (a && b) { + selector = (stateOrApiRef: any, args: any, instanceIdParam: any) => { + const isAPIRef = checkIsAPIRef(stateOrApiRef); + const instanceId = + instanceIdParam ?? (isAPIRef ? stateOrApiRef.current.instanceId : DEFAULT_INSTANCE_ID); + const state = isAPIRef ? stateOrApiRef.current.state : stateOrApiRef; + const va = a(state, args, instanceId); + return b(va, args); + }; + } else { + throw new Error('Missing arguments'); + } + + // We use this property to detect if the selector was created with createSelector + // or it's only a simple function the receives the state and returns part of it. + selector.acceptsApiRef = true; + + return selector; +}) as unknown as CreateSelectorFunction; + +export const createSelectorMemoizedV8: CreateSelectorFunction = (...args: any) => { + const selector = (stateOrApiRef: any, selectorArgs: any, instanceId?: any) => { + const isAPIRef = checkIsAPIRef(stateOrApiRef); + const cacheKey = isAPIRef + ? stateOrApiRef.current.instanceId + : (instanceId ?? DEFAULT_INSTANCE_ID); + const state = isAPIRef ? stateOrApiRef.current.state : stateOrApiRef; + + if (process.env.NODE_ENV !== 'production') { + if (cacheKey.id === 'default') { + warnOnce([ + 'MUI X: A selector was called without passing the instance ID, which may impact the performance of the grid.', + 'To fix, call it with `apiRef`, for example `mySelector(apiRef)`, or pass the instance ID explicitly, for example `mySelector(state, apiRef.current.instanceId)`.', + ]); + } + } + + const cacheArgsInit = cache.get(cacheKey); + const cacheArgs = cacheArgsInit ?? new Map(); + const cacheFn = cacheArgs?.get(args); + + if (cacheArgs && cacheFn) { + // We pass the cache key because the called selector might have as + // dependency another selector created with this `createSelector`. + return cacheFn(state, selectorArgs, cacheKey); + } + + const fn = reselectCreateSelector(...args); + + if (!cacheArgsInit) { + cache.set(cacheKey, cacheArgs); + } + cacheArgs.set(args, fn); + + return fn(state, selectorArgs, cacheKey); + }; + + // We use this property to detect if the selector was created with createSelector + // or it's only a simple function the receives the state and returns part of it. + selector.acceptsApiRef = true; + + return selector; +}; From a9874181f45fb96160f6a196067efa6fe9966dd2 Mon Sep 17 00:00:00 2001 From: Bilal Shafi Date: Sat, 17 Aug 2024 16:31:45 +0500 Subject: [PATCH 25/45] Revert 8e5238ecfcacf2489899426aa46c19aa36c196ae --- .../GridCellCheckboxRenderer.tsx | 9 +- .../src/hooks/features/rowSelection/utils.ts | 134 ++++++------- .../src/hooks/utils/useGridSelectorV8.ts | 83 -------- .../x-data-grid/src/utils/createSelectorV8.ts | 181 ------------------ 4 files changed, 72 insertions(+), 335 deletions(-) delete mode 100644 packages/x-data-grid/src/hooks/utils/useGridSelectorV8.ts delete mode 100644 packages/x-data-grid/src/utils/createSelectorV8.ts diff --git a/packages/x-data-grid/src/components/columnSelection/GridCellCheckboxRenderer.tsx b/packages/x-data-grid/src/components/columnSelection/GridCellCheckboxRenderer.tsx index 5bbb0cf4322e..3d8284edde95 100644 --- a/packages/x-data-grid/src/components/columnSelection/GridCellCheckboxRenderer.tsx +++ b/packages/x-data-grid/src/components/columnSelection/GridCellCheckboxRenderer.tsx @@ -10,8 +10,8 @@ import { useGridRootProps } from '../../hooks/utils/useGridRootProps'; import { getDataGridUtilityClass } from '../../constants/gridClasses'; import type { DataGridProcessedProps } from '../../models/props/DataGridProps'; import type { GridRowSelectionCheckboxParams } from '../../models/params/gridRowSelectionCheckboxParams'; -import { useGridSelectorV8 } from '../../hooks/utils/useGridSelectorV8'; -import { gridSomeChildrenSelectedSelector } from '../../hooks/features/rowSelection/utils'; +import { useGridSelector } from '../../hooks/utils/useGridSelector'; +import { getGridSomeChildrenSelectedSelector } from '../../hooks/features/rowSelection/utils'; type OwnerState = { classes: DataGridProcessedProps['classes'] }; @@ -52,9 +52,8 @@ const GridCellCheckboxForwardRef = React.forwardRef(null); - const someChildrenSelected = useGridSelectorV8(apiRef, gridSomeChildrenSelectedSelector, { - groupId: id, - }); + const someChildrenSelectedSelector = getGridSomeChildrenSelectedSelector(id); + const someChildrenSelected = useGridSelector(apiRef, someChildrenSelectedSelector); const rippleRef = React.useRef(null); const handleRef = useForkRef(checkboxElement, ref); diff --git a/packages/x-data-grid/src/hooks/features/rowSelection/utils.ts b/packages/x-data-grid/src/hooks/features/rowSelection/utils.ts index ab8260bf71a7..ed72f8abf1d9 100644 --- a/packages/x-data-grid/src/hooks/features/rowSelection/utils.ts +++ b/packages/x-data-grid/src/hooks/features/rowSelection/utils.ts @@ -2,72 +2,75 @@ import type { DataGridProcessedProps } from '../../../models/props/DataGridProps import { GridSignature } from '../../utils/useGridApiEventHandler'; import { GRID_ROOT_GROUP_ID } from '../rows/gridRowsUtils'; import type { GridGroupNode, GridRowId, GridRowTreeConfig } from '../../../models/gridRows'; -import type { GridPrivateApiCommunity } from '../../../models/api/gridApiCommunity'; +import type { + GridPrivateApiCommunity, + GridApiCommunity, +} from '../../../models/api/gridApiCommunity'; import { gridFilteredRowsLookupSelector } from '../filter/gridFilterSelector'; import { gridSortedRowIdsSelector } from '../sorting/gridSortingSelector'; import { selectedIdsLookupSelector } from './gridRowSelectionSelector'; import { gridRowTreeSelector } from '../rows/gridRowsSelector'; -import { createSelectorV8 } from '../../../utils/createSelectorV8'; - -export const gridRowGroupSelectableChildrenSelector = createSelectorV8( - gridRowTreeSelector, - gridSortedRowIdsSelector, - gridFilteredRowsLookupSelector, - (rowTree, sortedRowIds, filteredRowsLookup, args) => { - const children = new Set(); - const { groupId, apiRef } = args; - if (groupId === undefined || apiRef === undefined) { - return children; - } - const groupNode = rowTree[groupId]; - if (!groupNode || groupNode.type !== 'group') { +import { createSelector } from '../../../utils/createSelector'; + +function getGridRowGroupSelectableChildrenSelector( + apiRef: React.MutableRefObject, + groupId: GridRowId, +) { + return createSelector( + gridRowTreeSelector, + gridSortedRowIdsSelector, + gridFilteredRowsLookupSelector, + (rowTree, sortedRowIds, filteredRowsLookup) => { + const groupNode = rowTree[groupId]; + if (!groupNode || groupNode.type !== 'group') { + return []; + } + + const children: GridRowId[] = []; + + const startIndex = sortedRowIds.findIndex((id) => id === groupId) + 1; + for ( + let index = startIndex; + index < sortedRowIds.length && rowTree[sortedRowIds[index]]?.depth > groupNode.depth; + index += 1 + ) { + const id = sortedRowIds[index]; + if (filteredRowsLookup[id] !== false && apiRef.current.isRowSelectable(id)) { + children.push(id); + } + } return children; - } + }, + ); +} - const startIndex = sortedRowIds.findIndex((id) => id === groupId) + 1; - for ( - let index = startIndex; - index < sortedRowIds.length && rowTree[sortedRowIds[index]]?.depth > groupNode.depth; - index += 1 - ) { - const id = sortedRowIds[index]; - if (filteredRowsLookup[id] !== false && apiRef.current.isRowSelectable(id)) { - children.add(id); +export function getGridSomeChildrenSelectedSelector(groupId: GridRowId) { + return createSelector( + gridRowTreeSelector, + gridSortedRowIdsSelector, + gridFilteredRowsLookupSelector, + selectedIdsLookupSelector, + (rowTree, sortedRowIds, filteredRowsLookup, rowSelectionLookup) => { + const groupNode = rowTree[groupId]; + if (!groupNode || groupNode.type !== 'group') { + return false; } - } - return children; - }, -); - -export const gridSomeChildrenSelectedSelector = createSelectorV8( - gridRowTreeSelector, - gridSortedRowIdsSelector, - gridFilteredRowsLookupSelector, - selectedIdsLookupSelector, - (rowTree, sortedRowIds, filteredRowsLookup, rowSelectionLookup, args) => { - const groupId = args.groupId; - if (groupId === undefined) { - return false; - } - const groupNode = rowTree[groupId]; - if (!groupNode || groupNode.type !== 'group') { - return false; - } - const startIndex = sortedRowIds.findIndex((id) => id === groupId) + 1; - for ( - let index = startIndex; - index < sortedRowIds.length && rowTree[sortedRowIds[index]]?.depth > groupNode.depth; - index += 1 - ) { - const id = sortedRowIds[index]; - if (filteredRowsLookup[id] !== false && rowSelectionLookup[id] !== undefined) { - return true; + const startIndex = sortedRowIds.findIndex((id) => id === groupId) + 1; + for ( + let index = startIndex; + index < sortedRowIds.length && rowTree[sortedRowIds[index]]?.depth > groupNode.depth; + index += 1 + ) { + const id = sortedRowIds[index]; + if (filteredRowsLookup[id] !== false && rowSelectionLookup[id] !== undefined) { + return true; + } } - } - return false; - }, -); + return false; + }, + ); +} export function isMultipleRowSelectionEnabled( props: Pick< @@ -146,11 +149,9 @@ export const findRowsToSelect = ( const rowNode = apiRef.current.getRowNode(selectedRow); if (rowNode?.type === 'group') { - const children = gridRowGroupSelectableChildrenSelector(apiRef, { - groupId: selectedRow, - apiRef, - }); - return rowsToSelect.concat(Array.from(children)); + const rowGroupChildrenSelector = getGridRowGroupSelectableChildrenSelector(apiRef, selectedRow); + const children = rowGroupChildrenSelector(apiRef); + return rowsToSelect.concat(children); } return rowsToSelect; }; @@ -172,11 +173,12 @@ export const findRowsToDeselect = ( const rowNode = apiRef.current.getRowNode(deselectedRow); if (rowNode?.type === 'group') { - const children = gridRowGroupSelectableChildrenSelector(apiRef, { - groupId: deselectedRow, + const rowGroupChildrenSelector = getGridRowGroupSelectableChildrenSelector( apiRef, - }); - return rowsToDeselect.concat(Array.from(children)); + deselectedRow, + ); + const children = rowGroupChildrenSelector(apiRef); + return rowsToDeselect.concat(children); } return rowsToDeselect; }; diff --git a/packages/x-data-grid/src/hooks/utils/useGridSelectorV8.ts b/packages/x-data-grid/src/hooks/utils/useGridSelectorV8.ts deleted file mode 100644 index 7b7b8a7ee81e..000000000000 --- a/packages/x-data-grid/src/hooks/utils/useGridSelectorV8.ts +++ /dev/null @@ -1,83 +0,0 @@ -import * as React from 'react'; -import type { GridApiCommon } from '../../models/api/gridApiCommon'; -import { OutputSelectorV8 } from '../../utils/createSelectorV8'; -import { useLazyRef } from './useLazyRef'; -import { useOnMount } from './useOnMount'; -import { warnOnce } from '../../internals/utils/warning'; -import type { GridCoreApi } from '../../models/api/gridCoreApi'; -import { fastObjectShallowCompare } from '../../utils/fastObjectShallowCompare'; - -function isOutputSelector( - selector: any, -): selector is OutputSelectorV8 { - return selector.acceptsApiRef; -} - -function applySelectorV8( - apiRef: React.MutableRefObject, - selector: ((state: Api['state']) => T) | OutputSelectorV8, - args: Args, - instanceId: GridCoreApi['instanceId'], -) { - if (isOutputSelector(selector)) { - return selector(apiRef, args); - } - return selector(apiRef.current.state, instanceId); -} - -const defaultCompare = Object.is; -export const objectShallowCompare = fastObjectShallowCompare; - -const createRefs = () => ({ state: null, equals: null, selector: null }) as any; - -export const useGridSelectorV8 = ( - apiRef: React.MutableRefObject, - selector: ((state: Api['state']) => T) | OutputSelectorV8, - args: Args = {} as Args, - equals: (a: T, b: T) => boolean = defaultCompare, -) => { - if (process.env.NODE_ENV !== 'production') { - if (!apiRef.current.state) { - warnOnce([ - 'MUI X: `useGridSelector` has been called before the initialization of the state.', - 'This hook can only be used inside the context of the grid.', - ]); - } - } - - const refs = useLazyRef< - { - state: T; - equals: typeof equals; - selector: typeof selector; - }, - never - >(createRefs); - const didInit = refs.current.selector !== null; - - const [state, setState] = React.useState( - // We don't use an initialization function to avoid allocations - (didInit ? null : applySelectorV8(apiRef, selector, args, apiRef.current.instanceId)) as T, - ); - - refs.current.state = state; - refs.current.equals = equals; - refs.current.selector = selector; - - useOnMount(() => { - return apiRef.current.store.subscribe(() => { - const newState = applySelectorV8( - apiRef, - refs.current.selector, - args, - apiRef.current.instanceId, - ) as T; - if (!refs.current.equals(refs.current.state, newState)) { - refs.current.state = newState; - setState(newState); - } - }); - }); - - return state; -}; diff --git a/packages/x-data-grid/src/utils/createSelectorV8.ts b/packages/x-data-grid/src/utils/createSelectorV8.ts deleted file mode 100644 index b64c12c3c354..000000000000 --- a/packages/x-data-grid/src/utils/createSelectorV8.ts +++ /dev/null @@ -1,181 +0,0 @@ -import * as React from 'react'; -import { createSelector as reselectCreateSelector, Selector, SelectorResultArray } from 'reselect'; -import type { GridCoreApi } from '../models/api/gridCoreApi'; -import { warnOnce } from '../internals/utils/warning'; - -type CacheKey = { id: number }; - -export interface OutputSelectorV8 { - ( - apiRef: React.MutableRefObject<{ state: State; instanceId: GridCoreApi['instanceId'] }>, - args: Args, - ): Result; - (state: State, instanceId: GridCoreApi['instanceId']): Result; - acceptsApiRef: boolean; -} - -type StateFromSelector = T extends (first: infer F, ...args: any[]) => any - ? F extends { state: infer F2 } - ? F2 - : F - : never; - -type StateFromSelectorList = Selectors extends [ - f: infer F, - ...other: infer R, -] - ? StateFromSelector extends StateFromSelectorList - ? StateFromSelector - : StateFromSelectorList - : {}; - -type SelectorResultArrayWithAdditionalArgs>> = [ - ...SelectorResultArray, - Record, -]; - -type SelectorArgs>, Result> = - // Input selectors as a separate array - | [ - selectors: [...Selectors], - combiner: (...args: SelectorResultArrayWithAdditionalArgs) => Result, - ] - // Input selectors as separate inline arguments - | [...Selectors, (...args: SelectorResultArrayWithAdditionalArgs) => Result]; - -type CreateSelectorFunction = >, Args, Result>( - ...items: SelectorArgs -) => OutputSelectorV8, Args, Result>; - -const cache = new WeakMap>(); - -function checkIsAPIRef(value: any) { - return 'current' in value && 'instanceId' in value.current; -} - -const DEFAULT_INSTANCE_ID = { id: 'default' }; - -export const createSelectorV8 = (( - a: Function, - b: Function, - c?: Function, - d?: Function, - e?: Function, - f?: Function, - ...other: any[] -) => { - if (other.length > 0) { - throw new Error('Unsupported number of selectors'); - } - - let selector: any; - - if (a && b && c && d && e && f) { - selector = (stateOrApiRef: any, args: any, instanceIdParam: any) => { - const isAPIRef = checkIsAPIRef(stateOrApiRef); - const instanceId = - instanceIdParam ?? (isAPIRef ? stateOrApiRef.current.instanceId : DEFAULT_INSTANCE_ID); - const state = isAPIRef ? stateOrApiRef.current.state : stateOrApiRef; - const va = a(state, args, instanceId); - const vb = b(state, args, instanceId); - const vc = c(state, args, instanceId); - const vd = d(state, args, instanceId); - const ve = e(state, args, instanceId); - return f(va, vb, vc, vd, ve, args); - }; - } else if (a && b && c && d && e) { - selector = (stateOrApiRef: any, args: any, instanceIdParam: any) => { - const isAPIRef = checkIsAPIRef(stateOrApiRef); - const instanceId = - instanceIdParam ?? (isAPIRef ? stateOrApiRef.current.instanceId : DEFAULT_INSTANCE_ID); - const state = isAPIRef ? stateOrApiRef.current.state : stateOrApiRef; - const va = a(state, args, instanceId); - const vb = b(state, args, instanceId); - const vc = c(state, args, instanceId); - const vd = d(state, args, instanceId); - return e(va, vb, vc, vd, args); - }; - } else if (a && b && c && d) { - selector = (stateOrApiRef: any, args: any, instanceIdParam: any) => { - const isAPIRef = checkIsAPIRef(stateOrApiRef); - const instanceId = - instanceIdParam ?? (isAPIRef ? stateOrApiRef.current.instanceId : DEFAULT_INSTANCE_ID); - const state = isAPIRef ? stateOrApiRef.current.state : stateOrApiRef; - const va = a(state, args, instanceId); - const vb = b(state, args, instanceId); - const vc = c(state, args, instanceId); - return d(va, vb, vc, args); - }; - } else if (a && b && c) { - selector = (stateOrApiRef: any, args: any, instanceIdParam: any) => { - const isAPIRef = checkIsAPIRef(stateOrApiRef); - const instanceId = - instanceIdParam ?? (isAPIRef ? stateOrApiRef.current.instanceId : DEFAULT_INSTANCE_ID); - const state = isAPIRef ? stateOrApiRef.current.state : stateOrApiRef; - const va = a(state, args, instanceId); - const vb = b(state, args, instanceId); - return c(va, vb, args); - }; - } else if (a && b) { - selector = (stateOrApiRef: any, args: any, instanceIdParam: any) => { - const isAPIRef = checkIsAPIRef(stateOrApiRef); - const instanceId = - instanceIdParam ?? (isAPIRef ? stateOrApiRef.current.instanceId : DEFAULT_INSTANCE_ID); - const state = isAPIRef ? stateOrApiRef.current.state : stateOrApiRef; - const va = a(state, args, instanceId); - return b(va, args); - }; - } else { - throw new Error('Missing arguments'); - } - - // We use this property to detect if the selector was created with createSelector - // or it's only a simple function the receives the state and returns part of it. - selector.acceptsApiRef = true; - - return selector; -}) as unknown as CreateSelectorFunction; - -export const createSelectorMemoizedV8: CreateSelectorFunction = (...args: any) => { - const selector = (stateOrApiRef: any, selectorArgs: any, instanceId?: any) => { - const isAPIRef = checkIsAPIRef(stateOrApiRef); - const cacheKey = isAPIRef - ? stateOrApiRef.current.instanceId - : (instanceId ?? DEFAULT_INSTANCE_ID); - const state = isAPIRef ? stateOrApiRef.current.state : stateOrApiRef; - - if (process.env.NODE_ENV !== 'production') { - if (cacheKey.id === 'default') { - warnOnce([ - 'MUI X: A selector was called without passing the instance ID, which may impact the performance of the grid.', - 'To fix, call it with `apiRef`, for example `mySelector(apiRef)`, or pass the instance ID explicitly, for example `mySelector(state, apiRef.current.instanceId)`.', - ]); - } - } - - const cacheArgsInit = cache.get(cacheKey); - const cacheArgs = cacheArgsInit ?? new Map(); - const cacheFn = cacheArgs?.get(args); - - if (cacheArgs && cacheFn) { - // We pass the cache key because the called selector might have as - // dependency another selector created with this `createSelector`. - return cacheFn(state, selectorArgs, cacheKey); - } - - const fn = reselectCreateSelector(...args); - - if (!cacheArgsInit) { - cache.set(cacheKey, cacheArgs); - } - cacheArgs.set(args, fn); - - return fn(state, selectorArgs, cacheKey); - }; - - // We use this property to detect if the selector was created with createSelector - // or it's only a simple function the receives the state and returns part of it. - selector.acceptsApiRef = true; - - return selector; -}; From 1cc312d1e6575383683161c3efb5fdf9540d993a Mon Sep 17 00:00:00 2001 From: Bilal Shafi Date: Sat, 17 Aug 2024 19:04:00 +0500 Subject: [PATCH 26/45] Revert "Make select all checkbox in indeterminate state select all the rows" This reverts commit 49ed4e071d777c643c3986449d3b5b4ebf3b216e. --- .../src/tests/rowSelection.DataGridPro.test.tsx | 10 +++++----- .../components/columnSelection/GridHeaderCheckbox.tsx | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/x-data-grid-pro/src/tests/rowSelection.DataGridPro.test.tsx b/packages/x-data-grid-pro/src/tests/rowSelection.DataGridPro.test.tsx index 4b0540943dfd..3f894f842e3f 100644 --- a/packages/x-data-grid-pro/src/tests/rowSelection.DataGridPro.test.tsx +++ b/packages/x-data-grid-pro/src/tests/rowSelection.DataGridPro.test.tsx @@ -80,8 +80,8 @@ describe(' - Row selection', () => { name: /select all rows/i, }); fireEvent.click(selectAllCheckbox); - expect(apiRef.current.getSelectedRows()).to.have.length(4); - expect(selectAllCheckbox.checked).to.equal(true); + expect(apiRef.current.getSelectedRows()).to.have.length(0); + expect(selectAllCheckbox.checked).to.equal(false); }); it('should select all visible rows if pagination is not enabled', () => { @@ -170,7 +170,7 @@ describe(' - Row selection', () => { expect(selectAllCheckbox.checked).to.equal(true); }); - it('should select all the rows of the current page if 1 row of the current page is selected', () => { + it('should unselect all the rows of the current page if 1 row of the current page is selected', () => { render( - Row selection', () => { name: /select all rows/i, }); fireEvent.click(selectAllCheckbox); - expect(apiRef.current.getSelectedRows()).to.have.keys([0, 2, 3]); - expect(selectAllCheckbox.checked).to.equal(true); + expect(apiRef.current.getSelectedRows()).to.have.keys([0]); + expect(selectAllCheckbox.checked).to.equal(false); }); it('should not set the header checkbox in a indeterminate state when some rows of other pages are not selected', () => { diff --git a/packages/x-data-grid/src/components/columnSelection/GridHeaderCheckbox.tsx b/packages/x-data-grid/src/components/columnSelection/GridHeaderCheckbox.tsx index 8e09bfca7b73..71e12c5db38b 100644 --- a/packages/x-data-grid/src/components/columnSelection/GridHeaderCheckbox.tsx +++ b/packages/x-data-grid/src/components/columnSelection/GridHeaderCheckbox.tsx @@ -133,7 +133,7 @@ const GridHeaderCheckbox = React.forwardRef Date: Wed, 18 Sep 2024 23:55:21 +0500 Subject: [PATCH 27/45] propagateRowSelection -> rowSelectionPropagation --- .../RowGroupingPropagateSelection.js | 36 ++++++-- .../RowGroupingPropagateSelection.tsx | 37 ++++++-- .../RowGroupingPropagateSelection.tsx.preview | 7 -- .../data-grid/row-grouping/row-grouping.md | 25 ++++-- .../x/api/data-grid/data-grid-premium.json | 8 +- docs/pages/x/api/data-grid/data-grid-pro.json | 8 +- .../x/api/data-grid/grid-actions-col-def.json | 3 +- docs/pages/x/api/data-grid/grid-col-def.json | 3 +- .../data-grid/grid-single-select-col-def.json | 3 +- .../data-grid-premium/data-grid-premium.json | 6 +- .../data-grid-pro/data-grid-pro.json | 6 +- .../src/DataGridPremium/DataGridPremium.tsx | 21 ++--- .../src/DataGridPro/DataGridPro.tsx | 21 ++--- .../src/DataGridPro/useDataGridProProps.ts | 2 +- .../features/filter/gridFilterSelector.ts | 1 - .../rowSelection/useGridRowSelection.ts | 75 +++++++++++----- .../src/hooks/features/rowSelection/utils.ts | 90 ++++++++++++------- .../src/models/gridRowSelectionModel.ts | 2 + .../src/models/props/DataGridProps.ts | 16 ++-- scripts/x-data-grid-premium.exports.json | 2 +- scripts/x-data-grid-pro.exports.json | 2 +- scripts/x-data-grid.exports.json | 2 +- 22 files changed, 243 insertions(+), 133 deletions(-) delete mode 100644 docs/data/data-grid/row-grouping/RowGroupingPropagateSelection.tsx.preview diff --git a/docs/data/data-grid/row-grouping/RowGroupingPropagateSelection.js b/docs/data/data-grid/row-grouping/RowGroupingPropagateSelection.js index 30d590c9d4c5..6da2dd356e12 100644 --- a/docs/data/data-grid/row-grouping/RowGroupingPropagateSelection.js +++ b/docs/data/data-grid/row-grouping/RowGroupingPropagateSelection.js @@ -5,10 +5,15 @@ import { useKeepGroupedColumnsHidden, } from '@mui/x-data-grid-premium'; import { useMovieData } from '@mui/x-data-grid-generator'; +import FormControl from '@mui/material/FormControl'; +import InputLabel from '@mui/material/InputLabel'; +import MenuItem from '@mui/material/MenuItem'; +import Select from '@mui/material/Select'; export default function RowGroupingPropagateSelection() { const data = useMovieData(); const apiRef = useGridApiRef(); + const [value, setValue] = React.useState('both'); const initialState = useKeepGroupedColumnsHidden({ apiRef, @@ -19,15 +24,30 @@ export default function RowGroupingPropagateSelection() { }, }); + const handleValueChange = React.useCallback((event) => { + setValue(event.target.value); + }, []); + return ( -
- +
+ + Propagation behavior + + +
+ +
); } diff --git a/docs/data/data-grid/row-grouping/RowGroupingPropagateSelection.tsx b/docs/data/data-grid/row-grouping/RowGroupingPropagateSelection.tsx index 30d590c9d4c5..2410f52fea2b 100644 --- a/docs/data/data-grid/row-grouping/RowGroupingPropagateSelection.tsx +++ b/docs/data/data-grid/row-grouping/RowGroupingPropagateSelection.tsx @@ -3,12 +3,18 @@ import { DataGridPremium, useGridApiRef, useKeepGroupedColumnsHidden, + GridRowSelectionPropagation, } from '@mui/x-data-grid-premium'; import { useMovieData } from '@mui/x-data-grid-generator'; +import FormControl from '@mui/material/FormControl'; +import InputLabel from '@mui/material/InputLabel'; +import MenuItem from '@mui/material/MenuItem'; +import Select, { SelectChangeEvent } from '@mui/material/Select'; export default function RowGroupingPropagateSelection() { const data = useMovieData(); const apiRef = useGridApiRef(); + const [value, setValue] = React.useState('both'); const initialState = useKeepGroupedColumnsHidden({ apiRef, @@ -19,15 +25,30 @@ export default function RowGroupingPropagateSelection() { }, }); + const handleValueChange = React.useCallback((event: SelectChangeEvent) => { + setValue(event.target.value as GridRowSelectionPropagation); + }, []); + return ( -
- +
+ + Propagation behavior + + +
+ +
); } diff --git a/docs/data/data-grid/row-grouping/RowGroupingPropagateSelection.tsx.preview b/docs/data/data-grid/row-grouping/RowGroupingPropagateSelection.tsx.preview deleted file mode 100644 index 761de773b092..000000000000 --- a/docs/data/data-grid/row-grouping/RowGroupingPropagateSelection.tsx.preview +++ /dev/null @@ -1,7 +0,0 @@ - \ No newline at end of file diff --git a/docs/data/data-grid/row-grouping/row-grouping.md b/docs/data/data-grid/row-grouping/row-grouping.md index d10d197c4cd6..c6c3b17d526c 100644 --- a/docs/data/data-grid/row-grouping/row-grouping.md +++ b/docs/data/data-grid/row-grouping/row-grouping.md @@ -308,19 +308,28 @@ If you are dynamically switching the `leafField` or `mainGroupingCriteria`, the ## Automatic parents and children selection By default, selecting a parent row will not select its children. -Set the `propagateRowSelection` prop to `true` to achieve the following behavior. +You can override this behavior by setting using the `rowSelectionPropagation` prop. -1. Selecting/deselecting a parent row would select/deselect all the children rows. -2. When all the child rows are selected, the parent row will be auto selected. -3. When a child row is deselected, if one or more parent rows are already selected, they will be moved to an indeterminate state. -4. "Select All" checkbox in the header row would select/deselect all the rows including child rows. -5. After applying the filtering, the previously selected rows which are not in the newly filtered rows will be auto deselected. +It has four possible values, `'none'` (current behavior), `'parents'`, `'children'`, and `'both'`. The following table shows the behavior of each value on user actions "Select" and "Deselect": + +| Value | Select | Deselect | +| :----------- | :----------------------------------------------------------------------- | :----------------------------------------------------------------- | +| `'none'` | Only select the clicked row | Only deselect the clicked row | +| `'parents'` | Auto select parents whose all the descendants are selected | Auto deselect parents whose atleast one descendant is not selected | +| `'children'` | Selecting a parent row will automatically select all the descendant rows | Deselect all the descendant rows on deselecting a parent | +| `'both'` | Both `parent` and `children` behaviors | Both `parent` and `children` behaviors | + +In the following example, you can test the different values of the `rowSelectionPropagation` prop. {{"demo": "RowGroupingPropagateSelection.js", "bg": "inline", "defaultCodeOpen": false}} :::info -When the row selection propagation feature is enabled, only the filtered rows will be kept selected. -If some rows were selected before filtering, they will be auto deselected if they are not among the newly filtered rows. +The row selection propagation feature will only affect the filtered rows. +If some rows were selected before filtering, auto selection will not be applied on them. +::: + +:::warning +If `props.disableMultipleRowSelection` is set to `true`, the row selection propagation feature will work like `'none'`. ::: :::warning diff --git a/docs/pages/x/api/data-grid/data-grid-premium.json b/docs/pages/x/api/data-grid/data-grid-premium.json index 791c368c9306..afa7231fd9f2 100644 --- a/docs/pages/x/api/data-grid/data-grid-premium.json +++ b/docs/pages/x/api/data-grid/data-grid-premium.json @@ -565,7 +565,6 @@ "returned": "Promise | R" } }, - "propagateRowSelection": { "type": { "name": "bool" }, "default": "false" }, "resizeThrottleMs": { "type": { "name": "number" }, "default": "60" }, "rowBufferPx": { "type": { "name": "number" }, "default": "150" }, "rowCount": { "type": { "name": "number" } }, @@ -589,6 +588,13 @@ "description": "Array<number
| string>
| number
| string" } }, + "rowSelectionPropagation": { + "type": { + "name": "enum", + "description": "'both'
| 'children'
| 'none'
| 'parents'" + }, + "default": "'none'" + }, "rowsLoadingMode": { "type": { "name": "enum", "description": "'client'
| 'server'" } }, diff --git a/docs/pages/x/api/data-grid/data-grid-pro.json b/docs/pages/x/api/data-grid/data-grid-pro.json index 0abb7a9adbf6..6a07a59752b7 100644 --- a/docs/pages/x/api/data-grid/data-grid-pro.json +++ b/docs/pages/x/api/data-grid/data-grid-pro.json @@ -505,7 +505,6 @@ "returned": "Promise | R" } }, - "propagateRowSelection": { "type": { "name": "bool" }, "default": "false" }, "resizeThrottleMs": { "type": { "name": "number" }, "default": "60" }, "rowBufferPx": { "type": { "name": "number" }, "default": "150" }, "rowCount": { "type": { "name": "number" } }, @@ -524,6 +523,13 @@ "description": "Array<number
| string>
| number
| string" } }, + "rowSelectionPropagation": { + "type": { + "name": "enum", + "description": "'both'
| 'children'
| 'none'
| 'parents'" + }, + "default": "'none'" + }, "rowsLoadingMode": { "type": { "name": "enum", "description": "'client'
| 'server'" } }, diff --git a/docs/pages/x/api/data-grid/grid-actions-col-def.json b/docs/pages/x/api/data-grid/grid-actions-col-def.json index 26d5acce67f3..ecf251480e1a 100644 --- a/docs/pages/x/api/data-grid/grid-actions-col-def.json +++ b/docs/pages/x/api/data-grid/grid-actions-col-def.json @@ -85,8 +85,7 @@ } }, "renderHeaderFilter": { - "type": { "description": "(params: GridRenderHeaderFilterProps) => React.ReactNode" }, - "isProPlan": true + "type": { "description": "(params: GridRenderHeaderFilterProps) => React.ReactNode" } }, "resizable": { "type": { "description": "boolean" }, "default": "true" }, "sortable": { "type": { "description": "boolean" }, "default": "true" }, diff --git a/docs/pages/x/api/data-grid/grid-col-def.json b/docs/pages/x/api/data-grid/grid-col-def.json index bfe97f8a9704..2e8eed38928b 100644 --- a/docs/pages/x/api/data-grid/grid-col-def.json +++ b/docs/pages/x/api/data-grid/grid-col-def.json @@ -78,8 +78,7 @@ } }, "renderHeaderFilter": { - "type": { "description": "(params: GridRenderHeaderFilterProps) => React.ReactNode" }, - "isProPlan": true + "type": { "description": "(params: GridRenderHeaderFilterProps) => React.ReactNode" } }, "resizable": { "type": { "description": "boolean" }, "default": "true" }, "sortable": { "type": { "description": "boolean" }, "default": "true" }, diff --git a/docs/pages/x/api/data-grid/grid-single-select-col-def.json b/docs/pages/x/api/data-grid/grid-single-select-col-def.json index 669318bc4848..da00444723e9 100644 --- a/docs/pages/x/api/data-grid/grid-single-select-col-def.json +++ b/docs/pages/x/api/data-grid/grid-single-select-col-def.json @@ -85,8 +85,7 @@ } }, "renderHeaderFilter": { - "type": { "description": "(params: GridRenderHeaderFilterProps) => React.ReactNode" }, - "isProPlan": true + "type": { "description": "(params: GridRenderHeaderFilterProps) => React.ReactNode" } }, "resizable": { "type": { "description": "boolean" }, "default": "true" }, "sortable": { "type": { "description": "boolean" }, "default": "true" }, diff --git a/docs/translations/api-docs/data-grid/data-grid-premium/data-grid-premium.json b/docs/translations/api-docs/data-grid/data-grid-premium/data-grid-premium.json index f3afe275dbf5..f9cbefda1d66 100644 --- a/docs/translations/api-docs/data-grid/data-grid-premium/data-grid-premium.json +++ b/docs/translations/api-docs/data-grid/data-grid-premium/data-grid-premium.json @@ -589,9 +589,6 @@ "Promise | R": "The final values to update the row." } }, - "propagateRowSelection": { - "description": "If true, following behavior happens with nested data: 1. Selecting/deselecting a parent row would select/deselect all the children rows. 2. When all the child rows are selected, the parent row will be auto selected. 3. When a child row is deselected, if one or more parent rows are already selected, they will be moved to an indeterminate state. 4. Select All checkbox in the header row would select/deselect all the rows including child rows. Works with tree data and row grouping on the client-side only." - }, "resizeThrottleMs": { "description": "The milliseconds throttle delay for resizing the grid." }, "rowBufferPx": { "description": "Row region in pixels to render before/after the viewport" }, "rowCount": { @@ -610,6 +607,9 @@ "rows": { "description": "Set of rows of type GridRowsProp." }, "rowSelection": { "description": "If false, the row selection mode is disabled." }, "rowSelectionModel": { "description": "Sets the row selection model of the Data Grid." }, + "rowSelectionPropagation": { + "description": "The following behavior happens for each of the possible values: 1. none - No row selection propagation. 2. parents - Selecting all children will auto-select the parent(s). 3. children - Selecting a parent will auto-select all its descendants. 4. both - Both parents and children behavior.
Works with tree data and row grouping on the client-side only." + }, "rowsLoadingMode": { "description": "Loading rows can be processed on the server or client-side. Set it to 'client' if you would like enable infnite loading. Set it to 'server' if you would like to enable lazy loading. * @default "client"" }, diff --git a/docs/translations/api-docs/data-grid/data-grid-pro/data-grid-pro.json b/docs/translations/api-docs/data-grid/data-grid-pro/data-grid-pro.json index 63e241808003..29e846260b7a 100644 --- a/docs/translations/api-docs/data-grid/data-grid-pro/data-grid-pro.json +++ b/docs/translations/api-docs/data-grid/data-grid-pro/data-grid-pro.json @@ -535,9 +535,6 @@ "Promise | R": "The final values to update the row." } }, - "propagateRowSelection": { - "description": "If true, following behavior happens with nested data: 1. Selecting/deselecting a parent row would select/deselect all the children rows. 2. When all the child rows are selected, the parent row will be auto selected. 3. When a child row is deselected, if one or more parent rows are already selected, they will be moved to an indeterminate state. 4. Select All checkbox in the header row would select/deselect all the rows including child rows. Works with tree data and row grouping on the client-side only." - }, "resizeThrottleMs": { "description": "The milliseconds throttle delay for resizing the grid." }, "rowBufferPx": { "description": "Row region in pixels to render before/after the viewport" }, "rowCount": { @@ -552,6 +549,9 @@ "rows": { "description": "Set of rows of type GridRowsProp." }, "rowSelection": { "description": "If false, the row selection mode is disabled." }, "rowSelectionModel": { "description": "Sets the row selection model of the Data Grid." }, + "rowSelectionPropagation": { + "description": "The following behavior happens for each of the possible values: 1. none - No row selection propagation. 2. parents - Selecting all children will auto-select the parent(s). 3. children - Selecting a parent will auto-select all its descendants. 4. both - Both parents and children behavior.
Works with tree data and row grouping on the client-side only." + }, "rowsLoadingMode": { "description": "Loading rows can be processed on the server or client-side. Set it to 'client' if you would like enable infnite loading. Set it to 'server' if you would like to enable lazy loading. * @default "client"" }, diff --git a/packages/x-data-grid-premium/src/DataGridPremium/DataGridPremium.tsx b/packages/x-data-grid-premium/src/DataGridPremium/DataGridPremium.tsx index 7691f8e52491..4ed7a8568a21 100644 --- a/packages/x-data-grid-premium/src/DataGridPremium/DataGridPremium.tsx +++ b/packages/x-data-grid-premium/src/DataGridPremium/DataGridPremium.tsx @@ -910,16 +910,6 @@ DataGridPremiumRaw.propTypes = { * @returns {Promise | R} The final values to update the row. */ processRowUpdate: PropTypes.func, - /** - * If `true`, following behavior happens with nested data: - * 1. Selecting/deselecting a parent row would select/deselect all the children rows. - * 2. When all the child rows are selected, the parent row will be auto selected. - * 3. When a child row is deselected, if one or more parent rows are already selected, they will be moved to an indeterminate state. - * 4. Select All checkbox in the header row would select/deselect all the rows including child rows. - * Works with tree data and row grouping on the client-side only. - * @default false - */ - propagateRowSelection: PropTypes.bool, /** * The milliseconds throttle delay for resizing the grid. * @default 60 @@ -985,6 +975,17 @@ DataGridPremiumRaw.propTypes = { PropTypes.number, PropTypes.string, ]), + /** + * The following behavior happens for each of the possible values: + * 1. `none` - No row selection propagation. + * 2. `parents` - Selecting all children will auto-select the parent(s). + * 3. `children` - Selecting a parent will auto-select all its descendants. + * 4. `both` - Both `parents` and `children` behavior. + * + * Works with tree data and row grouping on the client-side only. + * @default 'none' + */ + rowSelectionPropagation: PropTypes.oneOf(['both', 'children', 'none', 'parents']), /** * Loading rows can be processed on the server or client-side. * Set it to 'client' if you would like enable infnite loading. diff --git a/packages/x-data-grid-pro/src/DataGridPro/DataGridPro.tsx b/packages/x-data-grid-pro/src/DataGridPro/DataGridPro.tsx index 0d038267180b..26d67f196851 100644 --- a/packages/x-data-grid-pro/src/DataGridPro/DataGridPro.tsx +++ b/packages/x-data-grid-pro/src/DataGridPro/DataGridPro.tsx @@ -826,16 +826,6 @@ DataGridProRaw.propTypes = { * @returns {Promise | R} The final values to update the row. */ processRowUpdate: PropTypes.func, - /** - * If `true`, following behavior happens with nested data: - * 1. Selecting/deselecting a parent row would select/deselect all the children rows. - * 2. When all the child rows are selected, the parent row will be auto selected. - * 3. When a child row is deselected, if one or more parent rows are already selected, they will be moved to an indeterminate state. - * 4. Select All checkbox in the header row would select/deselect all the rows including child rows. - * Works with tree data and row grouping on the client-side only. - * @default false - */ - propagateRowSelection: PropTypes.bool, /** * The milliseconds throttle delay for resizing the grid. * @default 60 @@ -891,6 +881,17 @@ DataGridProRaw.propTypes = { PropTypes.number, PropTypes.string, ]), + /** + * The following behavior happens for each of the possible values: + * 1. `none` - No row selection propagation. + * 2. `parents` - Selecting all children will auto-select the parent(s). + * 3. `children` - Selecting a parent will auto-select all its descendants. + * 4. `both` - Both `parents` and `children` behavior. + * + * Works with tree data and row grouping on the client-side only. + * @default 'none' + */ + rowSelectionPropagation: PropTypes.oneOf(['both', 'children', 'none', 'parents']), /** * Loading rows can be processed on the server or client-side. * Set it to 'client' if you would like enable infnite loading. diff --git a/packages/x-data-grid-pro/src/DataGridPro/useDataGridProProps.ts b/packages/x-data-grid-pro/src/DataGridPro/useDataGridProProps.ts index 71ecf9e1b305..32bbcc3175f1 100644 --- a/packages/x-data-grid-pro/src/DataGridPro/useDataGridProProps.ts +++ b/packages/x-data-grid-pro/src/DataGridPro/useDataGridProProps.ts @@ -46,7 +46,7 @@ export const DATA_GRID_PRO_PROPS_DEFAULT_VALUES: DataGridProPropsWithDefaultValu getDetailPanelHeight: () => 500, headerFilters: false, keepColumnPositionIfDraggedOutside: false, - propagateRowSelection: false, + rowSelectionPropagation: 'none', rowReordering: false, rowsLoadingMode: 'client', scrollEndThreshold: 80, diff --git a/packages/x-data-grid/src/hooks/features/filter/gridFilterSelector.ts b/packages/x-data-grid/src/hooks/features/filter/gridFilterSelector.ts index 95500cb21e25..99c00205db46 100644 --- a/packages/x-data-grid/src/hooks/features/filter/gridFilterSelector.ts +++ b/packages/x-data-grid/src/hooks/features/filter/gridFilterSelector.ts @@ -106,7 +106,6 @@ export const gridFilteredSortedRowIdsSelector = createSelectorMemoized( (filteredSortedRowEntries) => filteredSortedRowEntries.map((row) => row.id), ); - /** * Get a `Set` containing the ids of the rows accessible after the filtering process. * Contains the collapsed children. diff --git a/packages/x-data-grid/src/hooks/features/rowSelection/useGridRowSelection.ts b/packages/x-data-grid/src/hooks/features/rowSelection/useGridRowSelection.ts index a3bcd78a6062..0d2094901f02 100644 --- a/packages/x-data-grid/src/hooks/features/rowSelection/useGridRowSelection.ts +++ b/packages/x-data-grid/src/hooks/features/rowSelection/useGridRowSelection.ts @@ -66,6 +66,7 @@ export const rowSelectionStateInitializer: GridStateInitializer< /** * @requires useGridRows (state, method) - can be after * @requires useGridParamsApi (method) - can be after + * @requires useGridParamsApi (method) - can be after * @requires useGridFocus (state) - can be after * @requires useGridKeyboardNavigation (`cellKeyDown` event must first be consumed by it) */ @@ -85,7 +86,7 @@ export const useGridRowSelection = ( | 'classes' | 'keepNonExistentRowsSelected' | 'rowSelection' - | 'propagateRowSelection' + | 'rowSelectionPropagation' | 'signature' >, ): void => { @@ -101,6 +102,9 @@ export const useGridRowSelection = ( [props.rowSelection], ); + const applyRowSelectionPropagation = + !props.disableMultipleRowSelection && (props.rowSelectionPropagation ?? 'none') !== 'none'; + const propRowSelectionModel = React.useMemo(() => { return getSelectionModelPropValue( props.rowSelectionModel, @@ -226,8 +230,13 @@ export const useGridRowSelection = ( const newSelection: GridRowId[] = []; if (isSelected) { newSelection.push(id); - if (props.propagateRowSelection) { - const rowsToSelect = findRowsToSelect(apiRef, tree, id); + if (applyRowSelectionPropagation) { + const rowsToSelect = findRowsToSelect( + apiRef, + tree, + id, + props.rowSelectionPropagation ?? 'none', + ); rowsToSelect.forEach((rowId) => { newSelection.push(rowId); }); @@ -245,12 +254,22 @@ export const useGridRowSelection = ( if (isSelected) { newSelection.add(id); - if (props.propagateRowSelection) { - const rowsToSelect = findRowsToSelect(apiRef, tree, id); + if (applyRowSelectionPropagation) { + const rowsToSelect = findRowsToSelect( + apiRef, + tree, + id, + props.rowSelectionPropagation ?? 'none', + ); rowsToSelect.forEach(newSelection.add, newSelection); } - } else if (props.propagateRowSelection) { - const rowsToDeselect = findRowsToDeselect(apiRef, tree, id); + } else if (applyRowSelectionPropagation) { + const rowsToDeselect = findRowsToDeselect( + apiRef, + tree, + id, + props.rowSelectionPropagation ?? 'none', + ); rowsToDeselect.forEach((parentId) => { newSelection.delete(parentId); }); @@ -262,7 +281,14 @@ export const useGridRowSelection = ( } } }, - [apiRef, logger, canHaveMultipleSelection, tree, props.propagateRowSelection], + [ + apiRef, + logger, + applyRowSelectionPropagation, + tree, + props.rowSelectionPropagation, + canHaveMultipleSelection, + ], ); const selectRows = React.useCallback( @@ -271,13 +297,14 @@ export const useGridRowSelection = ( const selectableIds = ids.filter((id) => apiRef.current.isRowSelectable(id)); + const rowSelectionPropagation = props.rowSelectionPropagation ?? 'none'; let newSelection: GridRowId[]; if (resetSelection) { if (isSelected) { newSelection = selectableIds; - if (props.propagateRowSelection) { + if (applyRowSelectionPropagation) { selectableIds.forEach((id) => { - const rowsToSelect = findRowsToSelect(apiRef, tree, id); + const rowsToSelect = findRowsToSelect(apiRef, tree, id, rowSelectionPropagation); rowsToSelect.forEach((rowId) => { newSelection.push(rowId); }); @@ -295,16 +322,16 @@ export const useGridRowSelection = ( selectableIds.forEach((id) => { if (isSelected) { selectionLookup[id] = id; - if (props.propagateRowSelection) { - const rowsToSelect = findRowsToSelect(apiRef, tree, id); + if (applyRowSelectionPropagation) { + const rowsToSelect = findRowsToSelect(apiRef, tree, id, rowSelectionPropagation); rowsToSelect.forEach((rowId) => { selectionLookup[rowId] = rowId; }); } } else { delete selectionLookup[id]; - if (props.propagateRowSelection) { - const rowsToDeselect = findRowsToDeselect(apiRef, tree, id); + if (applyRowSelectionPropagation) { + const rowsToDeselect = findRowsToDeselect(apiRef, tree, id, rowSelectionPropagation); rowsToDeselect.forEach((parentId) => { delete selectionLookup[parentId]; }); @@ -320,7 +347,14 @@ export const useGridRowSelection = ( apiRef.current.setRowSelectionModel(newSelection); } }, - [apiRef, logger, canHaveMultipleSelection, props.propagateRowSelection, tree], + [ + logger, + props.rowSelectionPropagation, + canHaveMultipleSelection, + apiRef, + applyRowSelectionPropagation, + tree, + ], ); const selectRowRange = React.useCallback( @@ -581,7 +615,7 @@ export const useGridRowSelection = ( ); const handleRowPropagation = React.useCallback(() => { - if (!props.propagateRowSelection) { + if (props.rowSelectionPropagation === 'none') { return; } const filteredRows = gridFilteredSortedRowIdsSetSelector(apiRef); @@ -602,7 +636,7 @@ export const useGridRowSelection = ( } }); apiRef.current.selectRows(Array.from(newSelectedRows), true, true); - }, [apiRef, props.propagateRowSelection, tree]); + }, [apiRef, props.rowSelectionPropagation, tree]); useGridApiEventHandler( apiRef, @@ -675,9 +709,6 @@ export const useGridRowSelection = ( }, [apiRef, canHaveMultipleSelection, checkboxSelection, isStateControlled, props.rowSelection]); React.useEffect(() => { - // TODO v8: Remove when `propagateRowSelection` is the default behavior - if (props.propagateRowSelection) { - runIfRowSelectionIsEnabled(handleRowPropagation); - } - }, [handleRowPropagation, props.propagateRowSelection, runIfRowSelectionIsEnabled]); + runIfRowSelectionIsEnabled(handleRowPropagation); + }, [handleRowPropagation, runIfRowSelectionIsEnabled]); }; diff --git a/packages/x-data-grid/src/hooks/features/rowSelection/utils.ts b/packages/x-data-grid/src/hooks/features/rowSelection/utils.ts index ed72f8abf1d9..505c1fa1a7d4 100644 --- a/packages/x-data-grid/src/hooks/features/rowSelection/utils.ts +++ b/packages/x-data-grid/src/hooks/features/rowSelection/utils.ts @@ -1,3 +1,4 @@ +import { GridRowSelectionPropagation } from '@mui/x-data-grid-pro'; import type { DataGridProcessedProps } from '../../../models/props/DataGridProps'; import { GridSignature } from '../../utils/useGridApiEventHandler'; import { GRID_ROOT_GROUP_ID } from '../rows/gridRowsUtils'; @@ -126,32 +127,44 @@ export const findRowsToSelect = ( apiRef: React.MutableRefObject, tree: GridRowTreeConfig, selectedRow: GridRowId, + rowSelectionPropagation: GridRowSelectionPropagation, ) => { const filteredRows = gridFilteredRowsLookupSelector(apiRef); const rowsToSelect: GridRowId[] = []; - const traverseParents = (rowId: GridRowId) => { - const siblings: GridRowId[] = getFilteredRowNodeSiblings(apiRef, tree, filteredRows, rowId); - if ( - siblings.length === 0 || - siblings.every((sibling) => apiRef.current.isRowSelected(sibling)) - ) { - const rowNode = apiRef.current.getRowNode(rowId) as GridGroupNode; - const parent = rowNode.parent; - if (parent && parent !== GRID_ROOT_GROUP_ID && apiRef.current.isRowSelectable(parent)) { - rowsToSelect.push(parent); - traverseParents(parent); + if (rowSelectionPropagation === 'none') { + return rowsToSelect; + } + + if (rowSelectionPropagation === 'both' || rowSelectionPropagation === 'parents') { + const traverseParents = (rowId: GridRowId) => { + const siblings: GridRowId[] = getFilteredRowNodeSiblings(apiRef, tree, filteredRows, rowId); + if ( + siblings.length === 0 || + siblings.every((sibling) => apiRef.current.isRowSelected(sibling)) + ) { + const rowNode = apiRef.current.getRowNode(rowId) as GridGroupNode; + const parent = rowNode.parent; + if (parent && parent !== GRID_ROOT_GROUP_ID && apiRef.current.isRowSelectable(parent)) { + rowsToSelect.push(parent); + traverseParents(parent); + } } - } - }; - traverseParents(selectedRow); + }; + traverseParents(selectedRow); + } - const rowNode = apiRef.current.getRowNode(selectedRow); + if (rowSelectionPropagation === 'both' || rowSelectionPropagation === 'children') { + const rowNode = apiRef.current.getRowNode(selectedRow); - if (rowNode?.type === 'group') { - const rowGroupChildrenSelector = getGridRowGroupSelectableChildrenSelector(apiRef, selectedRow); - const children = rowGroupChildrenSelector(apiRef); - return rowsToSelect.concat(children); + if (rowNode?.type === 'group') { + const rowGroupChildrenSelector = getGridRowGroupSelectableChildrenSelector( + apiRef, + selectedRow, + ); + const children = rowGroupChildrenSelector(apiRef); + return rowsToSelect.concat(children); + } } return rowsToSelect; }; @@ -160,25 +173,34 @@ export const findRowsToDeselect = ( apiRef: React.MutableRefObject, tree: GridRowTreeConfig, deselectedRow: GridRowId, + rowSelectionPropagation: GridRowSelectionPropagation, ) => { const rowsToDeselect: GridRowId[] = []; - const allParents = getRowNodeParents(tree, deselectedRow); - allParents.forEach((parent) => { - const isSelected = apiRef.current.isRowSelected(parent); - if (isSelected) { - rowsToDeselect.push(parent); + if (rowSelectionPropagation === 'none') { + return rowsToDeselect; + } + + if (rowSelectionPropagation === 'both' || rowSelectionPropagation === 'parents') { + const allParents = getRowNodeParents(tree, deselectedRow); + allParents.forEach((parent) => { + const isSelected = apiRef.current.isRowSelected(parent); + if (isSelected) { + rowsToDeselect.push(parent); + } + }); + } + + if (rowSelectionPropagation === 'both' || rowSelectionPropagation === 'children') { + const rowNode = apiRef.current.getRowNode(deselectedRow); + if (rowNode?.type === 'group') { + const rowGroupChildrenSelector = getGridRowGroupSelectableChildrenSelector( + apiRef, + deselectedRow, + ); + const children = rowGroupChildrenSelector(apiRef); + return rowsToDeselect.concat(children); } - }); - - const rowNode = apiRef.current.getRowNode(deselectedRow); - if (rowNode?.type === 'group') { - const rowGroupChildrenSelector = getGridRowGroupSelectableChildrenSelector( - apiRef, - deselectedRow, - ); - const children = rowGroupChildrenSelector(apiRef); - return rowsToDeselect.concat(children); } return rowsToDeselect; }; diff --git a/packages/x-data-grid/src/models/gridRowSelectionModel.ts b/packages/x-data-grid/src/models/gridRowSelectionModel.ts index 599d21df8b8f..a354efcdeb78 100644 --- a/packages/x-data-grid/src/models/gridRowSelectionModel.ts +++ b/packages/x-data-grid/src/models/gridRowSelectionModel.ts @@ -1,5 +1,7 @@ import { GridRowId } from './gridRows'; +export type GridRowSelectionPropagation = 'none' | 'parents' | 'children' | 'both'; + export type GridInputRowSelectionModel = readonly GridRowId[] | GridRowId; export type GridRowSelectionModel = readonly GridRowId[]; diff --git a/packages/x-data-grid/src/models/props/DataGridProps.ts b/packages/x-data-grid/src/models/props/DataGridProps.ts index a1185debf0fd..b082d51f01d6 100644 --- a/packages/x-data-grid/src/models/props/DataGridProps.ts +++ b/packages/x-data-grid/src/models/props/DataGridProps.ts @@ -33,6 +33,7 @@ import { GridColumnGroupingModel } from '../gridColumnGrouping'; import { GridPaginationMeta, GridPaginationModel } from '../gridPaginationProps'; import type { GridAutosizeOptions } from '../../hooks/features/columnResize'; import type { GridDataSource } from '../gridDataSource'; +import type { GridRowSelectionPropagation } from '../gridRowSelectionModel'; export interface GridExperimentalFeatures { /** @@ -817,15 +818,16 @@ export interface DataGridProSharedPropsWithDefaultValue { */ headerFilters: boolean; /** - * If `true`, following behavior happens with nested data: - * 1. Selecting/deselecting a parent row would select/deselect all the children rows. - * 2. When all the child rows are selected, the parent row will be auto selected. - * 3. When a child row is deselected, if one or more parent rows are already selected, they will be moved to an indeterminate state. - * 4. Select All checkbox in the header row would select/deselect all the rows including child rows. + * The following behavior happens for each of the possible values: + * 1. `none` - No row selection propagation. + * 2. `parents` - Selecting all children will auto-select the parent(s). + * 3. `children` - Selecting a parent will auto-select all its descendants. + * 4. `both` - Both `parents` and `children` behavior. + * * Works with tree data and row grouping on the client-side only. - * @default false + * @default 'none' */ - propagateRowSelection: boolean; + rowSelectionPropagation: GridRowSelectionPropagation; } export interface DataGridProSharedPropsWithoutDefaultValue { diff --git a/scripts/x-data-grid-premium.exports.json b/scripts/x-data-grid-premium.exports.json index cbdc58fcf2e9..76ba9bd6182d 100644 --- a/scripts/x-data-grid-premium.exports.json +++ b/scripts/x-data-grid-premium.exports.json @@ -331,7 +331,6 @@ { "name": "gridFilteredRowsLookupSelector", "kind": "Variable" }, { "name": "gridFilteredSortedRowEntriesSelector", "kind": "Variable" }, { "name": "gridFilteredSortedRowIdsSelector", "kind": "Variable" }, - { "name": "gridFilteredSortedRowIdsSetSelector", "kind": "Variable" }, { "name": "gridFilteredSortedTopLevelRowEntriesSelector", "kind": "Variable" }, { "name": "gridFilteredTopLevelRowCountSelector", "kind": "Variable" }, { "name": "GridFilterForm", "kind": "Variable" }, @@ -536,6 +535,7 @@ { "name": "GridRowSelectionApi", "kind": "Interface" }, { "name": "GridRowSelectionCheckboxParams", "kind": "Interface" }, { "name": "GridRowSelectionModel", "kind": "TypeAlias" }, + { "name": "GridRowSelectionPropagation", "kind": "TypeAlias" }, { "name": "gridRowSelectionStateSelector", "kind": "Variable" }, { "name": "gridRowsLoadingSelector", "kind": "Variable" }, { "name": "gridRowsLookupSelector", "kind": "Variable" }, diff --git a/scripts/x-data-grid-pro.exports.json b/scripts/x-data-grid-pro.exports.json index e78e8bc69454..d5e14c148c83 100644 --- a/scripts/x-data-grid-pro.exports.json +++ b/scripts/x-data-grid-pro.exports.json @@ -299,7 +299,6 @@ { "name": "gridFilteredRowsLookupSelector", "kind": "Variable" }, { "name": "gridFilteredSortedRowEntriesSelector", "kind": "Variable" }, { "name": "gridFilteredSortedRowIdsSelector", "kind": "Variable" }, - { "name": "gridFilteredSortedRowIdsSetSelector", "kind": "Variable" }, { "name": "gridFilteredSortedTopLevelRowEntriesSelector", "kind": "Variable" }, { "name": "gridFilteredTopLevelRowCountSelector", "kind": "Variable" }, { "name": "GridFilterForm", "kind": "Variable" }, @@ -489,6 +488,7 @@ { "name": "GridRowSelectionApi", "kind": "Interface" }, { "name": "GridRowSelectionCheckboxParams", "kind": "Interface" }, { "name": "GridRowSelectionModel", "kind": "TypeAlias" }, + { "name": "GridRowSelectionPropagation", "kind": "TypeAlias" }, { "name": "gridRowSelectionStateSelector", "kind": "Variable" }, { "name": "gridRowsLoadingSelector", "kind": "Variable" }, { "name": "gridRowsLookupSelector", "kind": "Variable" }, diff --git a/scripts/x-data-grid.exports.json b/scripts/x-data-grid.exports.json index bafb403edf8f..b77a45869881 100644 --- a/scripts/x-data-grid.exports.json +++ b/scripts/x-data-grid.exports.json @@ -269,7 +269,6 @@ { "name": "gridFilteredRowsLookupSelector", "kind": "Variable" }, { "name": "gridFilteredSortedRowEntriesSelector", "kind": "Variable" }, { "name": "gridFilteredSortedRowIdsSelector", "kind": "Variable" }, - { "name": "gridFilteredSortedRowIdsSetSelector", "kind": "Variable" }, { "name": "gridFilteredSortedTopLevelRowEntriesSelector", "kind": "Variable" }, { "name": "gridFilteredTopLevelRowCountSelector", "kind": "Variable" }, { "name": "GridFilterForm", "kind": "Variable" }, @@ -439,6 +438,7 @@ { "name": "GridRowSelectionApi", "kind": "Interface" }, { "name": "GridRowSelectionCheckboxParams", "kind": "Interface" }, { "name": "GridRowSelectionModel", "kind": "TypeAlias" }, + { "name": "GridRowSelectionPropagation", "kind": "TypeAlias" }, { "name": "gridRowSelectionStateSelector", "kind": "Variable" }, { "name": "gridRowsLoadingSelector", "kind": "Variable" }, { "name": "gridRowsLookupSelector", "kind": "Variable" }, From d0f1c8bba2af4f1d5edc4f5f4f58ccd24fa2b19b Mon Sep 17 00:00:00 2001 From: Bilal Shafi Date: Tue, 24 Sep 2024 16:56:19 +0500 Subject: [PATCH 28/45] Update --- .../features/filter/gridFilterSelector.ts | 15 --------------- .../rowSelection/useGridRowSelection.ts | 19 +++++++++---------- 2 files changed, 9 insertions(+), 25 deletions(-) diff --git a/packages/x-data-grid/src/hooks/features/filter/gridFilterSelector.ts b/packages/x-data-grid/src/hooks/features/filter/gridFilterSelector.ts index 99c00205db46..f9733a04f577 100644 --- a/packages/x-data-grid/src/hooks/features/filter/gridFilterSelector.ts +++ b/packages/x-data-grid/src/hooks/features/filter/gridFilterSelector.ts @@ -106,21 +106,6 @@ export const gridFilteredSortedRowIdsSelector = createSelectorMemoized( (filteredSortedRowEntries) => filteredSortedRowEntries.map((row) => row.id), ); -/** - * Get a `Set` containing the ids of the rows accessible after the filtering process. - * Contains the collapsed children. - * @category Filtering - * @ignore - Do not document. - */ -export const gridFilteredSortedRowIdsSetSelector = createSelectorMemoized( - gridFilteredSortedRowEntriesSelector, - (filteredSortedRowEntries) => { - const filteredSortedRowIdsSetSelector = new Set(); - filteredSortedRowEntries.map((row) => filteredSortedRowIdsSetSelector.add(row.id)); - return filteredSortedRowIdsSetSelector; - }, -); - /** * Get the ids to position in the current tree level lookup of the rows accessible after the filtering process. * Does not contain the collapsed children. diff --git a/packages/x-data-grid/src/hooks/features/rowSelection/useGridRowSelection.ts b/packages/x-data-grid/src/hooks/features/rowSelection/useGridRowSelection.ts index 0d2094901f02..760c873c6efc 100644 --- a/packages/x-data-grid/src/hooks/features/rowSelection/useGridRowSelection.ts +++ b/packages/x-data-grid/src/hooks/features/rowSelection/useGridRowSelection.ts @@ -22,7 +22,7 @@ import { gridFocusCellSelector } from '../focus/gridFocusStateSelector'; import { gridExpandedSortedRowIdsSelector, gridFilterModelSelector, - gridFilteredSortedRowIdsSetSelector, + gridFilteredRowsLookupSelector, } from '../filter/gridFilterSelector'; import { GRID_CHECKBOX_SELECTION_COL_DEF, GRID_ACTIONS_COLUMN_TYPE } from '../../../colDef'; import { GridCellModes } from '../../../models/gridEditRowModel'; @@ -618,24 +618,23 @@ export const useGridRowSelection = ( if (props.rowSelectionPropagation === 'none') { return; } - const filteredRows = gridFilteredSortedRowIdsSetSelector(apiRef); + const filteredRowsLookup = gridFilteredRowsLookupSelector(apiRef); const selectedRows = selectedGridRowsSelector(apiRef); - const newSelectedRows: Set = new Set(); + const newSelectedRows: GridRowId[] = []; selectedRows.forEach((_, id) => { - if (filteredRows.has(id)) { + if (filteredRowsLookup[id]) { const node = tree[id]; if (node.type === 'group' && !(node as GridGroupNode).isAutoGenerated) { - // Keep those previously selected tree data parents whose all children are filtered - // out and they've become leaf node now in `newSelectedRows` so that they are not auto deselected - if (!node.children.some((childId) => newSelectedRows.has(childId))) { - newSelectedRows.add(id); + // Keep previously selected tree data parents selected if all their children are filtered out + if (!node.children.some((childId) => filteredRowsLookup[childId])) { + newSelectedRows.push(id); } } else { - newSelectedRows.add(id); + newSelectedRows.push(id); } } }); - apiRef.current.selectRows(Array.from(newSelectedRows), true, true); + apiRef.current.selectRows(newSelectedRows, true, true); }, [apiRef, props.rowSelectionPropagation, tree]); useGridApiEventHandler( From c1472309bb87a7d7f7bcc9e6384a5d895ba7ad97 Mon Sep 17 00:00:00 2001 From: Bilal Shafi Date: Tue, 24 Sep 2024 18:28:01 +0500 Subject: [PATCH 29/45] Fix some tests --- .../tests/rowSelection.DataGridPremium.test.tsx | 14 +++++++------- .../src/tests/rowSelection.DataGridPro.test.tsx | 4 ++-- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/packages/x-data-grid-premium/src/tests/rowSelection.DataGridPremium.test.tsx b/packages/x-data-grid-premium/src/tests/rowSelection.DataGridPremium.test.tsx index 7133ee943288..dd97bb14917f 100644 --- a/packages/x-data-grid-premium/src/tests/rowSelection.DataGridPremium.test.tsx +++ b/packages/x-data-grid-premium/src/tests/rowSelection.DataGridPremium.test.tsx @@ -57,10 +57,10 @@ describe(' - Row selection', () => { ); } - describe('prop: propagateRowSelection', () => { + describe('prop: rowSelectionPropagation="both"', () => { it('should select all the children when selecting a parent', () => { render( - , + , ); fireEvent.click(getCell(1, 0).querySelector('input')!); @@ -73,7 +73,7 @@ describe(' - Row selection', () => { it('should deselect all the children when deselecting a parent', () => { render( - , + , ); fireEvent.click(getCell(1, 0).querySelector('input')!); @@ -89,7 +89,7 @@ describe(' - Row selection', () => { it('should put the parent into indeterminate if some but not all the children are selected', () => { render( - Row selection', () => { it('should auto select the parent if all the children are selected', () => { render( - Row selection', () => { it('should deselect auto selected parent if one of the children is deselected', () => { render( - Row selection', () => { it('should deselect unfiltered rows after filtering', () => { render( - Row selection', () => { }); }); - describe('prop: propagateRowSelection', () => { + describe('prop: rowSelectionPropagation="both"', () => { const rows: GridRowsProp = [ { hierarchy: ['Sarah'], @@ -373,7 +373,7 @@ describe(' - Row selection', () => { rows={rows} columns={columns} getTreeDataPath={getTreeDataPath} - propagateRowSelection + rowSelectionPropagation="both" checkboxSelection {...props} /> From 9f789acc8da7352e2a71e53954c45c29f7ab38c3 Mon Sep 17 00:00:00 2001 From: Bilal Shafi Date: Wed, 25 Sep 2024 10:54:17 +0500 Subject: [PATCH 30/45] Support indeterminateCheckboxAction prop on group header checkbox --- .../rowSelection.DataGridPremium.test.tsx | 10 ++++- .../GridCellCheckboxRenderer.tsx | 22 ++++++---- .../src/hooks/features/rowSelection/utils.ts | 41 +++++++++++++------ 3 files changed, 49 insertions(+), 24 deletions(-) diff --git a/packages/x-data-grid-premium/src/tests/rowSelection.DataGridPremium.test.tsx b/packages/x-data-grid-premium/src/tests/rowSelection.DataGridPremium.test.tsx index dd97bb14917f..f8f60d0cf436 100644 --- a/packages/x-data-grid-premium/src/tests/rowSelection.DataGridPremium.test.tsx +++ b/packages/x-data-grid-premium/src/tests/rowSelection.DataGridPremium.test.tsx @@ -60,7 +60,10 @@ describe(' - Row selection', () => { describe('prop: rowSelectionPropagation="both"', () => { it('should select all the children when selecting a parent', () => { render( - , + , ); fireEvent.click(getCell(1, 0).querySelector('input')!); @@ -73,7 +76,10 @@ describe(' - Row selection', () => { it('should deselect all the children when deselecting a parent', () => { render( - , + , ); fireEvent.click(getCell(1, 0).querySelector('input')!); diff --git a/packages/x-data-grid/src/components/columnSelection/GridCellCheckboxRenderer.tsx b/packages/x-data-grid/src/components/columnSelection/GridCellCheckboxRenderer.tsx index 3d8284edde95..99d687148029 100644 --- a/packages/x-data-grid/src/components/columnSelection/GridCellCheckboxRenderer.tsx +++ b/packages/x-data-grid/src/components/columnSelection/GridCellCheckboxRenderer.tsx @@ -4,14 +4,14 @@ import { unstable_composeClasses as composeClasses, unstable_useForkRef as useForkRef, } from '@mui/utils'; -import type { GridRenderCellParams } from '../../models/params/gridCellParams'; import { useGridApiContext } from '../../hooks/utils/useGridApiContext'; import { useGridRootProps } from '../../hooks/utils/useGridRootProps'; import { getDataGridUtilityClass } from '../../constants/gridClasses'; +import { useGridSelector } from '../../hooks/utils/useGridSelector'; +import { getCheckboxPropsSelector } from '../../hooks/features/rowSelection/utils'; import type { DataGridProcessedProps } from '../../models/props/DataGridProps'; import type { GridRowSelectionCheckboxParams } from '../../models/params/gridRowSelectionCheckboxParams'; -import { useGridSelector } from '../../hooks/utils/useGridSelector'; -import { getGridSomeChildrenSelectedSelector } from '../../hooks/features/rowSelection/utils'; +import type { GridRenderCellParams } from '../../models/params/gridCellParams'; type OwnerState = { classes: DataGridProcessedProps['classes'] }; @@ -34,7 +34,6 @@ const GridCellCheckboxForwardRef = React.forwardRef(null); - const someChildrenSelectedSelector = getGridSomeChildrenSelectedSelector(id); - const someChildrenSelected = useGridSelector(apiRef, someChildrenSelectedSelector); - const rippleRef = React.useRef(null); const handleRef = useForkRef(checkboxElement, ref); @@ -96,20 +92,28 @@ const GridCellCheckboxForwardRef = React.forwardRef, groupId: GridRowId, ) { - return createSelector( + return createSelectorMemoized( gridRowTreeSelector, gridSortedRowIdsSelector, gridFilteredRowsLookupSelector, @@ -45,8 +46,9 @@ function getGridRowGroupSelectableChildrenSelector( ); } -export function getGridSomeChildrenSelectedSelector(groupId: GridRowId) { - return createSelector( +// TODO v8: Use `createSelectorV8` +export function getCheckboxPropsSelector(groupId: GridRowId) { + return createSelectorMemoized( gridRowTreeSelector, gridSortedRowIdsSelector, gridFilteredRowsLookupSelector, @@ -54,9 +56,15 @@ export function getGridSomeChildrenSelectedSelector(groupId: GridRowId) { (rowTree, sortedRowIds, filteredRowsLookup, rowSelectionLookup) => { const groupNode = rowTree[groupId]; if (!groupNode || groupNode.type !== 'group') { - return false; + return { + isIndeterminate: false, + isChecked: + filteredRowsLookup[groupId] !== false && rowSelectionLookup[groupId] !== undefined, + }; } + let selectableDescendentsCount = 0; + let selectedDescendentsCount = 0; const startIndex = sortedRowIds.findIndex((id) => id === groupId) + 1; for ( let index = startIndex; @@ -64,11 +72,18 @@ export function getGridSomeChildrenSelectedSelector(groupId: GridRowId) { index += 1 ) { const id = sortedRowIds[index]; - if (filteredRowsLookup[id] !== false && rowSelectionLookup[id] !== undefined) { - return true; + if (filteredRowsLookup[id] !== false) { + selectableDescendentsCount += 1; + if (rowSelectionLookup[id] !== undefined) { + selectedDescendentsCount += 1; + } } } - return false; + return { + isIndeterminate: + selectedDescendentsCount > 0 && selectedDescendentsCount < selectableDescendentsCount, + isChecked: selectedDescendentsCount > 0, + }; }, ); } From ac850505a2b312de41b2fdffa4172527e84fc54c Mon Sep 17 00:00:00 2001 From: Bilal Shafi Date: Wed, 25 Sep 2024 14:19:51 +0500 Subject: [PATCH 31/45] Recompute selected parents on filtering --- .../rowSelection/useGridRowSelection.ts | 104 ++++++++++-------- 1 file changed, 56 insertions(+), 48 deletions(-) diff --git a/packages/x-data-grid/src/hooks/features/rowSelection/useGridRowSelection.ts b/packages/x-data-grid/src/hooks/features/rowSelection/useGridRowSelection.ts index 760c873c6efc..6685d5e4c1a0 100644 --- a/packages/x-data-grid/src/hooks/features/rowSelection/useGridRowSelection.ts +++ b/packages/x-data-grid/src/hooks/features/rowSelection/useGridRowSelection.ts @@ -21,7 +21,6 @@ import { gridPaginatedVisibleSortedGridRowIdsSelector } from '../pagination'; import { gridFocusCellSelector } from '../focus/gridFocusStateSelector'; import { gridExpandedSortedRowIdsSelector, - gridFilterModelSelector, gridFilteredRowsLookupSelector, } from '../filter/gridFilterSelector'; import { GRID_CHECKBOX_SELECTION_COL_DEF, GRID_ACTIONS_COLUMN_TYPE } from '../../../colDef'; @@ -406,28 +405,61 @@ export const useGridRowSelection = ( props.signature === GridSignature.DataGrid ? 'private' : 'public', ); - const removeOutdatedSelection = React.useCallback(() => { - if (props.keepNonExistentRowsSelected) { - return; - } - const currentSelection = gridRowSelectionStateSelector(apiRef.current.state); - const rowsLookup = gridRowsLookupSelector(apiRef); + const removeOutdatedSelection = React.useCallback( + (filterModelUpdated = false) => { + const currentSelection = gridRowSelectionStateSelector(apiRef.current.state); + const filteredRowsLookup = gridFilteredRowsLookupSelector(apiRef); - // We clone the existing object to avoid mutating the same object returned by the selector to others part of the project - const selectionLookup = { ...selectedIdsLookupSelector(apiRef) }; + // We clone the existing object to avoid mutating the same object returned by the selector to others part of the project + const selectionLookup = { ...selectedIdsLookupSelector(apiRef) }; - let hasChanged = false; - currentSelection.forEach((id: GridRowId) => { - if (!rowsLookup[id]) { - delete selectionLookup[id]; - hasChanged = true; - } - }); + let hasChanged = false; + currentSelection.forEach((id: GridRowId) => { + if (filteredRowsLookup[id] === false) { + if (props.keepNonExistentRowsSelected) { + return; + } + delete selectionLookup[id]; + hasChanged = true; + return; + } + if ( + props.rowSelectionPropagation !== 'both' && + props.rowSelectionPropagation !== 'parents' + ) { + return; + } + const node = tree[id]; + if (node.type === 'group') { + const isAutoGenerated = (node as GridGroupNode).isAutoGenerated; + if (isAutoGenerated) { + delete selectionLookup[id]; + hasChanged = true; + return; + } + // Keep previously selected tree data parents selected if all their children are filtered out + if (!node.children.every((childId) => filteredRowsLookup[childId] === false)) { + delete selectionLookup[id]; + hasChanged = true; + } + } + }); - if (hasChanged) { - apiRef.current.setRowSelectionModel(Object.values(selectionLookup)); - } - }, [apiRef, props.keepNonExistentRowsSelected]); + const isNestedData = + // @ts-ignore - FIXME: remove the need to `ts-ignore` + props.treeData || (apiRef.current.state.rowGrouping?.model?.length ?? 0) > 0; + + if (hasChanged || (isNestedData && filterModelUpdated)) { + const newSelection = Object.values(selectionLookup); + if (isNestedData) { + apiRef.current.selectRows(newSelection, true, true); + } else { + apiRef.current.setRowSelectionModel(newSelection); + } + } + }, + [apiRef, props.keepNonExistentRowsSelected, tree, props.rowSelectionPropagation], + ); const handleSingleRowSelection = React.useCallback( (id: GridRowId, event: React.MouseEvent | React.KeyboardEvent) => { @@ -532,8 +564,7 @@ export const useGridRowSelection = ( ? gridPaginatedVisibleSortedGridRowIdsSelector(apiRef) : gridExpandedSortedRowIdsSelector(apiRef); - const filterModel = gridFilterModelSelector(apiRef); - apiRef.current.selectRows(rowsToBeSelected, params.value, filterModel?.items.length > 0); + apiRef.current.selectRows(rowsToBeSelected, params.value); }, [apiRef, props.checkboxSelectionVisibleOnly, props.pagination, props.paginationMode], ); @@ -614,29 +645,6 @@ export const useGridRowSelection = ( [apiRef, handleSingleRowSelection, selectRows, visibleRows.rows, canHaveMultipleSelection], ); - const handleRowPropagation = React.useCallback(() => { - if (props.rowSelectionPropagation === 'none') { - return; - } - const filteredRowsLookup = gridFilteredRowsLookupSelector(apiRef); - const selectedRows = selectedGridRowsSelector(apiRef); - const newSelectedRows: GridRowId[] = []; - selectedRows.forEach((_, id) => { - if (filteredRowsLookup[id]) { - const node = tree[id]; - if (node.type === 'group' && !(node as GridGroupNode).isAutoGenerated) { - // Keep previously selected tree data parents selected if all their children are filtered out - if (!node.children.some((childId) => filteredRowsLookup[childId])) { - newSelectedRows.push(id); - } - } else { - newSelectedRows.push(id); - } - } - }); - apiRef.current.selectRows(newSelectedRows, true, true); - }, [apiRef, props.rowSelectionPropagation, tree]); - useGridApiEventHandler( apiRef, 'sortedRowsSet', @@ -662,7 +670,7 @@ export const useGridRowSelection = ( useGridApiEventHandler( apiRef, 'filteredRowsSet', - runIfRowSelectionIsEnabled(handleRowPropagation), + runIfRowSelectionIsEnabled(() => removeOutdatedSelection(true)), ); React.useEffect(() => { @@ -708,6 +716,6 @@ export const useGridRowSelection = ( }, [apiRef, canHaveMultipleSelection, checkboxSelection, isStateControlled, props.rowSelection]); React.useEffect(() => { - runIfRowSelectionIsEnabled(handleRowPropagation); - }, [handleRowPropagation, runIfRowSelectionIsEnabled]); + runIfRowSelectionIsEnabled(removeOutdatedSelection); + }, [removeOutdatedSelection, runIfRowSelectionIsEnabled]); }; From 1aac47d2c20b5979c364e9cb8ad495875aea23e1 Mon Sep 17 00:00:00 2001 From: Bilal Shafi Date: Thu, 26 Sep 2024 16:42:39 +0500 Subject: [PATCH 32/45] Update signature of rowSelectionPropagation prop --- .../RowGroupingPropagateSelection.js | 57 ++++++++---- .../RowGroupingPropagateSelection.tsx | 58 ++++++++---- .../data-grid/row-grouping/row-grouping.md | 30 ++++--- .../x/api/data-grid/data-grid-premium.json | 7 +- docs/pages/x/api/data-grid/data-grid-pro.json | 7 +- .../x/api/data-grid/grid-actions-col-def.json | 3 +- docs/pages/x/api/data-grid/grid-col-def.json | 3 +- .../data-grid/grid-single-select-col-def.json | 3 +- .../data-grid-premium/data-grid-premium.json | 2 +- .../data-grid-pro/data-grid-pro.json | 2 +- .../src/DataGridPremium/DataGridPremium.tsx | 19 ++-- .../rowSelection.DataGridPremium.test.tsx | 51 +++++------ .../src/DataGridPro/DataGridPro.tsx | 19 ++-- .../src/DataGridPro/useDataGridProProps.ts | 8 +- .../src/models/dataGridProProps.ts | 5 -- .../tests/rowSelection.DataGridPro.test.tsx | 4 +- .../rowSelection/useGridRowSelection.ts | 89 ++++++++++++------- .../src/hooks/features/rowSelection/utils.ts | 55 +++++++----- packages/x-data-grid/src/internals/index.ts | 1 + .../src/models/gridRowSelectionModel.ts | 5 +- .../src/models/props/DataGridProps.ts | 19 ++-- 21 files changed, 270 insertions(+), 177 deletions(-) diff --git a/docs/data/data-grid/row-grouping/RowGroupingPropagateSelection.js b/docs/data/data-grid/row-grouping/RowGroupingPropagateSelection.js index 6da2dd356e12..5baa5839b6f8 100644 --- a/docs/data/data-grid/row-grouping/RowGroupingPropagateSelection.js +++ b/docs/data/data-grid/row-grouping/RowGroupingPropagateSelection.js @@ -5,15 +5,17 @@ import { useKeepGroupedColumnsHidden, } from '@mui/x-data-grid-premium'; import { useMovieData } from '@mui/x-data-grid-generator'; -import FormControl from '@mui/material/FormControl'; -import InputLabel from '@mui/material/InputLabel'; -import MenuItem from '@mui/material/MenuItem'; -import Select from '@mui/material/Select'; +import FormControlLabel from '@mui/material/FormControlLabel'; +import Checkbox from '@mui/material/Checkbox'; +import Stack from '@mui/material/Stack'; export default function RowGroupingPropagateSelection() { const data = useMovieData(); const apiRef = useGridApiRef(); - const [value, setValue] = React.useState('both'); + const [rowSelectionPropagation, setRowSelectionPropagation] = React.useState({ + parents: true, + descendants: true, + }); const initialState = useKeepGroupedColumnsHidden({ apiRef, @@ -24,28 +26,45 @@ export default function RowGroupingPropagateSelection() { }, }); - const handleValueChange = React.useCallback((event) => { - setValue(event.target.value); - }, []); - return (
- - Propagation behavior - - + + + setRowSelectionPropagation((prev) => ({ + ...prev, + descendants: event.target.checked, + })) + } + /> + } + label="Auto select descendants" + /> + + setRowSelectionPropagation((prev) => ({ + ...prev, + parents: event.target.checked, + })) + } + /> + } + label="Auto select parents" + /> +
diff --git a/docs/data/data-grid/row-grouping/RowGroupingPropagateSelection.tsx b/docs/data/data-grid/row-grouping/RowGroupingPropagateSelection.tsx index 2410f52fea2b..9229e086666a 100644 --- a/docs/data/data-grid/row-grouping/RowGroupingPropagateSelection.tsx +++ b/docs/data/data-grid/row-grouping/RowGroupingPropagateSelection.tsx @@ -6,15 +6,18 @@ import { GridRowSelectionPropagation, } from '@mui/x-data-grid-premium'; import { useMovieData } from '@mui/x-data-grid-generator'; -import FormControl from '@mui/material/FormControl'; -import InputLabel from '@mui/material/InputLabel'; -import MenuItem from '@mui/material/MenuItem'; -import Select, { SelectChangeEvent } from '@mui/material/Select'; +import FormControlLabel from '@mui/material/FormControlLabel'; +import Checkbox from '@mui/material/Checkbox'; +import Stack from '@mui/material/Stack'; export default function RowGroupingPropagateSelection() { const data = useMovieData(); const apiRef = useGridApiRef(); - const [value, setValue] = React.useState('both'); + const [rowSelectionPropagation, setRowSelectionPropagation] = + React.useState({ + parents: true, + descendants: true, + }); const initialState = useKeepGroupedColumnsHidden({ apiRef, @@ -25,28 +28,45 @@ export default function RowGroupingPropagateSelection() { }, }); - const handleValueChange = React.useCallback((event: SelectChangeEvent) => { - setValue(event.target.value as GridRowSelectionPropagation); - }, []); - return (
- - Propagation behavior - - + + + setRowSelectionPropagation((prev) => ({ + ...prev, + descendants: event.target.checked, + })) + } + /> + } + label="Auto select descendants" + /> + + setRowSelectionPropagation((prev) => ({ + ...prev, + parents: event.target.checked, + })) + } + /> + } + label="Auto select parents" + /> +
diff --git a/docs/data/data-grid/row-grouping/row-grouping.md b/docs/data/data-grid/row-grouping/row-grouping.md index c6c3b17d526c..482d707c70c9 100644 --- a/docs/data/data-grid/row-grouping/row-grouping.md +++ b/docs/data/data-grid/row-grouping/row-grouping.md @@ -308,23 +308,33 @@ If you are dynamically switching the `leafField` or `mainGroupingCriteria`, the ## Automatic parents and children selection By default, selecting a parent row will not select its children. -You can override this behavior by setting using the `rowSelectionPropagation` prop. +You can override this behavior by using the `rowSelectionPropagation` prop. -It has four possible values, `'none'` (current behavior), `'parents'`, `'children'`, and `'both'`. The following table shows the behavior of each value on user actions "Select" and "Deselect": +Here's how it's structured: -| Value | Select | Deselect | -| :----------- | :----------------------------------------------------------------------- | :----------------------------------------------------------------- | -| `'none'` | Only select the clicked row | Only deselect the clicked row | -| `'parents'` | Auto select parents whose all the descendants are selected | Auto deselect parents whose atleast one descendant is not selected | -| `'children'` | Selecting a parent row will automatically select all the descendant rows | Deselect all the descendant rows on deselecting a parent | -| `'both'` | Both `parent` and `children` behaviors | Both `parent` and `children` behaviors | +```ts +type GridRowSelectionPropagation = { + descendants: boolean; + parents: boolean; +}; +``` + +When `rowSelectionPropagation.descendants` is set to `true`. + +- Selecting a parent will auto-select all its filtered descendants. +- Deselecting a parent row will auto-deselect all its filtered descendants. + +When `rowSelectionPropagation.parents` is set to `true`. + +- Selecting all the filtered descendants of a parent would auto-select it. +- Deselecting a descendant of a selected parent would deselect the parent. -In the following example, you can test the different values of the `rowSelectionPropagation` prop. +The example below demonstrates the usage of the `rowSelectionPropagation` prop. {{"demo": "RowGroupingPropagateSelection.js", "bg": "inline", "defaultCodeOpen": false}} :::info -The row selection propagation feature will only affect the filtered rows. +The `autoSelectDescendants` and `autoSelectParents` props will only affect the filtered rows. If some rows were selected before filtering, auto selection will not be applied on them. ::: diff --git a/docs/pages/x/api/data-grid/data-grid-premium.json b/docs/pages/x/api/data-grid/data-grid-premium.json index afa7231fd9f2..b3b7dbe31634 100644 --- a/docs/pages/x/api/data-grid/data-grid-premium.json +++ b/docs/pages/x/api/data-grid/data-grid-premium.json @@ -589,11 +589,8 @@ } }, "rowSelectionPropagation": { - "type": { - "name": "enum", - "description": "'both'
| 'children'
| 'none'
| 'parents'" - }, - "default": "'none'" + "type": { "name": "shape", "description": "{ descendants?: bool, parents?: bool }" }, + "default": "{ parents: false, descendants: false }" }, "rowsLoadingMode": { "type": { "name": "enum", "description": "'client'
| 'server'" } diff --git a/docs/pages/x/api/data-grid/data-grid-pro.json b/docs/pages/x/api/data-grid/data-grid-pro.json index 6a07a59752b7..c994dfc81a0a 100644 --- a/docs/pages/x/api/data-grid/data-grid-pro.json +++ b/docs/pages/x/api/data-grid/data-grid-pro.json @@ -524,11 +524,8 @@ } }, "rowSelectionPropagation": { - "type": { - "name": "enum", - "description": "'both'
| 'children'
| 'none'
| 'parents'" - }, - "default": "'none'" + "type": { "name": "shape", "description": "{ descendants?: bool, parents?: bool }" }, + "default": "{ parents: false, descendants: false }" }, "rowsLoadingMode": { "type": { "name": "enum", "description": "'client'
| 'server'" } diff --git a/docs/pages/x/api/data-grid/grid-actions-col-def.json b/docs/pages/x/api/data-grid/grid-actions-col-def.json index ecf251480e1a..26d5acce67f3 100644 --- a/docs/pages/x/api/data-grid/grid-actions-col-def.json +++ b/docs/pages/x/api/data-grid/grid-actions-col-def.json @@ -85,7 +85,8 @@ } }, "renderHeaderFilter": { - "type": { "description": "(params: GridRenderHeaderFilterProps) => React.ReactNode" } + "type": { "description": "(params: GridRenderHeaderFilterProps) => React.ReactNode" }, + "isProPlan": true }, "resizable": { "type": { "description": "boolean" }, "default": "true" }, "sortable": { "type": { "description": "boolean" }, "default": "true" }, diff --git a/docs/pages/x/api/data-grid/grid-col-def.json b/docs/pages/x/api/data-grid/grid-col-def.json index 2e8eed38928b..bfe97f8a9704 100644 --- a/docs/pages/x/api/data-grid/grid-col-def.json +++ b/docs/pages/x/api/data-grid/grid-col-def.json @@ -78,7 +78,8 @@ } }, "renderHeaderFilter": { - "type": { "description": "(params: GridRenderHeaderFilterProps) => React.ReactNode" } + "type": { "description": "(params: GridRenderHeaderFilterProps) => React.ReactNode" }, + "isProPlan": true }, "resizable": { "type": { "description": "boolean" }, "default": "true" }, "sortable": { "type": { "description": "boolean" }, "default": "true" }, diff --git a/docs/pages/x/api/data-grid/grid-single-select-col-def.json b/docs/pages/x/api/data-grid/grid-single-select-col-def.json index da00444723e9..669318bc4848 100644 --- a/docs/pages/x/api/data-grid/grid-single-select-col-def.json +++ b/docs/pages/x/api/data-grid/grid-single-select-col-def.json @@ -85,7 +85,8 @@ } }, "renderHeaderFilter": { - "type": { "description": "(params: GridRenderHeaderFilterProps) => React.ReactNode" } + "type": { "description": "(params: GridRenderHeaderFilterProps) => React.ReactNode" }, + "isProPlan": true }, "resizable": { "type": { "description": "boolean" }, "default": "true" }, "sortable": { "type": { "description": "boolean" }, "default": "true" }, diff --git a/docs/translations/api-docs/data-grid/data-grid-premium/data-grid-premium.json b/docs/translations/api-docs/data-grid/data-grid-premium/data-grid-premium.json index f9cbefda1d66..e7ad32e24845 100644 --- a/docs/translations/api-docs/data-grid/data-grid-premium/data-grid-premium.json +++ b/docs/translations/api-docs/data-grid/data-grid-premium/data-grid-premium.json @@ -608,7 +608,7 @@ "rowSelection": { "description": "If false, the row selection mode is disabled." }, "rowSelectionModel": { "description": "Sets the row selection model of the Data Grid." }, "rowSelectionPropagation": { - "description": "The following behavior happens for each of the possible values: 1. none - No row selection propagation. 2. parents - Selecting all children will auto-select the parent(s). 3. children - Selecting a parent will auto-select all its descendants. 4. both - Both parents and children behavior.
Works with tree data and row grouping on the client-side only." + "description": "When rowSelectionPropagation.descendants is set to true. - Selecting a parent will auto-select all its filtered descendants. - Deselecting a parent will auto-deselect all its filtered descendants.
When rowSelectionPropagation.parents=true - Selecting all descendants of a parent would auto-select it. - Deselecting a descendant of a selected parent would deselect the parent.
Works with tree data and row grouping on the client-side only." }, "rowsLoadingMode": { "description": "Loading rows can be processed on the server or client-side. Set it to 'client' if you would like enable infnite loading. Set it to 'server' if you would like to enable lazy loading. * @default "client"" diff --git a/docs/translations/api-docs/data-grid/data-grid-pro/data-grid-pro.json b/docs/translations/api-docs/data-grid/data-grid-pro/data-grid-pro.json index 29e846260b7a..c12f9a12a267 100644 --- a/docs/translations/api-docs/data-grid/data-grid-pro/data-grid-pro.json +++ b/docs/translations/api-docs/data-grid/data-grid-pro/data-grid-pro.json @@ -550,7 +550,7 @@ "rowSelection": { "description": "If false, the row selection mode is disabled." }, "rowSelectionModel": { "description": "Sets the row selection model of the Data Grid." }, "rowSelectionPropagation": { - "description": "The following behavior happens for each of the possible values: 1. none - No row selection propagation. 2. parents - Selecting all children will auto-select the parent(s). 3. children - Selecting a parent will auto-select all its descendants. 4. both - Both parents and children behavior.
Works with tree data and row grouping on the client-side only." + "description": "When rowSelectionPropagation.descendants is set to true. - Selecting a parent will auto-select all its filtered descendants. - Deselecting a parent will auto-deselect all its filtered descendants.
When rowSelectionPropagation.parents=true - Selecting all descendants of a parent would auto-select it. - Deselecting a descendant of a selected parent would deselect the parent.
Works with tree data and row grouping on the client-side only." }, "rowsLoadingMode": { "description": "Loading rows can be processed on the server or client-side. Set it to 'client' if you would like enable infnite loading. Set it to 'server' if you would like to enable lazy loading. * @default "client"" diff --git a/packages/x-data-grid-premium/src/DataGridPremium/DataGridPremium.tsx b/packages/x-data-grid-premium/src/DataGridPremium/DataGridPremium.tsx index 4ed7a8568a21..44f61539c074 100644 --- a/packages/x-data-grid-premium/src/DataGridPremium/DataGridPremium.tsx +++ b/packages/x-data-grid-premium/src/DataGridPremium/DataGridPremium.tsx @@ -976,16 +976,21 @@ DataGridPremiumRaw.propTypes = { PropTypes.string, ]), /** - * The following behavior happens for each of the possible values: - * 1. `none` - No row selection propagation. - * 2. `parents` - Selecting all children will auto-select the parent(s). - * 3. `children` - Selecting a parent will auto-select all its descendants. - * 4. `both` - Both `parents` and `children` behavior. + * When `rowSelectionPropagation.descendants` is set to `true`. + * - Selecting a parent will auto-select all its filtered descendants. + * - Deselecting a parent will auto-deselect all its filtered descendants. + * + * When `rowSelectionPropagation.parents=true` + * - Selecting all descendants of a parent would auto-select it. + * - Deselecting a descendant of a selected parent would deselect the parent. * * Works with tree data and row grouping on the client-side only. - * @default 'none' + * @default { parents: false, descendants: false } */ - rowSelectionPropagation: PropTypes.oneOf(['both', 'children', 'none', 'parents']), + rowSelectionPropagation: PropTypes.shape({ + descendants: PropTypes.bool, + parents: PropTypes.bool, + }), /** * Loading rows can be processed on the server or client-side. * Set it to 'client' if you would like enable infnite loading. diff --git a/packages/x-data-grid-premium/src/tests/rowSelection.DataGridPremium.test.tsx b/packages/x-data-grid-premium/src/tests/rowSelection.DataGridPremium.test.tsx index f8f60d0cf436..ae52bbe755a5 100644 --- a/packages/x-data-grid-premium/src/tests/rowSelection.DataGridPremium.test.tsx +++ b/packages/x-data-grid-premium/src/tests/rowSelection.DataGridPremium.test.tsx @@ -45,26 +45,30 @@ const baselineProps: BaselineProps = { describe(' - Row selection', () => { const { render } = createRenderer(); - let apiRef: React.MutableRefObject; - - function Test(props: Partial) { - apiRef = useGridApiRef(); - - return ( -
- -
- ); - } + describe('props: rowSelectionPropagation.descendants=true & rowSelectionPropagation.parents=true', () => { + let apiRef: React.MutableRefObject; + + function Test(props: Partial) { + apiRef = useGridApiRef(); + + return ( +
+ +
+ ); + } - describe('prop: rowSelectionPropagation="both"', () => { it('should select all the children when selecting a parent', () => { - render( - , - ); + render(); fireEvent.click(getCell(1, 0).querySelector('input')!); expect(apiRef.current.getSelectedRows()).to.have.keys([ @@ -75,12 +79,7 @@ describe(' - Row selection', () => { }); it('should deselect all the children when deselecting a parent', () => { - render( - , - ); + render(); fireEvent.click(getCell(1, 0).querySelector('input')!); expect(apiRef.current.getSelectedRows()).to.have.keys([ @@ -95,7 +94,6 @@ describe(' - Row selection', () => { it('should put the parent into indeterminate if some but not all the children are selected', () => { render( - Row selection', () => { it('should auto select the parent if all the children are selected', () => { render( - Row selection', () => { it('should deselect auto selected parent if one of the children is deselected', () => { render( - Row selection', () => { it('should deselect unfiltered rows after filtering', () => { render( 500, headerFilters: false, keepColumnPositionIfDraggedOutside: false, - rowSelectionPropagation: 'none', + rowSelectionPropagation: ROW_SELECTION_PROPAGATION_DEFAULT, rowReordering: false, rowsLoadingMode: 'client', scrollEndThreshold: 80, diff --git a/packages/x-data-grid-pro/src/models/dataGridProProps.ts b/packages/x-data-grid-pro/src/models/dataGridProProps.ts index 1af6cbe61f58..aa159f0baedd 100644 --- a/packages/x-data-grid-pro/src/models/dataGridProProps.ts +++ b/packages/x-data-grid-pro/src/models/dataGridProProps.ts @@ -79,11 +79,6 @@ export interface DataGridProPropsWithDefaultValue - Row selection', () => { }); }); - describe('prop: rowSelectionPropagation="both"', () => { + describe('props: rowSelectionPropagation.descendants=true & rowSelectionPropagation.parents=true', () => { const rows: GridRowsProp = [ { hierarchy: ['Sarah'], @@ -373,7 +373,7 @@ describe(' - Row selection', () => { rows={rows} columns={columns} getTreeDataPath={getTreeDataPath} - rowSelectionPropagation="both" + rowSelectionPropagation={{ descendants: true, parents: true }} checkboxSelection {...props} /> diff --git a/packages/x-data-grid/src/hooks/features/rowSelection/useGridRowSelection.ts b/packages/x-data-grid/src/hooks/features/rowSelection/useGridRowSelection.ts index 6685d5e4c1a0..eb3cd0e14dc5 100644 --- a/packages/x-data-grid/src/hooks/features/rowSelection/useGridRowSelection.ts +++ b/packages/x-data-grid/src/hooks/features/rowSelection/useGridRowSelection.ts @@ -11,7 +11,7 @@ import { GridSignature, useGridApiEventHandler } from '../../utils/useGridApiEve import { useGridApiMethod } from '../../utils/useGridApiMethod'; import { useGridLogger } from '../../utils/useGridLogger'; import { useGridSelector } from '../../utils/useGridSelector'; -import { gridRowsLookupSelector, gridRowTreeSelector } from '../rows/gridRowsSelector'; +import { gridRowMaximumTreeDepthSelector, gridRowTreeSelector } from '../rows/gridRowsSelector'; import { gridRowSelectionStateSelector, selectedGridRowsSelector, @@ -101,8 +101,9 @@ export const useGridRowSelection = ( [props.rowSelection], ); - const applyRowSelectionPropagation = - !props.disableMultipleRowSelection && (props.rowSelectionPropagation ?? 'none') !== 'none'; + const applyAutoSelection = + props.signature !== GridSignature.DataGrid && + (props.rowSelectionPropagation?.parents || props.rowSelectionPropagation?.descendants); const propRowSelectionModel = React.useMemo(() => { return getSelectionModelPropValue( @@ -130,6 +131,7 @@ export const useGridRowSelection = ( const canHaveMultipleSelection = isMultipleRowSelectionEnabled(props); const visibleRows = useGridVisibleRows(apiRef, props); const tree = useGridSelector(apiRef, gridRowTreeSelector); + const isNestedData = useGridSelector(apiRef, gridRowMaximumTreeDepthSelector) > 1; const expandMouseRowRangeSelection = React.useCallback( (id: GridRowId) => { @@ -229,12 +231,13 @@ export const useGridRowSelection = ( const newSelection: GridRowId[] = []; if (isSelected) { newSelection.push(id); - if (applyRowSelectionPropagation) { + if (applyAutoSelection) { const rowsToSelect = findRowsToSelect( apiRef, tree, id, - props.rowSelectionPropagation ?? 'none', + props.rowSelectionPropagation?.descendants ?? false, + props.rowSelectionPropagation?.parents ?? false, ); rowsToSelect.forEach((rowId) => { newSelection.push(rowId); @@ -253,21 +256,23 @@ export const useGridRowSelection = ( if (isSelected) { newSelection.add(id); - if (applyRowSelectionPropagation) { + if (applyAutoSelection) { const rowsToSelect = findRowsToSelect( apiRef, tree, id, - props.rowSelectionPropagation ?? 'none', + props.rowSelectionPropagation?.descendants ?? false, + props.rowSelectionPropagation?.parents ?? false, ); rowsToSelect.forEach(newSelection.add, newSelection); } - } else if (applyRowSelectionPropagation) { + } else if (applyAutoSelection) { const rowsToDeselect = findRowsToDeselect( apiRef, tree, id, - props.rowSelectionPropagation ?? 'none', + props.rowSelectionPropagation?.descendants ?? false, + props.rowSelectionPropagation?.parents ?? false, ); rowsToDeselect.forEach((parentId) => { newSelection.delete(parentId); @@ -283,9 +288,10 @@ export const useGridRowSelection = ( [ apiRef, logger, - applyRowSelectionPropagation, + applyAutoSelection, tree, - props.rowSelectionPropagation, + props.rowSelectionPropagation?.descendants, + props.rowSelectionPropagation?.parents, canHaveMultipleSelection, ], ); @@ -296,14 +302,19 @@ export const useGridRowSelection = ( const selectableIds = ids.filter((id) => apiRef.current.isRowSelectable(id)); - const rowSelectionPropagation = props.rowSelectionPropagation ?? 'none'; let newSelection: GridRowId[]; if (resetSelection) { if (isSelected) { newSelection = selectableIds; - if (applyRowSelectionPropagation) { + if (applyAutoSelection) { selectableIds.forEach((id) => { - const rowsToSelect = findRowsToSelect(apiRef, tree, id, rowSelectionPropagation); + const rowsToSelect = findRowsToSelect( + apiRef, + tree, + id, + props.rowSelectionPropagation?.descendants ?? false, + props.rowSelectionPropagation?.parents ?? false, + ); rowsToSelect.forEach((rowId) => { newSelection.push(rowId); }); @@ -321,16 +332,28 @@ export const useGridRowSelection = ( selectableIds.forEach((id) => { if (isSelected) { selectionLookup[id] = id; - if (applyRowSelectionPropagation) { - const rowsToSelect = findRowsToSelect(apiRef, tree, id, rowSelectionPropagation); + if (applyAutoSelection) { + const rowsToSelect = findRowsToSelect( + apiRef, + tree, + id, + props.rowSelectionPropagation?.descendants ?? false, + props.rowSelectionPropagation?.parents ?? false, + ); rowsToSelect.forEach((rowId) => { selectionLookup[rowId] = rowId; }); } } else { delete selectionLookup[id]; - if (applyRowSelectionPropagation) { - const rowsToDeselect = findRowsToDeselect(apiRef, tree, id, rowSelectionPropagation); + if (applyAutoSelection) { + const rowsToDeselect = findRowsToDeselect( + apiRef, + tree, + id, + props.rowSelectionPropagation?.descendants ?? false, + props.rowSelectionPropagation?.parents ?? false, + ); rowsToDeselect.forEach((parentId) => { delete selectionLookup[parentId]; }); @@ -348,11 +371,12 @@ export const useGridRowSelection = ( }, [ logger, - props.rowSelectionPropagation, + applyAutoSelection, canHaveMultipleSelection, apiRef, - applyRowSelectionPropagation, tree, + props.rowSelectionPropagation?.descendants, + props.rowSelectionPropagation?.parents, ], ); @@ -406,7 +430,7 @@ export const useGridRowSelection = ( ); const removeOutdatedSelection = React.useCallback( - (filterModelUpdated = false) => { + (sortModelUpdated = false) => { const currentSelection = gridRowSelectionStateSelector(apiRef.current.state); const filteredRowsLookup = gridFilteredRowsLookupSelector(apiRef); @@ -423,10 +447,7 @@ export const useGridRowSelection = ( hasChanged = true; return; } - if ( - props.rowSelectionPropagation !== 'both' && - props.rowSelectionPropagation !== 'parents' - ) { + if (!props.rowSelectionPropagation?.parents) { return; } const node = tree[id]; @@ -445,11 +466,7 @@ export const useGridRowSelection = ( } }); - const isNestedData = - // @ts-ignore - FIXME: remove the need to `ts-ignore` - props.treeData || (apiRef.current.state.rowGrouping?.model?.length ?? 0) > 0; - - if (hasChanged || (isNestedData && filterModelUpdated)) { + if (hasChanged || (isNestedData && !sortModelUpdated)) { const newSelection = Object.values(selectionLookup); if (isNestedData) { apiRef.current.selectRows(newSelection, true, true); @@ -458,7 +475,13 @@ export const useGridRowSelection = ( } } }, - [apiRef, props.keepNonExistentRowsSelected, tree, props.rowSelectionPropagation], + [ + apiRef, + isNestedData, + props.rowSelectionPropagation?.parents, + props.keepNonExistentRowsSelected, + tree, + ], ); const handleSingleRowSelection = React.useCallback( @@ -648,7 +671,7 @@ export const useGridRowSelection = ( useGridApiEventHandler( apiRef, 'sortedRowsSet', - runIfRowSelectionIsEnabled(removeOutdatedSelection), + runIfRowSelectionIsEnabled(() => removeOutdatedSelection(true)), ); useGridApiEventHandler(apiRef, 'rowClick', runIfRowSelectionIsEnabled(handleRowClick)); useGridApiEventHandler( @@ -670,7 +693,7 @@ export const useGridRowSelection = ( useGridApiEventHandler( apiRef, 'filteredRowsSet', - runIfRowSelectionIsEnabled(() => removeOutdatedSelection(true)), + runIfRowSelectionIsEnabled(removeOutdatedSelection), ); React.useEffect(() => { diff --git a/packages/x-data-grid/src/hooks/features/rowSelection/utils.ts b/packages/x-data-grid/src/hooks/features/rowSelection/utils.ts index 162f44d48b0d..00c7090abcf9 100644 --- a/packages/x-data-grid/src/hooks/features/rowSelection/utils.ts +++ b/packages/x-data-grid/src/hooks/features/rowSelection/utils.ts @@ -1,24 +1,29 @@ -import { GridRowSelectionPropagation } from '@mui/x-data-grid-pro'; import { GridSignature } from '../../utils/useGridApiEventHandler'; import { GRID_ROOT_GROUP_ID } from '../rows/gridRowsUtils'; import { gridFilteredRowsLookupSelector } from '../filter/gridFilterSelector'; import { gridSortedRowIdsSelector } from '../sorting/gridSortingSelector'; import { selectedIdsLookupSelector } from './gridRowSelectionSelector'; import { gridRowTreeSelector } from '../rows/gridRowsSelector'; -import { createSelectorMemoized } from '../../../utils/createSelector'; +import { createSelector } from '../../../utils/createSelector'; import type { GridGroupNode, GridRowId, GridRowTreeConfig } from '../../../models/gridRows'; import type { DataGridProcessedProps } from '../../../models/props/DataGridProps'; import type { GridPrivateApiCommunity, GridApiCommunity, } from '../../../models/api/gridApiCommunity'; +import type { GridRowSelectionPropagation } from '../../../models/gridRowSelectionModel'; + +export const ROW_SELECTION_PROPAGATION_DEFAULT: GridRowSelectionPropagation = { + parents: false, + descendants: false, +}; // TODO v8: Use `createSelectorV8` -function getGridRowGroupSelectableChildrenSelector( +function getGridRowGroupSelectableDescendantsSelector( apiRef: React.MutableRefObject, groupId: GridRowId, ) { - return createSelectorMemoized( + return createSelector( gridRowTreeSelector, gridSortedRowIdsSelector, gridFilteredRowsLookupSelector, @@ -28,7 +33,7 @@ function getGridRowGroupSelectableChildrenSelector( return []; } - const children: GridRowId[] = []; + const descendants: GridRowId[] = []; const startIndex = sortedRowIds.findIndex((id) => id === groupId) + 1; for ( @@ -38,17 +43,17 @@ function getGridRowGroupSelectableChildrenSelector( ) { const id = sortedRowIds[index]; if (filteredRowsLookup[id] !== false && apiRef.current.isRowSelectable(id)) { - children.push(id); + descendants.push(id); } } - return children; + return descendants; }, ); } // TODO v8: Use `createSelectorV8` export function getCheckboxPropsSelector(groupId: GridRowId) { - return createSelectorMemoized( + return createSelector( gridRowTreeSelector, gridSortedRowIdsSelector, gridFilteredRowsLookupSelector, @@ -81,7 +86,9 @@ export function getCheckboxPropsSelector(groupId: GridRowId) { } return { isIndeterminate: - selectedDescendentsCount > 0 && selectedDescendentsCount < selectableDescendentsCount, + (selectedDescendentsCount > 0 && selectedDescendentsCount < selectableDescendentsCount) || + (selectedDescendentsCount === selectableDescendentsCount && + rowSelectionLookup[groupId] === undefined), isChecked: selectedDescendentsCount > 0, }; }, @@ -142,16 +149,17 @@ export const findRowsToSelect = ( apiRef: React.MutableRefObject, tree: GridRowTreeConfig, selectedRow: GridRowId, - rowSelectionPropagation: GridRowSelectionPropagation, + autoSelectDescendants: boolean, + autoSelectParents: boolean, ) => { const filteredRows = gridFilteredRowsLookupSelector(apiRef); const rowsToSelect: GridRowId[] = []; - if (rowSelectionPropagation === 'none') { + if (!autoSelectDescendants && !autoSelectParents) { return rowsToSelect; } - if (rowSelectionPropagation === 'both' || rowSelectionPropagation === 'parents') { + if (autoSelectParents) { const traverseParents = (rowId: GridRowId) => { const siblings: GridRowId[] = getFilteredRowNodeSiblings(apiRef, tree, filteredRows, rowId); if ( @@ -169,16 +177,16 @@ export const findRowsToSelect = ( traverseParents(selectedRow); } - if (rowSelectionPropagation === 'both' || rowSelectionPropagation === 'children') { + if (autoSelectDescendants) { const rowNode = apiRef.current.getRowNode(selectedRow); if (rowNode?.type === 'group') { - const rowGroupChildrenSelector = getGridRowGroupSelectableChildrenSelector( + const rowGroupDescendantsSelector = getGridRowGroupSelectableDescendantsSelector( apiRef, selectedRow, ); - const children = rowGroupChildrenSelector(apiRef); - return rowsToSelect.concat(children); + const descendants = rowGroupDescendantsSelector(apiRef); + return rowsToSelect.concat(descendants); } } return rowsToSelect; @@ -188,15 +196,16 @@ export const findRowsToDeselect = ( apiRef: React.MutableRefObject, tree: GridRowTreeConfig, deselectedRow: GridRowId, - rowSelectionPropagation: GridRowSelectionPropagation, + autoSelectDescendants: boolean, + autoSelectParents: boolean, ) => { const rowsToDeselect: GridRowId[] = []; - if (rowSelectionPropagation === 'none') { + if (!autoSelectParents && !autoSelectDescendants) { return rowsToDeselect; } - if (rowSelectionPropagation === 'both' || rowSelectionPropagation === 'parents') { + if (autoSelectParents) { const allParents = getRowNodeParents(tree, deselectedRow); allParents.forEach((parent) => { const isSelected = apiRef.current.isRowSelected(parent); @@ -206,15 +215,15 @@ export const findRowsToDeselect = ( }); } - if (rowSelectionPropagation === 'both' || rowSelectionPropagation === 'children') { + if (autoSelectDescendants) { const rowNode = apiRef.current.getRowNode(deselectedRow); if (rowNode?.type === 'group') { - const rowGroupChildrenSelector = getGridRowGroupSelectableChildrenSelector( + const rowGroupDescendantsSelector = getGridRowGroupSelectableDescendantsSelector( apiRef, deselectedRow, ); - const children = rowGroupChildrenSelector(apiRef); - return rowsToDeselect.concat(children); + const descendants = rowGroupDescendantsSelector(apiRef); + return rowsToDeselect.concat(descendants); } } return rowsToDeselect; diff --git a/packages/x-data-grid/src/internals/index.ts b/packages/x-data-grid/src/internals/index.ts index 864b5b589013..19080fe44062 100644 --- a/packages/x-data-grid/src/internals/index.ts +++ b/packages/x-data-grid/src/internals/index.ts @@ -129,6 +129,7 @@ export { useGridColumnResize, columnResizeStateInitializer, } from '../hooks/features/columnResize/useGridColumnResize'; +export { ROW_SELECTION_PROPAGATION_DEFAULT } from '../hooks/features/rowSelection/utils'; export { useTimeout } from '../hooks/utils/useTimeout'; export { useGridVisibleRows, getVisibleRows } from '../hooks/utils/useGridVisibleRows'; diff --git a/packages/x-data-grid/src/models/gridRowSelectionModel.ts b/packages/x-data-grid/src/models/gridRowSelectionModel.ts index a354efcdeb78..92b98a2b8dca 100644 --- a/packages/x-data-grid/src/models/gridRowSelectionModel.ts +++ b/packages/x-data-grid/src/models/gridRowSelectionModel.ts @@ -1,6 +1,9 @@ import { GridRowId } from './gridRows'; -export type GridRowSelectionPropagation = 'none' | 'parents' | 'children' | 'both'; +export type GridRowSelectionPropagation = { + descendants?: boolean; + parents?: boolean; +}; export type GridInputRowSelectionModel = readonly GridRowId[] | GridRowId; diff --git a/packages/x-data-grid/src/models/props/DataGridProps.ts b/packages/x-data-grid/src/models/props/DataGridProps.ts index b082d51f01d6..316f05610003 100644 --- a/packages/x-data-grid/src/models/props/DataGridProps.ts +++ b/packages/x-data-grid/src/models/props/DataGridProps.ts @@ -818,16 +818,23 @@ export interface DataGridProSharedPropsWithDefaultValue { */ headerFilters: boolean; /** - * The following behavior happens for each of the possible values: - * 1. `none` - No row selection propagation. - * 2. `parents` - Selecting all children will auto-select the parent(s). - * 3. `children` - Selecting a parent will auto-select all its descendants. - * 4. `both` - Both `parents` and `children` behavior. + * When `rowSelectionPropagation.descendants` is set to `true`. + * - Selecting a parent will auto-select all its filtered descendants. + * - Deselecting a parent will auto-deselect all its filtered descendants. + * + * When `rowSelectionPropagation.parents=true` + * - Selecting all descendants of a parent would auto-select it. + * - Deselecting a descendant of a selected parent would deselect the parent. * * Works with tree data and row grouping on the client-side only. - * @default 'none' + * @default { parents: false, descendants: false } */ rowSelectionPropagation: GridRowSelectionPropagation; + /** + * If `true`, the rows will be gathered in a tree structure according to the `getTreeDataPath` prop. + * @default false + */ + treeData: boolean; } export interface DataGridProSharedPropsWithoutDefaultValue { From 79e1b91c1ba5265f3eed7df0bdefe81bc0f00ba8 Mon Sep 17 00:00:00 2001 From: Bilal Shafi Date: Thu, 26 Sep 2024 19:08:33 +0500 Subject: [PATCH 33/45] Handle faulty usecase with only auto select parents turned on --- .../GridCellCheckboxRenderer.tsx | 13 ++-- .../src/hooks/features/rowSelection/utils.ts | 66 ++++++++++++------- 2 files changed, 52 insertions(+), 27 deletions(-) diff --git a/packages/x-data-grid/src/components/columnSelection/GridCellCheckboxRenderer.tsx b/packages/x-data-grid/src/components/columnSelection/GridCellCheckboxRenderer.tsx index 99d687148029..174cefb025d0 100644 --- a/packages/x-data-grid/src/components/columnSelection/GridCellCheckboxRenderer.tsx +++ b/packages/x-data-grid/src/components/columnSelection/GridCellCheckboxRenderer.tsx @@ -86,15 +86,18 @@ const GridCellCheckboxForwardRef = React.forwardRef 0 && selectedDescendentsCount < selectableDescendentsCount) || (selectedDescendentsCount === selectableDescendentsCount && rowSelectionLookup[groupId] === undefined), - isChecked: selectedDescendentsCount > 0, + isChecked: autoSelectParents + ? selectedDescendentsCount > 0 + : rowSelectionLookup[groupId] === groupId, }; }, ); @@ -153,23 +161,48 @@ export const findRowsToSelect = ( autoSelectParents: boolean, ) => { const filteredRows = gridFilteredRowsLookupSelector(apiRef); - const rowsToSelect: GridRowId[] = []; + const selectedIdsLookup = selectedIdsLookupSelector(apiRef); + const rowsToSelect: Set = new Set([]); if (!autoSelectDescendants && !autoSelectParents) { return rowsToSelect; } + if (autoSelectDescendants) { + const rowNode = tree[selectedRow]; + + if (rowNode?.type === 'group') { + const rowGroupDescendantsSelector = getGridRowGroupSelectableDescendantsSelector( + apiRef, + selectedRow, + ); + const descendants = rowGroupDescendantsSelector(apiRef); + descendants.forEach(rowsToSelect.add, rowsToSelect); + } + } + if (autoSelectParents) { + const checkAllDescendantsSelected = (rowId: GridRowId): boolean => { + if (selectedIdsLookup[rowId] !== rowId && !rowsToSelect.has(rowId)) { + return false; + } + const node = tree[rowId]; + if (node?.type !== 'group') { + return true; + } + return node.children.every((child) => checkAllDescendantsSelected(child)); + }; + const traverseParents = (rowId: GridRowId) => { const siblings: GridRowId[] = getFilteredRowNodeSiblings(apiRef, tree, filteredRows, rowId); if ( siblings.length === 0 || - siblings.every((sibling) => apiRef.current.isRowSelected(sibling)) + siblings.every((sibling) => checkAllDescendantsSelected(sibling)) ) { const rowNode = apiRef.current.getRowNode(rowId) as GridGroupNode; const parent = rowNode.parent; if (parent && parent !== GRID_ROOT_GROUP_ID && apiRef.current.isRowSelectable(parent)) { - rowsToSelect.push(parent); + rowsToSelect.add(parent); traverseParents(parent); } } @@ -177,19 +210,7 @@ export const findRowsToSelect = ( traverseParents(selectedRow); } - if (autoSelectDescendants) { - const rowNode = apiRef.current.getRowNode(selectedRow); - - if (rowNode?.type === 'group') { - const rowGroupDescendantsSelector = getGridRowGroupSelectableDescendantsSelector( - apiRef, - selectedRow, - ); - const descendants = rowGroupDescendantsSelector(apiRef); - return rowsToSelect.concat(descendants); - } - } - return rowsToSelect; + return Array.from(rowsToSelect); }; export const findRowsToDeselect = ( @@ -200,6 +221,7 @@ export const findRowsToDeselect = ( autoSelectParents: boolean, ) => { const rowsToDeselect: GridRowId[] = []; + const selectedIdsLookup = selectedIdsLookupSelector(apiRef); if (!autoSelectParents && !autoSelectDescendants) { return rowsToDeselect; @@ -208,7 +230,7 @@ export const findRowsToDeselect = ( if (autoSelectParents) { const allParents = getRowNodeParents(tree, deselectedRow); allParents.forEach((parent) => { - const isSelected = apiRef.current.isRowSelected(parent); + const isSelected = selectedIdsLookup[parent] === parent; if (isSelected) { rowsToDeselect.push(parent); } @@ -216,7 +238,7 @@ export const findRowsToDeselect = ( } if (autoSelectDescendants) { - const rowNode = apiRef.current.getRowNode(deselectedRow); + const rowNode = tree[deselectedRow]; if (rowNode?.type === 'group') { const rowGroupDescendantsSelector = getGridRowGroupSelectableDescendantsSelector( apiRef, From 87d9aef3af6d31541d7f6b7364dc7ef8f02d3179 Mon Sep 17 00:00:00 2001 From: Bilal Shafi Date: Thu, 26 Sep 2024 19:24:34 +0500 Subject: [PATCH 34/45] Some small updates and docs improvement --- .../data-grid/row-grouping/row-grouping.md | 18 ++++++++++++------ .../src/hooks/features/rowSelection/utils.ts | 4 ++-- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/docs/data/data-grid/row-grouping/row-grouping.md b/docs/data/data-grid/row-grouping/row-grouping.md index 482d707c70c9..fd20397d1208 100644 --- a/docs/data/data-grid/row-grouping/row-grouping.md +++ b/docs/data/data-grid/row-grouping/row-grouping.md @@ -314,8 +314,8 @@ Here's how it's structured: ```ts type GridRowSelectionPropagation = { - descendants: boolean; - parents: boolean; + descendants?: boolean; // default: false + parents?: boolean; // default: false }; ``` @@ -326,20 +326,26 @@ When `rowSelectionPropagation.descendants` is set to `true`. When `rowSelectionPropagation.parents` is set to `true`. -- Selecting all the filtered descendants of a parent would auto-select it. -- Deselecting a descendant of a selected parent would deselect the parent. +- Selecting all the filtered descendants of a parent would auto-select the parent. +- Deselecting a descendant of a selected parent would auto-deselect the parent. The example below demonstrates the usage of the `rowSelectionPropagation` prop. {{"demo": "RowGroupingPropagateSelection.js", "bg": "inline", "defaultCodeOpen": false}} :::info -The `autoSelectDescendants` and `autoSelectParents` props will only affect the filtered rows. +The row selection propagation will also affect the "Select all" checkbox in a similar way like any other group checkbox. +::: + +:::info +The row selection propagation will only affect the filtered rows. If some rows were selected before filtering, auto selection will not be applied on them. + +The selected unfiltered rows will be auto-deselected when the filter is applied. ::: :::warning -If `props.disableMultipleRowSelection` is set to `true`, the row selection propagation feature will work like `'none'`. +If `props.disableMultipleRowSelection` is set to `true`, the row selection propagation feature will not work. ::: :::warning diff --git a/packages/x-data-grid/src/hooks/features/rowSelection/utils.ts b/packages/x-data-grid/src/hooks/features/rowSelection/utils.ts index 88c972bfee68..82a29c74ca5c 100644 --- a/packages/x-data-grid/src/hooks/features/rowSelection/utils.ts +++ b/packages/x-data-grid/src/hooks/features/rowSelection/utils.ts @@ -138,7 +138,7 @@ const getFilteredRowNodeSiblings = ( filteredRows: Record, id: GridRowId, ) => { - const node = apiRef.current.getRowNode(id); + const node = tree[id]; if (!node) { return []; } @@ -199,7 +199,7 @@ export const findRowsToSelect = ( siblings.length === 0 || siblings.every((sibling) => checkAllDescendantsSelected(sibling)) ) { - const rowNode = apiRef.current.getRowNode(rowId) as GridGroupNode; + const rowNode = tree[rowId] as GridGroupNode; const parent = rowNode.parent; if (parent && parent !== GRID_ROOT_GROUP_ID && apiRef.current.isRowSelectable(parent)) { rowsToSelect.add(parent); From 0ccad5bb4957f745ae40cd98ca27d4e28d1b3730 Mon Sep 17 00:00:00 2001 From: Bilal Shafi Date: Thu, 26 Sep 2024 19:41:44 +0500 Subject: [PATCH 35/45] Housekeeping --- docs/data/data-grid/row-grouping/row-grouping.md | 4 ++-- .../components/columnSelection/GridCellCheckboxRenderer.tsx | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/data/data-grid/row-grouping/row-grouping.md b/docs/data/data-grid/row-grouping/row-grouping.md index fd20397d1208..002f4fce1bfa 100644 --- a/docs/data/data-grid/row-grouping/row-grouping.md +++ b/docs/data/data-grid/row-grouping/row-grouping.md @@ -314,8 +314,8 @@ Here's how it's structured: ```ts type GridRowSelectionPropagation = { - descendants?: boolean; // default: false - parents?: boolean; // default: false + descendants?: boolean; // default: false + parents?: boolean; // default: false }; ``` diff --git a/packages/x-data-grid/src/components/columnSelection/GridCellCheckboxRenderer.tsx b/packages/x-data-grid/src/components/columnSelection/GridCellCheckboxRenderer.tsx index 174cefb025d0..5bfadd9b96f7 100644 --- a/packages/x-data-grid/src/components/columnSelection/GridCellCheckboxRenderer.tsx +++ b/packages/x-data-grid/src/components/columnSelection/GridCellCheckboxRenderer.tsx @@ -90,7 +90,7 @@ const GridCellCheckboxForwardRef = React.forwardRef Date: Thu, 26 Sep 2024 23:19:44 +0500 Subject: [PATCH 36/45] Fix lint-check --- docs/data/data-grid/tree-data/tree-data.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/data/data-grid/tree-data/tree-data.md b/docs/data/data-grid/tree-data/tree-data.md index 31ad8d277e19..b6c67e6528cb 100644 --- a/docs/data/data-grid/tree-data/tree-data.md +++ b/docs/data/data-grid/tree-data/tree-data.md @@ -89,7 +89,7 @@ Same behavior as for the [Row grouping](/x/react-data-grid/row-grouping/#group-e ## Automatic parents and children selection -Same behavior as for the [Row grouping](/x/react-data-grid/row-grouping/#propagate-row-selection). +Same behavior as for the [Row grouping](/x/react-data-grid/row-grouping/#automatic-parents-and-children-selection). ## Gaps in the tree From 0427cf5c746b9aa85968de2c82c8e12418e78e89 Mon Sep 17 00:00:00 2001 From: Bilal Shafi Date: Sat, 28 Sep 2024 11:59:28 +0500 Subject: [PATCH 37/45] Cleanup --- .../data/data-grid/row-grouping/row-grouping.md | 17 +++++++---------- .../src/models/dataGridProProps.ts | 5 +++++ .../rowSelection/useGridRowSelection.ts | 10 +++++++++- .../src/models/props/DataGridProps.ts | 5 ----- 4 files changed, 21 insertions(+), 16 deletions(-) diff --git a/docs/data/data-grid/row-grouping/row-grouping.md b/docs/data/data-grid/row-grouping/row-grouping.md index 002f4fce1bfa..8430b6047dc1 100644 --- a/docs/data/data-grid/row-grouping/row-grouping.md +++ b/docs/data/data-grid/row-grouping/row-grouping.md @@ -307,7 +307,7 @@ If you are dynamically switching the `leafField` or `mainGroupingCriteria`, the ## Automatic parents and children selection -By default, selecting a parent row will not select its children. +By default, selecting a parent row does not select its children. You can override this behavior by using the `rowSelectionPropagation` prop. Here's how it's structured: @@ -321,8 +321,8 @@ type GridRowSelectionPropagation = { When `rowSelectionPropagation.descendants` is set to `true`. -- Selecting a parent will auto-select all its filtered descendants. -- Deselecting a parent row will auto-deselect all its filtered descendants. +- Selecting a parent would auto-select all its filtered descendants. +- Deselecting a parent row would auto-deselect all its filtered descendants. When `rowSelectionPropagation.parents` is set to `true`. @@ -334,22 +334,19 @@ The example below demonstrates the usage of the `rowSelectionPropagation` prop. {{"demo": "RowGroupingPropagateSelection.js", "bg": "inline", "defaultCodeOpen": false}} :::info -The row selection propagation will also affect the "Select all" checkbox in a similar way like any other group checkbox. +The row selection propagation also affects the "Select all" checkbox in a similar way like any other group checkbox. ::: :::info -The row selection propagation will only affect the filtered rows. -If some rows were selected before filtering, auto selection will not be applied on them. - -The selected unfiltered rows will be auto-deselected when the filter is applied. +The selected unfiltered rows will be auto-deselected when the filter is applied. Row selection propagation is not applied to the unfiltered rows. ::: :::warning -If `props.disableMultipleRowSelection` is set to `true`, the row selection propagation feature will not work. +If `props.disableMultipleRowSelection` is set to `true`, the row selection propagation feature would not work. ::: :::warning -The row selection propagation is a client-side feature and not recommended to be used with the [server-side data source](/x/react-data-grid/server-side-data/), since it will only work on the partially loaded data. +The row selection propagation is a client-side feature and not recommended to be used with the [server-side data source](/x/react-data-grid/server-side-data/), since it would work on the partially loaded data only. ::: ## Get the rows in a group diff --git a/packages/x-data-grid-pro/src/models/dataGridProProps.ts b/packages/x-data-grid-pro/src/models/dataGridProProps.ts index aa159f0baedd..1af6cbe61f58 100644 --- a/packages/x-data-grid-pro/src/models/dataGridProProps.ts +++ b/packages/x-data-grid-pro/src/models/dataGridProProps.ts @@ -79,6 +79,11 @@ export interface DataGridProPropsWithDefaultValue( (model) => { if ( @@ -429,6 +431,9 @@ export const useGridRowSelection = ( props.signature === GridSignature.DataGrid ? 'private' : 'public', ); + /* + * EVENTS + */ const removeOutdatedSelection = React.useCallback( (sortModelUpdated = false) => { const currentSelection = gridRowSelectionStateSelector(apiRef.current.state); @@ -696,6 +701,9 @@ export const useGridRowSelection = ( ); useGridApiEventHandler(apiRef, 'cellKeyDown', runIfRowSelectionIsEnabled(handleCellKeyDown)); + /* + * EFFECTS + */ React.useEffect(() => { if (propRowSelectionModel !== undefined) { apiRef.current.setRowSelectionModel(propRowSelectionModel); diff --git a/packages/x-data-grid/src/models/props/DataGridProps.ts b/packages/x-data-grid/src/models/props/DataGridProps.ts index 0607d7e52aa2..8d4495333596 100644 --- a/packages/x-data-grid/src/models/props/DataGridProps.ts +++ b/packages/x-data-grid/src/models/props/DataGridProps.ts @@ -845,11 +845,6 @@ export interface DataGridProSharedPropsWithDefaultValue { * @default { parents: false, descendants: false } */ rowSelectionPropagation: GridRowSelectionPropagation; - /** - * If `true`, the rows will be gathered in a tree structure according to the `getTreeDataPath` prop. - * @default false - */ - treeData: boolean; } export interface DataGridProSharedPropsWithoutDefaultValue { From 4cb493aaa8fa927de6b134ed7ababb7ce2503890 Mon Sep 17 00:00:00 2001 From: Bilal Shafi Date: Sat, 28 Sep 2024 15:21:30 +0500 Subject: [PATCH 38/45] Add tests --- .../rowSelection.DataGridPremium.test.tsx | 99 ++- .../tests/rowSelection.DataGridPro.test.tsx | 597 +++++++++++++----- 2 files changed, 494 insertions(+), 202 deletions(-) diff --git a/packages/x-data-grid-premium/src/tests/rowSelection.DataGridPremium.test.tsx b/packages/x-data-grid-premium/src/tests/rowSelection.DataGridPremium.test.tsx index ae52bbe755a5..d330b84ebd8c 100644 --- a/packages/x-data-grid-premium/src/tests/rowSelection.DataGridPremium.test.tsx +++ b/packages/x-data-grid-premium/src/tests/rowSelection.DataGridPremium.test.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import { createRenderer, fireEvent, act } from '@mui/internal-test-utils'; +import { createRenderer, fireEvent } from '@mui/internal-test-utils'; import { getCell } from 'test/utils/helperFn'; import { expect } from 'chai'; import { @@ -45,7 +45,7 @@ const baselineProps: BaselineProps = { describe(' - Row selection', () => { const { render } = createRenderer(); - describe('props: rowSelectionPropagation.descendants=true & rowSelectionPropagation.parents=true', () => { + describe('props: rowSelectionPropagation = { descendants: true, parents: true }', () => { let apiRef: React.MutableRefObject; function Test(props: Partial) { @@ -61,6 +61,7 @@ describe(' - Row selection', () => { descendants: true, parents: true, }} + initialState={{ rowGrouping: { model: ['category1'] } }} {...props} />
@@ -68,7 +69,7 @@ describe(' - Row selection', () => { } it('should select all the children when selecting a parent', () => { - render(); + render(); fireEvent.click(getCell(1, 0).querySelector('input')!); expect(apiRef.current.getSelectedRows()).to.have.keys([ @@ -79,7 +80,7 @@ describe(' - Row selection', () => { }); it('should deselect all the children when deselecting a parent', () => { - render(); + render(); fireEvent.click(getCell(1, 0).querySelector('input')!); expect(apiRef.current.getSelectedRows()).to.have.keys([ @@ -91,27 +92,8 @@ describe(' - Row selection', () => { expect(apiRef.current.getSelectedRows().size).to.equal(0); }); - it('should put the parent into indeterminate if some but not all the children are selected', () => { - render( - , - ); - - fireEvent.click(getCell(1, 0).querySelector('input')!); - expect(getCell(0, 0).querySelector('input')!).to.have.attr('data-indeterminate', 'true'); - }); - it('should auto select the parent if all the children are selected', () => { - render( - , - ); + render(); fireEvent.click(getCell(1, 0).querySelector('input')!); fireEvent.click(getCell(2, 0).querySelector('input')!); @@ -125,13 +107,7 @@ describe(' - Row selection', () => { }); it('should deselect auto selected parent if one of the children is deselected', () => { - render( - , - ); + render(); fireEvent.click(getCell(1, 0).querySelector('input')!); fireEvent.click(getCell(2, 0).querySelector('input')!); @@ -146,34 +122,43 @@ describe(' - Row selection', () => { expect(apiRef.current.getSelectedRows()).to.have.keys([0, 2]); }); - it('should deselect unfiltered rows after filtering', () => { - render( - , - ); - - fireEvent.click(getCell(1, 0).querySelector('input')!); - fireEvent.click(getCell(2, 0).querySelector('input')!); - fireEvent.click(getCell(3, 0).querySelector('input')!); - fireEvent.click(getCell(5, 0).querySelector('input')!); + describe("prop: indeterminateCheckboxAction = 'select'", () => { + it('should select all the children when selecting an indeterminate parent', () => { + render( + , + ); + + fireEvent.click(getCell(2, 0).querySelector('input')!); + expect(getCell(0, 0).querySelector('input')!).to.have.attr('data-indeterminate', 'true'); + fireEvent.click(getCell(0, 0).querySelector('input')!); + expect(apiRef.current.getSelectedRows()).to.have.keys([ + 0, + 1, + 2, + 'auto-generated-row-category1/Cat A', + ]); + }); + }); - expect(apiRef.current.getSelectedRows()).to.have.keys([ - 0, - 1, - 2, - 'auto-generated-row-category1/Cat A', - 3, - ]); - act(() => { - apiRef.current.setFilterModel({ - items: [], - quickFilterValues: ['Cat B'], - }); + describe("prop: indeterminateCheckboxAction = 'deselect'", () => { + it('should deselect all the children when selecting an indeterminate parent', () => { + render( + , + ); + + fireEvent.click(getCell(2, 0).querySelector('input')!); + expect(getCell(0, 0).querySelector('input')!).to.have.attr('data-indeterminate', 'true'); + fireEvent.click(getCell(0, 0).querySelector('input')!); + expect(apiRef.current.getSelectedRows().size).to.equal(0); }); - expect(apiRef.current.getSelectedRows()).to.have.keys([3]); }); }); }); diff --git a/packages/x-data-grid-pro/src/tests/rowSelection.DataGridPro.test.tsx b/packages/x-data-grid-pro/src/tests/rowSelection.DataGridPro.test.tsx index 06d182707cb6..23994a6684d0 100644 --- a/packages/x-data-grid-pro/src/tests/rowSelection.DataGridPro.test.tsx +++ b/packages/x-data-grid-pro/src/tests/rowSelection.DataGridPro.test.tsx @@ -46,6 +46,161 @@ describe(' - Row selection', () => { ); } + 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: GridColDef[] = [ + { field: 'jobTitle', headerName: 'Job Title', width: 200 }, + { + field: 'recruitmentDate', + headerName: 'Recruitment Date', + type: 'date', + width: 150, + }, + ]; + + const getTreeDataPath: DataGridProProps['getTreeDataPath'] = (row) => row.hierarchy; + + function TreeDataGrid(props: Partial) { + apiRef = useGridApiRef(); + return ( +
+ +
+ ); + } + + it('should keep the previously selected tree data parent selected if it becomes leaf after filtering', () => { + render(); + + fireEvent.click( + screen.getByRole('checkbox', { + name: /select all rows/i, + }), + ); + + expect(apiRef.current.getSelectedRows()).to.have.length(15); + + act(() => { + apiRef.current.setFilterModel({ + items: [ + { + field: 'jobTitle', + value: 'Head of Sales', + operator: 'equals', + }, + ], + }); + }); + + expect(apiRef.current.getSelectedRows()).to.have.keys([1]); + }); + + it('should put the parent into indeterminate if some but not all the children are selected', () => { + render(); + + fireEvent.click(getCell(2, 0).querySelector('input')!); + expect(getCell(1, 0).querySelector('input')!).to.have.attr('data-indeterminate', 'true'); + }); + describe('prop: checkboxSelectionVisibleOnly = false', () => { it('should select all rows of all pages if no row is selected', () => { render( @@ -223,16 +378,14 @@ describe(' - Row selection', () => { const data = React.useMemo(() => getBasicGridData(rowLength, 2), [rowLength]); - const rows = data.rows.slice( - paginationModel.pageSize * paginationModel.page, - paginationModel.pageSize * (paginationModel.page + 1), - ); - return (
- Row selection', () => { }); }); - describe('props: rowSelectionPropagation.descendants=true & rowSelectionPropagation.parents=true', () => { - 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: GridColDef[] = [ - { field: 'jobTitle', headerName: 'Job Title', width: 200 }, - { - field: 'recruitmentDate', - headerName: 'Recruitment Date', - type: 'date', - width: 150, - }, - ]; - - const getTreeDataPath: DataGridProProps['getTreeDataPath'] = (row) => row.hierarchy; - - function TreeDataGrid(props: Partial) { - apiRef = useGridApiRef(); + describe('prop: rowSelectionPropagation = { descendants: false, parents: false }', () => { + function SelectionPropagationGrid(props: Partial) { + return ( + + ); + } + + it('should select the parent only when selecting it', () => { + render(); + + fireEvent.click(getCell(1, 0).querySelector('input')!); + expect(apiRef.current.getSelectedRows()).to.have.keys([1]); + }); + + it('should deselect the parent only when deselecting it', () => { + render(); + + fireEvent.click(getCell(1, 0).querySelector('input')!); + fireEvent.click(getCell(2, 0).querySelector('input')!); + expect(apiRef.current.getSelectedRows()).to.have.keys([1, 2]); + fireEvent.click(getCell(1, 0).querySelector('input')!); + expect(apiRef.current.getSelectedRows()).to.have.keys([2]); + }); + + it('should not auto select the parent if all the children are selected', () => { + render(); + + fireEvent.click(getCell(2, 0).querySelector('input')!); + fireEvent.click(getCell(3, 0).querySelector('input')!); + fireEvent.click(getCell(4, 0).querySelector('input')!); + fireEvent.click(getCell(5, 0).querySelector('input')!); + fireEvent.click(getCell(6, 0).querySelector('input')!); + fireEvent.click(getCell(7, 0).querySelector('input')!); + // The parent row (Thomas, id: 1) should not be among the selected rows + expect(apiRef.current.getSelectedRows()).to.have.keys([2, 3, 4, 5, 6, 7]); + }); + + it('should not deselect selected parent if one of the children is deselected', () => { + render(); + + fireEvent.click(getCell(1, 0).querySelector('input')!); + fireEvent.click(getCell(2, 0).querySelector('input')!); + fireEvent.click(getCell(3, 0).querySelector('input')!); + fireEvent.click(getCell(4, 0).querySelector('input')!); + fireEvent.click(getCell(5, 0).querySelector('input')!); + fireEvent.click(getCell(6, 0).querySelector('input')!); + fireEvent.click(getCell(7, 0).querySelector('input')!); + expect(apiRef.current.getSelectedRows()).to.have.keys([1, 2, 3, 4, 5, 6, 7]); + fireEvent.click(getCell(2, 0).querySelector('input')!); + // The parent row (Thomas, id: 1) should still be among the selected rows + expect(apiRef.current.getSelectedRows()).to.have.keys([1, 3, 4, 5, 6, 7]); + }); + + it('should select only the unwrapped rows when clicking "Select All" checkbox', () => { + render(); + + fireEvent.click(screen.getByRole('checkbox', { name: /select all rows/i })); + expect(apiRef.current.getSelectedRows()).to.have.keys([0, 1, 8]); + }); + + it('should deselect only the unwrapped rows when clicking "Select All" checkbox', () => { + render(); + + fireEvent.click(screen.getByRole('checkbox', { name: /select all rows/i })); + expect(apiRef.current.getSelectedRows()).to.have.keys([0, 1, 8]); + fireEvent.click(screen.getByRole('checkbox', { name: /select all rows/i })); + expect(apiRef.current.getSelectedRows().size).to.equal(0); + }); + }); + + describe('prop: rowSelectionPropagation = { descendants: true, parents: false }', () => { + function SelectionPropagationGrid(props: Partial) { return ( -
- -
+ ); } it('should select all the children when selecting a parent', () => { - render(); + render(); fireEvent.click(getCell(1, 0).querySelector('input')!); expect(apiRef.current.getSelectedRows()).to.have.keys([1, 2, 3, 4, 5, 6, 7]); }); it('should deselect all the children when deselecting a parent', () => { - render(); + render(); fireEvent.click(getCell(1, 0).querySelector('input')!); expect(apiRef.current.getSelectedRows()).to.have.keys([1, 2, 3, 4, 5, 6, 7]); @@ -434,15 +540,173 @@ describe(' - Row selection', () => { expect(apiRef.current.getSelectedRows().size).to.equal(0); }); - it('should put the parent into indeterminate if some but not all the children are selected', () => { - render(); + it('should not auto select the parent if all the children are selected', () => { + render(); - fireEvent.click(getCell(11, 0).querySelector('input')!); - expect(getCell(8, 0).querySelector('input')!).to.have.attr('data-indeterminate', 'true'); + fireEvent.click(getCell(2, 0).querySelector('input')!); + fireEvent.click(getCell(3, 0).querySelector('input')!); + fireEvent.click(getCell(4, 0).querySelector('input')!); + fireEvent.click(getCell(5, 0).querySelector('input')!); + fireEvent.click(getCell(6, 0).querySelector('input')!); + fireEvent.click(getCell(7, 0).querySelector('input')!); + // The parent row (Thomas, id: 1) should not be among the selected rows + expect(apiRef.current.getSelectedRows()).to.have.keys([2, 3, 4, 5, 6, 7]); + }); + + it('should not deselect selected parent if one of the children is deselected', () => { + render(); + + fireEvent.click(getCell(1, 0).querySelector('input')!); + expect(apiRef.current.getSelectedRows()).to.have.keys([1, 2, 3, 4, 5, 6, 7]); + fireEvent.click(getCell(2, 0).querySelector('input')!); + // The parent row (Thomas, id: 1) should still be among the selected rows + expect(apiRef.current.getSelectedRows()).to.have.keys([1, 3, 4, 5, 6, 7]); + }); + + it('should select all the nested rows when clicking "Select All" checkbox', () => { + render(); + + fireEvent.click(screen.getByRole('checkbox', { name: /select all rows/i })); + expect(apiRef.current.getSelectedRows().size).to.equal(15); + }); + + it('should deselect all the nested rows when clicking "Select All" checkbox', () => { + render(); + + fireEvent.click(screen.getByRole('checkbox', { name: /select all rows/i })); + expect(apiRef.current.getSelectedRows().size).to.equal(15); + fireEvent.click(screen.getByRole('checkbox', { name: /select all rows/i })); + expect(apiRef.current.getSelectedRows().size).to.equal(0); + }); + + describe('prop: isRowSelectable', () => { + it("should not select a parent or it's descendants if not allowed", () => { + render( + params.id !== 1} + />, + ); + + fireEvent.click(getCell(1, 0).querySelector('input')!); + expect(apiRef.current.getSelectedRows().size).to.equal(0); + }); + + it('should not auto-select a descendant if not allowed', () => { + render( + params.id !== 2} + />, + ); + + fireEvent.click(getCell(1, 0).querySelector('input')!); + expect(apiRef.current.getSelectedRows()).to.have.keys([1, 3, 4, 5, 6, 7]); + }); + }); + }); + + describe('prop: rowSelectionPropagation = { descendants: false, parents: true }', () => { + function SelectionPropagationGrid(props: Partial) { + return ( + + ); + } + + it('should select the parent only when selecting it', () => { + render(); + + fireEvent.click(getCell(1, 0).querySelector('input')!); + expect(apiRef.current.getSelectedRows()).to.have.keys([1]); + }); + + it('should deselect the parent only when deselecting it', () => { + render(); + + fireEvent.click(getCell(1, 0).querySelector('input')!); + fireEvent.click(getCell(2, 0).querySelector('input')!); + expect(apiRef.current.getSelectedRows()).to.have.keys([1, 2]); + fireEvent.click(getCell(1, 0).querySelector('input')!); + expect(apiRef.current.getSelectedRows()).to.have.keys([2]); }); it('should auto select the parent if all the children are selected', () => { - render(); + render(); + + fireEvent.click(getCell(2, 0).querySelector('input')!); + fireEvent.click(getCell(3, 0).querySelector('input')!); + fireEvent.click(getCell(4, 0).querySelector('input')!); + fireEvent.click(getCell(5, 0).querySelector('input')!); + fireEvent.click(getCell(6, 0).querySelector('input')!); + fireEvent.click(getCell(7, 0).querySelector('input')!); + // The parent row (Thomas, id: 1) should be among the selected rows + expect(apiRef.current.getSelectedRows()).to.have.keys([2, 3, 4, 5, 6, 7, 1]); + }); + + it('should deselect selected parent if one of the children is deselected', () => { + render(); + + fireEvent.click(getCell(2, 0).querySelector('input')!); + fireEvent.click(getCell(3, 0).querySelector('input')!); + fireEvent.click(getCell(4, 0).querySelector('input')!); + fireEvent.click(getCell(5, 0).querySelector('input')!); + fireEvent.click(getCell(6, 0).querySelector('input')!); + fireEvent.click(getCell(7, 0).querySelector('input')!); + expect(apiRef.current.getSelectedRows()).to.have.keys([2, 3, 4, 5, 6, 7, 1]); + fireEvent.click(getCell(2, 0).querySelector('input')!); + // The parent row (Thomas, id: 1) should not be among the selected rows + expect(apiRef.current.getSelectedRows()).to.have.keys([3, 4, 5, 6, 7]); + }); + + describe('prop: isRowSelectable', () => { + it('should not auto select a parent if not allowed', () => { + render( + params.id !== 1} + />, + ); + + fireEvent.click(getCell(2, 0).querySelector('input')!); + fireEvent.click(getCell(3, 0).querySelector('input')!); + fireEvent.click(getCell(4, 0).querySelector('input')!); + fireEvent.click(getCell(5, 0).querySelector('input')!); + fireEvent.click(getCell(6, 0).querySelector('input')!); + fireEvent.click(getCell(7, 0).querySelector('input')!); + // The parent row (Thomas, id: 1) should still not be among the selected rows + expect(apiRef.current.getSelectedRows()).to.have.keys([2, 3, 4, 5, 6, 7]); + }); + }); + }); + + describe('prop: rowSelectionPropagation = { descendants: true, parents: true }', () => { + function SelectionPropagationGrid(props: Partial) { + return ( + + ); + } + + it('should select all the children when selecting a parent', () => { + render(); + + fireEvent.click(getCell(1, 0).querySelector('input')!); + expect(apiRef.current.getSelectedRows()).to.have.keys([1, 2, 3, 4, 5, 6, 7]); + }); + + it('should deselect all the children when deselecting a parent', () => { + render(); + + fireEvent.click(getCell(1, 0).querySelector('input')!); + expect(apiRef.current.getSelectedRows()).to.have.keys([1, 2, 3, 4, 5, 6, 7]); + fireEvent.click(getCell(1, 0).querySelector('input')!); + expect(apiRef.current.getSelectedRows().size).to.equal(0); + }); + + it('should auto select the parent if all the children are selected', () => { + render(); fireEvent.click(getCell(9, 0).querySelector('input')!); fireEvent.click(getCell(11, 0).querySelector('input')!); @@ -453,7 +717,7 @@ describe(' - Row selection', () => { }); it('should deselect auto selected parent if one of the children is deselected', () => { - render(); + render(); fireEvent.click(getCell(9, 0).querySelector('input')!); fireEvent.click(getCell(11, 0).querySelector('input')!); @@ -463,20 +727,63 @@ describe(' - Row selection', () => { expect(apiRef.current.getSelectedRows()).to.have.keys([11, 12, 13, 14]); }); - it('should deselect unfiltered rows after filtering', () => { - render(); + describe("prop: indeterminateCheckboxAction = 'select'", () => { + it('should select all the children when selecting an indeterminate parent', () => { + render( + , + ); - fireEvent.click(getCell(9, 0).querySelector('input')!); - fireEvent.click(getCell(11, 0).querySelector('input')!); - fireEvent.click(getCell(12, 0).querySelector('input')!); - expect(apiRef.current.getSelectedRows()).to.have.keys([9, 10, 11, 12, 8, 13, 14]); - act(() => { - apiRef.current.setFilterModel({ - items: [], - quickFilterValues: ['Linda'], + fireEvent.click(getCell(2, 0).querySelector('input')!); + expect(getCell(1, 0).querySelector('input')!).to.have.attr('data-indeterminate', 'true'); + fireEvent.click(getCell(1, 0).querySelector('input')!); + expect(apiRef.current.getSelectedRows()).to.have.keys([1, 2, 3, 4, 5, 6, 7]); + }); + }); + + describe("prop: indeterminateCheckboxAction = 'deselect'", () => { + it('should deselect all the children when selecting an indeterminate parent', () => { + render( + , + ); + + fireEvent.click(getCell(2, 0).querySelector('input')!); + expect(getCell(1, 0).querySelector('input')!).to.have.attr('data-indeterminate', 'true'); + fireEvent.click(getCell(1, 0).querySelector('input')!); + expect(apiRef.current.getSelectedRows().size).to.equal(0); + }); + }); + + describe('prop: keepNonExistentRowsSelected = true', () => { + it('should keep non-existent rows selected on filtering', () => { + render(); + + fireEvent.click(getCell(1, 0).querySelector('input')!); + expect(apiRef.current.getSelectedRows()).to.have.keys([1, 2, 3, 4, 5, 6, 7]); + + act(() => { + apiRef.current.setFilterModel({ + items: [ + { + field: 'jobTitle', + value: 'Head of Human Resources', + operator: 'equals', + }, + ], + }); }); + + fireEvent.click(getCell(0, 0).querySelector('input')!); + + expect(apiRef.current.getSelectedRows()).to.have.keys([0, 1, 2, 3, 4, 5, 6, 7]); }); - expect(apiRef.current.getSelectedRows()).to.have.keys([12, 8]); }); }); From c16e7f4b8d60f1a59729555ee8cdad12a0a06eb2 Mon Sep 17 00:00:00 2001 From: Bilal Shafi Date: Tue, 1 Oct 2024 21:34:05 +0500 Subject: [PATCH 39/45] Improve check for id = 0 --- .../src/hooks/features/rowSelection/utils.ts | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/packages/x-data-grid/src/hooks/features/rowSelection/utils.ts b/packages/x-data-grid/src/hooks/features/rowSelection/utils.ts index 82a29c74ca5c..a2af0ddb72b8 100644 --- a/packages/x-data-grid/src/hooks/features/rowSelection/utils.ts +++ b/packages/x-data-grid/src/hooks/features/rowSelection/utils.ts @@ -18,6 +18,9 @@ export const ROW_SELECTION_PROPAGATION_DEFAULT: GridRowSelectionPropagation = { descendants: false, }; +const isValidRowId = (id: GridRowId | null): id is GridRowId => + id !== null && (typeof id === 'number' || typeof id === 'string'); + // TODO v8: Use `createSelectorV8` function getGridRowGroupSelectableDescendantsSelector( apiRef: React.MutableRefObject, @@ -121,7 +124,7 @@ const getRowNodeParents = (tree: GridRowTreeConfig, id: GridRowId) => { let parent: GridRowId | null = id; - while (parent && parent !== GRID_ROOT_GROUP_ID) { + while (isValidRowId(parent) && parent !== GRID_ROOT_GROUP_ID) { const node = tree[parent] as GridGroupNode; if (!node) { return parents; @@ -133,7 +136,6 @@ const getRowNodeParents = (tree: GridRowTreeConfig, id: GridRowId) => { }; const getFilteredRowNodeSiblings = ( - apiRef: React.MutableRefObject, tree: GridRowTreeConfig, filteredRows: Record, id: GridRowId, @@ -144,7 +146,7 @@ const getFilteredRowNodeSiblings = ( } const parent = node.parent; - if (!parent) { + if (!isValidRowId(parent)) { return []; } @@ -194,14 +196,14 @@ export const findRowsToSelect = ( }; const traverseParents = (rowId: GridRowId) => { - const siblings: GridRowId[] = getFilteredRowNodeSiblings(apiRef, tree, filteredRows, rowId); + const siblings: GridRowId[] = getFilteredRowNodeSiblings(tree, filteredRows, rowId); if ( siblings.length === 0 || siblings.every((sibling) => checkAllDescendantsSelected(sibling)) ) { const rowNode = tree[rowId] as GridGroupNode; const parent = rowNode.parent; - if (parent && parent !== GRID_ROOT_GROUP_ID && apiRef.current.isRowSelectable(parent)) { + if (isValidRowId(parent) && parent !== GRID_ROOT_GROUP_ID && apiRef.current.isRowSelectable(parent)) { rowsToSelect.add(parent); traverseParents(parent); } From 3548c1f4f39ad14ea2d55678f2619d7b8c79e04c Mon Sep 17 00:00:00 2001 From: Bilal Shafi Date: Wed, 2 Oct 2024 17:16:16 +0500 Subject: [PATCH 40/45] Resolve a few comments --- .../src/hooks/features/rowSelection/utils.ts | 87 ++++++++----------- 1 file changed, 37 insertions(+), 50 deletions(-) diff --git a/packages/x-data-grid/src/hooks/features/rowSelection/utils.ts b/packages/x-data-grid/src/hooks/features/rowSelection/utils.ts index a2af0ddb72b8..29077c86545f 100644 --- a/packages/x-data-grid/src/hooks/features/rowSelection/utils.ts +++ b/packages/x-data-grid/src/hooks/features/rowSelection/utils.ts @@ -18,40 +18,32 @@ export const ROW_SELECTION_PROPAGATION_DEFAULT: GridRowSelectionPropagation = { descendants: false, }; -const isValidRowId = (id: GridRowId | null): id is GridRowId => - id !== null && (typeof id === 'number' || typeof id === 'string'); - -// TODO v8: Use `createSelectorV8` -function getGridRowGroupSelectableDescendantsSelector( +function getGridRowGroupSelectableDescendants( apiRef: React.MutableRefObject, groupId: GridRowId, ) { - return createSelector( - gridRowTreeSelector, - gridSortedRowIdsSelector, - gridFilteredRowsLookupSelector, - (rowTree, sortedRowIds, filteredRowsLookup) => { - const groupNode = rowTree[groupId]; - if (!groupNode || groupNode.type !== 'group') { - return []; - } - - const descendants: GridRowId[] = []; + const rowTree = gridRowTreeSelector(apiRef); + const sortedRowIds = gridSortedRowIdsSelector(apiRef); + const filteredRowsLookup = gridFilteredRowsLookupSelector(apiRef); + const groupNode = rowTree[groupId]; + if (!groupNode || groupNode.type !== 'group') { + return []; + } - const startIndex = sortedRowIds.findIndex((id) => id === groupId) + 1; - for ( - let index = startIndex; - index < sortedRowIds.length && rowTree[sortedRowIds[index]]?.depth > groupNode.depth; - index += 1 - ) { - const id = sortedRowIds[index]; - if (filteredRowsLookup[id] !== false && apiRef.current.isRowSelectable(id)) { - descendants.push(id); - } - } - return descendants; - }, - ); + const descendants: GridRowId[] = []; + + const startIndex = sortedRowIds.findIndex((id) => id === groupId) + 1; + for ( + let index = startIndex; + index < sortedRowIds.length && rowTree[sortedRowIds[index]]?.depth > groupNode.depth; + index += 1 + ) { + const id = sortedRowIds[index]; + if (filteredRowsLookup[id] !== false && apiRef.current.isRowSelectable(id)) { + descendants.push(id); + } + } + return descendants; } // TODO v8: Use `createSelectorV8` @@ -124,7 +116,7 @@ const getRowNodeParents = (tree: GridRowTreeConfig, id: GridRowId) => { let parent: GridRowId | null = id; - while (isValidRowId(parent) && parent !== GRID_ROOT_GROUP_ID) { + while (parent != null && parent !== GRID_ROOT_GROUP_ID) { const node = tree[parent] as GridGroupNode; if (!node) { return parents; @@ -146,13 +138,13 @@ const getFilteredRowNodeSiblings = ( } const parent = node.parent; - if (!isValidRowId(parent)) { + if (parent == null) { return []; } const parentNode = tree[parent] as GridGroupNode; - return [...parentNode.children].filter((childId) => childId !== id && filteredRows[childId]); + return parentNode.children.filter((childId) => childId !== id && filteredRows[childId]); }; export const findRowsToSelect = ( @@ -174,11 +166,7 @@ export const findRowsToSelect = ( const rowNode = tree[selectedRow]; if (rowNode?.type === 'group') { - const rowGroupDescendantsSelector = getGridRowGroupSelectableDescendantsSelector( - apiRef, - selectedRow, - ); - const descendants = rowGroupDescendantsSelector(apiRef); + const descendants = getGridRowGroupSelectableDescendants(apiRef, selectedRow); descendants.forEach(rowsToSelect.add, rowsToSelect); } } @@ -192,18 +180,19 @@ export const findRowsToSelect = ( if (node?.type !== 'group') { return true; } - return node.children.every((child) => checkAllDescendantsSelected(child)); + return node.children.every(checkAllDescendantsSelected); }; const traverseParents = (rowId: GridRowId) => { const siblings: GridRowId[] = getFilteredRowNodeSiblings(tree, filteredRows, rowId); - if ( - siblings.length === 0 || - siblings.every((sibling) => checkAllDescendantsSelected(sibling)) - ) { + if (siblings.length === 0 || siblings.every(checkAllDescendantsSelected)) { const rowNode = tree[rowId] as GridGroupNode; const parent = rowNode.parent; - if (isValidRowId(parent) && parent !== GRID_ROOT_GROUP_ID && apiRef.current.isRowSelectable(parent)) { + if ( + parent != null && + parent !== GRID_ROOT_GROUP_ID && + apiRef.current.isRowSelectable(parent) + ) { rowsToSelect.add(parent); traverseParents(parent); } @@ -242,12 +231,10 @@ export const findRowsToDeselect = ( if (autoSelectDescendants) { const rowNode = tree[deselectedRow]; if (rowNode?.type === 'group') { - const rowGroupDescendantsSelector = getGridRowGroupSelectableDescendantsSelector( - apiRef, - deselectedRow, - ); - const descendants = rowGroupDescendantsSelector(apiRef); - return rowsToDeselect.concat(descendants); + const descendants = getGridRowGroupSelectableDescendants(apiRef, deselectedRow); + descendants.forEach((descendant) => { + rowsToDeselect.push(descendant); + }); } } return rowsToDeselect; From 201be1475fefd33b3dbb9423896026dbaebc568e Mon Sep 17 00:00:00 2001 From: Bilal Shafi Date: Wed, 2 Oct 2024 17:33:14 +0500 Subject: [PATCH 41/45] Optimization --- .../rowSelection/useGridRowSelection.ts | 58 +++++++++++-------- .../src/hooks/features/rowSelection/utils.ts | 25 ++++---- 2 files changed, 46 insertions(+), 37 deletions(-) diff --git a/packages/x-data-grid/src/hooks/features/rowSelection/useGridRowSelection.ts b/packages/x-data-grid/src/hooks/features/rowSelection/useGridRowSelection.ts index 594a0a3337ff..7f6bee3106e7 100644 --- a/packages/x-data-grid/src/hooks/features/rowSelection/useGridRowSelection.ts +++ b/packages/x-data-grid/src/hooks/features/rowSelection/useGridRowSelection.ts @@ -231,19 +231,20 @@ export const useGridRowSelection = ( logger.debug(`Setting selection for row ${id}`); const newSelection: GridRowId[] = []; + const addRow = (rowId: GridRowId) => { + newSelection.push(rowId); + }; if (isSelected) { - newSelection.push(id); + addRow(id); if (applyAutoSelection) { - const rowsToSelect = findRowsToSelect( + findRowsToSelect( apiRef, tree, id, props.rowSelectionPropagation?.descendants ?? false, props.rowSelectionPropagation?.parents ?? false, + addRow, ); - rowsToSelect.forEach((rowId) => { - newSelection.push(rowId); - }); } } @@ -256,29 +257,33 @@ export const useGridRowSelection = ( const newSelection: Set = new Set(selection); newSelection.delete(id); + const addRow = (rowId: GridRowId) => { + newSelection.add(rowId); + }; + const removeRow = (rowId: GridRowId) => { + newSelection.delete(rowId); + }; if (isSelected) { - newSelection.add(id); + addRow(id); if (applyAutoSelection) { - const rowsToSelect = findRowsToSelect( + findRowsToSelect( apiRef, tree, id, props.rowSelectionPropagation?.descendants ?? false, props.rowSelectionPropagation?.parents ?? false, + addRow, ); - rowsToSelect.forEach(newSelection.add, newSelection); } } else if (applyAutoSelection) { - const rowsToDeselect = findRowsToDeselect( + findRowsToDeselect( apiRef, tree, id, props.rowSelectionPropagation?.descendants ?? false, props.rowSelectionPropagation?.parents ?? false, + removeRow, ); - rowsToDeselect.forEach((parentId) => { - newSelection.delete(parentId); - }); } const isSelectionValid = newSelection.size < 2 || canHaveMultipleSelection; @@ -309,17 +314,18 @@ export const useGridRowSelection = ( if (isSelected) { newSelection = selectableIds; if (applyAutoSelection) { + const addRow = (rowId: GridRowId) => { + newSelection.push(rowId); + }; selectableIds.forEach((id) => { - const rowsToSelect = findRowsToSelect( + findRowsToSelect( apiRef, tree, id, props.rowSelectionPropagation?.descendants ?? false, props.rowSelectionPropagation?.parents ?? false, + addRow, ); - rowsToSelect.forEach((rowId) => { - newSelection.push(rowId); - }); }); } } else { @@ -330,35 +336,37 @@ export const useGridRowSelection = ( const selectionLookup = { ...selectedIdsLookupSelector(apiRef), }; + const addRow = (rowId: GridRowId) => { + newSelection.push(rowId); + }; + const removeRow = (rowId: GridRowId) => { + delete selectionLookup[rowId]; + }; selectableIds.forEach((id) => { if (isSelected) { selectionLookup[id] = id; if (applyAutoSelection) { - const rowsToSelect = findRowsToSelect( + findRowsToSelect( apiRef, tree, id, props.rowSelectionPropagation?.descendants ?? false, props.rowSelectionPropagation?.parents ?? false, + addRow, ); - rowsToSelect.forEach((rowId) => { - selectionLookup[rowId] = rowId; - }); } } else { - delete selectionLookup[id]; + removeRow(id); if (applyAutoSelection) { - const rowsToDeselect = findRowsToDeselect( + findRowsToDeselect( apiRef, tree, id, props.rowSelectionPropagation?.descendants ?? false, props.rowSelectionPropagation?.parents ?? false, + removeRow, ); - rowsToDeselect.forEach((parentId) => { - delete selectionLookup[parentId]; - }); } } }); diff --git a/packages/x-data-grid/src/hooks/features/rowSelection/utils.ts b/packages/x-data-grid/src/hooks/features/rowSelection/utils.ts index 29077c86545f..2e44e50e44cf 100644 --- a/packages/x-data-grid/src/hooks/features/rowSelection/utils.ts +++ b/packages/x-data-grid/src/hooks/features/rowSelection/utils.ts @@ -153,13 +153,14 @@ export const findRowsToSelect = ( selectedRow: GridRowId, autoSelectDescendants: boolean, autoSelectParents: boolean, + addRow: (rowId: GridRowId) => void, ) => { const filteredRows = gridFilteredRowsLookupSelector(apiRef); const selectedIdsLookup = selectedIdsLookupSelector(apiRef); - const rowsToSelect: Set = new Set([]); + const selectedDescendants: Set = new Set([]); if (!autoSelectDescendants && !autoSelectParents) { - return rowsToSelect; + return; } if (autoSelectDescendants) { @@ -167,13 +168,16 @@ export const findRowsToSelect = ( if (rowNode?.type === 'group') { const descendants = getGridRowGroupSelectableDescendants(apiRef, selectedRow); - descendants.forEach(rowsToSelect.add, rowsToSelect); + descendants.forEach((rowId) => { + addRow(rowId); + selectedDescendants.add(rowId); + }); } } if (autoSelectParents) { const checkAllDescendantsSelected = (rowId: GridRowId): boolean => { - if (selectedIdsLookup[rowId] !== rowId && !rowsToSelect.has(rowId)) { + if (selectedIdsLookup[rowId] !== rowId && !selectedDescendants.has(rowId)) { return false; } const node = tree[rowId]; @@ -193,15 +197,13 @@ export const findRowsToSelect = ( parent !== GRID_ROOT_GROUP_ID && apiRef.current.isRowSelectable(parent) ) { - rowsToSelect.add(parent); + addRow(parent); traverseParents(parent); } } }; traverseParents(selectedRow); } - - return Array.from(rowsToSelect); }; export const findRowsToDeselect = ( @@ -210,12 +212,12 @@ export const findRowsToDeselect = ( deselectedRow: GridRowId, autoSelectDescendants: boolean, autoSelectParents: boolean, + removeRow: (rowId: GridRowId) => void, ) => { - const rowsToDeselect: GridRowId[] = []; const selectedIdsLookup = selectedIdsLookupSelector(apiRef); if (!autoSelectParents && !autoSelectDescendants) { - return rowsToDeselect; + return; } if (autoSelectParents) { @@ -223,7 +225,7 @@ export const findRowsToDeselect = ( allParents.forEach((parent) => { const isSelected = selectedIdsLookup[parent] === parent; if (isSelected) { - rowsToDeselect.push(parent); + removeRow(parent); } }); } @@ -233,9 +235,8 @@ export const findRowsToDeselect = ( if (rowNode?.type === 'group') { const descendants = getGridRowGroupSelectableDescendants(apiRef, deselectedRow); descendants.forEach((descendant) => { - rowsToDeselect.push(descendant); + removeRow(descendant); }); } } - return rowsToDeselect; }; From b044d6f42598db9478cf7dc9a229d954891d6e3a Mon Sep 17 00:00:00 2001 From: Bilal Shafi Date: Wed, 2 Oct 2024 18:39:57 +0500 Subject: [PATCH 42/45] Fix the failing test --- .../src/hooks/features/rowSelection/useGridRowSelection.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/x-data-grid/src/hooks/features/rowSelection/useGridRowSelection.ts b/packages/x-data-grid/src/hooks/features/rowSelection/useGridRowSelection.ts index 7f6bee3106e7..d50883fa5c36 100644 --- a/packages/x-data-grid/src/hooks/features/rowSelection/useGridRowSelection.ts +++ b/packages/x-data-grid/src/hooks/features/rowSelection/useGridRowSelection.ts @@ -337,7 +337,7 @@ export const useGridRowSelection = ( ...selectedIdsLookupSelector(apiRef), }; const addRow = (rowId: GridRowId) => { - newSelection.push(rowId); + selectionLookup[rowId] = rowId; }; const removeRow = (rowId: GridRowId) => { delete selectionLookup[rowId]; From e5ee1b2d15f48b5336c8cf3b2be94f588aa5e366 Mon Sep 17 00:00:00 2001 From: Bilal Shafi Date: Wed, 2 Oct 2024 23:30:52 +0500 Subject: [PATCH 43/45] Update condition --- packages/x-data-grid/src/hooks/features/rowSelection/utils.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/x-data-grid/src/hooks/features/rowSelection/utils.ts b/packages/x-data-grid/src/hooks/features/rowSelection/utils.ts index 2e44e50e44cf..ba1ff85f77be 100644 --- a/packages/x-data-grid/src/hooks/features/rowSelection/utils.ts +++ b/packages/x-data-grid/src/hooks/features/rowSelection/utils.ts @@ -198,6 +198,7 @@ export const findRowsToSelect = ( apiRef.current.isRowSelectable(parent) ) { addRow(parent); + selectedDescendants.add(parent); traverseParents(parent); } } From 6ea605f4263d2dee960045708ec344360b6a4d8b Mon Sep 17 00:00:00 2001 From: Bilal Shafi Date: Thu, 3 Oct 2024 22:52:32 +0500 Subject: [PATCH 44/45] Improve documentation as per suggestions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Andrew Cherniavskii Co-authored-by: José Rodolfo Freitas Signed-off-by: Bilal Shafi --- docs/data/data-grid/row-grouping/row-grouping.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/data/data-grid/row-grouping/row-grouping.md b/docs/data/data-grid/row-grouping/row-grouping.md index 8430b6047dc1..80bb399fdab9 100644 --- a/docs/data/data-grid/row-grouping/row-grouping.md +++ b/docs/data/data-grid/row-grouping/row-grouping.md @@ -334,7 +334,7 @@ The example below demonstrates the usage of the `rowSelectionPropagation` prop. {{"demo": "RowGroupingPropagateSelection.js", "bg": "inline", "defaultCodeOpen": false}} :::info -The row selection propagation also affects the "Select all" checkbox in a similar way like any other group checkbox. +The row selection propagation also affects the "Select all" checkbox like any other group checkbox. ::: :::info @@ -342,11 +342,11 @@ The selected unfiltered rows will be auto-deselected when the filter is applied. ::: :::warning -If `props.disableMultipleRowSelection` is set to `true`, the row selection propagation feature would not work. +If `props.disableMultipleRowSelection` is set to `true`, the row selection propagation doesn't apply. ::: :::warning -The row selection propagation is a client-side feature and not recommended to be used with the [server-side data source](/x/react-data-grid/server-side-data/), since it would work on the partially loaded data only. +Row selection propagation is a client-side feature and is not supported with the [server-side data source](/x/react-data-grid/server-side-data/). ::: ## Get the rows in a group From 87f71d42e2a363a2d04ca769b90ec0355c8b8e9e Mon Sep 17 00:00:00 2001 From: Bilal Shafi Date: Fri, 4 Oct 2024 06:26:13 +0500 Subject: [PATCH 45/45] Update the docs Signed-off-by: Bilal Shafi --- docs/data/data-grid/row-grouping/row-grouping.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/data/data-grid/row-grouping/row-grouping.md b/docs/data/data-grid/row-grouping/row-grouping.md index 80bb399fdab9..7e234f5c3632 100644 --- a/docs/data/data-grid/row-grouping/row-grouping.md +++ b/docs/data/data-grid/row-grouping/row-grouping.md @@ -338,7 +338,7 @@ The row selection propagation also affects the "Select all" checkbox like any ot ::: :::info -The selected unfiltered rows will be auto-deselected when the filter is applied. Row selection propagation is not applied to the unfiltered rows. +The selected rows that do not pass the filtering criteria are automatically deselected when the filter is applied. Row selection propagation is not applied to the unfiltered rows. ::: :::warning