@mui/x-data-grid-pro 8.19.0 → 8.21.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 (71) hide show
  1. package/CHANGELOG.md +174 -0
  2. package/DataGridPro/DataGridPro.js +29 -2
  3. package/components/GridRowReorderCell.js +15 -3
  4. package/components/headerFiltering/GridHeaderFilterCell.js +2 -3
  5. package/esm/DataGridPro/DataGridPro.js +29 -2
  6. package/esm/components/GridRowReorderCell.js +15 -3
  7. package/esm/components/headerFiltering/GridHeaderFilterCell.js +2 -3
  8. package/esm/hooks/features/dataSource/useGridDataSourceBasePro.js +1 -1
  9. package/esm/hooks/features/rowReorder/commonReorderConditions.d.ts +30 -0
  10. package/esm/hooks/features/rowReorder/commonReorderConditions.js +78 -0
  11. package/esm/hooks/features/rowReorder/index.d.ts +2 -1
  12. package/esm/hooks/features/rowReorder/index.js +2 -1
  13. package/esm/hooks/features/rowReorder/models.d.ts +17 -0
  14. package/esm/hooks/features/rowReorder/models.js +1 -0
  15. package/esm/hooks/features/rowReorder/reorderExecutor.d.ts +27 -0
  16. package/esm/hooks/features/rowReorder/reorderExecutor.js +29 -0
  17. package/esm/hooks/features/rowReorder/reorderValidator.d.ts +12 -0
  18. package/esm/hooks/features/rowReorder/reorderValidator.js +14 -0
  19. package/esm/hooks/features/rowReorder/types.d.ts +25 -0
  20. package/esm/hooks/features/rowReorder/types.js +1 -0
  21. package/esm/hooks/features/rowReorder/useGridRowReorder.d.ts +1 -1
  22. package/esm/hooks/features/rowReorder/useGridRowReorder.js +167 -80
  23. package/esm/hooks/features/rowReorder/utils.d.ts +82 -0
  24. package/esm/hooks/features/rowReorder/utils.js +259 -0
  25. package/esm/hooks/features/rows/useGridRowsOverridableMethods.d.ts +7 -0
  26. package/esm/hooks/features/rows/useGridRowsOverridableMethods.js +59 -0
  27. package/esm/hooks/features/treeData/treeDataReorderExecutor.d.ts +11 -0
  28. package/esm/hooks/features/treeData/treeDataReorderExecutor.js +534 -0
  29. package/esm/hooks/features/treeData/treeDataReorderValidator.d.ts +2 -0
  30. package/esm/hooks/features/treeData/treeDataReorderValidator.js +35 -0
  31. package/esm/hooks/features/treeData/useGridTreeData.d.ts +3 -3
  32. package/esm/hooks/features/treeData/useGridTreeData.js +49 -4
  33. package/esm/hooks/features/treeData/utils.d.ts +8 -0
  34. package/esm/hooks/features/treeData/utils.js +96 -0
  35. package/esm/index.js +1 -1
  36. package/esm/internals/index.d.ts +8 -0
  37. package/esm/internals/index.js +6 -0
  38. package/esm/models/dataGridProProps.d.ts +32 -4
  39. package/esm/models/gridRowOrderChangeParams.d.ts +29 -5
  40. package/hooks/features/dataSource/useGridDataSourceBasePro.js +1 -1
  41. package/hooks/features/rowReorder/commonReorderConditions.d.ts +30 -0
  42. package/hooks/features/rowReorder/commonReorderConditions.js +84 -0
  43. package/hooks/features/rowReorder/index.d.ts +2 -1
  44. package/hooks/features/rowReorder/models.d.ts +17 -0
  45. package/hooks/features/rowReorder/models.js +5 -0
  46. package/hooks/features/rowReorder/reorderExecutor.d.ts +27 -0
  47. package/hooks/features/rowReorder/reorderExecutor.js +37 -0
  48. package/hooks/features/rowReorder/reorderValidator.d.ts +12 -0
  49. package/hooks/features/rowReorder/reorderValidator.js +21 -0
  50. package/hooks/features/rowReorder/types.d.ts +25 -0
  51. package/hooks/features/rowReorder/types.js +5 -0
  52. package/hooks/features/rowReorder/useGridRowReorder.d.ts +1 -1
  53. package/hooks/features/rowReorder/useGridRowReorder.js +168 -81
  54. package/hooks/features/rowReorder/utils.d.ts +82 -0
  55. package/hooks/features/rowReorder/utils.js +286 -0
  56. package/hooks/features/rows/useGridRowsOverridableMethods.d.ts +7 -0
  57. package/hooks/features/rows/useGridRowsOverridableMethods.js +67 -0
  58. package/hooks/features/treeData/treeDataReorderExecutor.d.ts +11 -0
  59. package/hooks/features/treeData/treeDataReorderExecutor.js +541 -0
  60. package/hooks/features/treeData/treeDataReorderValidator.d.ts +2 -0
  61. package/hooks/features/treeData/treeDataReorderValidator.js +41 -0
  62. package/hooks/features/treeData/useGridTreeData.d.ts +3 -3
  63. package/hooks/features/treeData/useGridTreeData.js +48 -3
  64. package/hooks/features/treeData/utils.d.ts +8 -0
  65. package/hooks/features/treeData/utils.js +109 -0
  66. package/index.js +1 -1
  67. package/internals/index.d.ts +8 -0
  68. package/internals/index.js +53 -1
  69. package/models/dataGridProProps.d.ts +32 -4
  70. package/models/gridRowOrderChangeParams.d.ts +29 -5
  71. package/package.json +4 -4
@@ -0,0 +1 @@
1
+ export {};
@@ -7,4 +7,4 @@ export declare const rowReorderStateInitializer: GridStateInitializer;
7
7
  * Hook for row reordering (Pro package)
8
8
  * @requires useGridRows (method)
9
9
  */
10
- export declare const useGridRowReorder: (apiRef: RefObject<GridPrivateApiPro>, props: Pick<DataGridProProcessedProps, "rowReordering" | "onRowOrderChange" | "classes" | "treeData" | "dataSource">) => void;
10
+ export declare const useGridRowReorder: (apiRef: RefObject<GridPrivateApiPro>, props: Pick<DataGridProProcessedProps, "rowReordering" | "onRowOrderChange" | "classes" | "treeData" | "dataSource" | "isValidRowReorder">) => void;
@@ -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,13 +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 timeoutRowId = React.useRef('');
51
+ const timeoutInfoRef = React.useRef(EMPTY_TIMEOUT_INFO);
50
52
  const timeout = useTimeout();
51
53
  const previousReorderState = React.useRef(EMPTY_REORDER_STATE);
52
54
  const dropTarget = React.useRef({
@@ -61,26 +63,30 @@ export const useGridRowReorder = (apiRef, props) => {
61
63
  }, []);
62
64
 
63
65
  // TODO: remove sortModel check once row reorder is sorting compatible
64
- // remove treeData check once row reorder is treeData compatible
65
66
  const isRowReorderDisabled = React.useMemo(() => {
66
- return !props.rowReordering || !!sortModel.length || props.treeData;
67
- }, [props.rowReordering, sortModel, props.treeData]);
68
- const applyDropIndicator = React.useCallback((targetRowId, position) => {
69
- // Remove existing drop indicator from previous target
70
- if (previousDropIndicatorRef.current) {
71
- previousDropIndicatorRef.current.classList.remove(classes.rowDropAbove, classes.rowDropBelow);
72
- previousDropIndicatorRef.current = null;
73
- }
74
-
75
- // Apply new drop indicator
76
- if (targetRowId !== undefined && position !== null) {
77
- const targetRow = apiRef.current.rootElementRef?.current?.querySelector(`[data-id="${targetRowId}"]`);
78
- if (targetRow) {
79
- targetRow.classList.add(position === 'above' ? classes.rowDropAbove : classes.rowDropBelow);
80
- 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';
81
80
  }
81
+ if (relativeY > bottomThreshold) {
82
+ return 'below';
83
+ }
84
+ return 'inside';
82
85
  }
83
- }, [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]);
84
90
  const applyDraggedState = React.useCallback((rowId, isDragged) => {
85
91
  if (rowId) {
86
92
  const draggedRow = apiRef.current.rootElementRef?.current?.querySelector(`[data-id="${rowId}"]`);
@@ -93,7 +99,7 @@ export const useGridRowReorder = (apiRef, props) => {
93
99
  }
94
100
  }
95
101
  }, [apiRef, classes.rowBeingDragged]);
96
- const applyRowAnimation = React.useCallback(callback => {
102
+ const applyRowAnimation = React.useCallback(async callback => {
97
103
  const rootElement = apiRef.current.rootElementRef?.current;
98
104
  if (!rootElement) {
99
105
  return;
@@ -110,7 +116,7 @@ export const useGridRowReorder = (apiRef, props) => {
110
116
  initialPositions.set(rowId, row.getBoundingClientRect());
111
117
  }
112
118
  });
113
- callback();
119
+ await callback();
114
120
 
115
121
  // Use `requestAnimationFrame` to ensure DOM has updated
116
122
  requestAnimationFrame(() => {
@@ -152,15 +158,20 @@ export const useGridRowReorder = (apiRef, props) => {
152
158
  if (isRowReorderDisabled || Object.keys(editRowsState).length !== 0) {
153
159
  return;
154
160
  }
155
- if (timeoutRowId.current) {
161
+ if (timeoutInfoRef.current) {
156
162
  timeout.clear();
157
- timeoutRowId.current = '';
163
+ timeoutInfoRef.current = EMPTY_TIMEOUT_INFO;
158
164
  }
159
165
  logger.debug(`Start dragging row ${params.id}`);
160
166
  // Prevent drag events propagation.
161
167
  // For more information check here https://github.com/mui/mui-x/issues/2680.
162
168
  event.stopPropagation();
163
- 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
+ }));
164
175
  dragRowNode.current = event.currentTarget;
165
176
  // Apply cell-level dragging class to the drag handle
166
177
  dragRowNode.current.classList.add(classes.rowDragging);
@@ -184,35 +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
  }
210
225
  const sortedRowIndexLookup = gridExpandedSortedRowIndexLookupSelector(apiRef);
211
226
  const targetRowIndex = sortedRowIndexLookup[params.id];
212
227
  const sourceRowIndex = sortedRowIndexLookup[dragRowId];
213
-
214
- // Determine drop position based on relativeY position within the row
215
- const dropPosition = relativeY < midPoint ? 'above' : 'below';
216
228
  const currentReorderState = {
217
229
  dragDirection: targetRowIndex < sourceRowIndex ? 'up' : 'down',
218
230
  previousTargetId: params.id,
@@ -225,7 +237,7 @@ export const useGridRowReorder = (apiRef, props) => {
225
237
 
226
238
  // Check if this is an adjacent position
227
239
  const isAdjacentPosition = dropPosition === 'above' && targetRowIndex === sourceRowIndex + 1 || dropPosition === 'below' && targetRowIndex === sourceRowIndex - 1;
228
- const validatedIndex = apiRef.current.unstable_applyPipeProcessors('getRowReorderTargetIndex', -1, {
240
+ const isRowReorderValid = apiRef.current.unstable_applyPipeProcessors('isRowReorderValid', false, {
229
241
  sourceRowId: dragRowId,
230
242
  targetRowId: params.id,
231
243
  dropPosition,
@@ -233,13 +245,21 @@ export const useGridRowReorder = (apiRef, props) => {
233
245
  });
234
246
 
235
247
  // Show drop indicator for valid drops OR adjacent positions OR same node
236
- if (validatedIndex !== -1 || isAdjacentPosition || isSameNode) {
248
+ if (isRowReorderValid || isAdjacentPosition || isSameNode) {
237
249
  dropTarget.current = {
238
250
  targetRowId: params.id,
239
251
  targetRowIndex,
240
252
  dropPosition
241
253
  };
242
- 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
+ }));
243
263
  } else {
244
264
  // Clear indicators for invalid drops
245
265
  dropTarget.current = {
@@ -247,7 +267,12 @@ export const useGridRowReorder = (apiRef, props) => {
247
267
  targetRowIndex: null,
248
268
  dropPosition: null
249
269
  };
250
- 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
+ }));
251
276
  }
252
277
  previousReorderState.current = currentReorderState;
253
278
  }
@@ -258,16 +283,16 @@ export const useGridRowReorder = (apiRef, props) => {
258
283
  } else {
259
284
  event.dataTransfer.dropEffect = 'copy';
260
285
  }
261
- }, [dragRowId, apiRef, logger, timeout, applyDropIndicator]);
262
- const handleDragEnd = React.useCallback((_, event) => {
286
+ }, [dragRowId, apiRef, logger, timeout, calculateDropPosition]);
287
+ const handleDragEnd = React.useCallback(async (_, event) => {
263
288
  // Call the gridEditRowsStateSelector directly to avoid infnite loop
264
289
  const editRowsState = gridEditRowsStateSelector(apiRef);
265
290
  if (dragRowId === '' || isRowReorderDisabled || Object.keys(editRowsState).length !== 0) {
266
291
  return;
267
292
  }
268
- if (timeoutRowId.current) {
293
+ if (timeoutInfoRef.current) {
269
294
  timeout.clear();
270
- timeoutRowId.current = '';
295
+ timeoutInfoRef.current = EMPTY_TIMEOUT_INFO;
271
296
  }
272
297
  logger.debug('End dragging row');
273
298
  event.preventDefault();
@@ -279,11 +304,6 @@ export const useGridRowReorder = (apiRef, props) => {
279
304
  const dragDirection = previousReorderState.current.dragDirection;
280
305
  previousReorderState.current = EMPTY_REORDER_STATE;
281
306
 
282
- // Clear visual indicators and dragged state
283
- applyDropIndicator(null, null);
284
- applyDraggedState(dragRowId, false);
285
- apiRef.current.setRowDragActive(false);
286
-
287
307
  // Check if the row was dropped outside the grid.
288
308
  if (!event.dataTransfer || event.dataTransfer.dropEffect === 'none') {
289
309
  // Reset drop target state
@@ -294,29 +314,80 @@ export const useGridRowReorder = (apiRef, props) => {
294
314
  };
295
315
  originRowIndex.current = null;
296
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
+ }));
297
325
  return;
298
326
  }
299
327
  if (dropTarget.current.targetRowIndex !== null && dropTarget.current.targetRowId !== null) {
300
- const sourceRowIndex = originRowIndex.current;
301
- const targetRowIndex = dropTarget.current.targetRowIndex;
302
- const validatedIndex = apiRef.current.unstable_applyPipeProcessors('getRowReorderTargetIndex', targetRowIndex, {
328
+ const isRowReorderValid = apiRef.current.unstable_applyPipeProcessors('isRowReorderValid', false, {
303
329
  sourceRowId: dragRowId,
304
330
  targetRowId: dropTarget.current.targetRowId,
305
331
  dropPosition: dropTarget.current.dropPosition,
306
332
  dragDirection: dragDirection
307
333
  });
308
- if (validatedIndex !== -1) {
309
- applyRowAnimation(() => {
310
- 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
+ }
341
+
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);
311
356
 
312
- // Emit the rowOrderChange event only once when the reordering stops.
313
- const rowOrderChangeParams = {
314
- row: apiRef.current.getRow(dragRowId),
315
- targetIndex: validatedIndex,
316
- oldIndex: sourceRowIndex
317
- };
318
- apiRef.current.publishEvent('rowOrderChange', rowOrderChangeParams);
319
- });
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
+ }));
320
391
  }
321
392
  }
322
393
 
@@ -327,8 +398,9 @@ export const useGridRowReorder = (apiRef, props) => {
327
398
  dropPosition: null
328
399
  };
329
400
  setDragRowId('');
330
- }, [apiRef, dragRowId, isRowReorderDisabled, logger, applyDropIndicator, applyDraggedState, timeout, applyRowAnimation]);
331
- const getRowReorderTargetIndex = React.useCallback((initialValue, {
401
+ }, [apiRef, dragRowId, isRowReorderDisabled, logger, applyDraggedState, timeout, applyRowAnimation]);
402
+ const isValidRowReorderProp = props.isValidRowReorder;
403
+ const isRowReorderValid = React.useCallback((initialValue, {
332
404
  sourceRowId,
333
405
  targetRowId,
334
406
  dropPosition,
@@ -341,24 +413,39 @@ export const useGridRowReorder = (apiRef, props) => {
341
413
  const targetRowIndex = sortedRowIndexLookup[targetRowId];
342
414
  const sourceRowIndex = sortedRowIndexLookup[sourceRowId];
343
415
 
344
- // Check if this drop would result in no actual movement
416
+ // Apply internal validation: check if this drop would result in no actual movement
345
417
  const isAdjacentNode = dropPosition === 'above' && targetRowIndex === sourceRowIndex + 1 ||
346
418
  // dragging to immediately below (above next row)
347
419
  dropPosition === 'below' && targetRowIndex === sourceRowIndex - 1; // dragging to immediately above (below previous row)
348
420
 
349
421
  if (isAdjacentNode || sourceRowIndex === targetRowIndex) {
350
- // Return -1 to prevent actual movement (indicators handled separately)
351
- return -1;
422
+ return false;
352
423
  }
353
- let finalTargetIndex;
354
- if (dragDirection === 'up') {
355
- finalTargetIndex = dropPosition === 'above' ? targetRowIndex : targetRowIndex + 1;
356
- } else {
357
- 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
+ }
358
445
  }
359
- return finalTargetIndex;
360
- }, [apiRef]);
361
- useGridRegisterPipeProcessor(apiRef, 'getRowReorderTargetIndex', getRowReorderTargetIndex);
446
+ return true;
447
+ }, [apiRef, isValidRowReorderProp]);
448
+ useGridRegisterPipeProcessor(apiRef, 'isRowReorderValid', isRowReorderValid);
362
449
  useGridEvent(apiRef, 'rowDragStart', handleDragStart);
363
450
  useGridEvent(apiRef, 'rowDragOver', handleDragOver);
364
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
+ }