448 lines
18 KiB
JavaScript
448 lines
18 KiB
JavaScript
import _extends from "@babel/runtime/helpers/esm/extends";
|
|
import * as React from 'react';
|
|
import { useGridApiMethod } from '../../utils/useGridApiMethod';
|
|
import { useGridLogger } from '../../utils/useGridLogger';
|
|
import { gridRowCountSelector, gridRowsLookupSelector, gridRowTreeSelector, gridRowGroupingNameSelector, gridRowTreeDepthsSelector, gridDataRowIdsSelector, gridRowsDataRowIdToIdLookupSelector, gridRowMaximumTreeDepthSelector } from './gridRowsSelector';
|
|
import { useTimeout } from '../../utils/useTimeout';
|
|
import { GridSignature, useGridApiEventHandler } from '../../utils/useGridApiEventHandler';
|
|
import { useGridVisibleRows } from '../../utils/useGridVisibleRows';
|
|
import { gridSortedRowIdsSelector } from '../sorting/gridSortingSelector';
|
|
import { gridFilteredRowsLookupSelector } from '../filter/gridFilterSelector';
|
|
import { getTreeNodeDescendants, createRowsInternalCache, getRowsStateFromCache, isAutoGeneratedRow, GRID_ROOT_GROUP_ID, GRID_ID_AUTOGENERATED, updateCacheWithNewRows, getTopLevelRowCount, getRowIdFromRowModel } from './gridRowsUtils';
|
|
import { useGridRegisterPipeApplier } from '../../core/pipeProcessing';
|
|
export const rowsStateInitializer = (state, props, apiRef) => {
|
|
apiRef.current.caches.rows = createRowsInternalCache({
|
|
rows: props.rows,
|
|
getRowId: props.getRowId,
|
|
loading: props.loading,
|
|
rowCount: props.rowCount
|
|
});
|
|
return _extends({}, state, {
|
|
rows: getRowsStateFromCache({
|
|
apiRef,
|
|
rowCountProp: props.rowCount,
|
|
loadingProp: props.loading,
|
|
previousTree: null,
|
|
previousTreeDepths: null
|
|
})
|
|
});
|
|
};
|
|
export const useGridRows = (apiRef, props) => {
|
|
if (process.env.NODE_ENV !== 'production') {
|
|
try {
|
|
// Freeze the `rows` prop so developers have a fast failure if they try to use Array.prototype.push().
|
|
Object.freeze(props.rows);
|
|
} catch (error) {
|
|
// Sometimes, it's impossible to freeze, so we give up on it.
|
|
}
|
|
}
|
|
const logger = useGridLogger(apiRef, 'useGridRows');
|
|
const currentPage = useGridVisibleRows(apiRef, props);
|
|
const lastUpdateMs = React.useRef(Date.now());
|
|
const timeout = useTimeout();
|
|
const getRow = React.useCallback(id => {
|
|
const model = gridRowsLookupSelector(apiRef)[id];
|
|
if (model) {
|
|
return model;
|
|
}
|
|
const node = apiRef.current.getRowNode(id);
|
|
if (node && isAutoGeneratedRow(node)) {
|
|
return {
|
|
[GRID_ID_AUTOGENERATED]: id
|
|
};
|
|
}
|
|
return null;
|
|
}, [apiRef]);
|
|
const getRowIdProp = props.getRowId;
|
|
const getRowId = React.useCallback(row => {
|
|
if (GRID_ID_AUTOGENERATED in row) {
|
|
return row[GRID_ID_AUTOGENERATED];
|
|
}
|
|
if (getRowIdProp) {
|
|
return getRowIdProp(row);
|
|
}
|
|
return row.id;
|
|
}, [getRowIdProp]);
|
|
const lookup = React.useMemo(() => currentPage.rows.reduce((acc, {
|
|
id
|
|
}, index) => {
|
|
acc[id] = index;
|
|
return acc;
|
|
}, {}), [currentPage.rows]);
|
|
const throttledRowsChange = React.useCallback(({
|
|
cache,
|
|
throttle
|
|
}) => {
|
|
const run = () => {
|
|
lastUpdateMs.current = Date.now();
|
|
apiRef.current.setState(state => _extends({}, state, {
|
|
rows: getRowsStateFromCache({
|
|
apiRef,
|
|
rowCountProp: props.rowCount,
|
|
loadingProp: props.loading,
|
|
previousTree: gridRowTreeSelector(apiRef),
|
|
previousTreeDepths: gridRowTreeDepthsSelector(apiRef)
|
|
})
|
|
}));
|
|
apiRef.current.publishEvent('rowsSet');
|
|
apiRef.current.forceUpdate();
|
|
};
|
|
timeout.clear();
|
|
apiRef.current.caches.rows = cache;
|
|
if (!throttle) {
|
|
run();
|
|
return;
|
|
}
|
|
const throttleRemainingTimeMs = props.throttleRowsMs - (Date.now() - lastUpdateMs.current);
|
|
if (throttleRemainingTimeMs > 0) {
|
|
timeout.start(throttleRemainingTimeMs, run);
|
|
return;
|
|
}
|
|
run();
|
|
}, [props.throttleRowsMs, props.rowCount, props.loading, apiRef, timeout]);
|
|
|
|
/**
|
|
* API METHODS
|
|
*/
|
|
const setRows = React.useCallback(rows => {
|
|
logger.debug(`Updating all rows, new length ${rows.length}`);
|
|
const cache = createRowsInternalCache({
|
|
rows,
|
|
getRowId: props.getRowId,
|
|
loading: props.loading,
|
|
rowCount: props.rowCount
|
|
});
|
|
const prevCache = apiRef.current.caches.rows;
|
|
cache.rowsBeforePartialUpdates = prevCache.rowsBeforePartialUpdates;
|
|
throttledRowsChange({
|
|
cache,
|
|
throttle: true
|
|
});
|
|
}, [logger, props.getRowId, props.loading, props.rowCount, throttledRowsChange, apiRef]);
|
|
const updateRows = React.useCallback(updates => {
|
|
if (props.signature === GridSignature.DataGrid && updates.length > 1) {
|
|
throw new Error(["MUI: You can't update several rows at once in `apiRef.current.updateRows` on the DataGrid.", 'You need to upgrade to DataGridPro or DataGridPremium component to unlock this feature.'].join('\n'));
|
|
}
|
|
const nonPinnedRowsUpdates = [];
|
|
updates.forEach(update => {
|
|
const id = getRowIdFromRowModel(update, props.getRowId, 'A row was provided without id when calling updateRows():');
|
|
const rowNode = apiRef.current.getRowNode(id);
|
|
if ((rowNode == null ? void 0 : rowNode.type) === 'pinnedRow') {
|
|
// @ts-ignore because otherwise `release:build` doesn't work
|
|
const pinnedRowsCache = apiRef.current.caches.pinnedRows;
|
|
const prevModel = pinnedRowsCache.idLookup[id];
|
|
if (prevModel) {
|
|
pinnedRowsCache.idLookup[id] = _extends({}, prevModel, update);
|
|
}
|
|
} else {
|
|
nonPinnedRowsUpdates.push(update);
|
|
}
|
|
});
|
|
const cache = updateCacheWithNewRows({
|
|
updates: nonPinnedRowsUpdates,
|
|
getRowId: props.getRowId,
|
|
previousCache: apiRef.current.caches.rows
|
|
});
|
|
throttledRowsChange({
|
|
cache,
|
|
throttle: true
|
|
});
|
|
}, [props.signature, props.getRowId, throttledRowsChange, apiRef]);
|
|
const getRowModels = React.useCallback(() => {
|
|
const dataRows = gridDataRowIdsSelector(apiRef);
|
|
const idRowsLookup = gridRowsLookupSelector(apiRef);
|
|
return new Map(dataRows.map(id => {
|
|
var _idRowsLookup$id;
|
|
return [id, (_idRowsLookup$id = idRowsLookup[id]) != null ? _idRowsLookup$id : {}];
|
|
}));
|
|
}, [apiRef]);
|
|
const getRowsCount = React.useCallback(() => gridRowCountSelector(apiRef), [apiRef]);
|
|
const getAllRowIds = React.useCallback(() => gridDataRowIdsSelector(apiRef), [apiRef]);
|
|
const getRowIndexRelativeToVisibleRows = React.useCallback(id => lookup[id], [lookup]);
|
|
const setRowChildrenExpansion = React.useCallback((id, isExpanded) => {
|
|
const currentNode = apiRef.current.getRowNode(id);
|
|
if (!currentNode) {
|
|
throw new Error(`MUI: No row with id #${id} found`);
|
|
}
|
|
if (currentNode.type !== 'group') {
|
|
throw new Error('MUI: Only group nodes can be expanded or collapsed');
|
|
}
|
|
const newNode = _extends({}, currentNode, {
|
|
childrenExpanded: isExpanded
|
|
});
|
|
apiRef.current.setState(state => {
|
|
return _extends({}, state, {
|
|
rows: _extends({}, state.rows, {
|
|
tree: _extends({}, state.rows.tree, {
|
|
[id]: newNode
|
|
})
|
|
})
|
|
});
|
|
});
|
|
apiRef.current.forceUpdate();
|
|
apiRef.current.publishEvent('rowExpansionChange', newNode);
|
|
}, [apiRef]);
|
|
const getRowNode = React.useCallback(id => {
|
|
var _ref;
|
|
return (_ref = gridRowTreeSelector(apiRef)[id]) != null ? _ref : null;
|
|
}, [apiRef]);
|
|
const getRowGroupChildren = React.useCallback(({
|
|
skipAutoGeneratedRows = true,
|
|
groupId,
|
|
applySorting,
|
|
applyFiltering
|
|
}) => {
|
|
const tree = gridRowTreeSelector(apiRef);
|
|
let children;
|
|
if (applySorting) {
|
|
const groupNode = tree[groupId];
|
|
if (!groupNode) {
|
|
return [];
|
|
}
|
|
const sortedRowIds = gridSortedRowIdsSelector(apiRef);
|
|
children = [];
|
|
const startIndex = sortedRowIds.findIndex(id => id === groupId) + 1;
|
|
for (let index = startIndex; index < sortedRowIds.length && tree[sortedRowIds[index]].depth > groupNode.depth; index += 1) {
|
|
const id = sortedRowIds[index];
|
|
if (!skipAutoGeneratedRows || !isAutoGeneratedRow(tree[id])) {
|
|
children.push(id);
|
|
}
|
|
}
|
|
} else {
|
|
children = getTreeNodeDescendants(tree, groupId, skipAutoGeneratedRows);
|
|
}
|
|
if (applyFiltering) {
|
|
const filteredRowsLookup = gridFilteredRowsLookupSelector(apiRef);
|
|
children = children.filter(childId => filteredRowsLookup[childId] !== false);
|
|
}
|
|
return children;
|
|
}, [apiRef]);
|
|
const setRowIndex = React.useCallback((rowId, targetIndex) => {
|
|
const node = apiRef.current.getRowNode(rowId);
|
|
if (!node) {
|
|
throw new Error(`MUI: No row with id #${rowId} found`);
|
|
}
|
|
if (node.parent !== GRID_ROOT_GROUP_ID) {
|
|
throw new Error(`MUI: The row reordering do not support reordering of grouped rows yet`);
|
|
}
|
|
if (node.type !== 'leaf') {
|
|
throw new Error(`MUI: The row reordering do not support reordering of footer or grouping rows`);
|
|
}
|
|
apiRef.current.setState(state => {
|
|
const group = gridRowTreeSelector(state, apiRef.current.instanceId)[GRID_ROOT_GROUP_ID];
|
|
const allRows = group.children;
|
|
const oldIndex = allRows.findIndex(row => row === rowId);
|
|
if (oldIndex === -1 || oldIndex === targetIndex) {
|
|
return state;
|
|
}
|
|
logger.debug(`Moving row ${rowId} to index ${targetIndex}`);
|
|
const updatedRows = [...allRows];
|
|
updatedRows.splice(targetIndex, 0, updatedRows.splice(oldIndex, 1)[0]);
|
|
return _extends({}, state, {
|
|
rows: _extends({}, state.rows, {
|
|
tree: _extends({}, state.rows.tree, {
|
|
[GRID_ROOT_GROUP_ID]: _extends({}, group, {
|
|
children: updatedRows
|
|
})
|
|
})
|
|
})
|
|
});
|
|
});
|
|
apiRef.current.publishEvent('rowsSet');
|
|
}, [apiRef, logger]);
|
|
const replaceRows = React.useCallback((firstRowToRender, newRows) => {
|
|
if (props.signature === GridSignature.DataGrid && newRows.length > 1) {
|
|
throw new Error(["MUI: You can't replace rows using `apiRef.current.unstable_replaceRows` on the DataGrid.", 'You need to upgrade to DataGridPro or DataGridPremium component to unlock this feature.'].join('\n'));
|
|
}
|
|
if (newRows.length === 0) {
|
|
return;
|
|
}
|
|
const treeDepth = gridRowMaximumTreeDepthSelector(apiRef);
|
|
if (treeDepth > 1) {
|
|
throw new Error('`apiRef.current.unstable_replaceRows` is not compatible with tree data and row grouping');
|
|
}
|
|
const tree = _extends({}, gridRowTreeSelector(apiRef));
|
|
const dataRowIdToModelLookup = _extends({}, gridRowsLookupSelector(apiRef));
|
|
const dataRowIdToIdLookup = _extends({}, gridRowsDataRowIdToIdLookupSelector(apiRef));
|
|
const rootGroup = tree[GRID_ROOT_GROUP_ID];
|
|
const rootGroupChildren = [...rootGroup.children];
|
|
const seenIds = new Set();
|
|
for (let i = 0; i < newRows.length; i += 1) {
|
|
const rowModel = newRows[i];
|
|
const rowId = getRowIdFromRowModel(rowModel, props.getRowId, 'A row was provided without id when calling replaceRows().');
|
|
const [removedRowId] = rootGroupChildren.splice(firstRowToRender + i, 1, rowId);
|
|
if (!seenIds.has(removedRowId)) {
|
|
delete dataRowIdToModelLookup[removedRowId];
|
|
delete dataRowIdToIdLookup[removedRowId];
|
|
delete tree[removedRowId];
|
|
}
|
|
const rowTreeNodeConfig = {
|
|
id: rowId,
|
|
depth: 0,
|
|
parent: GRID_ROOT_GROUP_ID,
|
|
type: 'leaf',
|
|
groupingKey: null
|
|
};
|
|
dataRowIdToModelLookup[rowId] = rowModel;
|
|
dataRowIdToIdLookup[rowId] = rowId;
|
|
tree[rowId] = rowTreeNodeConfig;
|
|
seenIds.add(rowId);
|
|
}
|
|
tree[GRID_ROOT_GROUP_ID] = _extends({}, rootGroup, {
|
|
children: rootGroupChildren
|
|
});
|
|
|
|
// Removes potential remaining skeleton rows from the dataRowIds.
|
|
const dataRowIds = rootGroupChildren.filter(childId => tree[childId].type === 'leaf');
|
|
apiRef.current.caches.rows.dataRowIdToModelLookup = dataRowIdToModelLookup;
|
|
apiRef.current.caches.rows.dataRowIdToIdLookup = dataRowIdToIdLookup;
|
|
apiRef.current.setState(state => _extends({}, state, {
|
|
rows: _extends({}, state.rows, {
|
|
dataRowIdToModelLookup,
|
|
dataRowIdToIdLookup,
|
|
dataRowIds,
|
|
tree
|
|
})
|
|
}));
|
|
apiRef.current.publishEvent('rowsSet');
|
|
}, [apiRef, props.signature, props.getRowId]);
|
|
const rowApi = {
|
|
getRow,
|
|
getRowId,
|
|
getRowModels,
|
|
getRowsCount,
|
|
getAllRowIds,
|
|
setRows,
|
|
updateRows,
|
|
getRowNode,
|
|
getRowIndexRelativeToVisibleRows,
|
|
unstable_replaceRows: replaceRows
|
|
};
|
|
const rowProApi = {
|
|
setRowIndex,
|
|
setRowChildrenExpansion,
|
|
getRowGroupChildren
|
|
};
|
|
|
|
/**
|
|
* EVENTS
|
|
*/
|
|
const groupRows = React.useCallback(() => {
|
|
logger.info(`Row grouping pre-processing have changed, regenerating the row tree`);
|
|
let cache;
|
|
if (apiRef.current.caches.rows.rowsBeforePartialUpdates === props.rows) {
|
|
// The `props.rows` did not change since the last row grouping
|
|
// We can use the current rows cache which contains the partial updates done recently.
|
|
cache = _extends({}, apiRef.current.caches.rows, {
|
|
updates: {
|
|
type: 'full',
|
|
rows: gridDataRowIdsSelector(apiRef)
|
|
}
|
|
});
|
|
} else {
|
|
// The `props.rows` has changed since the last row grouping
|
|
// We must use the new `props.rows` on the new grouping
|
|
// This occurs because this event is triggered before the `useEffect` on the rows when both the grouping pre-processing and the rows changes on the same render
|
|
cache = createRowsInternalCache({
|
|
rows: props.rows,
|
|
getRowId: props.getRowId,
|
|
loading: props.loading,
|
|
rowCount: props.rowCount
|
|
});
|
|
}
|
|
throttledRowsChange({
|
|
cache,
|
|
throttle: false
|
|
});
|
|
}, [logger, apiRef, props.rows, props.getRowId, props.loading, props.rowCount, throttledRowsChange]);
|
|
const handleStrategyProcessorChange = React.useCallback(methodName => {
|
|
if (methodName === 'rowTreeCreation') {
|
|
groupRows();
|
|
}
|
|
}, [groupRows]);
|
|
const handleStrategyActivityChange = React.useCallback(() => {
|
|
// `rowTreeCreation` is the only processor ran when `strategyAvailabilityChange` is fired.
|
|
// All the other processors listen to `rowsSet` which will be published by the `groupRows` method below.
|
|
if (apiRef.current.getActiveStrategy('rowTree') !== gridRowGroupingNameSelector(apiRef)) {
|
|
groupRows();
|
|
}
|
|
}, [apiRef, groupRows]);
|
|
useGridApiEventHandler(apiRef, 'activeStrategyProcessorChange', handleStrategyProcessorChange);
|
|
useGridApiEventHandler(apiRef, 'strategyAvailabilityChange', handleStrategyActivityChange);
|
|
|
|
/**
|
|
* APPLIERS
|
|
*/
|
|
const applyHydrateRowsProcessor = React.useCallback(() => {
|
|
apiRef.current.setState(state => {
|
|
const response = apiRef.current.unstable_applyPipeProcessors('hydrateRows', {
|
|
tree: gridRowTreeSelector(state, apiRef.current.instanceId),
|
|
treeDepths: gridRowTreeDepthsSelector(state, apiRef.current.instanceId),
|
|
dataRowIds: gridDataRowIdsSelector(state, apiRef.current.instanceId),
|
|
dataRowIdToModelLookup: gridRowsLookupSelector(state, apiRef.current.instanceId),
|
|
dataRowIdToIdLookup: gridRowsDataRowIdToIdLookupSelector(state, apiRef.current.instanceId)
|
|
});
|
|
return _extends({}, state, {
|
|
rows: _extends({}, state.rows, response, {
|
|
totalTopLevelRowCount: getTopLevelRowCount({
|
|
tree: response.tree,
|
|
rowCountProp: props.rowCount
|
|
})
|
|
})
|
|
});
|
|
});
|
|
apiRef.current.publishEvent('rowsSet');
|
|
apiRef.current.forceUpdate();
|
|
}, [apiRef, props.rowCount]);
|
|
useGridRegisterPipeApplier(apiRef, 'hydrateRows', applyHydrateRowsProcessor);
|
|
useGridApiMethod(apiRef, rowApi, 'public');
|
|
useGridApiMethod(apiRef, rowProApi, props.signature === GridSignature.DataGrid ? 'private' : 'public');
|
|
|
|
// The effect do not track any value defined synchronously during the 1st render by hooks called after `useGridRows`
|
|
// As a consequence, the state generated by the 1st run of this useEffect will always be equal to the initialization one
|
|
const isFirstRender = React.useRef(true);
|
|
React.useEffect(() => {
|
|
if (isFirstRender.current) {
|
|
isFirstRender.current = false;
|
|
return;
|
|
}
|
|
const areNewRowsAlreadyInState = apiRef.current.caches.rows.rowsBeforePartialUpdates === props.rows;
|
|
const isNewLoadingAlreadyInState = apiRef.current.caches.rows.loadingPropBeforePartialUpdates === props.loading;
|
|
const isNewRowCountAlreadyInState = apiRef.current.caches.rows.rowCountPropBeforePartialUpdates === props.rowCount;
|
|
|
|
// The new rows have already been applied (most likely in the `'rowGroupsPreProcessingChange'` listener)
|
|
if (areNewRowsAlreadyInState) {
|
|
// If the loading prop has changed, we need to update its value in the state because it won't be done by `throttledRowsChange`
|
|
if (!isNewLoadingAlreadyInState) {
|
|
apiRef.current.setState(state => _extends({}, state, {
|
|
rows: _extends({}, state.rows, {
|
|
loading: props.loading
|
|
})
|
|
}));
|
|
apiRef.current.caches.rows.loadingPropBeforePartialUpdates = props.loading;
|
|
apiRef.current.forceUpdate();
|
|
}
|
|
if (!isNewRowCountAlreadyInState) {
|
|
apiRef.current.setState(state => _extends({}, state, {
|
|
rows: _extends({}, state.rows, {
|
|
totalRowCount: Math.max(props.rowCount || 0, state.rows.totalRowCount),
|
|
totalTopLevelRowCount: Math.max(props.rowCount || 0, state.rows.totalTopLevelRowCount)
|
|
})
|
|
}));
|
|
apiRef.current.caches.rows.rowCountPropBeforePartialUpdates = props.rowCount;
|
|
apiRef.current.forceUpdate();
|
|
}
|
|
return;
|
|
}
|
|
logger.debug(`Updating all rows, new length ${props.rows.length}`);
|
|
throttledRowsChange({
|
|
cache: createRowsInternalCache({
|
|
rows: props.rows,
|
|
getRowId: props.getRowId,
|
|
loading: props.loading,
|
|
rowCount: props.rowCount
|
|
}),
|
|
throttle: false
|
|
});
|
|
}, [props.rows, props.rowCount, props.getRowId, props.loading, logger, throttledRowsChange, apiRef]);
|
|
}; |