@opencorestack/opengridx 3.7.1 → 3.8.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 +18 -0
- package/dist/opengridx.es.js +4768 -4715
- package/dist/opengridx.es.js.map +1 -1
- package/dist/opengridx.umd.js +15 -15
- package/dist/opengridx.umd.js.map +1 -1
- package/docs/features/filtering.md +2 -0
- package/lib/components/DataGrid/DataGrid.tsx +2 -4
- package/lib/components/DataGrid/GridListView.tsx +1 -1
- package/lib/components/DataGrid/GridPinnedRows.tsx +1 -1
- package/lib/components/DataGrid/GridVirtualRows.tsx +1 -1
- package/lib/hooks/core/useDataGrid.ts +40 -35
- package/lib/hooks/core/useGridAggregationApi.ts +1 -1
- package/lib/hooks/core/useGridCellSelection.ts +3 -4
- package/lib/hooks/core/useGridClipboardPaste.ts +1 -1
- package/lib/hooks/core/useGridDetailPanel.ts +1 -1
- package/lib/hooks/core/useGridFillHandle.ts +1 -1
- package/lib/hooks/core/useGridHierarchy.ts +2 -2
- package/lib/hooks/core/useGridKeyboardNavigation.ts +1 -1
- package/lib/hooks/core/useGridLiveRowSelection.ts +9 -2
- package/lib/hooks/core/useGridRangeEditing.ts +1 -1
- package/lib/hooks/core/useGridRowInteractions.ts +3 -2
- package/lib/hooks/features/useGridSpanning.ts +3 -3
- package/lib/hooks/useRowGrouping.ts +19 -10
- package/lib/hooks/useRowReorder.ts +9 -3
- package/lib/types/state.ts +9 -0
- package/lib/utils/editing/targets.ts +2 -3
- package/lib/utils/gridApiRows.ts +1 -1
- package/lib/utils/grouping/groupedExportRows.ts +1 -1
- package/lib/utils/grouping/layeredMap.ts +52 -0
- package/lib/utils/rowIndex.ts +67 -0
- package/package.json +1 -1
|
@@ -104,7 +104,9 @@ Every operator in `GridFilterOperator` also works in a programmatic `filterModel
|
|
|
104
104
|
|
|
105
105
|
- Only **visible** columns with `filterable !== false` are searched. The row `id` and fields that are not columns are not searched.
|
|
106
106
|
- Each column is searched by its value after `valueGetter` and, when the column has a `valueFormatter`, by the formatted text as well. `Date` values are searched as `YYYY-MM-DD`; objects are skipped.
|
|
107
|
+
- Without a `valueFormatter`, `singleSelect` columns are also searched by their option label, `boolean` columns by Yes / No and `date` columns by the locale date the cell shows (since v3.7.1).
|
|
107
108
|
- The toolbar search box splits what the user types on whitespace, so `john london` becomes `['john', 'london']` and matches a row with first name John and city London.
|
|
109
|
+
- The searched text is cached per row object and column set, so typing does not re-read every cell. Two consequences: change a row by passing a **new** row object (mutating it in place keeps the old text), and when a `valueFormatter` reads state outside the row, pass new column definitions when that state changes.
|
|
108
110
|
|
|
109
111
|
---
|
|
110
112
|
|
|
@@ -208,9 +208,7 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
|
|
|
208
208
|
const isInternalLoading = state.dataSource.loading;
|
|
209
209
|
const effectiveLoading = loading || isInternalLoading;
|
|
210
210
|
|
|
211
|
-
const effectiveRows =
|
|
212
|
-
return state.rows.allRows.map(id => state.rows.idRowsLookup.get(id)!) as R[];
|
|
213
|
-
}, [state.rows]);
|
|
211
|
+
const effectiveRows = state.rows.rows as R[];
|
|
214
212
|
|
|
215
213
|
const { treeDataHandlers, rowGroupingHandlers, activeHierarchyHandlers, rowMetaMap } = useGridHierarchy<R>({
|
|
216
214
|
rows: effectiveRows,
|
|
@@ -354,7 +352,7 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
|
|
|
354
352
|
// Indices address the consumer's own rows, whatever the sort, filter, page or pinning.
|
|
355
353
|
const rowReorderHandlers = useRowReorder({
|
|
356
354
|
rows: effectiveRows,
|
|
357
|
-
|
|
355
|
+
indexById: state.rows.indexById,
|
|
358
356
|
onRowOrderChange,
|
|
359
357
|
rowReordering
|
|
360
358
|
});
|
|
@@ -38,7 +38,7 @@ export interface GridListViewProps<R extends GridRowModel> {
|
|
|
38
38
|
listViewColumn: GridListViewColDef<R>;
|
|
39
39
|
/** Grid columns by field; a list field that names one gets its valueGetter / valueFormatter. */
|
|
40
40
|
columnsByField: ReadonlyMap<string, GridColDef>;
|
|
41
|
-
rowMetaMap:
|
|
41
|
+
rowMetaMap: ReadonlyMap<GridRowId, GridRowMeta>;
|
|
42
42
|
noRowsLabel: string;
|
|
43
43
|
rowHeight: number;
|
|
44
44
|
checkboxSelection: boolean;
|
|
@@ -53,7 +53,7 @@ export interface GridPinnedRowsProps<R extends GridRowModel> {
|
|
|
53
53
|
colspanMap?: Map<GridRowId, Record<string, CellColSpanInfo>>;
|
|
54
54
|
rowSpanningCaches?: RowSpanningCaches;
|
|
55
55
|
rowHeight: number;
|
|
56
|
-
rowMetaMap:
|
|
56
|
+
rowMetaMap: ReadonlyMap<GridRowId, GridRowMeta>;
|
|
57
57
|
/** Resolves a row's id; defaults to `row.id`. */
|
|
58
58
|
getRowId?: (row: R) => GridRowId;
|
|
59
59
|
/** Index of the first of these rows in the grid's renderable rows (top-pinned + centre + bottom-pinned). */
|
|
@@ -69,7 +69,7 @@ export interface GridVirtualRowsProps<R extends GridRowModel> {
|
|
|
69
69
|
unpinnedRowsLength: number;
|
|
70
70
|
/** Last center row index in the render window: skeleton rows render only once it reaches the end of the data. */
|
|
71
71
|
lastRenderedRowIndex: number;
|
|
72
|
-
rowMetaMap:
|
|
72
|
+
rowMetaMap: ReadonlyMap<GridRowId, GridRowMeta>;
|
|
73
73
|
loadingOverlay?: React.ReactNode;
|
|
74
74
|
/** Added to a row's rowIndex to get its `aria-rowindex` (header rows and earlier pages included). */
|
|
75
75
|
ariaRowIndexOffset?: number;
|
|
@@ -41,7 +41,9 @@ let warnedDuplicateIds = false;
|
|
|
41
41
|
function buildRowsState(rows: GridRowModel[], getRowId: GetRowId, knownIds?: Map<GridRowModel, GridRowId>): GridState['rows'] {
|
|
42
42
|
const idRowsLookup = new Map<GridRowId, GridRowModel>();
|
|
43
43
|
const idByRow = new Map<GridRowModel, GridRowId>();
|
|
44
|
+
const indexById = new Map<GridRowId, number>();
|
|
44
45
|
const allRows: GridRowId[] = [];
|
|
46
|
+
const stored: GridRowModel[] = [];
|
|
45
47
|
const duplicates: GridRowId[] = [];
|
|
46
48
|
|
|
47
49
|
rows.forEach(row => {
|
|
@@ -52,7 +54,9 @@ function buildRowsState(rows: GridRowModel[], getRowId: GetRowId, knownIds?: Map
|
|
|
52
54
|
}
|
|
53
55
|
idRowsLookup.set(id, row);
|
|
54
56
|
idByRow.set(row, id);
|
|
57
|
+
indexById.set(id, allRows.length);
|
|
55
58
|
allRows.push(id);
|
|
59
|
+
stored.push(row);
|
|
56
60
|
});
|
|
57
61
|
|
|
58
62
|
if (duplicates.length > 0 && !warnedDuplicateIds && process.env.NODE_ENV !== 'production') {
|
|
@@ -64,7 +68,7 @@ function buildRowsState(rows: GridRowModel[], getRowId: GetRowId, knownIds?: Map
|
|
|
64
68
|
);
|
|
65
69
|
}
|
|
66
70
|
|
|
67
|
-
return { idRowsLookup, allRows, idByRow };
|
|
71
|
+
return { idRowsLookup, allRows, idByRow, rows: stored, indexById };
|
|
68
72
|
}
|
|
69
73
|
|
|
70
74
|
function createInitialState<R extends GridRowModel>(rows: R[], getRowId: GetRowId, columns: GridColDef<R>[], columnVisibilityModel: GridColumnVisibilityModel = {}): GridState {
|
|
@@ -104,10 +108,39 @@ function createInitialState<R extends GridRowModel>(rows: R[], getRowId: GetRowI
|
|
|
104
108
|
};
|
|
105
109
|
}
|
|
106
110
|
|
|
111
|
+
/**
|
|
112
|
+
* Stores committed edits under the ids of the rows that were edited. Edits never add, remove or
|
|
113
|
+
* reorder rows, so the id maps are updated in place (copying them cost ~100 ms each at 1M rows)
|
|
114
|
+
* and keep their identity: `getRowIdOf` and everything memoized on it stay valid. Only the row
|
|
115
|
+
* array is copied, with the edited positions patched. Writing the same entries again is a no-op,
|
|
116
|
+
* so React re-running the reducer (StrictMode, rebasing) gives the same result.
|
|
117
|
+
*/
|
|
118
|
+
function replaceStoredRows(
|
|
119
|
+
state: GridState,
|
|
120
|
+
edits: Iterable<readonly [GridRowId, GridRowModel]>,
|
|
121
|
+
): GridState {
|
|
122
|
+
const { idRowsLookup, idByRow, indexById } = state.rows;
|
|
123
|
+
let rows: GridRowModel[] | null = null;
|
|
124
|
+
for (const [id, row] of edits) {
|
|
125
|
+
const index = indexById.get(id);
|
|
126
|
+
if (index === undefined) continue;
|
|
127
|
+
const previous = idRowsLookup.get(id);
|
|
128
|
+
if (previous !== undefined && previous !== row) idByRow.delete(previous);
|
|
129
|
+
idRowsLookup.set(id, row);
|
|
130
|
+
idByRow.set(row, id);
|
|
131
|
+
if (state.rows.rows[index] !== row) {
|
|
132
|
+
rows ??= state.rows.rows.slice();
|
|
133
|
+
rows[index] = row;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
if (!rows) return state;
|
|
137
|
+
return { ...state, rows: { ...state.rows, rows } };
|
|
138
|
+
}
|
|
139
|
+
|
|
107
140
|
function gridReducer(state: GridState, action: GridAction): GridState {
|
|
108
141
|
switch (action.type) {
|
|
109
142
|
case 'SET_ROWS': {
|
|
110
|
-
const currentRows = state.rows.
|
|
143
|
+
const currentRows = state.rows.rows;
|
|
111
144
|
const newRows = typeof action.payload === 'function'
|
|
112
145
|
? action.payload(currentRows)
|
|
113
146
|
: action.payload;
|
|
@@ -120,40 +153,12 @@ function gridReducer(state: GridState, action: GridAction): GridState {
|
|
|
120
153
|
};
|
|
121
154
|
}
|
|
122
155
|
|
|
123
|
-
case 'REPLACE_ROW':
|
|
124
|
-
|
|
125
|
-
const previous = state.rows.idRowsLookup.get(action.id);
|
|
126
|
-
if (previous === undefined) return state;
|
|
127
|
-
const idRowsLookup = new Map(state.rows.idRowsLookup);
|
|
128
|
-
idRowsLookup.set(action.id, action.row);
|
|
129
|
-
const idByRow = new Map(state.rows.idByRow);
|
|
130
|
-
idByRow.delete(previous);
|
|
131
|
-
idByRow.set(action.row, action.id);
|
|
132
|
-
return {
|
|
133
|
-
...state,
|
|
134
|
-
rows: { ...state.rows, idRowsLookup, idByRow },
|
|
135
|
-
};
|
|
136
|
-
}
|
|
156
|
+
case 'REPLACE_ROW':
|
|
157
|
+
return replaceStoredRows(state, [[action.id, action.row]]);
|
|
137
158
|
|
|
138
|
-
case 'REPLACE_ROWS':
|
|
159
|
+
case 'REPLACE_ROWS':
|
|
139
160
|
// A batch edit (paste, range clear, undo, redo): every stored row in one update.
|
|
140
|
-
|
|
141
|
-
const idRowsLookup = new Map(state.rows.idRowsLookup);
|
|
142
|
-
const idByRow = new Map(state.rows.idByRow);
|
|
143
|
-
for (const [id, row] of action.rows) {
|
|
144
|
-
const previous = idRowsLookup.get(id);
|
|
145
|
-
if (previous === undefined) continue;
|
|
146
|
-
changed = true;
|
|
147
|
-
idRowsLookup.set(id, row);
|
|
148
|
-
idByRow.delete(previous);
|
|
149
|
-
idByRow.set(row, id);
|
|
150
|
-
}
|
|
151
|
-
if (!changed) return state;
|
|
152
|
-
return {
|
|
153
|
-
...state,
|
|
154
|
-
rows: { ...state.rows, idRowsLookup, idByRow },
|
|
155
|
-
};
|
|
156
|
-
}
|
|
161
|
+
return replaceStoredRows(state, action.rows);
|
|
157
162
|
|
|
158
163
|
case 'SET_COLUMNS': {
|
|
159
164
|
const columnLookup = new Map<string, GridColDef>();
|
|
@@ -321,7 +326,7 @@ export function useDataGrid<R extends GridRowModel = GridRowModel>(params: UseDa
|
|
|
321
326
|
const [initialApi] = useState<GridApi>(() => ({
|
|
322
327
|
...createGridApiPlaceholder(),
|
|
323
328
|
getRow: (id: GridRowId) => stateRef.current.rows.idRowsLookup.get(id) || null,
|
|
324
|
-
getAllRows: () => stateRef.current.rows.
|
|
329
|
+
getAllRows: () => stateRef.current.rows.rows.slice(),
|
|
325
330
|
getColumn: (field: string) => stateRef.current.columns.lookup.get(field) || null,
|
|
326
331
|
getAllColumns: () => stateRef.current.columns.all,
|
|
327
332
|
}));
|
|
@@ -25,7 +25,7 @@ export interface UseGridAggregationApiParams<R extends GridRowModel> {
|
|
|
25
25
|
rootAggregationPosition: GridGroupAggregationPosition;
|
|
26
26
|
aggregationPositions: Map<GridRowId, GridGroupAggregationPosition>;
|
|
27
27
|
};
|
|
28
|
-
rowMetaMap:
|
|
28
|
+
rowMetaMap: ReadonlyMap<GridRowId, GridRowMeta>;
|
|
29
29
|
columns: GridColDef<R>[];
|
|
30
30
|
getRowId: (row: GridRowModel) => GridRowId;
|
|
31
31
|
/** The column that shows the group labels, so export order matches the screen when sorting by it. */
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
|
|
2
2
|
import type { RefObject } from 'react';
|
|
3
|
+
import { getRowIndex } from '../../utils/rowIndex';
|
|
3
4
|
import { scrollColumnIntoView, scrollRowIntoView } from '../../utils/scroll';
|
|
4
5
|
import { isRtl } from '../../utils/direction';
|
|
5
6
|
import { isSyntheticRowId } from '../../utils/syntheticRows';
|
|
@@ -76,7 +77,7 @@ export interface UseGridCellSelectionParams<R extends GridRowModel> {
|
|
|
76
77
|
navigationColumns: ReadonlyArray<RangeColumn<R>>;
|
|
77
78
|
columnIndexMap: Map<string, number>;
|
|
78
79
|
spanning: GridSpanningResult;
|
|
79
|
-
rowMetaMap:
|
|
80
|
+
rowMetaMap: ReadonlyMap<GridRowId, GridRowMeta>;
|
|
80
81
|
layout: LayoutResult<R>;
|
|
81
82
|
rowHeight: number;
|
|
82
83
|
pinnedTopRowCount: number;
|
|
@@ -223,9 +224,7 @@ export function useGridCellSelection<R extends GridRowModel>(params: UseGridCell
|
|
|
223
224
|
|
|
224
225
|
const context = useMemo<CellRangeContext | null>(() => {
|
|
225
226
|
if (!enabled) return null;
|
|
226
|
-
const rowIds = allRenderableRows
|
|
227
|
-
const rowIndexById = new Map<GridRowId, number>();
|
|
228
|
-
rowIds.forEach((id, index) => { if (!rowIndexById.has(id)) rowIndexById.set(id, index); });
|
|
227
|
+
const { rowIds, rowIndexById } = getRowIndex(allRenderableRows, getRowId, 'renderable');
|
|
229
228
|
const fields = rangeColumns.map(c => c.field);
|
|
230
229
|
return {
|
|
231
230
|
rowIds,
|
|
@@ -49,7 +49,7 @@ export interface UseGridClipboardPasteParams<R extends GridRowModel> {
|
|
|
49
49
|
/** System columns and the rendered data columns, in render order. */
|
|
50
50
|
navigationColumns: ReadonlyArray<GridColDef<R>>;
|
|
51
51
|
isCellEditable?: (params: GridCellParams<R>) => boolean;
|
|
52
|
-
rowMetaMap:
|
|
52
|
+
rowMetaMap: ReadonlyMap<GridRowId, GridRowMeta>;
|
|
53
53
|
getSpanOrigin?: (rowId: GridRowId, field: string) => { rowId: GridRowId; field: string } | null;
|
|
54
54
|
/** Whether a cell editor is open: paste and Delete then belong to it. */
|
|
55
55
|
isEditing: boolean;
|
|
@@ -9,7 +9,7 @@ export interface UseGridDetailPanelParams {
|
|
|
9
9
|
/** `detailPanelExpandedRowIds` prop (controlled when defined). */
|
|
10
10
|
controlledExpandedRowIds: Set<GridRowId> | undefined;
|
|
11
11
|
onDetailPanelExpandedRowIdsChange: ((expandedRowIds: Set<GridRowId>) => void) | undefined;
|
|
12
|
-
rowMetaMap:
|
|
12
|
+
rowMetaMap: ReadonlyMap<GridRowId, GridRowMeta>;
|
|
13
13
|
}
|
|
14
14
|
|
|
15
15
|
export interface UseGridDetailPanelResult {
|
|
@@ -47,7 +47,7 @@ export interface UseGridFillHandleParams<R extends GridRowModel> {
|
|
|
47
47
|
navigationColumns: ReadonlyArray<GridColDef<R>>;
|
|
48
48
|
columnIndexMap: Map<string, number>;
|
|
49
49
|
isCellEditable?: (params: GridCellParams<R>) => boolean;
|
|
50
|
-
rowMetaMap:
|
|
50
|
+
rowMetaMap: ReadonlyMap<GridRowId, GridRowMeta>;
|
|
51
51
|
getSpanOrigin?: (rowId: GridRowId, field: string) => { rowId: GridRowId; field: string } | null;
|
|
52
52
|
batchEdit: UseGridBatchEditReturn<R>;
|
|
53
53
|
onFill?: (result: GridFillResult) => void;
|
|
@@ -3,7 +3,7 @@ import { useTreeData } from '../useTreeData';
|
|
|
3
3
|
import { useRowGrouping, type UseRowGroupingParams } from '../useRowGrouping';
|
|
4
4
|
import type { GridRowGroupingModel, GridRowId, GridRowMeta, GridRowModel } from '../../types';
|
|
5
5
|
|
|
6
|
-
const EMPTY_ROW_META_MAP:
|
|
6
|
+
const EMPTY_ROW_META_MAP: ReadonlyMap<GridRowId, GridRowMeta> = new Map();
|
|
7
7
|
|
|
8
8
|
export interface UseGridHierarchyParams<R extends GridRowModel> extends Pick<
|
|
9
9
|
UseRowGroupingParams<R>,
|
|
@@ -71,7 +71,7 @@ export function useGridHierarchy<R extends GridRowModel>(params: UseGridHierarch
|
|
|
71
71
|
|
|
72
72
|
const activeHierarchyHandlers = isTreeData ? treeDataHandlers : (isRowGrouping ? rowGroupingHandlers : null);
|
|
73
73
|
|
|
74
|
-
const rowMetaMap = useMemo<
|
|
74
|
+
const rowMetaMap = useMemo<ReadonlyMap<GridRowId, GridRowMeta>>(() => {
|
|
75
75
|
if (isTreeData) return treeDataHandlers.rowMetaMap;
|
|
76
76
|
if (isRowGrouping) return rowGroupingHandlers.rowMetaMap;
|
|
77
77
|
return EMPTY_ROW_META_MAP;
|
|
@@ -71,7 +71,7 @@ export interface UseGridKeyboardNavigationParams<R extends GridRowModel> {
|
|
|
71
71
|
/** Resolves a row's id (getRowId); defaults to `row.id`. */
|
|
72
72
|
getRowId?: (row: R) => GridRowId;
|
|
73
73
|
/** Hierarchy metadata. Synthetic group rows are never edited or selected from the keyboard. */
|
|
74
|
-
rowMetaMap?:
|
|
74
|
+
rowMetaMap?: ReadonlyMap<GridRowId, GridRowMeta>;
|
|
75
75
|
/** Enter on a non-editable cell: the same handler a row click runs. */
|
|
76
76
|
onRowActivate?: (params: GridRowParams<R>) => void;
|
|
77
77
|
/**
|
|
@@ -37,13 +37,20 @@ export function useGridLiveRowSelection(params: UseGridLiveRowSelectionParams):
|
|
|
37
37
|
|
|
38
38
|
const liveRowIds = useMemo(() => {
|
|
39
39
|
if (!enabled || selectedRowIds.size === 0) return selectedRowIds;
|
|
40
|
+
// Check first, copy only when something is stale: select-all at 1M rows selects 1M ids.
|
|
40
41
|
let stale = false;
|
|
42
|
+
for (const id of selectedRowIds) {
|
|
43
|
+
if (!rowsLookup.has(id) && !rowMetaMap.has(id)) {
|
|
44
|
+
stale = true;
|
|
45
|
+
break;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
if (!stale) return selectedRowIds;
|
|
41
49
|
const live = new Set<GridRowId>();
|
|
42
50
|
for (const id of selectedRowIds) {
|
|
43
51
|
if (rowsLookup.has(id) || rowMetaMap.has(id)) live.add(id);
|
|
44
|
-
else stale = true;
|
|
45
52
|
}
|
|
46
|
-
return
|
|
53
|
+
return live;
|
|
47
54
|
}, [enabled, selectedRowIds, rowsLookup, rowMetaMap]);
|
|
48
55
|
|
|
49
56
|
// Latest callback, so an inline onRowSelectionModelChange does not report again on every render.
|
|
@@ -29,7 +29,7 @@ export interface UseGridRangeEditingParams<R extends GridRowModel> {
|
|
|
29
29
|
navigationColumns: GridColDef<R>[];
|
|
30
30
|
columnIndexMap: Map<string, number>;
|
|
31
31
|
isCellEditable?: (params: GridCellParams<R>) => boolean;
|
|
32
|
-
rowMetaMap:
|
|
32
|
+
rowMetaMap: ReadonlyMap<GridRowId, GridRowMeta>;
|
|
33
33
|
getSpanOrigin?: (rowId: GridRowId, field: string) => { rowId: GridRowId; field: string } | null;
|
|
34
34
|
isEditing: boolean;
|
|
35
35
|
batchEdit: UseGridBatchEditReturn<R>;
|
|
@@ -3,6 +3,7 @@ import { useGridRowSelection, type UseGridRowSelectionReturn } from './useGridRo
|
|
|
3
3
|
import { PIVOT_GRAND_TOTAL_ID } from '../../utils/pivot';
|
|
4
4
|
import { isSyntheticRowId } from '../../utils/syntheticRows';
|
|
5
5
|
import type { GridRowId, GridRowMeta, GridRowModel, GridRowParams } from '../../types';
|
|
6
|
+
import { getRowIndex } from '../../utils/rowIndex';
|
|
6
7
|
|
|
7
8
|
export interface UseGridRowInteractionsParams<R extends GridRowModel> {
|
|
8
9
|
/** Data rows that pass the filter (pinned included, synthetic group rows excluded). */
|
|
@@ -14,7 +15,7 @@ export interface UseGridRowInteractionsParams<R extends GridRowModel> {
|
|
|
14
15
|
disableMultipleRowSelection: boolean;
|
|
15
16
|
disableRowSelectionOnClick: boolean;
|
|
16
17
|
isHierarchyEnabled: boolean;
|
|
17
|
-
rowMetaMap:
|
|
18
|
+
rowMetaMap: ReadonlyMap<GridRowId, GridRowMeta>;
|
|
18
19
|
/** The active tree-data / row-grouping handlers, if any. */
|
|
19
20
|
activeHierarchyHandlers: { toggleExpansion: (id: GridRowId) => void } | null;
|
|
20
21
|
onRowClick: ((params: GridRowParams<R>) => void) | undefined;
|
|
@@ -41,7 +42,7 @@ export function useGridRowInteractions<R extends GridRowModel>(
|
|
|
41
42
|
// filter: never rows hidden by it, synthetic group rows, the pivot grand total, or stale
|
|
42
43
|
// ids in the selection.
|
|
43
44
|
const selectableRowIds = useMemo(() => {
|
|
44
|
-
const ids = dataRows
|
|
45
|
+
const { rowIds: ids } = getRowIndex(dataRows, getRowId, 'data');
|
|
45
46
|
return isPivotActive ? ids.filter(id => id !== PIVOT_GRAND_TOTAL_ID) : ids;
|
|
46
47
|
}, [dataRows, getRowId, isPivotActive]);
|
|
47
48
|
const rowSelection = useGridRowSelection({
|
|
@@ -63,7 +63,7 @@ export interface UseGridSpanningParams<R extends GridRowModel> {
|
|
|
63
63
|
rightPinnedColumns: GridColDef<R>[];
|
|
64
64
|
/** Rows whose detail panel is expanded. A row span ends at such a row, above its panel. */
|
|
65
65
|
expandedRowIds?: Set<GridRowId>;
|
|
66
|
-
rowMetaMap?:
|
|
66
|
+
rowMetaMap?: ReadonlyMap<GridRowId, GridRowMeta>;
|
|
67
67
|
/** Resolves a row's id (the grid's getRowId); span caches are keyed by it. Defaults to `row.id`. */
|
|
68
68
|
getRowId?: (row: R) => GridRowId;
|
|
69
69
|
}
|
|
@@ -246,7 +246,7 @@ export function useGridSpanning<R extends GridRowModel>(params: UseGridSpanningP
|
|
|
246
246
|
interface SectionContext<R extends GridRowModel> {
|
|
247
247
|
spanColumns: { columns: SpanColumn<R>[]; leftCount: number; unpinnedCount: number };
|
|
248
248
|
expandedRowIds?: Set<GridRowId>;
|
|
249
|
-
rowMetaMap?:
|
|
249
|
+
rowMetaMap?: ReadonlyMap<GridRowId, GridRowMeta>;
|
|
250
250
|
getRowId: (row: R) => GridRowId;
|
|
251
251
|
warned: Set<string>;
|
|
252
252
|
out: SpanComputation;
|
|
@@ -390,7 +390,7 @@ function buildParams<R extends GridRowModel>(
|
|
|
390
390
|
colDef: GridColDef<R>,
|
|
391
391
|
rowIndex: number,
|
|
392
392
|
colIndex: number,
|
|
393
|
-
rowMetaMap:
|
|
393
|
+
rowMetaMap: ReadonlyMap<GridRowId, GridRowMeta> | undefined,
|
|
394
394
|
warned: Set<string>,
|
|
395
395
|
): GridRenderCellParams<R> {
|
|
396
396
|
const raw = row[colDef.field as keyof R];
|
|
@@ -15,6 +15,7 @@ import { sortItemsBySortModel, type GridSortValue } from '../utils/sorting';
|
|
|
15
15
|
import { callSafely, getCellValue } from '../utils/values';
|
|
16
16
|
import type { GridColumnLookup } from '../utils/columnLookup';
|
|
17
17
|
import { computeAggregations } from '../utils/aggregation';
|
|
18
|
+
import { LayeredMap } from '../utils/grouping/layeredMap';
|
|
18
19
|
|
|
19
20
|
/** Where a group's aggregates are shown: on the group row, on a subtotal row after its children, or not at all. */
|
|
20
21
|
export type GridGroupAggregationPosition = 'inline' | 'footer' | null;
|
|
@@ -436,16 +437,24 @@ export function useRowGrouping<R extends GridRowModel>(params: UseRowGroupingPar
|
|
|
436
437
|
|
|
437
438
|
const getNode = useCallback((id: GridRowId) => treeNodes.get(id), [treeNodes]);
|
|
438
439
|
|
|
439
|
-
|
|
440
|
+
// Leaf entries do not depend on expansion: built once per tree, so expanding or collapsing a
|
|
441
|
+
// group rebuilds only the group entries (a LayeredMap reads both without copying).
|
|
442
|
+
const leafMetaMap = useMemo<Map<GridRowId, GridRowMeta>>(() => {
|
|
440
443
|
const map = new Map<GridRowId, GridRowMeta>();
|
|
441
444
|
treeNodes.forEach((node, id) => {
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
445
|
+
if (groupRowVariants.has(id)) return;
|
|
446
|
+
// One object per row (not shared per depth): `params.rowMeta` is public, and a consumer that
|
|
447
|
+
// writes to it must not change other rows.
|
|
448
|
+
map.set(id, { hasChildren: false, treeDepth: node.depth, isGroupRow: false });
|
|
449
|
+
});
|
|
450
|
+
return map;
|
|
451
|
+
}, [treeNodes, groupRowVariants]);
|
|
452
|
+
|
|
453
|
+
const rowMetaMap = useMemo<ReadonlyMap<GridRowId, GridRowMeta>>(() => {
|
|
454
|
+
const map = new Map<GridRowId, GridRowMeta>();
|
|
455
|
+
groupRowVariants.forEach((variants, id) => {
|
|
456
|
+
const node = treeNodes.get(id);
|
|
457
|
+
if (!node) return;
|
|
449
458
|
const groupMeta: GridRowMeta = {
|
|
450
459
|
hasChildren: true,
|
|
451
460
|
treeDepth: node.depth,
|
|
@@ -467,8 +476,8 @@ export function useRowGrouping<R extends GridRowModel>(params: UseRowGroupingPar
|
|
|
467
476
|
});
|
|
468
477
|
}
|
|
469
478
|
});
|
|
470
|
-
return map;
|
|
471
|
-
}, [treeNodes, groupRowVariants, expandedGroupIds, aggregationPositions]);
|
|
479
|
+
return new LayeredMap(leafMetaMap, map);
|
|
480
|
+
}, [leafMetaMap, treeNodes, groupRowVariants, expandedGroupIds, aggregationPositions]);
|
|
472
481
|
|
|
473
482
|
return useMemo(() => ({
|
|
474
483
|
treeNodes,
|
|
@@ -11,6 +11,11 @@ export interface UseRowReorderProps<R extends GridRowModel = GridRowModel> {
|
|
|
11
11
|
*/
|
|
12
12
|
rows: R[];
|
|
13
13
|
getRowId?: (row: R) => GridRowId;
|
|
14
|
+
/**
|
|
15
|
+
* Row id → index in `rows`, when the caller already has it (the grid's row store does). Saves
|
|
16
|
+
* a pass over every row each time `rows` changes, such as after every cell edit.
|
|
17
|
+
*/
|
|
18
|
+
indexById?: ReadonlyMap<GridRowId, number>;
|
|
14
19
|
rowReordering?: boolean;
|
|
15
20
|
}
|
|
16
21
|
|
|
@@ -28,16 +33,17 @@ export interface UseRowReorderReturn {
|
|
|
28
33
|
export function useRowReorder<R extends GridRowModel = GridRowModel>(
|
|
29
34
|
props: UseRowReorderProps<R>
|
|
30
35
|
): UseRowReorderReturn {
|
|
31
|
-
const { onRowOrderChange, rows, getRowId, rowReordering = false } = props;
|
|
36
|
+
const { onRowOrderChange, rows, getRowId, indexById: givenIndexById, rowReordering = false } = props;
|
|
32
37
|
const [draggedRowId, setDraggedRowId] = useState<GridRowId | null>(null);
|
|
33
38
|
const [dragOverRowId, setDragOverRowId] = useState<GridRowId | null>(null);
|
|
34
39
|
const detachDragEndRef = useRef<() => void>(() => {});
|
|
35
40
|
|
|
36
|
-
const indexById = useMemo(() => {
|
|
41
|
+
const indexById = useMemo<ReadonlyMap<GridRowId, number>>(() => {
|
|
42
|
+
if (givenIndexById) return givenIndexById;
|
|
37
43
|
const map = new Map<GridRowId, number>();
|
|
38
44
|
rows.forEach((row, index) => map.set(getRowId ? getRowId(row) : row.id, index));
|
|
39
45
|
return map;
|
|
40
|
-
}, [rows, getRowId]);
|
|
46
|
+
}, [givenIndexById, rows, getRowId]);
|
|
41
47
|
|
|
42
48
|
const canReorderRow = useCallback((id: GridRowId) => indexById.has(id), [indexById]);
|
|
43
49
|
|
package/lib/types/state.ts
CHANGED
|
@@ -2,11 +2,20 @@ import type { GridRowId, GridRowModel, GridPinnedRows } from './rows';
|
|
|
2
2
|
import type { GridColDef, GridColumnVisibilityModel, GridPinnedColumns } from './columns';
|
|
3
3
|
|
|
4
4
|
export interface GridInternalState {
|
|
5
|
+
/**
|
|
6
|
+
* The row store. Cell edits (`REPLACE_ROW` / `REPLACE_ROWS`) update `idRowsLookup`, `idByRow`
|
|
7
|
+
* and `indexById` IN PLACE and only replace `rows`: never rely on those maps changing identity
|
|
8
|
+
* to detect an edit; depend on `rows` (or `state.rows`) instead. `SET_ROWS` builds new maps.
|
|
9
|
+
*/
|
|
5
10
|
rows: {
|
|
6
11
|
idRowsLookup: Map<GridRowId, GridRowModel>;
|
|
7
12
|
allRows: GridRowId[];
|
|
8
13
|
/** Row object → its getRowId() key. Rows are stored untouched, so this is how ids are resolved. */
|
|
9
14
|
idByRow: Map<GridRowModel, GridRowId>;
|
|
15
|
+
/** The stored rows in `allRows` order. */
|
|
16
|
+
rows: GridRowModel[];
|
|
17
|
+
/** Row id → its index in `allRows` / `rows`. */
|
|
18
|
+
indexById: Map<GridRowId, number>;
|
|
10
19
|
};
|
|
11
20
|
pagination: {
|
|
12
21
|
/** Server-reported total (dataSource responses); undefined until one arrives. */
|
|
@@ -5,6 +5,7 @@ import { isSystemField } from '../focus';
|
|
|
5
5
|
import { isSyntheticRowId } from '../syntheticRows';
|
|
6
6
|
import { resolveCellEditable } from './index';
|
|
7
7
|
import { getCellValue } from '../values';
|
|
8
|
+
import { getRowIndex } from '../rowIndex';
|
|
8
9
|
|
|
9
10
|
/**
|
|
10
11
|
* Where batch edits (paste, range clear, undo/redo) land: the renderable rows and the range columns
|
|
@@ -26,9 +27,7 @@ export function buildEditTargetContext<R extends GridRowModel>(
|
|
|
26
27
|
getRowId: (row: R) => GridRowId,
|
|
27
28
|
navigationColumns: ReadonlyArray<GridColDef<R>>,
|
|
28
29
|
): GridEditTargetContext<R> {
|
|
29
|
-
const rowIds = rows
|
|
30
|
-
const rowIndexById = new Map<GridRowId, number>();
|
|
31
|
-
rowIds.forEach((id, index) => { if (!rowIndexById.has(id)) rowIndexById.set(id, index); });
|
|
30
|
+
const { rowIds, rowIndexById } = getRowIndex(rows, getRowId, 'renderable');
|
|
32
31
|
const columns = navigationColumns.filter(c => isRangeColumnField(c.field));
|
|
33
32
|
const colIndexByField = new Map<string, number>();
|
|
34
33
|
columns.forEach((c, index) => { colIndexByField.set(c.field, index); });
|
package/lib/utils/gridApiRows.ts
CHANGED
|
@@ -9,7 +9,7 @@ export interface CollectAllFilteredRowsParams<R extends GridRowModel> {
|
|
|
9
9
|
/** Every unpinned row that passes the filter, sorted, across all pages. */
|
|
10
10
|
sortedUnpinnedRows: R[];
|
|
11
11
|
pinnedBottomRows: R[];
|
|
12
|
-
rowMetaMap:
|
|
12
|
+
rowMetaMap: ReadonlyMap<GridRowId, GridRowMeta>;
|
|
13
13
|
getRowId: (row: GridRowModel) => GridRowId;
|
|
14
14
|
/**
|
|
15
15
|
* With a hierarchy: the rows that pass the filter (the set select-all and the footer use).
|
|
@@ -16,7 +16,7 @@ export interface BuildGroupedExportRowsParams<R extends GridRowModel> {
|
|
|
16
16
|
*/
|
|
17
17
|
rows: R[];
|
|
18
18
|
getRowId: (row: R) => GridRowId;
|
|
19
|
-
rowMetaMap:
|
|
19
|
+
rowMetaMap: ReadonlyMap<GridRowId, GridRowMeta>;
|
|
20
20
|
columns: GridColDef<R>[];
|
|
21
21
|
aggregationModel: GridAggregationModel;
|
|
22
22
|
/** The footer's grand total (already over the filtered data rows); omitted when null. */
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A read-only view of two maps with disjoint keys, read as one: `top` first, then `base`. Row
|
|
3
|
+
* grouping keeps one entry per leaf row in `base` (built once per tree) and the group rows in
|
|
4
|
+
* `top` (rebuilt on every expand / collapse), so toggling a group at 1M rows does not copy a
|
|
5
|
+
* 1M-entry map. Iteration yields `base` entries, then `top` entries.
|
|
6
|
+
*/
|
|
7
|
+
export class LayeredMap<K, V> implements ReadonlyMap<K, V> {
|
|
8
|
+
private readonly base: ReadonlyMap<K, V>;
|
|
9
|
+
private readonly top: ReadonlyMap<K, V>;
|
|
10
|
+
|
|
11
|
+
constructor(base: ReadonlyMap<K, V>, top: ReadonlyMap<K, V>) {
|
|
12
|
+
this.base = base;
|
|
13
|
+
this.top = top;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
get size(): number {
|
|
17
|
+
return this.base.size + this.top.size;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
get(key: K): V | undefined {
|
|
21
|
+
const value = this.top.get(key);
|
|
22
|
+
return value !== undefined || this.top.has(key) ? value : this.base.get(key);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
has(key: K): boolean {
|
|
26
|
+
return this.top.has(key) || this.base.has(key);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
forEach(callback: (value: V, key: K, map: ReadonlyMap<K, V>) => void, thisArg?: unknown): void {
|
|
30
|
+
this.base.forEach((value, key) => callback.call(thisArg, value, key, this));
|
|
31
|
+
this.top.forEach((value, key) => callback.call(thisArg, value, key, this));
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
*entries(): MapIterator<[K, V]> {
|
|
35
|
+
yield* this.base.entries();
|
|
36
|
+
yield* this.top.entries();
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
*keys(): MapIterator<K> {
|
|
40
|
+
yield* this.base.keys();
|
|
41
|
+
yield* this.top.keys();
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
*values(): MapIterator<V> {
|
|
45
|
+
yield* this.base.values();
|
|
46
|
+
yield* this.top.values();
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
[Symbol.iterator](): MapIterator<[K, V]> {
|
|
50
|
+
return this.entries();
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import type { GridRowId } from '../types';
|
|
2
|
+
|
|
3
|
+
/** The ids of a row array in order, and each id's first index. */
|
|
4
|
+
export interface GridRowIndex {
|
|
5
|
+
rowIds: GridRowId[];
|
|
6
|
+
rowIndexById: Map<GridRowId, number>;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
interface CacheEntry {
|
|
10
|
+
rows: readonly object[];
|
|
11
|
+
value: GridRowIndex;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// Last result per id resolver and view. The grid's resolver keeps its identity while the row store
|
|
15
|
+
// keeps its id maps (cell edits do), so the entry lives exactly as long as it can be reused.
|
|
16
|
+
const cache = new WeakMap<(row: never) => GridRowId, Map<string, CacheEntry>>();
|
|
17
|
+
|
|
18
|
+
function build<R extends object>(rows: readonly R[], getRowId: (row: R) => GridRowId): GridRowIndex {
|
|
19
|
+
const rowIds = new Array<GridRowId>(rows.length);
|
|
20
|
+
const rowIndexById = new Map<GridRowId, number>();
|
|
21
|
+
for (let index = 0; index < rows.length; index++) {
|
|
22
|
+
const id = getRowId(rows[index]);
|
|
23
|
+
rowIds[index] = id;
|
|
24
|
+
if (!rowIndexById.has(id)) rowIndexById.set(id, index);
|
|
25
|
+
}
|
|
26
|
+
return { rowIds, rowIndexById };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* `rows.map(getRowId)` plus an id → first index map, reusing the previous result for the same
|
|
31
|
+
* resolver and `view` when the ids are unchanged position by position. A cell edit replaces row
|
|
32
|
+
* objects but never moves or re-keys rows, so after an edit only the replaced positions are
|
|
33
|
+
* resolved (a reference comparison per row instead of a map lookup per row, ~20× cheaper at 1M
|
|
34
|
+
* rows), and callers memoized on the returned object do not recompute. Treat the result as
|
|
35
|
+
* read-only: it may be shared.
|
|
36
|
+
*/
|
|
37
|
+
export function getRowIndex<R extends object>(
|
|
38
|
+
rows: readonly R[],
|
|
39
|
+
getRowId: (row: R) => GridRowId,
|
|
40
|
+
view: string,
|
|
41
|
+
): GridRowIndex {
|
|
42
|
+
let byView = cache.get(getRowId);
|
|
43
|
+
const previous = byView?.get(view);
|
|
44
|
+
if (previous && previous.rows.length === rows.length) {
|
|
45
|
+
const prevRows = previous.rows as readonly R[];
|
|
46
|
+
const prevIds = previous.value.rowIds;
|
|
47
|
+
let same = true;
|
|
48
|
+
for (let index = 0; index < rows.length; index++) {
|
|
49
|
+
const row = rows[index];
|
|
50
|
+
if (row !== prevRows[index] && getRowId(row) !== prevIds[index]) {
|
|
51
|
+
same = false;
|
|
52
|
+
break;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
if (same) {
|
|
56
|
+
previous.rows = rows;
|
|
57
|
+
return previous.value;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
const value = build(rows, getRowId);
|
|
61
|
+
if (!byView) {
|
|
62
|
+
byView = new Map();
|
|
63
|
+
cache.set(getRowId, byView);
|
|
64
|
+
}
|
|
65
|
+
byView.set(view, { rows, value });
|
|
66
|
+
return value;
|
|
67
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@opencorestack/opengridx",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.8.0",
|
|
4
4
|
"description": "OpenGridX: High-performance React data infrastructure. Unlock advanced Row Grouping, Excel Export, and Column Pinning without the usual \"Pro\" gatekeeping. Built for speed, scale, and complete architectural freedom. Fully open, virtualization-ready, and feature-complete.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/opengridx.umd.js",
|