@mui/x-data-grid
Version:
The Community plan edition of the Data Grid components (MUI X).
284 lines (271 loc) • 10.5 kB
JavaScript
import _extends from "@babel/runtime/helpers/esm/extends";
import { gridPinnedRowsSelector } from './gridRowsSelector';
import { gridDimensionsSelector } from '../dimensions/gridDimensionsSelectors';
export const GRID_ROOT_GROUP_ID = `auto-generated-group-node-root`;
export const GRID_ID_AUTOGENERATED = Symbol('mui.id_autogenerated');
export const buildRootGroup = () => ({
type: 'group',
id: GRID_ROOT_GROUP_ID,
depth: -1,
groupingField: null,
groupingKey: null,
isAutoGenerated: true,
children: [],
childrenFromPath: {},
childrenExpanded: true,
parent: null
});
/**
* A helper function to check if the id provided is valid.
* @param {GridRowId} id Id as [[GridRowId]].
* @param {GridRowModel | Partial<GridRowModel>} row Row as [[GridRowModel]].
* @param {string} detailErrorMessage A custom error message to display for invalid IDs
*/
export function checkGridRowIdIsValid(id, row, detailErrorMessage = 'A row was provided without id in the rows prop:') {
if (id == null) {
throw new Error(['MUI X: The data grid component requires all rows to have a unique `id` property.', 'Alternatively, you can use the `getRowId` prop to specify a custom id for each row.', detailErrorMessage, JSON.stringify(row)].join('\n'));
}
}
export const getRowIdFromRowModel = (rowModel, getRowId, detailErrorMessage) => {
const id = getRowId ? getRowId(rowModel) : rowModel.id;
checkGridRowIdIsValid(id, rowModel, detailErrorMessage);
return id;
};
export const createRowsInternalCache = ({
rows,
getRowId,
loading,
rowCount
}) => {
const updates = {
type: 'full',
rows: []
};
const dataRowIdToModelLookup = {};
const dataRowIdToIdLookup = {};
for (let i = 0; i < rows.length; i += 1) {
const model = rows[i];
const id = getRowIdFromRowModel(model, getRowId);
dataRowIdToModelLookup[id] = model;
dataRowIdToIdLookup[id] = id;
updates.rows.push(id);
}
return {
rowsBeforePartialUpdates: rows,
loadingPropBeforePartialUpdates: loading,
rowCountPropBeforePartialUpdates: rowCount,
updates,
dataRowIdToIdLookup,
dataRowIdToModelLookup
};
};
export const getTopLevelRowCount = ({
tree,
rowCountProp = 0
}) => {
const rootGroupNode = tree[GRID_ROOT_GROUP_ID];
return Math.max(rowCountProp, rootGroupNode.children.length + (rootGroupNode.footerId == null ? 0 : 1));
};
export const getRowsStateFromCache = ({
apiRef,
rowCountProp = 0,
loadingProp,
previousTree,
previousTreeDepths
}) => {
const cache = apiRef.current.caches.rows;
// 1. Apply the "rowTreeCreation" family processing.
const {
tree: unProcessedTree,
treeDepths: unProcessedTreeDepths,
dataRowIds: unProcessedDataRowIds,
groupingName
} = apiRef.current.applyStrategyProcessor('rowTreeCreation', {
previousTree,
previousTreeDepths,
updates: cache.updates,
dataRowIdToIdLookup: cache.dataRowIdToIdLookup,
dataRowIdToModelLookup: cache.dataRowIdToModelLookup
});
// 2. Apply the "hydrateRows" pipe-processing.
const groupingParamsWithHydrateRows = apiRef.current.unstable_applyPipeProcessors('hydrateRows', {
tree: unProcessedTree,
treeDepths: unProcessedTreeDepths,
dataRowIdToIdLookup: cache.dataRowIdToIdLookup,
dataRowIds: unProcessedDataRowIds,
dataRowIdToModelLookup: cache.dataRowIdToModelLookup
});
// 3. Reset the cache updates
apiRef.current.caches.rows.updates = {
type: 'partial',
actions: {
insert: [],
modify: [],
remove: []
},
idToActionLookup: {}
};
return _extends({}, groupingParamsWithHydrateRows, {
totalRowCount: Math.max(rowCountProp, groupingParamsWithHydrateRows.dataRowIds.length),
totalTopLevelRowCount: getTopLevelRowCount({
tree: groupingParamsWithHydrateRows.tree,
rowCountProp
}),
groupingName,
loading: loadingProp
});
};
export const isAutoGeneratedRow = rowNode => rowNode.type === 'skeletonRow' || rowNode.type === 'footer' || rowNode.type === 'group' && rowNode.isAutoGenerated || rowNode.type === 'pinnedRow' && rowNode.isAutoGenerated;
export const getTreeNodeDescendants = (tree, parentId, skipAutoGeneratedRows) => {
const node = tree[parentId];
if (node.type !== 'group') {
return [];
}
const validDescendants = [];
for (let i = 0; i < node.children.length; i += 1) {
const child = node.children[i];
if (!skipAutoGeneratedRows || !isAutoGeneratedRow(tree[child])) {
validDescendants.push(child);
}
const childDescendants = getTreeNodeDescendants(tree, child, skipAutoGeneratedRows);
for (let j = 0; j < childDescendants.length; j += 1) {
validDescendants.push(childDescendants[j]);
}
}
if (!skipAutoGeneratedRows && node.footerId != null) {
validDescendants.push(node.footerId);
}
return validDescendants;
};
export const updateCacheWithNewRows = ({
previousCache,
getRowId,
updates
}) => {
if (previousCache.updates.type === 'full') {
throw new Error('MUI X: Unable to prepare a partial update if a full update is not applied yet.');
}
// Remove duplicate updates.
// A server can batch updates, and send several updates for the same row in one fn call.
const uniqueUpdates = new Map();
updates.forEach(update => {
const id = getRowIdFromRowModel(update, getRowId, 'A row was provided without id when calling updateRows():');
if (uniqueUpdates.has(id)) {
uniqueUpdates.set(id, _extends({}, uniqueUpdates.get(id), update));
} else {
uniqueUpdates.set(id, update);
}
});
const partialUpdates = {
type: 'partial',
actions: {
insert: [...(previousCache.updates.actions.insert ?? [])],
modify: [...(previousCache.updates.actions.modify ?? [])],
remove: [...(previousCache.updates.actions.remove ?? [])]
},
idToActionLookup: _extends({}, previousCache.updates.idToActionLookup)
};
const dataRowIdToModelLookup = _extends({}, previousCache.dataRowIdToModelLookup);
const dataRowIdToIdLookup = _extends({}, previousCache.dataRowIdToIdLookup);
const alreadyAppliedActionsToRemove = {
insert: {},
modify: {},
remove: {}
};
// Depending on the action already applied to the data row,
// We might want drop the already-applied-update.
// For instance:
// - if you delete then insert, then you don't want to apply the deletion in the tree.
// - if you insert, then modify, then you just want to apply the insertion in the tree.
uniqueUpdates.forEach((partialRow, id) => {
const actionAlreadyAppliedToRow = partialUpdates.idToActionLookup[id];
// Action === "delete"
// eslint-disable-next-line no-underscore-dangle
if (partialRow._action === 'delete') {
// If the data row has been removed since the last state update,
// Then do nothing.
if (actionAlreadyAppliedToRow === 'remove' || !dataRowIdToModelLookup[id]) {
return;
}
// If the data row has been inserted / modified since the last state update,
// Then drop this "insert" / "modify" update.
if (actionAlreadyAppliedToRow != null) {
alreadyAppliedActionsToRemove[actionAlreadyAppliedToRow][id] = true;
}
// Remove the data row from the lookups and add it to the "delete" update.
partialUpdates.actions.remove.push(id);
delete dataRowIdToModelLookup[id];
delete dataRowIdToIdLookup[id];
return;
}
const oldRow = dataRowIdToModelLookup[id];
// Action === "modify"
if (oldRow) {
// If the data row has been removed since the last state update,
// Then drop this "remove" update and add it to the "modify" update instead.
if (actionAlreadyAppliedToRow === 'remove') {
alreadyAppliedActionsToRemove.remove[id] = true;
partialUpdates.actions.modify.push(id);
}
// If the date has not been inserted / modified since the last state update,
// Then add it to the "modify" update (if it has been inserted it should just remain "inserted").
else if (actionAlreadyAppliedToRow == null) {
partialUpdates.actions.modify.push(id);
}
// Update the data row lookups.
dataRowIdToModelLookup[id] = _extends({}, oldRow, partialRow);
return;
}
// Action === "insert"
// If the data row has been removed since the last state update,
// Then drop the "remove" update and add it to the "insert" update instead.
if (actionAlreadyAppliedToRow === 'remove') {
alreadyAppliedActionsToRemove.remove[id] = true;
partialUpdates.actions.insert.push(id);
}
// If the data row has not been inserted since the last state update,
// Then add it to the "insert" update.
// `actionAlreadyAppliedToRow` can't be equal to "modify", otherwise we would have an `oldRow` above.
else if (actionAlreadyAppliedToRow == null) {
partialUpdates.actions.insert.push(id);
}
// Update the data row lookups.
dataRowIdToModelLookup[id] = partialRow;
dataRowIdToIdLookup[id] = id;
});
const actionTypeWithActionsToRemove = Object.keys(alreadyAppliedActionsToRemove);
for (let i = 0; i < actionTypeWithActionsToRemove.length; i += 1) {
const actionType = actionTypeWithActionsToRemove[i];
const idsToRemove = alreadyAppliedActionsToRemove[actionType];
if (Object.keys(idsToRemove).length > 0) {
partialUpdates.actions[actionType] = partialUpdates.actions[actionType].filter(id => !idsToRemove[id]);
}
}
return {
dataRowIdToModelLookup,
dataRowIdToIdLookup,
updates: partialUpdates,
rowsBeforePartialUpdates: previousCache.rowsBeforePartialUpdates,
loadingPropBeforePartialUpdates: previousCache.loadingPropBeforePartialUpdates,
rowCountPropBeforePartialUpdates: previousCache.rowCountPropBeforePartialUpdates
};
};
export function calculatePinnedRowsHeight(apiRef) {
const pinnedRows = gridPinnedRowsSelector(apiRef);
const topPinnedRowsHeight = pinnedRows?.top?.reduce((acc, value) => {
acc += apiRef.current.unstable_getRowHeight(value.id);
return acc;
}, 0) || 0;
const bottomPinnedRowsHeight = pinnedRows?.bottom?.reduce((acc, value) => {
acc += apiRef.current.unstable_getRowHeight(value.id);
return acc;
}, 0) || 0;
return {
top: topPinnedRowsHeight,
bottom: bottomPinnedRowsHeight
};
}
export function getMinimalContentHeight(apiRef) {
const dimensions = gridDimensionsSelector(apiRef.current.state);
return `var(--DataGrid-overlayHeight, ${2 * dimensions.rowHeight}px)`;
}