@opencorestack/opengridx 3.7.0 → 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 +40 -0
- package/dist/ai.cjs +3 -3
- package/dist/ai.cjs.map +1 -1
- package/dist/ai.es.js +3 -3
- package/dist/ai.es.js.map +1 -1
- package/dist/opengridx.css +1 -1
- package/dist/opengridx.es.js +4982 -4853
- 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.css +0 -3
- 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/components/ListView/ListView.css +4 -4
- 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/core/useLayout.ts +34 -24
- package/lib/hooks/features/useGridSpanning.ts +3 -3
- package/lib/hooks/useRowGrouping.ts +19 -10
- package/lib/hooks/useRowReorder.ts +9 -3
- package/lib/styles/base/reset.css +5 -3
- package/lib/types/state.ts +9 -0
- package/lib/utils/editing/targets.ts +2 -3
- package/lib/utils/export/exportShared.ts +22 -4
- package/lib/utils/export/index.ts +8 -4
- package/lib/utils/filtering/index.ts +8 -2
- 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/parsing.ts +24 -1
- package/lib/utils/rowIndex.ts +67 -0
- package/lib/utils/values.ts +31 -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
|
|
|
@@ -19,9 +19,6 @@
|
|
|
19
19
|
border-bottom-left-radius: max(0px, calc(var(--ogx-grid-border-radius, var(--ogx-border-radius-lg)) - var(--ogx-grid-border-width)));
|
|
20
20
|
border-bottom-right-radius: max(0px, calc(var(--ogx-grid-border-radius, var(--ogx-border-radius-lg)) - var(--ogx-grid-border-width)));
|
|
21
21
|
}
|
|
22
|
-
.ogx:hover {
|
|
23
|
-
box-shadow: var(--ogx-shadow-md);
|
|
24
|
-
}
|
|
25
22
|
.ogx__viewport {
|
|
26
23
|
flex: 1;
|
|
27
24
|
overflow: auto;
|
|
@@ -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;
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
.ogx-list-view__row:hover {
|
|
31
|
-
background: var(--ogx-
|
|
31
|
+
background: var(--ogx-grid-row-hover-background);
|
|
32
32
|
}
|
|
33
33
|
|
|
34
34
|
/* Keyboard focus (roving tab stop between list rows). */
|
|
@@ -41,11 +41,11 @@
|
|
|
41
41
|
}
|
|
42
42
|
|
|
43
43
|
.ogx-list-view__row--selected {
|
|
44
|
-
background: var(--ogx-selection-bg,
|
|
44
|
+
background: var(--ogx-selection-bg, var(--ogx-grid-row-selected-background));
|
|
45
45
|
}
|
|
46
46
|
|
|
47
47
|
.ogx-list-view__row--selected:hover {
|
|
48
|
-
background: var(--ogx-selection-hover-bg,
|
|
48
|
+
background: var(--ogx-selection-hover-bg, var(--ogx-grid-row-selected-hover-background));
|
|
49
49
|
}
|
|
50
50
|
|
|
51
51
|
/* ─── Checkbox column inside list row ────────────────────────────────────── */
|
|
@@ -79,7 +79,7 @@
|
|
|
79
79
|
min-width: 0;
|
|
80
80
|
font-family: var(--ogx-font-family);
|
|
81
81
|
font-size: var(--ogx-font-size-sm);
|
|
82
|
-
color: var(--ogx-grid-
|
|
82
|
+
color: var(--ogx-grid-row-text);
|
|
83
83
|
}
|
|
84
84
|
|
|
85
85
|
/* ─── Toolbar in list view mode ──────────────────────────────────────────── */
|
|
@@ -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({
|
|
@@ -245,7 +245,9 @@ export function useLayout<R extends GridRowModel>(params: UseLayoutParams<R>): L
|
|
|
245
245
|
getRowId = defaultGetRowId,
|
|
246
246
|
} = params;
|
|
247
247
|
|
|
248
|
-
|
|
248
|
+
// Rows and columns are measured in separate memos: a column resize must not walk every row
|
|
249
|
+
// (1M rows), and a sort or filter must not recompute column widths.
|
|
250
|
+
const rowLayout = useMemo(() => {
|
|
249
251
|
const unpinnedRows = pagination ? paginatedUnpinnedRows : sortedUnpinnedRows;
|
|
250
252
|
|
|
251
253
|
const heightOf = (row: R, rowIndex: number) => {
|
|
@@ -279,6 +281,35 @@ export function useLayout<R extends GridRowModel>(params: UseLayoutParams<R>): L
|
|
|
279
281
|
const pinnedTopHeight = pinnedBlockHeight(pinnedTopRows, pinnedTopRowsLength);
|
|
280
282
|
const pinnedBottomHeight = pinnedBlockHeight(pinnedBottomRows, pinnedBottomRowsLength);
|
|
281
283
|
|
|
284
|
+
return {
|
|
285
|
+
rowHeights,
|
|
286
|
+
cumulativeHeights,
|
|
287
|
+
unpinnedRowsHeight,
|
|
288
|
+
pinnedTopHeight,
|
|
289
|
+
pinnedBottomHeight,
|
|
290
|
+
pinnedTopRowsLength,
|
|
291
|
+
pinnedBottomRowsLength,
|
|
292
|
+
unpinnedRowsLength: rowCount,
|
|
293
|
+
};
|
|
294
|
+
}, [
|
|
295
|
+
rowHeight,
|
|
296
|
+
pagination,
|
|
297
|
+
paginatedUnpinnedRows,
|
|
298
|
+
sortedUnpinnedRows,
|
|
299
|
+
expandedRowIds,
|
|
300
|
+
getDetailPanelHeight,
|
|
301
|
+
detailPanelHeights,
|
|
302
|
+
pinnedTopRowsLength,
|
|
303
|
+
pinnedBottomRowsLength,
|
|
304
|
+
pinnedTopRows,
|
|
305
|
+
pinnedBottomRows,
|
|
306
|
+
paginationMode,
|
|
307
|
+
isLoading,
|
|
308
|
+
pageSize,
|
|
309
|
+
getRowId,
|
|
310
|
+
]);
|
|
311
|
+
|
|
312
|
+
const columnLayout = useMemo(() => {
|
|
282
313
|
// Pinned columns render in pinnedColumns.left / .right order; unpinned ones in column order.
|
|
283
314
|
const { left: rawLeftPinnedCols, center: unpinnedCols, right: rawRightPinnedCols } =
|
|
284
315
|
getPinnedColumnGroups(visibleOrderedColumns, pinnedColumns);
|
|
@@ -326,11 +357,6 @@ export function useLayout<R extends GridRowModel>(params: UseLayoutParams<R>): L
|
|
|
326
357
|
}, [] as number[]);
|
|
327
358
|
|
|
328
359
|
return {
|
|
329
|
-
rowHeights,
|
|
330
|
-
cumulativeHeights,
|
|
331
|
-
unpinnedRowsHeight,
|
|
332
|
-
pinnedTopHeight,
|
|
333
|
-
pinnedBottomHeight,
|
|
334
360
|
leftPinnedCols,
|
|
335
361
|
rightPinnedCols,
|
|
336
362
|
unpinnedColsWithWidth,
|
|
@@ -342,22 +368,8 @@ export function useLayout<R extends GridRowModel>(params: UseLayoutParams<R>): L
|
|
|
342
368
|
unpinnedTotalWidth,
|
|
343
369
|
systemColumnsWidth,
|
|
344
370
|
pinnedSystemColumnsWidth,
|
|
345
|
-
pinnedTopRowsLength,
|
|
346
|
-
pinnedBottomRowsLength,
|
|
347
|
-
unpinnedRowsLength: unpinnedRows.length,
|
|
348
371
|
};
|
|
349
372
|
}, [
|
|
350
|
-
rowHeight,
|
|
351
|
-
pagination,
|
|
352
|
-
paginatedUnpinnedRows,
|
|
353
|
-
sortedUnpinnedRows,
|
|
354
|
-
expandedRowIds,
|
|
355
|
-
getDetailPanelHeight,
|
|
356
|
-
detailPanelHeights,
|
|
357
|
-
pinnedTopRowsLength,
|
|
358
|
-
pinnedBottomRowsLength,
|
|
359
|
-
pinnedTopRows,
|
|
360
|
-
pinnedBottomRows,
|
|
361
373
|
visibleOrderedColumns,
|
|
362
374
|
pinnedColumns,
|
|
363
375
|
columnWidths,
|
|
@@ -367,9 +379,7 @@ export function useLayout<R extends GridRowModel>(params: UseLayoutParams<R>): L
|
|
|
367
379
|
rowReordering,
|
|
368
380
|
pinCheckboxColumn,
|
|
369
381
|
pinExpandColumn,
|
|
370
|
-
paginationMode,
|
|
371
|
-
isLoading,
|
|
372
|
-
pageSize,
|
|
373
|
-
getRowId,
|
|
374
382
|
]);
|
|
383
|
+
|
|
384
|
+
return useMemo<LayoutResult<R>>(() => ({ ...rowLayout, ...columnLayout }), [rowLayout, columnLayout]);
|
|
375
385
|
}
|
|
@@ -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
|
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
/* Only the grid's own elements, including its portalled menus and panels (all carry an ogx class).
|
|
2
|
+
:where() keeps the specificity at zero, so a consumer's own box-sizing rule still wins. */
|
|
3
|
+
:where([class*="ogx"], [class*="ogx"] *),
|
|
4
|
+
:where([class*="ogx"], [class*="ogx"] *)::before,
|
|
5
|
+
:where([class*="ogx"], [class*="ogx"] *)::after {
|
|
4
6
|
box-sizing: border-box;
|
|
5
7
|
}
|
|
6
8
|
.ogx--dragging {
|
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); });
|