@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.
- package/CHANGELOG.md +408 -3
- package/README.md +93 -72
- package/dist/index.d.ts +611 -121
- package/dist/opengridx.css +1 -1
- package/dist/opengridx.es.js +7992 -5650
- package/dist/opengridx.es.js.map +1 -1
- package/dist/opengridx.umd.js +17 -23
- package/dist/opengridx.umd.js.map +1 -1
- package/docs/API_REFERENCE.md +348 -161
- package/docs/README.md +6 -1
- package/docs/architecture/datagrid-orchestration.md +81 -0
- package/docs/architecture/grid-row-meta.md +102 -0
- package/docs/architecture/use-grid-columns.md +128 -0
- package/docs/architecture/use-grid-controlled-state.md +157 -0
- package/docs/architecture/use-grid-row-pipeline.md +120 -0
- package/docs/architecture/use-grid-scroll-sync.md +111 -0
- package/docs/architecture/use-grid-state-snapshot.md +90 -0
- package/docs/architecture/use-grid-virtualization.md +115 -0
- package/docs/architecture/use-grid-visible-rows.md +85 -0
- package/docs/assets/banner-alt.png +0 -0
- package/docs/assets/banner.png +0 -0
- package/docs/assets/hero.png +0 -0
- package/docs/assets/logo.png +0 -0
- package/docs/components/aggregation-footer.md +15 -10
- package/docs/components/cell.md +16 -6
- package/docs/components/column-group-header.md +55 -28
- package/docs/components/column-resize.md +17 -13
- package/docs/components/column-visibility.md +45 -22
- package/docs/components/datagrid.md +14 -12
- package/docs/components/empty-state.md +2 -1
- package/docs/components/error-overlay.md +12 -7
- package/docs/components/filter-panel.md +28 -8
- package/docs/components/header.md +28 -9
- package/docs/components/pagination.md +14 -8
- package/docs/components/row.md +21 -5
- package/docs/components/toolbar.md +27 -9
- package/docs/components/tooltip.md +16 -7
- package/docs/customization/slots-api.md +100 -36
- package/docs/customization/theming.md +66 -18
- package/docs/features/aggregation-pivot.md +75 -9
- package/docs/features/cell-spanning.md +80 -125
- package/docs/features/clipboard.md +41 -16
- package/docs/features/custom-pagination.md +38 -18
- package/docs/features/data-source.md +36 -13
- package/docs/features/editing-reordering.md +125 -16
- package/docs/features/export-guide.md +147 -41
- package/docs/features/filtering.md +44 -6
- package/docs/features/infinite-scroll.md +54 -46
- package/docs/features/keyboard-navigation.md +83 -0
- package/docs/features/list-view.md +58 -5
- package/docs/features/loading-states.md +18 -20
- package/docs/features/master-detail.md +18 -5
- package/docs/features/pdf-export.md +56 -19
- package/docs/features/pinning.md +23 -5
- package/docs/features/selection.md +50 -10
- package/docs/features/sorting-pagination.md +66 -14
- package/docs/features/state-persistence.md +78 -10
- package/docs/features/toolbar-customization.md +26 -17
- package/docs/features/tree-data-grouping.md +44 -9
- package/docs/features/virtualization.md +64 -15
- package/docs/migration/v1-to-v2.md +115 -0
- package/docs/migration/v2-to-v3.md +1222 -0
- package/docs/performance.md +124 -0
- package/docs/roadmap.md +16 -10
- package/lib/components/Cell/Cell.css +13 -1
- package/lib/components/Cell/Cell.tsx +130 -57
- package/lib/components/Cell/CellErrorBoundary.tsx +12 -6
- package/lib/components/Cell/GridEditInputCell.tsx +114 -41
- package/lib/components/ColumnGroupHeader/ColumnGroupHeader.css +7 -1
- package/lib/components/ColumnResizeHandle/ColumnResizeHandle.css +11 -0
- package/lib/components/ColumnResizeHandle/ColumnResizeHandle.tsx +104 -38
- package/lib/components/ColumnResizeHandle/clampResizeWidth.ts +13 -0
- package/lib/components/ColumnVisibilityPanel/ColumnVisibilityPanel.css +6 -0
- package/lib/components/ColumnVisibilityPanel/ColumnVisibilityPanel.tsx +21 -13
- package/lib/components/DataGrid/DataGrid.css +84 -12
- package/lib/components/DataGrid/DataGrid.tsx +606 -872
- package/lib/components/DataGrid/GridAggregationFooter.tsx +53 -17
- package/lib/components/DataGrid/GridEmptyState.tsx +7 -1
- package/lib/components/DataGrid/GridErrorOverlay.tsx +15 -12
- package/lib/components/DataGrid/GridListView.tsx +166 -23
- package/lib/components/DataGrid/GridLiveRegion.tsx +31 -0
- package/lib/components/DataGrid/GridLoadingOverlay.tsx +27 -0
- package/lib/components/DataGrid/GridPaginationArea.tsx +44 -0
- package/lib/components/DataGrid/GridPinnedRows.tsx +65 -11
- package/lib/components/DataGrid/GridStandaloneColumnPanel.tsx +40 -38
- package/lib/components/DataGrid/GridToolbarSlot.tsx +51 -0
- package/lib/components/DataGrid/GridVirtualRows.tsx +48 -25
- package/lib/components/DetailPanel/DetailPanel.css +0 -6
- package/lib/components/DetailPanel/DetailPanel.tsx +34 -5
- package/lib/components/DetailPanel/DetailPanelContent.tsx +17 -0
- package/lib/components/FilterPanel/FilterPanel.css +13 -2
- package/lib/components/FilterPanel/FilterPanel.tsx +272 -99
- package/lib/components/Header/ColumnMenu.tsx +91 -39
- package/lib/components/Header/Header.css +3 -3
- package/lib/components/Header/Header.tsx +179 -160
- package/lib/components/ListView/ListView.css +25 -0
- package/lib/components/ListView/ListViewRow.tsx +79 -10
- package/lib/components/Pagination/Pagination.tsx +15 -7
- package/lib/components/Row/Row.tsx +185 -75
- package/lib/components/Toolbar/GlobalSearch.css +6 -3
- package/lib/components/Toolbar/GlobalSearch.tsx +26 -27
- package/lib/components/Toolbar/GridToolbar.css +12 -11
- package/lib/components/Toolbar/GridToolbar.tsx +133 -89
- package/lib/components/Toolbar/PivotPanel.tsx +89 -49
- package/lib/components/Tooltip/Tooltip.css +20 -0
- package/lib/components/Tooltip/Tooltip.tsx +68 -35
- package/lib/components/ui/Button.tsx +3 -0
- package/lib/components/ui/Checkbox.tsx +2 -1
- package/lib/components/ui/ExpandIcon.css +0 -9
- package/lib/components/ui/ExpandIcon.tsx +4 -1
- package/lib/hooks/core/gridApiPlaceholder.ts +34 -0
- package/lib/hooks/core/gridToolbarHostContext.ts +14 -0
- package/lib/hooks/core/useDataGrid.ts +115 -173
- package/lib/hooks/core/useGridAggregationApi.ts +72 -0
- package/lib/hooks/core/useGridApiMethods.ts +115 -0
- package/lib/hooks/core/useGridApiRef.ts +15 -3
- package/lib/hooks/core/useGridApiRefBinding.ts +18 -0
- package/lib/hooks/core/useGridAriaRows.ts +38 -0
- package/lib/hooks/core/useGridClipboardApi.ts +38 -0
- package/lib/hooks/core/useGridColumnLookup.ts +30 -0
- package/lib/hooks/core/useGridColumns.tsx +284 -76
- package/lib/hooks/core/useGridColumnsPanel.ts +77 -0
- package/lib/hooks/core/useGridControlledState.ts +99 -6
- package/lib/hooks/core/useGridDetailPanel.ts +51 -0
- package/lib/hooks/core/useGridDevWarnings.ts +123 -0
- package/lib/hooks/core/useGridFocusHandlers.ts +50 -0
- package/lib/hooks/core/useGridGroupingColumn.ts +62 -0
- package/lib/hooks/core/useGridHeaderHandlers.ts +92 -0
- package/lib/hooks/core/useGridHierarchy.ts +81 -0
- package/lib/hooks/core/useGridKeyboardNavigation.ts +452 -250
- package/lib/hooks/core/useGridLiveRowSelection.ts +67 -0
- package/lib/hooks/core/useGridPageCorrection.ts +55 -0
- package/lib/hooks/core/useGridRowIdOf.ts +32 -0
- package/lib/hooks/core/useGridRowInteractions.ts +81 -0
- package/lib/hooks/core/useGridRowPipeline.ts +69 -39
- package/lib/hooks/core/useGridRowSelection.ts +91 -0
- package/lib/hooks/core/useGridScrollSync.ts +80 -32
- package/lib/hooks/core/useGridScrollToIndexesApi.ts +45 -0
- package/lib/hooks/core/useGridStateSnapshot.ts +40 -5
- package/lib/hooks/core/useGridThemeDimensions.ts +49 -0
- package/lib/hooks/core/useGridToolbarProps.ts +97 -0
- package/lib/hooks/core/useGridViewport.ts +38 -0
- package/lib/hooks/core/useGridViewportSize.ts +26 -0
- package/lib/hooks/core/useGridVisibleRows.ts +20 -7
- package/lib/hooks/core/useLayout.ts +215 -195
- package/lib/hooks/core/useRowsScrollEndLatch.ts +41 -0
- package/lib/hooks/core/useStableColumns.ts +34 -0
- package/lib/hooks/features/useAggregation.ts +86 -90
- package/lib/hooks/features/useColumnGroupReorderGuard.ts +34 -0
- package/lib/hooks/features/useDetailPanelHeights.ts +29 -0
- package/lib/hooks/features/useGridClipboard.ts +133 -73
- package/lib/hooks/features/useGridDataSource.ts +354 -128
- package/lib/hooks/features/useGridEditing.ts +211 -51
- package/lib/hooks/features/useGridPivot.ts +92 -0
- package/lib/hooks/features/useGridSpanRenderWindow.ts +104 -0
- package/lib/hooks/features/useGridSpanning.ts +410 -306
- package/lib/hooks/features/usePivot.ts +0 -0
- package/lib/hooks/features/useServerTreeChildren.ts +50 -0
- package/lib/hooks/useColumnReorder.ts +38 -44
- package/lib/hooks/useRowGrouping.ts +335 -214
- package/lib/hooks/useRowReorder.ts +62 -31
- package/lib/hooks/useTreeData.ts +330 -262
- package/lib/index.ts +30 -2
- package/lib/state/index.ts +1 -0
- package/lib/state/types.ts +7 -1
- package/lib/state/useGridStateStorage.ts +88 -29
- package/lib/theme/DataGridThemeProvider.tsx +143 -38
- package/lib/theme/gridThemeContext.ts +5 -0
- package/lib/theme/index.ts +5 -0
- package/lib/theme/presets.ts +5 -4
- package/lib/theme/types.ts +40 -2
- package/lib/types/index.ts +311 -108
- package/lib/utils/aggregation/index.ts +157 -0
- package/lib/utils/aria/index.ts +40 -0
- package/lib/utils/columnGroups.ts +110 -0
- package/lib/utils/columnLookup.ts +33 -0
- package/lib/utils/columnWidth.ts +22 -0
- package/lib/utils/dataSource/index.ts +46 -0
- package/lib/utils/detailPanel.ts +66 -0
- package/lib/utils/dragData.ts +44 -0
- package/lib/utils/editing/index.ts +30 -0
- package/lib/utils/export/exportShared.ts +197 -0
- package/lib/utils/export/exportToExcelAdvanced.ts +344 -121
- package/lib/utils/export/exportToPdf.ts +143 -92
- package/lib/utils/export/groupLabel.ts +14 -0
- package/lib/utils/export/index.ts +278 -272
- package/lib/utils/export/pdfText.ts +50 -0
- package/lib/utils/filtering/index.ts +277 -148
- package/lib/utils/focus/index.ts +115 -0
- package/lib/utils/gridApiRows.ts +35 -0
- package/lib/utils/gridModes.ts +77 -0
- package/lib/utils/grouping/groupedExportRows.ts +91 -0
- package/lib/utils/pagination/index.ts +56 -0
- package/lib/utils/pinning/index.ts +77 -33
- package/lib/utils/pivot/index.ts +368 -0
- package/lib/utils/scroll/index.ts +78 -0
- package/lib/utils/scroll/scrollEnd.ts +18 -0
- package/lib/utils/sorting/index.ts +154 -30
- package/lib/utils/syntheticRows.ts +12 -0
- package/lib/utils/values.ts +206 -0
- package/lib/utils/viewport.ts +14 -0
- package/llms.txt +272 -166
- package/package.json +18 -6
- package/lib/components/ColumnGroupHeader/ColumnGroupHeader.tsx +0 -220
- package/lib/components/ColumnVisibilityPanel/ColumnVisibilityPanel.test.tsx +0 -85
- package/lib/components/DataGrid/DataGrid.regression.test.tsx +0 -132
- package/lib/components/DataGrid/DataGrid.smoke.test.tsx +0 -58
- package/lib/components/DataGrid/DataGrid.v2.test.tsx +0 -127
- package/lib/components/Header/Header.test.tsx +0 -135
- package/lib/components/Pagination/Pagination.test.tsx +0 -64
- package/lib/hooks/core/useDataGrid.regression.test.ts +0 -66
- package/lib/hooks/core/useGridControlledState.test.ts +0 -86
- package/lib/hooks/core/useGridKeyboardNavigation.test.ts +0 -176
- package/lib/hooks/core/useGridRowPipeline.test.ts +0 -117
- package/lib/hooks/features/useAggregation.test.ts +0 -175
- package/lib/hooks/features/usePivot.test.ts +0 -202
- package/lib/hooks/useRowGrouping.test.ts +0 -137
- package/lib/utils/export/exportToPdf.test.ts +0 -120
- package/lib/utils/export/groupedExport.test.ts +0 -144
- package/lib/utils/filtering/filtering.test.ts +0 -178
- package/lib/utils/sorting/sorting.test.ts +0 -88
package/lib/hooks/useTreeData.ts
CHANGED
|
@@ -1,299 +1,364 @@
|
|
|
1
1
|
import { useMemo, useState, useCallback, useEffect } from 'react';
|
|
2
|
-
import { GridRowModel, GridRowId, GridTreeNode, GridFilterModel, GridSortItem } from '../types';
|
|
3
|
-
import
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
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';
|
|
8
|
+
|
|
9
|
+
const EMPTY_OVERRIDES: Map<GridRowId, boolean> = new Map();
|
|
10
|
+
|
|
11
|
+
interface ExpansionOverrides {
|
|
12
|
+
configKey: string;
|
|
13
|
+
overrides: Map<GridRowId, boolean>;
|
|
14
|
+
}
|
|
6
15
|
|
|
7
16
|
interface UseTreeDataProps<R extends GridRowModel> {
|
|
8
17
|
rows: R[];
|
|
9
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
|
+
*/
|
|
10
24
|
getTreeDataPath?: (row: R) => string[];
|
|
11
25
|
treeData?: boolean;
|
|
12
26
|
defaultGroupingExpansionDepth?: number;
|
|
13
27
|
filterModel?: GridFilterModel;
|
|
14
28
|
sortModel?: GridSortItem[];
|
|
15
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';
|
|
16
36
|
}
|
|
17
37
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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
|
+
}
|
|
29
62
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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
|
+
}
|
|
37
117
|
}
|
|
38
118
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
const id = getRowId(row);
|
|
48
|
-
const path = getTreeDataPath(row);
|
|
49
|
-
const pathKey = getPathKey(path);
|
|
50
|
-
|
|
51
|
-
pathLookup.set(pathKey, id);
|
|
52
|
-
|
|
53
|
-
nodes.set(id, {
|
|
54
|
-
id,
|
|
55
|
-
parentId: null,
|
|
56
|
-
depth: path.length - 1,
|
|
57
|
-
groupingKey: path[path.length - 1],
|
|
58
|
-
isExpanded: false,
|
|
59
|
-
children: [],
|
|
60
|
-
serverChildrenCount: row.serverChildrenCount
|
|
61
|
-
});
|
|
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,
|
|
62
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
|
+
}
|
|
63
187
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
const ensureAncestors = (childPath: string[]) => {
|
|
74
|
-
if (childPath.length <= 1) return;
|
|
75
|
-
|
|
76
|
-
const parentPath = childPath.slice(0, -1);
|
|
77
|
-
const parentPathKey = getPathKey(parentPath);
|
|
78
|
-
let parentId = pathLookup.get(parentPathKey);
|
|
79
|
-
|
|
80
|
-
if (parentId === undefined) {
|
|
81
|
-
parentId = `auto-group-${parentPathKey}`;
|
|
82
|
-
|
|
83
|
-
if (!nodes.has(parentId)) {
|
|
84
|
-
const groupLabel = parentPath[parentPath.length - 1];
|
|
85
|
-
nodes.set(parentId, {
|
|
86
|
-
id: parentId,
|
|
87
|
-
parentId: null,
|
|
88
|
-
depth: parentPath.length - 1,
|
|
89
|
-
groupingKey: groupLabel,
|
|
90
|
-
isExpanded: false,
|
|
91
|
-
children: [],
|
|
92
|
-
label: groupLabel
|
|
93
|
-
});
|
|
94
|
-
pathLookup.set(parentPathKey, parentId);
|
|
95
|
-
|
|
96
|
-
const groupRow = {
|
|
97
|
-
id: parentId,
|
|
98
|
-
name: groupLabel,
|
|
99
|
-
_isGroupRow: true
|
|
100
|
-
} as unknown as GridRowModel;
|
|
101
|
-
groupRows.push(groupRow);
|
|
102
|
-
|
|
103
|
-
if (parentPath.length === 1) {
|
|
104
|
-
roots.push(parentId);
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
ensureAncestors(parentPath);
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
return parentId;
|
|
112
|
-
};
|
|
113
|
-
|
|
114
|
-
const parentId = ensureAncestors(path);
|
|
115
|
-
|
|
116
|
-
if (parentId !== undefined) {
|
|
117
|
-
const parentNode = nodes.get(parentId);
|
|
118
|
-
const childNode = nodes.get(id);
|
|
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
|
+
}
|
|
119
196
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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);
|
|
127
212
|
}
|
|
128
|
-
}
|
|
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
|
+
}
|
|
129
228
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
node.parentId = parentId;
|
|
145
|
-
const parentNode = nodes.get(parentId);
|
|
146
|
-
if (parentNode) {
|
|
147
|
-
parentNode.children = parentNode.children || [];
|
|
148
|
-
if (!parentNode.children.includes(groupId)) {
|
|
149
|
-
parentNode.children.push(groupId);
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
});
|
|
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;
|
|
155
243
|
|
|
156
|
-
|
|
244
|
+
const isActive = treeData && Boolean(getTreeDataPath);
|
|
157
245
|
|
|
246
|
+
const build = useMemo<TreeBuild>(() => {
|
|
247
|
+
if (!treeData || !getTreeDataPath) return EMPTY_BUILD;
|
|
248
|
+
return buildTree(rows, getRowId, getTreeDataPath);
|
|
158
249
|
}, [rows, getTreeDataPath, treeData, getRowId]);
|
|
159
250
|
|
|
160
|
-
const
|
|
161
|
-
if (!treeData) return new Set<GridRowId>();
|
|
162
|
-
|
|
163
|
-
const initialExpanded = new Set<GridRowId>();
|
|
164
|
-
treeNodes.forEach(node => {
|
|
165
|
-
if (node.children && node.children.length > 0) {
|
|
166
|
-
if (defaultGroupingExpansionDepth === -1 || node.depth < defaultGroupingExpansionDepth) {
|
|
167
|
-
initialExpanded.add(node.id);
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
});
|
|
171
|
-
return initialExpanded;
|
|
172
|
-
}, [treeNodes, treeData, defaultGroupingExpansionDepth]);
|
|
251
|
+
const { treeNodes, rootIds, groupingRows, syntheticIds, rowLookup } = build;
|
|
173
252
|
|
|
174
|
-
|
|
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]);
|
|
175
259
|
|
|
260
|
+
const isMissingPathGetter = treeData && !getTreeDataPath;
|
|
176
261
|
useEffect(() => {
|
|
177
|
-
if (
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
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]);
|
|
265
|
+
|
|
266
|
+
const rowFilter = useMemo(
|
|
267
|
+
() => (filterMode === 'server' ? null : createRowFilter(filterModel, columnLookup)),
|
|
268
|
+
[filterMode, filterModel, columnLookup]
|
|
269
|
+
);
|
|
270
|
+
|
|
271
|
+
const visibility = useMemo(() => computeVisibility(build, rowFilter), [build, rowFilter]);
|
|
272
|
+
|
|
273
|
+
// Same model as useRowGrouping: depth-based default plus user overrides keyed by config.
|
|
274
|
+
// Rebuilding the tree (row edits, lazily loaded children) keeps the user's choices.
|
|
275
|
+
const configKey = `${treeData}|${defaultGroupingExpansionDepth}`;
|
|
276
|
+
const [expansionState, setExpansionState] = useState<ExpansionOverrides>(() => ({
|
|
277
|
+
configKey,
|
|
278
|
+
overrides: new Map(),
|
|
279
|
+
}));
|
|
280
|
+
const overrides = expansionState.configKey === configKey ? expansionState.overrides : EMPTY_OVERRIDES;
|
|
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.
|
|
284
|
+
const isDefaultExpanded = useCallback((node: GridTreeNode | undefined) => (
|
|
285
|
+
((node?.children?.length ?? 0) > 0 || (node?.serverChildrenCount ?? 0) > 0) &&
|
|
286
|
+
(defaultGroupingExpansionDepth === -1 || (node?.depth ?? 0) < defaultGroupingExpansionDepth)
|
|
287
|
+
), [defaultGroupingExpansionDepth]);
|
|
288
|
+
|
|
289
|
+
const expandedGroupIds = useMemo<Set<GridRowId>>(() => {
|
|
290
|
+
const ids = new Set<GridRowId>();
|
|
291
|
+
if (!isActive) return ids;
|
|
292
|
+
treeNodes.forEach((node, id) => {
|
|
293
|
+
// A lazily loaded node can be expanded before it has any children, so an
|
|
294
|
+
// explicit override counts even when children is still empty.
|
|
295
|
+
if (overrides.get(id) ?? isDefaultExpanded(node)) ids.add(id);
|
|
296
|
+
});
|
|
297
|
+
return ids;
|
|
298
|
+
}, [isActive, treeNodes, overrides, isDefaultExpanded]);
|
|
181
299
|
|
|
182
300
|
const toggleExpansion = useCallback((id: GridRowId) => {
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
const
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
} else {
|
|
190
|
-
next.add(id);
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
if (isExpanding && onRowExpansionChange) {
|
|
194
|
-
const node = treeNodes.get(id);
|
|
195
|
-
if (node) {
|
|
196
|
-
onRowExpansionChange(node);
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
return next;
|
|
301
|
+
const isExpanding = !expandedGroupIds.has(id);
|
|
302
|
+
setExpansionState(prev => {
|
|
303
|
+
const base = prev.configKey === configKey ? prev.overrides : EMPTY_OVERRIDES;
|
|
304
|
+
const next = new Map(base);
|
|
305
|
+
next.set(id, isExpanding);
|
|
306
|
+
return { configKey, overrides: next };
|
|
201
307
|
});
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
const getVisibleRows = useCallback(() => {
|
|
205
|
-
|
|
206
|
-
if (!treeData) return null;
|
|
207
|
-
|
|
208
|
-
const rowLookup = new Map<GridRowId, GridRowModel>();
|
|
209
|
-
rows.forEach(r => rowLookup.set(getRowId(r), r));
|
|
210
|
-
groupingRows.forEach(r => rowLookup.set(r.id, r));
|
|
211
|
-
|
|
212
|
-
const matchesFilter = (row: GridRowModel) => {
|
|
213
|
-
return isRowMatchingFilter(row, filterModel!);
|
|
214
|
-
};
|
|
215
|
-
|
|
216
|
-
const filterCache = new Map<GridRowId, boolean>();
|
|
217
|
-
|
|
218
|
-
const isNodeVisible = (id: GridRowId): boolean => {
|
|
219
|
-
if (filterCache.has(id)) return filterCache.get(id)!;
|
|
220
|
-
|
|
221
|
-
const row = rowLookup.get(id);
|
|
222
|
-
if (!row) return false;
|
|
223
|
-
|
|
224
|
-
const selfMatch = matchesFilter(row);
|
|
225
|
-
if (selfMatch) {
|
|
226
|
-
filterCache.set(id, true);
|
|
227
|
-
return true;
|
|
228
|
-
}
|
|
229
|
-
|
|
308
|
+
// Called outside the state updater: updaters may run twice, and this can trigger a server fetch.
|
|
309
|
+
if (isExpanding && onRowExpansionChange) {
|
|
230
310
|
const node = treeNodes.get(id);
|
|
231
|
-
if (node
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
if (comp !== 0) return comp;
|
|
311
|
+
if (node) onRowExpansionChange(node);
|
|
312
|
+
}
|
|
313
|
+
}, [expandedGroupIds, configKey, treeNodes, onRowExpansionChange]);
|
|
314
|
+
|
|
315
|
+
const isClientSort = sortingMode !== 'server' && Boolean(sortModel && sortModel.length > 0);
|
|
316
|
+
|
|
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;
|
|
326
|
+
|
|
327
|
+
const { visible, visibleChildren } = visibility;
|
|
328
|
+
const activeSortModel = isClientSort && sortModel ? sortModel : [];
|
|
329
|
+
|
|
330
|
+
const readSortValue = (id: GridRowId, sortItem: GridSortItem): GridSortValue => {
|
|
331
|
+
if (syntheticIds.has(id)) {
|
|
332
|
+
return { value: sortItem.field === labelField ? treeNodes.get(id)?.label : undefined };
|
|
254
333
|
}
|
|
255
|
-
|
|
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 };
|
|
256
337
|
};
|
|
257
338
|
|
|
258
339
|
const result: GridRowModel[] = [];
|
|
259
|
-
|
|
260
340
|
const seenIds = new Set<GridRowId>();
|
|
261
341
|
|
|
262
|
-
const traverse = (ids: GridRowId[]) => {
|
|
263
|
-
|
|
264
|
-
const
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
visibleIds.forEach(id => {
|
|
269
|
-
if (seenIds.has(id)) return;
|
|
270
|
-
|
|
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;
|
|
271
347
|
const row = rowLookup.get(id);
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
_treeDepth: node.depth,
|
|
280
|
-
_isExpanded: expandedGroupIds.has(id),
|
|
281
|
-
_hasChildren: Boolean((node.children && node.children.length > 0) || (node.serverChildrenCount && node.serverChildrenCount > 0)),
|
|
282
|
-
_descendantCount: node.children ? node.children.length : 0
|
|
283
|
-
};
|
|
284
|
-
result.push(enhancedRow);
|
|
285
|
-
|
|
286
|
-
if (expandedGroupIds.has(id) && node.children && node.children.length > 0) {
|
|
287
|
-
traverse(node.children);
|
|
288
|
-
}
|
|
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);
|
|
289
355
|
}
|
|
290
|
-
}
|
|
356
|
+
}
|
|
291
357
|
};
|
|
292
358
|
|
|
293
359
|
traverse(rootIds);
|
|
294
360
|
return result;
|
|
295
|
-
|
|
296
|
-
}, [treeData, rows, groupingRows, rootIds, treeNodes, expandedGroupIds, filterModel, sortModel, getRowId]);
|
|
361
|
+
}, [isActive, visibility, isClientSort, sortModel, syntheticIds, treeNodes, rowLookup, columnLookup, rootIds, expandedGroupIds]);
|
|
297
362
|
|
|
298
363
|
const isGroupExpanded = useCallback((id: GridRowId) => expandedGroupIds.has(id), [expandedGroupIds]);
|
|
299
364
|
const getNode = useCallback((id: GridRowId) => treeNodes.get(id), [treeNodes]);
|
|
@@ -314,21 +379,24 @@ export function useTreeData<R extends GridRowModel>(props: UseTreeDataProps<R>)
|
|
|
314
379
|
|
|
315
380
|
const rowMetaMap = useMemo<Map<GridRowId, GridRowMeta>>(() => {
|
|
316
381
|
const map = new Map<GridRowId, GridRowMeta>();
|
|
317
|
-
const
|
|
382
|
+
const { visibleChildren, descendantCounts } = visibility;
|
|
318
383
|
treeNodes.forEach((node, id) => {
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
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,
|
|
324
390
|
treeDepth: node.depth,
|
|
325
|
-
descendantCount:
|
|
326
|
-
isGroupRow:
|
|
391
|
+
descendantCount: isUnloadedLazyNode ? undefined : (descendantCounts.get(id) ?? 0),
|
|
392
|
+
isGroupRow: isSynthetic,
|
|
327
393
|
isExpanded: expandedGroupIds.has(id),
|
|
328
|
-
}
|
|
394
|
+
};
|
|
395
|
+
if (isSynthetic) meta.groupLabel = node.label;
|
|
396
|
+
map.set(id, meta);
|
|
329
397
|
});
|
|
330
398
|
return map;
|
|
331
|
-
}, [treeNodes,
|
|
399
|
+
}, [treeNodes, syntheticIds, visibility, expandedGroupIds]);
|
|
332
400
|
|
|
333
401
|
return useMemo(() => ({
|
|
334
402
|
treeNodes,
|