@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,14 +1,27 @@
1
- import { useEffect, useRef, useCallback } from 'react';
2
- import {
3
- GridDataSource,
4
- GridRowModel,
5
- GridSortItem,
6
- GridFilterModel,
1
+ import { useEffect, useLayoutEffect, useRef, useCallback } from 'react';
2
+ import type {
3
+ GridDataSource,
4
+ GridRowId,
5
+ GridRowModel,
6
+ GridSortItem,
7
+ GridFilterModel,
7
8
  GridPaginationModel,
8
- GridGetRowsParams,
9
+ GridGetRowsResponse,
9
10
  GridAggregationModel,
10
11
  GridAggregationResult,
11
12
  } from '../../types';
13
+ import { GRID_ALL_ROWS_END_ROW, isServerDrivenDataSource } from '../../utils/dataSource';
14
+
15
+ /** Rapid sort, filter and page changes are collapsed into one request. */
16
+ const FETCH_DEBOUNCE_MS = 300;
17
+
18
+ /**
19
+ * - `page`: server pagination. Each request is one page and replaces the rows.
20
+ * - `infinite`: infinite scroll. Rows are appended, from the end of what is loaded up to the
21
+ * current page, so no range is ever skipped.
22
+ * - `all`: the grid pages on the client, so it loads every row once.
23
+ */
24
+ type FetchKind = 'page' | 'infinite' | 'all';
12
25
 
13
26
  interface UseGridDataSourceParams<R extends GridRowModel> {
14
27
  dataSource?: GridDataSource<R>;
@@ -19,14 +32,80 @@ interface UseGridDataSourceParams<R extends GridRowModel> {
19
32
  sortingMode?: 'client' | 'server';
20
33
  filterMode?: 'client' | 'server';
21
34
  aggregationModel?: GridAggregationModel;
35
+ /**
36
+ * Row grouping is active. It turns pagination off (every group renders in one scrollable view),
37
+ * so a `paginationMode: 'server'` source is asked for every row instead of the first page.
38
+ */
39
+ rowGroupingActive?: boolean;
40
+ /** Resolves a row's id, to skip fetched rows that are already loaded. Defaults to `row.id`. */
41
+ getRowId?: (row: R) => GridRowId;
22
42
  setRows: (rows: R[] | ((prev: R[]) => R[]), preserveRowCount?: boolean) => void;
23
43
  setRowCount: (count: number) => void;
24
44
  setDataSourceLoading: (loading: boolean) => void;
25
45
  setDataSourceError: (error: unknown) => void;
26
- onAggregationResults?: (results: GridAggregationResult) => void;
46
+ onAggregationResults?: (results: GridAggregationResult) => void;
47
+ /**
48
+ * Infinite scroll: called with page 0 when a sort, filter, page size or dataSource change
49
+ * restarts the list from its first row.
50
+ */
51
+ onPaginationModelChange?: (model: GridPaginationModel) => void;
52
+ }
53
+
54
+ export interface UseGridDataSourceReturn {
55
+ /** Requests the current rows now (skipping the debounce), e.g. to retry after an error. */
56
+ fetchRows: () => Promise<void>;
57
+ /**
58
+ * Loads the children of a server-side tree node and appends them. A path that is already
59
+ * loading or loaded for the current rows is not requested again. `onError` runs when the
60
+ * request fails; the grid-level error is left alone because the rows themselves are fine.
61
+ */
62
+ fetchChildren: (parentId: GridRowId, groupKeys: string[], onError?: (error: unknown) => void) => Promise<void>;
63
+ }
64
+
65
+ interface InfiniteState {
66
+ /** The page the list is loaded up to (inclusive). */
67
+ page: number;
68
+ /** Server offset just past the last loaded row. */
69
+ loadedEnd: number;
70
+ /** `endRow` of the request in flight, or null. */
71
+ inFlightEnd: number | null;
72
+ }
73
+
74
+ const defaultGetRowId = (row: GridRowModel): GridRowId => row.id;
75
+
76
+ function getFetchKind(paginationMode: UseGridDataSourceParams<GridRowModel>['paginationMode'], rowGroupingActive: boolean): FetchKind {
77
+ if (paginationMode === 'infinite') return 'infinite';
78
+ // Row grouping shows no pager, so a page-sized request would leave the other rows unreachable.
79
+ if (paginationMode === 'server') return rowGroupingActive ? 'all' : 'page';
80
+ return 'all';
81
+ }
82
+
83
+ /**
84
+ * Drops fetched rows whose id (getRowId) repeats within the response, keeping the first
85
+ * occurrence, and pairs each row with its id. Rows are kept as given: the store keys them by
86
+ * getRowId. Runs outside any state updater, so a getRowId that throws for a fetched row fails the
87
+ * request (error overlay) instead of throwing inside React's update.
88
+ */
89
+ function identifyRows<R extends GridRowModel>(rows: R[], getRowId: (row: R) => GridRowId): { id: GridRowId; row: R }[] {
90
+ const seen = new Set<GridRowId>();
91
+ const result: { id: GridRowId; row: R }[] = [];
92
+ rows.forEach(row => {
93
+ const id = getRowId(row);
94
+ if (seen.has(id)) return;
95
+ seen.add(id);
96
+ result.push({ id, row });
97
+ });
98
+ return result;
99
+ }
100
+
101
+ /** The loaded rows followed by the fetched rows whose id is not loaded yet. */
102
+ function appendNewRows<R extends GridRowModel>(loaded: R[], fetched: { id: GridRowId; row: R }[], getRowId: (row: R) => GridRowId): R[] {
103
+ const loadedIds = new Set<GridRowId>(loaded.map(getRowId));
104
+ const added = fetched.filter(entry => !loadedIds.has(entry.id)).map(entry => entry.row);
105
+ return added.length === 0 ? loaded : [...loaded, ...added];
27
106
  }
28
107
 
29
- export function useGridDataSource<R extends GridRowModel>(params: UseGridDataSourceParams<R>) {
108
+ export function useGridDataSource<R extends GridRowModel>(params: UseGridDataSourceParams<R>): UseGridDataSourceReturn {
30
109
  const {
31
110
  dataSource,
32
111
  sortModel,
@@ -36,155 +115,302 @@ export function useGridDataSource<R extends GridRowModel>(params: UseGridDataSou
36
115
  sortingMode,
37
116
  filterMode,
38
117
  aggregationModel,
39
- setRows,
40
- setRowCount,
41
- setDataSourceLoading,
42
- setDataSourceError,
43
- onAggregationResults,
118
+ rowGroupingActive = false,
44
119
  } = params;
45
120
 
46
- const prevParamsRef = useRef<{
47
- sortModel: GridSortItem[];
48
- filterModel: GridFilterModel;
49
- page: number;
50
- pageSize: number;
51
- } | null>(null);
121
+ const kind = getFetchKind(paginationMode, rowGroupingActive);
122
+ const loadsAllForGrouping = Boolean(dataSource) && paginationMode === 'server' && rowGroupingActive;
123
+ const warnedGroupingRef = useRef(false);
124
+ useEffect(() => {
125
+ if (process.env.NODE_ENV === 'production' || warnedGroupingRef.current || !loadsAllForGrouping) return;
126
+ warnedGroupingRef.current = true;
127
+ console.warn(
128
+ "[OpenGridX] `rowGroupingModel` turns pagination off, so the dataSource with `paginationMode: 'server'` is " +
129
+ 'asked for every row (startRow 0, endRow Number.MAX_SAFE_INTEGER) instead of one page. ' +
130
+ 'See docs/features/data-source.md.'
131
+ );
132
+ }, [loadsAllForGrouping]);
133
+ const getRows = dataSource?.getRows;
134
+ const serverDriven = isServerDrivenDataSource({ dataSource, paginationMode, sortingMode, filterMode });
52
135
 
53
- const activeRequestRef = useRef<number>(0);
136
+ // What the loaded rows depend on, compared by content: an inline filterModel, sortModel or
137
+ // aggregationModel (a new object with the same content on every parent render) does not refetch.
138
+ const datasetKey = JSON.stringify([
139
+ kind,
140
+ kind !== 'all' || sortingMode === 'server' ? sortModel : null,
141
+ kind !== 'all' || filterMode === 'server' ? filterModel : null,
142
+ serverDriven ? aggregationModel ?? null : null,
143
+ kind === 'all' ? null : paginationModel.pageSize,
144
+ ]);
145
+ // Client pagination has every row already, so page changes never refetch.
146
+ const requestedPage = kind === 'all' ? 0 : paginationModel.page;
147
+ const requestedPageSize = kind === 'all' ? 0 : paginationModel.pageSize;
54
148
 
55
- const fetchRows = useCallback(async () => {
56
- if (!dataSource) return;
149
+ // Requests read the latest params when they start and when they land.
150
+ const latestRef = useRef({ params, kind });
151
+ useLayoutEffect(() => {
152
+ latestRef.current = { params, kind };
153
+ });
57
154
 
58
- const requestId = ++activeRequestRef.current;
155
+ /** Bumped to drop the rows request in flight: a response whose token is stale is ignored. */
156
+ const tokenRef = useRef(0);
157
+ /** Bumped when the rows are replaced: children fetched for the old rows are ignored. */
158
+ const rowsGenerationRef = useRef(0);
159
+ const datasetRef = useRef<{ key: string; getRows: GridDataSource<R>['getRows'] } | null>(null);
160
+ const infiniteRef = useRef<InfiniteState>({ page: 0, loadedEnd: 0, inFlightEnd: null });
161
+ const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
162
+ /** A rows request is scheduled or in flight. */
163
+ const rowsPendingRef = useRef(false);
164
+ const childrenPendingRef = useRef(0);
165
+ const loadingRef = useRef(false);
166
+ /** Children paths requested for the current rows (loading or loaded). */
167
+ const childRequestsRef = useRef(new Set<string>());
59
168
 
60
- setDataSourceLoading(true);
61
- setDataSourceError(null);
169
+ const syncLoading = useCallback(() => {
170
+ const loading = rowsPendingRef.current || childrenPendingRef.current > 0;
171
+ if (loading === loadingRef.current) return;
172
+ loadingRef.current = loading;
173
+ latestRef.current.params.setDataSourceLoading(loading);
174
+ }, []);
62
175
 
63
- const startRow = paginationModel.page * paginationModel.pageSize;
64
- const endRow = startRow + paginationModel.pageSize;
176
+ const clearTimer = useCallback(() => {
177
+ if (timerRef.current !== null) {
178
+ clearTimeout(timerRef.current);
179
+ timerRef.current = null;
180
+ }
181
+ }, []);
65
182
 
66
- const requestParams: GridGetRowsParams = {
67
- startRow,
68
- endRow,
69
- sortModel,
70
- filterModel,
71
- groupKeys: [],
72
- aggregationModel,
73
- };
183
+ const invalidateRowsRequest = useCallback(() => {
184
+ tokenRef.current++;
185
+ infiniteRef.current.inFlightEnd = null;
186
+ rowsPendingRef.current = false;
187
+ }, []);
74
188
 
75
- try {
76
- const response = await dataSource.getRows(requestParams);
189
+ const invalidateChildren = useCallback(() => {
190
+ rowsGenerationRef.current++;
191
+ childRequestsRef.current.clear();
192
+ childrenPendingRef.current = 0;
193
+ }, []);
77
194
 
78
- if (requestId !== activeRequestRef.current) {
79
- return;
195
+ /** Runs one rows request. Resolves true when an infinite list needs the next range right away. */
196
+ const requestRowsOnce = useCallback(async (): Promise<boolean> => {
197
+ const { params: p, kind: k } = latestRef.current;
198
+ const source = p.dataSource;
199
+ if (!source) return false;
200
+ const infinite = infiniteRef.current;
201
+ const pageSize = p.paginationModel.pageSize;
202
+
203
+ let startRow: number;
204
+ let endRow: number;
205
+ if (k === 'infinite') {
206
+ // One range at a time: the request in flight asks for the rest when it lands.
207
+ if (infinite.inFlightEnd !== null) return false;
208
+ startRow = infinite.loadedEnd;
209
+ endRow = (infinite.page + 1) * pageSize;
210
+ if (endRow <= startRow) {
211
+ rowsPendingRef.current = false;
212
+ syncLoading();
213
+ return false;
80
214
  }
215
+ infinite.inFlightEnd = endRow;
216
+ } else if (k === 'page') {
217
+ startRow = p.paginationModel.page * pageSize;
218
+ endRow = startRow + pageSize;
219
+ } else {
220
+ startRow = 0;
221
+ endRow = GRID_ALL_ROWS_END_ROW;
222
+ }
81
223
 
82
- let shouldAppend = false;
224
+ const token = ++tokenRef.current;
225
+ rowsPendingRef.current = true;
226
+ syncLoading();
227
+ p.setDataSourceError(null);
83
228
 
84
- if (paginationMode === 'infinite') {
85
- const prev = prevParamsRef.current;
86
- if (prev) {
229
+ let response: GridGetRowsResponse<R>;
230
+ try {
231
+ response = await source.getRows({
232
+ startRow,
233
+ endRow,
234
+ sortModel: p.sortModel,
235
+ filterModel: p.filterModel,
236
+ groupKeys: [],
237
+ aggregationModel: p.aggregationModel,
238
+ });
239
+ } catch (error) {
240
+ if (token !== tokenRef.current) return false;
241
+ infinite.inFlightEnd = null;
242
+ rowsPendingRef.current = false;
243
+ latestRef.current.params.setDataSourceError(error);
244
+ console.error('Data Source Error:', error);
245
+ syncLoading();
246
+ return false;
247
+ }
248
+ if (token !== tokenRef.current) return false;
87
249
 
88
- const sortChanged = JSON.stringify(prev.sortModel) !== JSON.stringify(sortModel);
89
- const filterChanged = JSON.stringify(prev.filterModel) !== JSON.stringify(filterModel);
90
- const pageSizeChanged = prev.pageSize !== paginationModel.pageSize;
250
+ // A malformed response, or a getRowId that throws for a fetched row, fails the request like a
251
+ // rejected getRows: error overlay, loading cleared, nothing thrown.
252
+ try {
253
+ const { params: current } = latestRef.current;
254
+ const getRowId = current.getRowId ?? (defaultGetRowId as (row: R) => GridRowId);
255
+ const fetched = identifyRows(response.rows, getRowId);
256
+ if (k === 'infinite' && startRow > 0) {
257
+ current.setRows(prev => appendNewRows(prev, fetched, getRowId), true);
258
+ } else {
259
+ current.setRows(fetched.map(entry => entry.row), true);
260
+ invalidateChildren();
261
+ }
262
+ if (response.aggregationResults) current.onAggregationResults?.(response.aggregationResults);
263
+ if (response.rowCount !== undefined) current.setRowCount(response.rowCount);
91
264
 
92
- if (!sortChanged && !filterChanged && !pageSizeChanged && paginationModel.page > prev.page) {
93
- shouldAppend = true;
94
- }
95
- }
265
+ if (k === 'infinite') {
266
+ infinite.inFlightEnd = null;
267
+ infinite.loadedEnd = startRow + response.rows.length;
268
+ // A short response is the end of the data; otherwise catch up with a page that moved on.
269
+ const receivedAll = response.rows.length >= endRow - startRow;
270
+ if (receivedAll && (infinite.page + 1) * pageSize > endRow) return true;
96
271
  }
272
+ } catch (error) {
273
+ infinite.inFlightEnd = null;
274
+ rowsPendingRef.current = false;
275
+ latestRef.current.params.setDataSourceError(error);
276
+ console.error('Data Source Error:', error);
277
+ syncLoading();
278
+ return false;
279
+ }
280
+ rowsPendingRef.current = false;
281
+ syncLoading();
282
+ return false;
283
+ }, [syncLoading, invalidateChildren]);
97
284
 
98
- if (shouldAppend) {
99
- setRows((prevRows) => [...prevRows, ...response.rows], true);
100
- } else {
101
- setRows(response.rows, true);
285
+ const requestRows = useCallback(async (): Promise<void> => {
286
+ while (await requestRowsOnce()) {
287
+ // keep extending the infinite list until it reaches the current page
288
+ }
289
+ }, [requestRowsOnce]);
290
+
291
+ // Layout effect: loading is on before the first paint, so the empty state never flashes up
292
+ // while the first request waits for its debounce.
293
+ useLayoutEffect(() => {
294
+ if (!getRows) {
295
+ if (datasetRef.current) {
296
+ // The dataSource was removed: nothing it still returns may reach the rows.
297
+ datasetRef.current = null;
298
+ clearTimer();
299
+ invalidateRowsRequest();
300
+ invalidateChildren();
301
+ latestRef.current.params.setDataSourceError(null);
302
+ syncLoading();
102
303
  }
304
+ return;
305
+ }
103
306
 
104
- if (response.aggregationResults && onAggregationResults) {
105
- onAggregationResults(response.aggregationResults);
307
+ const previous = datasetRef.current;
308
+ const infinite = infiniteRef.current;
309
+ if (!previous || previous.key !== datasetKey || previous.getRows !== getRows) {
310
+ datasetRef.current = { key: datasetKey, getRows };
311
+ invalidateRowsRequest();
312
+ invalidateChildren();
313
+ // A new sort, filter, page size or dataSource restarts an infinite list at its first page.
314
+ const restart = kind === 'infinite' && previous !== null && requestedPage !== 0;
315
+ infinite.page = restart ? 0 : requestedPage;
316
+ infinite.loadedEnd = 0;
317
+ if (restart) {
318
+ const { params: p } = latestRef.current;
319
+ p.onPaginationModelChange?.({ ...p.paginationModel, page: 0 });
320
+ }
321
+ } else if (kind === 'infinite') {
322
+ if (requestedPage < infinite.page) {
323
+ // Going back to an earlier page reloads the list up to that page.
324
+ invalidateRowsRequest();
325
+ infinite.loadedEnd = 0;
106
326
  }
327
+ infinite.page = requestedPage;
328
+ } else {
329
+ // A new page supersedes the request in flight.
330
+ invalidateRowsRequest();
331
+ }
107
332
 
108
- if (response.rowCount !== undefined) {
109
- setRowCount(response.rowCount);
110
- } else if (paginationMode === 'infinite') {
111
- // rowCount is not required for infinite scroll — the grid uses appendRows
333
+ if (kind === 'infinite') {
334
+ const inFlight = infinite.inFlightEnd !== null;
335
+ if (inFlight || (infinite.page + 1) * requestedPageSize <= infinite.loadedEnd) {
336
+ // The request in flight extends the list when it lands, or the rows are already loaded.
337
+ rowsPendingRef.current = inFlight;
338
+ syncLoading();
339
+ return;
112
340
  }
341
+ }
113
342
 
114
- prevParamsRef.current = {
115
- sortModel,
116
- filterModel,
117
- page: paginationModel.page,
118
- pageSize: paginationModel.pageSize
119
- };
343
+ rowsPendingRef.current = true;
344
+ syncLoading();
345
+ timerRef.current = setTimeout(() => {
346
+ timerRef.current = null;
347
+ void requestRows();
348
+ }, FETCH_DEBOUNCE_MS);
349
+ return clearTimer;
350
+ }, [getRows, datasetKey, kind, requestedPage, requestedPageSize, clearTimer, invalidateRowsRequest, invalidateChildren, syncLoading, requestRows]);
120
351
 
121
- } catch (error) {
122
- if (requestId === activeRequestRef.current) {
123
- setDataSourceError(error);
124
- console.error('Data Source Error:', error);
352
+ // Nothing still in flight may write to an unmounted grid. (A grid that is shown again, e.g. by
353
+ // StrictMode or <Activity>, reruns the effect above and requests what it still needs.)
354
+ useEffect(() => () => {
355
+ invalidateRowsRequest();
356
+ invalidateChildren();
357
+ }, [invalidateRowsRequest, invalidateChildren]);
358
+
359
+ const fetchRows = useCallback((): Promise<void> => {
360
+ clearTimer();
361
+ return requestRows();
362
+ }, [clearTimer, requestRows]);
363
+
364
+ const fetchChildren = useCallback(async (
365
+ _parentId: GridRowId,
366
+ groupKeys: string[],
367
+ onError?: (error: unknown) => void,
368
+ ): Promise<void> => {
369
+ const { params: p } = latestRef.current;
370
+ const source = p.dataSource;
371
+ if (!source) return;
372
+
373
+ const requestKey = JSON.stringify(groupKeys);
374
+ const requests = childRequestsRef.current;
375
+ if (requests.has(requestKey)) return;
376
+ const generation = rowsGenerationRef.current;
377
+ requests.add(requestKey);
378
+ childrenPendingRef.current++;
379
+ syncLoading();
380
+
381
+ try {
382
+ const response = await source.getRows({
383
+ startRow: 0,
384
+ endRow: GRID_ALL_ROWS_END_ROW,
385
+ sortModel: p.sortModel,
386
+ filterModel: p.filterModel,
387
+ groupKeys,
388
+ aggregationModel: p.aggregationModel,
389
+ });
390
+ // Children of rows that have since been replaced (another page, sort or source) are dropped.
391
+ if (generation !== rowsGenerationRef.current) return;
392
+ if (response.rows.length > 0) {
393
+ const { params: current } = latestRef.current;
394
+ const getRowId = current.getRowId ?? (defaultGetRowId as (row: R) => GridRowId);
395
+ const fetched = identifyRows(response.rows, getRowId);
396
+ current.setRows(prev => appendNewRows(prev, fetched, getRowId), true);
125
397
  }
398
+ } catch (error) {
399
+ if (generation !== rowsGenerationRef.current) return;
400
+ // Forget the request so that expanding the node again retries it.
401
+ requests.delete(requestKey);
402
+ console.error('Failed to fetch children:', error);
403
+ onError?.(error);
126
404
  } finally {
127
- if (requestId === activeRequestRef.current) {
128
- setDataSourceLoading(false);
405
+ if (generation === rowsGenerationRef.current) {
406
+ childrenPendingRef.current--;
407
+ syncLoading();
129
408
  }
130
409
  }
131
- }, [
132
- dataSource,
133
- paginationModel.page,
134
- paginationModel.pageSize,
135
- sortModel,
136
- filterModel,
137
- paginationMode,
138
- aggregationModel,
139
- onAggregationResults,
140
- setRows,
141
- setRowCount,
142
- setDataSourceLoading,
143
- setDataSourceError
144
- ]);
145
-
146
- useEffect(() => {
147
- const isServerSide = paginationMode === 'server' || paginationMode === 'infinite' || sortingMode === 'server' || filterMode === 'server';
148
-
149
- if (dataSource && isServerSide) {
150
- const timer = setTimeout(() => {
151
- fetchRows();
152
- }, 300);
153
- return () => clearTimeout(timer);
154
- }
155
- }, [fetchRows, dataSource, paginationMode, sortingMode, filterMode]);
156
-
157
- const fetchChildren = useCallback(async (_parentId: string | number, groupKeys: string[]) => {
158
- if (!dataSource) return;
159
-
160
- setDataSourceLoading(true);
161
- try {
162
- const response = await dataSource.getRows({
163
- startRow: 0,
164
- endRow: -1,
165
- sortModel,
166
- filterModel,
167
- groupKeys
168
- });
169
-
170
- if (response.rows.length > 0) {
171
- setRows((prevRows) => {
172
-
173
- const existingIds = new Set(prevRows.map(r => r.id));
174
- const newRows = response.rows.filter(r => !existingIds.has(r.id));
175
- return [...prevRows, ...newRows];
176
- });
177
- }
178
- } catch (error) {
179
- setDataSourceError(error);
180
- console.error('Failed to fetch children:', error);
181
- } finally {
182
- setDataSourceLoading(false);
183
- }
184
- }, [dataSource, sortModel, filterModel, setRows, setDataSourceLoading, setDataSourceError]);
410
+ }, [syncLoading]);
185
411
 
186
412
  return {
187
413
  fetchRows,
188
- fetchChildren
414
+ fetchChildren,
189
415
  };
190
416
  }