@mui/x-data-grid 9.12.0 → 9.14.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 (92) hide show
  1. package/CHANGELOG.md +301 -0
  2. package/DataGrid/DataGrid.js +1 -1
  3. package/DataGrid/DataGrid.mjs +1 -1
  4. package/components/GridPagination.js +1 -1
  5. package/components/GridPagination.mjs +1 -1
  6. package/components/GridRow.d.mts +1 -0
  7. package/components/GridRow.d.ts +1 -0
  8. package/components/GridRow.js +41 -6
  9. package/components/GridRow.mjs +41 -6
  10. package/components/GridScrollArea.js +20 -7
  11. package/components/GridScrollArea.mjs +21 -8
  12. package/components/base/GridOverlays.js +6 -5
  13. package/components/base/GridOverlays.mjs +6 -5
  14. package/components/cell/GridActionsCell.js +53 -50
  15. package/components/cell/GridActionsCell.mjs +53 -50
  16. package/components/cell/GridActionsCellItem.js +0 -1
  17. package/components/cell/GridActionsCellItem.mjs +0 -1
  18. package/components/cell/GridCell.js +15 -4
  19. package/components/cell/GridCell.mjs +15 -4
  20. package/components/cell/GridEditBooleanCell.d.mts +1 -1
  21. package/components/cell/GridEditBooleanCell.d.ts +1 -1
  22. package/components/cell/GridEditBooleanCell.js +4 -4
  23. package/components/cell/GridEditBooleanCell.mjs +4 -4
  24. package/components/cell/GridEditDateCell.d.mts +1 -1
  25. package/components/cell/GridEditDateCell.d.ts +1 -1
  26. package/components/cell/GridEditDateCell.js +5 -5
  27. package/components/cell/GridEditDateCell.mjs +5 -5
  28. package/components/cell/GridEditInputCell.d.mts +1 -1
  29. package/components/cell/GridEditInputCell.d.ts +1 -1
  30. package/components/cell/GridEditInputCell.js +1 -1
  31. package/components/cell/GridEditInputCell.mjs +1 -1
  32. package/components/cell/GridEditLongTextCell.d.mts +1 -1
  33. package/components/cell/GridEditLongTextCell.d.ts +1 -1
  34. package/components/cell/GridEditLongTextCell.js +2 -2
  35. package/components/cell/GridEditLongTextCell.mjs +2 -2
  36. package/components/cell/GridEditSingleSelectCell.d.mts +1 -1
  37. package/components/cell/GridEditSingleSelectCell.d.ts +1 -1
  38. package/components/cell/GridEditSingleSelectCell.js +4 -4
  39. package/components/cell/GridEditSingleSelectCell.mjs +4 -4
  40. package/components/quickFilter/QuickFilter.js +7 -2
  41. package/components/quickFilter/QuickFilter.mjs +7 -2
  42. package/components/toolbarV8/Toolbar.d.mts +5 -1
  43. package/components/toolbarV8/Toolbar.d.ts +5 -1
  44. package/components/toolbarV8/Toolbar.js +34 -12
  45. package/components/toolbarV8/Toolbar.mjs +33 -11
  46. package/components/virtualization/GridVirtualScroller.js +3 -0
  47. package/components/virtualization/GridVirtualScroller.mjs +3 -0
  48. package/hooks/core/useGridVirtualizer.js +14 -1
  49. package/hooks/core/useGridVirtualizer.mjs +14 -1
  50. package/hooks/features/columnResize/useGridColumnResize.js +4 -1
  51. package/hooks/features/columnResize/useGridColumnResize.mjs +4 -1
  52. package/hooks/features/dataSource/models.d.mts +2 -2
  53. package/hooks/features/dataSource/models.d.ts +2 -2
  54. package/hooks/features/dataSource/useGridDataSourceBase.js +12 -8
  55. package/hooks/features/dataSource/useGridDataSourceBase.mjs +13 -9
  56. package/hooks/features/pivoting/gridPivotingInterfaces.d.mts +2 -2
  57. package/hooks/features/pivoting/gridPivotingInterfaces.d.ts +2 -2
  58. package/hooks/features/rowSelection/useGridRowSelection.d.mts +1 -1
  59. package/hooks/features/rowSelection/useGridRowSelection.d.ts +1 -1
  60. package/hooks/features/rowSelection/useGridRowSelection.js +51 -10
  61. package/hooks/features/rowSelection/useGridRowSelection.mjs +53 -12
  62. package/hooks/features/rowSelection/utils.js +5 -0
  63. package/hooks/features/rowSelection/utils.mjs +5 -0
  64. package/hooks/features/rows/gridRowsUtils.d.mts +37 -3
  65. package/hooks/features/rows/gridRowsUtils.d.ts +37 -3
  66. package/hooks/features/rows/gridRowsUtils.js +120 -23
  67. package/hooks/features/rows/gridRowsUtils.mjs +115 -22
  68. package/hooks/features/rows/useGridRows.js +10 -1
  69. package/hooks/features/rows/useGridRows.mjs +10 -1
  70. package/index.js +1 -1
  71. package/index.mjs +1 -1
  72. package/internals/index.d.mts +2 -2
  73. package/internals/index.d.ts +2 -2
  74. package/internals/index.js +28 -0
  75. package/internals/index.mjs +2 -2
  76. package/material/variables.d.mts +6 -1
  77. package/material/variables.d.ts +6 -1
  78. package/material/variables.js +17 -3
  79. package/material/variables.mjs +14 -4
  80. package/models/api/gridRowApi.d.mts +7 -7
  81. package/models/api/gridRowApi.d.ts +7 -7
  82. package/models/gridDataSource.d.mts +3 -0
  83. package/models/gridDataSource.d.ts +3 -0
  84. package/models/gridRows.d.mts +28 -1
  85. package/models/gridRows.d.ts +28 -1
  86. package/models/props/DataGridProps.d.mts +3 -3
  87. package/models/props/DataGridProps.d.ts +3 -3
  88. package/package.json +6 -6
  89. package/utils/keyboardUtils.d.mts +1 -0
  90. package/utils/keyboardUtils.d.ts +1 -0
  91. package/utils/keyboardUtils.js +6 -0
  92. package/utils/keyboardUtils.mjs +5 -0
@@ -9,4 +9,4 @@ export declare const rowSelectionStateInitializer: GridStateInitializer<Pick<Dat
9
9
  * @requires useGridFocus (state) - can be after
10
10
  * @requires useGridKeyboardNavigation (`cellKeyDown` event must first be consumed by it)
11
11
  */
12
- export declare const useGridRowSelection: (apiRef: RefObject<GridPrivateApiCommunity>, props: Pick<DataGridProcessedProps, 'checkboxSelection' | 'rowSelectionModel' | 'onRowSelectionModelChange' | 'disableMultipleRowSelection' | 'disableRowSelectionOnClick' | 'disableRowSelectionExcludeModel' | 'isRowSelectable' | 'checkboxSelectionVisibleOnly' | 'pagination' | 'paginationMode' | 'filterMode' | 'classes' | 'keepNonExistentRowsSelected' | 'rowSelection' | 'rowSelectionPropagation' | 'signature'>) => void;
12
+ export declare const useGridRowSelection: (apiRef: RefObject<GridPrivateApiCommunity>, props: Pick<DataGridProcessedProps, 'checkboxSelection' | 'rowSelectionModel' | 'onRowSelectionModelChange' | 'disableMultipleRowSelection' | 'disableRowSelectionOnClick' | 'disableRowSelectionExcludeModel' | 'isRowSelectable' | 'checkboxSelectionVisibleOnly' | 'pagination' | 'paginationMode' | 'filterMode' | 'classes' | 'keepNonExistentRowsSelected' | 'rowSelection' | 'rowSelectionPropagation' | 'dataSource' | 'signature'>) => void;
@@ -9,4 +9,4 @@ export declare const rowSelectionStateInitializer: GridStateInitializer<Pick<Dat
9
9
  * @requires useGridFocus (state) - can be after
10
10
  * @requires useGridKeyboardNavigation (`cellKeyDown` event must first be consumed by it)
11
11
  */
12
- export declare const useGridRowSelection: (apiRef: RefObject<GridPrivateApiCommunity>, props: Pick<DataGridProcessedProps, 'checkboxSelection' | 'rowSelectionModel' | 'onRowSelectionModelChange' | 'disableMultipleRowSelection' | 'disableRowSelectionOnClick' | 'disableRowSelectionExcludeModel' | 'isRowSelectable' | 'checkboxSelectionVisibleOnly' | 'pagination' | 'paginationMode' | 'filterMode' | 'classes' | 'keepNonExistentRowsSelected' | 'rowSelection' | 'rowSelectionPropagation' | 'signature'>) => void;
12
+ export declare const useGridRowSelection: (apiRef: RefObject<GridPrivateApiCommunity>, props: Pick<DataGridProcessedProps, 'checkboxSelection' | 'rowSelectionModel' | 'onRowSelectionModelChange' | 'disableMultipleRowSelection' | 'disableRowSelectionOnClick' | 'disableRowSelectionExcludeModel' | 'isRowSelectable' | 'checkboxSelectionVisibleOnly' | 'pagination' | 'paginationMode' | 'filterMode' | 'classes' | 'keepNonExistentRowsSelected' | 'rowSelection' | 'rowSelectionPropagation' | 'dataSource' | 'signature'>) => void;
@@ -17,6 +17,7 @@ var _useGridApiMethod = require("../../utils/useGridApiMethod");
17
17
  var _useGridLogger = require("../../utils/useGridLogger");
18
18
  var _useGridSelector = require("../../utils/useGridSelector");
19
19
  var _gridRowsSelector = require("../rows/gridRowsSelector");
20
+ var _gridRowsUtils = require("../rows/gridRowsUtils");
20
21
  var _gridRowSelectionSelector = require("./gridRowSelectionSelector");
21
22
  var _gridFocusStateSelector = require("../focus/gridFocusStateSelector");
22
23
  var _gridFilterSelector = require("../filter/gridFilterSelector");
@@ -127,6 +128,10 @@ You need to upgrade to DataGridPro or DataGridPremium component to unlock multip
127
128
  if (props.rowSelection === false) {
128
129
  return false;
129
130
  }
131
+ const rowNode = (0, _gridRowsSelector.gridRowNodeSelector)(apiRef, id);
132
+ if (rowNode?.type === 'footer' || rowNode?.type === 'pinnedRow' || rowNode?.type === 'skeletonRow') {
133
+ return false;
134
+ }
130
135
 
131
136
  // If `keepNonExistentRowsSelected` is true, we might run in a case where row selectability is checked for a row that does not exist.
132
137
  // Since that row was previously selected (otherwise it would not be checked at this point), we return true.
@@ -136,10 +141,6 @@ You need to upgrade to DataGridPro or DataGridPremium component to unlock multip
136
141
  if (propIsRowSelectable && !propIsRowSelectable(apiRef.current.getRowParams(id))) {
137
142
  return false;
138
143
  }
139
- const rowNode = (0, _gridRowsSelector.gridRowNodeSelector)(apiRef, id);
140
- if (rowNode?.type === 'footer' || rowNode?.type === 'pinnedRow') {
141
- return false;
142
- }
143
144
  return true;
144
145
  }, [apiRef, props.rowSelection, props.keepNonExistentRowsSelected, propIsRowSelectable]);
145
146
  const getSelectedRows = React.useCallback(() => (0, _gridRowSelectionSelector.gridRowSelectionIdsSelector)(apiRef), [apiRef]);
@@ -315,6 +316,7 @@ You need to upgrade to DataGridPro or DataGridPremium component to unlock multip
315
316
  * EVENTS
316
317
  */
317
318
  const isFirstRender = React.useRef(true);
319
+ const previousRowsLookupRef = React.useRef(null);
318
320
  const removeOutdatedSelection = React.useCallback(() => {
319
321
  if (isFirstRender.current) {
320
322
  return;
@@ -323,6 +325,11 @@ You need to upgrade to DataGridPro or DataGridPremium component to unlock multip
323
325
  const rowsLookup = (0, _gridRowsSelector.gridRowsLookupSelector)(apiRef);
324
326
  const rowTree = (0, _gridRowsSelector.gridRowTreeSelector)(apiRef);
325
327
  const filteredRowsLookup = (0, _gridFilterSelector.gridFilteredRowsLookupSelector)(apiRef);
328
+ const previousRowsLookup = previousRowsLookupRef.current;
329
+ previousRowsLookupRef.current = rowsLookup;
330
+ // Read the depth at call time. The event that runs this callback can fire in the same
331
+ // tick as the state update that changes the depth, before the render-scope value updates.
332
+ const isNested = (0, _gridRowsSelector.gridRowMaximumTreeDepthSelector)(apiRef) > 1;
326
333
  const isNonExistent = id => {
327
334
  if (props.filterMode === 'server') {
328
335
  return !rowsLookup[id];
@@ -350,6 +357,32 @@ You need to upgrade to DataGridPro or DataGridPremium component to unlock multip
350
357
  return node.children.some(childId => isOrHasSelectableDescendant(childId));
351
358
  };
352
359
  let hasChanged = false;
360
+
361
+ // Rows can load after their parent was selected (server-side lazy loading).
362
+ // They arrive unselected, so the parent re-derivation below would drop the parent.
363
+ // With `rowSelectionPropagation.descendants`, the rows that appeared since the last run
364
+ // inherit the selection state of their closest previously known ancestor instead.
365
+ if (isNested && props.rowSelectionPropagation?.descendants && currentSelection.type === 'include' && currentSelection.ids.size > 0 && previousRowsLookup !== null && previousRowsLookup !== rowsLookup) {
366
+ const isNewRow = id => rowsLookup[id] !== undefined && previousRowsLookup[id] === undefined;
367
+ const inheritsSelection = id => {
368
+ const parentId = rowTree[id]?.parent;
369
+ if (parentId == null || parentId === _gridRowsUtils.GRID_ROOT_GROUP_ID) {
370
+ return false;
371
+ }
372
+ if (selectionManager.has(parentId)) {
373
+ return true;
374
+ }
375
+ // A row that appeared together with its parent reads the state further up the chain.
376
+ return isNewRow(parentId) ? inheritsSelection(parentId) : false;
377
+ };
378
+ const dataRowIds = (0, _gridRowsSelector.gridDataRowIdsSelector)(apiRef);
379
+ for (const id of dataRowIds) {
380
+ if (isNewRow(id) && filteredRowsLookup[id] !== false && inheritsSelection(id) && apiRef.current.isRowSelectable(id)) {
381
+ selectionManager.select(id);
382
+ hasChanged = true;
383
+ }
384
+ }
385
+ }
353
386
  for (const id of currentSelection.ids) {
354
387
  if (isNonExistent(id)) {
355
388
  if (props.keepNonExistentRowsSelected) {
@@ -382,9 +415,10 @@ You need to upgrade to DataGridPro or DataGridPremium component to unlock multip
382
415
  // For nested data, on row tree updation (filtering, adding rows, etc.) when the selection is
383
416
  // not empty, we need to re-run scanning of the tree to propagate the selection changes
384
417
  // Example: A parent whose de-selected children are filtered out should now be selected
385
- const shouldReapplyPropagation = isNestedData && props.rowSelectionPropagation?.parents && (newSelectionModel.ids.size > 0 ||
386
- // In case of exclude selection, newSelectionModel.ids.size === 0 means all rows are selected
387
- newSelectionModel.type === 'exclude');
418
+ // With a data source, an `exclude` model is left alone: the grid only knows the loaded
419
+ // subset of the rows, so rewriting the model into an `include` of the known rows would
420
+ // drop the "rows that load later are selected too" semantics of the select-all state.
421
+ const shouldReapplyPropagation = isNested && props.rowSelectionPropagation?.parents && (newSelectionModel.type === 'exclude' ? props.dataSource === undefined : newSelectionModel.ids.size > 0);
388
422
  if (hasChanged || shouldReapplyPropagation) {
389
423
  if (shouldReapplyPropagation) {
390
424
  if (newSelectionModel.type === 'exclude') {
@@ -398,13 +432,20 @@ You need to upgrade to DataGridPro or DataGridPremium component to unlock multip
398
432
  }
399
433
  apiRef.current.selectRows(selectedRowIds, true, true);
400
434
  } else {
401
- apiRef.current.selectRows(Array.from(newSelectionModel.ids), true, true);
435
+ // `getPropagatedRowSelectionModel` reads the sibling selection state from the model
436
+ // built above, so the rows selected during this run count for the parent derivation.
437
+ // `selectRows` would read the state from before this run instead.
438
+ const propagatedModel = apiRef.current.getPropagatedRowSelectionModel(newSelectionModel);
439
+ const isSelectionValid = propagatedModel.ids.size < 2 || canHaveMultipleSelection;
440
+ if (isSelectionValid && (propagatedModel.type !== currentSelection.type || propagatedModel.ids.size !== currentSelection.ids.size || !Array.from(propagatedModel.ids).every(id => currentSelection.ids.has(id)))) {
441
+ apiRef.current.setRowSelectionModel(propagatedModel, 'multipleRowsSelection');
442
+ }
402
443
  }
403
444
  } else {
404
445
  apiRef.current.setRowSelectionModel(newSelectionModel, 'multipleRowsSelection');
405
446
  }
406
447
  }
407
- }, [apiRef, isNestedData, props.rowSelectionPropagation?.parents, props.keepNonExistentRowsSelected, props.filterMode, getRowsToBeSelected]);
448
+ }, [apiRef, props.rowSelectionPropagation?.parents, props.rowSelectionPropagation?.descendants, props.keepNonExistentRowsSelected, props.filterMode, props.dataSource, canHaveMultipleSelection, getRowsToBeSelected]);
408
449
  const handleSingleRowSelection = React.useCallback((id, event) => {
409
450
  const hasCtrlKey = event.metaKey || event.ctrlKey;
410
451
 
@@ -542,7 +583,7 @@ You need to upgrade to DataGridPro or DataGridPremium component to unlock multip
542
583
  handleSingleRowSelection(params.id, event);
543
584
  return;
544
585
  }
545
- if (String.fromCharCode(event.keyCode) === 'A' && (event.ctrlKey || event.metaKey)) {
586
+ if ((0, _keyboardUtils.isSelectAllShortcut)(event)) {
546
587
  event.preventDefault();
547
588
  if (canHaveMultipleSelection) {
548
589
  toggleAllRows(true);
@@ -9,13 +9,14 @@ import { useGridEvent } from "../../utils/useGridEvent.mjs";
9
9
  import { useGridApiMethod } from "../../utils/useGridApiMethod.mjs";
10
10
  import { useGridLogger } from "../../utils/useGridLogger.mjs";
11
11
  import { useGridSelector } from "../../utils/useGridSelector.mjs";
12
- import { gridRowsLookupSelector, gridRowMaximumTreeDepthSelector, gridRowNodeSelector, gridRowTreeSelector } from "../rows/gridRowsSelector.mjs";
12
+ import { gridDataRowIdsSelector, gridRowsLookupSelector, gridRowMaximumTreeDepthSelector, gridRowNodeSelector, gridRowTreeSelector } from "../rows/gridRowsSelector.mjs";
13
+ import { GRID_ROOT_GROUP_ID } from "../rows/gridRowsUtils.mjs";
13
14
  import { gridRowSelectionManagerSelector, gridRowSelectionStateSelector, gridRowSelectionCountSelector, gridRowSelectionIdsSelector } from "./gridRowSelectionSelector.mjs";
14
15
  import { gridFocusCellSelector } from "../focus/gridFocusStateSelector.mjs";
15
16
  import { gridExpandedSortedRowIdsSelector, gridFilteredRowsLookupSelector, gridFilterModelSelector, gridQuickFilterValuesSelector } from "../filter/gridFilterSelector.mjs";
16
17
  import { GRID_CHECKBOX_SELECTION_COL_DEF, GRID_ACTIONS_COLUMN_TYPE } from "../../../colDef/index.mjs";
17
18
  import { GridCellModes } from "../../../models/gridEditRowModel.mjs";
18
- import { isKeyboardEvent, isNavigationKey } from "../../../utils/keyboardUtils.mjs";
19
+ import { isKeyboardEvent, isNavigationKey, isSelectAllShortcut } from "../../../utils/keyboardUtils.mjs";
19
20
  import { getVisibleRows } from "../../utils/useGridVisibleRows.mjs";
20
21
  import { GRID_DETAIL_PANEL_TOGGLE_FIELD } from "../../../internals/constants.mjs";
21
22
  import { gridClasses } from "../../../constants/gridClasses.mjs";
@@ -119,6 +120,10 @@ You need to upgrade to DataGridPro or DataGridPremium component to unlock multip
119
120
  if (props.rowSelection === false) {
120
121
  return false;
121
122
  }
123
+ const rowNode = gridRowNodeSelector(apiRef, id);
124
+ if (rowNode?.type === 'footer' || rowNode?.type === 'pinnedRow' || rowNode?.type === 'skeletonRow') {
125
+ return false;
126
+ }
122
127
 
123
128
  // If `keepNonExistentRowsSelected` is true, we might run in a case where row selectability is checked for a row that does not exist.
124
129
  // Since that row was previously selected (otherwise it would not be checked at this point), we return true.
@@ -128,10 +133,6 @@ You need to upgrade to DataGridPro or DataGridPremium component to unlock multip
128
133
  if (propIsRowSelectable && !propIsRowSelectable(apiRef.current.getRowParams(id))) {
129
134
  return false;
130
135
  }
131
- const rowNode = gridRowNodeSelector(apiRef, id);
132
- if (rowNode?.type === 'footer' || rowNode?.type === 'pinnedRow') {
133
- return false;
134
- }
135
136
  return true;
136
137
  }, [apiRef, props.rowSelection, props.keepNonExistentRowsSelected, propIsRowSelectable]);
137
138
  const getSelectedRows = React.useCallback(() => gridRowSelectionIdsSelector(apiRef), [apiRef]);
@@ -307,6 +308,7 @@ You need to upgrade to DataGridPro or DataGridPremium component to unlock multip
307
308
  * EVENTS
308
309
  */
309
310
  const isFirstRender = React.useRef(true);
311
+ const previousRowsLookupRef = React.useRef(null);
310
312
  const removeOutdatedSelection = React.useCallback(() => {
311
313
  if (isFirstRender.current) {
312
314
  return;
@@ -315,6 +317,11 @@ You need to upgrade to DataGridPro or DataGridPremium component to unlock multip
315
317
  const rowsLookup = gridRowsLookupSelector(apiRef);
316
318
  const rowTree = gridRowTreeSelector(apiRef);
317
319
  const filteredRowsLookup = gridFilteredRowsLookupSelector(apiRef);
320
+ const previousRowsLookup = previousRowsLookupRef.current;
321
+ previousRowsLookupRef.current = rowsLookup;
322
+ // Read the depth at call time. The event that runs this callback can fire in the same
323
+ // tick as the state update that changes the depth, before the render-scope value updates.
324
+ const isNested = gridRowMaximumTreeDepthSelector(apiRef) > 1;
318
325
  const isNonExistent = id => {
319
326
  if (props.filterMode === 'server') {
320
327
  return !rowsLookup[id];
@@ -342,6 +349,32 @@ You need to upgrade to DataGridPro or DataGridPremium component to unlock multip
342
349
  return node.children.some(childId => isOrHasSelectableDescendant(childId));
343
350
  };
344
351
  let hasChanged = false;
352
+
353
+ // Rows can load after their parent was selected (server-side lazy loading).
354
+ // They arrive unselected, so the parent re-derivation below would drop the parent.
355
+ // With `rowSelectionPropagation.descendants`, the rows that appeared since the last run
356
+ // inherit the selection state of their closest previously known ancestor instead.
357
+ if (isNested && props.rowSelectionPropagation?.descendants && currentSelection.type === 'include' && currentSelection.ids.size > 0 && previousRowsLookup !== null && previousRowsLookup !== rowsLookup) {
358
+ const isNewRow = id => rowsLookup[id] !== undefined && previousRowsLookup[id] === undefined;
359
+ const inheritsSelection = id => {
360
+ const parentId = rowTree[id]?.parent;
361
+ if (parentId == null || parentId === GRID_ROOT_GROUP_ID) {
362
+ return false;
363
+ }
364
+ if (selectionManager.has(parentId)) {
365
+ return true;
366
+ }
367
+ // A row that appeared together with its parent reads the state further up the chain.
368
+ return isNewRow(parentId) ? inheritsSelection(parentId) : false;
369
+ };
370
+ const dataRowIds = gridDataRowIdsSelector(apiRef);
371
+ for (const id of dataRowIds) {
372
+ if (isNewRow(id) && filteredRowsLookup[id] !== false && inheritsSelection(id) && apiRef.current.isRowSelectable(id)) {
373
+ selectionManager.select(id);
374
+ hasChanged = true;
375
+ }
376
+ }
377
+ }
345
378
  for (const id of currentSelection.ids) {
346
379
  if (isNonExistent(id)) {
347
380
  if (props.keepNonExistentRowsSelected) {
@@ -374,9 +407,10 @@ You need to upgrade to DataGridPro or DataGridPremium component to unlock multip
374
407
  // For nested data, on row tree updation (filtering, adding rows, etc.) when the selection is
375
408
  // not empty, we need to re-run scanning of the tree to propagate the selection changes
376
409
  // Example: A parent whose de-selected children are filtered out should now be selected
377
- const shouldReapplyPropagation = isNestedData && props.rowSelectionPropagation?.parents && (newSelectionModel.ids.size > 0 ||
378
- // In case of exclude selection, newSelectionModel.ids.size === 0 means all rows are selected
379
- newSelectionModel.type === 'exclude');
410
+ // With a data source, an `exclude` model is left alone: the grid only knows the loaded
411
+ // subset of the rows, so rewriting the model into an `include` of the known rows would
412
+ // drop the "rows that load later are selected too" semantics of the select-all state.
413
+ const shouldReapplyPropagation = isNested && props.rowSelectionPropagation?.parents && (newSelectionModel.type === 'exclude' ? props.dataSource === undefined : newSelectionModel.ids.size > 0);
380
414
  if (hasChanged || shouldReapplyPropagation) {
381
415
  if (shouldReapplyPropagation) {
382
416
  if (newSelectionModel.type === 'exclude') {
@@ -390,13 +424,20 @@ You need to upgrade to DataGridPro or DataGridPremium component to unlock multip
390
424
  }
391
425
  apiRef.current.selectRows(selectedRowIds, true, true);
392
426
  } else {
393
- apiRef.current.selectRows(Array.from(newSelectionModel.ids), true, true);
427
+ // `getPropagatedRowSelectionModel` reads the sibling selection state from the model
428
+ // built above, so the rows selected during this run count for the parent derivation.
429
+ // `selectRows` would read the state from before this run instead.
430
+ const propagatedModel = apiRef.current.getPropagatedRowSelectionModel(newSelectionModel);
431
+ const isSelectionValid = propagatedModel.ids.size < 2 || canHaveMultipleSelection;
432
+ if (isSelectionValid && (propagatedModel.type !== currentSelection.type || propagatedModel.ids.size !== currentSelection.ids.size || !Array.from(propagatedModel.ids).every(id => currentSelection.ids.has(id)))) {
433
+ apiRef.current.setRowSelectionModel(propagatedModel, 'multipleRowsSelection');
434
+ }
394
435
  }
395
436
  } else {
396
437
  apiRef.current.setRowSelectionModel(newSelectionModel, 'multipleRowsSelection');
397
438
  }
398
439
  }
399
- }, [apiRef, isNestedData, props.rowSelectionPropagation?.parents, props.keepNonExistentRowsSelected, props.filterMode, getRowsToBeSelected]);
440
+ }, [apiRef, props.rowSelectionPropagation?.parents, props.rowSelectionPropagation?.descendants, props.keepNonExistentRowsSelected, props.filterMode, props.dataSource, canHaveMultipleSelection, getRowsToBeSelected]);
400
441
  const handleSingleRowSelection = React.useCallback((id, event) => {
401
442
  const hasCtrlKey = event.metaKey || event.ctrlKey;
402
443
 
@@ -534,7 +575,7 @@ You need to upgrade to DataGridPro or DataGridPremium component to unlock multip
534
575
  handleSingleRowSelection(params.id, event);
535
576
  return;
536
577
  }
537
- if (String.fromCharCode(event.keyCode) === 'A' && (event.ctrlKey || event.metaKey)) {
578
+ if (isSelectAllShortcut(event)) {
538
579
  event.preventDefault();
539
580
  if (canHaveMultipleSelection) {
540
581
  toggleAllRows(true);
@@ -67,6 +67,11 @@ const checkboxPropsSelector = exports.checkboxPropsSelector = (0, _createSelecto
67
67
  return;
68
68
  }
69
69
  const node = rowTree[itemToTraverseId];
70
+ // Skeleton rows are placeholders for rows that are not loaded yet.
71
+ // They have no selection state, so they do not affect the parent checkbox state.
72
+ if (node?.type === 'skeletonRow') {
73
+ return;
74
+ }
70
75
  if (node?.type === 'group') {
71
76
  node.children.forEach(traverseDescendants);
72
77
  }
@@ -60,6 +60,11 @@ export const checkboxPropsSelector = createSelector(gridColumnDefinitionsSelecto
60
60
  return;
61
61
  }
62
62
  const node = rowTree[itemToTraverseId];
63
+ // Skeleton rows are placeholders for rows that are not loaded yet.
64
+ // They have no selection state, so they do not affect the parent checkbox state.
65
+ if (node?.type === 'skeletonRow') {
66
+ return;
67
+ }
63
68
  if (node?.type === 'group') {
64
69
  node.children.forEach(traverseDescendants);
65
70
  }
@@ -1,5 +1,5 @@
1
1
  import type { RefObject } from '@mui/x-internals/types';
2
- import type { GridAutoGeneratedGroupNode, GridAutoGeneratedPinnedRowNode, GridColDef, GridFooterNode, GridGroupNode, GridRowId, GridRowIdGetter, GridRowModel, GridRowModelUpdate, GridRowTreeConfig, GridSkeletonRowNode, GridTreeNode, GridValidRowModel } from "../../../models/index.mjs";
2
+ import type { GridAutoGeneratedGroupNode, GridAutoGeneratedPinnedRowNode, GridColDef, GridFooterNode, GridGroupNode, GridRowId, GridRowIdGetter, GridRowModel, GridRowModelUpdate, GridRowModelReplace, GridRowTreeConfig, GridSkeletonRowNode, GridTreeNode, GridValidRowModel } from "../../../models/index.mjs";
3
3
  import type { DataGridProcessedProps } from "../../../models/props/DataGridProps.mjs";
4
4
  import type { GridApiCommunity, GridPrivateApiCommunity } from "../../../models/api/gridApiCommunity.mjs";
5
5
  import type { GridRowsInternalCache, GridRowsState, GridRowTreeCreationParams } from "./gridRowsInterfaces.mjs";
@@ -43,6 +43,40 @@ export declare const getRowsStateFromCache: ({
43
43
  export declare const isAutogeneratedRow: (row: GridRowModel) => boolean;
44
44
  export declare const isAutogeneratedRowNode: (rowNode: GridTreeNode) => rowNode is GridFooterNode | GridSkeletonRowNode | GridAutoGeneratedGroupNode | GridAutoGeneratedPinnedRowNode;
45
45
  export declare const getTreeNodeDescendants: (tree: GridRowTreeConfig, parentId: GridRowId, skipAutoGeneratedRows: boolean, directChildrenOnly?: boolean) => GridRowId[];
46
+ export declare const isReplaceUpdate: (update: GridRowModelUpdate | GridRowModelReplace) => update is GridRowModelReplace;
47
+ /**
48
+ * Extracts the replacement row from a `{ _action: 'replace', row }` update.
49
+ * The marker lives on the throwaway envelope, so the row object itself is never touched.
50
+ * @param {GridRowModelReplace} update The update provided to `updateRows()`.
51
+ * @returns {GridRowModel} The object to store as the row, verbatim.
52
+ */
53
+ export declare const getReplaceRow: (update: GridRowModelReplace) => GridRowModel;
54
+ /**
55
+ * Merges a partial update into the existing row.
56
+ * Prototype-preservation rules:
57
+ * 1. If partialRow is a class instance (non-plain object), use its prototype — the
58
+ * caller is passing in a fully-constructed instance and owns the prototype chain.
59
+ * 2. If partialRow is a plain object (Object.prototype), prefer oldRow's prototype so
60
+ * that a partial-update object does not silently discard the existing row's class.
61
+ * 3. If both are plain objects the result is also a plain object (no-op).
62
+ *
63
+ * Note: private class fields (#field syntax) cannot be preserved through a spread-style
64
+ * merge because the brand check is tied to the original instance. Rows that rely on
65
+ * private fields must either supply a fully-constructed instance as partialRow (rule 1
66
+ * above) or use `_action: 'replace'` rather than a plain-object partial update.
67
+ * @param {GridRowModel} oldRow The row currently stored in the lookup.
68
+ * @param {GridRowModelUpdate} partialRow The update to merge into it.
69
+ * @returns {GridRowModel} The merged row.
70
+ */
71
+ export declare const mergeRowUpdate: (oldRow: GridRowModel, partialRow: GridRowModelUpdate) => GridRowModel;
72
+ /**
73
+ * Warns when a replace demotes a class instance to a plain object, which happens when the
74
+ * replacement was built with a spread instead of being passed as-is.
75
+ * Dev-only, does nothing in production.
76
+ * @param {GridRowModel | undefined} oldRow The row being replaced, if any.
77
+ * @param {GridRowModel} replacement The row provided in a `{ _action: 'replace', row }` update.
78
+ */
79
+ export declare const warnIfReplaceLosesPrototype: (oldRow: GridRowModel | undefined, replacement: GridRowModel) => void;
46
80
  export declare const updateCacheWithNewRows: ({
47
81
  previousCache,
48
82
  getRowId,
@@ -51,10 +85,10 @@ export declare const updateCacheWithNewRows: ({
51
85
  }: {
52
86
  previousCache: GridRowsInternalCache;
53
87
  getRowId: DataGridProcessedProps['getRowId'];
54
- updates: GridRowModelUpdate[];
88
+ updates: Array<GridRowModelUpdate | GridRowModelReplace>;
55
89
  groupKeys?: string[];
56
90
  }) => GridRowsInternalCache;
57
91
  export declare const minimalContentHeight = "var(--DataGrid-overlayHeight, calc(var(--height) * 2))";
58
- export declare function computeRowsUpdates(apiRef: RefObject<GridApiCommunity>, updates: GridRowModelUpdate[], getRowId: DataGridProcessedProps['getRowId']): GridRowModelUpdate[];
92
+ export declare function computeRowsUpdates(apiRef: RefObject<GridApiCommunity>, updates: Array<GridRowModelUpdate | GridRowModelReplace>, getRowId: DataGridProcessedProps['getRowId']): (GridRowModelReplace<GridValidRowModel> | GridRowModelUpdate)[];
59
93
  export declare const getValidRowHeight: (rowHeightProp: any, defaultRowHeight: number, warningMessage: string) => number;
60
94
  export declare const rowHeightWarning: string;
@@ -1,5 +1,5 @@
1
1
  import type { RefObject } from '@mui/x-internals/types';
2
- import type { GridAutoGeneratedGroupNode, GridAutoGeneratedPinnedRowNode, GridColDef, GridFooterNode, GridGroupNode, GridRowId, GridRowIdGetter, GridRowModel, GridRowModelUpdate, GridRowTreeConfig, GridSkeletonRowNode, GridTreeNode, GridValidRowModel } from "../../../models/index.js";
2
+ import type { GridAutoGeneratedGroupNode, GridAutoGeneratedPinnedRowNode, GridColDef, GridFooterNode, GridGroupNode, GridRowId, GridRowIdGetter, GridRowModel, GridRowModelUpdate, GridRowModelReplace, GridRowTreeConfig, GridSkeletonRowNode, GridTreeNode, GridValidRowModel } from "../../../models/index.js";
3
3
  import type { DataGridProcessedProps } from "../../../models/props/DataGridProps.js";
4
4
  import type { GridApiCommunity, GridPrivateApiCommunity } from "../../../models/api/gridApiCommunity.js";
5
5
  import type { GridRowsInternalCache, GridRowsState, GridRowTreeCreationParams } from "./gridRowsInterfaces.js";
@@ -43,6 +43,40 @@ export declare const getRowsStateFromCache: ({
43
43
  export declare const isAutogeneratedRow: (row: GridRowModel) => boolean;
44
44
  export declare const isAutogeneratedRowNode: (rowNode: GridTreeNode) => rowNode is GridFooterNode | GridSkeletonRowNode | GridAutoGeneratedGroupNode | GridAutoGeneratedPinnedRowNode;
45
45
  export declare const getTreeNodeDescendants: (tree: GridRowTreeConfig, parentId: GridRowId, skipAutoGeneratedRows: boolean, directChildrenOnly?: boolean) => GridRowId[];
46
+ export declare const isReplaceUpdate: (update: GridRowModelUpdate | GridRowModelReplace) => update is GridRowModelReplace;
47
+ /**
48
+ * Extracts the replacement row from a `{ _action: 'replace', row }` update.
49
+ * The marker lives on the throwaway envelope, so the row object itself is never touched.
50
+ * @param {GridRowModelReplace} update The update provided to `updateRows()`.
51
+ * @returns {GridRowModel} The object to store as the row, verbatim.
52
+ */
53
+ export declare const getReplaceRow: (update: GridRowModelReplace) => GridRowModel;
54
+ /**
55
+ * Merges a partial update into the existing row.
56
+ * Prototype-preservation rules:
57
+ * 1. If partialRow is a class instance (non-plain object), use its prototype — the
58
+ * caller is passing in a fully-constructed instance and owns the prototype chain.
59
+ * 2. If partialRow is a plain object (Object.prototype), prefer oldRow's prototype so
60
+ * that a partial-update object does not silently discard the existing row's class.
61
+ * 3. If both are plain objects the result is also a plain object (no-op).
62
+ *
63
+ * Note: private class fields (#field syntax) cannot be preserved through a spread-style
64
+ * merge because the brand check is tied to the original instance. Rows that rely on
65
+ * private fields must either supply a fully-constructed instance as partialRow (rule 1
66
+ * above) or use `_action: 'replace'` rather than a plain-object partial update.
67
+ * @param {GridRowModel} oldRow The row currently stored in the lookup.
68
+ * @param {GridRowModelUpdate} partialRow The update to merge into it.
69
+ * @returns {GridRowModel} The merged row.
70
+ */
71
+ export declare const mergeRowUpdate: (oldRow: GridRowModel, partialRow: GridRowModelUpdate) => GridRowModel;
72
+ /**
73
+ * Warns when a replace demotes a class instance to a plain object, which happens when the
74
+ * replacement was built with a spread instead of being passed as-is.
75
+ * Dev-only, does nothing in production.
76
+ * @param {GridRowModel | undefined} oldRow The row being replaced, if any.
77
+ * @param {GridRowModel} replacement The row provided in a `{ _action: 'replace', row }` update.
78
+ */
79
+ export declare const warnIfReplaceLosesPrototype: (oldRow: GridRowModel | undefined, replacement: GridRowModel) => void;
46
80
  export declare const updateCacheWithNewRows: ({
47
81
  previousCache,
48
82
  getRowId,
@@ -51,10 +85,10 @@ export declare const updateCacheWithNewRows: ({
51
85
  }: {
52
86
  previousCache: GridRowsInternalCache;
53
87
  getRowId: DataGridProcessedProps['getRowId'];
54
- updates: GridRowModelUpdate[];
88
+ updates: Array<GridRowModelUpdate | GridRowModelReplace>;
55
89
  groupKeys?: string[];
56
90
  }) => GridRowsInternalCache;
57
91
  export declare const minimalContentHeight = "var(--DataGrid-overlayHeight, calc(var(--height) * 2))";
58
- export declare function computeRowsUpdates(apiRef: RefObject<GridApiCommunity>, updates: GridRowModelUpdate[], getRowId: DataGridProcessedProps['getRowId']): GridRowModelUpdate[];
92
+ export declare function computeRowsUpdates(apiRef: RefObject<GridApiCommunity>, updates: Array<GridRowModelUpdate | GridRowModelReplace>, getRowId: DataGridProcessedProps['getRowId']): (GridRowModelReplace<GridValidRowModel> | GridRowModelUpdate)[];
59
93
  export declare const getValidRowHeight: (rowHeightProp: any, defaultRowHeight: number, warningMessage: string) => number;
60
94
  export declare const rowHeightWarning: string;
@@ -7,9 +7,10 @@ Object.defineProperty(exports, "__esModule", {
7
7
  exports.buildRootGroup = exports.GRID_ROOT_GROUP_ID = exports.GRID_ID_AUTOGENERATED = void 0;
8
8
  exports.checkGridRowIdIsValid = checkGridRowIdIsValid;
9
9
  exports.computeRowsUpdates = computeRowsUpdates;
10
- exports.updateCacheWithNewRows = exports.rowHeightWarning = exports.minimalContentHeight = exports.isAutogeneratedRowNode = exports.isAutogeneratedRow = exports.getValidRowHeight = exports.getTreeNodeDescendants = exports.getTopLevelRowCount = exports.getRowsStateFromCache = exports.getRowValue = exports.getRowIdFromRowModel = exports.createRowsInternalCache = void 0;
10
+ exports.warnIfReplaceLosesPrototype = exports.updateCacheWithNewRows = exports.rowHeightWarning = exports.minimalContentHeight = exports.mergeRowUpdate = exports.isReplaceUpdate = exports.isAutogeneratedRowNode = exports.isAutogeneratedRow = exports.getValidRowHeight = exports.getTreeNodeDescendants = exports.getTopLevelRowCount = exports.getRowsStateFromCache = exports.getRowValue = exports.getRowIdFromRowModel = exports.getReplaceRow = exports.createRowsInternalCache = void 0;
11
11
  var _extends2 = _interopRequireDefault(require("@babel/runtime/helpers/extends"));
12
12
  var _formatErrorMessage2 = _interopRequireDefault(require("@mui/x-internals/formatErrorMessage"));
13
+ var _warning = require("@mui/x-internals/warning");
13
14
  var _gridRowsSelector = require("./gridRowsSelector");
14
15
  const GRID_ROOT_GROUP_ID = exports.GRID_ROOT_GROUP_ID = `auto-generated-group-node-root`;
15
16
  const GRID_ID_AUTOGENERATED = exports.GRID_ID_AUTOGENERATED = Symbol('mui.id_autogenerated');
@@ -178,6 +179,83 @@ const getTreeNodeDescendants = (tree, parentId, skipAutoGeneratedRows, directChi
178
179
  return validDescendants;
179
180
  };
180
181
  exports.getTreeNodeDescendants = getTreeNodeDescendants;
182
+ const isReplaceUpdate = update =>
183
+ // eslint-disable-next-line no-underscore-dangle
184
+ update._action === 'replace';
185
+
186
+ /**
187
+ * Extracts the replacement row from a `{ _action: 'replace', row }` update.
188
+ * The marker lives on the throwaway envelope, so the row object itself is never touched.
189
+ * @param {GridRowModelReplace} update The update provided to `updateRows()`.
190
+ * @returns {GridRowModel} The object to store as the row, verbatim.
191
+ */
192
+ exports.isReplaceUpdate = isReplaceUpdate;
193
+ const getReplaceRow = update => {
194
+ const {
195
+ row
196
+ } = update;
197
+ if (row == null) {
198
+ throw new Error(process.env.NODE_ENV !== "production" ? `MUI X Data Grid: A row update with \`_action: 'replace'\` was provided without a \`row\` property.
199
+ Without it, the Data Grid does not know which object to store for that row.
200
+ Provide the replacement object in the \`row\` property, for example \`updateRows([{ _action: 'replace', row }])\`.
201
+ For more detail, see https://mui.com/x/react-data-grid/row-updates/.` : (0, _formatErrorMessage2.default)(304));
202
+ }
203
+ return row;
204
+ };
205
+
206
+ /**
207
+ * Merges a partial update into the existing row.
208
+ * Prototype-preservation rules:
209
+ * 1. If partialRow is a class instance (non-plain object), use its prototype — the
210
+ * caller is passing in a fully-constructed instance and owns the prototype chain.
211
+ * 2. If partialRow is a plain object (Object.prototype), prefer oldRow's prototype so
212
+ * that a partial-update object does not silently discard the existing row's class.
213
+ * 3. If both are plain objects the result is also a plain object (no-op).
214
+ *
215
+ * Note: private class fields (#field syntax) cannot be preserved through a spread-style
216
+ * merge because the brand check is tied to the original instance. Rows that rely on
217
+ * private fields must either supply a fully-constructed instance as partialRow (rule 1
218
+ * above) or use `_action: 'replace'` rather than a plain-object partial update.
219
+ * @param {GridRowModel} oldRow The row currently stored in the lookup.
220
+ * @param {GridRowModelUpdate} partialRow The update to merge into it.
221
+ * @returns {GridRowModel} The merged row.
222
+ */
223
+ exports.getReplaceRow = getReplaceRow;
224
+ const mergeRowUpdate = (oldRow, partialRow) => {
225
+ const partialRowProto = Object.getPrototypeOf(partialRow);
226
+ const isPartialRowPlain = partialRowProto === Object.prototype || partialRowProto === null;
227
+ const proto = isPartialRowPlain ? Object.getPrototypeOf(oldRow) : partialRowProto;
228
+ const merged = Object.create(proto);
229
+ Object.assign(merged, oldRow, partialRow);
230
+ return merged;
231
+ };
232
+
233
+ /**
234
+ * Warns when a replace demotes a class instance to a plain object, which happens when the
235
+ * replacement was built with a spread instead of being passed as-is.
236
+ * Dev-only, does nothing in production.
237
+ * @param {GridRowModel | undefined} oldRow The row being replaced, if any.
238
+ * @param {GridRowModel} replacement The row provided in a `{ _action: 'replace', row }` update.
239
+ */
240
+ exports.mergeRowUpdate = mergeRowUpdate;
241
+ const warnIfReplaceLosesPrototype = (oldRow, replacement) => {
242
+ if (process.env.NODE_ENV === 'production' || !oldRow) {
243
+ return;
244
+ }
245
+ const replacementProto = Object.getPrototypeOf(replacement);
246
+ if (replacementProto !== Object.prototype && replacementProto !== null) {
247
+ return;
248
+ }
249
+ const oldRowProto = Object.getPrototypeOf(oldRow);
250
+ if (oldRowProto === Object.prototype || oldRowProto === null) {
251
+ return;
252
+ }
253
+
254
+ // The message must stay free of row-specific details: `warnOnce()` caches by message, so
255
+ // interpolating the id would log once per row and grow the cache with every update.
256
+ (0, _warning.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/.']);
257
+ };
258
+ exports.warnIfReplaceLosesPrototype = warnIfReplaceLosesPrototype;
181
259
  const updateCacheWithNewRows = ({
182
260
  previousCache,
183
261
  getRowId,
@@ -191,10 +269,28 @@ const updateCacheWithNewRows = ({
191
269
  // Remove duplicate updates.
192
270
  // A server can batch updates, and send several updates for the same row in one fn call.
193
271
  const uniqueUpdates = new Map();
272
+ // Ids whose accumulated update is stored as the row instead of being merged into the
273
+ // existing one. The flag lives outside the row object so that it is never mutated.
274
+ const replaceIds = new Set();
194
275
  updates.forEach(update => {
195
- const id = getRowIdFromRowModel(update, getRowId, 'A row was provided without id when calling updateRows():');
196
- if (uniqueUpdates.has(id)) {
197
- uniqueUpdates.set(id, (0, _extends2.default)({}, uniqueUpdates.get(id), update));
276
+ // A replace is provided as a `{ _action: 'replace', row }` envelope: the marker stays
277
+ // on the throwaway envelope and the inner row is used verbatim, so that the object
278
+ // provided by the caller is the one that ends up in the lookup.
279
+ const isReplace = isReplaceUpdate(update);
280
+ const row = isReplace ? getReplaceRow(update) : update;
281
+ const id = getRowIdFromRowModel(row, getRowId, 'A row was provided without id when calling updateRows():');
282
+ if (isReplace) {
283
+ // A replace resets whatever has been accumulated for that id.
284
+ uniqueUpdates.set(id, row);
285
+ replaceIds.add(id);
286
+ } else if (uniqueUpdates.has(id)) {
287
+ const accumulatedUpdate = uniqueUpdates.get(id);
288
+ if (process.env.NODE_ENV !== 'production' && replaceIds.has(id)) {
289
+ (0, _warning.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/.']);
290
+ }
291
+ // The merge keeps the accumulated entry's prototype and the id stays in `replaceIds`,
292
+ // so the result is still applied as a replace.
293
+ uniqueUpdates.set(id, mergeRowUpdate(accumulatedUpdate, update));
198
294
  } else {
199
295
  uniqueUpdates.set(id, update);
200
296
  }
@@ -246,6 +342,15 @@ const updateCacheWithNewRows = ({
246
342
  }
247
343
  const oldRow = dataRowIdToModelLookup[id];
248
344
 
345
+ // Action === "replace"
346
+ // The bookkeeping is the same as for "modify" / "insert" below, only the way the row
347
+ // is stored differs: the object is stored as-is so that its prototype chain, private
348
+ // fields (#field) and reference identity are all preserved.
349
+ const isReplace = replaceIds.has(id);
350
+ if (isReplace) {
351
+ warnIfReplaceLosesPrototype(oldRow, partialRow);
352
+ }
353
+
249
354
  // Action === "modify"
250
355
  if (oldRow) {
251
356
  // If the data row has been removed since the last state update,
@@ -261,23 +366,7 @@ const updateCacheWithNewRows = ({
261
366
  }
262
367
 
263
368
  // Update the data row lookups.
264
- // Prototype-preservation rules:
265
- // 1. If partialRow is a class instance (non-plain object), use its prototype — the
266
- // caller is passing in a fully-constructed instance and owns the prototype chain.
267
- // 2. If partialRow is a plain object (Object.prototype), prefer oldRow's prototype so
268
- // that a partial-update object does not silently discard the existing row's class.
269
- // 3. If both are plain objects the result is also a plain object (no-op).
270
- //
271
- // Note: private class fields (#field syntax) cannot be preserved through a spread-style
272
- // merge because the brand check is tied to the original instance. Rows that rely on
273
- // private fields must supply a fully-constructed instance as partialRow (rule 1 above)
274
- // rather than a plain-object partial update.
275
- const partialRowProto = Object.getPrototypeOf(partialRow);
276
- const isPartialRowPlain = partialRowProto === Object.prototype || partialRowProto === null;
277
- const proto = isPartialRowPlain ? Object.getPrototypeOf(oldRow) : partialRowProto;
278
- const merged = Object.create(proto);
279
- Object.assign(merged, oldRow, partialRow);
280
- dataRowIdToModelLookup[id] = merged;
369
+ dataRowIdToModelLookup[id] = isReplace ? partialRow : mergeRowUpdate(oldRow, partialRow);
281
370
  return;
282
371
  }
283
372
 
@@ -319,16 +408,24 @@ const minimalContentHeight = exports.minimalContentHeight = 'var(--DataGrid-over
319
408
  function computeRowsUpdates(apiRef, updates, getRowId) {
320
409
  const nonPinnedRowsUpdates = [];
321
410
  updates.forEach(update => {
322
- const id = getRowIdFromRowModel(update, getRowId, 'A row was provided without id when calling updateRows():');
411
+ const isReplace = isReplaceUpdate(update);
412
+ const row = isReplace ? getReplaceRow(update) : update;
413
+ const id = getRowIdFromRowModel(row, getRowId, 'A row was provided without id when calling updateRows():');
323
414
  const rowNode = (0, _gridRowsSelector.gridRowNodeSelector)(apiRef, id);
324
415
  if (rowNode?.type === 'pinnedRow') {
325
416
  // @ts-ignore because otherwise `release:build` doesn't work
326
417
  const pinnedRowsCache = apiRef.current.caches.pinnedRows;
327
418
  const prevModel = pinnedRowsCache.idLookup[id];
328
419
  if (prevModel) {
329
- pinnedRowsCache.idLookup[id] = (0, _extends2.default)({}, prevModel, update);
420
+ if (isReplace) {
421
+ warnIfReplaceLosesPrototype(prevModel, row);
422
+ pinnedRowsCache.idLookup[id] = row;
423
+ } else {
424
+ pinnedRowsCache.idLookup[id] = mergeRowUpdate(prevModel, update);
425
+ }
330
426
  }
331
427
  } else {
428
+ // The envelope is passed through untouched, `updateCacheWithNewRows` unwraps it.
332
429
  nonPinnedRowsUpdates.push(update);
333
430
  }
334
431
  });