@mui/x-data-grid-pro 8.18.0 → 8.20.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (85) hide show
  1. package/CHANGELOG.md +175 -0
  2. package/DataGridPro/DataGridPro.js +42 -8
  3. package/DataGridPro/useDataGridProComponent.js +3 -2
  4. package/components/GridDetailPanelToggleCell.js +0 -10
  5. package/components/GridRowReorderCell.js +15 -13
  6. package/components/GridTreeDataGroupingCell.js +0 -10
  7. package/components/headerFiltering/GridHeaderFilterCell.js +2 -3
  8. package/esm/DataGridPro/DataGridPro.js +42 -8
  9. package/esm/DataGridPro/useDataGridProComponent.js +4 -3
  10. package/esm/components/GridDetailPanelToggleCell.js +0 -10
  11. package/esm/components/GridRowReorderCell.js +15 -13
  12. package/esm/components/GridTreeDataGroupingCell.js +0 -10
  13. package/esm/components/headerFiltering/GridHeaderFilterCell.js +2 -3
  14. package/esm/hooks/features/columnPinning/useGridColumnPinningPreProcessors.js +4 -0
  15. package/esm/hooks/features/dataSource/useGridDataSourceBasePro.js +1 -1
  16. package/esm/hooks/features/detailPanel/useGridDetailPanel.js +6 -5
  17. package/esm/hooks/features/infiniteLoader/useGridInfiniteLoader.js +4 -4
  18. package/esm/hooks/features/rowReorder/commonReorderConditions.d.ts +30 -0
  19. package/esm/hooks/features/rowReorder/commonReorderConditions.js +78 -0
  20. package/esm/hooks/features/rowReorder/index.d.ts +2 -1
  21. package/esm/hooks/features/rowReorder/index.js +2 -1
  22. package/esm/hooks/features/rowReorder/models.d.ts +17 -0
  23. package/esm/hooks/features/rowReorder/models.js +1 -0
  24. package/esm/hooks/features/rowReorder/reorderExecutor.d.ts +27 -0
  25. package/esm/hooks/features/rowReorder/reorderExecutor.js +29 -0
  26. package/esm/hooks/features/rowReorder/reorderValidator.d.ts +12 -0
  27. package/esm/hooks/features/rowReorder/reorderValidator.js +14 -0
  28. package/esm/hooks/features/rowReorder/types.d.ts +25 -0
  29. package/esm/hooks/features/rowReorder/types.js +1 -0
  30. package/esm/hooks/features/rowReorder/useGridRowReorder.d.ts +1 -1
  31. package/esm/hooks/features/rowReorder/useGridRowReorder.js +171 -82
  32. package/esm/hooks/features/rowReorder/utils.d.ts +82 -0
  33. package/esm/hooks/features/rowReorder/utils.js +259 -0
  34. package/esm/hooks/features/rows/useGridRowsOverridableMethods.d.ts +7 -0
  35. package/esm/hooks/features/rows/useGridRowsOverridableMethods.js +59 -0
  36. package/esm/hooks/features/serverSideLazyLoader/useGridInfiniteLoadingIntersection.js +3 -3
  37. package/esm/hooks/features/treeData/treeDataReorderExecutor.d.ts +11 -0
  38. package/esm/hooks/features/treeData/treeDataReorderExecutor.js +534 -0
  39. package/esm/hooks/features/treeData/treeDataReorderValidator.d.ts +2 -0
  40. package/esm/hooks/features/treeData/treeDataReorderValidator.js +35 -0
  41. package/esm/hooks/features/treeData/useGridTreeData.d.ts +3 -3
  42. package/esm/hooks/features/treeData/useGridTreeData.js +49 -4
  43. package/esm/hooks/features/treeData/utils.d.ts +8 -0
  44. package/esm/hooks/features/treeData/utils.js +96 -0
  45. package/esm/index.js +1 -1
  46. package/esm/internals/index.d.ts +8 -0
  47. package/esm/internals/index.js +6 -0
  48. package/esm/models/dataGridProProps.d.ts +32 -4
  49. package/esm/models/gridRowOrderChangeParams.d.ts +29 -5
  50. package/hooks/features/columnPinning/useGridColumnPinningPreProcessors.js +4 -0
  51. package/hooks/features/dataSource/useGridDataSourceBasePro.js +1 -1
  52. package/hooks/features/detailPanel/useGridDetailPanel.js +5 -4
  53. package/hooks/features/infiniteLoader/useGridInfiniteLoader.js +2 -2
  54. package/hooks/features/rowReorder/commonReorderConditions.d.ts +30 -0
  55. package/hooks/features/rowReorder/commonReorderConditions.js +84 -0
  56. package/hooks/features/rowReorder/index.d.ts +2 -1
  57. package/hooks/features/rowReorder/models.d.ts +17 -0
  58. package/hooks/features/rowReorder/models.js +5 -0
  59. package/hooks/features/rowReorder/reorderExecutor.d.ts +27 -0
  60. package/hooks/features/rowReorder/reorderExecutor.js +37 -0
  61. package/hooks/features/rowReorder/reorderValidator.d.ts +12 -0
  62. package/hooks/features/rowReorder/reorderValidator.js +21 -0
  63. package/hooks/features/rowReorder/types.d.ts +25 -0
  64. package/hooks/features/rowReorder/types.js +5 -0
  65. package/hooks/features/rowReorder/useGridRowReorder.d.ts +1 -1
  66. package/hooks/features/rowReorder/useGridRowReorder.js +169 -80
  67. package/hooks/features/rowReorder/utils.d.ts +82 -0
  68. package/hooks/features/rowReorder/utils.js +286 -0
  69. package/hooks/features/rows/useGridRowsOverridableMethods.d.ts +7 -0
  70. package/hooks/features/rows/useGridRowsOverridableMethods.js +67 -0
  71. package/hooks/features/serverSideLazyLoader/useGridInfiniteLoadingIntersection.js +3 -3
  72. package/hooks/features/treeData/treeDataReorderExecutor.d.ts +11 -0
  73. package/hooks/features/treeData/treeDataReorderExecutor.js +541 -0
  74. package/hooks/features/treeData/treeDataReorderValidator.d.ts +2 -0
  75. package/hooks/features/treeData/treeDataReorderValidator.js +41 -0
  76. package/hooks/features/treeData/useGridTreeData.d.ts +3 -3
  77. package/hooks/features/treeData/useGridTreeData.js +48 -3
  78. package/hooks/features/treeData/utils.d.ts +8 -0
  79. package/hooks/features/treeData/utils.js +109 -0
  80. package/index.js +1 -1
  81. package/internals/index.d.ts +8 -0
  82. package/internals/index.js +53 -1
  83. package/models/dataGridProProps.d.ts +32 -4
  84. package/models/gridRowOrderChangeParams.d.ts +29 -5
  85. package/package.json +4 -4
@@ -4,22 +4,25 @@ import _extends from "@babel/runtime/helpers/esm/extends";
4
4
  import * as React from 'react';
5
5
  import useTimeout from '@mui/utils/useTimeout';
6
6
  import composeClasses from '@mui/utils/composeClasses';
7
- import { useGridLogger, useGridEvent, getDataGridUtilityClass, useGridSelector, gridSortModelSelector, useGridEventPriority, gridRowNodeSelector, gridRowMaximumTreeDepthSelector, useGridApiMethod } from '@mui/x-data-grid';
8
- import { gridEditRowsStateSelector, useGridRegisterPipeProcessor, gridExpandedSortedRowIndexLookupSelector } from '@mui/x-data-grid/internals';
7
+ import { useGridLogger, useGridEvent, getDataGridUtilityClass, useGridSelector, gridSortModelSelector, useGridEventPriority, gridRowNodeSelector, gridRowMaximumTreeDepthSelector, useGridApiMethod, gridExpandedSortedRowIdsSelector, gridRowTreeSelector, gridExpandedSortedRowIndexLookupSelector, GRID_ROOT_GROUP_ID } from '@mui/x-data-grid';
8
+ import { gridEditRowsStateSelector, useGridRegisterPipeProcessor } from '@mui/x-data-grid/internals';
9
9
  import { GRID_REORDER_COL_DEF } from "./gridRowReorderColDef.js";
10
+ import { findCellElement } from "./utils.js";
10
11
  const EMPTY_REORDER_STATE = {
11
12
  previousTargetId: null,
12
13
  dragDirection: null,
13
14
  previousDropPosition: null
14
15
  };
16
+ const TIMEOUT_CLEAR_BUFFER_PX = 5;
17
+ const EMPTY_TIMEOUT_INFO = {
18
+ rowId: null
19
+ };
15
20
  const useUtilityClasses = ownerState => {
16
21
  const {
17
22
  classes
18
23
  } = ownerState;
19
24
  const slots = {
20
25
  rowDragging: ['row--dragging'],
21
- rowDropAbove: ['row--dropAbove'],
22
- rowDropBelow: ['row--dropBelow'],
23
26
  rowBeingDragged: ['row--beingDragged']
24
27
  };
25
28
  return composeClasses(slots, getDataGridUtilityClass, classes);
@@ -40,14 +43,12 @@ export const useGridRowReorder = (apiRef, props) => {
40
43
  const dragRowNode = React.useRef(null);
41
44
  const originRowIndex = React.useRef(null);
42
45
  const removeDnDStylesTimeout = React.useRef(undefined);
43
- const previousDropIndicatorRef = React.useRef(null);
44
46
  const ownerState = {
45
47
  classes: props.classes
46
48
  };
47
49
  const classes = useUtilityClasses(ownerState);
48
50
  const [dragRowId, setDragRowId] = React.useState('');
49
- const sortedRowIndexLookup = useGridSelector(apiRef, gridExpandedSortedRowIndexLookupSelector);
50
- const timeoutRowId = React.useRef('');
51
+ const timeoutInfoRef = React.useRef(EMPTY_TIMEOUT_INFO);
51
52
  const timeout = useTimeout();
52
53
  const previousReorderState = React.useRef(EMPTY_REORDER_STATE);
53
54
  const dropTarget = React.useRef({
@@ -62,26 +63,30 @@ export const useGridRowReorder = (apiRef, props) => {
62
63
  }, []);
63
64
 
64
65
  // TODO: remove sortModel check once row reorder is sorting compatible
65
- // remove treeData check once row reorder is treeData compatible
66
66
  const isRowReorderDisabled = React.useMemo(() => {
67
- return !props.rowReordering || !!sortModel.length || props.treeData;
68
- }, [props.rowReordering, sortModel, props.treeData]);
69
- const applyDropIndicator = React.useCallback((targetRowId, position) => {
70
- // Remove existing drop indicator from previous target
71
- if (previousDropIndicatorRef.current) {
72
- previousDropIndicatorRef.current.classList.remove(classes.rowDropAbove, classes.rowDropBelow);
73
- previousDropIndicatorRef.current = null;
74
- }
75
-
76
- // Apply new drop indicator
77
- if (targetRowId !== undefined && position !== null) {
78
- const targetRow = apiRef.current.rootElementRef?.current?.querySelector(`[data-id="${targetRowId}"]`);
79
- if (targetRow) {
80
- targetRow.classList.add(position === 'above' ? classes.rowDropAbove : classes.rowDropBelow);
81
- previousDropIndicatorRef.current = targetRow;
67
+ return !props.rowReordering || !!sortModel.length;
68
+ }, [props.rowReordering, sortModel]);
69
+ const calculateDropPosition = React.useCallback(event => {
70
+ // For tree data, we need to find the cell element to avoid flickerings on top 20% selection
71
+ const targetElement = props.treeData ? findCellElement(event.target) : event.target;
72
+ const targetRect = targetElement.getBoundingClientRect();
73
+ const relativeY = Math.floor(event.clientY - targetRect.top);
74
+ if (props.treeData) {
75
+ // For tree data: top 20% = above, middle 60% = over, bottom 20% = below
76
+ const topThreshold = targetRect.height * 0.2;
77
+ const bottomThreshold = targetRect.height * 0.8;
78
+ if (relativeY < topThreshold) {
79
+ return 'above';
82
80
  }
81
+ if (relativeY > bottomThreshold) {
82
+ return 'below';
83
+ }
84
+ return 'inside';
83
85
  }
84
- }, [apiRef, classes]);
86
+ // For flat data and row grouping: split at midpoint
87
+ const midPoint = targetRect.height / 2;
88
+ return relativeY < midPoint ? 'above' : 'below';
89
+ }, [props.treeData]);
85
90
  const applyDraggedState = React.useCallback((rowId, isDragged) => {
86
91
  if (rowId) {
87
92
  const draggedRow = apiRef.current.rootElementRef?.current?.querySelector(`[data-id="${rowId}"]`);
@@ -94,7 +99,7 @@ export const useGridRowReorder = (apiRef, props) => {
94
99
  }
95
100
  }
96
101
  }, [apiRef, classes.rowBeingDragged]);
97
- const applyRowAnimation = React.useCallback(callback => {
102
+ const applyRowAnimation = React.useCallback(async callback => {
98
103
  const rootElement = apiRef.current.rootElementRef?.current;
99
104
  if (!rootElement) {
100
105
  return;
@@ -111,7 +116,7 @@ export const useGridRowReorder = (apiRef, props) => {
111
116
  initialPositions.set(rowId, row.getBoundingClientRect());
112
117
  }
113
118
  });
114
- callback();
119
+ await callback();
115
120
 
116
121
  // Use `requestAnimationFrame` to ensure DOM has updated
117
122
  requestAnimationFrame(() => {
@@ -153,15 +158,20 @@ export const useGridRowReorder = (apiRef, props) => {
153
158
  if (isRowReorderDisabled || Object.keys(editRowsState).length !== 0) {
154
159
  return;
155
160
  }
156
- if (timeoutRowId.current) {
161
+ if (timeoutInfoRef.current) {
157
162
  timeout.clear();
158
- timeoutRowId.current = '';
163
+ timeoutInfoRef.current = EMPTY_TIMEOUT_INFO;
159
164
  }
160
165
  logger.debug(`Start dragging row ${params.id}`);
161
166
  // Prevent drag events propagation.
162
167
  // For more information check here https://github.com/mui/mui-x/issues/2680.
163
168
  event.stopPropagation();
164
- apiRef.current.setRowDragActive(true);
169
+ apiRef.current.setState(state => _extends({}, state, {
170
+ rowReorder: _extends({}, state.rowReorder, {
171
+ isActive: true,
172
+ draggedRowId: params.id
173
+ })
174
+ }));
165
175
  dragRowNode.current = event.currentTarget;
166
176
  // Apply cell-level dragging class to the drag handle
167
177
  dragRowNode.current.classList.add(classes.rowDragging);
@@ -172,9 +182,10 @@ export const useGridRowReorder = (apiRef, props) => {
172
182
  removeDnDStylesTimeout.current = setTimeout(() => {
173
183
  dragRowNode.current.classList.remove(classes.rowDragging);
174
184
  });
185
+ const sortedRowIndexLookup = gridExpandedSortedRowIndexLookupSelector(apiRef);
175
186
  originRowIndex.current = sortedRowIndexLookup[params.id];
176
187
  apiRef.current.setCellFocus(params.id, GRID_REORDER_COL_DEF.field);
177
- }, [apiRef, isRowReorderDisabled, logger, classes.rowDragging, applyDraggedState, sortedRowIndexLookup, timeout]);
188
+ }, [apiRef, isRowReorderDisabled, logger, classes.rowDragging, applyDraggedState, timeout]);
178
189
  const handleDragOver = React.useCallback((params, event) => {
179
190
  if (dragRowId === '') {
180
191
  return;
@@ -184,34 +195,36 @@ export const useGridRowReorder = (apiRef, props) => {
184
195
  if (!sourceNode || !targetNode || targetNode.type === 'footer' || targetNode.type === 'pinnedRow' || !event.target) {
185
196
  return;
186
197
  }
187
-
188
- // Find the relative 'y' mouse position based on the event.target
189
- const targetRect = event.target.getBoundingClientRect();
190
- const relativeY = Math.floor(event.clientY - targetRect.top);
191
- const midPoint = Math.floor(targetRect.height / 2);
192
198
  logger.debug(`Dragging over row ${params.id}`);
193
199
  event.preventDefault();
194
200
  // Prevent drag events propagation.
195
201
  // For more information check here https://github.com/mui/mui-x/issues/2680.
196
202
  event.stopPropagation();
197
- if (timeoutRowId.current && timeoutRowId.current !== params.id) {
203
+ if (timeoutInfoRef.current && (timeoutInfoRef.current.rowId !== params.id ||
204
+ // Avoid accidental opening of node when the user is moving over a row
205
+ event.clientY > timeoutInfoRef.current.clientY + TIMEOUT_CLEAR_BUFFER_PX || event.clientY < timeoutInfoRef.current.clientY - TIMEOUT_CLEAR_BUFFER_PX || event.clientX > timeoutInfoRef.current.clientX + TIMEOUT_CLEAR_BUFFER_PX || event.clientX < timeoutInfoRef.current.clientX - TIMEOUT_CLEAR_BUFFER_PX)) {
198
206
  timeout.clear();
199
- timeoutRowId.current = '';
207
+ timeoutInfoRef.current = EMPTY_TIMEOUT_INFO;
200
208
  }
201
- if (targetNode.type === 'group' && targetNode.depth < sourceNode.depth && !targetNode.childrenExpanded && !timeoutRowId.current) {
209
+
210
+ // Calculate drop position using new logic
211
+ const dropPosition = calculateDropPosition(event);
212
+ if (targetNode.type === 'group' && !targetNode.childrenExpanded && !timeoutInfoRef.current.rowId && targetNode.id !== sourceNode.id && (dropPosition === 'inside' || targetNode.depth < sourceNode.depth)) {
202
213
  timeout.start(500, () => {
203
214
  const rowNode = gridRowNodeSelector(apiRef, params.id);
204
215
  // TODO: Handle `dataSource` case with https://github.com/mui/mui-x/issues/18947
205
216
  apiRef.current.setRowChildrenExpansion(params.id, !rowNode.childrenExpanded);
206
217
  });
207
- timeoutRowId.current = params.id;
218
+ timeoutInfoRef.current = {
219
+ rowId: params.id,
220
+ clientY: event.clientY,
221
+ clientX: event.clientX
222
+ };
208
223
  return;
209
224
  }
225
+ const sortedRowIndexLookup = gridExpandedSortedRowIndexLookupSelector(apiRef);
210
226
  const targetRowIndex = sortedRowIndexLookup[params.id];
211
227
  const sourceRowIndex = sortedRowIndexLookup[dragRowId];
212
-
213
- // Determine drop position based on relativeY position within the row
214
- const dropPosition = relativeY < midPoint ? 'above' : 'below';
215
228
  const currentReorderState = {
216
229
  dragDirection: targetRowIndex < sourceRowIndex ? 'up' : 'down',
217
230
  previousTargetId: params.id,
@@ -224,7 +237,7 @@ export const useGridRowReorder = (apiRef, props) => {
224
237
 
225
238
  // Check if this is an adjacent position
226
239
  const isAdjacentPosition = dropPosition === 'above' && targetRowIndex === sourceRowIndex + 1 || dropPosition === 'below' && targetRowIndex === sourceRowIndex - 1;
227
- const validatedIndex = apiRef.current.unstable_applyPipeProcessors('getRowReorderTargetIndex', -1, {
240
+ const isRowReorderValid = apiRef.current.unstable_applyPipeProcessors('isRowReorderValid', false, {
228
241
  sourceRowId: dragRowId,
229
242
  targetRowId: params.id,
230
243
  dropPosition,
@@ -232,13 +245,21 @@ export const useGridRowReorder = (apiRef, props) => {
232
245
  });
233
246
 
234
247
  // Show drop indicator for valid drops OR adjacent positions OR same node
235
- if (validatedIndex !== -1 || isAdjacentPosition || isSameNode) {
248
+ if (isRowReorderValid || isAdjacentPosition || isSameNode) {
236
249
  dropTarget.current = {
237
250
  targetRowId: params.id,
238
251
  targetRowIndex,
239
252
  dropPosition
240
253
  };
241
- applyDropIndicator(params.id, dropPosition);
254
+ // Update state with drop target
255
+ apiRef.current.setState(state => _extends({}, state, {
256
+ rowReorder: _extends({}, state.rowReorder, {
257
+ dropTarget: {
258
+ rowId: params.id,
259
+ position: dropPosition
260
+ }
261
+ })
262
+ }));
242
263
  } else {
243
264
  // Clear indicators for invalid drops
244
265
  dropTarget.current = {
@@ -246,7 +267,12 @@ export const useGridRowReorder = (apiRef, props) => {
246
267
  targetRowIndex: null,
247
268
  dropPosition: null
248
269
  };
249
- applyDropIndicator(null, null);
270
+ // Clear state drop target
271
+ apiRef.current.setState(state => _extends({}, state, {
272
+ rowReorder: _extends({}, state.rowReorder, {
273
+ dropTarget: undefined
274
+ })
275
+ }));
250
276
  }
251
277
  previousReorderState.current = currentReorderState;
252
278
  }
@@ -257,16 +283,16 @@ export const useGridRowReorder = (apiRef, props) => {
257
283
  } else {
258
284
  event.dataTransfer.dropEffect = 'copy';
259
285
  }
260
- }, [dragRowId, apiRef, logger, timeout, sortedRowIndexLookup, applyDropIndicator]);
261
- const handleDragEnd = React.useCallback((_, event) => {
286
+ }, [dragRowId, apiRef, logger, timeout, calculateDropPosition]);
287
+ const handleDragEnd = React.useCallback(async (_, event) => {
262
288
  // Call the gridEditRowsStateSelector directly to avoid infnite loop
263
289
  const editRowsState = gridEditRowsStateSelector(apiRef);
264
290
  if (dragRowId === '' || isRowReorderDisabled || Object.keys(editRowsState).length !== 0) {
265
291
  return;
266
292
  }
267
- if (timeoutRowId.current) {
293
+ if (timeoutInfoRef.current) {
268
294
  timeout.clear();
269
- timeoutRowId.current = '';
295
+ timeoutInfoRef.current = EMPTY_TIMEOUT_INFO;
270
296
  }
271
297
  logger.debug('End dragging row');
272
298
  event.preventDefault();
@@ -278,11 +304,6 @@ export const useGridRowReorder = (apiRef, props) => {
278
304
  const dragDirection = previousReorderState.current.dragDirection;
279
305
  previousReorderState.current = EMPTY_REORDER_STATE;
280
306
 
281
- // Clear visual indicators and dragged state
282
- applyDropIndicator(null, null);
283
- applyDraggedState(dragRowId, false);
284
- apiRef.current.setRowDragActive(false);
285
-
286
307
  // Check if the row was dropped outside the grid.
287
308
  if (!event.dataTransfer || event.dataTransfer.dropEffect === 'none') {
288
309
  // Reset drop target state
@@ -293,29 +314,80 @@ export const useGridRowReorder = (apiRef, props) => {
293
314
  };
294
315
  originRowIndex.current = null;
295
316
  setDragRowId('');
317
+ // Clear visual indicators and dragged state
318
+ applyDraggedState(dragRowId, false);
319
+ apiRef.current.setState(state => _extends({}, state, {
320
+ rowReorder: {
321
+ isActive: false,
322
+ draggedRowId: null
323
+ }
324
+ }));
296
325
  return;
297
326
  }
298
327
  if (dropTarget.current.targetRowIndex !== null && dropTarget.current.targetRowId !== null) {
299
- const sourceRowIndex = originRowIndex.current;
300
- const targetRowIndex = dropTarget.current.targetRowIndex;
301
- const validatedIndex = apiRef.current.unstable_applyPipeProcessors('getRowReorderTargetIndex', targetRowIndex, {
328
+ const isRowReorderValid = apiRef.current.unstable_applyPipeProcessors('isRowReorderValid', false, {
302
329
  sourceRowId: dragRowId,
303
330
  targetRowId: dropTarget.current.targetRowId,
304
331
  dropPosition: dropTarget.current.dropPosition,
305
332
  dragDirection: dragDirection
306
333
  });
307
- if (validatedIndex !== -1) {
308
- applyRowAnimation(() => {
309
- apiRef.current.setRowIndex(dragRowId, validatedIndex);
334
+ if (isRowReorderValid) {
335
+ try {
336
+ const rowTree = gridRowTreeSelector(apiRef);
337
+ const sourceNode = gridRowNodeSelector(apiRef, dragRowId);
338
+ if (!sourceNode) {
339
+ throw new Error(`MUI X: No row node found for id #${dragRowId}`);
340
+ }
310
341
 
311
- // Emit the rowOrderChange event only once when the reordering stops.
312
- const rowOrderChangeParams = {
313
- row: apiRef.current.getRow(dragRowId),
314
- targetIndex: validatedIndex,
315
- oldIndex: sourceRowIndex
316
- };
317
- apiRef.current.publishEvent('rowOrderChange', rowOrderChangeParams);
318
- });
342
+ // Calculate oldParent and oldIndex
343
+ const oldParent = sourceNode.parent;
344
+ const oldParentNode = rowTree[oldParent];
345
+ const oldIndexInParent = oldParentNode.children.indexOf(dragRowId) ?? originRowIndex.current;
346
+ await applyRowAnimation(async () => {
347
+ await apiRef.current.setRowPosition(dragRowId, dropTarget.current.targetRowId, dropTarget.current.dropPosition);
348
+ const updatedTree = gridRowTreeSelector(apiRef);
349
+ const updatedNode = updatedTree[dragRowId];
350
+ if (!updatedNode) {
351
+ throw new Error(`MUI X: Row node for id #${dragRowId} not found after move`);
352
+ }
353
+ const newParent = updatedNode.parent;
354
+ const newParentNode = updatedTree[newParent];
355
+ const newIndexInParent = newParentNode.children.indexOf(dragRowId);
356
+
357
+ // Only emit event and clear state after successful reorder
358
+ const rowOrderChangeParams = {
359
+ row: apiRef.current.getRow(dragRowId),
360
+ oldIndex: oldIndexInParent,
361
+ targetIndex: newIndexInParent,
362
+ oldParent: oldParent === GRID_ROOT_GROUP_ID ? null : oldParent,
363
+ newParent: newParent === GRID_ROOT_GROUP_ID ? null : newParent
364
+ };
365
+ applyDraggedState(dragRowId, false);
366
+ apiRef.current.setState(state => _extends({}, state, {
367
+ rowReorder: {
368
+ isActive: false,
369
+ draggedRowId: null
370
+ }
371
+ }));
372
+ apiRef.current.publishEvent('rowOrderChange', rowOrderChangeParams);
373
+ });
374
+ } catch (error) {
375
+ // Handle error: reset visual state but don't publish success event
376
+ applyDraggedState(dragRowId, false);
377
+ apiRef.current.setState(state => _extends({}, state, {
378
+ rowReorder: {
379
+ isActive: false,
380
+ draggedRowId: null
381
+ }
382
+ }));
383
+ }
384
+ } else {
385
+ applyDraggedState(dragRowId, false);
386
+ apiRef.current.setState(state => _extends({}, state, {
387
+ rowReorder: _extends({}, state.rowReorder, {
388
+ dropTarget: undefined
389
+ })
390
+ }));
319
391
  }
320
392
  }
321
393
 
@@ -326,8 +398,9 @@ export const useGridRowReorder = (apiRef, props) => {
326
398
  dropPosition: null
327
399
  };
328
400
  setDragRowId('');
329
- }, [apiRef, dragRowId, isRowReorderDisabled, logger, applyDropIndicator, applyDraggedState, timeout, applyRowAnimation]);
330
- const getRowReorderTargetIndex = React.useCallback((initialValue, {
401
+ }, [apiRef, dragRowId, isRowReorderDisabled, logger, applyDraggedState, timeout, applyRowAnimation]);
402
+ const isValidRowReorderProp = props.isValidRowReorder;
403
+ const isRowReorderValid = React.useCallback((initialValue, {
331
404
  sourceRowId,
332
405
  targetRowId,
333
406
  dropPosition,
@@ -336,27 +409,43 @@ export const useGridRowReorder = (apiRef, props) => {
336
409
  if (gridRowMaximumTreeDepthSelector(apiRef) > 1) {
337
410
  return initialValue;
338
411
  }
412
+ const sortedRowIndexLookup = gridExpandedSortedRowIndexLookupSelector(apiRef);
339
413
  const targetRowIndex = sortedRowIndexLookup[targetRowId];
340
414
  const sourceRowIndex = sortedRowIndexLookup[sourceRowId];
341
415
 
342
- // Check if this drop would result in no actual movement
416
+ // Apply internal validation: check if this drop would result in no actual movement
343
417
  const isAdjacentNode = dropPosition === 'above' && targetRowIndex === sourceRowIndex + 1 ||
344
418
  // dragging to immediately below (above next row)
345
419
  dropPosition === 'below' && targetRowIndex === sourceRowIndex - 1; // dragging to immediately above (below previous row)
346
420
 
347
421
  if (isAdjacentNode || sourceRowIndex === targetRowIndex) {
348
- // Return -1 to prevent actual movement (indicators handled separately)
349
- return -1;
422
+ return false;
350
423
  }
351
- let finalTargetIndex;
352
- if (dragDirection === 'up') {
353
- finalTargetIndex = dropPosition === 'above' ? targetRowIndex : targetRowIndex + 1;
354
- } else {
355
- finalTargetIndex = dropPosition === 'above' ? targetRowIndex - 1 : targetRowIndex;
424
+
425
+ // Internal validation passed, now apply additional user validation if provided
426
+ if (isValidRowReorderProp) {
427
+ const expandedSortedRowIds = gridExpandedSortedRowIdsSelector(apiRef);
428
+ const rowTree = gridRowTreeSelector(apiRef);
429
+ const sourceNode = rowTree[sourceRowId];
430
+ const targetNode = rowTree[targetRowId];
431
+ const prevNode = targetRowIndex > 0 ? rowTree[expandedSortedRowIds[targetRowIndex - 1]] : null;
432
+ const nextNode = targetRowIndex < expandedSortedRowIds.length - 1 ? rowTree[expandedSortedRowIds[targetRowIndex + 1]] : null;
433
+ const context = {
434
+ apiRef,
435
+ sourceNode,
436
+ targetNode,
437
+ prevNode,
438
+ nextNode,
439
+ dropPosition,
440
+ dragDirection
441
+ };
442
+ if (!isValidRowReorderProp(context)) {
443
+ return false;
444
+ }
356
445
  }
357
- return finalTargetIndex;
358
- }, [apiRef, sortedRowIndexLookup]);
359
- useGridRegisterPipeProcessor(apiRef, 'getRowReorderTargetIndex', getRowReorderTargetIndex);
446
+ return true;
447
+ }, [apiRef, isValidRowReorderProp]);
448
+ useGridRegisterPipeProcessor(apiRef, 'isRowReorderValid', isRowReorderValid);
360
449
  useGridEvent(apiRef, 'rowDragStart', handleDragStart);
361
450
  useGridEvent(apiRef, 'rowDragOver', handleDragOver);
362
451
  useGridEvent(apiRef, 'rowDragEnd', handleDragEnd);
@@ -0,0 +1,82 @@
1
+ import type { RefObject } from '@mui/x-internals/types';
2
+ import { type GridRowId, type GridTreeNode, type GridGroupNode, type GridRowTreeConfig, type GridKeyValue, type GridValidRowModel } from '@mui/x-data-grid';
3
+ import { type ReorderOperationType } from "./types.js";
4
+ import type { GridPrivateApiPro } from "../../../models/gridApiPro.js";
5
+ import { DataGridProProcessedProps } from "../../../models/dataGridProProps.js";
6
+ export { getNodePathInTree } from "../../../utils/tree/utils.js";
7
+ /**
8
+ * Finds the closest cell element from the given event target.
9
+ * If the target itself is a cell, returns it.
10
+ * Otherwise, searches for the closest parent with 'cell' in its className.
11
+ * @param target - The event target to start searching from
12
+ * @returns The cell element or the original target if no cell is found
13
+ */
14
+ export declare function findCellElement(target: EventTarget | null): Element;
15
+ export declare function determineOperationType(sourceNode: GridTreeNode, targetNode: GridTreeNode): ReorderOperationType;
16
+ export declare function calculateTargetIndex(sourceNode: GridTreeNode, targetNode: GridTreeNode, isLastChild: boolean, rowTree: Record<GridRowId, GridTreeNode>): number;
17
+ export declare const collectAllLeafDescendants: (groupNode: GridGroupNode, tree: GridRowTreeConfig) => GridRowId[];
18
+ export declare const collectAllDescendants: (groupNode: GridGroupNode, tree: GridRowTreeConfig) => GridTreeNode[];
19
+ export declare const isDescendantOf: (possibleDescendant: GridTreeNode, ancestor: GridTreeNode, tree: GridRowTreeConfig) => boolean;
20
+ export declare const updateDescendantDepths: (group: GridGroupNode, tree: GridRowTreeConfig, depthDiff: number) => void;
21
+ /**
22
+ * Finds an existing group node with the same groupingKey and groupingField under a parent.
23
+ *
24
+ * @param parentNode - The parent group node to search in
25
+ * @param groupingKey - The grouping key to match
26
+ * @param groupingField - The grouping field to match
27
+ * @param tree - The row tree configuration
28
+ * @returns The existing group node if found, null otherwise
29
+ */
30
+ export declare function findExistingGroupWithSameKey(parentNode: GridGroupNode, groupingKey: GridKeyValue, groupingField: string, tree: GridRowTreeConfig): GridGroupNode | null;
31
+ /**
32
+ * Removes empty ancestor groups from the tree after a row move operation.
33
+ * Walks up the tree from the given group, removing any empty groups encountered.
34
+ *
35
+ * @param groupId - The ID of the group to start checking from
36
+ * @param tree - The row tree configuration
37
+ * @param removedGroups - Set to track which groups have been removed
38
+ * @returns The number of root-level groups that were removed
39
+ */
40
+ export declare function removeEmptyAncestors(groupId: GridRowId, tree: GridRowTreeConfig, removedGroups: Set<GridRowId>): number;
41
+ export declare function handleProcessRowUpdateError(error: any, onProcessRowUpdateError?: DataGridProProcessedProps['onProcessRowUpdateError']): void;
42
+ /**
43
+ * Handles batch row updates with partial failure tracking.
44
+ *
45
+ * This class is designed for operations that need to update multiple rows
46
+ * atomically (like moving entire groups), while gracefully handling cases
47
+ * where some updates succeed and others fail.
48
+ *
49
+ * @example
50
+ * ```tsx
51
+ * const updater = new BatchRowUpdater(apiRef, processRowUpdate, onError);
52
+ *
53
+ * // Queue multiple updates
54
+ * updater.queueUpdate('row1', originalRow1, newRow1);
55
+ * updater.queueUpdate('row2', originalRow2, newRow2);
56
+ *
57
+ * // Execute all updates
58
+ * const { successful, failed, updates } = await updater.executeAll();
59
+ *
60
+ * // Handle results
61
+ * if (successful.length > 0) {
62
+ * apiRef.current.updateRows(updates);
63
+ * }
64
+ * ```
65
+ */
66
+ export declare class BatchRowUpdater {
67
+ private apiRef;
68
+ private processRowUpdate;
69
+ private onProcessRowUpdateError;
70
+ private rowsToUpdate;
71
+ private originalRows;
72
+ private successfulRowIds;
73
+ private failedRowIds;
74
+ private pendingRowUpdates;
75
+ constructor(apiRef: RefObject<GridPrivateApiPro>, processRowUpdate: DataGridProProcessedProps['processRowUpdate'] | undefined, onProcessRowUpdateError: DataGridProProcessedProps['onProcessRowUpdateError'] | undefined);
76
+ queueUpdate(rowId: GridRowId, originalRow: GridValidRowModel, updatedRow: GridValidRowModel): void;
77
+ executeAll(): Promise<{
78
+ successful: GridRowId[];
79
+ failed: GridRowId[];
80
+ updates: GridValidRowModel[];
81
+ }>;
82
+ }