@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,5 +1,5 @@
1
- import React, { useState, useCallback, useRef } from 'react';
2
- import type { GridRowId, GridRowModel } from '../../types';
1
+ import { useState, useCallback, useRef, useEffect, useLayoutEffect, useMemo } from 'react';
2
+ import type { GridColDef, GridRowId, GridRowModel } from '../../types';
3
3
 
4
4
  export interface GridEditingState {
5
5
  editingCell: {
@@ -13,87 +13,247 @@ export interface GridEditingState {
13
13
  export interface UseGridEditingParams<R extends GridRowModel> {
14
14
  rows: R[];
15
15
  getRowId: (row: R) => GridRowId;
16
+ /** Column definitions; used to find the edited column's `valueSetter`. */
17
+ columns?: GridColDef<R>[];
16
18
  processRowUpdate?: (newRow: R, oldRow: R) => R | Promise<R>;
17
19
  onProcessRowUpdateError?: (error: unknown) => void;
18
- onRowChange?: (newRow: R) => void;
20
+ /**
21
+ * Receives the committed row and the id of the row that was edited. Match on `id` (the key the
22
+ * grid resolved with getRowId), not on a field of the returned row.
23
+ */
24
+ onRowChange?: (newRow: R, id: GridRowId) => void;
25
+ }
26
+
27
+ export interface StopCellEditParams {
28
+ /** Discard the pending value instead of committing it. */
29
+ cancel?: boolean;
30
+ /**
31
+ * Only act if this cell is the one being edited. Editors pass their own cell so that a late
32
+ * blur or unmount cannot end another cell's edit.
33
+ */
34
+ id?: GridRowId;
35
+ field?: string;
19
36
  }
20
37
 
21
38
  type EditingCellState = GridEditingState['editingCell'];
39
+ type EditingCell = NonNullable<EditingCellState>;
22
40
 
23
- export function useGridEditing<R extends GridRowModel>(params: UseGridEditingParams<R>) {
24
- const {
25
- rows,
26
- getRowId,
27
- processRowUpdate,
28
- onProcessRowUpdateError,
29
- onRowChange
30
- } = params;
41
+ interface PendingCommit {
42
+ value: unknown;
43
+ promise: Promise<void>;
44
+ }
45
+
46
+ const RESOLVED: Promise<void> = Promise.resolve();
47
+
48
+ /** A misuse of the editing contract (e.g. processRowUpdate returned nothing), as opposed to a consumer rejection. */
49
+ class OpenGridXEditError extends Error {}
50
+
51
+ function isSameValue(a: unknown, b: unknown): boolean {
52
+ if (a instanceof Date && b instanceof Date) return a.getTime() === b.getTime();
53
+ return Object.is(a, b);
54
+ }
55
+
56
+ function isPromiseLike(value: unknown): value is PromiseLike<unknown> {
57
+ return typeof value === 'object' && value !== null && typeof (value as { then?: unknown }).then === 'function';
58
+ }
31
59
 
60
+ /**
61
+ * Cell edit sessions and their commits.
62
+ *
63
+ * - One commit per edit: a stop while a commit is in flight (Enter then blur, Tab then the blur that
64
+ * moving focus causes) shares that commit instead of calling `processRowUpdate` again. A stop that
65
+ * arrives after the value changed again commits the newer value once the first commit settles.
66
+ * - A commit that settles late only closes the editor if that same session is still open with the
67
+ * committed value; it never closes another cell's edit or drops text typed in the meantime. Its
68
+ * row is always applied: `processRowUpdate` has already run, and may already have persisted it.
69
+ * - Starting an edit on another cell commits the current one first, so an edit whose editor is gone
70
+ * (for example scrolled out of the render window) is never silently dropped.
71
+ * - The returned functions keep their identity for the lifetime of the grid.
72
+ */
73
+ export function useGridEditing<R extends GridRowModel>(params: UseGridEditingParams<R>) {
32
74
  const [editingCell, setEditingCellState] = useState<EditingCellState>(null);
33
75
  const editingCellRef = useRef<EditingCellState>(null);
76
+ // Id of the current edit session; bumped whenever a cell enters edit mode.
77
+ const sessionRef = useRef(0);
78
+ const pendingRef = useRef(new Map<number, PendingCommit>());
79
+ const warnedFieldsRef = useRef(new Set<string>());
80
+ const mountedRef = useRef(false);
34
81
 
35
- const setEditingCell = useCallback((val: React.SetStateAction<EditingCellState>) => {
36
- setEditingCellState(val);
37
- editingCellRef.current = typeof val === 'function' ? val(editingCellRef.current) : val;
38
- }, []);
82
+ // Latest props, read when an edit is committed so the callbacks below can stay stable.
83
+ const latestRef = useRef(params);
84
+ useLayoutEffect(() => {
85
+ latestRef.current = params;
86
+ });
39
87
 
40
- const startCellEdit = useCallback((editParams: { id: GridRowId; field: string; value: unknown }) => {
41
- setEditingCell({
42
- id: editParams.id,
43
- field: editParams.field,
44
- value: editParams.value,
45
- originalValue: editParams.value
46
- });
47
- }, [setEditingCell]);
88
+ useEffect(() => {
89
+ mountedRef.current = true;
90
+ return () => { mountedRef.current = false; };
91
+ }, []);
48
92
 
49
- const stopCellEdit = useCallback(async (stopParams?: { cancel?: boolean }) => {
50
- const currentCell = editingCellRef.current;
51
- if (!currentCell) return;
93
+ const setEditingCell = useCallback((next: EditingCellState) => {
94
+ editingCellRef.current = next;
95
+ setEditingCellState(next);
96
+ }, []);
52
97
 
53
- if (stopParams?.cancel) {
54
- setEditingCell(null);
55
- return;
98
+ const commit = useCallback((token: number, cell: EditingCell): Promise<void> => {
99
+ const inFlight = pendingRef.current.get(token);
100
+ if (inFlight) {
101
+ if (isSameValue(inFlight.value, cell.value)) return inFlight.promise;
102
+ // The value changed while a commit was in flight: commit the newer value once it settles.
103
+ return inFlight.promise.then(() => {
104
+ const now = editingCellRef.current;
105
+ if (sessionRef.current !== token || !now || isSameValue(now.value, inFlight.value)) return;
106
+ return commit(token, now);
107
+ });
56
108
  }
57
109
 
58
- const { id, field, value, originalValue } = currentCell;
110
+ const isCurrent = () => sessionRef.current === token && editingCellRef.current !== null;
59
111
 
60
- if (value === originalValue) {
61
- setEditingCell(null);
62
- return;
112
+ if (isSameValue(cell.value, cell.originalValue)) {
113
+ if (isCurrent()) setEditingCell(null);
114
+ return RESOLVED;
63
115
  }
64
116
 
65
- const existingRow = rows.find(r => getRowId(r) === id);
117
+ const { rows, getRowId, columns, processRowUpdate } = latestRef.current;
118
+ const existingRow = rows.find(r => getRowId(r) === cell.id);
66
119
  if (!existingRow) {
67
- setEditingCell(null);
68
- return;
120
+ if (isCurrent()) setEditingCell(null);
121
+ return RESOLVED;
69
122
  }
70
123
 
71
- const newRow = { ...existingRow, [field]: value };
124
+ const report = (error: unknown) => {
125
+ const onError = latestRef.current.onProcessRowUpdateError;
126
+ if (onError) {
127
+ onError(error);
128
+ } else if (process.env.NODE_ENV !== 'production' && error instanceof OpenGridXEditError) {
129
+ console.error(error.message);
130
+ }
131
+ };
72
132
 
73
- try {
74
- if (processRowUpdate) {
75
- const processedRow = await processRowUpdate(newRow, existingRow);
76
- onRowChange?.(processedRow);
133
+ const apply = (processed: unknown) => {
134
+ if (typeof processed !== 'object' || processed === null) {
135
+ report(new OpenGridXEditError(
136
+ `[OpenGridX] processRowUpdate must return the updated row object (or a Promise of it); ` +
137
+ `it returned ${processed === null ? 'null' : typeof processed}. The edit was not saved and the editor stays open.`
138
+ ));
139
+ return;
140
+ }
141
+ latestRef.current.onRowChange?.(processed as R, cell.id);
142
+ const now = editingCellRef.current;
143
+ if (sessionRef.current !== token || !now) return;
144
+ if (isSameValue(now.value, cell.value)) {
145
+ setEditingCell(null);
77
146
  } else {
78
- onRowChange?.(newRow);
147
+ // Newer text was typed while this commit was in flight: keep editing it.
148
+ setEditingCell({ ...now, originalValue: cell.value });
79
149
  }
80
- setEditingCell(null);
150
+ };
151
+
152
+ // Register the commit before calling out, so a re-entrant stop joins it.
153
+ let settle!: () => void;
154
+ let fail!: (error: unknown) => void;
155
+ const promise = new Promise<void>((resolve, reject) => { settle = resolve; fail = reject; });
156
+ const entry: PendingCommit = { value: cell.value, promise };
157
+ pendingRef.current.set(token, entry);
158
+ const finish = () => {
159
+ if (pendingRef.current.get(token) === entry) pendingRef.current.delete(token);
160
+ };
161
+
162
+ let result: unknown;
163
+ try {
164
+ const colDef = columns?.find(c => c.field === cell.field);
165
+ const newRow = buildEditedRow(existingRow, cell, colDef, warnedFieldsRef.current);
166
+ result = processRowUpdate ? processRowUpdate(newRow, existingRow) : newRow;
81
167
  } catch (error) {
82
- onProcessRowUpdateError?.(error);
168
+ // Consumer code (valueSetter / processRowUpdate) threw: report it, keep the editor open.
169
+ finish();
170
+ report(error);
171
+ settle();
172
+ return promise;
83
173
  }
84
174
 
85
- }, [setEditingCell, rows, getRowId, processRowUpdate, onRowChange, onProcessRowUpdateError]);
175
+ const complete = (processed: unknown) => {
176
+ finish();
177
+ try {
178
+ apply(processed);
179
+ settle();
180
+ } catch (error) {
181
+ // A failure while the grid stores the row is a grid error, not a processRowUpdate one.
182
+ fail(error);
183
+ }
184
+ };
86
185
 
87
- const setEditCellValue = useCallback((editParams: { id: GridRowId; field: string; value: unknown }) => {
88
- if (editingCell && editingCell.id === editParams.id && editingCell.field === editParams.field) {
89
- setEditingCell(prev => prev ? { ...prev, value: editParams.value } : null);
186
+ if (isPromiseLike(result)) {
187
+ Promise.resolve(result).then(complete, (error: unknown) => {
188
+ finish();
189
+ report(error);
190
+ settle();
191
+ });
192
+ } else {
193
+ complete(result);
90
194
  }
91
- }, [editingCell, setEditingCell]);
195
+ return promise;
196
+ }, [setEditingCell]);
92
197
 
93
- return {
198
+ const startCellEdit = useCallback((editParams: { id: GridRowId; field: string; value: unknown }) => {
199
+ const current = editingCellRef.current;
200
+ if (current) {
201
+ if (current.id === editParams.id && current.field === editParams.field) return;
202
+ // Commit the edit in progress before switching cells.
203
+ void commit(sessionRef.current, current);
204
+ }
205
+ sessionRef.current += 1;
206
+ setEditingCell({
207
+ id: editParams.id,
208
+ field: editParams.field,
209
+ value: editParams.value,
210
+ originalValue: editParams.value
211
+ });
212
+ }, [commit, setEditingCell]);
213
+
214
+ const stopCellEdit = useCallback((stopParams?: StopCellEditParams): Promise<void> => {
215
+ const cell = editingCellRef.current;
216
+ if (!cell || !mountedRef.current) return RESOLVED;
217
+ if (stopParams?.id !== undefined && stopParams.id !== cell.id) return RESOLVED;
218
+ if (stopParams?.field !== undefined && stopParams.field !== cell.field) return RESOLVED;
219
+
220
+ if (stopParams?.cancel) {
221
+ setEditingCell(null);
222
+ return RESOLVED;
223
+ }
224
+ return commit(sessionRef.current, cell);
225
+ }, [commit, setEditingCell]);
226
+
227
+ const setEditCellValue = useCallback((editParams: { id: GridRowId; field: string; value: unknown }) => {
228
+ const current = editingCellRef.current;
229
+ if (!current || current.id !== editParams.id || current.field !== editParams.field) return;
230
+ setEditingCell({ ...current, value: editParams.value });
231
+ }, [setEditingCell]);
232
+
233
+ return useMemo(() => ({
94
234
  editingCell,
95
235
  startCellEdit,
96
236
  stopCellEdit,
97
237
  setEditCellValue
98
- };
238
+ }), [editingCell, startCellEdit, stopCellEdit, setEditCellValue]);
239
+ }
240
+
241
+ function buildEditedRow<R extends GridRowModel>(
242
+ row: R,
243
+ cell: EditingCell,
244
+ colDef: GridColDef<R> | undefined,
245
+ warnedFields: Set<string>
246
+ ): R {
247
+ if (colDef?.valueSetter) {
248
+ return colDef.valueSetter({ value: cell.value, row, field: cell.field });
249
+ }
250
+ if (colDef?.valueGetter && process.env.NODE_ENV !== 'production' && !warnedFields.has(cell.field)) {
251
+ warnedFields.add(cell.field);
252
+ console.warn(
253
+ `[OpenGridX] Column "${cell.field}" is editable and has a valueGetter but no valueSetter. ` +
254
+ `The edited value was written to row["${cell.field}"], which the valueGetter may ignore, so the cell ` +
255
+ 'can keep showing the old value. Add a valueSetter that maps the value back onto the row.'
256
+ );
257
+ }
258
+ return { ...row, [cell.field]: cell.value };
99
259
  }
@@ -0,0 +1,92 @@
1
+ import { useEffect, useMemo, useRef } from 'react';
2
+ import type { GridColDef, GridFilterModel, GridPivotModel, GridRowModel, GridSortItem } from '../../types';
3
+ import { filterRows } from '../../utils/filtering';
4
+ import { buildColumnLookup } from '../../utils/columnLookup';
5
+ import { computePivot, splitPivotFilterModel, EMPTY_PIVOT_RESULT } from '../../utils/pivot';
6
+
7
+ const EMPTY_FILTER_MODEL: GridFilterModel = { items: [] };
8
+ const EMPTY_SORT_MODEL: GridSortItem[] = [];
9
+
10
+ export interface UseGridPivotParams {
11
+ rows: GridRowModel[];
12
+ columns: GridColDef[];
13
+ pivotMode: boolean;
14
+ pivotModel: GridPivotModel;
15
+ filterModel: GridFilterModel;
16
+ sortModel: GridSortItem[];
17
+ /** Pivoting is client-side: with a dataSource the grid only holds one page, so pivot mode is not applied. */
18
+ hasDataSource: boolean;
19
+ /** treeData or a rowGroupingModel was requested; both are turned off while pivoting. */
20
+ hierarchyRequested: boolean;
21
+ }
22
+
23
+ export interface UseGridPivotResult {
24
+ /** Pivot mode is on, the model is usable and the grid is client-side. */
25
+ isActive: boolean;
26
+ /** Sorted pivot data rows followed by the Grand Total row. */
27
+ rows: GridRowModel[];
28
+ columns: GridColDef[];
29
+ /**
30
+ * Filter and sort models for the row pipeline. While pivoting they are empty: the pivot already
31
+ * applied them (filters to the source rows or the pivot rows, sorting with the Grand Total kept last).
32
+ */
33
+ pipelineFilterModel: GridFilterModel;
34
+ pipelineSortModel: GridSortItem[];
35
+ }
36
+
37
+ /**
38
+ * DataGrid's pivot stage. Source-field filters and the quick filter run on the source rows before
39
+ * pivoting, so pivot values and the Grand Total reflect them; conditions on generated value columns run
40
+ * on the pivot rows. Sorting orders the pivot data rows and never moves the Grand Total.
41
+ */
42
+ export function useGridPivot(params: UseGridPivotParams): UseGridPivotResult {
43
+ const { rows, columns, pivotMode, pivotModel, filterModel, sortModel, hasDataSource, hierarchyRequested } = params;
44
+ const enabled = pivotMode && !hasDataSource;
45
+
46
+ const sourceFields = useMemo(() => new Set(columns.map((c) => c.field)), [columns]);
47
+ // No source column is on screen while pivoting (the row-label columns show the row fields
48
+ // whatever the visibility model says), so the quick filter searches every filterable source column.
49
+ const sourceLookup = useMemo(() => buildColumnLookup(columns), [columns]);
50
+ const splitFilter = useMemo(
51
+ () => splitPivotFilterModel(filterModel, sourceFields),
52
+ [filterModel, sourceFields],
53
+ );
54
+ const sourceRows = useMemo(
55
+ () => (enabled ? filterRows(rows, splitFilter.source, sourceLookup) : rows),
56
+ [enabled, rows, splitFilter.source, sourceLookup],
57
+ );
58
+ const pivot = useMemo(
59
+ () => (enabled
60
+ ? computePivot(sourceRows, columns, pivotModel, { outputFilterModel: splitFilter.output, sortModel })
61
+ : EMPTY_PIVOT_RESULT),
62
+ [enabled, sourceRows, columns, pivotModel, splitFilter.output, sortModel],
63
+ );
64
+ const isActive = enabled && pivot.isValid;
65
+
66
+ const warnedRef = useRef({ dataSource: false, hierarchy: false });
67
+ useEffect(() => {
68
+ if (process.env.NODE_ENV === 'production') return;
69
+ if (pivotMode && hasDataSource && !warnedRef.current.dataSource) {
70
+ warnedRef.current.dataSource = true;
71
+ console.warn(
72
+ '[OpenGridX] `pivotMode` is ignored when a `dataSource` is set: pivoting is client-side and the grid ' +
73
+ 'only holds the rows of one server page. Pivot on the server, or pass the full dataset as `rows`.'
74
+ );
75
+ }
76
+ if (isActive && hierarchyRequested && !warnedRef.current.hierarchy) {
77
+ warnedRef.current.hierarchy = true;
78
+ console.warn(
79
+ '[OpenGridX] `treeData` and `rowGroupingModel` are turned off while `pivotMode` is active: the pivot ' +
80
+ 'rows are already grouped by the pivot row fields.'
81
+ );
82
+ }
83
+ }, [pivotMode, hasDataSource, isActive, hierarchyRequested]);
84
+
85
+ return {
86
+ isActive,
87
+ rows: isActive ? pivot.pivotRows : rows,
88
+ columns: isActive ? pivot.pivotColumns : columns,
89
+ pipelineFilterModel: isActive ? EMPTY_FILTER_MODEL : filterModel,
90
+ pipelineSortModel: isActive ? EMPTY_SORT_MODEL : sortModel,
91
+ };
92
+ }
@@ -0,0 +1,104 @@
1
+ import { useMemo } from 'react';
2
+ import type { GridColDef, GridRowId, GridRowModel } from '../../types';
3
+ import type { GridSpanningResult } from './useGridSpanning';
4
+
5
+ interface RowRenderContext {
6
+ firstRowIndex: number;
7
+ lastRowIndex: number;
8
+ firstColumnIndex: number;
9
+ lastColumnIndex: number;
10
+ }
11
+
12
+ export interface UseGridSpanRowWindowParams {
13
+ spanning: GridSpanningResult;
14
+ renderContext: RowRenderContext;
15
+ offsetTop: number;
16
+ /** Cumulative heights of the center rows (layout output). */
17
+ cumulativeHeights: number[];
18
+ }
19
+
20
+ /**
21
+ * Extends the vertical render window back to the origin row of any row span that reaches the first
22
+ * rendered row. Only the origin renders the merged content, so without this the visible part of a
23
+ * span whose origin has scrolled above the overscan window would be blank.
24
+ */
25
+ export function useGridSpanRowWindow(params: UseGridSpanRowWindowParams): { renderContext: RowRenderContext; offsetTop: number } {
26
+ const { spanning, renderContext, offsetTop, cumulativeHeights } = params;
27
+
28
+ return useMemo(() => {
29
+ if (!spanning.hasRowSpan) return { renderContext, offsetTop };
30
+ let first = Math.max(0, renderContext.firstRowIndex);
31
+ for (let start = spanning.getCenterRowSpanStart(first); start < first; start = spanning.getCenterRowSpanStart(first)) {
32
+ first = start;
33
+ }
34
+ if (first >= renderContext.firstRowIndex) return { renderContext, offsetTop };
35
+ return {
36
+ renderContext: { ...renderContext, firstRowIndex: first },
37
+ offsetTop: first > 0 ? cumulativeHeights[first - 1] ?? 0 : 0,
38
+ };
39
+ }, [spanning, renderContext, offsetTop, cumulativeHeights]);
40
+ }
41
+
42
+ interface ColumnLayout<R extends GridRowModel> {
43
+ leftPinnedCols: GridColDef<R>[];
44
+ unpinnedColsWithWidth: (GridColDef<R> & { width: number })[];
45
+ rightPinnedCols: GridColDef<R>[];
46
+ unpinnedAccWidths: number[];
47
+ unpinnedTotalWidth: number;
48
+ }
49
+
50
+ export interface UseGridSpanColumnWindowParams<R extends GridRowModel> {
51
+ spanning: GridSpanningResult;
52
+ /** Unpinned column window chosen by the virtualization. */
53
+ firstColumnIndex: number;
54
+ lastColumnIndex: number;
55
+ virtualColumns: GridColDef<R>[];
56
+ layout: ColumnLayout<R>;
57
+ /** Rows that will be rendered, with their resolved id (rows do not have to carry it in `row.id`). */
58
+ rows: { row: R; id?: GridRowId }[];
59
+ }
60
+
61
+ /**
62
+ * Widens the horizontal render window so every colSpan in the rendered rows is either fully inside
63
+ * it or fully outside it. A span origin left of the window would otherwise render nothing while its
64
+ * covered columns render nothing either, shifting the rest of the row left; a span that runs past
65
+ * the right edge would overflow the right spacer.
66
+ */
67
+ export function useGridSpanColumnWindow<R extends GridRowModel>(params: UseGridSpanColumnWindowParams<R>): GridColDef<R>[] {
68
+ const { spanning, firstColumnIndex, lastColumnIndex, virtualColumns, layout, rows } = params;
69
+
70
+ return useMemo(() => {
71
+ if (!spanning.hasColSpan || layout.unpinnedColsWithWidth.length === 0) return virtualColumns;
72
+
73
+ let first = firstColumnIndex;
74
+ let last = lastColumnIndex;
75
+ let changed = true;
76
+ while (changed) {
77
+ changed = false;
78
+ for (const { row, id } of rows) {
79
+ const ranges = spanning.getUnpinnedColSpanRanges(id ?? row.id);
80
+ if (!ranges) continue;
81
+ for (let k = 0; k < ranges.length; k += 2) {
82
+ const start = ranges[k];
83
+ const end = ranges[k + 1];
84
+ if (end < first || start > last) continue;
85
+ if (start < first) { first = start; changed = true; }
86
+ if (end > last) { last = end; changed = true; }
87
+ }
88
+ }
89
+ }
90
+
91
+ if (first === firstColumnIndex && last === lastColumnIndex) return virtualColumns;
92
+
93
+ const { leftPinnedCols, unpinnedColsWithWidth, rightPinnedCols, unpinnedAccWidths, unpinnedTotalWidth } = layout;
94
+ const leftSpacerWidth = first > 0 ? unpinnedAccWidths[first - 1] : 0;
95
+ const rightSpacerWidth = unpinnedTotalWidth - unpinnedAccWidths[last];
96
+ return [
97
+ ...leftPinnedCols,
98
+ ...(leftSpacerWidth > 0 ? [{ field: '__spacer_left__', width: leftSpacerWidth, isSpacer: true } as GridColDef<R>] : []),
99
+ ...unpinnedColsWithWidth.slice(first, last + 1),
100
+ ...(rightSpacerWidth > 0 ? [{ field: '__spacer_right__', width: rightSpacerWidth, isSpacer: true } as GridColDef<R>] : []),
101
+ ...rightPinnedCols,
102
+ ];
103
+ }, [spanning, firstColumnIndex, lastColumnIndex, virtualColumns, layout, rows]);
104
+ }