@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.
- package/CHANGELOG.md +399 -0
- package/README.md +95 -72
- package/dist/index.d.ts +590 -131
- package/dist/opengridx.css +1 -1
- package/dist/opengridx.es.js +7891 -5631
- 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 -166
- 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 +6 -7
- package/docs/components/datagrid.md +15 -13
- 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 +90 -38
- 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 +126 -39
- 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 +72 -19
- package/docs/features/pinning.md +24 -5
- package/docs/features/selection.md +50 -10
- package/docs/features/sorting-pagination.md +64 -14
- package/docs/features/state-persistence.md +78 -10
- package/docs/features/toolbar-customization.md +26 -17
- package/docs/features/tree-data-grouping.md +36 -8
- package/docs/features/virtualization.md +37 -14
- package/docs/migration/v1-to-v2.md +115 -0
- package/docs/migration/v2-to-v3.md +1233 -0
- package/docs/performance.md +124 -0
- package/docs/roadmap.md +5 -5
- package/lib/components/Cell/Cell.css +17 -1
- package/lib/components/Cell/Cell.tsx +129 -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 +15 -2
- 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 +85 -12
- package/lib/components/DataGrid/DataGrid.tsx +582 -876
- package/lib/components/DataGrid/GridAggregationFooter.tsx +53 -17
- package/lib/components/DataGrid/GridErrorOverlay.tsx +15 -12
- package/lib/components/DataGrid/GridListView.tsx +165 -24
- 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 +62 -11
- package/lib/components/DataGrid/GridStandaloneColumnPanel.tsx +40 -38
- package/lib/components/DataGrid/GridToolbarSlot.tsx +51 -0
- package/lib/components/DataGrid/GridVirtualRows.tsx +40 -24
- 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 +6 -3
- package/lib/components/Header/Header.tsx +181 -160
- package/lib/components/ListView/ListView.css +25 -0
- package/lib/components/ListView/ListViewRow.tsx +76 -10
- package/lib/components/Pagination/Pagination.tsx +15 -7
- package/lib/components/Row/Row.tsx +177 -77
- 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 +19 -4
- 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 +279 -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 +104 -5
- 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 +447 -243
- 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 +17 -5
- 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 +82 -51
- 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 +298 -160
- package/lib/hooks/useRowReorder.ts +62 -31
- package/lib/hooks/useTreeData.ts +287 -220
- package/lib/index.ts +28 -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 +313 -116
- package/lib/utils/aggregation/index.ts +119 -24
- 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 +278 -139
- 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 +55 -5
- 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 +261 -164
- package/package.json +13 -5
- package/lib/components/ColumnGroupHeader/ColumnGroupHeader.tsx +0 -220
- package/lib/components/ColumnVisibilityPanel/ColumnVisibilityPanel.test.tsx +0 -85
- package/lib/components/DataGrid/DataGrid.defects.test.tsx +0 -103
- 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/DataGrid/DataGrid.virtualization.browser.test.tsx +0 -183
- 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 -298
- package/lib/hooks/useTreeData.test.ts +0 -83
- package/lib/utils/aggregation/aggregation.test.ts +0 -16
- package/lib/utils/export/exportToExcelAdvanced.test.ts +0 -109
- 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,8 +1,10 @@
|
|
|
1
|
-
import { useMemo, useState, useCallback } from 'react';
|
|
2
|
-
import { GridRowModel, GridRowId, GridTreeNode, GridFilterModel, GridSortItem } from '../types';
|
|
3
|
-
import
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
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
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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
|
-
|
|
119
|
-
};
|
|
244
|
+
const isActive = treeData && Boolean(getTreeDataPath);
|
|
120
245
|
|
|
121
|
-
|
|
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
|
-
|
|
124
|
-
const parentNode = nodes.get(parentId);
|
|
125
|
-
const childNode = nodes.get(id);
|
|
251
|
+
const { treeNodes, rootIds, groupingRows, syntheticIds, rowLookup } = build;
|
|
126
252
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
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
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
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
|
-
|
|
266
|
+
const rowFilter = useMemo(
|
|
267
|
+
() => (filterMode === 'server' ? null : createRowFilter(filterModel, columnLookup)),
|
|
268
|
+
[filterMode, filterModel, columnLookup]
|
|
269
|
+
);
|
|
164
270
|
|
|
165
|
-
|
|
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
|
-
|
|
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 (!
|
|
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
|
-
}, [
|
|
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
|
|
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
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
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
|
-
|
|
241
|
-
|
|
242
|
-
};
|
|
327
|
+
const { visible, visibleChildren } = visibility;
|
|
328
|
+
const activeSortModel = isClientSort && sortModel ? sortModel : [];
|
|
243
329
|
|
|
244
|
-
const
|
|
245
|
-
if (
|
|
246
|
-
|
|
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
|
-
|
|
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
|
|
266
|
-
|
|
267
|
-
|
|
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
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
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
|
|
382
|
+
const { visibleChildren, descendantCounts } = visibility;
|
|
319
383
|
treeNodes.forEach((node, id) => {
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
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:
|
|
327
|
-
isGroupRow:
|
|
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,
|
|
399
|
+
}, [treeNodes, syntheticIds, visibility, expandedGroupIds]);
|
|
333
400
|
|
|
334
401
|
return useMemo(() => ({
|
|
335
402
|
treeNodes,
|