@opencorestack/opengridx 2.0.4 → 3.0.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 (221) hide show
  1. package/CHANGELOG.md +408 -3
  2. package/README.md +93 -72
  3. package/dist/index.d.ts +611 -121
  4. package/dist/opengridx.css +1 -1
  5. package/dist/opengridx.es.js +7992 -5650
  6. package/dist/opengridx.es.js.map +1 -1
  7. package/dist/opengridx.umd.js +17 -23
  8. package/dist/opengridx.umd.js.map +1 -1
  9. package/docs/API_REFERENCE.md +348 -161
  10. package/docs/README.md +6 -1
  11. package/docs/architecture/datagrid-orchestration.md +81 -0
  12. package/docs/architecture/grid-row-meta.md +102 -0
  13. package/docs/architecture/use-grid-columns.md +128 -0
  14. package/docs/architecture/use-grid-controlled-state.md +157 -0
  15. package/docs/architecture/use-grid-row-pipeline.md +120 -0
  16. package/docs/architecture/use-grid-scroll-sync.md +111 -0
  17. package/docs/architecture/use-grid-state-snapshot.md +90 -0
  18. package/docs/architecture/use-grid-virtualization.md +115 -0
  19. package/docs/architecture/use-grid-visible-rows.md +85 -0
  20. package/docs/assets/banner-alt.png +0 -0
  21. package/docs/assets/banner.png +0 -0
  22. package/docs/assets/hero.png +0 -0
  23. package/docs/assets/logo.png +0 -0
  24. package/docs/components/aggregation-footer.md +15 -10
  25. package/docs/components/cell.md +16 -6
  26. package/docs/components/column-group-header.md +55 -28
  27. package/docs/components/column-resize.md +17 -13
  28. package/docs/components/column-visibility.md +45 -22
  29. package/docs/components/datagrid.md +14 -12
  30. package/docs/components/empty-state.md +2 -1
  31. package/docs/components/error-overlay.md +12 -7
  32. package/docs/components/filter-panel.md +28 -8
  33. package/docs/components/header.md +28 -9
  34. package/docs/components/pagination.md +14 -8
  35. package/docs/components/row.md +21 -5
  36. package/docs/components/toolbar.md +27 -9
  37. package/docs/components/tooltip.md +16 -7
  38. package/docs/customization/slots-api.md +100 -36
  39. package/docs/customization/theming.md +66 -18
  40. package/docs/features/aggregation-pivot.md +75 -9
  41. package/docs/features/cell-spanning.md +80 -125
  42. package/docs/features/clipboard.md +41 -16
  43. package/docs/features/custom-pagination.md +38 -18
  44. package/docs/features/data-source.md +36 -13
  45. package/docs/features/editing-reordering.md +125 -16
  46. package/docs/features/export-guide.md +147 -41
  47. package/docs/features/filtering.md +44 -6
  48. package/docs/features/infinite-scroll.md +54 -46
  49. package/docs/features/keyboard-navigation.md +83 -0
  50. package/docs/features/list-view.md +58 -5
  51. package/docs/features/loading-states.md +18 -20
  52. package/docs/features/master-detail.md +18 -5
  53. package/docs/features/pdf-export.md +56 -19
  54. package/docs/features/pinning.md +23 -5
  55. package/docs/features/selection.md +50 -10
  56. package/docs/features/sorting-pagination.md +66 -14
  57. package/docs/features/state-persistence.md +78 -10
  58. package/docs/features/toolbar-customization.md +26 -17
  59. package/docs/features/tree-data-grouping.md +44 -9
  60. package/docs/features/virtualization.md +64 -15
  61. package/docs/migration/v1-to-v2.md +115 -0
  62. package/docs/migration/v2-to-v3.md +1222 -0
  63. package/docs/performance.md +124 -0
  64. package/docs/roadmap.md +16 -10
  65. package/lib/components/Cell/Cell.css +13 -1
  66. package/lib/components/Cell/Cell.tsx +130 -57
  67. package/lib/components/Cell/CellErrorBoundary.tsx +12 -6
  68. package/lib/components/Cell/GridEditInputCell.tsx +114 -41
  69. package/lib/components/ColumnGroupHeader/ColumnGroupHeader.css +7 -1
  70. package/lib/components/ColumnResizeHandle/ColumnResizeHandle.css +11 -0
  71. package/lib/components/ColumnResizeHandle/ColumnResizeHandle.tsx +104 -38
  72. package/lib/components/ColumnResizeHandle/clampResizeWidth.ts +13 -0
  73. package/lib/components/ColumnVisibilityPanel/ColumnVisibilityPanel.css +6 -0
  74. package/lib/components/ColumnVisibilityPanel/ColumnVisibilityPanel.tsx +21 -13
  75. package/lib/components/DataGrid/DataGrid.css +84 -12
  76. package/lib/components/DataGrid/DataGrid.tsx +606 -872
  77. package/lib/components/DataGrid/GridAggregationFooter.tsx +53 -17
  78. package/lib/components/DataGrid/GridEmptyState.tsx +7 -1
  79. package/lib/components/DataGrid/GridErrorOverlay.tsx +15 -12
  80. package/lib/components/DataGrid/GridListView.tsx +166 -23
  81. package/lib/components/DataGrid/GridLiveRegion.tsx +31 -0
  82. package/lib/components/DataGrid/GridLoadingOverlay.tsx +27 -0
  83. package/lib/components/DataGrid/GridPaginationArea.tsx +44 -0
  84. package/lib/components/DataGrid/GridPinnedRows.tsx +65 -11
  85. package/lib/components/DataGrid/GridStandaloneColumnPanel.tsx +40 -38
  86. package/lib/components/DataGrid/GridToolbarSlot.tsx +51 -0
  87. package/lib/components/DataGrid/GridVirtualRows.tsx +48 -25
  88. package/lib/components/DetailPanel/DetailPanel.css +0 -6
  89. package/lib/components/DetailPanel/DetailPanel.tsx +34 -5
  90. package/lib/components/DetailPanel/DetailPanelContent.tsx +17 -0
  91. package/lib/components/FilterPanel/FilterPanel.css +13 -2
  92. package/lib/components/FilterPanel/FilterPanel.tsx +272 -99
  93. package/lib/components/Header/ColumnMenu.tsx +91 -39
  94. package/lib/components/Header/Header.css +3 -3
  95. package/lib/components/Header/Header.tsx +179 -160
  96. package/lib/components/ListView/ListView.css +25 -0
  97. package/lib/components/ListView/ListViewRow.tsx +79 -10
  98. package/lib/components/Pagination/Pagination.tsx +15 -7
  99. package/lib/components/Row/Row.tsx +185 -75
  100. package/lib/components/Toolbar/GlobalSearch.css +6 -3
  101. package/lib/components/Toolbar/GlobalSearch.tsx +26 -27
  102. package/lib/components/Toolbar/GridToolbar.css +12 -11
  103. package/lib/components/Toolbar/GridToolbar.tsx +133 -89
  104. package/lib/components/Toolbar/PivotPanel.tsx +89 -49
  105. package/lib/components/Tooltip/Tooltip.css +20 -0
  106. package/lib/components/Tooltip/Tooltip.tsx +68 -35
  107. package/lib/components/ui/Button.tsx +3 -0
  108. package/lib/components/ui/Checkbox.tsx +2 -1
  109. package/lib/components/ui/ExpandIcon.css +0 -9
  110. package/lib/components/ui/ExpandIcon.tsx +4 -1
  111. package/lib/hooks/core/gridApiPlaceholder.ts +34 -0
  112. package/lib/hooks/core/gridToolbarHostContext.ts +14 -0
  113. package/lib/hooks/core/useDataGrid.ts +115 -173
  114. package/lib/hooks/core/useGridAggregationApi.ts +72 -0
  115. package/lib/hooks/core/useGridApiMethods.ts +115 -0
  116. package/lib/hooks/core/useGridApiRef.ts +15 -3
  117. package/lib/hooks/core/useGridApiRefBinding.ts +18 -0
  118. package/lib/hooks/core/useGridAriaRows.ts +38 -0
  119. package/lib/hooks/core/useGridClipboardApi.ts +38 -0
  120. package/lib/hooks/core/useGridColumnLookup.ts +30 -0
  121. package/lib/hooks/core/useGridColumns.tsx +284 -76
  122. package/lib/hooks/core/useGridColumnsPanel.ts +77 -0
  123. package/lib/hooks/core/useGridControlledState.ts +99 -6
  124. package/lib/hooks/core/useGridDetailPanel.ts +51 -0
  125. package/lib/hooks/core/useGridDevWarnings.ts +123 -0
  126. package/lib/hooks/core/useGridFocusHandlers.ts +50 -0
  127. package/lib/hooks/core/useGridGroupingColumn.ts +62 -0
  128. package/lib/hooks/core/useGridHeaderHandlers.ts +92 -0
  129. package/lib/hooks/core/useGridHierarchy.ts +81 -0
  130. package/lib/hooks/core/useGridKeyboardNavigation.ts +452 -250
  131. package/lib/hooks/core/useGridLiveRowSelection.ts +67 -0
  132. package/lib/hooks/core/useGridPageCorrection.ts +55 -0
  133. package/lib/hooks/core/useGridRowIdOf.ts +32 -0
  134. package/lib/hooks/core/useGridRowInteractions.ts +81 -0
  135. package/lib/hooks/core/useGridRowPipeline.ts +69 -39
  136. package/lib/hooks/core/useGridRowSelection.ts +91 -0
  137. package/lib/hooks/core/useGridScrollSync.ts +80 -32
  138. package/lib/hooks/core/useGridScrollToIndexesApi.ts +45 -0
  139. package/lib/hooks/core/useGridStateSnapshot.ts +40 -5
  140. package/lib/hooks/core/useGridThemeDimensions.ts +49 -0
  141. package/lib/hooks/core/useGridToolbarProps.ts +97 -0
  142. package/lib/hooks/core/useGridViewport.ts +38 -0
  143. package/lib/hooks/core/useGridViewportSize.ts +26 -0
  144. package/lib/hooks/core/useGridVisibleRows.ts +20 -7
  145. package/lib/hooks/core/useLayout.ts +215 -195
  146. package/lib/hooks/core/useRowsScrollEndLatch.ts +41 -0
  147. package/lib/hooks/core/useStableColumns.ts +34 -0
  148. package/lib/hooks/features/useAggregation.ts +86 -90
  149. package/lib/hooks/features/useColumnGroupReorderGuard.ts +34 -0
  150. package/lib/hooks/features/useDetailPanelHeights.ts +29 -0
  151. package/lib/hooks/features/useGridClipboard.ts +133 -73
  152. package/lib/hooks/features/useGridDataSource.ts +354 -128
  153. package/lib/hooks/features/useGridEditing.ts +211 -51
  154. package/lib/hooks/features/useGridPivot.ts +92 -0
  155. package/lib/hooks/features/useGridSpanRenderWindow.ts +104 -0
  156. package/lib/hooks/features/useGridSpanning.ts +410 -306
  157. package/lib/hooks/features/usePivot.ts +0 -0
  158. package/lib/hooks/features/useServerTreeChildren.ts +50 -0
  159. package/lib/hooks/useColumnReorder.ts +38 -44
  160. package/lib/hooks/useRowGrouping.ts +335 -214
  161. package/lib/hooks/useRowReorder.ts +62 -31
  162. package/lib/hooks/useTreeData.ts +330 -262
  163. package/lib/index.ts +30 -2
  164. package/lib/state/index.ts +1 -0
  165. package/lib/state/types.ts +7 -1
  166. package/lib/state/useGridStateStorage.ts +88 -29
  167. package/lib/theme/DataGridThemeProvider.tsx +143 -38
  168. package/lib/theme/gridThemeContext.ts +5 -0
  169. package/lib/theme/index.ts +5 -0
  170. package/lib/theme/presets.ts +5 -4
  171. package/lib/theme/types.ts +40 -2
  172. package/lib/types/index.ts +311 -108
  173. package/lib/utils/aggregation/index.ts +157 -0
  174. package/lib/utils/aria/index.ts +40 -0
  175. package/lib/utils/columnGroups.ts +110 -0
  176. package/lib/utils/columnLookup.ts +33 -0
  177. package/lib/utils/columnWidth.ts +22 -0
  178. package/lib/utils/dataSource/index.ts +46 -0
  179. package/lib/utils/detailPanel.ts +66 -0
  180. package/lib/utils/dragData.ts +44 -0
  181. package/lib/utils/editing/index.ts +30 -0
  182. package/lib/utils/export/exportShared.ts +197 -0
  183. package/lib/utils/export/exportToExcelAdvanced.ts +344 -121
  184. package/lib/utils/export/exportToPdf.ts +143 -92
  185. package/lib/utils/export/groupLabel.ts +14 -0
  186. package/lib/utils/export/index.ts +278 -272
  187. package/lib/utils/export/pdfText.ts +50 -0
  188. package/lib/utils/filtering/index.ts +277 -148
  189. package/lib/utils/focus/index.ts +115 -0
  190. package/lib/utils/gridApiRows.ts +35 -0
  191. package/lib/utils/gridModes.ts +77 -0
  192. package/lib/utils/grouping/groupedExportRows.ts +91 -0
  193. package/lib/utils/pagination/index.ts +56 -0
  194. package/lib/utils/pinning/index.ts +77 -33
  195. package/lib/utils/pivot/index.ts +368 -0
  196. package/lib/utils/scroll/index.ts +78 -0
  197. package/lib/utils/scroll/scrollEnd.ts +18 -0
  198. package/lib/utils/sorting/index.ts +154 -30
  199. package/lib/utils/syntheticRows.ts +12 -0
  200. package/lib/utils/values.ts +206 -0
  201. package/lib/utils/viewport.ts +14 -0
  202. package/llms.txt +272 -166
  203. package/package.json +18 -6
  204. package/lib/components/ColumnGroupHeader/ColumnGroupHeader.tsx +0 -220
  205. package/lib/components/ColumnVisibilityPanel/ColumnVisibilityPanel.test.tsx +0 -85
  206. package/lib/components/DataGrid/DataGrid.regression.test.tsx +0 -132
  207. package/lib/components/DataGrid/DataGrid.smoke.test.tsx +0 -58
  208. package/lib/components/DataGrid/DataGrid.v2.test.tsx +0 -127
  209. package/lib/components/Header/Header.test.tsx +0 -135
  210. package/lib/components/Pagination/Pagination.test.tsx +0 -64
  211. package/lib/hooks/core/useDataGrid.regression.test.ts +0 -66
  212. package/lib/hooks/core/useGridControlledState.test.ts +0 -86
  213. package/lib/hooks/core/useGridKeyboardNavigation.test.ts +0 -176
  214. package/lib/hooks/core/useGridRowPipeline.test.ts +0 -117
  215. package/lib/hooks/features/useAggregation.test.ts +0 -175
  216. package/lib/hooks/features/usePivot.test.ts +0 -202
  217. package/lib/hooks/useRowGrouping.test.ts +0 -137
  218. package/lib/utils/export/exportToPdf.test.ts +0 -120
  219. package/lib/utils/export/groupedExport.test.ts +0 -144
  220. package/lib/utils/filtering/filtering.test.ts +0 -178
  221. package/lib/utils/sorting/sorting.test.ts +0 -88
@@ -1,38 +1,72 @@
1
1
 
2
- import { useReducer, useCallback, useState, useRef, useEffect, useLayoutEffect } from 'react';
2
+ import { useReducer, useCallback, useState, useRef, useLayoutEffect } from 'react';
3
3
  import type {
4
4
  GridInternalState as GridState,
5
5
  GridRowModel,
6
6
  GridColDef,
7
7
  GridRowId,
8
- GridSortItem,
9
- GridFilterModel,
10
- GridPaginationModel,
11
8
  GridColumnVisibilityModel,
12
9
  GridApi
13
10
  } from '../../types';
11
+ import { createGridApiPlaceholder } from './gridApiPlaceholder';
12
+
13
+ // Sort, filter, pagination and selection deliberately have no slice here: they live in
14
+ // useGridControlledState, which is what the grid renders from. (Copies of them in this
15
+ // reducer used to drift from it, which made the imperative API a no-op.)
16
+ type GetRowId = (row: GridRowModel) => GridRowId;
14
17
 
15
18
  type GridAction =
16
- | { type: 'SET_ROWS'; payload: GridRowModel[] | ((prev: GridRowModel[]) => GridRowModel[]); preserveRowCount?: boolean }
19
+ | { type: 'SET_ROWS'; payload: GridRowModel[] | ((prev: GridRowModel[]) => GridRowModel[]); getRowId: GetRowId }
20
+ | { type: 'REPLACE_ROW'; id: GridRowId; row: GridRowModel }
17
21
  | { type: 'SET_COLUMNS'; payload: GridColDef[] }
18
- | { type: 'SET_SORT_MODEL'; payload: GridSortItem[] }
19
- | { type: 'SET_FILTER_MODEL'; payload: GridFilterModel }
20
- | { type: 'SET_PAGINATION_MODEL'; payload: GridPaginationModel }
21
- | { type: 'SET_SELECTION'; payload: Set<GridRowId> }
22
22
  | { type: 'SET_DIMENSIONS'; payload: { viewportWidth: number; viewportHeight: number } }
23
23
  | { type: 'SET_DATASOURCE_LOADING'; payload: boolean }
24
24
  | { type: 'SET_DATASOURCE_ERROR'; payload: unknown }
25
25
  | { type: 'SET_ROW_COUNT'; payload: number };
26
26
 
27
- function createInitialState<R extends GridRowModel>(rows: R[], columns: GridColDef<R>[], columnVisibilityModel: GridColumnVisibilityModel = {}): GridState {
27
+ const defaultGetRowId: GetRowId = (row) => row.id;
28
+
29
+ // Rows are re-keyed on every rows change, so warn once rather than on every update.
30
+ let warnedDuplicateIds = false;
31
+
32
+ /**
33
+ * Keys rows by getRowId(row). The row objects are stored as given (never copied or given an
34
+ * `id` field), so `idByRow` maps each one back to its key. A row object the store already
35
+ * holds keeps its key (`knownIds`), so a committed edit stored under the edited row's id stays
36
+ * there even when the stored object does not resolve to that id itself. A duplicate id keeps
37
+ * the first row, so a later row never silently replaces it (or renders twice), and warns once
38
+ * in development.
39
+ */
40
+ function buildRowsState(rows: GridRowModel[], getRowId: GetRowId, knownIds?: Map<GridRowModel, GridRowId>): GridState['rows'] {
28
41
  const idRowsLookup = new Map<GridRowId, GridRowModel>();
42
+ const idByRow = new Map<GridRowModel, GridRowId>();
29
43
  const allRows: GridRowId[] = [];
44
+ const duplicates: GridRowId[] = [];
30
45
 
31
46
  rows.forEach(row => {
32
- idRowsLookup.set(row.id, row);
33
- allRows.push(row.id);
47
+ const id = knownIds?.get(row) ?? getRowId(row);
48
+ if (idRowsLookup.has(id)) {
49
+ duplicates.push(id);
50
+ return;
51
+ }
52
+ idRowsLookup.set(id, row);
53
+ idByRow.set(row, id);
54
+ allRows.push(id);
34
55
  });
35
56
 
57
+ if (duplicates.length > 0 && !warnedDuplicateIds && process.env.NODE_ENV !== 'production') {
58
+ warnedDuplicateIds = true;
59
+ console.warn(
60
+ `[OpenGridX] ${duplicates.length} row(s) reuse the id of an earlier row and were ignored ` +
61
+ `(ids: ${duplicates.slice(0, 5).map(String).join(', ')}${duplicates.length > 5 ? ', …' : ''}). ` +
62
+ 'Every row needs a unique id; pass getRowId if the unique key lives in another field.'
63
+ );
64
+ }
65
+
66
+ return { idRowsLookup, allRows, idByRow };
67
+ }
68
+
69
+ function createInitialState<R extends GridRowModel>(rows: R[], getRowId: GetRowId, columns: GridColDef<R>[], columnVisibilityModel: GridColumnVisibilityModel = {}): GridState {
36
70
  const columnLookup = new Map<string, GridColDef>();
37
71
  const orderedFields: string[] = [];
38
72
 
@@ -42,41 +76,20 @@ function createInitialState<R extends GridRowModel>(rows: R[], columns: GridColD
42
76
  });
43
77
 
44
78
  return {
45
- rows: {
46
- idRowsLookup,
47
- allRows
48
- },
79
+ rows: buildRowsState(rows, getRowId),
49
80
  columns: {
50
81
  all: columns as unknown as GridColDef[],
51
82
  lookup: columnLookup,
52
83
  orderedFields,
53
84
  columnVisibilityModel
54
85
  },
55
- sorting: {
56
- sortModel: []
57
- },
58
- filter: {
59
- filterModel: { items: [] }
60
- },
61
86
  pagination: {
62
- paginationModel: { page: 0, pageSize: 100 },
63
- rowCount: rows.length
64
- },
65
- selection: {
66
- selectedRows: new Set()
87
+ rowCount: undefined
67
88
  },
68
89
  pinning: {
69
90
  pinnedColumns: {},
70
91
  pinnedRows: {}
71
92
  },
72
- virtualization: {
73
- renderContext: {
74
- firstRowIndex: 0,
75
- lastRowIndex: 0,
76
- firstColumnIndex: 0,
77
- lastColumnIndex: 0
78
- },
79
- },
80
93
  dimensions: {
81
94
  rowHeight: 52,
82
95
  headerHeight: 56,
@@ -93,28 +106,31 @@ function createInitialState<R extends GridRowModel>(rows: R[], columns: GridColD
93
106
  function gridReducer(state: GridState, action: GridAction): GridState {
94
107
  switch (action.type) {
95
108
  case 'SET_ROWS': {
96
- const currentRows = Array.from(state.rows.idRowsLookup.values());
97
- const newRows = typeof action.payload === 'function'
109
+ const currentRows = state.rows.allRows.map(id => state.rows.idRowsLookup.get(id)!);
110
+ const newRows = typeof action.payload === 'function'
98
111
  ? action.payload(currentRows)
99
112
  : action.payload;
100
113
 
101
- const idRowsLookup = new Map<GridRowId, GridRowModel>();
102
- const allRows: GridRowId[] = [];
103
-
104
- newRows.forEach(row => {
105
- idRowsLookup.set(row.id, row);
106
- allRows.push(row.id);
107
- });
114
+ // pagination.rowCount is the server-reported total only (SET_ROW_COUNT). Client row
115
+ // counts are derived from the rows the grid shows, so replacing rows never touches it.
116
+ return {
117
+ ...state,
118
+ rows: buildRowsState(newRows, action.getRowId, state.rows.idByRow),
119
+ };
120
+ }
108
121
 
122
+ case 'REPLACE_ROW': {
123
+ // Stores a committed edit under the id of the row that was edited, in the current store.
124
+ const previous = state.rows.idRowsLookup.get(action.id);
125
+ if (previous === undefined) return state;
126
+ const idRowsLookup = new Map(state.rows.idRowsLookup);
127
+ idRowsLookup.set(action.id, action.row);
128
+ const idByRow = new Map(state.rows.idByRow);
129
+ idByRow.delete(previous);
130
+ idByRow.set(action.row, action.id);
109
131
  return {
110
132
  ...state,
111
- rows: { idRowsLookup, allRows },
112
- pagination: {
113
- ...state.pagination,
114
- // Only override rowCount when we're NOT in server mode (i.e. no preserveRowCount flag)
115
- // Server-side fetches set rowCount separately via SET_ROW_COUNT.
116
- rowCount: action.preserveRowCount ? state.pagination.rowCount : newRows.length
117
- }
133
+ rows: { ...state.rows, idRowsLookup, idByRow },
118
134
  };
119
135
  }
120
136
 
@@ -138,33 +154,6 @@ function gridReducer(state: GridState, action: GridAction): GridState {
138
154
  };
139
155
  }
140
156
 
141
- case 'SET_SORT_MODEL':
142
- return {
143
- ...state,
144
- sorting: { sortModel: action.payload }
145
- };
146
-
147
- case 'SET_FILTER_MODEL':
148
- return {
149
- ...state,
150
- filter: { filterModel: action.payload }
151
- };
152
-
153
- case 'SET_PAGINATION_MODEL':
154
- return {
155
- ...state,
156
- pagination: {
157
- ...state.pagination,
158
- paginationModel: action.payload
159
- }
160
- };
161
-
162
- case 'SET_SELECTION':
163
- return {
164
- ...state,
165
- selection: { selectedRows: action.payload }
166
- };
167
-
168
157
  case 'SET_DIMENSIONS':
169
158
  return {
170
159
  ...state,
@@ -209,32 +198,47 @@ function gridReducer(state: GridState, action: GridAction): GridState {
209
198
 
210
199
  export interface UseDataGridParams<R extends GridRowModel = GridRowModel> {
211
200
  rows: R[];
201
+ /** Keys the store. Read when rows are set; defaults to `row.id`. */
202
+ getRowId?: (row: R) => GridRowId;
212
203
  columns: GridColDef<R>[];
213
204
  rowHeight?: number;
214
205
  headerHeight?: number;
215
- rowCount?: number;
216
206
  columnVisibilityModel?: GridColumnVisibilityModel;
217
207
  initialState?: import('../../state/types').GridInitialState;
208
+ /**
209
+ * When true (no dataSource owns the rows), the store follows the `rows` param: a new rows
210
+ * array replaces the stored rows in the same render, so rows and the columns rendered
211
+ * against them always change together.
212
+ */
213
+ syncRows?: boolean;
218
214
  }
219
215
 
216
+ /**
217
+ * The row store, plus dimensions and dataSource status. With `syncRows`, a new `rows` array
218
+ * replaces the stored rows during render (never one commit later, which let new columns run
219
+ * against the previous rows). Without it, `rows` only seeds the store and a dataSource fills
220
+ * it through setRows.
221
+ */
220
222
  export function useDataGrid<R extends GridRowModel = GridRowModel>(params: UseDataGridParams<R>) {
221
- const { rows, columns, rowHeight = 52, headerHeight = 56, columnVisibilityModel, initialState: propInitialState } = params;
223
+ const { rows, columns, rowHeight = 52, headerHeight = 56, columnVisibilityModel, initialState: propInitialState, syncRows = false } = params;
224
+ const getRowId = (params.getRowId ?? defaultGetRowId) as GetRowId;
225
+
226
+ // Latest getRowId for dispatches from effects and event handlers. An inline getRowId
227
+ // therefore never re-keys (or resets) the store by itself; new rows are keyed with it.
228
+ const getRowIdRef = useRef(getRowId);
229
+ useLayoutEffect(() => { getRowIdRef.current = getRowId; });
222
230
 
223
231
  const [internalInitialState] = useState(() =>
224
- createInitialState(rows, columns, columnVisibilityModel)
232
+ createInitialState(rows, getRowId, columns, columnVisibilityModel)
225
233
  );
226
234
 
227
235
  const [state, dispatch] = useReducer(gridReducer, {
228
236
  ...internalInitialState,
229
- sorting: propInitialState?.sorting || internalInitialState.sorting,
230
- filter: propInitialState?.filter || internalInitialState.filter,
231
237
  dataSource: propInitialState?.dataSource
232
238
  ? { ...internalInitialState.dataSource, ...propInitialState.dataSource }
233
239
  : internalInitialState.dataSource,
234
240
  pagination: {
235
- ...internalInitialState.pagination,
236
- ...(propInitialState?.pagination || {}),
237
- rowCount: params.rowCount ?? propInitialState?.pagination?.rowCount ?? internalInitialState.pagination.rowCount
241
+ rowCount: propInitialState?.pagination?.rowCount,
238
242
  },
239
243
  dimensions: {
240
244
  ...internalInitialState.dimensions,
@@ -243,42 +247,33 @@ export function useDataGrid<R extends GridRowModel = GridRowModel>(params: UseDa
243
247
  }
244
248
  });
245
249
 
250
+ // The rows (and sync mode) the store was last filled from. Updating the store while
251
+ // rendering makes React re-render before committing, so no commit pairs the columns of
252
+ // this render with the rows of the previous one.
253
+ const [syncedFrom, setSyncedFrom] = useState({ rows, syncRows });
254
+ if (syncedFrom.rows !== rows || syncedFrom.syncRows !== syncRows) {
255
+ setSyncedFrom({ rows, syncRows });
256
+ if (syncRows) dispatch({ type: 'SET_ROWS', payload: rows, getRowId });
257
+ }
258
+
246
259
  const stateRef = useRef(state);
247
260
  useLayoutEffect(() => { stateRef.current = state; });
248
261
 
249
- // Sync internal state when the external `rows` prop changes
250
- const prevRowsRef = useRef(rows);
251
- useEffect(() => {
252
- if (rows !== prevRowsRef.current) {
253
- prevRowsRef.current = rows;
254
- dispatch({ type: 'SET_ROWS', payload: rows as GridRowModel[] });
255
- }
256
- }, [rows]);
262
+ // `_preserveRowCount` is still accepted from callers written against the old signature;
263
+ // replacing rows never changes the server row count any more.
264
+ const setRows = useCallback((rowsOrUpdater: GridRowModel[] | ((prev: GridRowModel[]) => GridRowModel[]), _preserveRowCount?: boolean) => {
265
+ dispatch({ type: 'SET_ROWS', payload: rowsOrUpdater, getRowId: getRowIdRef.current });
266
+ }, []);
257
267
 
258
- const setRows = useCallback((rowsOrUpdater: GridRowModel[] | ((prev: GridRowModel[]) => GridRowModel[]), preserveRowCount?: boolean) => {
259
- dispatch({ type: 'SET_ROWS', payload: rowsOrUpdater, preserveRowCount });
268
+ /** Replaces the row stored under `id` (a committed edit); does nothing if that row is gone. */
269
+ const replaceRow = useCallback((id: GridRowId, row: GridRowModel) => {
270
+ dispatch({ type: 'REPLACE_ROW', id, row });
260
271
  }, []);
261
272
 
262
273
  const setColumns = useCallback((newColumns: GridColDef[]) => {
263
274
  dispatch({ type: 'SET_COLUMNS', payload: newColumns });
264
275
  }, []);
265
276
 
266
- const setSortModel = useCallback((sortModel: GridSortItem[]) => {
267
- dispatch({ type: 'SET_SORT_MODEL', payload: sortModel });
268
- }, []);
269
-
270
- const setFilterModel = useCallback((filterModel: GridFilterModel) => {
271
- dispatch({ type: 'SET_FILTER_MODEL', payload: filterModel });
272
- }, []);
273
-
274
- const setPaginationModel = useCallback((paginationModel: GridPaginationModel) => {
275
- dispatch({ type: 'SET_PAGINATION_MODEL', payload: paginationModel });
276
- }, []);
277
-
278
- const setSelection = useCallback((selectedRows: Set<GridRowId>) => {
279
- dispatch({ type: 'SET_SELECTION', payload: selectedRows });
280
- }, []);
281
-
282
277
  const setDimensions = useCallback((viewportWidth: number, viewportHeight: number) => {
283
278
  dispatch({ type: 'SET_DIMENSIONS', payload: { viewportWidth, viewportHeight } });
284
279
  }, []);
@@ -295,77 +290,24 @@ export function useDataGrid<R extends GridRowModel = GridRowModel>(params: UseDa
295
290
  dispatch({ type: 'SET_ROW_COUNT', payload: count });
296
291
  }, []);
297
292
 
298
- const apiRef = useRef<GridApi>({
293
+ // Store-backed getters live here. DataGrid installs the rest of the API (sort, filter,
294
+ // pagination, selection, visible rows and columns) from the state it actually renders.
295
+ const [initialApi] = useState<GridApi>(() => ({
296
+ ...createGridApiPlaceholder(),
299
297
  getRow: (id: GridRowId) => stateRef.current.rows.idRowsLookup.get(id) || null,
300
- getAllRows: () => Array.from(stateRef.current.rows.idRowsLookup.values()),
301
- getVisibleRows: () => Array.from(stateRef.current.rows.idRowsLookup.values()),
302
- getAllFilteredRows: () => Array.from(stateRef.current.rows.idRowsLookup.values()),
303
- getGroupedExportRows: () => null,
304
- getAggregationResult: () => null,
305
- getAggregationModel: () => null,
298
+ getAllRows: () => stateRef.current.rows.allRows.map(id => stateRef.current.rows.idRowsLookup.get(id)!),
306
299
  getColumn: (field: string) => stateRef.current.columns.lookup.get(field) || null,
307
300
  getAllColumns: () => stateRef.current.columns.all,
308
- getVisibleColumns: () => stateRef.current.columns.all,
309
- selectRow: (id: GridRowId, isSelected = true) => {
310
- const newSelection = new Set(stateRef.current.selection.selectedRows);
311
- if (isSelected) {
312
- newSelection.add(id);
313
- } else {
314
- newSelection.delete(id);
315
- }
316
- setSelection(newSelection);
317
- },
318
- selectRows: (ids: GridRowId[], isSelected = true) => {
319
- const newSelection = new Set(stateRef.current.selection.selectedRows);
320
- ids.forEach(id => {
321
- if (isSelected) {
322
- newSelection.add(id);
323
- } else {
324
- newSelection.delete(id);
325
- }
326
- });
327
- setSelection(newSelection);
328
- },
329
- getSelectedRows: () => Array.from(stateRef.current.selection.selectedRows),
330
- sortColumn: (field: string, direction) => {
331
- if (direction === null) {
332
- setSortModel(stateRef.current.sorting.sortModel.filter(item => item.field !== field));
333
- } else {
334
- const existingIndex = stateRef.current.sorting.sortModel.findIndex(item => item.field === field);
335
- if (existingIndex >= 0) {
336
- const newModel = [...stateRef.current.sorting.sortModel];
337
- newModel[existingIndex] = { field, sort: direction };
338
- setSortModel(newModel);
339
- } else {
340
- setSortModel([...stateRef.current.sorting.sortModel, { field, sort: direction }]);
341
- }
342
- }
343
- },
344
- getSortModel: () => stateRef.current.sorting.sortModel,
345
- setFilterModel,
346
- getFilterModel: () => stateRef.current.filter.filterModel,
347
- setPage: (page: number) => {
348
- setPaginationModel({ ...stateRef.current.pagination.paginationModel, page });
349
- },
350
- setPageSize: (pageSize: number) => {
351
- setPaginationModel({ ...stateRef.current.pagination.paginationModel, pageSize, page: 0 });
352
- },
353
- scrollToIndexes: () => {
354
-
355
- },
356
- copySelectedRows: () => Promise.resolve()
357
- });
301
+ }));
302
+ const apiRef = useRef<GridApi>(initialApi);
358
303
 
359
304
  return {
360
305
  state,
361
306
  dispatch,
362
307
  apiRef,
363
308
  setRows,
309
+ replaceRow,
364
310
  setColumns,
365
- setSortModel,
366
- setFilterModel,
367
- setPaginationModel,
368
- setSelection,
369
311
  setDimensions,
370
312
  setDataSourceLoading,
371
313
  setDataSourceError,
@@ -0,0 +1,72 @@
1
+ import { useLayoutEffect, useRef } from 'react';
2
+ import type { MutableRefObject } from 'react';
3
+ import { buildGroupedExportRows } from '../../utils/grouping/groupedExportRows';
4
+ import type { GridHierarchyVisibleRowsOptions, GridGroupAggregationPosition } from '../useRowGrouping';
5
+ import type {
6
+ GridAggregationModel,
7
+ GridAggregationResult,
8
+ GridApi,
9
+ GridColDef,
10
+ GridGroupedExportRow,
11
+ GridRowId,
12
+ GridRowMeta,
13
+ GridRowModel,
14
+ } from '../../types';
15
+
16
+ export interface UseGridAggregationApiParams<R extends GridRowModel> {
17
+ apiRef: MutableRefObject<GridApi>;
18
+ /** An aggregation model is set and the pivot is off. */
19
+ hasAggregation: boolean;
20
+ aggregationResult: GridAggregationResult;
21
+ aggregationModel: GridAggregationModel;
22
+ isRowGrouping: boolean;
23
+ rowGroupingHandlers: {
24
+ getVisibleRows: (options?: GridHierarchyVisibleRowsOptions) => R[] | null;
25
+ rootAggregationPosition: GridGroupAggregationPosition;
26
+ aggregationPositions: Map<GridRowId, GridGroupAggregationPosition>;
27
+ };
28
+ rowMetaMap: Map<GridRowId, GridRowMeta>;
29
+ columns: GridColDef<R>[];
30
+ getRowId: (row: GridRowModel) => GridRowId;
31
+ /** The column that shows the group labels, so export order matches the screen when sorting by it. */
32
+ hierarchyField?: string;
33
+ }
34
+
35
+ /**
36
+ * Installs `getAggregationResult`, `getAggregationModel` and `getGroupedExportRows` on the API.
37
+ * Like useGridApiMethods, the methods are installed in a layout effect and read the latest values
38
+ * from a ref, so they are live in a parent's layout effect on mount and never answer from a
39
+ * previous render.
40
+ */
41
+ export function useGridAggregationApi<R extends GridRowModel>(params: UseGridAggregationApiParams<R>): void {
42
+ const { apiRef } = params;
43
+ const latestRef = useRef(params);
44
+ useLayoutEffect(() => {
45
+ latestRef.current = params;
46
+ });
47
+
48
+ useLayoutEffect(() => {
49
+ const latest = () => latestRef.current;
50
+ apiRef.current.getAggregationResult = () => (latest().hasAggregation ? latest().aggregationResult : null);
51
+ apiRef.current.getAggregationModel = () => (latest().hasAggregation ? latest().aggregationModel : null);
52
+ apiRef.current.getGroupedExportRows = (): GridGroupedExportRow[] | null => {
53
+ const {
54
+ hasAggregation, aggregationResult, aggregationModel, isRowGrouping, rowGroupingHandlers,
55
+ rowMetaMap, columns, getRowId, hierarchyField,
56
+ } = latest();
57
+ if (!isRowGrouping) return null;
58
+ // Every group expanded, filtered and sorted like the screen: collapsed groups still
59
+ // export their rows. Subtotals and the grand total hidden by getAggregationPosition
60
+ // (null) are left out, as they are on screen.
61
+ return buildGroupedExportRows<R>({
62
+ rows: rowGroupingHandlers.getVisibleRows({ expandAll: true, labelField: hierarchyField }) ?? [],
63
+ getRowId,
64
+ rowMetaMap,
65
+ columns,
66
+ aggregationModel,
67
+ aggregationResult: hasAggregation && rowGroupingHandlers.rootAggregationPosition !== null ? aggregationResult : null,
68
+ isSubtotalHidden: (groupId) => rowGroupingHandlers.aggregationPositions.get(groupId) === null,
69
+ });
70
+ };
71
+ }, [apiRef]);
72
+ }
@@ -0,0 +1,115 @@
1
+ import { useLayoutEffect, useRef } from 'react';
2
+ import type {
3
+ GridApi,
4
+ GridColDef,
5
+ GridFilterModel,
6
+ GridPaginationModel,
7
+ GridRowId,
8
+ GridRowModel,
9
+ GridSortItem,
10
+ } from '../../types';
11
+ import { isSameSelection, nextSelectionForRows } from './useGridRowSelection';
12
+ import { nextSortModelForColumn } from './useGridHeaderHandlers';
13
+
14
+ export interface UseGridApiMethodsParams {
15
+ apiRef: React.MutableRefObject<GridApi>;
16
+
17
+ sortModel: GridSortItem[];
18
+ /** Mirrors a header click: multiSort merges into the model, otherwise the model is replaced. */
19
+ multiSort: boolean;
20
+ onSortModelChange: (model: GridSortItem[]) => void;
21
+
22
+ filterModel: GridFilterModel;
23
+ onFilterModelChange: (model: GridFilterModel) => void;
24
+
25
+ paginationModel: GridPaginationModel;
26
+ onPaginationModelChange: (model: GridPaginationModel) => void;
27
+
28
+ selectedRowIds: Set<GridRowId>;
29
+ /**
30
+ * A selection change made since the last commit (see useGridControlledState), or null. It
31
+ * wins over `selectedRowIds`, so the API is current inside onRowSelectionModelChange.
32
+ */
33
+ getPendingRowSelectionModel?: () => GridRowId[] | null;
34
+ onRowSelectionModelChange: (model: GridRowId[]) => void;
35
+ disableMultipleRowSelection: boolean;
36
+ /**
37
+ * Rows the grid made (group headers, subtotals, auto-created parents, the pivot Grand Total):
38
+ * `selectRow` / `selectRows` ignore their ids, as the checkbox, click and keyboard paths do.
39
+ */
40
+ isSyntheticRowId?: (id: GridRowId) => boolean;
41
+
42
+ /** Rows on screen: pinned top, the current page (or all rows), pinned bottom. */
43
+ getVisibleRows: () => GridRowModel[];
44
+ /** Every row that passes the filter, ignoring pagination and hierarchy expansion. */
45
+ getAllFilteredRows: () => GridRowModel[];
46
+ /** Columns on screen, in display order. */
47
+ visibleColumns: GridColDef[];
48
+ }
49
+
50
+ /**
51
+ * Installs the parts of the imperative API that read or change the state the grid renders
52
+ * from (sort, filter, pagination, selection, visible rows and columns). Setters go through
53
+ * the same handlers as the UI, so controlled props and change callbacks behave the same way.
54
+ *
55
+ * The methods read the latest values from a ref refreshed in a layout effect. A setter also
56
+ * writes its result into that ref, so consecutive calls in one tick (selectRow(1);
57
+ * selectRow(2)) build on each other instead of on the last render.
58
+ */
59
+ export function useGridApiMethods(params: UseGridApiMethodsParams): void {
60
+ const { apiRef } = params;
61
+ const latestRef = useRef(params);
62
+
63
+ useLayoutEffect(() => {
64
+ latestRef.current = params;
65
+ });
66
+
67
+ useLayoutEffect(() => {
68
+ const api = apiRef.current;
69
+ const latest = () => latestRef.current;
70
+
71
+ api.getSortModel = () => latest().sortModel;
72
+ api.sortColumn = (field, direction) => {
73
+ const { sortModel, multiSort, onSortModelChange } = latest();
74
+ const next = nextSortModelForColumn(sortModel, field, direction, multiSort);
75
+ latestRef.current = { ...latest(), sortModel: next };
76
+ onSortModelChange(next);
77
+ };
78
+
79
+ api.getFilterModel = () => latest().filterModel;
80
+ api.setFilterModel = (model) => {
81
+ latestRef.current = { ...latest(), filterModel: model };
82
+ latest().onFilterModelChange(model);
83
+ };
84
+
85
+ const setPaginationModel = (model: GridPaginationModel) => {
86
+ latestRef.current = { ...latest(), paginationModel: model };
87
+ latest().onPaginationModelChange(model);
88
+ };
89
+ api.setPage = (page) => setPaginationModel({ ...latest().paginationModel, page });
90
+ api.setPageSize = (pageSize) => setPaginationModel({ ...latest().paginationModel, pageSize, page: 0 });
91
+
92
+ const currentSelection = (): Set<GridRowId> => {
93
+ const pending = latest().getPendingRowSelectionModel?.();
94
+ return pending ? new Set(pending) : latest().selectedRowIds;
95
+ };
96
+ const setSelection = (ids: GridRowId[], isSelected: boolean) => {
97
+ const { disableMultipleRowSelection, onRowSelectionModelChange, isSyntheticRowId } = latest();
98
+ const dataIds = isSyntheticRowId ? ids.filter(id => !isSyntheticRowId(id)) : ids;
99
+ if (dataIds.length === 0) return;
100
+ const current = currentSelection();
101
+ const next = nextSelectionForRows(current, dataIds, isSelected, disableMultipleRowSelection);
102
+ // Like the UI paths, a call that changes nothing (selecting a selected row) reports nothing.
103
+ if (isSameSelection(current, next)) return;
104
+ latestRef.current = { ...latest(), selectedRowIds: new Set(next) };
105
+ onRowSelectionModelChange(next);
106
+ };
107
+ api.getSelectedRows = () => Array.from(currentSelection());
108
+ api.selectRow = (id, isSelected = true) => setSelection([id], isSelected);
109
+ api.selectRows = (ids, isSelected = true) => setSelection(ids, isSelected);
110
+
111
+ api.getVisibleRows = () => latest().getVisibleRows();
112
+ api.getAllFilteredRows = () => latest().getAllFilteredRows();
113
+ api.getVisibleColumns = () => latest().visibleColumns;
114
+ }, [apiRef]);
115
+ }
@@ -1,10 +1,22 @@
1
- import { useRef } from 'react';
1
+ import { useRef, useState } from 'react';
2
+ import type { MutableRefObject } from 'react';
2
3
  import type { GridApi } from '../../types';
4
+ import { createGridApiPlaceholder } from './gridApiPlaceholder';
3
5
 
4
6
  /**
5
7
  * Hook to create a ref for the DataGrid API.
6
8
  * This ref can be passed to the `apiRef` prop of the `DataGrid` component.
9
+ *
10
+ * `apiRef.current` is never null: until the grid mounts it holds an API whose methods are
11
+ * no-ops (getters return empty values), and the grid swaps in the live API in a layout
12
+ * effect, so it is available to the parent's layout effects and effects.
13
+ *
14
+ * The return type is spelled out so the published declaration is `MutableRefObject<GridApi>`,
15
+ * which matches `DataGridProps.apiRef` under both @types/react 18 and 19. Left to inference,
16
+ * React 19's types emit `RefObject<GridApi>`, which React 18's types treat as read-only and
17
+ * nullable, so `apiRef={useGridApiRef()}` failed to type-check on React 18.
7
18
  */
8
- export function useGridApiRef() {
9
- return useRef<GridApi>(null!);
19
+ export function useGridApiRef(): MutableRefObject<GridApi> {
20
+ const [placeholder] = useState(createGridApiPlaceholder);
21
+ return useRef<GridApi>(placeholder);
10
22
  }
@@ -0,0 +1,18 @@
1
+ import { useLayoutEffect } from 'react';
2
+ import type { MutableRefObject } from 'react';
3
+ import type { GridApi } from '../../types';
4
+
5
+ /**
6
+ * Points the consumer's `apiRef` prop at the grid's live API. A layout effect, so the live API
7
+ * is in place before the parent's layout effects run.
8
+ */
9
+ export function useGridApiRefBinding(
10
+ propApiRef: MutableRefObject<GridApi> | undefined,
11
+ apiRef: MutableRefObject<GridApi>
12
+ ): void {
13
+ useLayoutEffect(() => {
14
+ if (propApiRef) {
15
+ propApiRef.current = apiRef.current;
16
+ }
17
+ }, [propApiRef, apiRef]);
18
+ }