@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
|
@@ -10,10 +10,15 @@ import type {
|
|
|
10
10
|
GridRowMeta,
|
|
11
11
|
GridColDef
|
|
12
12
|
} from '../types';
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
13
|
+
import { createRowFilter } from '../utils/filtering';
|
|
14
|
+
import { sortItemsBySortModel, type GridSortValue } from '../utils/sorting';
|
|
15
|
+
import { callSafely, getCellValue } from '../utils/values';
|
|
16
|
+
import type { GridColumnLookup } from '../utils/columnLookup';
|
|
15
17
|
import { computeAggregations } from '../utils/aggregation';
|
|
16
18
|
|
|
19
|
+
/** Where a group's aggregates are shown: on the group row, on a subtotal row after its children, or not at all. */
|
|
20
|
+
export type GridGroupAggregationPosition = 'inline' | 'footer' | null;
|
|
21
|
+
|
|
17
22
|
export interface UseRowGroupingParams<R extends GridRowModel> {
|
|
18
23
|
rows: R[];
|
|
19
24
|
getRowId: (row: R) => GridRowId;
|
|
@@ -23,13 +28,44 @@ export interface UseRowGroupingParams<R extends GridRowModel> {
|
|
|
23
28
|
defaultGroupingExpansionDepth?: number;
|
|
24
29
|
filterModel?: GridFilterModel;
|
|
25
30
|
sortModel?: GridSortItem[];
|
|
26
|
-
getAggregationPosition?: (groupNode: GridTreeNode | null) =>
|
|
31
|
+
getAggregationPosition?: (groupNode: GridTreeNode | null) => GridGroupAggregationPosition;
|
|
32
|
+
/** Column definitions, so filter and sort read leaf cells through valueGetter and use the column type. */
|
|
33
|
+
columnLookup?: GridColumnLookup;
|
|
34
|
+
/** 'server': the rows arrive filtered, so the groups are built from them as they are. */
|
|
35
|
+
filterMode?: 'client' | 'server';
|
|
36
|
+
/** 'server': the rows arrive sorted, so groups and leaves keep the order the server sent them in. */
|
|
37
|
+
sortingMode?: 'client' | 'server';
|
|
27
38
|
}
|
|
28
39
|
|
|
29
|
-
|
|
40
|
+
// Group rows show their aggregates inline; the grand total (null) is the grid's footer row.
|
|
41
|
+
const defaultGetAggregationPosition = (groupNode: GridTreeNode | null): GridGroupAggregationPosition =>
|
|
42
|
+
groupNode === null ? 'footer' : 'inline';
|
|
43
|
+
|
|
44
|
+
// A getAggregationPosition that throws falls back to the default position for that node.
|
|
45
|
+
function callAggregationPosition(
|
|
46
|
+
getAggregationPosition: (groupNode: GridTreeNode | null) => GridGroupAggregationPosition,
|
|
47
|
+
groupNode: GridTreeNode | null,
|
|
48
|
+
): GridGroupAggregationPosition {
|
|
49
|
+
return callSafely(
|
|
50
|
+
() => getAggregationPosition(groupNode),
|
|
51
|
+
defaultGetAggregationPosition(groupNode),
|
|
52
|
+
'getAggregationPosition',
|
|
53
|
+
'getAggregationPosition threw; the default position is used',
|
|
54
|
+
);
|
|
55
|
+
}
|
|
30
56
|
|
|
31
|
-
|
|
32
|
-
|
|
57
|
+
// The group-row label: the column's groupingValueFormatter, or "field: value" (also when it throws).
|
|
58
|
+
function formatGroupLabel(colDef: GridColDef | undefined, field: string, value: unknown): string {
|
|
59
|
+
const fallback = `${field}: ${String(value)}`;
|
|
60
|
+
const formatter = colDef?.groupingValueFormatter;
|
|
61
|
+
if (!formatter) return fallback;
|
|
62
|
+
return callSafely(
|
|
63
|
+
() => formatter({ field, value }),
|
|
64
|
+
fallback,
|
|
65
|
+
`groupingValueFormatter:${field}`,
|
|
66
|
+
`groupingValueFormatter for column "${field}" threw; the group is labelled "${field}: value"`,
|
|
67
|
+
);
|
|
68
|
+
}
|
|
33
69
|
|
|
34
70
|
// Stable empty defaults — module-level constants prevent new object identity
|
|
35
71
|
// on every render, which would otherwise invalidate useMemo deps and cause
|
|
@@ -38,11 +74,73 @@ const EMPTY_ROW_GROUPING_MODEL: GridRowGroupingModel = [];
|
|
|
38
74
|
const EMPTY_AGGREGATION_MODEL: GridAggregationModel = {};
|
|
39
75
|
const EMPTY_OVERRIDES: Map<GridRowId, boolean> = new Map();
|
|
40
76
|
|
|
77
|
+
const FIELD_SEPARATOR = '\u001f';
|
|
78
|
+
|
|
79
|
+
/** Options of the hierarchy hooks' getVisibleRows. */
|
|
80
|
+
export interface GridHierarchyVisibleRowsOptions {
|
|
81
|
+
/** Walk every group regardless of expansion (exports, getAllFilteredRows). */
|
|
82
|
+
expandAll?: boolean;
|
|
83
|
+
/** The column that shows the hierarchy: sorting by it orders group rows by their grouping value / label. */
|
|
84
|
+
labelField?: string;
|
|
85
|
+
}
|
|
86
|
+
|
|
41
87
|
interface ExpansionOverrides {
|
|
42
88
|
configKey: string;
|
|
43
89
|
overrides: Map<GridRowId, boolean>;
|
|
44
90
|
}
|
|
45
91
|
|
|
92
|
+
/**
|
|
93
|
+
* The bucket key of a grouping value. Distinct values never share a key, so `null` and `'null'`,
|
|
94
|
+
* `1` and `'1'`, `true` and `'true'` are separate groups; equal dates share one. Strings keep their
|
|
95
|
+
* own text, so the group ids of string values stay readable.
|
|
96
|
+
*/
|
|
97
|
+
function groupKeyOf(value: unknown): string {
|
|
98
|
+
if (typeof value === 'string') return value;
|
|
99
|
+
if (value === null) return `${FIELD_SEPARATOR}null`;
|
|
100
|
+
if (value === undefined) return `${FIELD_SEPARATOR}undefined`;
|
|
101
|
+
if (value instanceof Date) return `${FIELD_SEPARATOR}date:${value.getTime()}`;
|
|
102
|
+
if (typeof value === 'object') {
|
|
103
|
+
try {
|
|
104
|
+
return `${FIELD_SEPARATOR}object:${JSON.stringify(value)}`;
|
|
105
|
+
} catch {
|
|
106
|
+
return `${FIELD_SEPARATOR}object:${String(value)}`;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return `${FIELD_SEPARATOR}${typeof value}:${String(value)}`;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const groupIdOf = (field: string, key: string, parentId: GridRowId | null): string =>
|
|
113
|
+
`auto-group-${field}-${key}-${parentId ?? 'root'}`;
|
|
114
|
+
|
|
115
|
+
/** The id of the subtotal row shown after a group's children when its aggregates are in 'footer' position. */
|
|
116
|
+
const footerIdOf = (groupId: GridRowId): string => `${groupId}${FIELD_SEPARATOR}footer`;
|
|
117
|
+
|
|
118
|
+
interface GroupRowVariants<R> {
|
|
119
|
+
/** The group row carrying its aggregates. */
|
|
120
|
+
inline: R;
|
|
121
|
+
/** The group row without aggregates ('footer' and null positions). */
|
|
122
|
+
bare: R;
|
|
123
|
+
/** The subtotal row, when there is an aggregation model. */
|
|
124
|
+
footer: R | null;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
interface GroupingBuild<R extends GridRowModel> {
|
|
128
|
+
treeNodes: Map<GridRowId, GridTreeNode>;
|
|
129
|
+
rootIds: GridRowId[];
|
|
130
|
+
groupIds: GridRowId[];
|
|
131
|
+
groupRowVariants: Map<GridRowId, GroupRowVariants<R>>;
|
|
132
|
+
/** Synthetic rows by id: every group row (with its aggregates) and every subtotal row. */
|
|
133
|
+
groupingRows: Map<GridRowId, R>;
|
|
134
|
+
/** The leaf rows in the tree, by id. */
|
|
135
|
+
rowLookup: Map<GridRowId, R>;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const encodePosition = (position: GridGroupAggregationPosition): string =>
|
|
139
|
+
position === 'footer' ? 'f' : position === null ? 'n' : 'i';
|
|
140
|
+
|
|
141
|
+
const decodePosition = (code: string | undefined): GridGroupAggregationPosition =>
|
|
142
|
+
code === 'f' ? 'footer' : code === 'n' ? null : 'inline';
|
|
143
|
+
|
|
46
144
|
export function useRowGrouping<R extends GridRowModel>(params: UseRowGroupingParams<R>) {
|
|
47
145
|
const {
|
|
48
146
|
rows,
|
|
@@ -53,48 +151,80 @@ export function useRowGrouping<R extends GridRowModel>(params: UseRowGroupingPar
|
|
|
53
151
|
defaultGroupingExpansionDepth = 0,
|
|
54
152
|
filterModel,
|
|
55
153
|
sortModel,
|
|
56
|
-
getAggregationPosition = defaultGetAggregationPosition
|
|
154
|
+
getAggregationPosition = defaultGetAggregationPosition,
|
|
155
|
+
columnLookup,
|
|
156
|
+
filterMode = 'client',
|
|
157
|
+
sortingMode = 'client',
|
|
57
158
|
} = params;
|
|
58
159
|
|
|
160
|
+
// Keyed by value, so an inline `rowGroupingModel={['region']}` or aggregation model does not
|
|
161
|
+
// rebuild (and re-aggregate) every group on each parent render.
|
|
162
|
+
const requestedModelKey = rowGroupingModel.join(FIELD_SEPARATOR);
|
|
163
|
+
const requestedModel = useMemo<GridRowGroupingModel>(
|
|
164
|
+
() => (requestedModelKey === '' ? [] : requestedModelKey.split(FIELD_SEPARATOR)),
|
|
165
|
+
[requestedModelKey]
|
|
166
|
+
);
|
|
167
|
+
const aggregationModelKey = JSON.stringify(Object.entries(aggregationModel));
|
|
168
|
+
const stableAggregationModel = useMemo<GridAggregationModel>(
|
|
169
|
+
() => Object.fromEntries(JSON.parse(aggregationModelKey) as Array<[string, string]>),
|
|
170
|
+
[aggregationModelKey]
|
|
171
|
+
);
|
|
172
|
+
const hasAggregation = Object.keys(stableAggregationModel).length > 0;
|
|
173
|
+
|
|
59
174
|
const columnsLookup = useMemo(() => {
|
|
60
175
|
const map = new Map<string, GridColDef<R>>();
|
|
61
176
|
(columns ?? []).forEach(col => map.set(col.field, col));
|
|
62
177
|
return map;
|
|
63
178
|
}, [columns]);
|
|
64
179
|
|
|
180
|
+
const isActive = requestedModel.length > 0;
|
|
181
|
+
|
|
182
|
+
// Fields with `groupable: false` are dropped before building, so the remaining levels keep their
|
|
183
|
+
// depths (indent, defaultGroupingExpansionDepth and export depth) as if the field was never listed.
|
|
184
|
+
const groupingFields = useMemo(
|
|
185
|
+
() => requestedModel.filter(field => columnsLookup.get(field)?.groupable !== false),
|
|
186
|
+
[requestedModel, columnsLookup]
|
|
187
|
+
);
|
|
188
|
+
const groupingFieldsKey = groupingFields.join(FIELD_SEPARATOR);
|
|
189
|
+
|
|
65
190
|
// User expand/collapse choices are stored as overrides on top of the depth-based
|
|
66
191
|
// default, keyed by the grouping config. Rebuilding the tree (row edits, new rows
|
|
67
192
|
// arrays) keeps them because group ids are deterministic; changing the grouping
|
|
68
193
|
// config itself discards them.
|
|
69
|
-
const configKey = `${
|
|
194
|
+
const configKey = `${groupingFieldsKey}|${defaultGroupingExpansionDepth}`;
|
|
70
195
|
const [expansionState, setExpansionState] = useState<ExpansionOverrides>(() => ({
|
|
71
196
|
configKey,
|
|
72
197
|
overrides: new Map(),
|
|
73
198
|
}));
|
|
74
199
|
const overrides = expansionState.configKey === configKey ? expansionState.overrides : EMPTY_OVERRIDES;
|
|
75
200
|
|
|
76
|
-
|
|
201
|
+
// Groups hold only the rows that pass the filter, so group subtotals, the "(n)" counts and the set
|
|
202
|
+
// of groups all describe what is shown. With filterMode 'server' the rows already are the result.
|
|
203
|
+
// null = every row passes.
|
|
204
|
+
const matchingRows = useMemo<Set<R> | null>(() => {
|
|
205
|
+
if (!isActive || filterMode === 'server') return null;
|
|
206
|
+
const rowFilter = createRowFilter(filterModel, columnLookup);
|
|
207
|
+
return rowFilter ? new Set(rows.filter(row => rowFilter(row))) : null;
|
|
208
|
+
}, [isActive, rows, filterMode, filterModel, columnLookup]);
|
|
209
|
+
|
|
210
|
+
const build = useMemo<GroupingBuild<R>>(() => {
|
|
77
211
|
const treeNodes = new Map<GridRowId, GridTreeNode>();
|
|
78
|
-
const groupingRows = new Map<GridRowId, R>();
|
|
79
212
|
const rootIds: GridRowId[] = [];
|
|
213
|
+
const groupIds: GridRowId[] = [];
|
|
214
|
+
const groupRowVariants = new Map<GridRowId, GroupRowVariants<R>>();
|
|
215
|
+
const groupingRows = new Map<GridRowId, R>();
|
|
216
|
+
const rowLookup = new Map<GridRowId, R>();
|
|
80
217
|
|
|
81
|
-
if (
|
|
82
|
-
return { treeNodes, rootIds, groupingRows };
|
|
218
|
+
if (!isActive) {
|
|
219
|
+
return { treeNodes, rootIds, groupIds, groupRowVariants, groupingRows, rowLookup };
|
|
83
220
|
}
|
|
84
221
|
|
|
85
|
-
const
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
const groupRows = (
|
|
90
|
-
currentRows: R[],
|
|
91
|
-
depth: number,
|
|
92
|
-
parentId: GridRowId | null
|
|
93
|
-
) => {
|
|
94
|
-
if (depth >= rowGroupingModel.length) {
|
|
95
|
-
|
|
96
|
-
return currentRows.map(row => {
|
|
222
|
+
const groupRows = (currentRows: R[], depth: number, parentId: GridRowId | null): GridRowId[] => {
|
|
223
|
+
if (depth >= groupingFields.length) {
|
|
224
|
+
const leaves = matchingRows ? currentRows.filter(row => matchingRows.has(row)) : currentRows;
|
|
225
|
+
return leaves.map(row => {
|
|
97
226
|
const id = getRowId(row);
|
|
227
|
+
rowLookup.set(id, row);
|
|
98
228
|
treeNodes.set(id, {
|
|
99
229
|
id,
|
|
100
230
|
parentId,
|
|
@@ -106,84 +236,70 @@ export function useRowGrouping<R extends GridRowModel>(params: UseRowGroupingPar
|
|
|
106
236
|
});
|
|
107
237
|
}
|
|
108
238
|
|
|
109
|
-
const field =
|
|
110
|
-
|
|
111
|
-
// Skip fields where the column has explicitly opted out of grouping.
|
|
239
|
+
const field = groupingFields[depth];
|
|
112
240
|
const colDef = columnsLookup.get(field);
|
|
113
|
-
if (colDef && colDef.groupable === false) {
|
|
114
|
-
return groupRows(currentRows, depth + 1, parentId);
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
const groups = new Map<string, R[]>();
|
|
118
241
|
|
|
119
|
-
//
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
const
|
|
123
|
-
|
|
124
|
-
|
|
242
|
+
// Bucket by the value the cell shows (through valueGetter), keyed so distinct values stay apart.
|
|
243
|
+
const buckets = new Map<string, { value: unknown; rows: R[] }>();
|
|
244
|
+
for (const row of currentRows) {
|
|
245
|
+
const value = getCellValue(row, field, colDef as GridColDef | undefined);
|
|
246
|
+
const key = groupKeyOf(value);
|
|
247
|
+
let bucket = buckets.get(key);
|
|
248
|
+
if (!bucket) {
|
|
249
|
+
bucket = { value, rows: [] };
|
|
250
|
+
buckets.set(key, bucket);
|
|
125
251
|
}
|
|
126
|
-
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
const groupIds: GridRowId[] = [];
|
|
130
|
-
|
|
131
|
-
// Create group rows
|
|
132
|
-
groups.forEach((groupRowsList, key) => {
|
|
133
|
-
// Use the raw value from the first row of the group for the groupingValue
|
|
134
|
-
const firstRow = groupRowsList[0];
|
|
135
|
-
const rawValue = firstRow[field];
|
|
252
|
+
bucket.rows.push(row);
|
|
253
|
+
}
|
|
136
254
|
|
|
137
|
-
|
|
255
|
+
const ids: GridRowId[] = [];
|
|
256
|
+
buckets.forEach((bucket, key) => {
|
|
257
|
+
// Group order is the order of first appearance among all rows, so filtering never
|
|
258
|
+
// reorders the groups; a group with no matching row is left out.
|
|
259
|
+
const matching = matchingRows ? bucket.rows.filter(row => matchingRows.has(row)) : bucket.rows;
|
|
260
|
+
if (matching.length === 0) return;
|
|
261
|
+
const groupId = groupIdOf(field, key, parentId);
|
|
262
|
+
ids.push(groupId);
|
|
138
263
|
groupIds.push(groupId);
|
|
139
264
|
|
|
140
|
-
//
|
|
141
|
-
const aggregatedValues: Record<string, unknown> =
|
|
142
|
-
computeAggregations(
|
|
265
|
+
// Aggregated over this group's (filtered) leaf rows.
|
|
266
|
+
const aggregatedValues: Record<string, unknown> = hasAggregation
|
|
267
|
+
? computeAggregations(matching, stableAggregationModel, columnsLookup, 'useRowGrouping')
|
|
268
|
+
: {};
|
|
143
269
|
|
|
144
|
-
const
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
id: groupId
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
groupingRows.set(groupId,
|
|
270
|
+
const inline = { [field]: bucket.value, ...aggregatedValues, id: groupId } as unknown as R;
|
|
271
|
+
const bare = { [field]: bucket.value, id: groupId } as unknown as R;
|
|
272
|
+
const footer = hasAggregation
|
|
273
|
+
? ({ ...aggregatedValues, id: footerIdOf(groupId) } as unknown as R)
|
|
274
|
+
: null;
|
|
275
|
+
groupRowVariants.set(groupId, { inline, bare, footer });
|
|
276
|
+
groupingRows.set(groupId, inline);
|
|
277
|
+
if (footer) groupingRows.set(footer.id, footer);
|
|
151
278
|
|
|
152
279
|
const treeNode: GridTreeNode = {
|
|
153
280
|
id: groupId,
|
|
154
281
|
parentId,
|
|
155
282
|
depth,
|
|
156
|
-
groupingKey:
|
|
283
|
+
groupingKey: String(bucket.value),
|
|
157
284
|
groupingField: field,
|
|
158
|
-
groupingValue:
|
|
285
|
+
groupingValue: bucket.value,
|
|
159
286
|
aggregatedValues,
|
|
160
287
|
isExpanded: false,
|
|
161
|
-
children: [],
|
|
162
|
-
label: colDef
|
|
163
|
-
|
|
164
|
-
: `${field}: ${String(rawValue)}`
|
|
288
|
+
children: [],
|
|
289
|
+
label: formatGroupLabel(colDef, field, bucket.value),
|
|
290
|
+
descendantCount: matching.length,
|
|
165
291
|
};
|
|
166
|
-
|
|
167
|
-
treeNode.aggregationPosition = getAggregationPosition(treeNode);
|
|
168
|
-
|
|
169
292
|
treeNodes.set(groupId, treeNode);
|
|
170
|
-
|
|
171
|
-
const childrenIds = groupRows(groupRowsList, depth + 1, groupId);
|
|
172
|
-
treeNodes.get(groupId)!.children = childrenIds;
|
|
173
|
-
|
|
174
|
-
const descendantCount = groupRowsList.length;
|
|
175
|
-
treeNodes.get(groupId)!.descendantCount = descendantCount;
|
|
293
|
+
treeNode.children = groupRows(bucket.rows, depth + 1, groupId);
|
|
176
294
|
});
|
|
177
|
-
|
|
178
|
-
return groupIds;
|
|
295
|
+
return ids;
|
|
179
296
|
};
|
|
180
297
|
|
|
181
|
-
|
|
182
|
-
rootIds
|
|
183
|
-
|
|
184
|
-
return { treeNodes, rootIds, groupingRows };
|
|
298
|
+
rootIds.push(...groupRows(rows, 0, null));
|
|
299
|
+
return { treeNodes, rootIds, groupIds, groupRowVariants, groupingRows, rowLookup };
|
|
300
|
+
}, [isActive, rows, matchingRows, groupingFields, hasAggregation, stableAggregationModel, getRowId, columnsLookup]);
|
|
185
301
|
|
|
186
|
-
|
|
302
|
+
const { treeNodes, rootIds, groupIds, groupRowVariants, groupingRows, rowLookup } = build;
|
|
187
303
|
|
|
188
304
|
const isDefaultExpanded = useCallback((node: GridTreeNode | undefined) => (
|
|
189
305
|
Boolean(node?.children && node.children.length > 0) &&
|
|
@@ -213,109 +329,129 @@ export function useRowGrouping<R extends GridRowModel>(params: UseRowGroupingPar
|
|
|
213
329
|
return expandedGroupIds.has(id);
|
|
214
330
|
}, [expandedGroupIds]);
|
|
215
331
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
332
|
+
// getAggregationPosition runs once per group per render (it receives the group's current
|
|
333
|
+
// expansion state), but only its answers are memoized on, so an inline callback does not
|
|
334
|
+
// rebuild the rows or the row metadata.
|
|
335
|
+
const positionsKey = hasAggregation
|
|
336
|
+
? groupIds.map(id => {
|
|
337
|
+
const node = treeNodes.get(id);
|
|
338
|
+
return node ? encodePosition(callAggregationPosition(getAggregationPosition, { ...node, isExpanded: expandedGroupIds.has(id) })) : 'i';
|
|
339
|
+
}).join('')
|
|
340
|
+
: '';
|
|
341
|
+
const aggregationPositions = useMemo<Map<GridRowId, GridGroupAggregationPosition>>(() => {
|
|
342
|
+
const map = new Map<GridRowId, GridGroupAggregationPosition>();
|
|
343
|
+
groupIds.forEach((id, index) => map.set(id, decodePosition(positionsKey[index])));
|
|
344
|
+
return map;
|
|
345
|
+
}, [groupIds, positionsKey]);
|
|
346
|
+
|
|
347
|
+
/** Where the grand total goes: `null` hides the grid's aggregation footer. */
|
|
348
|
+
const rootAggregationPosition: GridGroupAggregationPosition = hasAggregation ? callAggregationPosition(getAggregationPosition, null) : 'footer';
|
|
349
|
+
|
|
350
|
+
const isClientSort = sortingMode !== 'server' && Boolean(sortModel && sortModel.length > 0);
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* The rows to render, in order. `labelField` is the column that shows the group labels: sorting
|
|
354
|
+
* by it orders the groups by their grouping value (a group row has no value of its own there).
|
|
355
|
+
*/
|
|
356
|
+
const getVisibleRows = useCallback((options?: GridHierarchyVisibleRowsOptions): R[] | null => {
|
|
357
|
+
if (!isActive) return null;
|
|
358
|
+
const labelField = options?.labelField;
|
|
359
|
+
const expandAll = options?.expandAll ?? false;
|
|
360
|
+
|
|
361
|
+
const activeSortModel = isClientSort && sortModel ? sortModel : [];
|
|
362
|
+
|
|
363
|
+
const readSortValue = (id: GridRowId, sortItem: GridSortItem): GridSortValue => {
|
|
364
|
+
const node = treeNodes.get(id);
|
|
365
|
+
if (node && groupRowVariants.has(id)) {
|
|
366
|
+
const groupingField = node.groupingField ?? '';
|
|
367
|
+
if (sortItem.field === groupingField || sortItem.field === labelField) {
|
|
368
|
+
return { value: node.groupingValue, type: columnLookup?.byField.get(groupingField)?.type };
|
|
369
|
+
}
|
|
370
|
+
if (node.aggregatedValues && Object.prototype.hasOwnProperty.call(node.aggregatedValues, sortItem.field)) {
|
|
371
|
+
return { value: node.aggregatedValues[sortItem.field], type: columnLookup?.byField.get(sortItem.field)?.type };
|
|
236
372
|
}
|
|
237
|
-
return
|
|
373
|
+
return { value: undefined };
|
|
238
374
|
}
|
|
239
|
-
|
|
240
|
-
|
|
375
|
+
const row = rowLookup.get(id);
|
|
376
|
+
const colDef = columnLookup?.byField.get(sortItem.field);
|
|
377
|
+
return { value: row ? getCellValue(row, sortItem.field, colDef) : undefined, type: colDef?.type };
|
|
241
378
|
};
|
|
242
379
|
|
|
243
|
-
const
|
|
244
|
-
|
|
245
|
-
// Copy before sorting: ids may be node.children / rootIds from the memoized tree.
|
|
246
|
-
const visibleIds = filterModel ? ids.filter(doesNodeMatchFilter) : [...ids];
|
|
247
|
-
|
|
248
|
-
if (sortModel && sortModel.length > 0) {
|
|
249
|
-
visibleIds.sort((aId, bId) => {
|
|
250
|
-
const rowA = groupingRows.get(aId) || rowLookup.get(aId);
|
|
251
|
-
const rowB = groupingRows.get(bId) || rowLookup.get(bId);
|
|
252
|
-
|
|
253
|
-
if (!rowA || !rowB) return 0;
|
|
254
|
-
|
|
255
|
-
for (const sortItem of sortModel) {
|
|
256
|
-
const valA = rowA[sortItem.field];
|
|
257
|
-
const valB = rowB[sortItem.field];
|
|
258
|
-
const compareResult = compareValues(valA, valB, sortItem.sort);
|
|
259
|
-
if (compareResult !== 0) return compareResult;
|
|
260
|
-
}
|
|
261
|
-
return 0;
|
|
262
|
-
});
|
|
263
|
-
}
|
|
380
|
+
const result: R[] = [];
|
|
381
|
+
const seenIds = new Set<GridRowId>();
|
|
264
382
|
|
|
265
|
-
|
|
266
|
-
|
|
383
|
+
const traverse = (ids: readonly GridRowId[]) => {
|
|
384
|
+
const ordered = activeSortModel.length > 0 ? sortItemsBySortModel(ids, activeSortModel, readSortValue) : ids;
|
|
385
|
+
for (const id of ordered) {
|
|
386
|
+
if (seenIds.has(id)) continue;
|
|
387
|
+
seenIds.add(id);
|
|
388
|
+
|
|
389
|
+
const variants = groupRowVariants.get(id);
|
|
390
|
+
if (!variants) {
|
|
391
|
+
// Hierarchy info travels in rowMetaMap; the consumer's row object is passed through unchanged.
|
|
392
|
+
const row = rowLookup.get(id);
|
|
393
|
+
if (row) result.push(row);
|
|
394
|
+
continue;
|
|
395
|
+
}
|
|
267
396
|
|
|
268
397
|
const node = treeNodes.get(id);
|
|
269
|
-
const
|
|
270
|
-
|
|
271
|
-
const
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
_groupingField: node.groupingField,
|
|
282
|
-
_groupingValue: node.groupingValue,
|
|
283
|
-
_descendantCount: node.descendantCount
|
|
284
|
-
};
|
|
285
|
-
result.push(enhancedRow);
|
|
286
|
-
|
|
287
|
-
if (expandedGroupIds.has(id) && isGroup) {
|
|
288
|
-
traverse(node.children!);
|
|
289
|
-
}
|
|
398
|
+
const isExpanded = expandedGroupIds.has(id);
|
|
399
|
+
// null is a position ("hidden"), so no `??` default here.
|
|
400
|
+
const position = aggregationPositions.has(id) ? aggregationPositions.get(id) ?? null : 'inline';
|
|
401
|
+
// 'footer' moves the aggregates below the children; while collapsed there is no
|
|
402
|
+
// subtotal row, so they stay on the group row instead of disappearing.
|
|
403
|
+
const showInline = position === 'inline' || (position === 'footer' && !isExpanded);
|
|
404
|
+
result.push(showInline ? variants.inline : variants.bare);
|
|
405
|
+
|
|
406
|
+
if ((isExpanded || expandAll) && node?.children) {
|
|
407
|
+
traverse(node.children);
|
|
408
|
+
// expandAll (exports) walks the data only; exporters write their own subtotals.
|
|
409
|
+
if (isExpanded && !expandAll && position === 'footer' && variants.footer) result.push(variants.footer);
|
|
290
410
|
}
|
|
291
|
-
}
|
|
411
|
+
}
|
|
292
412
|
};
|
|
293
413
|
|
|
294
414
|
traverse(rootIds);
|
|
295
|
-
|
|
296
415
|
return result;
|
|
297
|
-
|
|
298
|
-
}, [rows, getRowId, treeNodes, rootIds, groupingRows, filterModel, sortModel, expandedGroupIds, rowGroupingModel]);
|
|
416
|
+
}, [isActive, isClientSort, sortModel, treeNodes, groupRowVariants, rowLookup, columnLookup, expandedGroupIds, aggregationPositions, rootIds]);
|
|
299
417
|
|
|
300
418
|
const getNode = useCallback((id: GridRowId) => treeNodes.get(id), [treeNodes]);
|
|
301
419
|
|
|
302
420
|
const rowMetaMap = useMemo<Map<GridRowId, GridRowMeta>>(() => {
|
|
303
421
|
const map = new Map<GridRowId, GridRowMeta>();
|
|
304
422
|
treeNodes.forEach((node, id) => {
|
|
305
|
-
const
|
|
306
|
-
|
|
307
|
-
|
|
423
|
+
const variants = groupRowVariants.get(id);
|
|
424
|
+
if (!variants) {
|
|
425
|
+
map.set(id, {
|
|
426
|
+
hasChildren: false,
|
|
427
|
+
treeDepth: node.depth,
|
|
428
|
+
isGroupRow: false,
|
|
429
|
+
});
|
|
430
|
+
return;
|
|
431
|
+
}
|
|
432
|
+
const groupMeta: GridRowMeta = {
|
|
433
|
+
hasChildren: true,
|
|
308
434
|
treeDepth: node.depth,
|
|
309
435
|
groupingField: node.groupingField,
|
|
310
436
|
groupingValue: node.groupingValue,
|
|
311
437
|
groupLabel: node.label,
|
|
312
438
|
descendantCount: node.descendantCount,
|
|
313
|
-
isGroupRow:
|
|
314
|
-
isExpanded:
|
|
315
|
-
}
|
|
439
|
+
isGroupRow: true,
|
|
440
|
+
isExpanded: expandedGroupIds.has(id),
|
|
441
|
+
};
|
|
442
|
+
map.set(id, groupMeta);
|
|
443
|
+
if (variants.footer && aggregationPositions.get(id) === 'footer') {
|
|
444
|
+
map.set(variants.footer.id, {
|
|
445
|
+
...groupMeta,
|
|
446
|
+
hasChildren: false,
|
|
447
|
+
treeDepth: node.depth + 1,
|
|
448
|
+
isExpanded: undefined,
|
|
449
|
+
isGroupFooter: true,
|
|
450
|
+
});
|
|
451
|
+
}
|
|
316
452
|
});
|
|
317
453
|
return map;
|
|
318
|
-
}, [treeNodes, expandedGroupIds]);
|
|
454
|
+
}, [treeNodes, groupRowVariants, expandedGroupIds, aggregationPositions]);
|
|
319
455
|
|
|
320
456
|
return useMemo(() => ({
|
|
321
457
|
treeNodes,
|
|
@@ -325,5 +461,7 @@ export function useRowGrouping<R extends GridRowModel>(params: UseRowGroupingPar
|
|
|
325
461
|
getVisibleRows,
|
|
326
462
|
getNode,
|
|
327
463
|
rowMetaMap,
|
|
328
|
-
|
|
464
|
+
aggregationPositions,
|
|
465
|
+
rootAggregationPosition,
|
|
466
|
+
}), [treeNodes, groupingRows, toggleExpansion, isGroupExpanded, getVisibleRows, getNode, rowMetaMap, aggregationPositions, rootAggregationPosition]);
|
|
329
467
|
}
|