From 79585ccdb276358df2c84c2567e779b07360019a Mon Sep 17 00:00:00 2001 From: Bilal Shafi Date: Mon, 29 May 2023 17:19:51 +0500 Subject: [PATCH 1/9] [DataGridPro] [Exploration] Lazy load tree data --- docs/data/data-grid/events/events.json | 8 + .../tree-data/TreeDataLazyLoading.js | 181 +++--------- .../tree-data/TreeDataLazyLoading.tsx | 208 ++++--------- .../tree-data/TreeDataLazyLoading.tsx.preview | 9 - docs/data/data-grid/tree-data/tree-data.md | 42 ++- docs/pages/x/api/data-grid/grid-api.md | 1 + .../src/DataGridPremium/DataGridPremium.tsx | 11 + .../src/DataGridPro/DataGridPro.tsx | 11 + .../DataGridPro/useDataGridProComponent.tsx | 4 + .../components/GridTreeDataGroupingCell.tsx | 94 ++++-- .../useGridLazyLoaderPreProcessors.tsx | 13 +- .../treeData/gridTreeDataLazyLoadingApi.ts | 13 + .../features/treeData/gridTreeDataUtils.ts | 45 +++ .../src/hooks/features/treeData/index.ts | 1 + .../treeData/useGridTreeDataLazyLoading.tsx | 98 ++++++ ...seGridTreeDataLazyLoadingPreProcessors.tsx | 279 ++++++++++++++++++ .../treeData/useGridTreeDataPreProcessors.tsx | 5 +- .../src/models/dataGridProProps.ts | 11 + .../x-data-grid-pro/src/models/gridApiPro.ts | 2 + .../src/typeOverloads/modules.ts | 5 + .../grid/x-data-grid/src/models/gridRows.ts | 16 + scripts/x-data-grid-premium.exports.json | 2 + scripts/x-data-grid-pro.exports.json | 2 + scripts/x-data-grid.exports.json | 1 + 24 files changed, 722 insertions(+), 340 deletions(-) delete mode 100644 docs/data/data-grid/tree-data/TreeDataLazyLoading.tsx.preview create mode 100644 packages/grid/x-data-grid-pro/src/hooks/features/treeData/gridTreeDataLazyLoadingApi.ts create mode 100644 packages/grid/x-data-grid-pro/src/hooks/features/treeData/useGridTreeDataLazyLoading.tsx create mode 100644 packages/grid/x-data-grid-pro/src/hooks/features/treeData/useGridTreeDataLazyLoadingPreProcessors.tsx diff --git a/docs/data/data-grid/events/events.json b/docs/data/data-grid/events/events.json index fe58e8db54264..6185f7887ab64 100644 --- a/docs/data/data-grid/events/events.json +++ b/docs/data/data-grid/events/events.json @@ -209,6 +209,14 @@ "params": "'pending' | 'finished'", "event": "MuiEvent<{}>" }, + { + "projects": ["x-data-grid-pro", "x-data-grid-premium"], + "name": "fetchRowChildren", + "description": "Fired when a new batch of rows is requested to be loaded. Called with a GridFetchRowsParams object.", + "params": "GridFetchRowChildrenParams", + "event": "MuiEvent<{}>", + "componentProp": "onFetchRowChildren" + }, { "projects": ["x-data-grid-pro", "x-data-grid-premium"], "name": "fetchRows", diff --git a/docs/data/data-grid/tree-data/TreeDataLazyLoading.js b/docs/data/data-grid/tree-data/TreeDataLazyLoading.js index 554493ee1abc1..97514a498aceb 100644 --- a/docs/data/data-grid/tree-data/TreeDataLazyLoading.js +++ b/docs/data/data-grid/tree-data/TreeDataLazyLoading.js @@ -1,15 +1,8 @@ // TODO rows v6: Adapt to new lazy loading api import * as React from 'react'; -import { - DataGridPro, - getDataGridUtilityClass, - useGridApiContext, - useGridApiRef, - useGridRootProps, -} from '@mui/x-data-grid-pro'; -import { unstable_composeClasses as composeClasses } from '@mui/material'; -import Box from '@mui/material/Box'; -import IconButton from '@mui/material/IconButton'; +import { DataGridPro, useGridApiRef } from '@mui/x-data-grid-pro'; +import Alert from '@mui/material/Alert'; +import Snackbar from '@mui/material/Snackbar'; export const isNavigationKey = (key) => key === 'Home' || @@ -128,155 +121,77 @@ const getChildren = (parentPath) => { ); }; -/** - * This is a naive implementation with terrible performances on a real dataset. - * This fake server is only here for demonstration purposes. - */ const fakeDataFetcher = (parentPath = []) => - new Promise((resolve) => { + new Promise((resolve, reject) => { setTimeout(() => { const rows = getChildren(parentPath).map((row) => ({ ...row, descendantCount: getChildren(row.hierarchy).length, })); + if (parentPath.length !== 0 && Math.random() > 0.7) { + // 30% probablity of failure + reject(new Error('Network call failed randomly')); + } resolve(rows); - }, 500 + Math.random() * 300); + }, 1000 + Math.random() * 300); }); const getTreeDataPath = (row) => row.hierarchy; -const useUtilityClasses = (ownerState) => { - const { classes } = ownerState; - - const slots = { - root: ['treeDataGroupingCell'], - toggle: ['treeDataGroupingCellToggle'], - }; - - return composeClasses(slots, getDataGridUtilityClass, classes); -}; - -/** - * Reproduce the behavior of the `GridTreeDataGroupingCell` component in `@mui/x-data-grid-pro` - * But base the amount of children on a `row.descendantCount` property rather than on the internal lookups. - */ -function GroupingCellWithLazyLoading(props) { - const { id, field, rowNode, row, hideDescendantCount, formattedValue } = props; - - const rootProps = useGridRootProps(); - const apiRef = useGridApiContext(); - const classes = useUtilityClasses({ classes: rootProps.classes }); - - const Icon = rowNode.childrenExpanded - ? rootProps.slots.treeDataCollapseIcon - : rootProps.slots.treeDataExpandIcon; - - const handleClick = (event) => { - apiRef.current.setRowChildrenExpansion(id, !rowNode.childrenExpanded); - apiRef.current.setCellFocus(id, field); - event.stopPropagation(); - }; - - return ( - -
- {row.descendantCount > 0 && ( - - - - )} -
- - {formattedValue === undefined ? rowNode.groupingKey : formattedValue} - {!hideDescendantCount && row.descendantCount > 0 - ? ` (${row.descendantCount})` - : ''} - -
- ); -} - -const CUSTOM_GROUPING_COL_DEF = { - renderCell: (params) => , -}; +const initRows = []; export default function TreeDataLazyLoading() { const apiRef = useGridApiRef(); - const [rows, setRows] = React.useState([]); - - React.useEffect(() => { - fakeDataFetcher().then(setRows); - - const handleRowExpansionChange = async (node) => { - const row = apiRef.current.getRow(node.id); - - if (!node.childrenExpanded || !row || row.childrenFetched) { - return; - } + const [loading, setLoading] = React.useState(false); + const [showSnackbar, setShowSnackbar] = React.useState(false); - apiRef.current.updateRows([ - { - id: `placeholder-children-${node.id}`, - hierarchy: [...row.hierarchy, ''], - }, - ]); - - const childrenRows = await fakeDataFetcher(row.hierarchy); - apiRef.current.updateRows([ - ...childrenRows, - { id: node.id, childrenFetched: true }, - { id: `placeholder-children-${node.id}`, _action: 'delete' }, - ]); - - if (childrenRows.length) { - apiRef.current.setRowChildrenExpansion(node.id, true); + const onFetchRowChildren = React.useCallback( + async ({ row, helpers }) => { + if (showSnackbar) { + setShowSnackbar(false); } - }; - - /** - * By default, the grid does not toggle the expansion of rows with 0 children - * We need to override the `cellKeyDown` event listener to force the expansion if there are children on the server - */ - const handleCellKeyDown = (params, event) => { - const cellParams = apiRef.current.getCellParams(params.id, params.field); - if (cellParams.colDef.type === 'treeDataGroup' && event.key === ' ') { - event.stopPropagation(); - event.preventDefault(); - event.defaultMuiPrevented = true; - - apiRef.current.setRowChildrenExpansion( - params.id, - !params.rowNode.childrenExpanded, - ); + try { + if (!row) { + setLoading(true); + } + const path = row ? getTreeDataPath(row) : []; + const data = await fakeDataFetcher(path); + helpers.success(data); + if (!row) { + setLoading(false); + } + } catch (error) { + // simulate network error + helpers.error(); + setShowSnackbar(true); + console.error(error); } - }; - - apiRef.current.subscribeEvent('rowExpansionChange', handleRowExpansionChange); - apiRef.current.subscribeEvent('cellKeyDown', handleCellKeyDown, { - isFirst: true, - }); - }, [apiRef]); + }, + [showSnackbar], + ); return (
row.descendantCount > 0} + rowsLoadingMode="server" /> + setShowSnackbar(false)} + autoHideDuration={6000} + > + setShowSnackbar(false)} severity="error"> + Could not fetch data, please try again. + +
); } diff --git a/docs/data/data-grid/tree-data/TreeDataLazyLoading.tsx b/docs/data/data-grid/tree-data/TreeDataLazyLoading.tsx index 62d75a498bdca..5ff673cc8cc17 100644 --- a/docs/data/data-grid/tree-data/TreeDataLazyLoading.tsx +++ b/docs/data/data-grid/tree-data/TreeDataLazyLoading.tsx @@ -2,22 +2,14 @@ import * as React from 'react'; import { DataGridPro, - getDataGridUtilityClass, - GridColDef, DataGridProProps, - GridEventListener, - GridGroupingColDefOverride, - GridRenderCellParams, - GridRowModel, - GridRowsProp, - GridGroupNode, - useGridApiContext, useGridApiRef, - useGridRootProps, + GridFetchRowChildrenParams, + GridValidRowModel, + GridRowModel, } from '@mui/x-data-grid-pro'; -import { unstable_composeClasses as composeClasses } from '@mui/material'; -import Box from '@mui/material/Box'; -import IconButton, { IconButtonProps } from '@mui/material/IconButton'; +import Alert from '@mui/material/Alert'; +import Snackbar from '@mui/material/Snackbar'; export const isNavigationKey = (key: string) => key === 'Home' || @@ -26,16 +18,7 @@ export const isNavigationKey = (key: string) => key.indexOf('Page') === 0 || key === ' '; -interface Row { - hierarchy: string[]; - jobTitle: string; - recruitmentDate: Date; - id: number; - descendantCount?: number; - childrenFetched?: boolean; -} - -const ALL_ROWS: GridRowModel[] = [ +const ALL_ROWS = [ { hierarchy: ['Sarah'], jobTitle: 'Head of Human Resources', @@ -128,7 +111,7 @@ const ALL_ROWS: GridRowModel[] = [ }, ]; -const columns: GridColDef[] = [ +const columns = [ { field: 'jobTitle', headerName: 'Job Title', width: 200 }, { field: 'recruitmentDate', @@ -145,164 +128,77 @@ const getChildren = (parentPath: string[]) => { ); }; -/** - * This is a naive implementation with terrible performances on a real dataset. - * This fake server is only here for demonstration purposes. - */ const fakeDataFetcher = (parentPath: string[] = []) => - new Promise[]>((resolve) => { + new Promise((resolve, reject) => { setTimeout(() => { const rows = getChildren(parentPath).map((row) => ({ ...row, descendantCount: getChildren(row.hierarchy).length, })); + if (parentPath.length !== 0 && Math.random() > 0.7) { + // 30% probablity of failure + reject(new Error('Network call failed randomly')); + } resolve(rows); - }, 500 + Math.random() * 300); + }, 1000 + Math.random() * 300); }); const getTreeDataPath: DataGridProProps['getTreeDataPath'] = (row) => row.hierarchy; -const useUtilityClasses = (ownerState: { classes: DataGridProProps['classes'] }) => { - const { classes } = ownerState; - - const slots = { - root: ['treeDataGroupingCell'], - toggle: ['treeDataGroupingCellToggle'], - }; - - return composeClasses(slots, getDataGridUtilityClass, classes); -}; - -interface GroupingCellWithLazyLoadingProps - extends GridRenderCellParams { - hideDescendantCount?: boolean; -} - -/** - * Reproduce the behavior of the `GridTreeDataGroupingCell` component in `@mui/x-data-grid-pro` - * But base the amount of children on a `row.descendantCount` property rather than on the internal lookups. - */ -function GroupingCellWithLazyLoading(props: GroupingCellWithLazyLoadingProps) { - const { id, field, rowNode, row, hideDescendantCount, formattedValue } = props; - - const rootProps = useGridRootProps(); - const apiRef = useGridApiContext(); - const classes = useUtilityClasses({ classes: rootProps.classes }); - - const Icon = rowNode.childrenExpanded - ? rootProps.slots.treeDataCollapseIcon - : rootProps.slots.treeDataExpandIcon; - - const handleClick: IconButtonProps['onClick'] = (event) => { - apiRef.current.setRowChildrenExpansion(id, !rowNode.childrenExpanded); - apiRef.current.setCellFocus(id, field); - event.stopPropagation(); - }; - - return ( - -
- {row.descendantCount > 0 && ( - - - - )} -
- - {formattedValue === undefined ? rowNode.groupingKey : formattedValue} - {!hideDescendantCount && row.descendantCount > 0 - ? ` (${row.descendantCount})` - : ''} - -
- ); -} - -const CUSTOM_GROUPING_COL_DEF: GridGroupingColDefOverride = { - renderCell: (params) => ( - - ), -}; +const initRows: GridRowModel[] = []; export default function TreeDataLazyLoading() { const apiRef = useGridApiRef(); - const [rows, setRows] = React.useState([]); - - React.useEffect(() => { - fakeDataFetcher().then(setRows); + const [loading, setLoading] = React.useState(false); + const [showSnackbar, setShowSnackbar] = React.useState(false); - const handleRowExpansionChange: GridEventListener<'rowExpansionChange'> = async ( - node, - ) => { - const row = apiRef.current.getRow(node.id) as Row | null; - - if (!node.childrenExpanded || !row || row.childrenFetched) { - return; + const onFetchRowChildren = React.useCallback( + async ({ row, helpers }: GridFetchRowChildrenParams) => { + if (showSnackbar) { + setShowSnackbar(false); } - - apiRef.current.updateRows([ - { - id: `placeholder-children-${node.id}`, - hierarchy: [...row.hierarchy, ''], - }, - ]); - - const childrenRows = await fakeDataFetcher(row.hierarchy); - apiRef.current.updateRows([ - ...childrenRows, - { id: node.id, childrenFetched: true }, - { id: `placeholder-children-${node.id}`, _action: 'delete' }, - ]); - - if (childrenRows.length) { - apiRef.current.setRowChildrenExpansion(node.id, true); + try { + if (!row) { + setLoading(true); + } + const path = row ? getTreeDataPath!(row) : []; + const data = (await fakeDataFetcher(path)) as GridValidRowModel[]; + helpers.success(data); + if (!row) { + setLoading(false); + } + } catch (error) { + // simulate network error + helpers.error(); + setShowSnackbar(true); + console.error(error); } - }; - - /** - * By default, the grid does not toggle the expansion of rows with 0 children - * We need to override the `cellKeyDown` event listener to force the expansion if there are children on the server - */ - const handleCellKeyDown: GridEventListener<'cellKeyDown'> = (params, event) => { - const cellParams = apiRef.current.getCellParams(params.id, params.field); - if (cellParams.colDef.type === 'treeDataGroup' && event.key === ' ') { - event.stopPropagation(); - event.preventDefault(); - event.defaultMuiPrevented = true; - - apiRef.current.setRowChildrenExpansion( - params.id, - !(params.rowNode as GridGroupNode).childrenExpanded, - ); - } - }; - - apiRef.current.subscribeEvent('rowExpansionChange', handleRowExpansionChange); - apiRef.current.subscribeEvent('cellKeyDown', handleCellKeyDown, { - isFirst: true, - }); - }, [apiRef]); + }, + [showSnackbar], + ); return (
row.descendantCount! > 0} + rowsLoadingMode="server" /> + setShowSnackbar(false)} + autoHideDuration={6000} + > + setShowSnackbar(false)} severity="error"> + Could not fetch data, please try again. + +
); } diff --git a/docs/data/data-grid/tree-data/TreeDataLazyLoading.tsx.preview b/docs/data/data-grid/tree-data/TreeDataLazyLoading.tsx.preview deleted file mode 100644 index b2ebdc84548ba..0000000000000 --- a/docs/data/data-grid/tree-data/TreeDataLazyLoading.tsx.preview +++ /dev/null @@ -1,9 +0,0 @@ - \ No newline at end of file diff --git a/docs/data/data-grid/tree-data/tree-data.md b/docs/data/data-grid/tree-data/tree-data.md index 06629f1e6435a..e1ac3cc6fef11 100644 --- a/docs/data/data-grid/tree-data/tree-data.md +++ b/docs/data/data-grid/tree-data/tree-data.md @@ -115,17 +115,43 @@ You can limit the sorting to the top-level rows with the `disableChildrenSorting ## Children lazy-loading -:::warning -This feature isn't implemented yet. It's coming. +To lazy-load tree data children, set the `rowsLoadingMode` prop to `server` and listen to the `fetchRowChildren` event or pass a handler to `onFetchRowChildren` prop. It is fired when user tries to expand a row, it recieves the parent row object and a `helpers` object as parameters which has the following signature. -👍 Upvote [issue #3377](https://github.com/mui/mui-x/issues/3377) if you want to see it land faster. -::: +```tsx +interface GridTreeDataLazyLoadHelpers { + success: (rows: GridRowModel[]) => void; + error: () => void; +} + +interface GridFetchRowChildrenParams { + row: GridRowModel | undefined; + helpers: GridTreeDataLazyLoadHelpers; +} +``` + +Do the API call in the `onFetchRowChildren` handler and call `helpers.success(newRows)` and `helpers.error()` respectively in case of success or error to let the grid update the related internal states. + +```tsx +async function onFetchRowChildren({ row, server }: GridFetchRowChildrenParams) { + try { + const childRows = await fetchRows(row); + helpers.success(childRows); + } catch (error) { + helpers.error(); + } +} -Alternatively, you can achieve a similar behavior by implementing this feature outside the component as shown below. -This implementation does not support every feature of the data grid but can be a good starting point for large datasets. + row.descendantCount! > 0} + rowsLoadingMode="server" +/>; +``` -The idea is to add a property `descendantCount` on the row and to use it instead of the internal grid state. -To do so, you need to override both the `renderCell` of the grouping column and to manually open the rows by listening to `rowExpansionChange` event. +Following demo implements a simple lazy-loading tree data grid using mock server. {{"demo": "TreeDataLazyLoading.js", "bg": "inline", "defaultCodeOpen": false}} diff --git a/docs/pages/x/api/data-grid/grid-api.md b/docs/pages/x/api/data-grid/grid-api.md index 0b92b9fd1ad46..ff82b3f7c40b9 100644 --- a/docs/pages/x/api/data-grid/grid-api.md +++ b/docs/pages/x/api/data-grid/grid-api.md @@ -109,6 +109,7 @@ import { GridApi } from '@mui/x-data-grid'; | setRowGroupingCriteriaIndex [](/x/introduction/licensing/#premium-plan) | (groupingCriteriaField: string, groupingIndex: number) => void | Sets the grouping index of a grouping criteria. | | setRowGroupingModel [](/x/introduction/licensing/#premium-plan) | (model: GridRowGroupingModel) => void | Sets the columns to use as grouping criteria. | | setRowIndex [](/x/introduction/licensing/#pro-plan) | (rowId: GridRowId, targetIndex: number) => void | Moves a row from its original position to the position given by `targetIndex`. | +| setRowLoadingStatus [](/x/introduction/licensing/#pro-plan) | (nodeId: GridTreeNode['id'], value: boolean) => void | Sets the loading status of a server side row. | | setRows | (rows: GridRowModel[]) => void | Sets a new set of rows. | | setRowSelectionModel | (rowIds: GridRowId[]) => void | Updates the selected rows to be those passed to the `rowIds` argument.
Any row already selected will be unselected. | | setSortModel | (model: GridSortModel) => void | Updates the sort model and triggers the sorting of rows. | diff --git a/packages/grid/x-data-grid-premium/src/DataGridPremium/DataGridPremium.tsx b/packages/grid/x-data-grid-premium/src/DataGridPremium/DataGridPremium.tsx index b6d1485bb0c2e..675d1bf5c6a22 100644 --- a/packages/grid/x-data-grid-premium/src/DataGridPremium/DataGridPremium.tsx +++ b/packages/grid/x-data-grid-premium/src/DataGridPremium/DataGridPremium.tsx @@ -446,6 +446,12 @@ DataGridPremiumRaw.propTypes = { * @returns {boolean} A boolean indicating if the cell is selectable. */ isRowSelectable: PropTypes.func, + /** + * Callback that returns true for those rows which have children on server. + * @param {GridValidRowModel} row The row to test. + * @returns {boolean} A boolean indicating if the row has children on server. + */ + isServerSideRow: PropTypes.func, /** * If `true`, moving the mouse pointer outside the grid before releasing the mouse button * in a column re-order action will not cause the column to jump back to its original position. @@ -625,6 +631,11 @@ DataGridPremiumRaw.propTypes = { * @param {string} inProgress Indicates if the task is in progress. */ onExcelExportStateChange: PropTypes.func, + /** + * Callback fired when children rows of a parent row are requested to be loaded. + * @param {GridFetchRowChildrenParams} params With all properties from [[GridFetchRowChildrenParams]]. + */ + onFetchRowChildren: PropTypes.func, /** * Callback fired when rowCount is set and the next batch of virtualized rows is rendered. * @param {GridFetchRowsParams} params With all properties from [[GridFetchRowsParams]]. diff --git a/packages/grid/x-data-grid-pro/src/DataGridPro/DataGridPro.tsx b/packages/grid/x-data-grid-pro/src/DataGridPro/DataGridPro.tsx index 16df6391a6963..4c16a88c94b4b 100644 --- a/packages/grid/x-data-grid-pro/src/DataGridPro/DataGridPro.tsx +++ b/packages/grid/x-data-grid-pro/src/DataGridPro/DataGridPro.tsx @@ -406,6 +406,12 @@ DataGridProRaw.propTypes = { * @returns {boolean} A boolean indicating if the cell is selectable. */ isRowSelectable: PropTypes.func, + /** + * Callback that returns true for those rows which have children on server. + * @param {GridValidRowModel} row The row to test. + * @returns {boolean} A boolean indicating if the row has children on server. + */ + isServerSideRow: PropTypes.func, /** * If `true`, moving the mouse pointer outside the grid before releasing the mouse button * in a column re-order action will not cause the column to jump back to its original position. @@ -566,6 +572,11 @@ DataGridProRaw.propTypes = { * @param {GridCallbackDetails} details Additional details for this callback. */ onDetailPanelExpandedRowIdsChange: PropTypes.func, + /** + * Callback fired when children rows of a parent row are requested to be loaded. + * @param {GridFetchRowChildrenParams} params With all properties from [[GridFetchRowChildrenParams]]. + */ + onFetchRowChildren: PropTypes.func, /** * Callback fired when rowCount is set and the next batch of virtualized rows is rendered. * @param {GridFetchRowsParams} params With all properties from [[GridFetchRowsParams]]. diff --git a/packages/grid/x-data-grid-pro/src/DataGridPro/useDataGridProComponent.tsx b/packages/grid/x-data-grid-pro/src/DataGridPro/useDataGridProComponent.tsx index 36ff4a78c824b..115b6224d4645 100644 --- a/packages/grid/x-data-grid-pro/src/DataGridPro/useDataGridProComponent.tsx +++ b/packages/grid/x-data-grid-pro/src/DataGridPro/useDataGridProComponent.tsx @@ -56,7 +56,9 @@ import { columnResizeStateInitializer, } from '../hooks/features/columnResize/useGridColumnResize'; import { useGridTreeData } from '../hooks/features/treeData/useGridTreeData'; +import { useGridTreeDataLazyLoading } from '../hooks/features/treeData/useGridTreeDataLazyLoading'; import { useGridTreeDataPreProcessors } from '../hooks/features/treeData/useGridTreeDataPreProcessors'; +import { useGridTreeDataLazyLoadingPreProcessors } from '../hooks/features/treeData/useGridTreeDataLazyLoadingPreProcessors'; import { useGridColumnPinning, columnPinningStateInitializer, @@ -89,6 +91,7 @@ export const useDataGridProComponent = ( useGridRowSelectionPreProcessors(apiRef, props); useGridRowReorderPreProcessors(apiRef, props); useGridTreeDataPreProcessors(apiRef, props); + useGridTreeDataLazyLoadingPreProcessors(apiRef, props); useGridLazyLoaderPreProcessors(apiRef, props); useGridRowPinningPreProcessors(apiRef); useGridDetailPanelPreProcessors(apiRef, props); @@ -122,6 +125,7 @@ export const useDataGridProComponent = ( useGridHeaderFiltering(apiRef, props); useGridTreeData(apiRef); + useGridTreeDataLazyLoading(apiRef, props); useGridKeyboardNavigation(apiRef, props); useGridRowSelection(apiRef, props); useGridColumnPinning(apiRef, props); diff --git a/packages/grid/x-data-grid-pro/src/components/GridTreeDataGroupingCell.tsx b/packages/grid/x-data-grid-pro/src/components/GridTreeDataGroupingCell.tsx index 53ae198016155..9f1aa69bc2da3 100644 --- a/packages/grid/x-data-grid-pro/src/components/GridTreeDataGroupingCell.tsx +++ b/packages/grid/x-data-grid-pro/src/components/GridTreeDataGroupingCell.tsx @@ -8,10 +8,13 @@ import { getDataGridUtilityClass, GridRenderCellParams, GridGroupNode, + GridServerSideGroupNode, } from '@mui/x-data-grid'; +import CircularProgress from '@mui/material/CircularProgress'; import { useGridRootProps } from '../hooks/utils/useGridRootProps'; -import { useGridApiContext } from '../hooks/utils/useGridApiContext'; +import { useGridPrivateApiContext } from '../hooks/utils/useGridPrivateApiContext'; import { DataGridProProcessedProps } from '../models/dataGridProProps'; +import { getLazyLoadingHelpers } from '../hooks/features/treeData/useGridTreeDataLazyLoading'; type OwnerState = { classes: DataGridProProcessedProps['classes'] }; @@ -26,7 +29,8 @@ const useUtilityClasses = (ownerState: OwnerState) => { return composeClasses(slots, getDataGridUtilityClass, classes); }; -interface GridTreeDataGroupingCellProps extends GridRenderCellParams { +interface GridTreeDataGroupingCellProps + extends GridRenderCellParams { hideDescendantCount?: boolean; /** * The cell offset multiplier used for calculating cell offset (`rowNode.depth * offsetMultiplier` px). @@ -35,11 +39,62 @@ interface GridTreeDataGroupingCellProps extends GridRenderCellParams { + descendantCount: number; +} + +function GridTreeDataGroupingCellIcon(props: GridTreeDataGroupingCellIconProps) { + const { rowNode, id, field, descendantCount } = props; + const apiRef = useGridPrivateApiContext(); + const rootProps = useGridRootProps(); + + const isServerSideNode = (rowNode as GridServerSideGroupNode).isServerSide; + const isDataLoading = (rowNode as GridServerSideGroupNode).isLoading; + const areChildrenFetched = (rowNode as GridServerSideGroupNode).childrenFetched; + + const handleClick = (event: React.MouseEvent) => { + if (isServerSideNode && !rowNode.childrenExpanded && !areChildrenFetched) { + const helpers = getLazyLoadingHelpers(apiRef, rowNode as GridServerSideGroupNode); + const row = apiRef.current.getRow(rowNode.id); + apiRef.current.setRowLoadingStatus(rowNode.id, true); + apiRef.current.publishEvent('fetchRowChildren', { row, helpers }); + } else { + apiRef.current.setRowChildrenExpansion(id, !rowNode.childrenExpanded); + } + apiRef.current.setCellFocus(id, field); + event.stopPropagation(); // TODO remove event.stopPropagation + }; + + const Icon = rowNode.childrenExpanded + ? rootProps.slots.treeDataCollapseIcon + : rootProps.slots.treeDataExpandIcon; + + if (isDataLoading) { + return ; + } + return descendantCount > 0 || isServerSideNode ? ( + + + + ) : null; +} + function GridTreeDataGroupingCell(props: GridTreeDataGroupingCellProps) { - const { id, field, formattedValue, rowNode, hideDescendantCount, offsetMultiplier = 2 } = props; + const { formattedValue, rowNode, hideDescendantCount, offsetMultiplier = 2, id, field } = props; const rootProps = useGridRootProps(); - const apiRef = useGridApiContext(); + const apiRef = useGridPrivateApiContext(); const ownerState: OwnerState = { classes: rootProps.classes }; const classes = useUtilityClasses(ownerState); const filteredDescendantCountLookup = useGridSelector( @@ -49,34 +104,15 @@ function GridTreeDataGroupingCell(props: GridTreeDataGroupingCellProps) { const filteredDescendantCount = filteredDescendantCountLookup[rowNode.id] ?? 0; - const Icon = rowNode.childrenExpanded - ? rootProps.slots.treeDataCollapseIcon - : rootProps.slots.treeDataExpandIcon; - - const handleClick = (event: React.MouseEvent) => { - apiRef.current.setRowChildrenExpansion(id, !rowNode.childrenExpanded); - apiRef.current.setCellFocus(id, field); - event.stopPropagation(); // TODO remove event.stopPropagation - }; - return (
- {filteredDescendantCount > 0 && ( - - - - )} +
{formattedValue === undefined ? rowNode.groupingKey : formattedValue} diff --git a/packages/grid/x-data-grid-pro/src/hooks/features/lazyLoader/useGridLazyLoaderPreProcessors.tsx b/packages/grid/x-data-grid-pro/src/hooks/features/lazyLoader/useGridLazyLoaderPreProcessors.tsx index c749523ddfab6..00b81f322af6c 100644 --- a/packages/grid/x-data-grid-pro/src/hooks/features/lazyLoader/useGridLazyLoaderPreProcessors.tsx +++ b/packages/grid/x-data-grid-pro/src/hooks/features/lazyLoader/useGridLazyLoaderPreProcessors.tsx @@ -13,7 +13,10 @@ const getSkeletonRowId = (index: number) => `${GRID_SKELETON_ROW_ROOT_ID}-${inde export const useGridLazyLoaderPreProcessors = ( privateApiRef: React.MutableRefObject, - props: Pick, + props: Pick< + DataGridProProcessedProps, + 'rowCount' | 'rowsLoadingMode' | 'experimentalFeatures' | 'treeData' | 'onFetchRows' + >, ) => { const { lazyLoading } = (props.experimentalFeatures ?? {}) as GridExperimentalProFeatures; @@ -26,7 +29,11 @@ export const useGridLazyLoaderPreProcessors = ( !lazyLoading || props.rowsLoadingMode !== 'server' || !props.rowCount || - rootGroup.children.length >= props.rowCount + rootGroup.children.length >= props.rowCount || + !props.onFetchRows || + // To avoid situations like https://codesandbox.io/s/nice-yalow-dkfb5n + // TODO: To remove when `treeData` supports (root level) lazy-loading + props.treeData ) { return groupingParams; } @@ -55,7 +62,7 @@ export const useGridLazyLoaderPreProcessors = ( tree, }; }, - [props.rowCount, props.rowsLoadingMode, lazyLoading], + [props.rowCount, props.rowsLoadingMode, lazyLoading, props.treeData, props.onFetchRows], ); useGridRegisterPipeProcessor(privateApiRef, 'hydrateRows', addSkeletonRows); diff --git a/packages/grid/x-data-grid-pro/src/hooks/features/treeData/gridTreeDataLazyLoadingApi.ts b/packages/grid/x-data-grid-pro/src/hooks/features/treeData/gridTreeDataLazyLoadingApi.ts new file mode 100644 index 0000000000000..c32fc4fde2742 --- /dev/null +++ b/packages/grid/x-data-grid-pro/src/hooks/features/treeData/gridTreeDataLazyLoadingApi.ts @@ -0,0 +1,13 @@ +import { GridTreeNode } from '@mui/x-data-grid'; + +/** + * The API for tree data lazy loading. + */ +export interface GridTreeDataLazyLoadingApi { + /** + * Sets the loading status of a server side row. + * @param {GridTreeNode['id']} nodeId The id of the node. + * @param {boolean} value The boolean value that's needs to be set. + */ + setRowLoadingStatus: (nodeId: GridTreeNode['id'], value: boolean) => void; +} diff --git a/packages/grid/x-data-grid-pro/src/hooks/features/treeData/gridTreeDataUtils.ts b/packages/grid/x-data-grid-pro/src/hooks/features/treeData/gridTreeDataUtils.ts index 4127610c5fb13..c2b55b0b7090a 100644 --- a/packages/grid/x-data-grid-pro/src/hooks/features/treeData/gridTreeDataUtils.ts +++ b/packages/grid/x-data-grid-pro/src/hooks/features/treeData/gridTreeDataUtils.ts @@ -4,12 +4,16 @@ import { GridTreeNode, GridFilterState, GridFilterModel, + GridServerSideGroupNode, + GridRowIdToModelLookup, + GridValidRowModel, } from '@mui/x-data-grid'; import { GridAggregatedFilterItemApplier, GridApiCommunity, passFilterLogic, } from '@mui/x-data-grid/internals'; +import { DataGridProProcessedProps } from '../../../models/dataGridProProps'; interface FilterRowTreeFromTreeDataParams { rowTree: GridRowTreeConfig; @@ -20,6 +24,7 @@ interface FilterRowTreeFromTreeDataParams { } export const TREE_DATA_STRATEGY = 'tree-data'; +export const TREE_DATA_LAZY_LOADING_STRATEGY = 'tree-data-lazy-loading'; /** * A node is visible if one of the following criteria is met: @@ -122,3 +127,43 @@ export const filterRowTreeFromTreeData = ( filteredDescendantCountLookup, }; }; + +export const iterateTreeNodes = ( + dataRowIdToModelLookup: GridRowIdToModelLookup, + tree: GridRowTreeConfig, + nodeId: GridTreeNode['id'], + isServerSideRow: DataGridProProcessedProps['isServerSideRow'], +) => { + const node = tree[nodeId]; + + if (node.type === 'leaf') { + const row = dataRowIdToModelLookup[node.id]; + if (!row) { + return; + } + if (row && isServerSideRow!(row)) { + const groupingField = '__no_field__'; + const groupingKey = node.groupingKey ?? '__no_key__'; + const updatedNode: GridServerSideGroupNode = { + ...node, + type: 'group', + children: [], + childrenFromPath: {}, + isAutoGenerated: false, + groupingField, + groupingKey, + isServerSide: true, + isLoading: false, + childrenFetched: false, + }; + tree[node.id] = updatedNode; + } + return; + } + + if (node.type === 'group') { + for (let i = 0; i < node.children.length; i += 1) { + iterateTreeNodes(dataRowIdToModelLookup, tree, node.children[i], isServerSideRow); + } + } +}; diff --git a/packages/grid/x-data-grid-pro/src/hooks/features/treeData/index.ts b/packages/grid/x-data-grid-pro/src/hooks/features/treeData/index.ts index 068a9c80be333..6251d94518f5c 100644 --- a/packages/grid/x-data-grid-pro/src/hooks/features/treeData/index.ts +++ b/packages/grid/x-data-grid-pro/src/hooks/features/treeData/index.ts @@ -1 +1,2 @@ export { GRID_TREE_DATA_GROUPING_FIELD } from './gridTreeDataGroupColDef'; +export { GridFetchRowChildrenParams } from './useGridTreeDataLazyLoading'; diff --git a/packages/grid/x-data-grid-pro/src/hooks/features/treeData/useGridTreeDataLazyLoading.tsx b/packages/grid/x-data-grid-pro/src/hooks/features/treeData/useGridTreeDataLazyLoading.tsx new file mode 100644 index 0000000000000..a25560091100e --- /dev/null +++ b/packages/grid/x-data-grid-pro/src/hooks/features/treeData/useGridTreeDataLazyLoading.tsx @@ -0,0 +1,98 @@ +import * as React from 'react'; +import { + GridServerSideGroupNode, + GridRowModel, + useGridApiMethod, + GRID_ROOT_GROUP_ID, + useGridApiOptionHandler, +} from '@mui/x-data-grid'; +import { GridTreeDataLazyLoadingApi } from './gridTreeDataLazyLoadingApi'; +import { GridPrivateApiPro } from '../../../models/gridApiPro'; +import { DataGridProProcessedProps } from '../../../models/dataGridProProps'; + +interface GridTreeDataLazyLoadHelpers { + success: (rows: GridRowModel[]) => void; + error: () => void; +} + +export interface GridFetchRowChildrenParams { + row: GridRowModel | undefined; + helpers: GridTreeDataLazyLoadHelpers; +} + +export const getLazyLoadingHelpers = ( + apiRef: React.MutableRefObject, + rowNode: GridServerSideGroupNode, +) => ({ + success: (rows: GridRowModel[]) => { + apiRef.current.updateRows(rows); + const previousNode = apiRef.current.getRowNode(rowNode.id) as GridServerSideGroupNode; + const id = rowNode!.id; + + const newNode: GridServerSideGroupNode = { + ...previousNode, + isLoading: false, + childrenFetched: true, + }; + apiRef.current.setState((state) => { + return { + ...state, + rows: { + ...state.rows, + tree: { ...state.rows.tree, [id]: newNode }, + }, + }; + }); + apiRef.current.setRowChildrenExpansion(rowNode!.id, true); + }, + error: () => { + apiRef.current.setRowLoadingStatus(rowNode!.id, false); + }, +}); + +export const useGridTreeDataLazyLoading = ( + apiRef: React.MutableRefObject, + props: Pick, +) => { + const setRowLoadingStatus = React.useCallback( + (id, isLoading) => { + const currentNode = apiRef.current.getRowNode(id) as GridServerSideGroupNode; + if (!currentNode) { + throw new Error(`MUI: No row with id #${id} found`); + } + + const newNode: GridServerSideGroupNode = { ...currentNode, isLoading }; + apiRef.current.setState((state) => { + return { + ...state, + rows: { + ...state.rows, + tree: { ...state.rows.tree, [id]: newNode }, + }, + }; + }); + apiRef.current.forceUpdate(); + }, + [apiRef], + ); + + const treeDataLazyLoadingApi: GridTreeDataLazyLoadingApi = { + setRowLoadingStatus, + }; + + useGridApiMethod(apiRef, treeDataLazyLoadingApi, 'public'); + useGridApiOptionHandler(apiRef, 'fetchRowChildren', props.onFetchRowChildren); + + /** + * EFFECTS + */ + React.useEffect(() => { + if (props.treeData && props.rowsLoadingMode === 'server') { + const helpers = getLazyLoadingHelpers( + apiRef, + apiRef.current.getRowNode(GRID_ROOT_GROUP_ID) as GridServerSideGroupNode, + ); + apiRef.current.publishEvent('fetchRowChildren', { row: undefined, helpers }); + } + }, [apiRef, props.treeData, props.rowsLoadingMode]); +}; diff --git a/packages/grid/x-data-grid-pro/src/hooks/features/treeData/useGridTreeDataLazyLoadingPreProcessors.tsx b/packages/grid/x-data-grid-pro/src/hooks/features/treeData/useGridTreeDataLazyLoadingPreProcessors.tsx new file mode 100644 index 0000000000000..6f294c9d7580f --- /dev/null +++ b/packages/grid/x-data-grid-pro/src/hooks/features/treeData/useGridTreeDataLazyLoadingPreProcessors.tsx @@ -0,0 +1,279 @@ +import * as React from 'react'; +import { + gridRowTreeSelector, + useFirstRender, + GridColDef, + GridRenderCellParams, + GridGroupNode, + GridRowId, + GRID_CHECKBOX_SELECTION_FIELD, + GRID_ROOT_GROUP_ID, + GridServerSideGroupNode, +} from '@mui/x-data-grid'; +import { + GridPipeProcessor, + GridStrategyProcessor, + useGridRegisterPipeProcessor, + useGridRegisterStrategyProcessor, +} from '@mui/x-data-grid/internals'; +import { + GRID_TREE_DATA_GROUPING_COL_DEF, + GRID_TREE_DATA_GROUPING_COL_DEF_FORCED_PROPERTIES, +} from './gridTreeDataGroupColDef'; +import { DataGridProProcessedProps } from '../../../models/dataGridProProps'; +import { + filterRowTreeFromTreeData, + iterateTreeNodes, + TREE_DATA_LAZY_LOADING_STRATEGY, +} from './gridTreeDataUtils'; +import { GridPrivateApiPro } from '../../../models/gridApiPro'; +import { + GridGroupingColDefOverride, + GridGroupingColDefOverrideParams, +} from '../../../models/gridGroupingColDefOverride'; +import { GridTreeDataGroupingCell } from '../../../components'; +import { createRowTree } from '../../../utils/tree/createRowTree'; +import { + GridTreePathDuplicateHandler, + RowTreeBuilderGroupingCriterion, +} from '../../../utils/tree/models'; +import { sortRowTree } from '../../../utils/tree/sortRowTree'; +import { updateRowTree } from '../../../utils/tree/updateRowTree'; + +export const useGridTreeDataLazyLoadingPreProcessors = ( + privateApiRef: React.MutableRefObject, + props: Pick< + DataGridProProcessedProps, + | 'treeData' + | 'groupingColDef' + | 'getTreeDataPath' + | 'disableChildrenSorting' + | 'disableChildrenFiltering' + | 'defaultGroupingExpansionDepth' + | 'isGroupExpandedByDefault' + | 'rowsLoadingMode' + | 'isServerSideRow' + >, +) => { + const setStrategyAvailability = React.useCallback(() => { + privateApiRef.current.setStrategyAvailability( + 'rowTree', + TREE_DATA_LAZY_LOADING_STRATEGY, + props.treeData && props.rowsLoadingMode === 'server' ? () => true : () => false, + ); + }, [privateApiRef, props.treeData, props.rowsLoadingMode]); + + const getGroupingColDef = React.useCallback(() => { + const groupingColDefProp = props.groupingColDef; + + let colDefOverride: GridGroupingColDefOverride | null | undefined; + if (typeof groupingColDefProp === 'function') { + const params: GridGroupingColDefOverrideParams = { + groupingName: TREE_DATA_LAZY_LOADING_STRATEGY, + fields: [], + }; + + colDefOverride = groupingColDefProp(params); + } else { + colDefOverride = groupingColDefProp; + } + + const { hideDescendantCount, ...colDefOverrideProperties } = colDefOverride ?? {}; + + const commonProperties: Omit = { + ...GRID_TREE_DATA_GROUPING_COL_DEF, + renderCell: (params) => ( + )} + hideDescendantCount={hideDescendantCount} + /> + ), + headerName: privateApiRef.current.getLocaleText('treeDataGroupingHeaderName'), + }; + + return { + ...commonProperties, + ...colDefOverrideProperties, + ...GRID_TREE_DATA_GROUPING_COL_DEF_FORCED_PROPERTIES, + }; + }, [privateApiRef, props.groupingColDef]); + + const updateGroupingColumn = React.useCallback>( + (columnsState) => { + const groupingColDefField = GRID_TREE_DATA_GROUPING_COL_DEF_FORCED_PROPERTIES.field; + + const shouldHaveGroupingColumn = props.treeData; + const prevGroupingColumn = columnsState.lookup[groupingColDefField]; + + if (shouldHaveGroupingColumn) { + const newGroupingColumn = getGroupingColDef(); + if (prevGroupingColumn) { + newGroupingColumn.width = prevGroupingColumn.width; + newGroupingColumn.flex = prevGroupingColumn.flex; + } + columnsState.lookup[groupingColDefField] = newGroupingColumn; + if (prevGroupingColumn == null) { + const index = columnsState.orderedFields[0] === GRID_CHECKBOX_SELECTION_FIELD ? 1 : 0; + columnsState.orderedFields = [ + ...columnsState.orderedFields.slice(0, index), + groupingColDefField, + ...columnsState.orderedFields.slice(index), + ]; + } + } else if (!shouldHaveGroupingColumn && prevGroupingColumn) { + delete columnsState.lookup[groupingColDefField]; + columnsState.orderedFields = columnsState.orderedFields.filter( + (field) => field !== groupingColDefField, + ); + } + + return columnsState; + }, + [props.treeData, getGroupingColDef], + ); + + const createRowTreeForTreeData = React.useCallback>( + (params) => { + if (!props.getTreeDataPath) { + throw new Error('MUI: No getTreeDataPath given.'); + } + + const getRowTreeBuilderNode = (rowId: GridRowId) => ({ + id: rowId, + path: props.getTreeDataPath!(params.dataRowIdToModelLookup[rowId]).map( + (key): RowTreeBuilderGroupingCriterion => ({ key, field: null }), + ), + }); + + const onDuplicatePath: GridTreePathDuplicateHandler = (firstId, secondId, path) => { + throw new Error( + [ + 'MUI: The path returned by `getTreeDataPath` should be unique.', + `The rows with id #${firstId} and #${secondId} have the same.`, + `Path: ${JSON.stringify(path.map((step) => step.key))}.`, + ].join('\n'), + ); + }; + + if (params.updates.type === 'full') { + return createRowTree({ + previousTree: params.previousTree, + nodes: params.updates.rows.map(getRowTreeBuilderNode), + defaultGroupingExpansionDepth: props.defaultGroupingExpansionDepth, + isGroupExpandedByDefault: props.isGroupExpandedByDefault, + groupingName: TREE_DATA_LAZY_LOADING_STRATEGY, + onDuplicatePath, + }); + } + + return updateRowTree({ + nodes: { + inserted: params.updates.actions.insert.map(getRowTreeBuilderNode), + modified: params.updates.actions.modify.map(getRowTreeBuilderNode), + removed: params.updates.actions.remove, + }, + previousTree: params.previousTree!, + previousTreeDepth: params.previousTreeDepths!, + defaultGroupingExpansionDepth: props.defaultGroupingExpansionDepth, + isGroupExpandedByDefault: props.isGroupExpandedByDefault, + groupingName: TREE_DATA_LAZY_LOADING_STRATEGY, + }); + }, + [props.getTreeDataPath, props.defaultGroupingExpansionDepth, props.isGroupExpandedByDefault], + ); + + const filterRows = React.useCallback>( + (params) => { + const rowTree = gridRowTreeSelector(privateApiRef); + + return filterRowTreeFromTreeData({ + rowTree, + isRowMatchingFilters: params.isRowMatchingFilters, + disableChildrenFiltering: props.disableChildrenFiltering, + filterModel: params.filterModel, + apiRef: privateApiRef, + }); + }, + [privateApiRef, props.disableChildrenFiltering], + ); + + const sortRows = React.useCallback>( + (params) => { + const rowTree = gridRowTreeSelector(privateApiRef); + + return sortRowTree({ + rowTree, + sortRowList: params.sortRowList, + disableChildrenSorting: props.disableChildrenSorting, + shouldRenderGroupBelowLeaves: false, + }); + }, + [privateApiRef, props.disableChildrenSorting], + ); + + const setServerSideGroups = React.useCallback>( + (params) => { + // To investigate: Avoid dual processing, make this part of `rowTreeCreation` strategy processor + if (Object.keys(params.tree).length === 1) { + // TODO: Fix this hack + (params.tree[GRID_ROOT_GROUP_ID] as GridServerSideGroupNode).isLoading = true; + return params; + } + + if (props.rowsLoadingMode !== 'server' || !props.isServerSideRow || !props.treeData) { + return params; + } + + iterateTreeNodes( + params.dataRowIdToModelLookup, + params.tree, + GRID_ROOT_GROUP_ID, + props.isServerSideRow, + ); + + return params; + }, + [props.isServerSideRow, props.treeData, props.rowsLoadingMode], + ); + + useGridRegisterPipeProcessor(privateApiRef, 'hydrateColumns', updateGroupingColumn); + useGridRegisterPipeProcessor(privateApiRef, 'hydrateRows', setServerSideGroups); + + useGridRegisterStrategyProcessor( + privateApiRef, + TREE_DATA_LAZY_LOADING_STRATEGY, + 'rowTreeCreation', + createRowTreeForTreeData, + ); + useGridRegisterStrategyProcessor( + privateApiRef, + TREE_DATA_LAZY_LOADING_STRATEGY, + 'filtering', + filterRows, + ); + useGridRegisterStrategyProcessor( + privateApiRef, + TREE_DATA_LAZY_LOADING_STRATEGY, + 'sorting', + sortRows, + ); + + /** + * 1ST RENDER + */ + useFirstRender(() => { + setStrategyAvailability(); + }); + + /** + * EFFECTS + */ + const isFirstRender = React.useRef(true); + React.useEffect(() => { + if (!isFirstRender.current) { + setStrategyAvailability(); + } else { + isFirstRender.current = false; + } + }, [setStrategyAvailability]); +}; diff --git a/packages/grid/x-data-grid-pro/src/hooks/features/treeData/useGridTreeDataPreProcessors.tsx b/packages/grid/x-data-grid-pro/src/hooks/features/treeData/useGridTreeDataPreProcessors.tsx index df50278abb0ab..dd1d15df73d4a 100644 --- a/packages/grid/x-data-grid-pro/src/hooks/features/treeData/useGridTreeDataPreProcessors.tsx +++ b/packages/grid/x-data-grid-pro/src/hooks/features/treeData/useGridTreeDataPreProcessors.tsx @@ -45,15 +45,16 @@ export const useGridTreeDataPreProcessors = ( | 'disableChildrenFiltering' | 'defaultGroupingExpansionDepth' | 'isGroupExpandedByDefault' + | 'rowsLoadingMode' >, ) => { const setStrategyAvailability = React.useCallback(() => { privateApiRef.current.setStrategyAvailability( 'rowTree', TREE_DATA_STRATEGY, - props.treeData ? () => true : () => false, + props.treeData && props.rowsLoadingMode === 'client' ? () => true : () => false, ); - }, [privateApiRef, props.treeData]); + }, [privateApiRef, props.treeData, props.rowsLoadingMode]); const getGroupingColDef = React.useCallback(() => { const groupingColDefProp = props.groupingColDef; diff --git a/packages/grid/x-data-grid-pro/src/models/dataGridProProps.ts b/packages/grid/x-data-grid-pro/src/models/dataGridProProps.ts index 7f3af31b3fc45..abd3104cbcff3 100644 --- a/packages/grid/x-data-grid-pro/src/models/dataGridProProps.ts +++ b/packages/grid/x-data-grid-pro/src/models/dataGridProProps.ts @@ -246,6 +246,11 @@ export interface DataGridProPropsWithoutDefaultValue; + /** + * Callback fired when children rows of a parent row are requested to be loaded. + * @param {GridFetchRowChildrenParams} params With all properties from [[GridFetchRowChildrenParams]]. + */ + onFetchRowChildren?: GridEventListener<'fetchRowChildren'>; /** * Rows data to pin on top or bottom. */ @@ -259,4 +264,10 @@ export interface DataGridProPropsWithoutDefaultValue boolean; } diff --git a/packages/grid/x-data-grid-pro/src/models/gridApiPro.ts b/packages/grid/x-data-grid-pro/src/models/gridApiPro.ts index bd5c6dddeccfc..cee3dde28a8f2 100644 --- a/packages/grid/x-data-grid-pro/src/models/gridApiPro.ts +++ b/packages/grid/x-data-grid-pro/src/models/gridApiPro.ts @@ -12,6 +12,7 @@ import type { GridRowPinningApi, GridDetailPanelPrivateApi, } from '../hooks'; +import type { GridTreeDataLazyLoadingApi } from '../hooks/features/treeData/gridTreeDataLazyLoadingApi'; /** * The api of `DataGridPro`. @@ -19,6 +20,7 @@ import type { export interface GridApiPro extends GridApiCommon, GridRowProApi, + GridTreeDataLazyLoadingApi, GridColumnPinningApi, GridDetailPanelApi, GridRowPinningApi, diff --git a/packages/grid/x-data-grid-pro/src/typeOverloads/modules.ts b/packages/grid/x-data-grid-pro/src/typeOverloads/modules.ts index 6300554f4f45e..31736352b8a48 100644 --- a/packages/grid/x-data-grid-pro/src/typeOverloads/modules.ts +++ b/packages/grid/x-data-grid-pro/src/typeOverloads/modules.ts @@ -10,6 +10,7 @@ import type { } from '../hooks/features/columnPinning/gridColumnPinningInterface'; import type { GridCanBeReorderedPreProcessingContext } from '../hooks/features/columnReorder/columnReorderInterfaces'; import { GridRowPinningInternalCache } from '../hooks/features/rowPinning/gridRowPinningInterface'; +import { GridFetchRowChildrenParams } from '../hooks/features/treeData/useGridTreeDataLazyLoading'; export interface GridColDefPro { /** @@ -47,6 +48,10 @@ export interface GridEventLookupPro { * Fired when a new batch of rows is requested to be loaded. Called with a [[GridFetchRowsParams]] object. */ fetchRows: { params: GridFetchRowsParams }; + /** + * Fired when a new batch of rows is requested to be loaded. Called with a [[GridFetchRowsParams]] object. + */ + fetchRowChildren: { params: GridFetchRowChildrenParams }; } export interface GridPipeProcessingLookupPro { diff --git a/packages/grid/x-data-grid/src/models/gridRows.ts b/packages/grid/x-data-grid/src/models/gridRows.ts index b97ee29055507..ee8861d48298c 100644 --- a/packages/grid/x-data-grid/src/models/gridRows.ts +++ b/packages/grid/x-data-grid/src/models/gridRows.ts @@ -114,6 +114,21 @@ export interface GridDataGroupNode extends GridBasicGroupNode { isAutoGenerated: false; } +export interface GridServerSideGroupNode extends GridDataGroupNode { + /** + * Will be used to store if the `fetchRowChildren` event is currently being fired for this node. + */ + isLoading: boolean; + /** + * If true, this node is a server side group node. + */ + isServerSide: boolean; + /** + * If true, this node has been expanded by the user and the children have been fetched. + */ + childrenFetched: boolean; +} + export type GridGroupNode = GridDataGroupNode | GridAutoGeneratedGroupNode; export type GridChildrenFromPathLookup = { @@ -166,6 +181,7 @@ export type GridPinnedRowNode = GridDataPinnedRowNode | GridAutoGeneratedPinnedR export type GridTreeNode = | GridLeafNode + | GridServerSideGroupNode | GridGroupNode | GridFooterNode | GridPinnedRowNode diff --git a/scripts/x-data-grid-premium.exports.json b/scripts/x-data-grid-premium.exports.json index ae513ba15ea75..7aacdca50f386 100644 --- a/scripts/x-data-grid-premium.exports.json +++ b/scripts/x-data-grid-premium.exports.json @@ -293,6 +293,7 @@ { "name": "GridExportOptions", "kind": "Interface" }, { "name": "GridExportStateParams", "kind": "Interface" }, { "name": "GridFeatureMode", "kind": "TypeAlias" }, + { "name": "GridFetchRowChildrenParams", "kind": "Interface" }, { "name": "GridFetchRowsParams", "kind": "Interface" }, { "name": "GridFileExportOptions", "kind": "Interface" }, { "name": "gridFilterableColumnDefinitionsSelector", "kind": "Variable" }, @@ -509,6 +510,7 @@ { "name": "GridSearchIcon", "kind": "Variable" }, { "name": "GridSelectedRowCount", "kind": "Variable" }, { "name": "GridSeparatorIcon", "kind": "Variable" }, + { "name": "GridServerSideGroupNode", "kind": "Interface" }, { "name": "GridSignature", "kind": "Enum" }, { "name": "GridSingleSelectColDef", "kind": "Interface" }, { "name": "GridSkeletonCell", "kind": "Function" }, diff --git a/scripts/x-data-grid-pro.exports.json b/scripts/x-data-grid-pro.exports.json index 95b1e32a0b994..253df90e53928 100644 --- a/scripts/x-data-grid-pro.exports.json +++ b/scripts/x-data-grid-pro.exports.json @@ -262,6 +262,7 @@ { "name": "GridExportOptions", "kind": "Interface" }, { "name": "GridExportStateParams", "kind": "Interface" }, { "name": "GridFeatureMode", "kind": "TypeAlias" }, + { "name": "GridFetchRowChildrenParams", "kind": "Interface" }, { "name": "GridFetchRowsParams", "kind": "Interface" }, { "name": "GridFileExportOptions", "kind": "Interface" }, { "name": "gridFilterableColumnDefinitionsSelector", "kind": "Variable" }, @@ -464,6 +465,7 @@ { "name": "GridSearchIcon", "kind": "Variable" }, { "name": "GridSelectedRowCount", "kind": "Variable" }, { "name": "GridSeparatorIcon", "kind": "Variable" }, + { "name": "GridServerSideGroupNode", "kind": "Interface" }, { "name": "GridSignature", "kind": "Enum" }, { "name": "GridSingleSelectColDef", "kind": "Interface" }, { "name": "GridSkeletonCell", "kind": "Function" }, diff --git a/scripts/x-data-grid.exports.json b/scripts/x-data-grid.exports.json index 532a331907fa6..fc3f9f46dbb78 100644 --- a/scripts/x-data-grid.exports.json +++ b/scripts/x-data-grid.exports.json @@ -420,6 +420,7 @@ { "name": "GridSearchIcon", "kind": "Variable" }, { "name": "GridSelectedRowCount", "kind": "Variable" }, { "name": "GridSeparatorIcon", "kind": "Variable" }, + { "name": "GridServerSideGroupNode", "kind": "Interface" }, { "name": "GridSignature", "kind": "Enum" }, { "name": "GridSingleSelectColDef", "kind": "Interface" }, { "name": "GridSkeletonCell", "kind": "Function" }, From 31bc7d94d34fd426d45004009d4a708289d5f8ea Mon Sep 17 00:00:00 2001 From: Bilal Shafi Date: Wed, 31 May 2023 17:30:59 +0500 Subject: [PATCH 2/9] Use real data in lazy load example + some improvements --- .../tree-data/TreeDataLazyLoading.tsx | 188 ++---------------- docs/data/data-grid/tree-data/tree-data.md | 9 +- .../src/hooks/useDemoData.ts | 83 +++++++- .../components/GridTreeDataGroupingCell.tsx | 17 +- .../src/models/dataGridProProps.ts | 6 + 5 files changed, 124 insertions(+), 179 deletions(-) diff --git a/docs/data/data-grid/tree-data/TreeDataLazyLoading.tsx b/docs/data/data-grid/tree-data/TreeDataLazyLoading.tsx index 5ff673cc8cc17..84c099814e809 100644 --- a/docs/data/data-grid/tree-data/TreeDataLazyLoading.tsx +++ b/docs/data/data-grid/tree-data/TreeDataLazyLoading.tsx @@ -1,204 +1,52 @@ -// TODO rows v6: Adapt to new lazy loading api import * as React from 'react'; import { DataGridPro, - DataGridProProps, useGridApiRef, GridFetchRowChildrenParams, GridValidRowModel, - GridRowModel, } from '@mui/x-data-grid-pro'; -import Alert from '@mui/material/Alert'; -import Snackbar from '@mui/material/Snackbar'; +import { useDemoData } from '@mui/x-data-grid-generator'; -export const isNavigationKey = (key: string) => - key === 'Home' || - key === 'End' || - key.indexOf('Arrow') === 0 || - key.indexOf('Page') === 0 || - key === ' '; - -const ALL_ROWS = [ - { - hierarchy: ['Sarah'], - jobTitle: 'Head of Human Resources', - recruitmentDate: new Date(2020, 8, 12), - id: 0, - }, - { - hierarchy: ['Thomas'], - jobTitle: 'Head of Sales', - recruitmentDate: new Date(2017, 3, 4), - id: 1, - }, - { - hierarchy: ['Thomas', 'Robert'], - jobTitle: 'Sales Person', - recruitmentDate: new Date(2020, 11, 20), - id: 2, - }, - { - hierarchy: ['Thomas', 'Karen'], - jobTitle: 'Sales Person', - recruitmentDate: new Date(2020, 10, 14), - id: 3, - }, - { - hierarchy: ['Thomas', 'Nancy'], - jobTitle: 'Sales Person', - recruitmentDate: new Date(2017, 10, 29), - id: 4, - }, - { - hierarchy: ['Thomas', 'Daniel'], - jobTitle: 'Sales Person', - recruitmentDate: new Date(2020, 7, 21), - id: 5, - }, - { - hierarchy: ['Thomas', 'Christopher'], - jobTitle: 'Sales Person', - recruitmentDate: new Date(2020, 7, 20), - id: 6, - }, - { - hierarchy: ['Thomas', 'Donald'], - jobTitle: 'Sales Person', - recruitmentDate: new Date(2019, 6, 28), - id: 7, - }, - { - hierarchy: ['Mary'], - jobTitle: 'Head of Engineering', - recruitmentDate: new Date(2016, 3, 14), - id: 8, - }, - { - hierarchy: ['Mary', 'Jennifer'], - jobTitle: 'Tech lead front', - recruitmentDate: new Date(2016, 5, 17), - id: 9, - }, - { - hierarchy: ['Mary', 'Jennifer', 'Anna'], - jobTitle: 'Front-end developer', - recruitmentDate: new Date(2019, 11, 7), - id: 10, - }, - { - hierarchy: ['Mary', 'Michael'], - jobTitle: 'Tech lead devops', - recruitmentDate: new Date(2021, 7, 1), - id: 11, - }, - { - hierarchy: ['Mary', 'Linda'], - jobTitle: 'Tech lead back', - recruitmentDate: new Date(2017, 0, 12), - id: 12, - }, - { - hierarchy: ['Mary', 'Linda', 'Elizabeth'], - jobTitle: 'Back-end developer', - recruitmentDate: new Date(2019, 2, 22), - id: 13, - }, - { - hierarchy: ['Mary', 'Linda', 'William'], - jobTitle: 'Back-end developer', - recruitmentDate: new Date(2018, 4, 19), - id: 14, - }, -]; - -const columns = [ - { field: 'jobTitle', headerName: 'Job Title', width: 200 }, - { - field: 'recruitmentDate', - headerName: 'Recruitment Date', - type: 'date', - width: 150, - }, -]; - -const getChildren = (parentPath: string[]) => { - const parentPathStr = parentPath.join('-'); - return ALL_ROWS.filter( - (row) => row.hierarchy.slice(0, -1).join('-') === parentPathStr, - ); -}; - -const fakeDataFetcher = (parentPath: string[] = []) => - new Promise((resolve, reject) => { - setTimeout(() => { - const rows = getChildren(parentPath).map((row) => ({ - ...row, - descendantCount: getChildren(row.hierarchy).length, - })); - if (parentPath.length !== 0 && Math.random() > 0.7) { - // 30% probablity of failure - reject(new Error('Network call failed randomly')); - } - resolve(rows); - }, 1000 + Math.random() * 300); - }); - -const getTreeDataPath: DataGridProProps['getTreeDataPath'] = (row) => row.hierarchy; - -const initRows: GridRowModel[] = []; +const initRows: GridValidRowModel[] = []; export default function TreeDataLazyLoading() { const apiRef = useGridApiRef(); - const [loading, setLoading] = React.useState(false); - const [showSnackbar, setShowSnackbar] = React.useState(false); + const { loading, data, lazyLoadTreeRows } = useDemoData({ + dataSet: 'Employee', + rowLength: 1000, + treeData: { maxDepth: 4, groupingField: 'name', averageChildren: 5 }, + }); const onFetchRowChildren = React.useCallback( async ({ row, helpers }: GridFetchRowChildrenParams) => { - if (showSnackbar) { - setShowSnackbar(false); - } try { - if (!row) { - setLoading(true); - } - const path = row ? getTreeDataPath!(row) : []; - const data = (await fakeDataFetcher(path)) as GridValidRowModel[]; - helpers.success(data); - if (!row) { - setLoading(false); - } + const path = row ? data.getTreeDataPath!(row) : []; + const rows = (await lazyLoadTreeRows({ path })) as GridValidRowModel[]; + helpers.success(rows); } catch (error) { // simulate network error helpers.error(); - setShowSnackbar(true); console.error(error); } }, - [showSnackbar], + [data.getTreeDataPath, lazyLoadTreeRows], ); + if (loading) { + return null; + } + return (
row.descendantCount! > 0} + isServerSideRow={(row) => row.hasChildren} + getDescendantCount={(row) => row.descendantCount} rowsLoadingMode="server" /> - setShowSnackbar(false)} - autoHideDuration={6000} - > - setShowSnackbar(false)} severity="error"> - Could not fetch data, please try again. - -
); } diff --git a/docs/data/data-grid/tree-data/tree-data.md b/docs/data/data-grid/tree-data/tree-data.md index e1ac3cc6fef11..5ae72042a8a84 100644 --- a/docs/data/data-grid/tree-data/tree-data.md +++ b/docs/data/data-grid/tree-data/tree-data.md @@ -129,10 +129,12 @@ interface GridFetchRowChildrenParams { } ``` -Do the API call in the `onFetchRowChildren` handler and call `helpers.success(newRows)` and `helpers.error()` respectively in case of success or error to let the grid update the related internal states. +The `onFetchRowChildren` handler is fired when the data for a specific row is requested, use it to fetch the data and call `helpers.success(newRows)` and `helpers.error()` respectively in case of success or error to let the grid update the related internal states. + +To enable lazy-loading for a given row, you also need to set the `isServerSideRow` prop to a function that returns `true` for the rows that have children and `false` for the rows that don't have children. If you have the information on server, you can provide an optional `getDescendantCount` prop which returns the number of descendants for a parent row. ```tsx -async function onFetchRowChildren({ row, server }: GridFetchRowChildrenParams) { +async function onFetchRowChildren({ row, helpers }: GridFetchRowChildrenParams) { try { const childRows = await fetchRows(row); helpers.success(childRows); @@ -146,7 +148,8 @@ async function onFetchRowChildren({ row, server }: GridFetchRowChildrenParams) { treeData getTreeDataPath={getTreeDataPath} onFetchRowChildren={onFetchRowChildren} - isServerSideRow={(row) => row.descendantCount! > 0} + isServerSideRow={(row) => row.hasChildren} + getDescendantCount={(row) => row.descendantCount} rowsLoadingMode="server" />; ``` diff --git a/packages/grid/x-data-grid-generator/src/hooks/useDemoData.ts b/packages/grid/x-data-grid-generator/src/hooks/useDemoData.ts index 677ff89be441b..e5010c8365e08 100644 --- a/packages/grid/x-data-grid-generator/src/hooks/useDemoData.ts +++ b/packages/grid/x-data-grid-generator/src/hooks/useDemoData.ts @@ -1,6 +1,6 @@ import * as React from 'react'; import LRUCache from 'lru-cache'; -import { GridColumnVisibilityModel } from '@mui/x-data-grid-premium'; +import { GridColumnVisibilityModel, GridRowModel } from '@mui/x-data-grid-premium'; import { GridDemoData, getRealGridData } from '../services/real-data-service'; import { getCommodityColumns } from '../columns/commodities.columns'; import { getEmployeeColumns } from '../columns/employees.columns'; @@ -17,11 +17,25 @@ const dataCache = new LRUCache({ ttl: 60 * 5 * 1e3, // 5 minutes }); +type ServerOptions = { + minDelay?: number; + maxDelay?: number; +}; + +const DEFAULT_SERVER_OPTIONS = { + minDelay: 300, + maxDelay: 1000, +}; + export type DemoDataReturnType = { data: DemoTreeDataValue; loading: boolean; setRowLength: (count: number) => void; loadNewData: () => void; + lazyLoadTreeRows: (params: { + path: string[]; + serverOptions?: ServerOptions; + }) => Promise; }; type DataSet = 'Commodity' | 'Employee'; @@ -122,6 +136,27 @@ export const getInitialState = (options: UseDemoDataOptions, columns: GridColDef return { columns: { columnVisibilityModel } }; }; +const findTreeDataRowChildren = ( + allRows: GridRowModel[], + parentPath: string[], + getTreeDataPath: (row: GridRowModel) => string[], + depth: number = 1, // the depth of the children to find relative to parentDepth, `-1` to find all +) => { + const parentDepth = parentPath.length; + const children = []; + for (let i = 0; i < allRows.length; i += 1) { + const row = allRows[i]; + const rowPath = getTreeDataPath(row); + if ( + ((depth < 0 && rowPath.length > parentDepth) || rowPath.length === parentDepth + depth) && + parentPath.every((value, index) => value === rowPath[index]) + ) { + children.push(row); + } + } + return children; +}; + export const useDemoData = (options: UseDemoDataOptions): DemoDataReturnType => { const [rowLength, setRowLength] = React.useState(options.rowLength); const [index, setIndex] = React.useState(0); @@ -206,6 +241,51 @@ export const useDemoData = (options: UseDemoDataOptions): DemoDataReturnType => columns, ]); + const lazyLoadTreeRows = React.useCallback( + ({ + path, + serverOptions = DEFAULT_SERVER_OPTIONS, + }: { + path: string[]; + serverOptions?: ServerOptions; + // TODO: Support server side filtering and sorting + filterModel?: any; + sortModel?: any; + }) => { + return new Promise((resolve, reject) => { + setTimeout(() => { + if (!options.treeData) { + reject(new Error('MUI: Please enable tree data in demo data options.')); + } + + const { maxDepth = 1, groupingField } = options.treeData!; + + const hasTreeData = maxDepth > 1 && groupingField != null; + if (!hasTreeData) { + reject( + new Error( + 'MUI: For tree data, maximum depth should be > 1 and grouping field should be set.', + ), + ); + } + const childRows = findTreeDataRowChildren(data.rows, path, data.getTreeDataPath!); + const childRowsWithDescendantCounts = childRows.map((row) => { + const descendants = findTreeDataRowChildren( + data.rows, + data.getTreeDataPath!(row), + data.getTreeDataPath!, + -1, + ); + const descendantCount = descendants.length; + return { ...row, descendantCount, hasChildren: descendantCount > 0 }; + }); + resolve(childRowsWithDescendantCounts); + }, Math.random() * (serverOptions.maxDelay! - serverOptions.minDelay!) + serverOptions.minDelay!); + }); + }, + [data, options.treeData], + ); + return { data, loading, @@ -213,5 +293,6 @@ export const useDemoData = (options: UseDemoDataOptions): DemoDataReturnType => loadNewData: () => { setIndex((oldIndex) => oldIndex + 1); }, + lazyLoadTreeRows, }; }; diff --git a/packages/grid/x-data-grid-pro/src/components/GridTreeDataGroupingCell.tsx b/packages/grid/x-data-grid-pro/src/components/GridTreeDataGroupingCell.tsx index 9f1aa69bc2da3..8a683c48cadba 100644 --- a/packages/grid/x-data-grid-pro/src/components/GridTreeDataGroupingCell.tsx +++ b/packages/grid/x-data-grid-pro/src/components/GridTreeDataGroupingCell.tsx @@ -40,12 +40,12 @@ interface GridTreeDataGroupingCellProps } interface GridTreeDataGroupingCellIconProps - extends Pick { + extends Pick { descendantCount: number; } function GridTreeDataGroupingCellIcon(props: GridTreeDataGroupingCellIconProps) { - const { rowNode, id, field, descendantCount } = props; + const { rowNode, row, id, field, descendantCount } = props; const apiRef = useGridPrivateApiContext(); const rootProps = useGridRootProps(); @@ -56,7 +56,6 @@ function GridTreeDataGroupingCellIcon(props: GridTreeDataGroupingCellIconProps) const handleClick = (event: React.MouseEvent) => { if (isServerSideNode && !rowNode.childrenExpanded && !areChildrenFetched) { const helpers = getLazyLoadingHelpers(apiRef, rowNode as GridServerSideGroupNode); - const row = apiRef.current.getRow(rowNode.id); apiRef.current.setRowLoadingStatus(rowNode.id, true); apiRef.current.publishEvent('fetchRowChildren', { row, helpers }); } else { @@ -95,6 +94,8 @@ function GridTreeDataGroupingCell(props: GridTreeDataGroupingCellProps) { const rootProps = useGridRootProps(); const apiRef = useGridPrivateApiContext(); + const row = apiRef.current.getRow(rowNode.id); + const isServerSideNode = (rowNode as GridServerSideGroupNode).isServerSide; const ownerState: OwnerState = { classes: rootProps.classes }; const classes = useUtilityClasses(ownerState); const filteredDescendantCountLookup = useGridSelector( @@ -104,6 +105,11 @@ function GridTreeDataGroupingCell(props: GridTreeDataGroupingCellProps) { const filteredDescendantCount = filteredDescendantCountLookup[rowNode.id] ?? 0; + const descendantCount = + isServerSideNode && rootProps.getDescendantCount + ? rootProps.getDescendantCount(row) + : filteredDescendantCount; + return (
@@ -111,12 +117,13 @@ function GridTreeDataGroupingCell(props: GridTreeDataGroupingCellProps) { id={id} field={field} rowNode={rowNode} - descendantCount={filteredDescendantCount} + row={row} + descendantCount={descendantCount} />
{formattedValue === undefined ? rowNode.groupingKey : formattedValue} - {!hideDescendantCount && filteredDescendantCount > 0 ? ` (${filteredDescendantCount})` : ''} + {!hideDescendantCount && descendantCount > 0 ? ` (${descendantCount})` : ''}
); diff --git a/packages/grid/x-data-grid-pro/src/models/dataGridProProps.ts b/packages/grid/x-data-grid-pro/src/models/dataGridProProps.ts index abd3104cbcff3..954f69a2fcf4f 100644 --- a/packages/grid/x-data-grid-pro/src/models/dataGridProProps.ts +++ b/packages/grid/x-data-grid-pro/src/models/dataGridProProps.ts @@ -270,4 +270,10 @@ export interface DataGridProPropsWithoutDefaultValue boolean; + /** + * Callback that checks the number of children for server side rows. i.e. rows that have `isServerSideRow` returning true. + * @param {GridValidRowModel} row The row to test. + * @returns {number} A boolean indicating if the row has children on server. + */ + getDescendantCount?: (row: GridValidRowModel) => number; } From 13e8406ef26938777be3057281f69adeb25db609 Mon Sep 17 00:00:00 2001 From: Bilal Shafi Date: Wed, 31 May 2023 18:25:04 +0500 Subject: [PATCH 3/9] Some housekeeping --- .../tree-data/TreeDataLazyLoading.js | 184 ++---------------- .../tree-data/TreeDataLazyLoading.tsx.preview | 9 + .../src/DataGridPremium/DataGridPremium.tsx | 6 + .../src/models/gridApiPremium.ts | 2 + .../src/DataGridPro/DataGridPro.tsx | 6 + .../src/hooks/features/treeData/index.ts | 3 +- 6 files changed, 42 insertions(+), 168 deletions(-) create mode 100644 docs/data/data-grid/tree-data/TreeDataLazyLoading.tsx.preview diff --git a/docs/data/data-grid/tree-data/TreeDataLazyLoading.js b/docs/data/data-grid/tree-data/TreeDataLazyLoading.js index 97514a498aceb..60008f54c2d01 100644 --- a/docs/data/data-grid/tree-data/TreeDataLazyLoading.js +++ b/docs/data/data-grid/tree-data/TreeDataLazyLoading.js @@ -1,197 +1,47 @@ -// TODO rows v6: Adapt to new lazy loading api import * as React from 'react'; import { DataGridPro, useGridApiRef } from '@mui/x-data-grid-pro'; -import Alert from '@mui/material/Alert'; -import Snackbar from '@mui/material/Snackbar'; - -export const isNavigationKey = (key) => - key === 'Home' || - key === 'End' || - key.indexOf('Arrow') === 0 || - key.indexOf('Page') === 0 || - key === ' '; - -const ALL_ROWS = [ - { - hierarchy: ['Sarah'], - jobTitle: 'Head of Human Resources', - recruitmentDate: new Date(2020, 8, 12), - id: 0, - }, - { - hierarchy: ['Thomas'], - jobTitle: 'Head of Sales', - recruitmentDate: new Date(2017, 3, 4), - id: 1, - }, - { - hierarchy: ['Thomas', 'Robert'], - jobTitle: 'Sales Person', - recruitmentDate: new Date(2020, 11, 20), - id: 2, - }, - { - hierarchy: ['Thomas', 'Karen'], - jobTitle: 'Sales Person', - recruitmentDate: new Date(2020, 10, 14), - id: 3, - }, - { - hierarchy: ['Thomas', 'Nancy'], - jobTitle: 'Sales Person', - recruitmentDate: new Date(2017, 10, 29), - id: 4, - }, - { - hierarchy: ['Thomas', 'Daniel'], - jobTitle: 'Sales Person', - recruitmentDate: new Date(2020, 7, 21), - id: 5, - }, - { - hierarchy: ['Thomas', 'Christopher'], - jobTitle: 'Sales Person', - recruitmentDate: new Date(2020, 7, 20), - id: 6, - }, - { - hierarchy: ['Thomas', 'Donald'], - jobTitle: 'Sales Person', - recruitmentDate: new Date(2019, 6, 28), - id: 7, - }, - { - hierarchy: ['Mary'], - jobTitle: 'Head of Engineering', - recruitmentDate: new Date(2016, 3, 14), - id: 8, - }, - { - hierarchy: ['Mary', 'Jennifer'], - jobTitle: 'Tech lead front', - recruitmentDate: new Date(2016, 5, 17), - id: 9, - }, - { - hierarchy: ['Mary', 'Jennifer', 'Anna'], - jobTitle: 'Front-end developer', - recruitmentDate: new Date(2019, 11, 7), - id: 10, - }, - { - hierarchy: ['Mary', 'Michael'], - jobTitle: 'Tech lead devops', - recruitmentDate: new Date(2021, 7, 1), - id: 11, - }, - { - hierarchy: ['Mary', 'Linda'], - jobTitle: 'Tech lead back', - recruitmentDate: new Date(2017, 0, 12), - id: 12, - }, - { - hierarchy: ['Mary', 'Linda', 'Elizabeth'], - jobTitle: 'Back-end developer', - recruitmentDate: new Date(2019, 2, 22), - id: 13, - }, - { - hierarchy: ['Mary', 'Linda', 'William'], - jobTitle: 'Back-end developer', - recruitmentDate: new Date(2018, 4, 19), - id: 14, - }, -]; - -const columns = [ - { field: 'jobTitle', headerName: 'Job Title', width: 200 }, - { - field: 'recruitmentDate', - headerName: 'Recruitment Date', - type: 'date', - width: 150, - }, -]; - -const getChildren = (parentPath) => { - const parentPathStr = parentPath.join('-'); - return ALL_ROWS.filter( - (row) => row.hierarchy.slice(0, -1).join('-') === parentPathStr, - ); -}; - -const fakeDataFetcher = (parentPath = []) => - new Promise((resolve, reject) => { - setTimeout(() => { - const rows = getChildren(parentPath).map((row) => ({ - ...row, - descendantCount: getChildren(row.hierarchy).length, - })); - if (parentPath.length !== 0 && Math.random() > 0.7) { - // 30% probablity of failure - reject(new Error('Network call failed randomly')); - } - resolve(rows); - }, 1000 + Math.random() * 300); - }); - -const getTreeDataPath = (row) => row.hierarchy; +import { useDemoData } from '@mui/x-data-grid-generator'; const initRows = []; export default function TreeDataLazyLoading() { const apiRef = useGridApiRef(); - const [loading, setLoading] = React.useState(false); - const [showSnackbar, setShowSnackbar] = React.useState(false); + const { loading, data, lazyLoadTreeRows } = useDemoData({ + dataSet: 'Employee', + rowLength: 1000, + treeData: { maxDepth: 4, groupingField: 'name', averageChildren: 5 }, + }); const onFetchRowChildren = React.useCallback( async ({ row, helpers }) => { - if (showSnackbar) { - setShowSnackbar(false); - } try { - if (!row) { - setLoading(true); - } - const path = row ? getTreeDataPath(row) : []; - const data = await fakeDataFetcher(path); - helpers.success(data); - if (!row) { - setLoading(false); - } + const path = row ? data.getTreeDataPath(row) : []; + const rows = await lazyLoadTreeRows({ path }); + helpers.success(rows); } catch (error) { // simulate network error helpers.error(); - setShowSnackbar(true); console.error(error); } }, - [showSnackbar], + [data.getTreeDataPath, lazyLoadTreeRows], ); + if (loading) { + return null; + } + return (
row.descendantCount > 0} + isServerSideRow={(row) => row.hasChildren} + getDescendantCount={(row) => row.descendantCount} rowsLoadingMode="server" /> - setShowSnackbar(false)} - autoHideDuration={6000} - > - setShowSnackbar(false)} severity="error"> - Could not fetch data, please try again. - -
); } diff --git a/docs/data/data-grid/tree-data/TreeDataLazyLoading.tsx.preview b/docs/data/data-grid/tree-data/TreeDataLazyLoading.tsx.preview new file mode 100644 index 0000000000000..c48c98adc0be1 --- /dev/null +++ b/docs/data/data-grid/tree-data/TreeDataLazyLoading.tsx.preview @@ -0,0 +1,9 @@ + row.hasChildren} + getDescendantCount={(row) => row.descendantCount} + rowsLoadingMode="server" +/> \ No newline at end of file diff --git a/packages/grid/x-data-grid-premium/src/DataGridPremium/DataGridPremium.tsx b/packages/grid/x-data-grid-premium/src/DataGridPremium/DataGridPremium.tsx index 675d1bf5c6a22..f0bd84dd7d189 100644 --- a/packages/grid/x-data-grid-premium/src/DataGridPremium/DataGridPremium.tsx +++ b/packages/grid/x-data-grid-premium/src/DataGridPremium/DataGridPremium.tsx @@ -337,6 +337,12 @@ DataGridPremiumRaw.propTypes = { * @returns {string} The CSS class to apply to the cell. */ getCellClassName: PropTypes.func, + /** + * Callback that checks the number of children for server side rows. i.e. rows that have `isServerSideRow` returning true. + * @param {GridValidRowModel} row The row to test. + * @returns {number} A boolean indicating if the row has children on server. + */ + getDescendantCount: PropTypes.func, /** * Function that returns the element to render in row detail. * @param {GridRowParams} params With all properties from [[GridRowParams]]. diff --git a/packages/grid/x-data-grid-premium/src/models/gridApiPremium.ts b/packages/grid/x-data-grid-premium/src/models/gridApiPremium.ts index 3577b6ca9c3bd..c52f97d378e9e 100644 --- a/packages/grid/x-data-grid-premium/src/models/gridApiPremium.ts +++ b/packages/grid/x-data-grid-premium/src/models/gridApiPremium.ts @@ -8,6 +8,7 @@ import { GridRowMultiSelectionApi, GridColumnReorderApi, GridRowProApi, + GridTreeDataLazyLoadingApi, } from '@mui/x-data-grid-pro'; import { GridInitialStatePremium, GridStatePremium } from './gridStatePremium'; import type { GridRowGroupingApi, GridExcelExportApi, GridAggregationApi } from '../hooks'; @@ -20,6 +21,7 @@ import { GridCellSelectionApi } from '../hooks/features/cellSelection/gridCellSe export interface GridApiPremium extends GridApiCommon, GridRowProApi, + GridTreeDataLazyLoadingApi, GridColumnPinningApi, GridDetailPanelApi, GridRowGroupingApi, diff --git a/packages/grid/x-data-grid-pro/src/DataGridPro/DataGridPro.tsx b/packages/grid/x-data-grid-pro/src/DataGridPro/DataGridPro.tsx index 4c16a88c94b4b..552542687d2f7 100644 --- a/packages/grid/x-data-grid-pro/src/DataGridPro/DataGridPro.tsx +++ b/packages/grid/x-data-grid-pro/src/DataGridPro/DataGridPro.tsx @@ -297,6 +297,12 @@ DataGridProRaw.propTypes = { * @returns {string} The CSS class to apply to the cell. */ getCellClassName: PropTypes.func, + /** + * Callback that checks the number of children for server side rows. i.e. rows that have `isServerSideRow` returning true. + * @param {GridValidRowModel} row The row to test. + * @returns {number} A boolean indicating if the row has children on server. + */ + getDescendantCount: PropTypes.func, /** * Function that returns the element to render in row detail. * @param {GridRowParams} params With all properties from [[GridRowParams]]. diff --git a/packages/grid/x-data-grid-pro/src/hooks/features/treeData/index.ts b/packages/grid/x-data-grid-pro/src/hooks/features/treeData/index.ts index 6251d94518f5c..3ea2d84942dbe 100644 --- a/packages/grid/x-data-grid-pro/src/hooks/features/treeData/index.ts +++ b/packages/grid/x-data-grid-pro/src/hooks/features/treeData/index.ts @@ -1,2 +1,3 @@ export { GRID_TREE_DATA_GROUPING_FIELD } from './gridTreeDataGroupColDef'; -export { GridFetchRowChildrenParams } from './useGridTreeDataLazyLoading'; +export type { GridFetchRowChildrenParams } from './useGridTreeDataLazyLoading'; +export type { GridTreeDataLazyLoadingApi } from './gridTreeDataLazyLoadingApi'; \ No newline at end of file From af31bedcb7c7ecc27e32ed06eb97bdc665c24786 Mon Sep 17 00:00:00 2001 From: Bilal Shafi Date: Wed, 31 May 2023 18:30:53 +0500 Subject: [PATCH 4/9] prettier --- .../grid/x-data-grid-pro/src/hooks/features/treeData/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/grid/x-data-grid-pro/src/hooks/features/treeData/index.ts b/packages/grid/x-data-grid-pro/src/hooks/features/treeData/index.ts index 3ea2d84942dbe..5edf6c4ac87a8 100644 --- a/packages/grid/x-data-grid-pro/src/hooks/features/treeData/index.ts +++ b/packages/grid/x-data-grid-pro/src/hooks/features/treeData/index.ts @@ -1,3 +1,3 @@ export { GRID_TREE_DATA_GROUPING_FIELD } from './gridTreeDataGroupColDef'; export type { GridFetchRowChildrenParams } from './useGridTreeDataLazyLoading'; -export type { GridTreeDataLazyLoadingApi } from './gridTreeDataLazyLoadingApi'; \ No newline at end of file +export type { GridTreeDataLazyLoadingApi } from './gridTreeDataLazyLoadingApi'; From 292868771103e184fbed3358253a5bbf9a34f1d1 Mon Sep 17 00:00:00 2001 From: Bilal Shafi Date: Sun, 11 Jun 2023 14:28:28 +0500 Subject: [PATCH 5/9] Server side filter POC --- .../tree-data/TreeDataLazyLoading.js | 25 ++++- .../tree-data/TreeDataLazyLoading.tsx | 27 +++++- .../tree-data/TreeDataLazyLoading.tsx.preview | 9 -- .../x/api/data-grid/data-grid-premium.json | 3 + docs/pages/x/api/data-grid/data-grid-pro.json | 3 + .../api-docs/data-grid/data-grid-premium.json | 3 + .../api-docs/data-grid/data-grid-pro.json | 3 + .../src/hooks/filterUtils.ts | 97 +++++++++++++++++++ .../src/hooks/useDemoData.ts | 41 +++++--- .../components/GridTreeDataGroupingCell.tsx | 4 +- .../treeData/useGridTreeDataLazyLoading.tsx | 68 +++++++++---- scripts/x-data-grid-premium.exports.json | 1 + scripts/x-data-grid-pro.exports.json | 1 + 13 files changed, 233 insertions(+), 52 deletions(-) delete mode 100644 docs/data/data-grid/tree-data/TreeDataLazyLoading.tsx.preview create mode 100644 packages/grid/x-data-grid-generator/src/hooks/filterUtils.ts diff --git a/docs/data/data-grid/tree-data/TreeDataLazyLoading.js b/docs/data/data-grid/tree-data/TreeDataLazyLoading.js index 60008f54c2d01..93015d49e386e 100644 --- a/docs/data/data-grid/tree-data/TreeDataLazyLoading.js +++ b/docs/data/data-grid/tree-data/TreeDataLazyLoading.js @@ -9,14 +9,16 @@ export default function TreeDataLazyLoading() { const { loading, data, lazyLoadTreeRows } = useDemoData({ dataSet: 'Employee', rowLength: 1000, - treeData: { maxDepth: 4, groupingField: 'name', averageChildren: 5 }, + treeData: { maxDepth: 2, groupingField: 'name', averageChildren: 10 }, }); const onFetchRowChildren = React.useCallback( - async ({ row, helpers }) => { + async ({ row, helpers, filterModel }) => { try { const path = row ? data.getTreeDataPath(row) : []; - const rows = await lazyLoadTreeRows({ path }); + const rows = await lazyLoadTreeRows({ + request: { path, filterModel }, + }); helpers.success(rows); } catch (error) { // simulate network error @@ -37,10 +39,27 @@ export default function TreeDataLazyLoading() { {...data} apiRef={apiRef} rows={initRows} + unstable_headerFilters onFetchRowChildren={onFetchRowChildren} + initialState={{ + ...data.initialState, + columns: { + ...data.initialState?.columns, + columnVisibilityModel: { + ...data.initialState?.columns?.columnVisibilityModel, + avatar: false, + }, + }, + filter: { + filterModel: { + items: [{ field: 'website', operator: 'contains', value: 'ab' }], + }, + }, + }} isServerSideRow={(row) => row.hasChildren} getDescendantCount={(row) => row.descendantCount} rowsLoadingMode="server" + filterMode="server" /> ); diff --git a/docs/data/data-grid/tree-data/TreeDataLazyLoading.tsx b/docs/data/data-grid/tree-data/TreeDataLazyLoading.tsx index 84c099814e809..abb9566f8765f 100644 --- a/docs/data/data-grid/tree-data/TreeDataLazyLoading.tsx +++ b/docs/data/data-grid/tree-data/TreeDataLazyLoading.tsx @@ -14,14 +14,16 @@ export default function TreeDataLazyLoading() { const { loading, data, lazyLoadTreeRows } = useDemoData({ dataSet: 'Employee', rowLength: 1000, - treeData: { maxDepth: 4, groupingField: 'name', averageChildren: 5 }, + treeData: { maxDepth: 2, groupingField: 'name', averageChildren: 10 }, }); const onFetchRowChildren = React.useCallback( - async ({ row, helpers }: GridFetchRowChildrenParams) => { + async ({ row, helpers, filterModel }: GridFetchRowChildrenParams) => { try { const path = row ? data.getTreeDataPath!(row) : []; - const rows = (await lazyLoadTreeRows({ path })) as GridValidRowModel[]; + const rows = (await lazyLoadTreeRows({ + request: { path, filterModel }, + })) as GridValidRowModel[]; helpers.success(rows); } catch (error) { // simulate network error @@ -42,10 +44,29 @@ export default function TreeDataLazyLoading() { {...data} apiRef={apiRef} rows={initRows} + unstable_headerFilters onFetchRowChildren={onFetchRowChildren} + initialState={{ + ...data.initialState, + columns: { + ...data.initialState?.columns, + columnVisibilityModel: { + ...data.initialState?.columns?.columnVisibilityModel, + avatar: false, + }, + }, + filter: { + filterModel: { + items: [ + { field: 'website', operator: 'contains', value: 'ab' }, + ] + } + } + }} isServerSideRow={(row) => row.hasChildren} getDescendantCount={(row) => row.descendantCount} rowsLoadingMode="server" + filterMode="server" /> ); diff --git a/docs/data/data-grid/tree-data/TreeDataLazyLoading.tsx.preview b/docs/data/data-grid/tree-data/TreeDataLazyLoading.tsx.preview deleted file mode 100644 index c48c98adc0be1..0000000000000 --- a/docs/data/data-grid/tree-data/TreeDataLazyLoading.tsx.preview +++ /dev/null @@ -1,9 +0,0 @@ - row.hasChildren} - getDescendantCount={(row) => row.descendantCount} - rowsLoadingMode="server" -/> \ No newline at end of file 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 63fd176f346e5..a93ef13cc7040 100644 --- a/docs/pages/x/api/data-grid/data-grid-premium.json +++ b/docs/pages/x/api/data-grid/data-grid-premium.json @@ -97,6 +97,7 @@ "default": "`(groupNode) => groupNode == null ? 'footer' : 'inline'`" }, "getCellClassName": { "type": { "name": "func" } }, + "getDescendantCount": { "type": { "name": "func" } }, "getDetailPanelContent": { "type": { "name": "func" } }, "getDetailPanelHeight": { "type": { "name": "func" }, "default": "\"() => 500\"" }, "getEstimatedRowHeight": { "type": { "name": "func" } }, @@ -117,6 +118,7 @@ "isCellEditable": { "type": { "name": "func" } }, "isGroupExpandedByDefault": { "type": { "name": "func" } }, "isRowSelectable": { "type": { "name": "func" } }, + "isServerSideRow": { "type": { "name": "func" } }, "keepColumnPositionIfDraggedOutside": { "type": { "name": "bool" } }, "keepNonExistentRowsSelected": { "type": { "name": "bool" } }, "loading": { "type": { "name": "bool" } }, @@ -158,6 +160,7 @@ "onColumnWidthChange": { "type": { "name": "func" } }, "onDetailPanelExpandedRowIdsChange": { "type": { "name": "func" } }, "onExcelExportStateChange": { "type": { "name": "func" } }, + "onFetchRowChildren": { "type": { "name": "func" } }, "onFetchRows": { "type": { "name": "func" } }, "onFilterModelChange": { "type": { "name": "func" } }, "onMenuClose": { "type": { "name": "func" } }, 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 45bdf2347c5be..f1c5d15d1be19 100644 --- a/docs/pages/x/api/data-grid/data-grid-pro.json +++ b/docs/pages/x/api/data-grid/data-grid-pro.json @@ -81,6 +81,7 @@ } }, "getCellClassName": { "type": { "name": "func" } }, + "getDescendantCount": { "type": { "name": "func" } }, "getDetailPanelContent": { "type": { "name": "func" } }, "getDetailPanelHeight": { "type": { "name": "func" }, "default": "\"() => 500\"" }, "getEstimatedRowHeight": { "type": { "name": "func" } }, @@ -101,6 +102,7 @@ "isCellEditable": { "type": { "name": "func" } }, "isGroupExpandedByDefault": { "type": { "name": "func" } }, "isRowSelectable": { "type": { "name": "func" } }, + "isServerSideRow": { "type": { "name": "func" } }, "keepColumnPositionIfDraggedOutside": { "type": { "name": "bool" } }, "keepNonExistentRowsSelected": { "type": { "name": "bool" } }, "loading": { "type": { "name": "bool" } }, @@ -138,6 +140,7 @@ "onColumnVisibilityModelChange": { "type": { "name": "func" } }, "onColumnWidthChange": { "type": { "name": "func" } }, "onDetailPanelExpandedRowIdsChange": { "type": { "name": "func" } }, + "onFetchRowChildren": { "type": { "name": "func" } }, "onFetchRows": { "type": { "name": "func" } }, "onFilterModelChange": { "type": { "name": "func" } }, "onMenuClose": { "type": { "name": "func" } }, diff --git a/docs/translations/api-docs/data-grid/data-grid-premium.json b/docs/translations/api-docs/data-grid/data-grid-premium.json index b9bd6b0b2aaac..f5017a139046b 100644 --- a/docs/translations/api-docs/data-grid/data-grid-premium.json +++ b/docs/translations/api-docs/data-grid/data-grid-premium.json @@ -47,6 +47,7 @@ "filterModel": "Set the filter model of the grid.", "getAggregationPosition": "Determines the position of an aggregated value.

Signature:
function(groupNode: GridGroupNode) => GridAggregationPosition | null
groupNode: The current group.
returns (GridAggregationPosition | null): Position of the aggregated value (if null, the group isn't aggregated).", "getCellClassName": "Function that applies CSS classes dynamically on cells.

Signature:
function(params: GridCellParams) => string
params: With all properties from GridCellParams.
returns (string): The CSS class to apply to the cell.", + "getDescendantCount": "Callback that checks the number of children for server side rows. i.e. rows that have isServerSideRow returning true.

Signature:
function(row: GridValidRowModel) => number
row: The row to test.
returns (number): A boolean indicating if the row has children on server.", "getDetailPanelContent": "Function that returns the element to render in row detail.

Signature:
function(params: GridRowParams) => JSX.Element
params: With all properties from GridRowParams.
returns (JSX.Element): The row detail element.", "getDetailPanelHeight": "Function that returns the height of the row detail panel.

Signature:
function(params: GridRowParams) => number | string
params: With all properties from GridRowParams.
returns (number | string): The height in pixels or "auto" to use the content height.", "getEstimatedRowHeight": "Function that returns the estimated height for a row. Only works if dynamic row height is used. Once the row height is measured this value is discarded.

Signature:
function(params: GridRowHeightParams) => number | null
params: With all properties from GridRowHeightParams.
returns (number | null): The estimated row height value. If null or undefined then the default row height, based on the density, is applied.", @@ -64,6 +65,7 @@ "isCellEditable": "Callback fired when a cell is rendered, returns true if the cell is editable.

Signature:
function(params: GridCellParams) => boolean
params: With all properties from GridCellParams.
returns (boolean): A boolean indicating if the cell is editable.", "isGroupExpandedByDefault": "Determines if a group should be expanded after its creation. This prop takes priority over the defaultGroupingExpansionDepth prop.

Signature:
function(node: GridGroupNode) => boolean
node: The node of the group to test.
returns (boolean): A boolean indicating if the group is expanded.", "isRowSelectable": "Determines if a row can be selected.

Signature:
function(params: GridRowParams) => boolean
params: With all properties from GridRowParams.
returns (boolean): A boolean indicating if the cell is selectable.", + "isServerSideRow": "Callback that returns true for those rows which have children on server.

Signature:
function(row: GridValidRowModel) => boolean
row: The row to test.
returns (boolean): A boolean indicating if the row has children on server.", "keepColumnPositionIfDraggedOutside": "If true, moving the mouse pointer outside the grid before releasing the mouse button in a column re-order action will not cause the column to jump back to its original position.", "keepNonExistentRowsSelected": "If true, the selection model will retain selected rows that do not exist. Useful when using server side pagination and row selections need to be retained when changing pages.", "loading": "If true, a loading overlay is displayed.", @@ -93,6 +95,7 @@ "onColumnWidthChange": "Callback fired when the width of a column is changed.

Signature:
function(params: GridColumnResizeParams, event: MuiEvent<React.MouseEvent>, details: GridCallbackDetails) => void
params: With all properties from GridColumnResizeParams.
event: The event object.
details: Additional details for this callback.", "onDetailPanelExpandedRowIdsChange": "Callback fired when the detail panel of a row is opened or closed.

Signature:
function(ids: Array<GridRowId>, details: GridCallbackDetails) => void
ids: The ids of the rows which have the detail panel open.
details: Additional details for this callback.", "onExcelExportStateChange": "Callback fired when the state of the Excel export changes.

Signature:
function(inProgress: string) => void
inProgress: Indicates if the task is in progress.", + "onFetchRowChildren": "Callback fired when children rows of a parent row are requested to be loaded.

Signature:
function(params: GridFetchRowChildrenParams) => void
params: With all properties from GridFetchRowChildrenParams.", "onFetchRows": "Callback fired when rowCount is set and the next batch of virtualized rows is rendered.

Signature:
function(params: GridFetchRowsParams, event: MuiEvent<{}>, details: GridCallbackDetails) => void
params: With all properties from GridFetchRowsParams.
event: The event object.
details: Additional details for this callback.", "onFilterModelChange": "Callback fired when the Filter model changes before the filters are applied.

Signature:
function(model: GridFilterModel, details: GridCallbackDetails) => void
model: With all properties from GridFilterModel.
details: Additional details for this callback.", "onMenuClose": "Callback fired when the menu is closed.

Signature:
function(params: GridMenuParams, event: MuiEvent<{}>, details: GridCallbackDetails) => void
params: With all properties from GridMenuParams.
event: The event object.
details: Additional details for this callback.", diff --git a/docs/translations/api-docs/data-grid/data-grid-pro.json b/docs/translations/api-docs/data-grid/data-grid-pro.json index 58dd8a3aea82e..6b0a79934f9e3 100644 --- a/docs/translations/api-docs/data-grid/data-grid-pro.json +++ b/docs/translations/api-docs/data-grid/data-grid-pro.json @@ -40,6 +40,7 @@ "filterMode": "Filtering can be processed on the server or client-side. Set it to 'server' if you would like to handle filtering on the server-side.", "filterModel": "Set the filter model of the grid.", "getCellClassName": "Function that applies CSS classes dynamically on cells.

Signature:
function(params: GridCellParams) => string
params: With all properties from GridCellParams.
returns (string): The CSS class to apply to the cell.", + "getDescendantCount": "Callback that checks the number of children for server side rows. i.e. rows that have isServerSideRow returning true.

Signature:
function(row: GridValidRowModel) => number
row: The row to test.
returns (number): A boolean indicating if the row has children on server.", "getDetailPanelContent": "Function that returns the element to render in row detail.

Signature:
function(params: GridRowParams) => JSX.Element
params: With all properties from GridRowParams.
returns (JSX.Element): The row detail element.", "getDetailPanelHeight": "Function that returns the height of the row detail panel.

Signature:
function(params: GridRowParams) => number | string
params: With all properties from GridRowParams.
returns (number | string): The height in pixels or "auto" to use the content height.", "getEstimatedRowHeight": "Function that returns the estimated height for a row. Only works if dynamic row height is used. Once the row height is measured this value is discarded.

Signature:
function(params: GridRowHeightParams) => number | null
params: With all properties from GridRowHeightParams.
returns (number | null): The estimated row height value. If null or undefined then the default row height, based on the density, is applied.", @@ -57,6 +58,7 @@ "isCellEditable": "Callback fired when a cell is rendered, returns true if the cell is editable.

Signature:
function(params: GridCellParams) => boolean
params: With all properties from GridCellParams.
returns (boolean): A boolean indicating if the cell is editable.", "isGroupExpandedByDefault": "Determines if a group should be expanded after its creation. This prop takes priority over the defaultGroupingExpansionDepth prop.

Signature:
function(node: GridGroupNode) => boolean
node: The node of the group to test.
returns (boolean): A boolean indicating if the group is expanded.", "isRowSelectable": "Determines if a row can be selected.

Signature:
function(params: GridRowParams) => boolean
params: With all properties from GridRowParams.
returns (boolean): A boolean indicating if the cell is selectable.", + "isServerSideRow": "Callback that returns true for those rows which have children on server.

Signature:
function(row: GridValidRowModel) => boolean
row: The row to test.
returns (boolean): A boolean indicating if the row has children on server.", "keepColumnPositionIfDraggedOutside": "If true, moving the mouse pointer outside the grid before releasing the mouse button in a column re-order action will not cause the column to jump back to its original position.", "keepNonExistentRowsSelected": "If true, the selection model will retain selected rows that do not exist. Useful when using server side pagination and row selections need to be retained when changing pages.", "loading": "If true, a loading overlay is displayed.", @@ -82,6 +84,7 @@ "onColumnVisibilityModelChange": "Callback fired when the column visibility model changes.

Signature:
function(model: GridColumnVisibilityModel, details: GridCallbackDetails) => void
model: The new model.
details: Additional details for this callback.", "onColumnWidthChange": "Callback fired when the width of a column is changed.

Signature:
function(params: GridColumnResizeParams, event: MuiEvent<React.MouseEvent>, details: GridCallbackDetails) => void
params: With all properties from GridColumnResizeParams.
event: The event object.
details: Additional details for this callback.", "onDetailPanelExpandedRowIdsChange": "Callback fired when the detail panel of a row is opened or closed.

Signature:
function(ids: Array<GridRowId>, details: GridCallbackDetails) => void
ids: The ids of the rows which have the detail panel open.
details: Additional details for this callback.", + "onFetchRowChildren": "Callback fired when children rows of a parent row are requested to be loaded.

Signature:
function(params: GridFetchRowChildrenParams) => void
params: With all properties from GridFetchRowChildrenParams.", "onFetchRows": "Callback fired when rowCount is set and the next batch of virtualized rows is rendered.

Signature:
function(params: GridFetchRowsParams, event: MuiEvent<{}>, details: GridCallbackDetails) => void
params: With all properties from GridFetchRowsParams.
event: The event object.
details: Additional details for this callback.", "onFilterModelChange": "Callback fired when the Filter model changes before the filters are applied.

Signature:
function(model: GridFilterModel, details: GridCallbackDetails) => void
model: With all properties from GridFilterModel.
details: Additional details for this callback.", "onMenuClose": "Callback fired when the menu is closed.

Signature:
function(params: GridMenuParams, event: MuiEvent<{}>, details: GridCallbackDetails) => void
params: With all properties from GridMenuParams.
event: The event object.
details: Additional details for this callback.", diff --git a/packages/grid/x-data-grid-generator/src/hooks/filterUtils.ts b/packages/grid/x-data-grid-generator/src/hooks/filterUtils.ts new file mode 100644 index 0000000000000..7e4340060ff6c --- /dev/null +++ b/packages/grid/x-data-grid-generator/src/hooks/filterUtils.ts @@ -0,0 +1,97 @@ +import { + GridColDef, + GridFilterOperator, + GridRowModel, + getGridStringOperators, + getGridBooleanOperators, + getGridDateOperators, + getGridNumericOperators, + getGridSingleSelectOperators, + GridFilterModel, +} from '@mui/x-data-grid-premium'; + +const FILTER_OPERATORS: { [columnType: string]: any } = { + string: getGridStringOperators, + boolean: getGridBooleanOperators, + date: getGridDateOperators, + dateTime: getGridDateOperators, + number: getGridNumericOperators, + singleSelect: getGridSingleSelectOperators, +}; + +export const getFilterOperators = (column: GridColDef): GridFilterOperator[] => { + return FILTER_OPERATORS[column.type || 'string'](); +}; + +export const filterServerSide = ( + columns: GridColDef[], + rows: GridRowModel[], + filterModel: GridFilterModel, +): GridRowModel[] => { + const filterModelItems = filterModel.items ?? []; + if (!filterModelItems.length) { + return rows; + } + const appliers = new Map(); + filterModelItems.forEach((filterItem) => { + const column = columns.find((col) => col.field === filterItem.field); + if (!column) { + return; + } + const filterOperator = getFilterOperators(column).find( + (operator) => operator.value === filterItem.operator, + ); + if (!filterOperator) { + return; + } + if (!filterItem.value && filterOperator.requiresFilterValue) { + return; + } + const applyFilterFn = filterOperator.getApplyFilterFn(filterItem, column); + if (!applyFilterFn) { + return; + } + appliers.set(column.field, applyFilterFn); + }); + + const addedRowsIds = new Set(); + + // find all the rows that satisfy the filter criteria + const flatFilteredRows = rows.filter((row) => { + let passed = true; + appliers.forEach((applier, field) => { + if (!applier({ value: row[field] })) { + passed = false; + } + }); + if (passed) { + addedRowsIds.add(row.id); + } + return passed; + }); + + const parents: GridRowModel[] = []; + let children: GridRowModel[] = []; + + // add all the parents of flatFilteredRows if not already added + flatFilteredRows.forEach((row) => { + // get all my parents + const currentPath = row.path.slice(0, -1); + while (currentPath.join('.') !== '') { + const foundRow = rows.find((r) => r.path.join('.') === currentPath.join('.')); + if (foundRow && !addedRowsIds.has(foundRow.id)) { + parents.push(foundRow); + addedRowsIds.add(foundRow.id); + } + currentPath.pop(); + } + // get all my children + children = children.concat( + rows.filter( + (r) => r.path.join('.').startsWith(row.path.join('.')) && !addedRowsIds.has(r.id), + ), + ); + }); + + return [...flatFilteredRows, ...parents, ...children]; +}; diff --git a/packages/grid/x-data-grid-generator/src/hooks/useDemoData.ts b/packages/grid/x-data-grid-generator/src/hooks/useDemoData.ts index e5010c8365e08..bfb4e9101ea96 100644 --- a/packages/grid/x-data-grid-generator/src/hooks/useDemoData.ts +++ b/packages/grid/x-data-grid-generator/src/hooks/useDemoData.ts @@ -1,6 +1,6 @@ import * as React from 'react'; import LRUCache from 'lru-cache'; -import { GridColumnVisibilityModel, GridRowModel } from '@mui/x-data-grid-premium'; +import { GridColumnVisibilityModel, GridRowModel, GridFilterModel } from '@mui/x-data-grid-premium'; import { GridDemoData, getRealGridData } from '../services/real-data-service'; import { getCommodityColumns } from '../columns/commodities.columns'; import { getEmployeeColumns } from '../columns/employees.columns'; @@ -11,6 +11,7 @@ import { DemoTreeDataValue, addTreeDataOptionsToDemoData, } from '../services/tree-data-generator'; +import { filterServerSide } from './filterUtils'; const dataCache = new LRUCache({ max: 10, @@ -27,15 +28,23 @@ const DEFAULT_SERVER_OPTIONS = { maxDelay: 1000, }; +interface LazyLoadTreeRowsRequest { + path: string[]; + // TODO: Support server side filtering and sorting + filterModel?: GridFilterModel; + sortModel?: any; +} +interface LazyLoadTreeRowsParams { + request: LazyLoadTreeRowsRequest; + serverOptions?: ServerOptions; +} + export type DemoDataReturnType = { data: DemoTreeDataValue; loading: boolean; setRowLength: (count: number) => void; loadNewData: () => void; - lazyLoadTreeRows: (params: { - path: string[]; - serverOptions?: ServerOptions; - }) => Promise; + lazyLoadTreeRows: (params: LazyLoadTreeRowsParams) => Promise; }; type DataSet = 'Commodity' | 'Employee'; @@ -243,15 +252,9 @@ export const useDemoData = (options: UseDemoDataOptions): DemoDataReturnType => const lazyLoadTreeRows = React.useCallback( ({ - path, + request: { path, filterModel }, serverOptions = DEFAULT_SERVER_OPTIONS, - }: { - path: string[]; - serverOptions?: ServerOptions; - // TODO: Support server side filtering and sorting - filterModel?: any; - sortModel?: any; - }) => { + }: LazyLoadTreeRowsParams) => { return new Promise((resolve, reject) => { setTimeout(() => { if (!options.treeData) { @@ -268,10 +271,16 @@ export const useDemoData = (options: UseDemoDataOptions): DemoDataReturnType => ), ); } - const childRows = findTreeDataRowChildren(data.rows, path, data.getTreeDataPath!); + + const filteredRows = + (filterModel?.items?.length ?? 0) > 0 + ? filterServerSide(columns, data.rows, filterModel!) + : data.rows; + const childRows = findTreeDataRowChildren(filteredRows, path, data.getTreeDataPath!); + const childRowsWithDescendantCounts = childRows.map((row) => { const descendants = findTreeDataRowChildren( - data.rows, + filteredRows, data.getTreeDataPath!(row), data.getTreeDataPath!, -1, @@ -283,7 +292,7 @@ export const useDemoData = (options: UseDemoDataOptions): DemoDataReturnType => }, Math.random() * (serverOptions.maxDelay! - serverOptions.minDelay!) + serverOptions.minDelay!); }); }, - [data, options.treeData], + [columns, data.getTreeDataPath, data.rows, options.treeData], ); return { diff --git a/packages/grid/x-data-grid-pro/src/components/GridTreeDataGroupingCell.tsx b/packages/grid/x-data-grid-pro/src/components/GridTreeDataGroupingCell.tsx index 8a683c48cadba..4b2b1c1e1dd4c 100644 --- a/packages/grid/x-data-grid-pro/src/components/GridTreeDataGroupingCell.tsx +++ b/packages/grid/x-data-grid-pro/src/components/GridTreeDataGroupingCell.tsx @@ -9,6 +9,7 @@ import { GridRenderCellParams, GridGroupNode, GridServerSideGroupNode, + gridFilterModelSelector, } from '@mui/x-data-grid'; import CircularProgress from '@mui/material/CircularProgress'; import { useGridRootProps } from '../hooks/utils/useGridRootProps'; @@ -48,6 +49,7 @@ function GridTreeDataGroupingCellIcon(props: GridTreeDataGroupingCellIconProps) const { rowNode, row, id, field, descendantCount } = props; const apiRef = useGridPrivateApiContext(); const rootProps = useGridRootProps(); + const filterModel = gridFilterModelSelector(apiRef); const isServerSideNode = (rowNode as GridServerSideGroupNode).isServerSide; const isDataLoading = (rowNode as GridServerSideGroupNode).isLoading; @@ -57,7 +59,7 @@ function GridTreeDataGroupingCellIcon(props: GridTreeDataGroupingCellIconProps) if (isServerSideNode && !rowNode.childrenExpanded && !areChildrenFetched) { const helpers = getLazyLoadingHelpers(apiRef, rowNode as GridServerSideGroupNode); apiRef.current.setRowLoadingStatus(rowNode.id, true); - apiRef.current.publishEvent('fetchRowChildren', { row, helpers }); + apiRef.current.publishEvent('fetchRowChildren', { row, helpers, filterModel }); } else { apiRef.current.setRowChildrenExpansion(id, !rowNode.childrenExpanded); } diff --git a/packages/grid/x-data-grid-pro/src/hooks/features/treeData/useGridTreeDataLazyLoading.tsx b/packages/grid/x-data-grid-pro/src/hooks/features/treeData/useGridTreeDataLazyLoading.tsx index a25560091100e..a6b9470501b34 100644 --- a/packages/grid/x-data-grid-pro/src/hooks/features/treeData/useGridTreeDataLazyLoading.tsx +++ b/packages/grid/x-data-grid-pro/src/hooks/features/treeData/useGridTreeDataLazyLoading.tsx @@ -5,6 +5,9 @@ import { useGridApiMethod, GRID_ROOT_GROUP_ID, useGridApiOptionHandler, + useGridApiEventHandler, + GridFilterModel, + GridEventListener, } from '@mui/x-data-grid'; import { GridTreeDataLazyLoadingApi } from './gridTreeDataLazyLoadingApi'; import { GridPrivateApiPro } from '../../../models/gridApiPro'; @@ -16,34 +19,40 @@ interface GridTreeDataLazyLoadHelpers { } export interface GridFetchRowChildrenParams { - row: GridRowModel | undefined; + row?: GridRowModel | undefined; helpers: GridTreeDataLazyLoadHelpers; + filterModel?: GridFilterModel; } export const getLazyLoadingHelpers = ( apiRef: React.MutableRefObject, rowNode: GridServerSideGroupNode, + updateType: 'partial' | 'full' = 'partial', ) => ({ success: (rows: GridRowModel[]) => { - apiRef.current.updateRows(rows); - const previousNode = apiRef.current.getRowNode(rowNode.id) as GridServerSideGroupNode; - const id = rowNode!.id; + if (updateType === 'full') { + apiRef.current.setRows(rows); + } else { + apiRef.current.updateRows(rows); + const previousNode = apiRef.current.getRowNode(rowNode.id) as GridServerSideGroupNode; + const id = rowNode!.id; - const newNode: GridServerSideGroupNode = { - ...previousNode, - isLoading: false, - childrenFetched: true, - }; - apiRef.current.setState((state) => { - return { - ...state, - rows: { - ...state.rows, - tree: { ...state.rows.tree, [id]: newNode }, - }, + const newNode: GridServerSideGroupNode = { + ...previousNode, + isLoading: false, + childrenFetched: true, }; - }); - apiRef.current.setRowChildrenExpansion(rowNode!.id, true); + apiRef.current.setState((state) => { + return { + ...state, + rows: { + ...state.rows, + tree: { ...state.rows.tree, [id]: newNode }, + }, + }; + }); + apiRef.current.setRowChildrenExpansion(rowNode!.id, true); + } }, error: () => { apiRef.current.setRowLoadingStatus(rowNode!.id, false); @@ -52,7 +61,10 @@ export const getLazyLoadingHelpers = ( export const useGridTreeDataLazyLoading = ( apiRef: React.MutableRefObject, - props: Pick, + props: Pick< + DataGridProProcessedProps, + 'treeData' | 'rowsLoadingMode' | 'onFetchRowChildren' | 'filterMode' + >, ) => { const setRowLoadingStatus = React.useCallback( (id, isLoading) => { @@ -76,23 +88,39 @@ export const useGridTreeDataLazyLoading = ( [apiRef], ); + const onFilterModelChange = React.useCallback>( + (filterModel: GridFilterModel) => { + if (props.treeData && props.rowsLoadingMode === 'server' && props.filterMode === 'server') { + const helpers = getLazyLoadingHelpers( + apiRef, + apiRef.current.getRowNode(GRID_ROOT_GROUP_ID) as GridServerSideGroupNode, + 'full', + ); + apiRef.current.publishEvent('fetchRowChildren', { filterModel, helpers }); + } + }, + [apiRef, props.filterMode, props.rowsLoadingMode, props.treeData], + ); + const treeDataLazyLoadingApi: GridTreeDataLazyLoadingApi = { setRowLoadingStatus, }; useGridApiMethod(apiRef, treeDataLazyLoadingApi, 'public'); useGridApiOptionHandler(apiRef, 'fetchRowChildren', props.onFetchRowChildren); + useGridApiEventHandler(apiRef, 'filterModelChange', onFilterModelChange); /** * EFFECTS */ React.useEffect(() => { if (props.treeData && props.rowsLoadingMode === 'server') { + const filterModel = apiRef.current.state.filter?.filterModel; const helpers = getLazyLoadingHelpers( apiRef, apiRef.current.getRowNode(GRID_ROOT_GROUP_ID) as GridServerSideGroupNode, ); - apiRef.current.publishEvent('fetchRowChildren', { row: undefined, helpers }); + apiRef.current.publishEvent('fetchRowChildren', { helpers, filterModel }); } }, [apiRef, props.treeData, props.rowsLoadingMode]); }; diff --git a/scripts/x-data-grid-premium.exports.json b/scripts/x-data-grid-premium.exports.json index 7aacdca50f386..ef14d1fe9a762 100644 --- a/scripts/x-data-grid-premium.exports.json +++ b/scripts/x-data-grid-premium.exports.json @@ -556,6 +556,7 @@ { "name": "GridTranslationKeys", "kind": "TypeAlias" }, { "name": "GridTreeBasicNode", "kind": "Interface" }, { "name": "GridTreeDataGroupingCell", "kind": "Function" }, + { "name": "GridTreeDataLazyLoadingApi", "kind": "Interface" }, { "name": "GridTreeNode", "kind": "TypeAlias" }, { "name": "GridTreeNodeWithRender", "kind": "TypeAlias" }, { "name": "GridTripleDotsVerticalIcon", "kind": "Variable" }, diff --git a/scripts/x-data-grid-pro.exports.json b/scripts/x-data-grid-pro.exports.json index 253df90e53928..569fd35038278 100644 --- a/scripts/x-data-grid-pro.exports.json +++ b/scripts/x-data-grid-pro.exports.json @@ -511,6 +511,7 @@ { "name": "GridTranslationKeys", "kind": "TypeAlias" }, { "name": "GridTreeBasicNode", "kind": "Interface" }, { "name": "GridTreeDataGroupingCell", "kind": "Function" }, + { "name": "GridTreeDataLazyLoadingApi", "kind": "Interface" }, { "name": "GridTreeNode", "kind": "TypeAlias" }, { "name": "GridTreeNodeWithRender", "kind": "TypeAlias" }, { "name": "GridTripleDotsVerticalIcon", "kind": "Variable" }, From 0e1ff6f60f9fa57b616083930a13f1d39bbc8324 Mon Sep 17 00:00:00 2001 From: Bilal Shafi Date: Sun, 11 Jun 2023 23:35:59 +0500 Subject: [PATCH 6/9] POC server side sorting --- .../tree-data/TreeDataLazyLoading.js | 62 +++--- .../tree-data/TreeDataLazyLoading.tsx | 70 ++++--- .../src/hooks/fakeServerUtils.ts | 193 ++++++++++++++++++ .../src/hooks/useDemoData.ts | 92 +-------- .../src/hooks/useQuery.ts | 130 +++++------- .../components/GridTreeDataGroupingCell.tsx | 4 +- .../treeData/useGridTreeDataLazyLoading.tsx | 33 ++- scripts/x-data-grid-generator.exports.json | 2 + 8 files changed, 358 insertions(+), 228 deletions(-) create mode 100644 packages/grid/x-data-grid-generator/src/hooks/fakeServerUtils.ts diff --git a/docs/data/data-grid/tree-data/TreeDataLazyLoading.js b/docs/data/data-grid/tree-data/TreeDataLazyLoading.js index 93015d49e386e..a58fbee6679ad 100644 --- a/docs/data/data-grid/tree-data/TreeDataLazyLoading.js +++ b/docs/data/data-grid/tree-data/TreeDataLazyLoading.js @@ -1,44 +1,57 @@ import * as React from 'react'; import { DataGridPro, useGridApiRef } from '@mui/x-data-grid-pro'; -import { useDemoData } from '@mui/x-data-grid-generator'; +import { + createFakeServer, + loadTreeDataServerRows, +} from '@mui/x-data-grid-generator'; const initRows = []; +const DATASET_OPTION = { + dataSet: 'Employee', + rowLength: 1000, + treeData: { maxDepth: 2, groupingField: 'name', averageChildren: 20 }, +}; + +const { columnsWithDefaultColDef, useQuery, ...data } = + createFakeServer(DATASET_OPTION); + +const emptyObject = {}; + export default function TreeDataLazyLoading() { const apiRef = useGridApiRef(); - const { loading, data, lazyLoadTreeRows } = useDemoData({ - dataSet: 'Employee', - rowLength: 1000, - treeData: { maxDepth: 2, groupingField: 'name', averageChildren: 10 }, - }); + const { rows: rowsServerSide } = useQuery(emptyObject); const onFetchRowChildren = React.useCallback( - async ({ row, helpers, filterModel }) => { - try { - const path = row ? data.getTreeDataPath(row) : []; - const rows = await lazyLoadTreeRows({ - request: { path, filterModel }, - }); - helpers.success(rows); - } catch (error) { - // simulate network error - helpers.error(); - console.error(error); - } + async ({ row, helpers, filterModel, sortModel }) => { + const serverRows = await loadTreeDataServerRows( + rowsServerSide, + { + filterModel, + sortModel, + path: row?.path ?? [], + }, + { + minDelay: 300, + maxDelay: 800, + }, + columnsWithDefaultColDef, + ); + + helpers.success(serverRows); }, - [data.getTreeDataPath, lazyLoadTreeRows], + [rowsServerSide], ); - if (loading) { - return null; - } - return (
row.path} + treeData unstable_headerFilters onFetchRowChildren={onFetchRowChildren} initialState={{ @@ -46,8 +59,8 @@ export default function TreeDataLazyLoading() { columns: { ...data.initialState?.columns, columnVisibilityModel: { - ...data.initialState?.columns?.columnVisibilityModel, avatar: false, + id: false, }, }, filter: { @@ -60,6 +73,7 @@ export default function TreeDataLazyLoading() { getDescendantCount={(row) => row.descendantCount} rowsLoadingMode="server" filterMode="server" + sortingMode="server" />
); diff --git a/docs/data/data-grid/tree-data/TreeDataLazyLoading.tsx b/docs/data/data-grid/tree-data/TreeDataLazyLoading.tsx index abb9566f8765f..3c212fa92c8a7 100644 --- a/docs/data/data-grid/tree-data/TreeDataLazyLoading.tsx +++ b/docs/data/data-grid/tree-data/TreeDataLazyLoading.tsx @@ -5,45 +5,58 @@ import { GridFetchRowChildrenParams, GridValidRowModel, } from '@mui/x-data-grid-pro'; -import { useDemoData } from '@mui/x-data-grid-generator'; +import { + createFakeServer, + loadTreeDataServerRows, + UseDemoDataOptions, +} from '@mui/x-data-grid-generator'; const initRows: GridValidRowModel[] = []; +const DATASET_OPTION: UseDemoDataOptions = { + dataSet: 'Employee', + rowLength: 1000, + treeData: { maxDepth: 2, groupingField: 'name', averageChildren: 20 }, +}; + +const { columnsWithDefaultColDef, useQuery, ...data } = + createFakeServer(DATASET_OPTION); + +const emptyObject = {}; + export default function TreeDataLazyLoading() { const apiRef = useGridApiRef(); - const { loading, data, lazyLoadTreeRows } = useDemoData({ - dataSet: 'Employee', - rowLength: 1000, - treeData: { maxDepth: 2, groupingField: 'name', averageChildren: 10 }, - }); + const { rows: rowsServerSide } = useQuery(emptyObject); const onFetchRowChildren = React.useCallback( - async ({ row, helpers, filterModel }: GridFetchRowChildrenParams) => { - try { - const path = row ? data.getTreeDataPath!(row) : []; - const rows = (await lazyLoadTreeRows({ - request: { path, filterModel }, - })) as GridValidRowModel[]; - helpers.success(rows); - } catch (error) { - // simulate network error - helpers.error(); - console.error(error); - } + async ({ row, helpers, filterModel, sortModel }: GridFetchRowChildrenParams) => { + const serverRows = await loadTreeDataServerRows( + rowsServerSide, + { + filterModel, + sortModel, + path: row?.path ?? [], + }, + { + minDelay: 300, + maxDelay: 800, + }, + columnsWithDefaultColDef, + ); + helpers.success(serverRows); }, - [data.getTreeDataPath, lazyLoadTreeRows], + [rowsServerSide], ); - if (loading) { - return null; - } - return (
row.path} + treeData unstable_headerFilters onFetchRowChildren={onFetchRowChildren} initialState={{ @@ -51,22 +64,21 @@ export default function TreeDataLazyLoading() { columns: { ...data.initialState?.columns, columnVisibilityModel: { - ...data.initialState?.columns?.columnVisibilityModel, avatar: false, + id: false, }, }, filter: { filterModel: { - items: [ - { field: 'website', operator: 'contains', value: 'ab' }, - ] - } - } + items: [{ field: 'website', operator: 'contains', value: 'ab' }], + }, + }, }} isServerSideRow={(row) => row.hasChildren} getDescendantCount={(row) => row.descendantCount} rowsLoadingMode="server" filterMode="server" + sortingMode="server" />
); diff --git a/packages/grid/x-data-grid-generator/src/hooks/fakeServerUtils.ts b/packages/grid/x-data-grid-generator/src/hooks/fakeServerUtils.ts new file mode 100644 index 0000000000000..0ed10f7e445ed --- /dev/null +++ b/packages/grid/x-data-grid-generator/src/hooks/fakeServerUtils.ts @@ -0,0 +1,193 @@ +import { + GridRowModel, + GridFilterModel, + GridSortModel, + GridLogicOperator, + GridFilterOperator, + GridColDef, +} from '@mui/x-data-grid-pro'; + +export const findTreeDataRowChildren = ( + allRows: GridRowModel[], + parentPath: string[], + pathKey: string = 'path', + depth: number = 1, // the depth of the children to find relative to parentDepth, `-1` to find all +) => { + const parentDepth = parentPath.length; + const children = []; + for (let i = 0; i < allRows.length; i += 1) { + const row = allRows[i]; + const rowPath = row[pathKey]; + if ( + ((depth < 0 && rowPath.length > parentDepth) || rowPath.length === parentDepth + depth) && + parentPath.every((value, index) => value === rowPath[index]) + ) { + children.push(row); + } + } + return children; +}; + +export const simplifiedValueGetter = (field: string, colDef: GridColDef) => (row: GridRowModel) => { + const params = { id: row.id, row, field, rowNode: {} }; + // @ts-ignore + return colDef.valueGetter?.(params) || row[field]; +}; + +export const getRowComparator = ( + sortModel: GridSortModel | undefined, + columnsWithDefaultColDef: GridColDef[], +) => { + if (!sortModel) { + const comparator = () => 0; + return comparator; + } + const sortOperators = sortModel.map((sortItem) => { + const columnField = sortItem.field; + const colDef = columnsWithDefaultColDef.find(({ field }) => field === columnField) as any; + return { + ...sortItem, + valueGetter: simplifiedValueGetter(columnField, colDef), + sortComparator: colDef.sortComparator, + }; + }); + + const comparator = (row1: GridRowModel, row2: GridRowModel) => + sortOperators.reduce((acc, { valueGetter, sort, sortComparator }) => { + if (acc !== 0) { + return acc; + } + const v1 = valueGetter(row1); + const v2 = valueGetter(row2); + return sort === 'desc' ? -1 * sortComparator(v1, v2) : sortComparator(v1, v2); + }, 0); + + return comparator; +}; + +const getFilterFunctions = ( + filterModel: GridFilterModel, + columnsWithDefaultColDef: GridColDef[], +) => { + return filterModel.items.map((filterItem) => { + const { field, operator } = filterItem; + const colDef = columnsWithDefaultColDef.find((column) => column.field === field) as any; + + const filterOperator: any = colDef.filterOperators.find( + ({ value }: GridFilterOperator) => operator === value, + ); + + let parsedValue = filterItem.value; + if (colDef.valueParser) { + const parser = colDef.valueParser; + parsedValue = Array.isArray(filterItem.value) + ? filterItem.value?.map((x) => parser(x)) + : parser(filterItem.value); + } + + return filterOperator?.getApplyFilterFn({ filterItem, value: parsedValue }, colDef); + }); +}; + +export const getFilteredRows = ( + rows: GridRowModel[], + filterModel: GridFilterModel | undefined, + columnsWithDefaultColDef: GridColDef[], +) => { + if (filterModel === undefined || filterModel.items.length === 0) { + return rows; + } + + const valueGetters = filterModel.items.map(({ field }) => + simplifiedValueGetter( + field, + columnsWithDefaultColDef.find((column) => column.field === field) as any, + ), + ); + const filterFunctions = getFilterFunctions(filterModel, columnsWithDefaultColDef); + + if (filterModel.logicOperator === GridLogicOperator.Or) { + return rows.filter((row: GridRowModel) => + filterModel.items.some((_, index) => { + const value = valueGetters[index](row); + return filterFunctions[index] === null ? true : filterFunctions[index]({ value }); + }), + ); + } + return rows.filter((row: GridRowModel) => + filterModel.items.every((_, index) => { + const value = valueGetters[index](row); + return filterFunctions[index] === null ? true : filterFunctions[index]({ value }); + }), + ); +}; + +export const getFilteredRowsServerSide = ( + rows: GridRowModel[], + filterModel: GridFilterModel | undefined, + columnsWithDefaultColDef: GridColDef[], + pathKey: string = 'path', +) => { + if (filterModel === undefined || filterModel.items.length === 0) { + return rows; + } + + const valueGetters = filterModel.items.map(({ field }) => + simplifiedValueGetter( + field, + columnsWithDefaultColDef.find((column) => column.field === field) as any, + ), + ); + const filterFunctions = getFilterFunctions(filterModel, columnsWithDefaultColDef); + + const addedRowsIds = new Set(); + const flatFilteredRows = + filterModel.logicOperator === GridLogicOperator.Or + ? rows.filter((row: GridRowModel) => + filterModel.items.some((_, index) => { + const value = valueGetters[index](row); + const keepRow = + filterFunctions[index] === null ? true : filterFunctions[index]({ value }); + if (keepRow) { + addedRowsIds.add(row.id); + } + return keepRow; + }), + ) + : rows.filter((row: GridRowModel) => + filterModel.items.every((_, index) => { + const value = valueGetters[index](row); + const keepRow = + filterFunctions[index] === null ? true : filterFunctions[index]({ value }); + if (keepRow) { + addedRowsIds.add(row.id); + } + return keepRow; + }), + ); + + const parents: GridRowModel[] = []; + let children: GridRowModel[] = []; + + // add all the parents of flatFilteredRows if not already added + flatFilteredRows.forEach((row) => { + // get all my parents + const currentPath = row[pathKey].slice(0, -1); + while (currentPath.join('.') !== '') { + const foundRow = rows.find((r) => r.path.join('.') === currentPath.join('.')); + if (foundRow && !addedRowsIds.has(foundRow.id)) { + parents.push(foundRow); + addedRowsIds.add(foundRow.id); + } + currentPath.pop(); + } + // get all my children + children = children.concat( + rows.filter( + (r) => r[pathKey].join('.').startsWith(row[pathKey].join('.')) && !addedRowsIds.has(r.id), + ), + ); + }); + + return [...flatFilteredRows, ...parents, ...children]; +}; diff --git a/packages/grid/x-data-grid-generator/src/hooks/useDemoData.ts b/packages/grid/x-data-grid-generator/src/hooks/useDemoData.ts index bfb4e9101ea96..677ff89be441b 100644 --- a/packages/grid/x-data-grid-generator/src/hooks/useDemoData.ts +++ b/packages/grid/x-data-grid-generator/src/hooks/useDemoData.ts @@ -1,6 +1,6 @@ import * as React from 'react'; import LRUCache from 'lru-cache'; -import { GridColumnVisibilityModel, GridRowModel, GridFilterModel } from '@mui/x-data-grid-premium'; +import { GridColumnVisibilityModel } from '@mui/x-data-grid-premium'; import { GridDemoData, getRealGridData } from '../services/real-data-service'; import { getCommodityColumns } from '../columns/commodities.columns'; import { getEmployeeColumns } from '../columns/employees.columns'; @@ -11,40 +11,17 @@ import { DemoTreeDataValue, addTreeDataOptionsToDemoData, } from '../services/tree-data-generator'; -import { filterServerSide } from './filterUtils'; const dataCache = new LRUCache({ max: 10, ttl: 60 * 5 * 1e3, // 5 minutes }); -type ServerOptions = { - minDelay?: number; - maxDelay?: number; -}; - -const DEFAULT_SERVER_OPTIONS = { - minDelay: 300, - maxDelay: 1000, -}; - -interface LazyLoadTreeRowsRequest { - path: string[]; - // TODO: Support server side filtering and sorting - filterModel?: GridFilterModel; - sortModel?: any; -} -interface LazyLoadTreeRowsParams { - request: LazyLoadTreeRowsRequest; - serverOptions?: ServerOptions; -} - export type DemoDataReturnType = { data: DemoTreeDataValue; loading: boolean; setRowLength: (count: number) => void; loadNewData: () => void; - lazyLoadTreeRows: (params: LazyLoadTreeRowsParams) => Promise; }; type DataSet = 'Commodity' | 'Employee'; @@ -145,27 +122,6 @@ export const getInitialState = (options: UseDemoDataOptions, columns: GridColDef return { columns: { columnVisibilityModel } }; }; -const findTreeDataRowChildren = ( - allRows: GridRowModel[], - parentPath: string[], - getTreeDataPath: (row: GridRowModel) => string[], - depth: number = 1, // the depth of the children to find relative to parentDepth, `-1` to find all -) => { - const parentDepth = parentPath.length; - const children = []; - for (let i = 0; i < allRows.length; i += 1) { - const row = allRows[i]; - const rowPath = getTreeDataPath(row); - if ( - ((depth < 0 && rowPath.length > parentDepth) || rowPath.length === parentDepth + depth) && - parentPath.every((value, index) => value === rowPath[index]) - ) { - children.push(row); - } - } - return children; -}; - export const useDemoData = (options: UseDemoDataOptions): DemoDataReturnType => { const [rowLength, setRowLength] = React.useState(options.rowLength); const [index, setIndex] = React.useState(0); @@ -250,51 +206,6 @@ export const useDemoData = (options: UseDemoDataOptions): DemoDataReturnType => columns, ]); - const lazyLoadTreeRows = React.useCallback( - ({ - request: { path, filterModel }, - serverOptions = DEFAULT_SERVER_OPTIONS, - }: LazyLoadTreeRowsParams) => { - return new Promise((resolve, reject) => { - setTimeout(() => { - if (!options.treeData) { - reject(new Error('MUI: Please enable tree data in demo data options.')); - } - - const { maxDepth = 1, groupingField } = options.treeData!; - - const hasTreeData = maxDepth > 1 && groupingField != null; - if (!hasTreeData) { - reject( - new Error( - 'MUI: For tree data, maximum depth should be > 1 and grouping field should be set.', - ), - ); - } - - const filteredRows = - (filterModel?.items?.length ?? 0) > 0 - ? filterServerSide(columns, data.rows, filterModel!) - : data.rows; - const childRows = findTreeDataRowChildren(filteredRows, path, data.getTreeDataPath!); - - const childRowsWithDescendantCounts = childRows.map((row) => { - const descendants = findTreeDataRowChildren( - filteredRows, - data.getTreeDataPath!(row), - data.getTreeDataPath!, - -1, - ); - const descendantCount = descendants.length; - return { ...row, descendantCount, hasChildren: descendantCount > 0 }; - }); - resolve(childRowsWithDescendantCounts); - }, Math.random() * (serverOptions.maxDelay! - serverOptions.minDelay!) + serverOptions.minDelay!); - }); - }, - [columns, data.getTreeDataPath, data.rows, options.treeData], - ); - return { data, loading, @@ -302,6 +213,5 @@ export const useDemoData = (options: UseDemoDataOptions): DemoDataReturnType => loadNewData: () => { setIndex((oldIndex) => oldIndex + 1); }, - lazyLoadTreeRows, }; }; diff --git a/packages/grid/x-data-grid-generator/src/hooks/useQuery.ts b/packages/grid/x-data-grid-generator/src/hooks/useQuery.ts index 530cd0bd66753..b04484f482e45 100644 --- a/packages/grid/x-data-grid-generator/src/hooks/useQuery.ts +++ b/packages/grid/x-data-grid-generator/src/hooks/useQuery.ts @@ -5,8 +5,6 @@ import { GridFilterModel, GridSortModel, GridRowId, - GridLogicOperator, - GridFilterOperator, GridColDef, } from '@mui/x-data-grid-pro'; import { isDeepEqual } from '@mui/x-data-grid/internals'; @@ -16,92 +14,56 @@ import { getColumnsFromOptions, getInitialState, } from './useDemoData'; +import { + findTreeDataRowChildren, + getFilteredRowsServerSide, + getRowComparator, + getFilteredRows, +} from './fakeServerUtils'; -const simplifiedValueGetter = (field: string, colDef: GridColDef) => (row: GridRowModel) => { - const params = { id: row.id, row, field, rowNode: {} }; - // @ts-ignore - return colDef.valueGetter?.(params) || row[field]; -}; - -const getRowComparator = ( - sortModel: GridSortModel | undefined, - columnsWithDefaultColDef: GridColDef[], -) => { - if (!sortModel) { - const comparator = () => 0; - return comparator; - } - const sortOperators = sortModel.map((sortItem) => { - const columnField = sortItem.field; - const colDef = columnsWithDefaultColDef.find(({ field }) => field === columnField) as any; - return { - ...sortItem, - valueGetter: simplifiedValueGetter(columnField, colDef), - sortComparator: colDef.sortComparator, - }; - }); - - const comparator = (row1: GridRowModel, row2: GridRowModel) => - sortOperators.reduce((acc, { valueGetter, sort, sortComparator }) => { - if (acc !== 0) { - return acc; - } - const v1 = valueGetter(row1); - const v2 = valueGetter(row2); - return sort === 'desc' ? -1 * sortComparator(v1, v2) : sortComparator(v1, v2); - }, 0); - - return comparator; -}; - -const getFilteredRows = ( +/** + * Simulates server data loading + */ +export const loadTreeDataServerRows = ( rows: GridRowModel[], - filterModel: GridFilterModel | undefined, + queryOptions: TreeDataQueryOptions, + serverOptions: ServerOptions, columnsWithDefaultColDef: GridColDef[], -) => { - if (filterModel === undefined || filterModel.items.length === 0) { - return rows; + pathKey: string = 'path', +): Promise => { + const { minDelay = 500, maxDelay = 800 } = serverOptions; + + if (maxDelay < minDelay) { + throw new Error('serverOptions.minDelay is larger than serverOptions.maxDelay '); } + const delay = Math.random() * (maxDelay - minDelay) + minDelay; + + const { path } = queryOptions; - const valueGetters = filterModel.items.map(({ field }) => - simplifiedValueGetter( - field, - columnsWithDefaultColDef.find((column) => column.field === field) as any, - ), + // apply filtering + const filteredRows = getFilteredRowsServerSide( + rows, + queryOptions.filterModel, + columnsWithDefaultColDef, ); - const filterFunctions = filterModel.items.map((filterItem) => { - const { field, operator } = filterItem; - const colDef = columnsWithDefaultColDef.find((column) => column.field === field) as any; - - const filterOperator: any = colDef.filterOperators.find( - ({ value }: GridFilterOperator) => operator === value, - ); - - let parsedValue = filterItem.value; - if (colDef.valueParser) { - const parser = colDef.valueParser; - parsedValue = Array.isArray(filterItem.value) - ? filterItem.value?.map((x) => parser(x)) - : parser(filterItem.value); - } - - return filterOperator?.getApplyFilterFn({ filterItem, value: parsedValue }, colDef); + + // find direct children referring to the `parentPath` + const childRows = findTreeDataRowChildren(filteredRows, path); + let childRowsWithDescendantCounts = childRows.map((row) => { + const descendants = findTreeDataRowChildren(filteredRows, row[pathKey], pathKey, -1); + const descendantCount = descendants.length; + return { ...row, descendantCount, hasChildren: descendantCount > 0 }; }); - if (filterModel.logicOperator === GridLogicOperator.Or) { - return rows.filter((row: GridRowModel) => - filterModel.items.some((_, index) => { - const value = valueGetters[index](row); - return filterFunctions[index] === null ? true : filterFunctions[index]({ value }); - }), - ); - } - return rows.filter((row: GridRowModel) => - filterModel.items.every((_, index) => { - const value = valueGetters[index](row); - return filterFunctions[index] === null ? true : filterFunctions[index]({ value }); - }), - ); + // apply sorting + const rowComparator = getRowComparator(queryOptions.sortModel, columnsWithDefaultColDef); + childRowsWithDescendantCounts = [...childRowsWithDescendantCounts].sort(rowComparator); + + return new Promise((resolve) => { + setTimeout(() => { + resolve(childRowsWithDescendantCounts); + }, delay); // simulate network latency + }); }; /** @@ -164,6 +126,8 @@ interface FakeServerResponse { totalRowCount: number; } +type FakeServerTreeDataResponse = GridRowModel[]; + interface PageInfo { totalRowCount?: number; nextCursor?: string; @@ -189,6 +153,12 @@ export interface QueryOptions { lastRowToRender?: number; } +export interface TreeDataQueryOptions { + path: string[]; + filterModel?: GridFilterModel; + sortModel?: GridSortModel; +} + const DEFAULT_DATASET_OPTIONS: UseDemoDataOptions = { dataSet: 'Commodity', rowLength: 100, diff --git a/packages/grid/x-data-grid-pro/src/components/GridTreeDataGroupingCell.tsx b/packages/grid/x-data-grid-pro/src/components/GridTreeDataGroupingCell.tsx index 4b2b1c1e1dd4c..d8bda91b1c830 100644 --- a/packages/grid/x-data-grid-pro/src/components/GridTreeDataGroupingCell.tsx +++ b/packages/grid/x-data-grid-pro/src/components/GridTreeDataGroupingCell.tsx @@ -10,6 +10,7 @@ import { GridGroupNode, GridServerSideGroupNode, gridFilterModelSelector, + gridSortModelSelector, } from '@mui/x-data-grid'; import CircularProgress from '@mui/material/CircularProgress'; import { useGridRootProps } from '../hooks/utils/useGridRootProps'; @@ -50,6 +51,7 @@ function GridTreeDataGroupingCellIcon(props: GridTreeDataGroupingCellIconProps) const apiRef = useGridPrivateApiContext(); const rootProps = useGridRootProps(); const filterModel = gridFilterModelSelector(apiRef); + const sortModel = gridSortModelSelector(apiRef); const isServerSideNode = (rowNode as GridServerSideGroupNode).isServerSide; const isDataLoading = (rowNode as GridServerSideGroupNode).isLoading; @@ -59,7 +61,7 @@ function GridTreeDataGroupingCellIcon(props: GridTreeDataGroupingCellIconProps) if (isServerSideNode && !rowNode.childrenExpanded && !areChildrenFetched) { const helpers = getLazyLoadingHelpers(apiRef, rowNode as GridServerSideGroupNode); apiRef.current.setRowLoadingStatus(rowNode.id, true); - apiRef.current.publishEvent('fetchRowChildren', { row, helpers, filterModel }); + apiRef.current.publishEvent('fetchRowChildren', { row, helpers, filterModel, sortModel }); } else { apiRef.current.setRowChildrenExpansion(id, !rowNode.childrenExpanded); } diff --git a/packages/grid/x-data-grid-pro/src/hooks/features/treeData/useGridTreeDataLazyLoading.tsx b/packages/grid/x-data-grid-pro/src/hooks/features/treeData/useGridTreeDataLazyLoading.tsx index a6b9470501b34..c13d99f51e000 100644 --- a/packages/grid/x-data-grid-pro/src/hooks/features/treeData/useGridTreeDataLazyLoading.tsx +++ b/packages/grid/x-data-grid-pro/src/hooks/features/treeData/useGridTreeDataLazyLoading.tsx @@ -8,6 +8,7 @@ import { useGridApiEventHandler, GridFilterModel, GridEventListener, + GridSortModel, } from '@mui/x-data-grid'; import { GridTreeDataLazyLoadingApi } from './gridTreeDataLazyLoadingApi'; import { GridPrivateApiPro } from '../../../models/gridApiPro'; @@ -22,6 +23,7 @@ export interface GridFetchRowChildrenParams { row?: GridRowModel | undefined; helpers: GridTreeDataLazyLoadHelpers; filterModel?: GridFilterModel; + sortModel?: GridSortModel; } export const getLazyLoadingHelpers = ( @@ -63,7 +65,7 @@ export const useGridTreeDataLazyLoading = ( apiRef: React.MutableRefObject, props: Pick< DataGridProProcessedProps, - 'treeData' | 'rowsLoadingMode' | 'onFetchRowChildren' | 'filterMode' + 'treeData' | 'rowsLoadingMode' | 'onFetchRowChildren' | 'filterMode' | 'sortingMode' >, ) => { const setRowLoadingStatus = React.useCallback( @@ -96,10 +98,34 @@ export const useGridTreeDataLazyLoading = ( apiRef.current.getRowNode(GRID_ROOT_GROUP_ID) as GridServerSideGroupNode, 'full', ); + if (props.sortingMode === 'server') { + const sortModel = apiRef.current.state.sorting.sortModel; + apiRef.current.publishEvent('fetchRowChildren', { filterModel, sortModel, helpers }); + return; + } apiRef.current.publishEvent('fetchRowChildren', { filterModel, helpers }); } }, - [apiRef, props.filterMode, props.rowsLoadingMode, props.treeData], + [apiRef, props.filterMode, props.rowsLoadingMode, props.sortingMode, props.treeData], + ); + + const onSortModelChange = React.useCallback>( + (sortModel: GridSortModel) => { + if (props.treeData && props.rowsLoadingMode === 'server' && props.sortingMode === 'server') { + const helpers = getLazyLoadingHelpers( + apiRef, + apiRef.current.getRowNode(GRID_ROOT_GROUP_ID) as GridServerSideGroupNode, + 'full', // refetch root nodes + ); + if (props.filterMode === 'server') { + const filterModel = apiRef.current.state.filter?.filterModel; + apiRef.current.publishEvent('fetchRowChildren', { filterModel, sortModel, helpers }); + return; + } + apiRef.current.publishEvent('fetchRowChildren', { sortModel, helpers }); + } + }, + [apiRef, props.filterMode, props.rowsLoadingMode, props.sortingMode, props.treeData], ); const treeDataLazyLoadingApi: GridTreeDataLazyLoadingApi = { @@ -109,6 +135,7 @@ export const useGridTreeDataLazyLoading = ( useGridApiMethod(apiRef, treeDataLazyLoadingApi, 'public'); useGridApiOptionHandler(apiRef, 'fetchRowChildren', props.onFetchRowChildren); useGridApiEventHandler(apiRef, 'filterModelChange', onFilterModelChange); + useGridApiEventHandler(apiRef, 'sortModelChange', onSortModelChange); /** * EFFECTS @@ -122,5 +149,5 @@ export const useGridTreeDataLazyLoading = ( ); apiRef.current.publishEvent('fetchRowChildren', { helpers, filterModel }); } - }, [apiRef, props.treeData, props.rowsLoadingMode]); + }, [apiRef, props.treeData, props.rowsLoadingMode, props.onFetchRowChildren]); }; diff --git a/scripts/x-data-grid-generator.exports.json b/scripts/x-data-grid-generator.exports.json index e69f7b247f042..64be4a4cf41f0 100644 --- a/scripts/x-data-grid-generator.exports.json +++ b/scripts/x-data-grid-generator.exports.json @@ -17,6 +17,7 @@ { "name": "GridDataGeneratorContext", "kind": "Interface" }, { "name": "GridDemoData", "kind": "Interface" }, { "name": "loadServerRows", "kind": "Variable" }, + { "name": "loadTreeDataServerRows", "kind": "Variable" }, { "name": "Movie", "kind": "TypeAlias" }, { "name": "QueryOptions", "kind": "Interface" }, { "name": "random", "kind": "Variable" }, @@ -74,6 +75,7 @@ { "name": "renderRating", "kind": "Function" }, { "name": "renderStatus", "kind": "Function" }, { "name": "renderTotalPrice", "kind": "Function" }, + { "name": "TreeDataQueryOptions", "kind": "Interface" }, { "name": "useBasicDemoData", "kind": "Variable" }, { "name": "useDemoData", "kind": "Variable" }, { "name": "UseDemoDataOptions", "kind": "Interface" }, From 02573eebfd503a4c1a3f6ff5e093d23c3e4f0fab Mon Sep 17 00:00:00 2001 From: Bilal Shafi Date: Tue, 13 Jun 2023 18:12:35 +0500 Subject: [PATCH 7/9] Introduce dataSource --- .../tree-data/TreeDataLazyLoading.js | 38 ++--- .../tree-data/TreeDataLazyLoading.tsx | 40 ++--- docs/data/data-grid/tree-data/tree-data.md | 40 ++--- .../components/GridTreeDataGroupingCell.tsx | 8 +- .../treeData/gridTreeDataLazyLoadingApi.ts | 1 + .../treeData/useGridTreeDataLazyLoading.tsx | 151 +++++++++++++----- ...seGridTreeDataLazyLoadingPreProcessors.tsx | 10 +- .../treeData/useGridTreeDataPreProcessors.tsx | 6 +- .../src/models/dataGridProProps.ts | 6 + .../src/models/gridDataSource.ts | 11 ++ .../grid/x-data-grid-pro/src/models/index.ts | 1 + 11 files changed, 192 insertions(+), 120 deletions(-) create mode 100644 packages/grid/x-data-grid-pro/src/models/gridDataSource.ts diff --git a/docs/data/data-grid/tree-data/TreeDataLazyLoading.js b/docs/data/data-grid/tree-data/TreeDataLazyLoading.js index a58fbee6679ad..82f004badeb52 100644 --- a/docs/data/data-grid/tree-data/TreeDataLazyLoading.js +++ b/docs/data/data-grid/tree-data/TreeDataLazyLoading.js @@ -22,24 +22,26 @@ export default function TreeDataLazyLoading() { const apiRef = useGridApiRef(); const { rows: rowsServerSide } = useQuery(emptyObject); - const onFetchRowChildren = React.useCallback( - async ({ row, helpers, filterModel, sortModel }) => { - const serverRows = await loadTreeDataServerRows( - rowsServerSide, - { - filterModel, - sortModel, - path: row?.path ?? [], - }, - { - minDelay: 300, - maxDelay: 800, - }, - columnsWithDefaultColDef, - ); + const dataSource = React.useMemo( + () => ({ + getRows: async ({ filterModel, sortModel, groupKeys }) => { + const serverRows = await loadTreeDataServerRows( + rowsServerSide, + { + filterModel, + sortModel, + path: groupKeys ?? [], + }, + { + minDelay: 300, + maxDelay: 800, + }, + columnsWithDefaultColDef, + ); - helpers.success(serverRows); - }, + return serverRows; + }, + }), [rowsServerSide], ); @@ -53,7 +55,7 @@ export default function TreeDataLazyLoading() { getTreeDataPath={(row) => row.path} treeData unstable_headerFilters - onFetchRowChildren={onFetchRowChildren} + unstable_dataSource={dataSource} initialState={{ ...data.initialState, columns: { diff --git a/docs/data/data-grid/tree-data/TreeDataLazyLoading.tsx b/docs/data/data-grid/tree-data/TreeDataLazyLoading.tsx index 3c212fa92c8a7..2bd062128a1c8 100644 --- a/docs/data/data-grid/tree-data/TreeDataLazyLoading.tsx +++ b/docs/data/data-grid/tree-data/TreeDataLazyLoading.tsx @@ -2,8 +2,8 @@ import * as React from 'react'; import { DataGridPro, useGridApiRef, - GridFetchRowChildrenParams, GridValidRowModel, + GetRowsParams, } from '@mui/x-data-grid-pro'; import { createFakeServer, @@ -28,23 +28,25 @@ export default function TreeDataLazyLoading() { const apiRef = useGridApiRef(); const { rows: rowsServerSide } = useQuery(emptyObject); - const onFetchRowChildren = React.useCallback( - async ({ row, helpers, filterModel, sortModel }: GridFetchRowChildrenParams) => { - const serverRows = await loadTreeDataServerRows( - rowsServerSide, - { - filterModel, - sortModel, - path: row?.path ?? [], - }, - { - minDelay: 300, - maxDelay: 800, - }, - columnsWithDefaultColDef, - ); - helpers.success(serverRows); - }, + const dataSource = React.useMemo( + () => ({ + getRows: async ({ filterModel, sortModel, groupKeys }: GetRowsParams) => { + const serverRows = await loadTreeDataServerRows( + rowsServerSide, + { + filterModel, + sortModel, + path: groupKeys ?? [], + }, + { + minDelay: 300, + maxDelay: 800, + }, + columnsWithDefaultColDef, + ); + return serverRows; + }, + }), [rowsServerSide], ); @@ -58,7 +60,7 @@ export default function TreeDataLazyLoading() { getTreeDataPath={(row) => row.path} treeData unstable_headerFilters - onFetchRowChildren={onFetchRowChildren} + unstable_dataSource={dataSource} initialState={{ ...data.initialState, columns: { diff --git a/docs/data/data-grid/tree-data/tree-data.md b/docs/data/data-grid/tree-data/tree-data.md index 5ae72042a8a84..a15053924ca55 100644 --- a/docs/data/data-grid/tree-data/tree-data.md +++ b/docs/data/data-grid/tree-data/tree-data.md @@ -115,45 +115,25 @@ You can limit the sorting to the top-level rows with the `disableChildrenSorting ## Children lazy-loading -To lazy-load tree data children, set the `rowsLoadingMode` prop to `server` and listen to the `fetchRowChildren` event or pass a handler to `onFetchRowChildren` prop. It is fired when user tries to expand a row, it recieves the parent row object and a `helpers` object as parameters which has the following signature. +To lazy-load tree data children, define a data source and pass it as the `unstable_dataSource` prop. ```tsx -interface GridTreeDataLazyLoadHelpers { - success: (rows: GridRowModel[]) => void; - error: () => void; -} - -interface GridFetchRowChildrenParams { - row: GridRowModel | undefined; - helpers: GridTreeDataLazyLoadHelpers; -} -``` - -The `onFetchRowChildren` handler is fired when the data for a specific row is requested, use it to fetch the data and call `helpers.success(newRows)` and `helpers.error()` respectively in case of success or error to let the grid update the related internal states. - -To enable lazy-loading for a given row, you also need to set the `isServerSideRow` prop to a function that returns `true` for the rows that have children and `false` for the rows that don't have children. If you have the information on server, you can provide an optional `getDescendantCount` prop which returns the number of descendants for a parent row. - -```tsx -async function onFetchRowChildren({ row, helpers }: GridFetchRowChildrenParams) { - try { - const childRows = await fetchRows(row); - helpers.success(childRows); - } catch (error) { - helpers.error(); - } +const dataSource = { + getRows: async ({ filterModel, sortModel, groupKeys }: GetRowsParams) => { + const rows = await fetchRows({ filterModel, sortModel, groupKeys }); + return rows; + }, } row.hasChildren} - getDescendantCount={(row) => row.descendantCount} - rowsLoadingMode="server" -/>; + unstable_dataSource={unstable_dataSource} +/> ``` +To enable lazy-loading for a given row, you also need to set the `isServerSideRow` prop to a function that returns `true` for the rows that have children and `false` for the rows that don't have children. If you have the information on server, you can provide an optional `getDescendantCount` prop which returns the number of descendants for a parent row. + Following demo implements a simple lazy-loading tree data grid using mock server. {{"demo": "TreeDataLazyLoading.js", "bg": "inline", "defaultCodeOpen": false}} diff --git a/packages/grid/x-data-grid-pro/src/components/GridTreeDataGroupingCell.tsx b/packages/grid/x-data-grid-pro/src/components/GridTreeDataGroupingCell.tsx index d8bda91b1c830..cc3900689553a 100644 --- a/packages/grid/x-data-grid-pro/src/components/GridTreeDataGroupingCell.tsx +++ b/packages/grid/x-data-grid-pro/src/components/GridTreeDataGroupingCell.tsx @@ -47,11 +47,9 @@ interface GridTreeDataGroupingCellIconProps } function GridTreeDataGroupingCellIcon(props: GridTreeDataGroupingCellIconProps) { - const { rowNode, row, id, field, descendantCount } = props; + const { rowNode, id, field, descendantCount } = props; const apiRef = useGridPrivateApiContext(); const rootProps = useGridRootProps(); - const filterModel = gridFilterModelSelector(apiRef); - const sortModel = gridSortModelSelector(apiRef); const isServerSideNode = (rowNode as GridServerSideGroupNode).isServerSide; const isDataLoading = (rowNode as GridServerSideGroupNode).isLoading; @@ -59,9 +57,7 @@ function GridTreeDataGroupingCellIcon(props: GridTreeDataGroupingCellIconProps) const handleClick = (event: React.MouseEvent) => { if (isServerSideNode && !rowNode.childrenExpanded && !areChildrenFetched) { - const helpers = getLazyLoadingHelpers(apiRef, rowNode as GridServerSideGroupNode); - apiRef.current.setRowLoadingStatus(rowNode.id, true); - apiRef.current.publishEvent('fetchRowChildren', { row, helpers, filterModel, sortModel }); + apiRef.current.fetchNodeChildren(id); } else { apiRef.current.setRowChildrenExpansion(id, !rowNode.childrenExpanded); } diff --git a/packages/grid/x-data-grid-pro/src/hooks/features/treeData/gridTreeDataLazyLoadingApi.ts b/packages/grid/x-data-grid-pro/src/hooks/features/treeData/gridTreeDataLazyLoadingApi.ts index c32fc4fde2742..d48ac79e17696 100644 --- a/packages/grid/x-data-grid-pro/src/hooks/features/treeData/gridTreeDataLazyLoadingApi.ts +++ b/packages/grid/x-data-grid-pro/src/hooks/features/treeData/gridTreeDataLazyLoadingApi.ts @@ -10,4 +10,5 @@ export interface GridTreeDataLazyLoadingApi { * @param {boolean} value The boolean value that's needs to be set. */ setRowLoadingStatus: (nodeId: GridTreeNode['id'], value: boolean) => void; + fetchNodeChildren: (nodeId: GridTreeNode['id']) => void; } diff --git a/packages/grid/x-data-grid-pro/src/hooks/features/treeData/useGridTreeDataLazyLoading.tsx b/packages/grid/x-data-grid-pro/src/hooks/features/treeData/useGridTreeDataLazyLoading.tsx index c13d99f51e000..68f9cf5a87664 100644 --- a/packages/grid/x-data-grid-pro/src/hooks/features/treeData/useGridTreeDataLazyLoading.tsx +++ b/packages/grid/x-data-grid-pro/src/hooks/features/treeData/useGridTreeDataLazyLoading.tsx @@ -4,15 +4,17 @@ import { GridRowModel, useGridApiMethod, GRID_ROOT_GROUP_ID, - useGridApiOptionHandler, useGridApiEventHandler, GridFilterModel, GridEventListener, GridSortModel, + GridRowTreeConfig, + GridRowId, } from '@mui/x-data-grid'; import { GridTreeDataLazyLoadingApi } from './gridTreeDataLazyLoadingApi'; import { GridPrivateApiPro } from '../../../models/gridApiPro'; import { DataGridProProcessedProps } from '../../../models/dataGridProProps'; +import { GetRowsParams, GridDataSource } from '../../../models/gridDataSource'; interface GridTreeDataLazyLoadHelpers { success: (rows: GridRowModel[]) => void; @@ -61,13 +63,92 @@ export const getLazyLoadingHelpers = ( }, }); +const getTopLevelRows = async ( + apiRef: React.MutableRefObject, + getRows: GridDataSource['getRows'], + getRowsParams: GetRowsParams, +) => { + const rows = await getRows(getRowsParams); + apiRef.current.setRows(rows); +}; + +type GetGroupKey = (row: GridRowModel) => any; + +const computeGroupKeys = ( + apiRef: React.MutableRefObject, + tree: GridRowTreeConfig, + nodeId: GridRowId, + getGroupKey: GetGroupKey, +): string[] => { + const groupKeys: string[] = []; + const currentNode = tree[nodeId] as GridServerSideGroupNode; + const traverseParents = (node: GridServerSideGroupNode) => { + const row = apiRef.current.getRow(node.id); + groupKeys.push(getGroupKey(row)); + if (node.parent && node.parent !== GRID_ROOT_GROUP_ID) { + traverseParents(tree[node.parent] as GridServerSideGroupNode); + } + }; + + traverseParents(currentNode); + return groupKeys.reverse(); +}; + +const getGroupKey = (row: GridRowModel) => row.name; + export const useGridTreeDataLazyLoading = ( apiRef: React.MutableRefObject, - props: Pick< - DataGridProProcessedProps, - 'treeData' | 'rowsLoadingMode' | 'onFetchRowChildren' | 'filterMode' | 'sortingMode' - >, + props: Pick, ) => { + const fetchNodeChildren = React.useCallback( + (nodeId: string | number) => { + if (props.unstable_dataSource?.getRows == null) { + return; + } + const node = apiRef.current.getRowNode(nodeId) as GridServerSideGroupNode; + apiRef.current.setRowLoadingStatus(nodeId, true); + const groupKeys = computeGroupKeys( + apiRef, + apiRef.current.state.rows.tree, + nodeId, + getGroupKey, + ); + const getRowsParams = { + filterModel: apiRef.current.state.filter.filterModel, + sortModel: apiRef.current.state.sorting.sortModel, + groupKeys, + }; + props + .unstable_dataSource!.getRows(getRowsParams) + .then((rows) => { + // TODO: Handle this (path generation) internally in `createRowTreeForTreeData` + apiRef.current.updateRows( + rows.map((row: GridRowModel) => ({ ...row, path: [...groupKeys, getGroupKey(row)] })), + ); + const newNode: GridServerSideGroupNode = { + ...node, + isLoading: false, + childrenFetched: true, + }; + apiRef.current.setState((state) => { + return { + ...state, + rows: { + ...state.rows, + tree: { ...state.rows.tree, [nodeId]: newNode }, + }, + }; + }); + apiRef.current.setRowChildrenExpansion(nodeId, !node.childrenExpanded); + }) + .catch((error) => { + apiRef.current.setRowLoadingStatus(nodeId, false); + throw error; + }); + }, + [apiRef, props.unstable_dataSource], + ); + const setRowLoadingStatus = React.useCallback( (id, isLoading) => { const currentNode = apiRef.current.getRowNode(id) as GridServerSideGroupNode; @@ -92,48 +173,38 @@ export const useGridTreeDataLazyLoading = ( const onFilterModelChange = React.useCallback>( (filterModel: GridFilterModel) => { - if (props.treeData && props.rowsLoadingMode === 'server' && props.filterMode === 'server') { - const helpers = getLazyLoadingHelpers( - apiRef, - apiRef.current.getRowNode(GRID_ROOT_GROUP_ID) as GridServerSideGroupNode, - 'full', - ); - if (props.sortingMode === 'server') { - const sortModel = apiRef.current.state.sorting.sortModel; - apiRef.current.publishEvent('fetchRowChildren', { filterModel, sortModel, helpers }); - return; - } - apiRef.current.publishEvent('fetchRowChildren', { filterModel, helpers }); + if (props.treeData && props.unstable_dataSource) { + const getRowsParams = { + filterModel, + sortModel: apiRef.current.state.sorting.sortModel, + groupKeys: [], // fetch root nodes + }; + getTopLevelRows(apiRef, props.unstable_dataSource.getRows, getRowsParams); } }, - [apiRef, props.filterMode, props.rowsLoadingMode, props.sortingMode, props.treeData], + [apiRef, props.unstable_dataSource, props.treeData], ); const onSortModelChange = React.useCallback>( (sortModel: GridSortModel) => { - if (props.treeData && props.rowsLoadingMode === 'server' && props.sortingMode === 'server') { - const helpers = getLazyLoadingHelpers( - apiRef, - apiRef.current.getRowNode(GRID_ROOT_GROUP_ID) as GridServerSideGroupNode, - 'full', // refetch root nodes - ); - if (props.filterMode === 'server') { - const filterModel = apiRef.current.state.filter?.filterModel; - apiRef.current.publishEvent('fetchRowChildren', { filterModel, sortModel, helpers }); - return; - } - apiRef.current.publishEvent('fetchRowChildren', { sortModel, helpers }); + if (props.treeData && props.unstable_dataSource) { + const getRowsParams = { + filterModel: apiRef.current.state.filter.filterModel, + sortModel, + groupKeys: [], // fetch root nodes + }; + getTopLevelRows(apiRef, props.unstable_dataSource.getRows, getRowsParams); } }, - [apiRef, props.filterMode, props.rowsLoadingMode, props.sortingMode, props.treeData], + [apiRef, props.unstable_dataSource, props.treeData], ); const treeDataLazyLoadingApi: GridTreeDataLazyLoadingApi = { setRowLoadingStatus, + fetchNodeChildren, }; useGridApiMethod(apiRef, treeDataLazyLoadingApi, 'public'); - useGridApiOptionHandler(apiRef, 'fetchRowChildren', props.onFetchRowChildren); useGridApiEventHandler(apiRef, 'filterModelChange', onFilterModelChange); useGridApiEventHandler(apiRef, 'sortModelChange', onSortModelChange); @@ -141,13 +212,15 @@ export const useGridTreeDataLazyLoading = ( * EFFECTS */ React.useEffect(() => { - if (props.treeData && props.rowsLoadingMode === 'server') { + if (props.treeData && props.unstable_dataSource) { const filterModel = apiRef.current.state.filter?.filterModel; - const helpers = getLazyLoadingHelpers( - apiRef, - apiRef.current.getRowNode(GRID_ROOT_GROUP_ID) as GridServerSideGroupNode, - ); - apiRef.current.publishEvent('fetchRowChildren', { helpers, filterModel }); + const sortModel = apiRef.current.state.sorting?.sortModel; + const getRowsParams = { + filterModel, + sortModel, + groupKeys: [], // fetch root nodes + }; + getTopLevelRows(apiRef, props.unstable_dataSource.getRows, getRowsParams); } - }, [apiRef, props.treeData, props.rowsLoadingMode, props.onFetchRowChildren]); + }, [apiRef, props.treeData, props.unstable_dataSource]); }; diff --git a/packages/grid/x-data-grid-pro/src/hooks/features/treeData/useGridTreeDataLazyLoadingPreProcessors.tsx b/packages/grid/x-data-grid-pro/src/hooks/features/treeData/useGridTreeDataLazyLoadingPreProcessors.tsx index 6f294c9d7580f..5f1e5f0aae9f5 100644 --- a/packages/grid/x-data-grid-pro/src/hooks/features/treeData/useGridTreeDataLazyLoadingPreProcessors.tsx +++ b/packages/grid/x-data-grid-pro/src/hooks/features/treeData/useGridTreeDataLazyLoadingPreProcessors.tsx @@ -51,7 +51,7 @@ export const useGridTreeDataLazyLoadingPreProcessors = ( | 'disableChildrenFiltering' | 'defaultGroupingExpansionDepth' | 'isGroupExpandedByDefault' - | 'rowsLoadingMode' + | 'unstable_dataSource' | 'isServerSideRow' >, ) => { @@ -59,9 +59,9 @@ export const useGridTreeDataLazyLoadingPreProcessors = ( privateApiRef.current.setStrategyAvailability( 'rowTree', TREE_DATA_LAZY_LOADING_STRATEGY, - props.treeData && props.rowsLoadingMode === 'server' ? () => true : () => false, + props.treeData && props.unstable_dataSource ? () => true : () => false, ); - }, [privateApiRef, props.treeData, props.rowsLoadingMode]); + }, [privateApiRef, props.treeData, props.unstable_dataSource]); const getGroupingColDef = React.useCallback(() => { const groupingColDefProp = props.groupingColDef; @@ -220,7 +220,7 @@ export const useGridTreeDataLazyLoadingPreProcessors = ( return params; } - if (props.rowsLoadingMode !== 'server' || !props.isServerSideRow || !props.treeData) { + if (!props.unstable_dataSource || !props.isServerSideRow || !props.treeData) { return params; } @@ -233,7 +233,7 @@ export const useGridTreeDataLazyLoadingPreProcessors = ( return params; }, - [props.isServerSideRow, props.treeData, props.rowsLoadingMode], + [props.isServerSideRow, props.treeData, props.unstable_dataSource], ); useGridRegisterPipeProcessor(privateApiRef, 'hydrateColumns', updateGroupingColumn); diff --git a/packages/grid/x-data-grid-pro/src/hooks/features/treeData/useGridTreeDataPreProcessors.tsx b/packages/grid/x-data-grid-pro/src/hooks/features/treeData/useGridTreeDataPreProcessors.tsx index dd1d15df73d4a..27ac6d56b1e77 100644 --- a/packages/grid/x-data-grid-pro/src/hooks/features/treeData/useGridTreeDataPreProcessors.tsx +++ b/packages/grid/x-data-grid-pro/src/hooks/features/treeData/useGridTreeDataPreProcessors.tsx @@ -45,16 +45,16 @@ export const useGridTreeDataPreProcessors = ( | 'disableChildrenFiltering' | 'defaultGroupingExpansionDepth' | 'isGroupExpandedByDefault' - | 'rowsLoadingMode' + | 'unstable_dataSource' >, ) => { const setStrategyAvailability = React.useCallback(() => { privateApiRef.current.setStrategyAvailability( 'rowTree', TREE_DATA_STRATEGY, - props.treeData && props.rowsLoadingMode === 'client' ? () => true : () => false, + props.treeData && props.unstable_dataSource ? () => false : () => true, ); - }, [privateApiRef, props.treeData, props.rowsLoadingMode]); + }, [privateApiRef, props.treeData, props.unstable_dataSource]); const getGroupingColDef = React.useCallback(() => { const groupingColDefProp = props.groupingColDef; diff --git a/packages/grid/x-data-grid-pro/src/models/dataGridProProps.ts b/packages/grid/x-data-grid-pro/src/models/dataGridProProps.ts index 954f69a2fcf4f..f6189b1d2f427 100644 --- a/packages/grid/x-data-grid-pro/src/models/dataGridProProps.ts +++ b/packages/grid/x-data-grid-pro/src/models/dataGridProProps.ts @@ -25,6 +25,7 @@ import { import { GridInitialStatePro } from './gridStatePro'; import { GridProSlotsComponent, UncapitalizedGridProSlotsComponent } from './gridProSlotsComponent'; import type { GridProSlotProps } from './gridProSlotProps'; +import type { GridDataSource } from './gridDataSource'; export interface GridExperimentalProFeatures extends GridExperimentalFeatures { /** @@ -157,6 +158,11 @@ export interface DataGridProPropsWithoutDefaultValue; + /** + * DataSource object to allow server-side data fetching + * @ignore - do not document. + */ + unstable_dataSource?: GridDataSource; /** * The initial state of the DataGridPro. * The data in it will be set in the state on initialization but will not be controlled. diff --git a/packages/grid/x-data-grid-pro/src/models/gridDataSource.ts b/packages/grid/x-data-grid-pro/src/models/gridDataSource.ts new file mode 100644 index 0000000000000..efed97acc032c --- /dev/null +++ b/packages/grid/x-data-grid-pro/src/models/gridDataSource.ts @@ -0,0 +1,11 @@ +import { GridFilterModel, GridSortModel } from '@mui/x-data-grid'; + +export interface GetRowsParams { + groupKeys: string[]; + sortModel: GridSortModel; + filterModel: GridFilterModel; +} + +export interface GridDataSource { + getRows: (params: GetRowsParams) => Promise; +} diff --git a/packages/grid/x-data-grid-pro/src/models/index.ts b/packages/grid/x-data-grid-pro/src/models/index.ts index 58cfd0bf2f8eb..4019258915571 100644 --- a/packages/grid/x-data-grid-pro/src/models/index.ts +++ b/packages/grid/x-data-grid-pro/src/models/index.ts @@ -4,3 +4,4 @@ export * from './gridRowOrderChangeParams'; export * from './gridFetchRowsParams'; export * from './gridProSlotsComponent'; export * from './gridProIconSlotsComponent'; +export * from './gridDataSource'; From 096f89e38bb7f9acec396982da9780f8ffe272fe Mon Sep 17 00:00:00 2001 From: Bilal Shafi Date: Wed, 14 Jun 2023 15:46:20 +0500 Subject: [PATCH 8/9] Minor refactor --- .../treeData/useGridTreeDataLazyLoading.tsx | 51 +++++++++---------- 1 file changed, 25 insertions(+), 26 deletions(-) diff --git a/packages/grid/x-data-grid-pro/src/hooks/features/treeData/useGridTreeDataLazyLoading.tsx b/packages/grid/x-data-grid-pro/src/hooks/features/treeData/useGridTreeDataLazyLoading.tsx index 68f9cf5a87664..55492564bdb36 100644 --- a/packages/grid/x-data-grid-pro/src/hooks/features/treeData/useGridTreeDataLazyLoading.tsx +++ b/packages/grid/x-data-grid-pro/src/hooks/features/treeData/useGridTreeDataLazyLoading.tsx @@ -101,7 +101,7 @@ export const useGridTreeDataLazyLoading = ( props: Pick, ) => { const fetchNodeChildren = React.useCallback( - (nodeId: string | number) => { + async (nodeId: string | number) => { if (props.unstable_dataSource?.getRows == null) { return; } @@ -118,33 +118,32 @@ export const useGridTreeDataLazyLoading = ( sortModel: apiRef.current.state.sorting.sortModel, groupKeys, }; - props - .unstable_dataSource!.getRows(getRowsParams) - .then((rows) => { - // TODO: Handle this (path generation) internally in `createRowTreeForTreeData` - apiRef.current.updateRows( - rows.map((row: GridRowModel) => ({ ...row, path: [...groupKeys, getGroupKey(row)] })), - ); - const newNode: GridServerSideGroupNode = { - ...node, - isLoading: false, - childrenFetched: true, + try { + const rows = await props.unstable_dataSource!.getRows(getRowsParams); + + // TODO: Handle this (path generation) internally in `createRowTreeForTreeData` + apiRef.current.updateRows( + rows.map((row: GridRowModel) => ({ ...row, path: [...groupKeys, getGroupKey(row)] })), + ); + const newNode: GridServerSideGroupNode = { + ...node, + isLoading: false, + childrenFetched: true, + }; + apiRef.current.setState((state) => { + return { + ...state, + rows: { + ...state.rows, + tree: { ...state.rows.tree, [nodeId]: newNode }, + }, }; - apiRef.current.setState((state) => { - return { - ...state, - rows: { - ...state.rows, - tree: { ...state.rows.tree, [nodeId]: newNode }, - }, - }; - }); - apiRef.current.setRowChildrenExpansion(nodeId, !node.childrenExpanded); - }) - .catch((error) => { - apiRef.current.setRowLoadingStatus(nodeId, false); - throw error; }); + apiRef.current.setRowChildrenExpansion(nodeId, true); + } catch (error) { + apiRef.current.setRowLoadingStatus(nodeId, false); + throw error; + } }, [apiRef, props.unstable_dataSource], ); From a1392ad3e563e87b2f7e37f200f6ef864e9f4657 Mon Sep 17 00:00:00 2001 From: Bilal Shafi Date: Wed, 14 Jun 2023 18:57:08 +0500 Subject: [PATCH 9/9] Refactor --- .../features/treeData/gridTreeDataUtils.ts | 40 +++++++++++++++++++ .../treeData/useGridTreeDataLazyLoading.tsx | 8 ++-- ...seGridTreeDataLazyLoadingPreProcessors.tsx | 20 +++------- 3 files changed, 51 insertions(+), 17 deletions(-) diff --git a/packages/grid/x-data-grid-pro/src/hooks/features/treeData/gridTreeDataUtils.ts b/packages/grid/x-data-grid-pro/src/hooks/features/treeData/gridTreeDataUtils.ts index 18e0f0422577f..dd9b29c91316f 100644 --- a/packages/grid/x-data-grid-pro/src/hooks/features/treeData/gridTreeDataUtils.ts +++ b/packages/grid/x-data-grid-pro/src/hooks/features/treeData/gridTreeDataUtils.ts @@ -14,6 +14,7 @@ import { passFilterLogic, } from '@mui/x-data-grid/internals'; import { DataGridProProcessedProps } from '../../../models/dataGridProProps'; +import { GridPrivateApiPro } from '../../../models/gridApiPro'; interface FilterRowTreeFromTreeDataParams { rowTree: GridRowTreeConfig; @@ -119,6 +120,44 @@ export const filterRowTreeFromTreeData = ( }; }; +/** + * For the server-side filter, we need to generate the filteredRowsLookup with all the rows + * returned by the server. + */ +export const getFilteredRowsLookup = ( + apiRef: React.MutableRefObject, + rowTree: GridRowTreeConfig, + getDescendentCount: DataGridProProcessedProps['getDescendantCount'], +): Omit => { + const filteredRowsLookup: Record = {}; + const filteredDescendantCountLookup: Record = {}; + + const filterTreeNode = (node: GridTreeNode) => { + if (node.type === 'group') { + node.children.forEach((childId) => { + const childNode = rowTree[childId]; + filterTreeNode(childNode); + }); + } + filteredRowsLookup[node.id] = true; + const row = apiRef.current.getRow(node.id); + filteredDescendantCountLookup[node.id] = getDescendentCount?.(row) ?? 0; + }; + + const nodes = Object.values(rowTree); + for (let i = 0; i < nodes.length; i += 1) { + const node = nodes[i]; + if (node.depth === 0) { + filterTreeNode(node); + } + } + + return { + filteredRowsLookup, + filteredDescendantCountLookup, + }; +}; + export const iterateTreeNodes = ( dataRowIdToModelLookup: GridRowIdToModelLookup, tree: GridRowTreeConfig, @@ -143,6 +182,7 @@ export const iterateTreeNodes = ( isAutoGenerated: false, groupingField, groupingKey, + childrenExpanded: false, isServerSide: true, isLoading: false, childrenFetched: false, diff --git a/packages/grid/x-data-grid-pro/src/hooks/features/treeData/useGridTreeDataLazyLoading.tsx b/packages/grid/x-data-grid-pro/src/hooks/features/treeData/useGridTreeDataLazyLoading.tsx index 55492564bdb36..e85ee0a402b1f 100644 --- a/packages/grid/x-data-grid-pro/src/hooks/features/treeData/useGridTreeDataLazyLoading.tsx +++ b/packages/grid/x-data-grid-pro/src/hooks/features/treeData/useGridTreeDataLazyLoading.tsx @@ -121,9 +121,11 @@ export const useGridTreeDataLazyLoading = ( try { const rows = await props.unstable_dataSource!.getRows(getRowsParams); - // TODO: Handle this (path generation) internally in `createRowTreeForTreeData` - apiRef.current.updateRows( - rows.map((row: GridRowModel) => ({ ...row, path: [...groupKeys, getGroupKey(row)] })), + // TODO: Remove this hack. Handle this (node updation) internally in `updateRows` + setTimeout(() => + apiRef.current.updateRows( + rows.map((row: GridRowModel) => ({ ...row, path: [...groupKeys, getGroupKey(row)] })), + ), ); const newNode: GridServerSideGroupNode = { ...node, diff --git a/packages/grid/x-data-grid-pro/src/hooks/features/treeData/useGridTreeDataLazyLoadingPreProcessors.tsx b/packages/grid/x-data-grid-pro/src/hooks/features/treeData/useGridTreeDataLazyLoadingPreProcessors.tsx index 26456cc83d40f..ecbc0ed981273 100644 --- a/packages/grid/x-data-grid-pro/src/hooks/features/treeData/useGridTreeDataLazyLoadingPreProcessors.tsx +++ b/packages/grid/x-data-grid-pro/src/hooks/features/treeData/useGridTreeDataLazyLoadingPreProcessors.tsx @@ -22,7 +22,7 @@ import { } from './gridTreeDataGroupColDef'; import { DataGridProProcessedProps } from '../../../models/dataGridProProps'; import { - filterRowTreeFromTreeData, + getFilteredRowsLookup, iterateTreeNodes, TREE_DATA_LAZY_LOADING_STRATEGY, } from './gridTreeDataUtils'; @@ -54,6 +54,7 @@ export const useGridTreeDataLazyLoadingPreProcessors = ( | 'isGroupExpandedByDefault' | 'unstable_dataSource' | 'isServerSideRow' + | 'getDescendantCount' >, ) => { const setStrategyAvailability = React.useCallback(() => { @@ -183,20 +184,11 @@ export const useGridTreeDataLazyLoadingPreProcessors = ( [props.getTreeDataPath, props.defaultGroupingExpansionDepth, props.isGroupExpandedByDefault], ); - const filterRows = React.useCallback>( - (params) => { - const rowTree = gridRowTreeSelector(privateApiRef); + const filterRows = React.useCallback>(() => { + const rowTree = gridRowTreeSelector(privateApiRef); - return filterRowTreeFromTreeData({ - rowTree, - isRowMatchingFilters: params.isRowMatchingFilters, - disableChildrenFiltering: props.disableChildrenFiltering, - filterModel: params.filterModel, - apiRef: privateApiRef, - }); - }, - [privateApiRef, props.disableChildrenFiltering], - ); + return getFilteredRowsLookup(privateApiRef, rowTree, props.getDescendantCount); + }, [privateApiRef, props.getDescendantCount]); const sortRows = React.useCallback>( (params) => {