@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,8 +1,10 @@
1
- import { useMemo, useState, useCallback } from 'react';
2
- import { GridRowModel, GridRowId, GridTreeNode, GridFilterModel, GridSortItem } from '../types';
3
- import type { GridRowMeta } from '../types';
4
- import { isRowMatchingFilter } from '../utils/filtering';
5
- import { compareValues } from '../utils/sorting';
1
+ import { useMemo, useState, useCallback, useEffect } from 'react';
2
+ import type { GridRowModel, GridRowId, GridTreeNode, GridFilterModel, GridSortItem, GridRowMeta } from '../types';
3
+ import { createRowFilter } from '../utils/filtering';
4
+ import { sortItemsBySortModel, type GridSortValue } from '../utils/sorting';
5
+ import { getCellValue } from '../utils/values';
6
+ import type { GridColumnLookup } from '../utils/columnLookup';
7
+ import type { GridHierarchyVisibleRowsOptions } from './useRowGrouping';
6
8
 
7
9
  const EMPTY_OVERRIDES: Map<GridRowId, boolean> = new Map();
8
10
 
@@ -14,155 +16,259 @@ interface ExpansionOverrides {
14
16
  interface UseTreeDataProps<R extends GridRowModel> {
15
17
  rows: R[];
16
18
  getRowId: (row: R) => GridRowId;
19
+ /**
20
+ * Returns the hierarchy path of a row. Keep it stable (module scope or useCallback): a new
21
+ * function identity rebuilds the whole tree, because the grid cannot tell whether it returns
22
+ * the same paths without calling it for every row.
23
+ */
17
24
  getTreeDataPath?: (row: R) => string[];
18
25
  treeData?: boolean;
19
26
  defaultGroupingExpansionDepth?: number;
20
27
  filterModel?: GridFilterModel;
21
28
  sortModel?: GridSortItem[];
22
29
  onRowExpansionChange?: (node: GridTreeNode) => void;
30
+ /** Column definitions, so filter and sort read cells through valueGetter and use the column type. */
31
+ columnLookup?: GridColumnLookup;
32
+ /** 'server': the rows arrive filtered, so the tree is not filtered again on the client. */
33
+ filterMode?: 'client' | 'server';
34
+ /** 'server': the rows arrive sorted, so siblings keep the order the server sent them in. */
35
+ sortingMode?: 'client' | 'server';
23
36
  }
24
37
 
25
- export function useTreeData<R extends GridRowModel>(props: UseTreeDataProps<R>) {
26
- const {
27
- rows,
28
- getRowId,
29
- getTreeDataPath,
30
- treeData = false,
31
- defaultGroupingExpansionDepth = 0,
32
- filterModel,
33
- sortModel,
34
- onRowExpansionChange
35
- } = props;
38
+ /**
39
+ * The lookup key of a tree path. JSON keeps segments apart, so a segment containing '/' (a file
40
+ * name, a URL) never collides with a deeper path: ['a/b'] and ['a', 'b'] are different nodes.
41
+ */
42
+ const pathKeyOf = (path: readonly string[]): string => JSON.stringify(path);
43
+
44
+ /** The id of the parent row the grid creates for a path segment that has no row of its own. */
45
+ const autoParentIdOf = (pathKey: string): string => `auto-group-${pathKey}`;
46
+
47
+ const MAX_IDS_IN_WARNING = 5;
48
+ const formatIds = (ids: readonly GridRowId[]): string =>
49
+ ids.slice(0, MAX_IDS_IN_WARNING).map(id => JSON.stringify(id)).join(', ') + (ids.length > MAX_IDS_IN_WARNING ? ', …' : '');
50
+
51
+ interface TreeBuild {
52
+ treeNodes: Map<GridRowId, GridTreeNode>;
53
+ rootIds: GridRowId[];
54
+ /** The rows created for path segments that have no row of their own (`{ id }` only). */
55
+ groupingRows: GridRowModel[];
56
+ syntheticIds: Set<GridRowId>;
57
+ /** Every row in the tree (consumer rows and auto-created parents), by id. */
58
+ rowLookup: Map<GridRowId, GridRowModel>;
59
+ /** Development warnings about the paths (empty paths, duplicate paths). */
60
+ issues: string[];
61
+ }
36
62
 
37
- const { treeNodes, rootIds, groupingRows } = useMemo(() => {
38
- if (!treeData || !getTreeDataPath) {
39
- return {
40
- treeNodes: new Map<GridRowId, GridTreeNode>(),
41
- rootIds: [],
42
- groupingRows: []
43
- };
63
+ const EMPTY_BUILD: TreeBuild = {
64
+ treeNodes: new Map(),
65
+ rootIds: [],
66
+ groupingRows: [],
67
+ syntheticIds: new Set(),
68
+ rowLookup: new Map(),
69
+ issues: [],
70
+ };
71
+
72
+ /**
73
+ * Builds the tree in one linear pass over the rows (each child is attached exactly once, so no
74
+ * membership scans). Rows keep their order among siblings; an auto-created parent takes the position
75
+ * of the first row under it.
76
+ */
77
+ function buildTree<R extends GridRowModel>(
78
+ rows: readonly R[],
79
+ getRowId: (row: R) => GridRowId,
80
+ getTreeDataPath: (row: R) => string[],
81
+ ): TreeBuild {
82
+ const treeNodes = new Map<GridRowId, GridTreeNode>();
83
+ const rootIds: GridRowId[] = [];
84
+ const groupingRows: GridRowModel[] = [];
85
+ const syntheticIds = new Set<GridRowId>();
86
+ const rowLookup = new Map<GridRowId, GridRowModel>();
87
+ const pathLookup = new Map<string, GridRowId>();
88
+ const issues: string[] = [];
89
+
90
+ const ids: GridRowId[] = new Array(rows.length);
91
+ const paths: string[][] = new Array(rows.length);
92
+ const emptyPathIds: GridRowId[] = [];
93
+ const duplicatePaths = new Map<string, GridRowId[]>();
94
+
95
+ // Pass 1: register every row and its path, so a parent row listed after its children is found.
96
+ rows.forEach((row, index) => {
97
+ const id = getRowId(row);
98
+ const rawPath: unknown = getTreeDataPath(row);
99
+ const path = Array.isArray(rawPath) ? rawPath.map(segment => String(segment)) : [];
100
+ ids[index] = id;
101
+ paths[index] = path;
102
+ rowLookup.set(id, row);
103
+
104
+ if (path.length === 0) {
105
+ emptyPathIds.push(id);
106
+ } else {
107
+ const key = pathKeyOf(path);
108
+ const owner = pathLookup.get(key);
109
+ if (owner === undefined) {
110
+ pathLookup.set(key, id);
111
+ } else {
112
+ // Children attach to the first row with this path; the others render as childless siblings.
113
+ const clash = duplicatePaths.get(key) ?? [owner];
114
+ clash.push(id);
115
+ duplicatePaths.set(key, clash);
116
+ }
44
117
  }
45
118
 
46
- const nodes = new Map<GridRowId, GridTreeNode>();
47
- const roots: GridRowId[] = [];
48
- const groupRows: GridRowModel[] = [];
49
- const pathLookup = new Map<string, GridRowId>();
50
-
51
- const getPathKey = (path: string[]) => path.join('/');
52
-
53
- rows.forEach(row => {
54
- const id = getRowId(row);
55
- const path = getTreeDataPath(row);
56
- const pathKey = getPathKey(path);
57
-
58
- pathLookup.set(pathKey, id);
59
-
60
- nodes.set(id, {
61
- id,
62
- parentId: null,
63
- depth: path.length - 1,
64
- groupingKey: path[path.length - 1],
65
- isExpanded: false,
66
- children: [],
67
- serverChildrenCount: row.serverChildrenCount
68
- });
119
+ treeNodes.set(id, {
120
+ id,
121
+ parentId: null,
122
+ depth: Math.max(path.length - 1, 0),
123
+ groupingKey: path[path.length - 1] ?? '',
124
+ isExpanded: false,
125
+ children: [],
126
+ serverChildrenCount: row.serverChildrenCount,
69
127
  });
128
+ });
129
+
130
+ // The node for a path (length >= 1), creating auto parents for segments that have no row.
131
+ const ensureNode = (path: string[]): GridRowId => {
132
+ const key = pathKeyOf(path);
133
+ const existing = pathLookup.get(key);
134
+ if (existing !== undefined) return existing;
135
+
136
+ const id = autoParentIdOf(key);
137
+ const label = path[path.length - 1];
138
+ const node: GridTreeNode = {
139
+ id,
140
+ parentId: null,
141
+ depth: path.length - 1,
142
+ groupingKey: label,
143
+ isExpanded: false,
144
+ children: [],
145
+ label,
146
+ };
147
+ treeNodes.set(id, node);
148
+ pathLookup.set(key, id);
149
+ syntheticIds.add(id);
150
+ const groupRow: GridRowModel = { id };
151
+ groupingRows.push(groupRow);
152
+ rowLookup.set(id, groupRow);
153
+
154
+ if (path.length === 1) {
155
+ rootIds.push(id);
156
+ } else {
157
+ const parentId = ensureNode(path.slice(0, -1));
158
+ node.parentId = parentId;
159
+ treeNodes.get(parentId)?.children?.push(id);
160
+ }
161
+ return id;
162
+ };
163
+
164
+ // Pass 2: link every row to its parent, in row order.
165
+ rows.forEach((_, index) => {
166
+ const id = ids[index];
167
+ const path = paths[index];
168
+ if (path.length <= 1) {
169
+ rootIds.push(id);
170
+ return;
171
+ }
172
+ const parentId = ensureNode(path.slice(0, -1));
173
+ const node = treeNodes.get(id);
174
+ if (node) node.parentId = parentId;
175
+ treeNodes.get(parentId)?.children?.push(id);
176
+ });
177
+
178
+ if (emptyPathIds.length > 0) {
179
+ issues.push(`[OpenGridX] treeData: getTreeDataPath returned an empty path for row(s) ${formatIds(emptyPathIds)}. They are shown as top-level rows.`);
180
+ }
181
+ duplicatePaths.forEach((clash, key) => {
182
+ issues.push(`[OpenGridX] treeData: rows ${formatIds(clash)} have the same path ${key}. Rows under that path are attached to row ${JSON.stringify(clash[0])}.`);
183
+ });
184
+
185
+ return { treeNodes, rootIds, groupingRows, syntheticIds, rowLookup, issues };
186
+ }
70
187
 
71
- rows.forEach(row => {
72
- const id = getRowId(row);
73
- const path = getTreeDataPath(row);
188
+ interface TreeVisibility {
189
+ /** Nodes shown under the filter: rows that match, and the ancestors of rows that match. */
190
+ visible: Set<GridRowId>;
191
+ /** Each node's children that are visible. */
192
+ visibleChildren: Map<GridRowId, GridRowId[]>;
193
+ /** Visible consumer rows below each node, at any depth (auto-created parents are not counted). */
194
+ descendantCounts: Map<GridRowId, number>;
195
+ }
74
196
 
75
- if (path.length === 1) {
76
- roots.push(id);
77
- return;
197
+ function computeVisibility(build: TreeBuild, rowFilter: ((row: GridRowModel) => boolean) | null): TreeVisibility {
198
+ const { treeNodes, rootIds, syntheticIds, rowLookup } = build;
199
+ const visible = new Set<GridRowId>();
200
+ const visibleChildren = new Map<GridRowId, GridRowId[]>();
201
+ const descendantCounts = new Map<GridRowId, number>();
202
+
203
+ const visit = (id: GridRowId): boolean => {
204
+ const node = treeNodes.get(id);
205
+ if (!node) return false;
206
+ const shownChildren: GridRowId[] = [];
207
+ let count = 0;
208
+ for (const childId of node.children ?? []) {
209
+ if (visit(childId)) {
210
+ shownChildren.push(childId);
211
+ count += (syntheticIds.has(childId) ? 0 : 1) + (descendantCounts.get(childId) ?? 0);
78
212
  }
213
+ }
214
+ visibleChildren.set(id, shownChildren);
215
+ descendantCounts.set(id, count);
216
+
217
+ // An auto-created parent has no data of its own: it is shown only for the rows under it.
218
+ const row = rowLookup.get(id);
219
+ const selfMatch = !syntheticIds.has(id) && row !== undefined && (rowFilter ? rowFilter(row) : true);
220
+ const isVisible = selfMatch || shownChildren.length > 0;
221
+ if (isVisible) visible.add(id);
222
+ return isVisible;
223
+ };
224
+
225
+ rootIds.forEach(visit);
226
+ return { visible, visibleChildren, descendantCounts };
227
+ }
79
228
 
80
- const ensureAncestors = (childPath: string[]) => {
81
- if (childPath.length <= 1) return;
82
-
83
- const parentPath = childPath.slice(0, -1);
84
- const parentPathKey = getPathKey(parentPath);
85
- let parentId = pathLookup.get(parentPathKey);
86
-
87
- if (parentId === undefined) {
88
- parentId = `auto-group-${parentPathKey}`;
89
-
90
- if (!nodes.has(parentId)) {
91
- const groupLabel = parentPath[parentPath.length - 1];
92
- nodes.set(parentId, {
93
- id: parentId,
94
- parentId: null,
95
- depth: parentPath.length - 1,
96
- groupingKey: groupLabel,
97
- isExpanded: false,
98
- children: [],
99
- label: groupLabel
100
- });
101
- pathLookup.set(parentPathKey, parentId);
102
-
103
- const groupRow = {
104
- id: parentId,
105
- name: groupLabel,
106
- _isGroupRow: true
107
- } as unknown as GridRowModel;
108
- groupRows.push(groupRow);
109
-
110
- if (parentPath.length === 1) {
111
- roots.push(parentId);
112
- }
113
-
114
- ensureAncestors(parentPath);
115
- }
116
- }
229
+ export function useTreeData<R extends GridRowModel>(props: UseTreeDataProps<R>) {
230
+ const {
231
+ rows,
232
+ getRowId,
233
+ getTreeDataPath,
234
+ treeData = false,
235
+ defaultGroupingExpansionDepth = 0,
236
+ filterModel,
237
+ sortModel,
238
+ onRowExpansionChange,
239
+ columnLookup,
240
+ filterMode = 'client',
241
+ sortingMode = 'client',
242
+ } = props;
117
243
 
118
- return parentId;
119
- };
244
+ const isActive = treeData && Boolean(getTreeDataPath);
120
245
 
121
- const parentId = ensureAncestors(path);
246
+ const build = useMemo<TreeBuild>(() => {
247
+ if (!treeData || !getTreeDataPath) return EMPTY_BUILD;
248
+ return buildTree(rows, getRowId, getTreeDataPath);
249
+ }, [rows, getTreeDataPath, treeData, getRowId]);
122
250
 
123
- if (parentId !== undefined) {
124
- const parentNode = nodes.get(parentId);
125
- const childNode = nodes.get(id);
251
+ const { treeNodes, rootIds, groupingRows, syntheticIds, rowLookup } = build;
126
252
 
127
- if (parentNode && childNode) {
128
- childNode.parentId = parentId;
129
- parentNode.children = parentNode.children || [];
130
- if (!parentNode.children.includes(id)) {
131
- parentNode.children.push(id);
132
- }
133
- }
134
- }
135
- });
253
+ // Development warnings, logged once per distinct problem rather than on every render.
254
+ const issuesKey = build.issues.join('\n');
255
+ useEffect(() => {
256
+ if (process.env.NODE_ENV === 'production' || !issuesKey) return;
257
+ issuesKey.split('\n').forEach(message => console.warn(message));
258
+ }, [issuesKey]);
136
259
 
137
- groupRows.forEach(groupRow => {
138
- const groupId = groupRow.id;
139
- const node = nodes.get(groupId);
140
- if (!node) return;
141
-
142
- const nodePathKey = (groupId as string).replace('auto-group-', '');
143
- const nodePath = nodePathKey.split('/');
144
-
145
- if (nodePath.length > 1) {
146
- const parentPath = nodePath.slice(0, -1);
147
- const parentPathKey = getPathKey(parentPath);
148
- const parentId = pathLookup.get(parentPathKey);
149
-
150
- if (parentId && nodes.has(parentId)) {
151
- node.parentId = parentId;
152
- const parentNode = nodes.get(parentId);
153
- if (parentNode) {
154
- parentNode.children = parentNode.children || [];
155
- if (!parentNode.children.includes(groupId)) {
156
- parentNode.children.push(groupId);
157
- }
158
- }
159
- }
160
- }
161
- });
260
+ const isMissingPathGetter = treeData && !getTreeDataPath;
261
+ useEffect(() => {
262
+ if (process.env.NODE_ENV === 'production' || !isMissingPathGetter) return;
263
+ console.warn('[OpenGridX] treeData is set without getTreeDataPath, so the rows are shown as a flat list. Pass getTreeDataPath={(row) => row.path} to build the tree.');
264
+ }, [isMissingPathGetter]);
162
265
 
163
- return { treeNodes: nodes, rootIds: roots, groupingRows: groupRows };
266
+ const rowFilter = useMemo(
267
+ () => (filterMode === 'server' ? null : createRowFilter(filterModel, columnLookup)),
268
+ [filterMode, filterModel, columnLookup]
269
+ );
164
270
 
165
- }, [rows, getTreeDataPath, treeData, getRowId]);
271
+ const visibility = useMemo(() => computeVisibility(build, rowFilter), [build, rowFilter]);
166
272
 
167
273
  // Same model as useRowGrouping: depth-based default plus user overrides keyed by config.
168
274
  // Rebuilding the tree (row edits, lazily loaded children) keeps the user's choices.
@@ -173,21 +279,23 @@ export function useTreeData<R extends GridRowModel>(props: UseTreeDataProps<R>)
173
279
  }));
174
280
  const overrides = expansionState.configKey === configKey ? expansionState.overrides : EMPTY_OVERRIDES;
175
281
 
282
+ // A lazy server node (serverChildrenCount > 0, children not loaded yet) is expandable too, so
283
+ // the default depth expands it and useServerTreeChildren loads its children.
176
284
  const isDefaultExpanded = useCallback((node: GridTreeNode | undefined) => (
177
- Boolean(node?.children && node.children.length > 0) &&
285
+ ((node?.children?.length ?? 0) > 0 || (node?.serverChildrenCount ?? 0) > 0) &&
178
286
  (defaultGroupingExpansionDepth === -1 || (node?.depth ?? 0) < defaultGroupingExpansionDepth)
179
287
  ), [defaultGroupingExpansionDepth]);
180
288
 
181
289
  const expandedGroupIds = useMemo<Set<GridRowId>>(() => {
182
290
  const ids = new Set<GridRowId>();
183
- if (!treeData) return ids;
291
+ if (!isActive) return ids;
184
292
  treeNodes.forEach((node, id) => {
185
293
  // A lazily loaded node can be expanded before it has any children, so an
186
294
  // explicit override counts even when children is still empty.
187
295
  if (overrides.get(id) ?? isDefaultExpanded(node)) ids.add(id);
188
296
  });
189
297
  return ids;
190
- }, [treeData, treeNodes, overrides, isDefaultExpanded]);
298
+ }, [isActive, treeNodes, overrides, isDefaultExpanded]);
191
299
 
192
300
  const toggleExpansion = useCallback((id: GridRowId) => {
193
301
  const isExpanding = !expandedGroupIds.has(id);
@@ -204,97 +312,53 @@ export function useTreeData<R extends GridRowModel>(props: UseTreeDataProps<R>)
204
312
  }
205
313
  }, [expandedGroupIds, configKey, treeNodes, onRowExpansionChange]);
206
314
 
207
- const getVisibleRows = useCallback(() => {
208
-
209
- if (!treeData) return null;
210
-
211
- const rowLookup = new Map<GridRowId, GridRowModel>();
212
- rows.forEach(r => rowLookup.set(getRowId(r), r));
213
- groupingRows.forEach(r => rowLookup.set(r.id, r));
214
-
215
- const matchesFilter = (row: GridRowModel) => (filterModel ? isRowMatchingFilter(row, filterModel) : true);
216
-
217
- const filterCache = new Map<GridRowId, boolean>();
218
-
219
- const isNodeVisible = (id: GridRowId): boolean => {
220
- if (filterCache.has(id)) return filterCache.get(id)!;
221
-
222
- const row = rowLookup.get(id);
223
- if (!row) return false;
224
-
225
- const selfMatch = matchesFilter(row);
226
- if (selfMatch) {
227
- filterCache.set(id, true);
228
- return true;
229
- }
315
+ const isClientSort = sortingMode !== 'server' && Boolean(sortModel && sortModel.length > 0);
230
316
 
231
- const node = treeNodes.get(id);
232
- if (node && node.children) {
233
- const childMatch = node.children.some(childId => isNodeVisible(childId));
234
- if (childMatch) {
235
- filterCache.set(id, true);
236
- return true;
237
- }
238
- }
317
+ /**
318
+ * The rows to render, in order. `labelField` is the column that shows the hierarchy (toggle,
319
+ * indent and the labels of auto-created parents): sorting by it orders auto-created parents by
320
+ * their label, since they have no cell values of their own.
321
+ */
322
+ const getVisibleRows = useCallback((options?: GridHierarchyVisibleRowsOptions): GridRowModel[] | null => {
323
+ if (!isActive) return null;
324
+ const labelField = options?.labelField;
325
+ const expandAll = options?.expandAll ?? false;
239
326
 
240
- filterCache.set(id, false);
241
- return false;
242
- };
327
+ const { visible, visibleChildren } = visibility;
328
+ const activeSortModel = isClientSort && sortModel ? sortModel : [];
243
329
 
244
- const sortNodes = (aId: GridRowId, bId: GridRowId) => {
245
- if (!sortModel || sortModel.length === 0) return 0;
246
- const rowA = rowLookup.get(aId);
247
- const rowB = rowLookup.get(bId);
248
- if (!rowA || !rowB) return 0;
249
-
250
- for (const sortItem of sortModel) {
251
- const valA = rowA[sortItem.field];
252
- const valB = rowB[sortItem.field];
253
- const comp = compareValues(valA, valB, sortItem.sort);
254
- if (comp !== 0) return comp;
330
+ const readSortValue = (id: GridRowId, sortItem: GridSortItem): GridSortValue => {
331
+ if (syntheticIds.has(id)) {
332
+ return { value: sortItem.field === labelField ? treeNodes.get(id)?.label : undefined };
255
333
  }
256
- return 0;
334
+ const row = rowLookup.get(id);
335
+ const colDef = columnLookup?.byField.get(sortItem.field);
336
+ return { value: row ? getCellValue(row, sortItem.field, colDef) : undefined, type: colDef?.type };
257
337
  };
258
338
 
259
339
  const result: GridRowModel[] = [];
260
-
261
340
  const seenIds = new Set<GridRowId>();
262
341
 
263
- const traverse = (ids: GridRowId[]) => {
264
-
265
- const visibleIds = ids.filter(id => isNodeVisible(id));
266
-
267
- visibleIds.sort(sortNodes);
268
-
269
- visibleIds.forEach(id => {
270
- if (seenIds.has(id)) return;
271
-
342
+ const traverse = (ids: readonly GridRowId[]) => {
343
+ const shown = ids.filter(id => visible.has(id));
344
+ const ordered = activeSortModel.length > 0 ? sortItemsBySortModel(shown, activeSortModel, readSortValue) : shown;
345
+ for (const id of ordered) {
346
+ if (seenIds.has(id)) continue;
272
347
  const row = rowLookup.get(id);
273
- const node = treeNodes.get(id);
274
-
275
- if (row && node) {
276
- seenIds.add(id);
277
-
278
- const enhancedRow = {
279
- ...row,
280
- _treeDepth: node.depth,
281
- _isExpanded: expandedGroupIds.has(id),
282
- _hasChildren: Boolean((node.children && node.children.length > 0) || (node.serverChildrenCount && node.serverChildrenCount > 0)),
283
- _descendantCount: node.children ? node.children.length : 0
284
- };
285
- result.push(enhancedRow);
286
-
287
- if (expandedGroupIds.has(id) && node.children && node.children.length > 0) {
288
- traverse(node.children);
289
- }
348
+ if (!row) continue;
349
+ seenIds.add(id);
350
+ // Hierarchy info travels in rowMetaMap; the consumer's row object is passed through unchanged.
351
+ result.push(row);
352
+ const children = visibleChildren.get(id);
353
+ if (children && children.length > 0 && (expandAll || expandedGroupIds.has(id))) {
354
+ traverse(children);
290
355
  }
291
- });
356
+ }
292
357
  };
293
358
 
294
359
  traverse(rootIds);
295
360
  return result;
296
-
297
- }, [treeData, rows, groupingRows, rootIds, treeNodes, expandedGroupIds, filterModel, sortModel, getRowId]);
361
+ }, [isActive, visibility, isClientSort, sortModel, syntheticIds, treeNodes, rowLookup, columnLookup, rootIds, expandedGroupIds]);
298
362
 
299
363
  const isGroupExpanded = useCallback((id: GridRowId) => expandedGroupIds.has(id), [expandedGroupIds]);
300
364
  const getNode = useCallback((id: GridRowId) => treeNodes.get(id), [treeNodes]);
@@ -315,21 +379,24 @@ export function useTreeData<R extends GridRowModel>(props: UseTreeDataProps<R>)
315
379
 
316
380
  const rowMetaMap = useMemo<Map<GridRowId, GridRowMeta>>(() => {
317
381
  const map = new Map<GridRowId, GridRowMeta>();
318
- const groupRowIds = new Set(groupingRows.map(r => r.id));
382
+ const { visibleChildren, descendantCounts } = visibility;
319
383
  treeNodes.forEach((node, id) => {
320
- map.set(id, {
321
- hasChildren: Boolean(
322
- (node.children && node.children.length > 0) ||
323
- (node.serverChildrenCount && node.serverChildrenCount > 0)
324
- ),
384
+ // A lazy node whose children are not loaded yet still has children on the server.
385
+ const isUnloadedLazyNode = (node.children?.length ?? 0) === 0 && (node.serverChildrenCount ?? 0) > 0;
386
+ const isSynthetic = syntheticIds.has(id);
387
+ const meta: GridRowMeta = {
388
+ // Based on the children the filter leaves visible, so no toggle expands to nothing.
389
+ hasChildren: (visibleChildren.get(id)?.length ?? 0) > 0 || isUnloadedLazyNode,
325
390
  treeDepth: node.depth,
326
- descendantCount: node.children ? node.children.length : undefined,
327
- isGroupRow: groupRowIds.has(id),
391
+ descendantCount: isUnloadedLazyNode ? undefined : (descendantCounts.get(id) ?? 0),
392
+ isGroupRow: isSynthetic,
328
393
  isExpanded: expandedGroupIds.has(id),
329
- });
394
+ };
395
+ if (isSynthetic) meta.groupLabel = node.label;
396
+ map.set(id, meta);
330
397
  });
331
398
  return map;
332
- }, [treeNodes, groupingRows, expandedGroupIds]);
399
+ }, [treeNodes, syntheticIds, visibility, expandedGroupIds]);
333
400
 
334
401
  return useMemo(() => ({
335
402
  treeNodes,