UNPKG

@mui/x-data-grid-premium

Version:

The Premium plan edition of the MUI X Data Grid Components.

360 lines (347 loc) 13.1 kB
"use strict"; 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;