@opencorestack/opengridx 2.1.0 → 3.0.1

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 (225) hide show
  1. package/CHANGELOG.md +399 -0
  2. package/README.md +95 -72
  3. package/dist/index.d.ts +590 -131
  4. package/dist/opengridx.css +1 -1
  5. package/dist/opengridx.es.js +7891 -5631
  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 -166
  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 +6 -7
  29. package/docs/components/datagrid.md +15 -13
  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 +90 -38
  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 +126 -39
  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 +72 -19
  54. package/docs/features/pinning.md +24 -5
  55. package/docs/features/selection.md +50 -10
  56. package/docs/features/sorting-pagination.md +64 -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 +36 -8
  60. package/docs/features/virtualization.md +37 -14
  61. package/docs/migration/v1-to-v2.md +115 -0
  62. package/docs/migration/v2-to-v3.md +1233 -0
  63. package/docs/performance.md +124 -0
  64. package/docs/roadmap.md +5 -5
  65. package/lib/components/Cell/Cell.css +17 -1
  66. package/lib/components/Cell/Cell.tsx +129 -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 +15 -2
  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 +85 -12
  76. package/lib/components/DataGrid/DataGrid.tsx +582 -876
  77. package/lib/components/DataGrid/GridAggregationFooter.tsx +53 -17
  78. package/lib/components/DataGrid/GridErrorOverlay.tsx +15 -12
  79. package/lib/components/DataGrid/GridListView.tsx +165 -24
  80. package/lib/components/DataGrid/GridLiveRegion.tsx +31 -0
  81. package/lib/components/DataGrid/GridLoadingOverlay.tsx +27 -0
  82. package/lib/components/DataGrid/GridPaginationArea.tsx +44 -0
  83. package/lib/components/DataGrid/GridPinnedRows.tsx +62 -11
  84. package/lib/components/DataGrid/GridStandaloneColumnPanel.tsx +40 -38
  85. package/lib/components/DataGrid/GridToolbarSlot.tsx +51 -0
  86. package/lib/components/DataGrid/GridVirtualRows.tsx +40 -24
  87. package/lib/components/DetailPanel/DetailPanel.css +0 -6
  88. package/lib/components/DetailPanel/DetailPanel.tsx +34 -5
  89. package/lib/components/DetailPanel/DetailPanelContent.tsx +17 -0
  90. package/lib/components/FilterPanel/FilterPanel.css +13 -2
  91. package/lib/components/FilterPanel/FilterPanel.tsx +272 -99
  92. package/lib/components/Header/ColumnMenu.tsx +91 -39
  93. package/lib/components/Header/Header.css +6 -3
  94. package/lib/components/Header/Header.tsx +181 -160
  95. package/lib/components/ListView/ListView.css +25 -0
  96. package/lib/components/ListView/ListViewRow.tsx +76 -10
  97. package/lib/components/Pagination/Pagination.tsx +15 -7
  98. package/lib/components/Row/Row.tsx +177 -77
  99. package/lib/components/Toolbar/GlobalSearch.css +6 -3
  100. package/lib/components/Toolbar/GlobalSearch.tsx +26 -27
  101. package/lib/components/Toolbar/GridToolbar.css +12 -11
  102. package/lib/components/Toolbar/GridToolbar.tsx +133 -89
  103. package/lib/components/Toolbar/PivotPanel.tsx +89 -49
  104. package/lib/components/Tooltip/Tooltip.css +20 -0
  105. package/lib/components/Tooltip/Tooltip.tsx +68 -35
  106. package/lib/components/ui/Button.tsx +3 -0
  107. package/lib/components/ui/Checkbox.tsx +2 -1
  108. package/lib/components/ui/ExpandIcon.css +0 -9
  109. package/lib/components/ui/ExpandIcon.tsx +4 -1
  110. package/lib/hooks/core/gridApiPlaceholder.ts +34 -0
  111. package/lib/hooks/core/gridToolbarHostContext.ts +14 -0
  112. package/lib/hooks/core/useDataGrid.ts +115 -173
  113. package/lib/hooks/core/useGridAggregationApi.ts +72 -0
  114. package/lib/hooks/core/useGridApiMethods.ts +115 -0
  115. package/lib/hooks/core/useGridApiRef.ts +19 -4
  116. package/lib/hooks/core/useGridApiRefBinding.ts +18 -0
  117. package/lib/hooks/core/useGridAriaRows.ts +38 -0
  118. package/lib/hooks/core/useGridClipboardApi.ts +38 -0
  119. package/lib/hooks/core/useGridColumnLookup.ts +30 -0
  120. package/lib/hooks/core/useGridColumns.tsx +279 -76
  121. package/lib/hooks/core/useGridColumnsPanel.ts +77 -0
  122. package/lib/hooks/core/useGridControlledState.ts +99 -6
  123. package/lib/hooks/core/useGridDetailPanel.ts +51 -0
  124. package/lib/hooks/core/useGridDevWarnings.ts +104 -5
  125. package/lib/hooks/core/useGridFocusHandlers.ts +50 -0
  126. package/lib/hooks/core/useGridGroupingColumn.ts +62 -0
  127. package/lib/hooks/core/useGridHeaderHandlers.ts +92 -0
  128. package/lib/hooks/core/useGridHierarchy.ts +81 -0
  129. package/lib/hooks/core/useGridKeyboardNavigation.ts +447 -243
  130. package/lib/hooks/core/useGridLiveRowSelection.ts +67 -0
  131. package/lib/hooks/core/useGridPageCorrection.ts +55 -0
  132. package/lib/hooks/core/useGridRowIdOf.ts +32 -0
  133. package/lib/hooks/core/useGridRowInteractions.ts +81 -0
  134. package/lib/hooks/core/useGridRowPipeline.ts +69 -39
  135. package/lib/hooks/core/useGridRowSelection.ts +91 -0
  136. package/lib/hooks/core/useGridScrollSync.ts +80 -32
  137. package/lib/hooks/core/useGridScrollToIndexesApi.ts +45 -0
  138. package/lib/hooks/core/useGridStateSnapshot.ts +40 -5
  139. package/lib/hooks/core/useGridThemeDimensions.ts +49 -0
  140. package/lib/hooks/core/useGridToolbarProps.ts +97 -0
  141. package/lib/hooks/core/useGridViewport.ts +38 -0
  142. package/lib/hooks/core/useGridViewportSize.ts +26 -0
  143. package/lib/hooks/core/useGridVisibleRows.ts +17 -5
  144. package/lib/hooks/core/useLayout.ts +215 -195
  145. package/lib/hooks/core/useRowsScrollEndLatch.ts +41 -0
  146. package/lib/hooks/core/useStableColumns.ts +34 -0
  147. package/lib/hooks/features/useAggregation.ts +82 -51
  148. package/lib/hooks/features/useColumnGroupReorderGuard.ts +34 -0
  149. package/lib/hooks/features/useDetailPanelHeights.ts +29 -0
  150. package/lib/hooks/features/useGridClipboard.ts +133 -73
  151. package/lib/hooks/features/useGridDataSource.ts +354 -128
  152. package/lib/hooks/features/useGridEditing.ts +211 -51
  153. package/lib/hooks/features/useGridPivot.ts +92 -0
  154. package/lib/hooks/features/useGridSpanRenderWindow.ts +104 -0
  155. package/lib/hooks/features/useGridSpanning.ts +410 -306
  156. package/lib/hooks/features/usePivot.ts +0 -0
  157. package/lib/hooks/features/useServerTreeChildren.ts +50 -0
  158. package/lib/hooks/useColumnReorder.ts +38 -44
  159. package/lib/hooks/useRowGrouping.ts +298 -160
  160. package/lib/hooks/useRowReorder.ts +62 -31
  161. package/lib/hooks/useTreeData.ts +287 -220
  162. package/lib/index.ts +28 -2
  163. package/lib/state/index.ts +1 -0
  164. package/lib/state/types.ts +7 -1
  165. package/lib/state/useGridStateStorage.ts +88 -29
  166. package/lib/theme/DataGridThemeProvider.tsx +143 -38
  167. package/lib/theme/gridThemeContext.ts +5 -0
  168. package/lib/theme/index.ts +5 -0
  169. package/lib/theme/presets.ts +5 -4
  170. package/lib/theme/types.ts +40 -2
  171. package/lib/types/index.ts +313 -116
  172. package/lib/utils/aggregation/index.ts +119 -24
  173. package/lib/utils/aria/index.ts +40 -0
  174. package/lib/utils/columnGroups.ts +110 -0
  175. package/lib/utils/columnLookup.ts +33 -0
  176. package/lib/utils/columnWidth.ts +22 -0
  177. package/lib/utils/dataSource/index.ts +46 -0
  178. package/lib/utils/detailPanel.ts +66 -0
  179. package/lib/utils/dragData.ts +44 -0
  180. package/lib/utils/editing/index.ts +30 -0
  181. package/lib/utils/export/exportShared.ts +197 -0
  182. package/lib/utils/export/exportToExcelAdvanced.ts +278 -139
  183. package/lib/utils/export/exportToPdf.ts +143 -92
  184. package/lib/utils/export/groupLabel.ts +14 -0
  185. package/lib/utils/export/index.ts +278 -272
  186. package/lib/utils/export/pdfText.ts +50 -0
  187. package/lib/utils/filtering/index.ts +277 -148
  188. package/lib/utils/focus/index.ts +115 -0
  189. package/lib/utils/gridApiRows.ts +35 -0
  190. package/lib/utils/gridModes.ts +77 -0
  191. package/lib/utils/grouping/groupedExportRows.ts +91 -0
  192. package/lib/utils/pagination/index.ts +56 -0
  193. package/lib/utils/pinning/index.ts +77 -33
  194. package/lib/utils/pivot/index.ts +368 -0
  195. package/lib/utils/scroll/index.ts +55 -5
  196. package/lib/utils/scroll/scrollEnd.ts +18 -0
  197. package/lib/utils/sorting/index.ts +154 -30
  198. package/lib/utils/syntheticRows.ts +12 -0
  199. package/lib/utils/values.ts +206 -0
  200. package/lib/utils/viewport.ts +14 -0
  201. package/llms.txt +261 -164
  202. package/package.json +13 -5
  203. package/lib/components/ColumnGroupHeader/ColumnGroupHeader.tsx +0 -220
  204. package/lib/components/ColumnVisibilityPanel/ColumnVisibilityPanel.test.tsx +0 -85
  205. package/lib/components/DataGrid/DataGrid.defects.test.tsx +0 -103
  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/DataGrid/DataGrid.virtualization.browser.test.tsx +0 -183
  210. package/lib/components/Header/Header.test.tsx +0 -135
  211. package/lib/components/Pagination/Pagination.test.tsx +0 -64
  212. package/lib/hooks/core/useDataGrid.regression.test.ts +0 -66
  213. package/lib/hooks/core/useGridControlledState.test.ts +0 -86
  214. package/lib/hooks/core/useGridKeyboardNavigation.test.ts +0 -176
  215. package/lib/hooks/core/useGridRowPipeline.test.ts +0 -117
  216. package/lib/hooks/features/useAggregation.test.ts +0 -175
  217. package/lib/hooks/features/usePivot.test.ts +0 -202
  218. package/lib/hooks/useRowGrouping.test.ts +0 -298
  219. package/lib/hooks/useTreeData.test.ts +0 -83
  220. package/lib/utils/aggregation/aggregation.test.ts +0 -16
  221. package/lib/utils/export/exportToExcelAdvanced.test.ts +0 -109
  222. package/lib/utils/export/exportToPdf.test.ts +0 -120
  223. package/lib/utils/export/groupedExport.test.ts +0 -144
  224. package/lib/utils/filtering/filtering.test.ts +0 -178
  225. package/lib/utils/sorting/sorting.test.ts +0 -88
@@ -1,349 +1,453 @@
1
-
2
- import { useState, useCallback, useEffect } from 'react';
3
- import type { GridRowId, GridColDef, GridRowModel, GridRenderCellParams } from '../../types';
1
+ import { useMemo } from 'react';
2
+ import type { GridRowId, GridColDef, GridRowModel, GridRenderCellParams, GridRowMeta } from '../../types';
3
+ import { isSyntheticRowId } from '../../utils/syntheticRows';
4
4
 
5
5
  type ColumnKey = string;
6
6
 
7
- export type CellColSpanInfo =
8
- | {
9
- spannedByColSpan: true;
10
- rightVisibleCellIndex: number;
11
- leftVisibleCellIndex: number;
7
+ /**
8
+ * Column-span information for one cell.
9
+ *
10
+ * Only span origins (colSpan > 1) and the cells they cover have an entry; every other cell has none
11
+ * and renders at its own resolved width. `leftVisibleCellIndex` / `rightVisibleCellIndex` are indices
12
+ * into the rendered data columns (left-pinned, then unpinned, then right-pinned).
13
+ *
14
+ * The hook stores only `colSpan` for an origin. `Row` adds the merged `width` and `flexGrow` from the
15
+ * columns it renders, so resizing a column never recomputes the spans.
16
+ */
17
+ export type CellColSpanInfo =
18
+ | {
19
+ spannedByColSpan: true;
20
+ rightVisibleCellIndex: number;
21
+ leftVisibleCellIndex: number;
12
22
  }
13
- | {
14
- spannedByColSpan: false;
15
- cellProps: {
16
- colSpan: number;
17
- width: number;
18
- };
23
+ | {
24
+ spannedByColSpan: false;
25
+ cellProps: {
26
+ colSpan: number;
27
+ width?: number;
28
+ flexGrow?: number;
29
+ };
19
30
  };
20
31
 
21
- type ColspanMap = Map<GridRowId, Record<ColumnKey, CellColSpanInfo>>;
32
+ export type ColspanMap = Map<GridRowId, Record<ColumnKey, CellColSpanInfo>>;
22
33
 
23
34
  export interface RowSpanningCaches {
24
- spannedCells: Record<GridRowId, Record<ColumnKey, number>>;
25
- hiddenCells: Record<GridRowId, Record<ColumnKey, boolean>>;
26
- hiddenCellOriginMap: Record<number, Record<ColumnKey, number>>;
35
+ /** Row id → field → number of rows the origin cell spans (only entries > 1). */
36
+ spannedCells: Record<GridRowId, Record<ColumnKey, number>>;
37
+ /** Row id → field → `true` for cells covered by a row span that starts in a row above. */
38
+ hiddenCells: Record<GridRowId, Record<ColumnKey, boolean>>;
39
+ /** Row id → field → id of the row whose span covers the hidden cell. */
40
+ hiddenCellOriginMap: Record<GridRowId, Record<ColumnKey, GridRowId>>;
27
41
  }
28
42
 
29
- export interface RowSpanningState {
30
- caches: RowSpanningCaches;
31
- processedRange: { firstRowIndex: number; lastRowIndex: number; };
43
+ /** The top-left cell of a merged area. */
44
+ export interface GridSpanOrigin {
45
+ rowId: GridRowId;
46
+ field: string;
32
47
  }
33
48
 
34
- export function useGridSpanning<R extends GridRowModel>(
35
- rows: R[],
36
- columns: GridColDef<R>[],
37
- columnWidths: Record<string, number>
38
- ) {
39
-
40
- const [colspanMap, setColspanMap] = useState<ColspanMap>(new Map());
41
-
42
- const [rowSpanningState, setRowSpanningState] = useState<RowSpanningState>({
43
- caches: {
44
- spannedCells: {},
45
- hiddenCells: {},
46
- hiddenCellOriginMap: {}
47
- },
48
- processedRange: { firstRowIndex: 0, lastRowIndex: 0 }
49
- });
50
-
51
- const resetColSpan = useCallback(() => {
52
- setColspanMap(new Map());
53
- }, []);
54
-
55
- const getCellColSpanInfo = useCallback((rowId: GridRowId, field: string): CellColSpanInfo | undefined => {
56
- return colspanMap.get(rowId)?.[field];
57
- }, [colspanMap]);
58
-
59
- const calculateColSpan = useCallback((
60
- rowId: GridRowId,
61
- row: R,
62
- rowIndex: number,
63
- minFirstColumn: number,
64
- maxLastColumn: number
65
- ) => {
66
- const newMap = new Map(colspanMap);
67
-
68
- for (let i = minFirstColumn; i < maxLastColumn; i++) {
69
- const colDef = columns[i];
70
- if (!colDef) continue;
71
-
72
- let colSpan = 1;
73
- if (colDef.colSpan) {
74
- if (typeof colDef.colSpan === 'function') {
75
- const params: GridRenderCellParams<R> = {
76
- row,
77
- value: row[colDef.field as keyof R],
78
- field: colDef.field,
79
- colDef,
80
- rowIndex,
81
- colIndex: i
82
- };
83
- colSpan = colDef.colSpan(params);
84
- } else {
85
- colSpan = colDef.colSpan;
86
- }
87
- }
88
-
89
- if (colSpan <= 1) {
90
- setCellColSpanInfo(newMap, rowId, colDef.field, {
91
- spannedByColSpan: false,
92
- cellProps: { colSpan: 1, width: columnWidths[colDef.field] || 100 }
93
- });
94
- continue;
95
- }
96
-
97
- let width = columnWidths[colDef.field] || 100;
98
- for (let j = 1; j < colSpan; j++) {
99
- if (i + j < maxLastColumn) {
100
- const nextCol = columns[i + j];
101
- if (nextCol) {
102
- width += columnWidths[nextCol.field] || 100;
103
-
104
- setCellColSpanInfo(newMap, rowId, nextCol.field, {
105
- spannedByColSpan: true,
106
- leftVisibleCellIndex: i,
107
- rightVisibleCellIndex: Math.min(i + colSpan - 1, columns.length - 1)
108
- });
109
- }
110
- }
111
- }
49
+ export interface UseGridSpanningParams<R extends GridRowModel> {
50
+ /** Row sections as they are rendered. A span never crosses from one section into another. */
51
+ pinnedTopRows: R[];
52
+ /** The scrolling rows that are rendered: the current page when paginated. */
53
+ centerRows: R[];
54
+ pinnedBottomRows: R[];
55
+ /** Visible data columns; used to look up the column definitions (colSpan, rowSpan, valueGetter). */
56
+ columns: GridColDef<R>[];
57
+ /**
58
+ * Rendered data columns per pinned section, in render order (the layout output). Only their
59
+ * fields and order matter here, so a width change does not recompute the spans.
60
+ */
61
+ leftPinnedColumns: GridColDef<R>[];
62
+ unpinnedColumns: GridColDef<R>[];
63
+ rightPinnedColumns: GridColDef<R>[];
64
+ /** Rows whose detail panel is expanded. A row span ends at such a row, above its panel. */
65
+ expandedRowIds?: Set<GridRowId>;
66
+ rowMetaMap?: Map<GridRowId, GridRowMeta>;
67
+ /** Resolves a row's id (the grid's getRowId); span caches are keyed by it. Defaults to `row.id`. */
68
+ getRowId?: (row: R) => GridRowId;
69
+ }
112
70
 
113
- setCellColSpanInfo(newMap, rowId, colDef.field, {
114
- spannedByColSpan: false,
115
- cellProps: { colSpan, width }
116
- });
71
+ const defaultGetRowId = <R extends GridRowModel>(row: R): GridRowId => row.id;
72
+
73
+ export interface GridSpanningResult {
74
+ hasColSpan: boolean;
75
+ hasRowSpan: boolean;
76
+ colspanMap: ColspanMap;
77
+ rowSpanningCaches: RowSpanningCaches;
78
+ /** For a cell covered by a colSpan or rowSpan, the origin cell of that span; otherwise `null`. */
79
+ getSpanOrigin: (rowId: GridRowId, field: string) => GridSpanOrigin | null;
80
+ /**
81
+ * Unpinned column index ranges merged by colSpan in a row, as flat pairs
82
+ * `[first0, last0, first1, last1, …]` in the unpinned-column index space.
83
+ */
84
+ getUnpinnedColSpanRanges: (rowId: GridRowId) => readonly number[] | undefined;
85
+ /**
86
+ * For a center (scrolling) row index, the index of the earliest row whose row span reaches it,
87
+ * or the index itself when no span from above reaches it.
88
+ */
89
+ getCenterRowSpanStart: (centerRowIndex: number) => number;
90
+ }
117
91
 
118
- i += colSpan - 1;
119
- }
92
+ interface SpanColumn<R extends GridRowModel> {
93
+ colDef: GridColDef<R>;
94
+ field: string;
95
+ /** Exclusive end of this column's pinned section, as an index into the rendered columns. */
96
+ sectionEnd: number;
97
+ hasColSpan: boolean;
98
+ hasRowSpan: boolean;
99
+ }
120
100
 
121
- setColspanMap(newMap);
122
- }, [colspanMap, columns, columnWidths]);
123
-
124
- const calculateRowSpan = useCallback((
125
- firstRowIndex: number,
126
- lastRowIndex: number
127
- ) => {
128
- const spannedCells: Record<GridRowId, Record<ColumnKey, number>> = {};
129
- const hiddenCells: Record<GridRowId, Record<ColumnKey, boolean>> = {};
130
- const hiddenCellOriginMap: Record<number, Record<ColumnKey, number>> = {};
131
-
132
- columns.forEach((colDef, columnIndex) => {
133
-
134
- if (!colDef.rowSpan) return;
135
-
136
- for (let index = firstRowIndex; index < lastRowIndex; index++) {
137
- const row = rows[index];
138
- if (!row) continue;
139
-
140
- if (hiddenCells[row.id]?.[colDef.field]) continue;
141
-
142
- const cellValue = colDef.valueGetter
143
- ? colDef.valueGetter({ row, field: colDef.field, value: row[colDef.field as keyof R] })
144
- : row[colDef.field as keyof R];
145
-
146
- let span = 1;
147
- if (typeof colDef.rowSpan === 'function') {
148
- const params: GridRenderCellParams<R> = {
149
- row,
150
- value: cellValue,
151
- field: colDef.field,
152
- colDef,
153
- rowIndex: index,
154
- colIndex: columnIndex
155
- };
156
- span = colDef.rowSpan(params);
157
- } else if (typeof colDef.rowSpan === 'number') {
158
- span = colDef.rowSpan;
159
- }
101
+ interface SpanComputation {
102
+ colspanMap: ColspanMap;
103
+ rowSpanningCaches: RowSpanningCaches;
104
+ /** For hidden cells whose origin sits in another column (colSpan × rowSpan rectangles). */
105
+ hiddenOriginFields: Map<GridRowId, Record<ColumnKey, string>>;
106
+ unpinnedRanges: Map<GridRowId, number[]>;
107
+ centerRowSpanStart: Map<number, number>;
108
+ }
160
109
 
161
- if (span > 1) {
162
- if (!spannedCells[row.id]) spannedCells[row.id] = {};
163
- spannedCells[row.id][colDef.field] = span;
110
+ const EMPTY_COLSPAN_MAP: ColspanMap = new Map();
111
+ const EMPTY_ROW_SPANNING_CACHES: RowSpanningCaches = { spannedCells: {}, hiddenCells: {}, hiddenCellOriginMap: {} };
112
+ const identityRowStart = (centerRowIndex: number) => centerRowIndex;
113
+ const noOrigin = () => null;
114
+ const noRanges = () => undefined;
115
+
116
+ const EMPTY_RESULT: GridSpanningResult = {
117
+ hasColSpan: false,
118
+ hasRowSpan: false,
119
+ colspanMap: EMPTY_COLSPAN_MAP,
120
+ rowSpanningCaches: EMPTY_ROW_SPANNING_CACHES,
121
+ getSpanOrigin: noOrigin,
122
+ getUnpinnedColSpanRanges: noRanges,
123
+ getCenterRowSpanStart: identityRowStart,
124
+ };
125
+
126
+ const FIELD_SEPARATOR = '\u0000';
127
+ const SECTION_SEPARATOR = '\u0001';
128
+
129
+ /**
130
+ * Normalises a span value: integers ≥ 1 pass through, fractions are floored, `Infinity` means
131
+ * "to the end of the section" (clamped later), and anything else (NaN, ≤ 0, non-numbers) is 1.
132
+ */
133
+ export function normalizeSpan(value: unknown): number {
134
+ if (typeof value !== 'number' || Number.isNaN(value)) return 1;
135
+ if (value === Infinity) return Number.MAX_SAFE_INTEGER;
136
+ if (!Number.isFinite(value)) return 1;
137
+ const span = Math.floor(value);
138
+ return span < 1 ? 1 : span;
139
+ }
164
140
 
165
- for (let k = 1; k < span; k++) {
166
- const nextIndex = index + k;
167
- if (nextIndex < lastRowIndex && rows[nextIndex]) {
168
- const nextRow = rows[nextIndex];
169
- if (!hiddenCells[nextRow.id]) hiddenCells[nextRow.id] = {};
170
- hiddenCells[nextRow.id][colDef.field] = true;
141
+ function sectionKey<R extends GridRowModel>(columns: GridColDef<R>[]): string {
142
+ return columns.map(c => c.field).join(FIELD_SEPARATOR);
143
+ }
171
144
 
172
- if (!hiddenCellOriginMap[nextIndex]) hiddenCellOriginMap[nextIndex] = {};
173
- hiddenCellOriginMap[nextIndex][colDef.field] = index;
145
+ /**
146
+ * Computes column and row spans for the rendered rows and columns.
147
+ *
148
+ * - Spans are computed over the rendered data columns only (hidden columns are skipped) and stop at a
149
+ * pinned-column section boundary; they are computed per row section (pinned top, center, pinned
150
+ * bottom) and never cross into another section.
151
+ * - An origin with colSpan c and rowSpan r covers the whole c × r rectangle. A cell covered by a
152
+ * colSpan is not evaluated as a rowSpan origin, a colSpan stops before a cell that a row span from
153
+ * above already covers, and a row span ends at a row whose detail panel is expanded.
154
+ * - Span values are normalised (see `normalizeSpan`) and clamped to what exists, so the work is
155
+ * O(rows × columns) at most, and zero when no column declares a span.
156
+ * - A span callback (or the valueGetter feeding it) that throws is treated as span 1, with a
157
+ * development warning, instead of unmounting the grid.
158
+ *
159
+ * The result is derived during render (memoised on rows and column structure), not set in an effect.
160
+ */
161
+ export function useGridSpanning<R extends GridRowModel>(params: UseGridSpanningParams<R>): GridSpanningResult {
162
+ const {
163
+ pinnedTopRows,
164
+ centerRows,
165
+ pinnedBottomRows,
166
+ columns,
167
+ leftPinnedColumns,
168
+ unpinnedColumns,
169
+ rightPinnedColumns,
170
+ expandedRowIds,
171
+ rowMetaMap,
172
+ getRowId = defaultGetRowId,
173
+ } = params;
174
+
175
+ const hasColSpan = useMemo(() => columns.some(c => c.colSpan !== undefined && c.colSpan !== null), [columns]);
176
+ const hasRowSpan = useMemo(() => columns.some(c => c.rowSpan !== undefined && c.rowSpan !== null), [columns]);
177
+ const hasSpans = hasColSpan || hasRowSpan;
178
+
179
+ // Only the order and pinned section of the rendered columns matter; a string key keeps the span
180
+ // computation from re-running on every column-resize tick (the layout arrays are rebuilt then).
181
+ const structureKey = hasSpans
182
+ ? [leftPinnedColumns, unpinnedColumns, rightPinnedColumns].map(sectionKey).join(SECTION_SEPARATOR)
183
+ : '';
184
+
185
+ const spanColumns = useMemo<{ columns: SpanColumn<R>[]; leftCount: number; unpinnedCount: number }>(() => {
186
+ if (!structureKey) return { columns: [], leftCount: 0, unpinnedCount: 0 };
187
+ const byField = new Map(columns.map(c => [c.field, c]));
188
+ const sections = structureKey.split(SECTION_SEPARATOR).map(s => (s ? s.split(FIELD_SEPARATOR) : []));
189
+ const result: SpanColumn<R>[] = [];
190
+ let end = 0;
191
+ for (const fields of sections) {
192
+ end += fields.length;
193
+ for (const field of fields) {
194
+ const colDef = byField.get(field) ?? ({ field } as GridColDef<R>);
195
+ result.push({
196
+ colDef,
197
+ field,
198
+ sectionEnd: end,
199
+ hasColSpan: colDef.colSpan !== undefined && colDef.colSpan !== null,
200
+ hasRowSpan: colDef.rowSpan !== undefined && colDef.rowSpan !== null,
201
+ });
174
202
  }
175
- }
176
203
  }
177
- }
178
- });
204
+ return { columns: result, leftCount: sections[0].length, unpinnedCount: sections[1].length };
205
+ }, [structureKey, columns]);
206
+
207
+ const computation = useMemo<SpanComputation | null>(() => {
208
+ if (!hasSpans || spanColumns.columns.length === 0) return null;
209
+ const out: SpanComputation = {
210
+ colspanMap: new Map(),
211
+ rowSpanningCaches: { spannedCells: {}, hiddenCells: {}, hiddenCellOriginMap: {} },
212
+ hiddenOriginFields: new Map(),
213
+ unpinnedRanges: new Map(),
214
+ centerRowSpanStart: new Map(),
215
+ };
216
+ const warned = new Set<string>();
217
+ const context = { spanColumns, expandedRowIds, rowMetaMap, getRowId, warned, out };
218
+ computeSection(pinnedTopRows, 0, false, context);
219
+ computeSection(centerRows, pinnedTopRows.length, true, context);
220
+ computeSection(pinnedBottomRows, pinnedTopRows.length + centerRows.length, false, context);
221
+ return out;
222
+ }, [hasSpans, spanColumns, pinnedTopRows, centerRows, pinnedBottomRows, expandedRowIds, rowMetaMap, getRowId]);
223
+
224
+ return useMemo<GridSpanningResult>(() => {
225
+ if (!computation) return EMPTY_RESULT;
226
+ const { colspanMap, rowSpanningCaches, hiddenOriginFields, unpinnedRanges, centerRowSpanStart } = computation;
227
+ const renderedColumns = spanColumns.columns;
228
+ return {
229
+ hasColSpan,
230
+ hasRowSpan,
231
+ colspanMap,
232
+ rowSpanningCaches,
233
+ getSpanOrigin: (rowId, field) => {
234
+ const colSpanInfo = colspanMap.get(rowId)?.[field];
235
+ if (colSpanInfo?.spannedByColSpan) return { rowId, field: renderedColumns[colSpanInfo.leftVisibleCellIndex].field };
236
+ const originRowId = rowSpanningCaches.hiddenCellOriginMap[rowId]?.[field];
237
+ if (originRowId === undefined) return null;
238
+ return { rowId: originRowId, field: hiddenOriginFields.get(rowId)?.[field] ?? field };
239
+ },
240
+ getUnpinnedColSpanRanges: (rowId) => unpinnedRanges.get(rowId),
241
+ getCenterRowSpanStart: (centerRowIndex) => centerRowSpanStart.get(centerRowIndex) ?? centerRowIndex,
242
+ };
243
+ }, [computation, hasColSpan, hasRowSpan, spanColumns]);
244
+ }
179
245
 
180
- setRowSpanningState({
181
- caches: { spannedCells, hiddenCells, hiddenCellOriginMap },
182
- processedRange: { firstRowIndex, lastRowIndex }
183
- });
184
- }, [rows, columns]);
185
-
186
- const resetRowSpan = useCallback(() => {
187
- setRowSpanningState({
188
- caches: {
189
- spannedCells: {},
190
- hiddenCells: {},
191
- hiddenCellOriginMap: {}
192
- },
193
- processedRange: { firstRowIndex: 0, lastRowIndex: 0 }
194
- });
195
- }, []);
246
+ interface SectionContext<R extends GridRowModel> {
247
+ spanColumns: { columns: SpanColumn<R>[]; leftCount: number; unpinnedCount: number };
248
+ expandedRowIds?: Set<GridRowId>;
249
+ rowMetaMap?: Map<GridRowId, GridRowMeta>;
250
+ getRowId: (row: R) => GridRowId;
251
+ warned: Set<string>;
252
+ out: SpanComputation;
253
+ }
196
254
 
197
- useEffect(() => {
255
+ function computeSection<R extends GridRowModel>(
256
+ rows: R[],
257
+ rowIndexOffset: number,
258
+ isCenter: boolean,
259
+ context: SectionContext<R>,
260
+ ): void {
261
+ const { spanColumns, expandedRowIds, rowMetaMap, getRowId, warned, out } = context;
262
+ const { columns, leftCount, unpinnedCount } = spanColumns;
263
+ const columnCount = columns.length;
264
+ const { colspanMap, rowSpanningCaches, hiddenOriginFields, unpinnedRanges, centerRowSpanStart } = out;
265
+
266
+ // Per column: the (section-local) row index until which a row span from above covers it, and
267
+ // the origin of that span. `coverEnd` is the furthest such row over all columns.
268
+ const coveredUntil = new Int32Array(columnCount);
269
+ const coverOriginRow: GridRowId[] = new Array(columnCount);
270
+ const coverOriginField: string[] = new Array(columnCount);
271
+ const coverOriginIndex = new Int32Array(columnCount);
272
+ let coverEnd = 0;
273
+
274
+ const spanColumnIndices: number[] = [];
275
+ columns.forEach((column, index) => {
276
+ if (column.hasColSpan || column.hasRowSpan) spanColumnIndices.push(index);
277
+ });
198
278
 
199
- const newMap = new Map<GridRowId, Record<ColumnKey, CellColSpanInfo>>();
279
+ const markHidden = (rowId: GridRowId, r: number, i: number) => {
280
+ const field = columns[i].field;
281
+ setRecord(rowSpanningCaches.hiddenCells, rowId, field, true);
282
+ setRecord(rowSpanningCaches.hiddenCellOriginMap, rowId, field, coverOriginRow[i]);
283
+ if (coverOriginField[i] !== field) setRecord(hiddenOriginFields, rowId, field, coverOriginField[i]);
284
+ if (isCenter) {
285
+ const start = centerRowSpanStart.get(r);
286
+ if (start === undefined || coverOriginIndex[i] < start) centerRowSpanStart.set(r, coverOriginIndex[i]);
287
+ }
288
+ };
200
289
 
201
- rows.forEach((row, index) => {
202
- for (let i = 0; i < columns.length; i++) {
203
- const colDef = columns[i];
204
- if (!colDef) continue;
290
+ /** Evaluates the spans of the cell at column `i` (not covered from above); returns the columns it takes. */
291
+ const processCell = (row: R, r: number, i: number): number => {
292
+ const column = columns[i];
293
+ const rowId = getRowId(row);
294
+ const cellParams = buildParams(row, rowId, column.colDef, rowIndexOffset + r, i, rowMetaMap, warned);
205
295
 
206
296
  let colSpan = 1;
207
- if (colDef.colSpan) {
208
- if (typeof colDef.colSpan === 'function') {
209
- const params: GridRenderCellParams<R> = {
210
- row,
211
- value: row[colDef.field as keyof R],
212
- field: colDef.field,
213
- colDef,
214
- rowIndex: index,
215
- colIndex: i
216
- };
217
- colSpan = colDef.colSpan(params);
218
- } else {
219
- colSpan = colDef.colSpan;
220
- }
297
+ if (column.hasColSpan) {
298
+ const limit = Math.min(evaluateSpan(column.colDef.colSpan, cellParams, 'colSpan', column.field, warned), column.sectionEnd - i);
299
+ while (colSpan < limit && coveredUntil[i + colSpan] <= r) colSpan++;
221
300
  }
222
301
 
223
- if (colSpan <= 1) {
224
- const colWidth = columnWidths[colDef.field] || (typeof colDef.width === 'number' ? colDef.width : 100);
225
- setCellColSpanInfo(newMap, row.id, colDef.field, {
226
- spannedByColSpan: false,
227
- cellProps: { colSpan: 1, width: colWidth }
228
- });
229
- continue;
302
+ let rowSpan = 1;
303
+ if (column.hasRowSpan) {
304
+ rowSpan = Math.min(evaluateSpan(column.colDef.rowSpan, cellParams, 'rowSpan', column.field, warned), rows.length - r);
305
+ // A span ends before the next synthetic row (group header, subtotal, auto-created
306
+ // parent, pivot Grand Total): it never merges data across a group boundary.
307
+ for (let k = 1; k < rowSpan; k++) {
308
+ if (isSyntheticRowId(getRowId(rows[r + k]), rowMetaMap)) {
309
+ rowSpan = k;
310
+ break;
311
+ }
312
+ }
313
+ if (rowSpan > 1 && expandedRowIds && expandedRowIds.size > 0) {
314
+ for (let k = 0; k < rowSpan - 1; k++) {
315
+ if (expandedRowIds.has(getRowId(rows[r + k]))) {
316
+ rowSpan = k + 1;
317
+ break;
318
+ }
319
+ }
320
+ }
230
321
  }
231
322
 
232
- let width = columnWidths[colDef.field] || (typeof colDef.width === 'number' ? colDef.width : 100);
233
- for (let j = 1; j < colSpan; j++) {
234
- if (i + j < columns.length) {
235
- const nextCol = columns[i + j];
236
- if (nextCol) {
237
- const nextColWidth = columnWidths[nextCol.field] || (typeof nextCol.width === 'number' ? nextCol.width : 100);
238
- width += nextColWidth;
239
-
240
- setCellColSpanInfo(newMap, row.id, nextCol.field, {
241
- spannedByColSpan: true,
242
- leftVisibleCellIndex: i,
243
- rightVisibleCellIndex: Math.min(i + colSpan - 1, columns.length - 1)
244
- });
323
+ if (colSpan > 1) {
324
+ setRecord(colspanMap, rowId, column.field, { spannedByColSpan: false, cellProps: { colSpan } });
325
+ for (let j = 1; j < colSpan; j++) {
326
+ const covered = columns[i + j];
327
+ setRecord(colspanMap, rowId, covered.field, {
328
+ spannedByColSpan: true,
329
+ leftVisibleCellIndex: i,
330
+ rightVisibleCellIndex: i + colSpan - 1,
331
+ });
332
+ }
333
+ if (i >= leftCount && i < leftCount + unpinnedCount) {
334
+ let ranges = unpinnedRanges.get(rowId);
335
+ if (!ranges) {
336
+ ranges = [];
337
+ unpinnedRanges.set(rowId, ranges);
338
+ }
339
+ ranges.push(i - leftCount, i + colSpan - 1 - leftCount);
245
340
  }
246
- }
247
341
  }
248
342
 
249
- setCellColSpanInfo(newMap, row.id, colDef.field, {
250
- spannedByColSpan: false,
251
- cellProps: { colSpan, width }
252
- });
253
-
254
- i += colSpan - 1;
255
- }
256
- });
257
-
258
- setColspanMap(newMap);
259
-
260
- const spannedCells: Record<GridRowId, Record<ColumnKey, number>> = {};
261
- const hiddenCells: Record<GridRowId, Record<ColumnKey, boolean>> = {};
262
- const hiddenCellOriginMap: Record<number, Record<ColumnKey, number>> = {};
263
-
264
- columns.forEach((colDef, columnIndex) => {
265
- if (!colDef.rowSpan) return;
266
-
267
- for (let index = 0; index < rows.length; index++) {
268
- const row = rows[index];
269
- if (!row) continue;
270
-
271
- if (hiddenCells[row.id]?.[colDef.field]) continue;
272
-
273
- const cellValue = colDef.valueGetter
274
- ? colDef.valueGetter({ row, field: colDef.field, value: row[colDef.field as keyof R] })
275
- : row[colDef.field as keyof R];
276
-
277
- let span = 1;
278
- if (typeof colDef.rowSpan === 'function') {
279
- const params: GridRenderCellParams<R> = {
280
- row,
281
- value: cellValue,
282
- field: colDef.field,
283
- colDef,
284
- rowIndex: index,
285
- colIndex: columnIndex
286
- };
287
- span = colDef.rowSpan(params);
288
- } else if (typeof colDef.rowSpan === 'number') {
289
- span = colDef.rowSpan;
343
+ if (rowSpan > 1) {
344
+ setRecord(rowSpanningCaches.spannedCells, rowId, column.field, rowSpan);
345
+ for (let j = i; j < i + colSpan; j++) {
346
+ coveredUntil[j] = r + rowSpan;
347
+ coverOriginRow[j] = rowId;
348
+ coverOriginField[j] = column.field;
349
+ coverOriginIndex[j] = r;
350
+ }
351
+ if (r + rowSpan > coverEnd) coverEnd = r + rowSpan;
290
352
  }
291
353
 
292
- if (span > 1) {
293
- if (!spannedCells[row.id]) spannedCells[row.id] = {};
294
- spannedCells[row.id][colDef.field] = span;
354
+ return colSpan;
355
+ };
295
356
 
296
- for (let k = 1; k < span; k++) {
297
- const nextIndex = index + k;
298
- if (nextIndex < rows.length && rows[nextIndex]) {
299
- const nextRow = rows[nextIndex];
300
- if (!hiddenCells[nextRow.id]) hiddenCells[nextRow.id] = {};
301
- hiddenCells[nextRow.id][colDef.field] = true;
357
+ for (let r = 0; r < rows.length; r++) {
358
+ const row = rows[r];
359
+ if (!row || row._isSkeleton) continue;
360
+ // Synthetic rows hold no data of their own: they are never a span origin (spans from above
361
+ // already end before them).
362
+ if (isSyntheticRowId(getRowId(row), rowMetaMap)) continue;
363
+
364
+ if (coverEnd <= r) {
365
+ // Nothing is covered from above: only the columns that declare a span need a look.
366
+ let next = 0;
367
+ for (const i of spanColumnIndices) {
368
+ if (i >= next) next = i + processCell(row, r, i);
369
+ }
370
+ continue;
371
+ }
302
372
 
303
- if (!hiddenCellOriginMap[nextIndex]) hiddenCellOriginMap[nextIndex] = {};
304
- hiddenCellOriginMap[nextIndex][colDef.field] = index;
373
+ let i = 0;
374
+ while (i < columnCount) {
375
+ if (coveredUntil[i] > r) {
376
+ markHidden(getRowId(row), r, i);
377
+ i++;
378
+ } else if (columns[i].hasColSpan || columns[i].hasRowSpan) {
379
+ i += processCell(row, r, i);
380
+ } else {
381
+ i++;
305
382
  }
306
- }
307
383
  }
308
- }
309
- });
384
+ }
385
+ }
310
386
 
311
- setRowSpanningState({
312
- caches: { spannedCells, hiddenCells, hiddenCellOriginMap },
313
- processedRange: { firstRowIndex: 0, lastRowIndex: rows.length }
314
- });
315
- }, [rows, columns, columnWidths]);
316
-
317
- const getSpannedCells = useCallback(() => rowSpanningState.caches.spannedCells, [rowSpanningState]);
318
- const getHiddenCells = useCallback(() => rowSpanningState.caches.hiddenCells, [rowSpanningState]);
319
- const getHiddenCellOriginMap = useCallback(() => rowSpanningState.caches.hiddenCellOriginMap, [rowSpanningState]);
320
-
321
- return {
322
-
323
- colspanMap,
324
- resetColSpan,
325
- getCellColSpanInfo,
326
- calculateColSpan,
327
-
328
- rowSpanningState,
329
- calculateRowSpan,
330
- resetRowSpan,
331
- getSpannedCells,
332
- getHiddenCells,
333
- getHiddenCellOriginMap
334
- };
387
+ function buildParams<R extends GridRowModel>(
388
+ row: R,
389
+ rowId: GridRowId,
390
+ colDef: GridColDef<R>,
391
+ rowIndex: number,
392
+ colIndex: number,
393
+ rowMetaMap: Map<GridRowId, GridRowMeta> | undefined,
394
+ warned: Set<string>,
395
+ ): GridRenderCellParams<R> {
396
+ const raw = row[colDef.field as keyof R];
397
+ let value: unknown = raw;
398
+ if (colDef.valueGetter) {
399
+ try {
400
+ value = colDef.valueGetter({ row, field: colDef.field, value: raw });
401
+ } catch (error) {
402
+ warnOnce(warned, `valueGetter:${colDef.field}`, `valueGetter for column "${colDef.field}" threw while computing its span`, error);
403
+ value = undefined;
404
+ }
405
+ }
406
+ return { row, value, field: colDef.field, colDef, rowIndex, colIndex, rowMeta: rowMetaMap?.get(rowId) };
407
+ }
408
+
409
+ function evaluateSpan<R extends GridRowModel>(
410
+ spec: GridColDef<R>['colSpan'],
411
+ cellParams: GridRenderCellParams<R>,
412
+ kind: 'colSpan' | 'rowSpan',
413
+ field: string,
414
+ warned: Set<string>,
415
+ ): number {
416
+ if (typeof spec === 'number') return normalizeSpan(spec);
417
+ if (typeof spec !== 'function') return 1;
418
+ try {
419
+ return normalizeSpan(spec(cellParams));
420
+ } catch (error) {
421
+ warnOnce(warned, `${kind}:${field}`, `${kind} for column "${field}" threw; the cell is rendered without a span`, error);
422
+ return 1;
423
+ }
335
424
  }
336
425
 
337
- function setCellColSpanInfo(
338
- map: ColspanMap,
339
- rowId: GridRowId,
340
- field: string,
341
- info: CellColSpanInfo
342
- ) {
343
- let columnInfo = map.get(rowId);
344
- if (!columnInfo) {
345
- columnInfo = {};
346
- map.set(rowId, columnInfo);
347
- }
348
- columnInfo[field] = info;
426
+ function warnOnce(warned: Set<string>, key: string, message: string, error: unknown): void {
427
+ if (process.env.NODE_ENV === 'production' || warned.has(key)) return;
428
+ warned.add(key);
429
+ console.warn(`[OpenGridX] ${message}.`, error);
430
+ }
431
+
432
+ function setRecord<V>(
433
+ target: Map<GridRowId, Record<ColumnKey, V>> | Record<GridRowId, Record<ColumnKey, V>>,
434
+ rowId: GridRowId,
435
+ field: string,
436
+ value: V,
437
+ ): void {
438
+ if (target instanceof Map) {
439
+ let record = target.get(rowId);
440
+ if (!record) {
441
+ record = {};
442
+ target.set(rowId, record);
443
+ }
444
+ record[field] = value;
445
+ return;
446
+ }
447
+ let record = target[rowId];
448
+ if (!record) {
449
+ record = {};
450
+ target[rowId] = record;
451
+ }
452
+ record[field] = value;
349
453
  }