@mui/x-data-grid
Version:
The Community plan edition of the MUI X Data Grid components.
421 lines (403 loc) • 18.1 kB
JavaScript
import _extends from "@babel/runtime/helpers/esm/extends";
import _formatErrorMessage from "@mui/x-internals/formatErrorMessage";
import { warnOnce } from '@mui/x-internals/warning';
import { gridRowNodeSelector } from "./gridRowsSelector.mjs";
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: [],
// Prototype-less so user-supplied grouping values like `'constructor'`
// or `'__proto__'` cannot collide with `Object.prototype` properties.
childrenFromPath: Object.create(null),
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(process.env.NODE_ENV !== "production" ? `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)}` : _formatErrorMessage(85, detailErrorMessage, JSON.stringify(row)));
}
}
export const getRowIdFromRowModel = (rowModel, getRowId, detailErrorMessage) => {
const id = getRowId ? getRowId(rowModel) : rowModel.id;
checkGridRowIdIsValid(id, rowModel, detailErrorMessage);
return id;
};
export const getRowValue = (row, colDef, apiRef) => {
if (!colDef) {
return undefined;
}
if (!colDef.valueGetter) {
return row[colDef.field];
}
const value = row[colDef.field];
return colDef.valueGetter(value, row, colDef, apiRef);
};
export const createRowsInternalCache = ({
rows,
getRowId,
loading,
rowCount
}) => {
const updates = {
type: 'full',
rows: []
};
const dataRowIdToModelLookup = {};
for (let i = 0; i < rows.length; i += 1) {
const model = rows[i];
const id = getRowIdFromRowModel(model, getRowId);
dataRowIdToModelLookup[id] = model;
updates.rows.push(id);
}
return {
rowsBeforePartialUpdates: rows,
loadingPropBeforePartialUpdates: loading,
rowCountPropBeforePartialUpdates: rowCount,
updates,
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,
previousGroupsToFetch
}) => {
const cache = apiRef.current.caches.rows;
// 1. Apply the "rowTreeCreation" family processing.
const {
tree: unProcessedTree,
treeDepths: unProcessedTreeDepths,
dataRowIds: unProcessedDataRowIds,
groupingName,
groupsToFetch = []
} = apiRef.current.applyStrategyProcessor('rowTreeCreation', {
previousTree,
previousTreeDepths,
updates: cache.updates,
dataRowIdToModelLookup: cache.dataRowIdToModelLookup,
previousGroupsToFetch
});
// 2. Apply the "hydrateRows" pipe-processing.
const groupingParamsWithHydrateRows = apiRef.current.unstable_applyPipeProcessors('hydrateRows', {
tree: unProcessedTree,
treeDepths: unProcessedTreeDepths,
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,
groupsToFetch
});
};
export const isAutogeneratedRow = row => GRID_ID_AUTOGENERATED in row;
export const isAutogeneratedRowNode = rowNode => rowNode.type === 'skeletonRow' || rowNode.type === 'footer' || rowNode.type === 'group' && rowNode.isAutoGenerated || rowNode.type === 'pinnedRow' && rowNode.isAutoGenerated;
export const getTreeNodeDescendants = (tree, parentId, skipAutoGeneratedRows, directChildrenOnly) => {
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 || !isAutogeneratedRowNode(tree[child])) {
validDescendants.push(child);
}
if (directChildrenOnly) {
continue;
}
const childDescendants = getTreeNodeDescendants(tree, child, skipAutoGeneratedRows, directChildrenOnly);
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 isReplaceUpdate = update =>
// eslint-disable-next-line no-underscore-dangle
update._action === 'replace';
/**
* Extracts the replacement row from a `{ _action: 'replace', row }` update.
* The marker lives on the throwaway envelope, so the row object itself is never touched.
* @param {GridRowModelReplace} update The update provided to `updateRows()`.
* @returns {GridRowModel} The object to store as the row, verbatim.
*/
export const getReplaceRow = update => {
const {
row
} = update;
if (row == null) {
throw new Error(process.env.NODE_ENV !== "production" ? `MUI X Data Grid: A row update with \`_action: 'replace'\` was provided without a \`row\` property.
Without it, the Data Grid does not know which object to store for that row.
Provide the replacement object in the \`row\` property, for example \`updateRows([{ _action: 'replace', row }])\`.
For more detail, see https://mui.com/x/react-data-grid/row-updates/.` : _formatErrorMessage(304));
}
return row;
};
/**
* Merges a partial update into the existing row.
* Prototype-preservation rules:
* 1. If partialRow is a class instance (non-plain object), use its prototype — the
* caller is passing in a fully-constructed instance and owns the prototype chain.
* 2. If partialRow is a plain object (Object.prototype), prefer oldRow's prototype so
* that a partial-update object does not silently discard the existing row's class.
* 3. If both are plain objects the result is also a plain object (no-op).
*
* Note: private class fields (#field syntax) cannot be preserved through a spread-style
* merge because the brand check is tied to the original instance. Rows that rely on
* private fields must either supply a fully-constructed instance as partialRow (rule 1
* above) or use `_action: 'replace'` rather than a plain-object partial update.
* @param {GridRowModel} oldRow The row currently stored in the lookup.
* @param {GridRowModelUpdate} partialRow The update to merge into it.
* @returns {GridRowModel} The merged row.
*/
export const mergeRowUpdate = (oldRow, partialRow) => {
const partialRowProto = Object.getPrototypeOf(partialRow);
const isPartialRowPlain = partialRowProto === Object.prototype || partialRowProto === null;
const proto = isPartialRowPlain ? Object.getPrototypeOf(oldRow) : partialRowProto;
const merged = Object.create(proto);
Object.assign(merged, oldRow, partialRow);
return merged;
};
/**
* Warns when a replace demotes a class instance to a plain object, which happens when the
* replacement was built with a spread instead of being passed as-is.
* Dev-only, does nothing in production.
* @param {GridRowModel | undefined} oldRow The row being replaced, if any.
* @param {GridRowModel} replacement The row provided in a `{ _action: 'replace', row }` update.
*/
export const warnIfReplaceLosesPrototype = (oldRow, replacement) => {
if (process.env.NODE_ENV === 'production' || !oldRow) {
return;
}
const replacementProto = Object.getPrototypeOf(replacement);
if (replacementProto !== Object.prototype && replacementProto !== null) {
return;
}
const oldRowProto = Object.getPrototypeOf(oldRow);
if (oldRowProto === Object.prototype || oldRowProto === null) {
return;
}
// The message must stay free of row-specific details: `warnOnce()` caches by message, so
// interpolating the id would log once per row and grow the cache with every update.
warnOnce(["MUI X Data Grid: A plain object was provided as the `row` of a `_action: 'replace'` update, but the row it replaces is a class instance.", "Building the replacement with a spread (`{ _action: 'replace', row: { ...row } }`) creates a plain object, dropping the prototype chain and the private fields that `_action: 'replace'` exists to preserve.", "Pass the instance itself instead, for example `{ _action: 'replace', row }`.", 'For more detail, see https://mui.com/x/react-data-grid/row-updates/.']);
};
export const updateCacheWithNewRows = ({
previousCache,
getRowId,
updates,
groupKeys
}) => {
if (previousCache.updates.type === 'full') {
throw new Error(process.env.NODE_ENV !== "production" ? 'MUI X: Unable to prepare a partial update if a full update is not applied yet.' : _formatErrorMessage(86));
}
// Remove duplicate updates.
// A server can batch updates, and send several updates for the same row in one fn call.
const uniqueUpdates = new Map();
// Ids whose accumulated update is stored as the row instead of being merged into the
// existing one. The flag lives outside the row object so that it is never mutated.
const replaceIds = new Set();
updates.forEach(update => {
// A replace is provided as a `{ _action: 'replace', row }` envelope: the marker stays
// on the throwaway envelope and the inner row is used verbatim, so that the object
// provided by the caller is the one that ends up in the lookup.
const isReplace = isReplaceUpdate(update);
const row = isReplace ? getReplaceRow(update) : update;
const id = getRowIdFromRowModel(row, getRowId, 'A row was provided without id when calling updateRows():');
if (isReplace) {
// A replace resets whatever has been accumulated for that id.
uniqueUpdates.set(id, row);
replaceIds.add(id);
} else if (uniqueUpdates.has(id)) {
const accumulatedUpdate = uniqueUpdates.get(id);
if (process.env.NODE_ENV !== 'production' && replaceIds.has(id)) {
warnOnce(["MUI X Data Grid: A row was provided with `_action: 'replace'` but it is not the last update for that row in this batch.", 'The remaining updates are merged onto the replacement, so the row keeps its prototype but is neither the same object nor carries its `#private` fields, which a merge cannot copy.', 'Make the replace the last update for that row if `apiRef.current.getRow(id)` must return the object you passed in.', 'For more detail, see https://mui.com/x/react-data-grid/row-updates/.']);
}
// The merge keeps the accumulated entry's prototype and the id stays in `replaceIds`,
// so the result is still applied as a replace.
uniqueUpdates.set(id, mergeRowUpdate(accumulatedUpdate, 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),
groupKeys
};
const dataRowIdToModelLookup = _extends({}, previousCache.dataRowIdToModelLookup);
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];
return;
}
const oldRow = dataRowIdToModelLookup[id];
// Action === "replace"
// The bookkeeping is the same as for "modify" / "insert" below, only the way the row
// is stored differs: the object is stored as-is so that its prototype chain, private
// fields (#field) and reference identity are all preserved.
const isReplace = replaceIds.has(id);
if (isReplace) {
warnIfReplaceLosesPrototype(oldRow, partialRow);
}
// 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] = isReplace ? partialRow : mergeRowUpdate(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;
});
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,
updates: partialUpdates,
rowsBeforePartialUpdates: previousCache.rowsBeforePartialUpdates,
loadingPropBeforePartialUpdates: previousCache.loadingPropBeforePartialUpdates,
rowCountPropBeforePartialUpdates: previousCache.rowCountPropBeforePartialUpdates
};
};
export const minimalContentHeight = 'var(--DataGrid-overlayHeight, calc(var(--height) * 2))';
export function computeRowsUpdates(apiRef, updates, getRowId) {
const nonPinnedRowsUpdates = [];
updates.forEach(update => {
const isReplace = isReplaceUpdate(update);
const row = isReplace ? getReplaceRow(update) : update;
const id = getRowIdFromRowModel(row, getRowId, 'A row was provided without id when calling updateRows():');
const rowNode = gridRowNodeSelector(apiRef, id);
if (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) {
if (isReplace) {
warnIfReplaceLosesPrototype(prevModel, row);
pinnedRowsCache.idLookup[id] = row;
} else {
pinnedRowsCache.idLookup[id] = mergeRowUpdate(prevModel, update);
}
}
} else {
// The envelope is passed through untouched, `updateCacheWithNewRows` unwraps it.
nonPinnedRowsUpdates.push(update);
}
});
return nonPinnedRowsUpdates;
}
let warnedOnceInvalidRowHeight = false;
export const getValidRowHeight = (rowHeightProp, defaultRowHeight, warningMessage) => {
if (typeof rowHeightProp === 'number' && rowHeightProp > 0) {
return rowHeightProp;
}
if (process.env.NODE_ENV !== 'production' && !warnedOnceInvalidRowHeight && typeof rowHeightProp !== 'undefined' && rowHeightProp !== null) {
console.warn(warningMessage);
warnedOnceInvalidRowHeight = true;
}
return defaultRowHeight;
};
export const rowHeightWarning = [`MUI X: The \`rowHeight\` prop should be a number greater than 0.`, `The default value will be used instead.`].join('\n');