@mui/x-data-grid-pro
Version:
The Pro plan edition of the MUI X Data Grid components.
750 lines (731 loc) • 35.2 kB
JavaScript
"use strict";
'use client';
var _interopRequireWildcard = require("@babel/runtime/helpers/interopRequireWildcard").default;
var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault").default;
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.useGridDataSourceNestedLazyLoader = void 0;
var _formatErrorMessage2 = _interopRequireDefault(require("@mui/x-internals/formatErrorMessage"));
var _extends2 = _interopRequireDefault(require("@babel/runtime/helpers/extends"));
var React = _interopRequireWildcard(require("react"));
var _throttle = require("@mui/x-internals/throttle");
var _isDeepEqual = require("@mui/x-internals/isDeepEqual");
var _useEventCallback = _interopRequireDefault(require("@mui/utils/useEventCallback"));
var _debounce = _interopRequireDefault(require("@mui/utils/debounce"));
var _xDataGrid = require("@mui/x-data-grid");
var _internals = require("@mui/x-data-grid/internals");
var _utils = require("../lazyLoader/utils");
var _useGridLazyLoaderPreProcessors = require("../lazyLoader/useGridLazyLoaderPreProcessors");
var _utils2 = require("../../../utils/tree/utils");
const INTERVAL_CACHE_INITIAL_STATE = {
firstRowToRender: 0,
lastRowToRender: 0
};
const GRID_SKELETON_ROW_NESTED_ID = 'auto-generated-skeleton-row-nested';
const getSkeletonRowId = index => `${_useGridLazyLoaderPreProcessors.GRID_SKELETON_ROW_ROOT_ID}-${index}`;
const getSkeletonNestedRowId = (index, parentId) => `${GRID_SKELETON_ROW_NESTED_ID}-${parentId}-${index}`;
/**
* Removes a row and its entire subtree from the working `tree`/`dataRowIdToModelLookup`
* copies. Ids in `skip` are left untouched, so a row that moved within the replaced range
* (and was re-added this pass) is kept.
*/
const deleteRowAndDescendants = (tree, dataRowIdToModelLookup, rowId, skip) => {
if (skip?.has(rowId)) {
return;
}
const node = tree[rowId];
if (node?.type === 'group') {
node.children.forEach(childId => deleteRowAndDescendants(tree, dataRowIdToModelLookup, childId, skip));
}
delete tree[rowId];
delete dataRowIdToModelLookup[rowId];
};
/**
* @requires useGridRows (state)
* @requires useGridPagination (state)
* @requires useGridScroll (method
*/
const useGridDataSourceNestedLazyLoader = (privateApiRef, props) => {
const isDataNested = (0, _xDataGrid.useGridSelector)(privateApiRef, () => props.treeData ? true : (privateApiRef.current.unstable_applyPipeProcessors('getRowsParams', {})?.groupFields?.length ?? 0) > 0);
const setStrategyAvailability = React.useCallback(() => {
privateApiRef.current.setStrategyAvailability(_internals.GridStrategyGroup.DataSource, _internals.DataSourceRowsUpdateStrategy.LazyLoadedGroupedData, props.dataSource && props.lazyLoading && isDataNested ? () => true : () => false);
}, [privateApiRef, props.lazyLoading, props.dataSource, isDataNested]);
const [isStrategyActive, setIsStrategyActive] = React.useState(false);
const renderedRowsIntervalCache = React.useRef(INTERVAL_CACHE_INITIAL_STATE);
const rowsStale = React.useRef(false);
const draggedRowId = React.useRef(null);
const pollingIntervalRef = React.useRef(null);
// Snapshot of the row tree taken right before a sort/filter triggered reset.
// Used by nested data updates that fire later in the auto-expansion chain so
// they can preserve expansion state below the first level.
const previousTreeRef = React.useRef(null);
const fetchRows = React.useCallback(params => {
privateApiRef.current.dataSource.fetchRows(_xDataGrid.GRID_ROOT_GROUP_ID, params);
}, [privateApiRef]);
const debouncedFetchRows = React.useMemo(() => (0, _debounce.default)(fetchRows, 0), [fetchRows]);
// Adjust the render context range to fit the pagination model's page size
// First row index should be decreased to the start of the page, end row index should be increased to the end of the page
const adjustRowParams = React.useCallback(params => {
if (typeof params.start !== 'number') {
return params;
}
const paginationModel = (0, _xDataGrid.gridPaginationModelSelector)(privateApiRef);
return (0, _extends2.default)({}, params, {
start: params.start - params.start % paginationModel.pageSize,
end: params.end + paginationModel.pageSize - params.end % paginationModel.pageSize - 1
});
}, [privateApiRef]);
const getChildrenCount = props.dataSource?.getChildrenCount;
const getGroupKey = props.dataSource?.getGroupKey;
const resetRowTree = React.useCallback(() => {
const previousTree = privateApiRef.current.state.rows.tree;
privateApiRef.current.caches.rows.dataRowIdToModelLookup = {};
privateApiRef.current.setState(state => (0, _extends2.default)({}, state, {
rows: (0, _extends2.default)({}, state.rows, {
tree: {
[_xDataGrid.GRID_ROOT_GROUP_ID]: (0, _internals.buildRootGroup)()
},
treeDepths: {},
dataRowIds: [],
dataRowIdToModelLookup: {},
groupsToFetch: []
})
}));
privateApiRef.current.publishEvent('rowsSet');
return previousTree;
}, [privateApiRef]);
const revalidateRows = (0, _useEventCallback.default)((firstRowIndex, lastRowIndex) => {
if (rowsStale.current || lastRowIndex <= firstRowIndex) {
return;
}
const expandedSortedRowIds = (0, _xDataGrid.gridExpandedSortedRowIdsSelector)(privateApiRef);
const sortModel = (0, _xDataGrid.gridSortModelSelector)(privateApiRef);
const filterModel = (0, _xDataGrid.gridFilterModelSelector)(privateApiRef);
const rowRangesByParent = new Map();
for (let i = firstRowIndex; i < lastRowIndex && i < expandedSortedRowIds.length; i += 1) {
const rowId = expandedSortedRowIds[i];
const rowNode = (0, _xDataGrid.gridRowNodeSelector)(privateApiRef, rowId);
if (!rowNode || rowNode.type === 'skeletonRow' || rowNode.parent == null) {
continue;
}
const parentNode = (0, _xDataGrid.gridRowNodeSelector)(privateApiRef, rowNode.parent);
if (!parentNode || parentNode.type !== 'group') {
continue;
}
const childIndex = parentNode.children.indexOf(rowId);
if (childIndex === -1) {
continue;
}
const existingRange = rowRangesByParent.get(rowNode.parent);
if (existingRange) {
existingRange.start = Math.min(existingRange.start, childIndex);
existingRange.end = Math.max(existingRange.end, childIndex);
} else {
rowRangesByParent.set(rowNode.parent, {
start: childIndex,
end: childIndex
});
}
}
rowRangesByParent.forEach((range, parentId) => {
if (parentId === _xDataGrid.GRID_ROOT_GROUP_ID) {
debouncedFetchRows(adjustRowParams({
start: range.start,
end: range.end,
sortModel,
filterModel,
groupKeys: []
}));
return;
}
privateApiRef.current.dataSource.fetchRows(parentId, (0, _extends2.default)({}, adjustRowParams({
start: range.start,
end: range.end,
sortModel,
filterModel
}), {
showChildrenLoading: false
}));
});
});
const stopPolling = React.useCallback(() => {
if (pollingIntervalRef.current !== null) {
clearInterval(pollingIntervalRef.current);
pollingIntervalRef.current = null;
}
}, []);
const startPolling = (0, _useEventCallback.default)(() => {
stopPolling();
if (props.dataSourceRevalidateMs <= 0) {
return;
}
pollingIntervalRef.current = setInterval(() => {
const {
firstRowToRender,
lastRowToRender
} = renderedRowsIntervalCache.current;
revalidateRows(firstRowToRender, lastRowToRender);
}, props.dataSourceRevalidateMs);
});
const addRootSkeletonRows = React.useCallback(() => {
const tree = (0, _extends2.default)({}, privateApiRef.current.state.rows.tree);
const rootGroup = tree[_xDataGrid.GRID_ROOT_GROUP_ID];
const rootGroupChildren = [...rootGroup.children];
const pageRowCount = privateApiRef.current.state.pagination.rowCount;
const rootChildrenCount = rootGroupChildren.length;
if (rootChildrenCount === 0) {
return;
}
let hasChanged = false;
// Nested Lazy loading only support VIEWPORT loading trigger, no need for a specific check
for (let i = 0; i < pageRowCount - rootChildrenCount; i += 1) {
const skeletonId = getSkeletonRowId(i + rootChildrenCount);
rootGroupChildren.push(skeletonId);
const skeletonRowNode = {
type: 'skeletonRow',
id: skeletonId,
parent: _xDataGrid.GRID_ROOT_GROUP_ID,
depth: 0
};
tree[skeletonId] = skeletonRowNode;
hasChanged = true;
}
if (!hasChanged) {
return;
}
tree[_xDataGrid.GRID_ROOT_GROUP_ID] = (0, _extends2.default)({}, rootGroup, {
children: rootGroupChildren
});
privateApiRef.current.setState(state => (0, _extends2.default)({}, state, {
rows: (0, _extends2.default)({}, state.rows, {
tree
})
}), 'addSkeletonRows');
privateApiRef.current.publishEvent('rowsSet');
}, [privateApiRef]);
const findSkeletonSectionAndFetchRows = React.useCallback((firstRowIndex, lastRowIndex, options = {}) => {
const sortModel = (0, _xDataGrid.gridSortModelSelector)(privateApiRef);
const filterModel = (0, _xDataGrid.gridFilterModelSelector)(privateApiRef);
const currentVisibleRows = (0, _internals.getVisibleRows)(privateApiRef);
const skeletonRowsSection = (0, _utils.findSkeletonRowsSection)({
apiRef: privateApiRef,
visibleRows: currentVisibleRows.rows,
range: {
firstRowIndex,
lastRowIndex
}
});
if (!skeletonRowsSection) {
if (!options.skipFallbackRevalidation) {
revalidateRows(firstRowIndex, lastRowIndex);
startPolling();
}
return false;
}
const expandedSortedRowIds = (0, _xDataGrid.gridExpandedSortedRowIdsSelector)(privateApiRef);
const skeletonNodesGroupedByParents = new Map();
// Group skeleton rows by their parent
for (let i = skeletonRowsSection.firstRowIndex; i <= skeletonRowsSection.lastRowIndex; i += 1) {
const rowId = expandedSortedRowIds[i];
const rowNode = (0, _xDataGrid.gridRowNodeSelector)(privateApiRef, rowId);
if (rowNode?.type === 'skeletonRow' && rowNode.parent != null) {
if (!skeletonNodesGroupedByParents.has(rowNode.parent)) {
skeletonNodesGroupedByParents.set(rowNode.parent, []);
}
skeletonNodesGroupedByParents.get(rowNode.parent).push(rowId);
}
}
// Process each parent group separately
skeletonNodesGroupedByParents.forEach((skeletonIds, parentId) => {
const parentNode = (0, _xDataGrid.gridRowNodeSelector)(privateApiRef, parentId);
if (!parentNode) {
return;
}
const parentChildren = parentNode.children;
// Find the first and last skeleton row indexes relative to parent
const firstSkeletonIdx = parentChildren.indexOf(skeletonIds[0]);
const lastSkeletonIdx = parentChildren.indexOf(skeletonIds[skeletonIds.length - 1]);
if (firstSkeletonIdx === -1 || lastSkeletonIdx === -1) {
return;
}
if (parentId === _xDataGrid.GRID_ROOT_GROUP_ID) {
debouncedFetchRows(adjustRowParams({
start: firstSkeletonIdx,
end: lastSkeletonIdx,
sortModel,
filterModel,
groupKeys: []
}));
} else {
privateApiRef.current.dataSource.fetchRows(parentId, adjustRowParams({
start: firstSkeletonIdx,
end: lastSkeletonIdx
}));
}
});
return true;
}, [privateApiRef, debouncedFetchRows, adjustRowParams, revalidateRows, startPolling]);
const fetchVisibleSkeletonRows = React.useCallback((options = {}) => {
const cachedInterval = renderedRowsIntervalCache.current;
let firstRowIndex = cachedInterval.firstRowToRender;
let lastRowIndex = cachedInterval.lastRowToRender;
if (lastRowIndex <= firstRowIndex) {
const renderContext = (0, _internals.gridRenderContextSelector)(privateApiRef);
firstRowIndex = renderContext.firstRowIndex;
lastRowIndex = renderContext.lastRowIndex;
}
if (lastRowIndex <= firstRowIndex) {
return false;
}
return findSkeletonSectionAndFetchRows(firstRowIndex, lastRowIndex, options);
}, [privateApiRef, findSkeletonSectionAndFetchRows]);
const cleanUpParentNodeAndGenerateSkeletonRows = React.useCallback(parentId => {
const dataRowIdToModelLookup = (0, _extends2.default)({}, privateApiRef.current.state.rows.dataRowIdToModelLookup);
const tree = (0, _extends2.default)({}, privateApiRef.current.state.rows.tree);
const deletedIds = new Set();
const rowNode = (0, _xDataGrid.gridRowNodeSelector)(privateApiRef, parentId);
if (!rowNode || rowNode.type !== 'group' || rowNode.children.length === 0) {
return;
}
// Remove current descendants from the tree
const traverse = nodeId => {
const node = (0, _xDataGrid.gridRowNodeSelector)(privateApiRef, nodeId);
if (!node) {
return;
}
// Recursively traverse children first (depth-first)
if (node.type === 'group' && node.children.length > 0) {
node.children.forEach(traverse);
}
if (deletedIds.has(nodeId)) {
return;
}
deletedIds.add(nodeId);
delete dataRowIdToModelLookup[nodeId];
delete tree[nodeId];
};
rowNode.children.forEach(traverse);
// Add skeleton rows for the children
const rowModel = (0, _xDataGrid.gridRowSelector)(privateApiRef, parentId);
if (!rowModel) {
return;
}
const childrenCount = getChildrenCount?.(rowModel) ?? 0;
if (childrenCount <= 0) {
return;
}
const newChildren = [];
for (let i = 0; i < childrenCount; i += 1) {
const skeletonId = getSkeletonNestedRowId(i, parentId);
if (tree[skeletonId]) {
newChildren.push(skeletonId);
continue;
}
tree[skeletonId] = {
type: 'skeletonRow',
id: skeletonId,
parent: parentId,
depth: rowNode.depth + 1
};
newChildren.push(skeletonId);
}
tree[parentId] = (0, _extends2.default)({}, rowNode, {
children: newChildren
});
privateApiRef.current.caches.rows.dataRowIdToModelLookup = dataRowIdToModelLookup;
privateApiRef.current.setState(state => (0, _extends2.default)({}, state, {
rows: (0, _extends2.default)({}, state.rows, {
tree,
dataRowIdToModelLookup,
dataRowIds: state.rows.dataRowIds.filter(id => !deletedIds.has(id))
})
}));
privateApiRef.current.publishEvent('rowsSet');
}, [privateApiRef, getChildrenCount]);
const replaceNestedRows = React.useCallback((startIndex, response, fetchParams, parentId = _xDataGrid.GRID_ROOT_GROUP_ID, previousTree) => {
if (response.rows.length === 0) {
return false;
}
const currentTree = privateApiRef.current.state.rows.tree;
const tree = (0, _extends2.default)({}, currentTree);
const treeToReadPreviousExpansionState = previousTree ?? currentTree;
const dataRowIdToModelLookup = (0, _extends2.default)({}, privateApiRef.current.state.rows.dataRowIdToModelLookup);
const targetGroup = tree[parentId];
const targetGroupChildren = [...targetGroup.children];
const targetGroupChildrenFromPath = Object.assign(Object.create(null), targetGroup.childrenFromPath);
const seenIds = new Set();
if (!getGroupKey) {
throw new Error(process.env.NODE_ENV !== "production" ? 'MUI X: No `getGroupKey` method provided with the dataSource.' : (0, _formatErrorMessage2.default)(121));
}
if (!getChildrenCount) {
throw new Error(process.env.NODE_ENV !== "production" ? 'MUI X: No `getChildrenCount` method provided with the dataSource.' : (0, _formatErrorMessage2.default)(122));
}
// Calculate parent depth (-1 for root, so children at root level have depth 0)
const parentDepth = parentId === _xDataGrid.GRID_ROOT_GROUP_ID ? -1 : targetGroup.depth;
const parentPath = parentId === _xDataGrid.GRID_ROOT_GROUP_ID ? [] : tree[parentId].path ?? [];
const groupFields = props.treeData || !('groupFields' in fetchParams) ? [] : fetchParams.groupFields ?? [];
// The Premium pivoting pipe processor adds `pivotModel` only while pivoting is active.
// Match the non-lazy row grouping path by capping default expansion at the last
// row-grouping level when pivoting is active.
const isPivotingActive = !props.treeData && fetchParams.pivotModel != null;
const maxDepth = isPivotingActive ? groupFields.length - 1 : undefined;
let hasExpandedGroupToFetch = false;
for (let i = 0; i < response.rows.length; i += 1) {
const rowModel = response.rows[i];
const childrenCount = getChildrenCount(rowModel);
if (childrenCount === -1) {
throw new Error(process.env.NODE_ENV !== "production" ? 'MUI X Data Grid: Nested lazy loading does not support unknown children count for now.\nIf this is a use-case that you are interested in, please open an issue: https://github.com/mui/mui-x/issues/new?template=2.feature.yml' : (0, _formatErrorMessage2.default)(284));
}
const rowId = (0, _xDataGrid.gridRowIdSelector)(privateApiRef, rowModel);
const [removedRowId] = targetGroupChildren.splice(startIndex + i, 1, rowId);
// Drop the replaced row and any subtree it owned (skipping rows re-added this
// pass) so a changed id swapping out an expanded group leaves no orphans behind.
deleteRowAndDescendants(tree, dataRowIdToModelLookup, removedRowId, seenIds);
let rowTreeNodeConfig;
const groupingKey = getGroupKey(rowModel);
const previousNode = treeToReadPreviousExpansionState[rowId];
const previousChildrenExpanded = previousNode?.type === 'group' ? previousNode.childrenExpanded : undefined;
if (childrenCount === 0) {
rowTreeNodeConfig = {
id: rowId,
depth: parentDepth + 1,
parent: parentId,
type: 'leaf',
groupingKey
};
} else {
const children = [];
for (let j = 0; j < childrenCount; j += 1) {
const skeletonId = getSkeletonNestedRowId(j, rowId);
children.push(skeletonId);
const skeletonRowNode = {
type: 'skeletonRow',
id: skeletonId,
parent: rowId,
depth: parentDepth + 2
};
tree[skeletonId] = skeletonRowNode;
}
const groupNode = {
id: rowId,
depth: parentDepth + 1,
parent: parentId,
type: 'group',
groupingKey,
path: [...parentPath, groupingKey],
children,
isAutoGenerated: false,
groupingField: props.treeData ? null : groupFields[parentPath.length] ?? null,
serverChildrenCount: childrenCount,
childrenFromPath: Object.create(null)
};
const childrenExpanded = (0, _utils2.checkGroupChildrenExpansion)(groupNode, props.defaultGroupingExpansionDepth, maxDepth, props.isGroupExpandedByDefault, previousChildrenExpanded);
rowTreeNodeConfig = (0, _extends2.default)({}, groupNode, {
childrenExpanded
});
hasExpandedGroupToFetch || (hasExpandedGroupToFetch = childrenExpanded === true);
}
dataRowIdToModelLookup[rowId] = rowModel;
tree[rowId] = rowTreeNodeConfig;
// Update parent's childrenFromPath lookup
const groupingFieldName = rowTreeNodeConfig.groupingField ?? '__no_field__';
const groupingKeyName = rowTreeNodeConfig.groupingKey ?? '__no_key__';
if (!targetGroupChildrenFromPath[groupingFieldName]) {
targetGroupChildrenFromPath[groupingFieldName] = {};
}
targetGroupChildrenFromPath[groupingFieldName][groupingKeyName.toString()] = rowId;
seenIds.add(rowId);
}
// Keep skeleton rows in place (for both root and nested parents) instead of
// filtering them out. Dropping them here would collapse any unfetched gap
// above the replaced range — e.g. after a jump/fast scroll that loads a
// middle range while the rows above it are still skeletons — because
// `addRootSkeletonRows` only appends padding at the end. Leaving the
// skeletons at their indices keeps loaded rows at their real positions.
// The sort/filter reset path starts from an empty children list, so there are no
// skeletons to preserve and `addRootSkeletonRows` pads normally.
tree[parentId] = (0, _extends2.default)({}, targetGroup, {
children: targetGroupChildren,
childrenFromPath: targetGroupChildrenFromPath
});
// Removes potential remaining skeleton rows from the dataRowIds.
// For the root parent the targetGroupChildren list IS the full root row order,
// so we can replace dataRowIds wholesale. For nested parents it only contains
// that subtree's children, so we merge into the existing list to avoid wiping
// sibling subtrees' row IDs.
let dataRowIds;
if (parentId === _xDataGrid.GRID_ROOT_GROUP_ID) {
dataRowIds = targetGroupChildren.filter(childId => tree[childId]?.type !== 'skeletonRow');
} else {
const newIds = new Set(targetGroupChildren.filter(childId => tree[childId]?.type !== 'skeletonRow'));
const previousIds = privateApiRef.current.state.rows.dataRowIds;
dataRowIds = previousIds.filter(id => tree[id] !== undefined && tree[id].type !== 'skeletonRow' && !newIds.has(id));
newIds.forEach(id => dataRowIds.push(id));
}
privateApiRef.current.caches.rows.dataRowIdToModelLookup = dataRowIdToModelLookup;
privateApiRef.current.setState(state => (0, _extends2.default)({}, state, {
rows: (0, _extends2.default)({}, state.rows, {
dataRowIdToModelLookup,
dataRowIds,
tree: (0, _extends2.default)({}, tree),
totalRowCount: parentId === _xDataGrid.GRID_ROOT_GROUP_ID ? response.rowCount ?? -1 : state.rows.totalRowCount
})
}));
privateApiRef.current.publishEvent('rowsSet');
return hasExpandedGroupToFetch;
}, [privateApiRef, getGroupKey, getChildrenCount, props.treeData, props.defaultGroupingExpansionDepth, props.isGroupExpandedByDefault]);
const removeDuplicateRows = React.useCallback((rows, parentId = _xDataGrid.GRID_ROOT_GROUP_ID) => {
const tree = (0, _extends2.default)({}, privateApiRef.current.state.rows.tree);
const dataRowIdToModelLookup = (0, _extends2.default)({}, privateApiRef.current.state.rows.dataRowIdToModelLookup);
const parentNode = tree[parentId];
if (!parentNode || parentNode.type !== 'group') {
return;
}
const parentChildren = [...parentNode.children];
const isRoot = parentId === _xDataGrid.GRID_ROOT_GROUP_ID;
const skeletonDepth = isRoot ? 0 : parentNode.depth + 1;
let duplicateRowCount = 0;
rows.forEach(row => {
const rowId = (0, _xDataGrid.gridRowIdSelector)(privateApiRef, row);
if (tree[rowId] || dataRowIdToModelLookup[rowId]) {
const index = parentChildren.indexOf(rowId);
if (index !== -1) {
const skeletonId = isRoot ? getSkeletonRowId(index) : getSkeletonNestedRowId(index, parentId);
parentChildren[index] = skeletonId;
tree[skeletonId] = {
type: 'skeletonRow',
id: skeletonId,
parent: parentId,
depth: skeletonDepth
};
}
// Remove the duplicate together with any subtree it owned.
// The following `replaceNestedRows` recomputes `dataRowIds` from the tree and re-inserts it.
deleteRowAndDescendants(tree, dataRowIdToModelLookup, rowId);
duplicateRowCount += 1;
}
});
if (duplicateRowCount > 0) {
tree[parentId] = (0, _extends2.default)({}, parentNode, {
children: parentChildren
});
privateApiRef.current.setState(state => (0, _extends2.default)({}, state, {
rows: (0, _extends2.default)({}, state.rows, {
tree,
dataRowIdToModelLookup
})
}));
}
}, [privateApiRef]);
const updateLoadedRows = React.useCallback((parentId, startIndex, rows) => {
if (rows.length === 0) {
return true;
}
const tree = privateApiRef.current.state.rows.tree;
const parentNode = tree[parentId];
if (!parentNode || parentNode.type !== 'group') {
return false;
}
const firstTargetRow = parentNode.children[startIndex];
if (!firstTargetRow || tree[firstTargetRow]?.type === 'skeletonRow') {
return false;
}
const existingRowIds = parentNode.children.slice(startIndex, startIndex + rows.length);
const newRowIds = rows.map(row => (0, _xDataGrid.gridRowIdSelector)(privateApiRef, row));
const sameRowIds = existingRowIds.length === newRowIds.length && existingRowIds.every((rowId, index) => rowId === newRowIds[index]);
if (!sameRowIds) {
return false;
}
const dataRowIdToModelLookup = privateApiRef.current.state.rows.dataRowIdToModelLookup;
const changedRows = rows.filter((row, index) => {
const existingRow = dataRowIdToModelLookup[existingRowIds[index]];
return !(0, _isDeepEqual.isDeepEqual)(row, existingRow);
});
if (changedRows.length > 0) {
const parentPath = parentId === _xDataGrid.GRID_ROOT_GROUP_ID ? [] : parentNode.path ?? [];
privateApiRef.current.updateNestedRows(changedRows, parentPath);
}
return true;
}, [privateApiRef]);
const handleDataUpdate = React.useCallback(params => {
if ('error' in params) {
return;
}
const {
response,
fetchParams
} = params;
let hasExpandedGroupToFetch = false;
const wasStale = rowsStale.current;
const pageRowCount = privateApiRef.current.state.pagination.rowCount;
if (fetchParams.groupKeys?.length === 0 && (response.rowCount !== undefined || pageRowCount === undefined)) {
privateApiRef.current.setRowCount(response.rowCount === undefined ? -1 : response.rowCount);
}
// scroll to the top if the rows are stale and the new request is for the first page
if (rowsStale.current && params.fetchParams.start === 0) {
privateApiRef.current.scroll({
top: 0
});
// Keep the nested lazy-loading tree shape after sort/filter.
// Plain `setRows()` would recreate expandable groups without child skeleton rows.
const previousTree = resetRowTree();
previousTreeRef.current = previousTree;
hasExpandedGroupToFetch = replaceNestedRows(0, response, fetchParams, _xDataGrid.GRID_ROOT_GROUP_ID, previousTree);
} else {
const filteredSortedRowIds = (0, _xDataGrid.gridFilteredSortedRowIdsSelector)(privateApiRef);
const startingIndex = typeof fetchParams.start === 'string' ? Math.max(filteredSortedRowIds.indexOf(fetchParams.start), 0) : fetchParams.start;
if (!updateLoadedRows(_xDataGrid.GRID_ROOT_GROUP_ID, startingIndex, response.rows)) {
removeDuplicateRows(response.rows);
hasExpandedGroupToFetch = replaceNestedRows(startingIndex, response, fetchParams);
}
}
rowsStale.current = false;
addRootSkeletonRows();
privateApiRef.current.setLoading(false);
privateApiRef.current.unstable_applyPipeProcessors('processDataSourceRows', {
params: params.fetchParams,
response
}, false);
privateApiRef.current.requestPipeProcessorsApplication('hydrateRows');
// After sort/filter the tree is reset and skeletons are re-added by
// `addRootSkeletonRows`, but the viewport's first/last indices may be unchanged
// (e.g. scrolled to top already) so `renderedRowsIntervalChange` does not fire.
// Trigger a scan so root-level skeletons sitting in the viewport get fetched.
if (hasExpandedGroupToFetch || wasStale) {
fetchVisibleSkeletonRows({
skipFallbackRevalidation: true
});
}
startPolling();
}, [privateApiRef, addRootSkeletonRows, replaceNestedRows, removeDuplicateRows, updateLoadedRows, fetchVisibleSkeletonRows, startPolling, resetRowTree]);
const handleNestedDataUpdate = React.useCallback(params => {
if ('error' in params) {
return;
}
const {
parentId,
fetchParams,
response
} = params;
// Get the relative start index from fetchParams
const startIndex = typeof fetchParams.start === 'number' ? fetchParams.start : 0;
let hasExpandedGroupToFetch = false;
if (!updateLoadedRows(parentId, startIndex, response.rows)) {
removeDuplicateRows(response.rows, parentId);
hasExpandedGroupToFetch = replaceNestedRows(startIndex, response, fetchParams, parentId, previousTreeRef.current ?? undefined);
}
if (hasExpandedGroupToFetch) {
fetchVisibleSkeletonRows({
skipFallbackRevalidation: true
});
}
startPolling();
}, [replaceNestedRows, removeDuplicateRows, updateLoadedRows, fetchVisibleSkeletonRows, startPolling]);
const handleRowCountChange = React.useCallback(newRowCount => {
if (rowsStale.current) {
return;
}
// Show error if unknown row-count
if (newRowCount < 0) {
throw new Error(process.env.NODE_ENV !== "production" ? 'MUI X Data Grid: Row count is unknown. Please provide a valid row count for lazy loading to work.' : (0, _formatErrorMessage2.default)(285));
}
addRootSkeletonRows();
privateApiRef.current.requestPipeProcessorsApplication('hydrateRows');
}, [privateApiRef, addRootSkeletonRows]);
const handleRenderedRowsIntervalChange = React.useCallback(params => {
if (rowsStale.current) {
return;
}
if (renderedRowsIntervalCache.current.firstRowToRender === params.firstRowIndex && renderedRowsIntervalCache.current.lastRowToRender === params.lastRowIndex) {
return;
}
findSkeletonSectionAndFetchRows(params.firstRowIndex, params.lastRowIndex);
renderedRowsIntervalCache.current = {
firstRowToRender: params.firstRowIndex,
lastRowToRender: params.lastRowIndex
};
}, [findSkeletonSectionAndFetchRows]);
const handleRowExpansionChange = React.useCallback(node => {
// A manual expand/collapse ends the post-sort/filter restoration cascade.
// Drop the pre-reset snapshot so subsequent nested fetches read expansion state
// from the live tree instead of re-applying stale expansion from before the last reset.
previousTreeRef.current = null;
if (node.childrenExpanded) {
fetchVisibleSkeletonRows();
return;
}
cleanUpParentNodeAndGenerateSkeletonRows(node.id);
// Collapsing shifts rows below the group up into the viewport without firing
// `renderedRowsIntervalChange` (same indices, different content), so scan again
// for skeletons now exposed in the cached viewport range.
fetchVisibleSkeletonRows();
}, [fetchVisibleSkeletonRows, cleanUpParentNodeAndGenerateSkeletonRows]);
const throttledHandleRenderedRowsIntervalChange = React.useMemo(() => (0, _throttle.throttle)(handleRenderedRowsIntervalChange, props.lazyLoadingRequestThrottleMs), [props.lazyLoadingRequestThrottleMs, handleRenderedRowsIntervalChange]);
React.useEffect(() => {
return () => {
throttledHandleRenderedRowsIntervalChange.clear();
stopPolling();
};
}, [throttledHandleRenderedRowsIntervalChange, stopPolling]);
React.useEffect(() => {
if (!isStrategyActive || props.dataSourceRevalidateMs <= 0) {
stopPolling();
}
}, [isStrategyActive, props.dataSourceRevalidateMs, stopPolling]);
const handleGridSortModelChange = React.useCallback(newSortModel => {
rowsStale.current = true;
renderedRowsIntervalCache.current = INTERVAL_CACHE_INITIAL_STATE;
throttledHandleRenderedRowsIntervalChange.clear();
stopPolling();
const paginationModel = (0, _xDataGrid.gridPaginationModelSelector)(privateApiRef);
const filterModel = (0, _xDataGrid.gridFilterModelSelector)(privateApiRef);
const getRowsParams = {
start: 0,
end: paginationModel.pageSize - 1,
sortModel: newSortModel,
filterModel
};
privateApiRef.current.setLoading(true);
debouncedFetchRows(getRowsParams);
}, [privateApiRef, debouncedFetchRows, throttledHandleRenderedRowsIntervalChange, stopPolling]);
const handleGridFilterModelChange = React.useCallback(newFilterModel => {
rowsStale.current = true;
renderedRowsIntervalCache.current = INTERVAL_CACHE_INITIAL_STATE;
throttledHandleRenderedRowsIntervalChange.clear();
stopPolling();
const paginationModel = (0, _xDataGrid.gridPaginationModelSelector)(privateApiRef);
const sortModel = (0, _xDataGrid.gridSortModelSelector)(privateApiRef);
const getRowsParams = {
start: 0,
end: paginationModel.pageSize - 1,
sortModel,
filterModel: newFilterModel
};
privateApiRef.current.setLoading(true);
debouncedFetchRows(getRowsParams);
}, [privateApiRef, debouncedFetchRows, throttledHandleRenderedRowsIntervalChange, stopPolling]);
const handleDragStart = React.useCallback(row => {
draggedRowId.current = row.id;
}, []);
const handleDragEnd = React.useCallback(() => {
draggedRowId.current = null;
}, []);
const handleStrategyActivityChange = React.useCallback(() => {
setIsStrategyActive(privateApiRef.current.getActiveStrategy(_internals.GridStrategyGroup.DataSource) === _internals.DataSourceRowsUpdateStrategy.LazyLoadedGroupedData);
}, [privateApiRef]);
(0, _internals.useGridRegisterStrategyProcessor)(privateApiRef, _internals.DataSourceRowsUpdateStrategy.LazyLoadedGroupedData, 'dataSourceRootRowsUpdate', handleDataUpdate);
(0, _internals.useGridRegisterStrategyProcessor)(privateApiRef, _internals.DataSourceRowsUpdateStrategy.LazyLoadedGroupedData, 'dataSourceNestedRowsUpdate', handleNestedDataUpdate);
(0, _xDataGrid.useGridEvent)(privateApiRef, 'strategyAvailabilityChange', handleStrategyActivityChange);
(0, _xDataGrid.useGridEvent)(privateApiRef, 'rowCountChange', (0, _internals.runIf)(isStrategyActive, handleRowCountChange));
(0, _xDataGrid.useGridEvent)(privateApiRef, 'rowExpansionChange', (0, _internals.runIf)(isStrategyActive, handleRowExpansionChange));
(0, _xDataGrid.useGridEvent)(privateApiRef, 'renderedRowsIntervalChange', (0, _internals.runIf)(isStrategyActive, throttledHandleRenderedRowsIntervalChange));
(0, _xDataGrid.useGridEvent)(privateApiRef, 'sortModelChange', (0, _internals.runIf)(isStrategyActive, handleGridSortModelChange));
(0, _xDataGrid.useGridEvent)(privateApiRef, 'filterModelChange', (0, _internals.runIf)(isStrategyActive, handleGridFilterModelChange));
(0, _xDataGrid.useGridEvent)(privateApiRef, 'rowDragStart', (0, _internals.runIf)(isStrategyActive, handleDragStart));
(0, _xDataGrid.useGridEvent)(privateApiRef, 'rowDragEnd', (0, _internals.runIf)(isStrategyActive, handleDragEnd));
React.useEffect(() => {
setStrategyAvailability();
}, [setStrategyAvailability]);
};
exports.useGridDataSourceNestedLazyLoader = useGridDataSourceNestedLazyLoader;