@mui/x-data-grid-premium
Version:
The Premium plan edition of the MUI X Data Grid Components.
360 lines (347 loc) • 13.1 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.BatchRowUpdater = void 0;
exports.adjustTargetNode = adjustTargetNode;
exports.calculateTargetIndex = calculateTargetIndex;
exports.conditions = exports.collectAllLeafDescendants = void 0;
exports.determineOperationType = determineOperationType;
exports.findExistingGroupWithSameKey = findExistingGroupWithSameKey;
exports.getNodePathInTree = void 0;
exports.handleProcessRowUpdateError = handleProcessRowUpdateError;
exports.removeEmptyAncestors = removeEmptyAncestors;
var _xDataGridPro = require("@mui/x-data-grid-pro");
var _warning = require("@mui/x-internals/warning");
// TODO: Share these conditions with the executor by making the contexts similar
/**
* Reusable validation conditions for row reordering validation
*/
const conditions = exports.conditions = {
// Node type checks
isGroupToGroup: ctx => ctx.sourceNode.type === 'group' && ctx.targetNode.type === 'group',
isLeafToLeaf: ctx => ctx.sourceNode.type === 'leaf' && ctx.targetNode.type === 'leaf',
isLeafToGroup: ctx => ctx.sourceNode.type === 'leaf' && ctx.targetNode.type === 'group',
isGroupToLeaf: ctx => ctx.sourceNode.type === 'group' && ctx.targetNode.type === 'leaf',
// Drop position checks
isDropAbove: ctx => ctx.dropPosition === 'above',
isDropBelow: ctx => ctx.dropPosition === 'below',
// Depth checks
sameDepth: ctx => ctx.sourceNode.depth === ctx.targetNode.depth,
sourceDepthGreater: ctx => ctx.sourceNode.depth > ctx.targetNode.depth,
targetDepthIsSourceMinusOne: ctx => ctx.targetNode.depth === ctx.sourceNode.depth - 1,
// Parent checks
sameParent: ctx => ctx.sourceNode.parent === ctx.targetNode.parent,
// Node state checks
targetGroupExpanded: ctx => (ctx.targetNode.type === 'group' && ctx.targetNode.childrenExpanded) ?? false,
targetGroupCollapsed: ctx => ctx.targetNode.type === 'group' && !ctx.targetNode.childrenExpanded,
// Previous/Next node checks
hasPrevNode: ctx => ctx.prevNode !== null,
hasNextNode: ctx => ctx.nextNode !== null,
prevIsLeaf: ctx => ctx.prevNode?.type === 'leaf',
prevIsGroup: ctx => ctx.prevNode?.type === 'group',
nextIsLeaf: ctx => ctx.nextNode?.type === 'leaf',
nextIsGroup: ctx => ctx.nextNode?.type === 'group',
prevDepthEquals: (ctx, depth) => ctx.prevNode?.depth === depth,
prevDepthEqualsSource: ctx => ctx.prevNode?.depth === ctx.sourceNode.depth,
// Complex checks
prevBelongsToSource: ctx => {
if (!ctx.prevNode) {
return false;
}
// Check if prevNode.parent OR any of its ancestors === sourceNode.id
let currentId = ctx.prevNode.parent;
while (currentId) {
if (currentId === ctx.sourceNode.id) {
return true;
}
const node = ctx.rowTree[currentId];
if (!node) {
break;
}
currentId = node.parent;
}
return false;
},
// Position checks
isAdjacentPosition: ctx => {
const {
sourceRowIndex,
targetRowIndex,
dropPosition
} = ctx;
return dropPosition === 'above' && targetRowIndex === sourceRowIndex + 1 || dropPosition === 'below' && targetRowIndex === sourceRowIndex - 1;
},
// First child check
targetFirstChildIsGroupWithSourceDepth: ctx => {
if (ctx.targetNode.type !== 'group') {
return false;
}
const targetGroup = ctx.targetNode;
const firstChild = targetGroup.children?.[0] ? ctx.rowTree[targetGroup.children[0]] : null;
return firstChild?.type === 'group' && firstChild.depth === ctx.sourceNode.depth;
},
targetFirstChildDepthEqualsSource: ctx => {
if (ctx.targetNode.type !== 'group') {
return false;
}
const targetGroup = ctx.targetNode;
const firstChild = targetGroup.children?.[0] ? ctx.rowTree[targetGroup.children[0]] : null;
return firstChild ? firstChild.depth === ctx.sourceNode.depth : false;
}
};
function determineOperationType(sourceNode, targetNode) {
if (sourceNode.parent === targetNode.parent) {
return 'same-parent-swap';
}
if (sourceNode.type === 'leaf') {
return 'cross-parent-leaf';
}
return 'cross-parent-group';
}
function calculateTargetIndex(sourceNode, targetNode, isLastChild, rowTree) {
if (sourceNode.parent === targetNode.parent && !isLastChild) {
// Same parent: find target's position in parent's children
const parent = rowTree[sourceNode.parent];
return parent.children.findIndex(id => id === targetNode.id);
}
if (isLastChild) {
// Append at the end
const targetParent = rowTree[targetNode.parent];
return targetParent.children.length;
}
// Find position in target parent
const targetParent = rowTree[targetNode.parent];
const targetIndex = targetParent.children.findIndex(id => id === targetNode.id);
return targetIndex >= 0 ? targetIndex : 0;
}
// Get the path from a node to the root in the tree
const getNodePathInTree = ({
id,
tree
}) => {
const path = [];
let node = tree[id];
while (node.id !== _xDataGridPro.GRID_ROOT_GROUP_ID) {
path.push({
field: node.type === 'leaf' ? null : node.groupingField,
key: node.groupingKey
});
node = tree[node.parent];
}
path.reverse();
return path;
};
// Recursively collect all leaf node IDs from a group
exports.getNodePathInTree = getNodePathInTree;
const collectAllLeafDescendants = (groupNode, tree) => {
const leafIds = [];
const collectFromNode = nodeId => {
const node = tree[nodeId];
if (node.type === 'leaf') {
leafIds.push(nodeId);
} else if (node.type === 'group') {
node.children.forEach(collectFromNode);
}
};
groupNode.children.forEach(collectFromNode);
return leafIds;
};
/**
* Adjusts the target node based on specific reorder scenarios and constraints.
*
* This function applies scenario-specific logic to find the actual target node
* for operations, handling cases like:
* - Moving to collapsed groups
* - Depth-based adjustments
* - End-of-list positioning
*
* @param sourceNode The node being moved
* @param targetNode The initial target node
* @param targetIndex The index of the target node in the visible rows
* @param placeholderIndex The index where the placeholder appears
* @param sortedFilteredRowIds Array of visible row IDs in display order
* @param apiRef Reference to the grid API
* @returns Object containing the adjusted target node and last child flag
*/
exports.collectAllLeafDescendants = collectAllLeafDescendants;
function adjustTargetNode(sourceNode, targetNode, targetIndex, placeholderIndex, sortedFilteredRowIds, apiRef) {
let adjustedTargetNode = targetNode;
let isLastChild = false;
// Handle end-of-list case
if (placeholderIndex >= sortedFilteredRowIds.length && sortedFilteredRowIds.length > 0) {
isLastChild = true;
}
// Case A and B adjustment: Move to last child of parent where target should be the node above
if (targetNode.type === 'group' && sourceNode.parent !== targetNode.parent && sourceNode.depth > targetNode.depth) {
// Find the first node with the same depth as source before target and quit early if a
// node with depth < source.depth is found
let i = targetIndex - 1;
while (i >= 0) {
const node = apiRef.current.getRowNode(sortedFilteredRowIds[i]);
if (node && node.depth < sourceNode.depth) {
break;
}
if (node && node.depth === sourceNode.depth) {
adjustedTargetNode = node;
break;
}
i -= 1;
}
}
// Case D adjustment: Leaf to group where we need previous leaf
if (sourceNode.type === 'leaf' && targetNode.type === 'group' && targetNode.depth < sourceNode.depth) {
isLastChild = true;
const prevIndex = placeholderIndex - 1;
if (prevIndex >= 0) {
const prevRowId = sortedFilteredRowIds[prevIndex];
const leafTargetNode = (0, _xDataGridPro.gridRowNodeSelector)(apiRef, prevRowId);
if (leafTargetNode && leafTargetNode.type === 'leaf') {
adjustedTargetNode = leafTargetNode;
}
}
}
return {
adjustedTargetNode,
isLastChild
};
}
/**
* Finds an existing group node with the same groupingKey and groupingField under a parent.
*
* @param parentNode - The parent group node to search in
* @param groupingKey - The grouping key to match
* @param groupingField - The grouping field to match
* @param tree - The row tree configuration
* @returns The existing group node if found, null otherwise
*/
function findExistingGroupWithSameKey(parentNode, groupingKey, groupingField, tree) {
for (const childId of parentNode.children) {
const childNode = tree[childId];
if (childNode && childNode.type === 'group' && childNode.groupingKey === groupingKey && childNode.groupingField === groupingField) {
return childNode;
}
}
return null;
}
/**
* Removes empty ancestor groups from the tree after a row move operation.
* Walks up the tree from the given group, removing any empty groups encountered.
*
* @param groupId - The ID of the group to start checking from
* @param tree - The row tree configuration
* @param removedGroups - Set to track which groups have been removed
* @returns The number of root-level groups that were removed
*/
function removeEmptyAncestors(groupId, tree, removedGroups) {
let rootLevelRemovals = 0;
let currentGroupId = groupId;
while (currentGroupId && currentGroupId !== _xDataGridPro.GRID_ROOT_GROUP_ID) {
const group = tree[currentGroupId];
if (!group) {
break;
}
const remainingChildren = group.children.filter(childId => !removedGroups.has(childId));
if (remainingChildren.length > 0) {
break;
}
if (group.depth === 0) {
rootLevelRemovals += 1;
}
removedGroups.add(currentGroupId);
currentGroupId = group.parent;
}
return rootLevelRemovals;
}
function handleProcessRowUpdateError(error, onProcessRowUpdateError) {
if (onProcessRowUpdateError) {
onProcessRowUpdateError(error);
} else if (process.env.NODE_ENV !== 'production') {
(0, _warning.warnOnce)(['MUI X: A call to `processRowUpdate()` threw an error which was not handled because `onProcessRowUpdateError()` is missing.', 'To handle the error pass a callback to the `onProcessRowUpdateError()` prop, for example `<DataGrid onProcessRowUpdateError={(error) => ...} />`.', 'For more detail, see https://mui.com/x/react-data-grid/editing/persistence/.'], 'error');
}
}
/**
* Handles batch row updates with partial failure tracking.
*
* This class is designed for operations that need to update multiple rows
* atomically (like moving entire groups), while gracefully handling cases
* where some updates succeed and others fail.
*
* @example
* ```tsx
* const updater = new BatchRowUpdater(processRowUpdate, onError);
*
* // Queue multiple updates
* updater.queueUpdate('row1', originalRow1, newRow1);
* updater.queueUpdate('row2', originalRow2, newRow2);
*
* // Execute all updates
* const { successful, failed, updates } = await updater.executeAll();
*
* // Handle results
* if (successful.length > 0) {
* apiRef.current.updateRows(updates);
* }
* ```
*/
class BatchRowUpdater {
rowsToUpdate = new Map();
originalRows = new Map();
successfulRowIds = new Set();
failedRowIds = new Set();
pendingRowUpdates = [];
constructor(processRowUpdate, onProcessRowUpdateError) {
this.processRowUpdate = processRowUpdate;
this.onProcessRowUpdateError = onProcessRowUpdateError;
}
queueUpdate(rowId, originalRow, updatedRow) {
this.originalRows.set(rowId, originalRow);
this.rowsToUpdate.set(rowId, updatedRow);
}
async executeAll() {
const rowIds = Array.from(this.rowsToUpdate.keys());
if (rowIds.length === 0) {
return {
successful: [],
failed: [],
updates: []
};
}
// Handle each row update, tracking success/failure
const handleRowUpdate = async rowId => {
const newRow = this.rowsToUpdate.get(rowId);
const oldRow = this.originalRows.get(rowId);
try {
if (typeof this.processRowUpdate === 'function') {
const params = {
rowId,
previousRow: oldRow,
updatedRow: newRow
};
const finalRow = await this.processRowUpdate(newRow, oldRow, params);
this.pendingRowUpdates.push(finalRow || newRow);
this.successfulRowIds.add(rowId);
} else {
this.pendingRowUpdates.push(newRow);
this.successfulRowIds.add(rowId);
}
} catch (error) {
this.failedRowIds.add(rowId);
handleProcessRowUpdateError(error, this.onProcessRowUpdateError);
}
};
// Use Promise.all with wrapped promises to avoid Promise.allSettled (browser support)
const promises = rowIds.map(rowId => {
return new Promise(resolve => {
handleRowUpdate(rowId).then(resolve).catch(resolve);
});
});
await Promise.all(promises);
return {
successful: Array.from(this.successfulRowIds),
failed: Array.from(this.failedRowIds),
updates: this.pendingRowUpdates
};
}
}
exports.BatchRowUpdater = BatchRowUpdater;