@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.
Files changed (45) hide show
  1. package/CHANGELOG.md +40 -0
  2. package/dist/ai.cjs +3 -3
  3. package/dist/ai.cjs.map +1 -1
  4. package/dist/ai.es.js +3 -3
  5. package/dist/ai.es.js.map +1 -1
  6. package/dist/opengridx.css +1 -1
  7. package/dist/opengridx.es.js +4982 -4853
  8. package/dist/opengridx.es.js.map +1 -1
  9. package/dist/opengridx.umd.js +15 -15
  10. package/dist/opengridx.umd.js.map +1 -1
  11. package/docs/features/filtering.md +2 -0
  12. package/lib/components/DataGrid/DataGrid.css +0 -3
  13. package/lib/components/DataGrid/DataGrid.tsx +2 -4
  14. package/lib/components/DataGrid/GridListView.tsx +1 -1
  15. package/lib/components/DataGrid/GridPinnedRows.tsx +1 -1
  16. package/lib/components/DataGrid/GridVirtualRows.tsx +1 -1
  17. package/lib/components/ListView/ListView.css +4 -4
  18. package/lib/hooks/core/useDataGrid.ts +40 -35
  19. package/lib/hooks/core/useGridAggregationApi.ts +1 -1
  20. package/lib/hooks/core/useGridCellSelection.ts +3 -4
  21. package/lib/hooks/core/useGridClipboardPaste.ts +1 -1
  22. package/lib/hooks/core/useGridDetailPanel.ts +1 -1
  23. package/lib/hooks/core/useGridFillHandle.ts +1 -1
  24. package/lib/hooks/core/useGridHierarchy.ts +2 -2
  25. package/lib/hooks/core/useGridKeyboardNavigation.ts +1 -1
  26. package/lib/hooks/core/useGridLiveRowSelection.ts +9 -2
  27. package/lib/hooks/core/useGridRangeEditing.ts +1 -1
  28. package/lib/hooks/core/useGridRowInteractions.ts +3 -2
  29. package/lib/hooks/core/useLayout.ts +34 -24
  30. package/lib/hooks/features/useGridSpanning.ts +3 -3
  31. package/lib/hooks/useRowGrouping.ts +19 -10
  32. package/lib/hooks/useRowReorder.ts +9 -3
  33. package/lib/styles/base/reset.css +5 -3
  34. package/lib/types/state.ts +9 -0
  35. package/lib/utils/editing/targets.ts +2 -3
  36. package/lib/utils/export/exportShared.ts +22 -4
  37. package/lib/utils/export/index.ts +8 -4
  38. package/lib/utils/filtering/index.ts +8 -2
  39. package/lib/utils/gridApiRows.ts +1 -1
  40. package/lib/utils/grouping/groupedExportRows.ts +1 -1
  41. package/lib/utils/grouping/layeredMap.ts +52 -0
  42. package/lib/utils/parsing.ts +24 -1
  43. package/lib/utils/rowIndex.ts +67 -0
  44. package/lib/utils/values.ts +31 -0
  45. 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 = useMemo(() => {
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
- getRowId: getRowIdOf,
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: Map<GridRowId, GridRowMeta>;
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: Map<GridRowId, GridRowMeta>;
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: Map<GridRowId, GridRowMeta>;
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-color-gray-50, #f8fafc);
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, rgba(59, 130, 246, 0.08));
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, rgba(59, 130, 246, 0.13));
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-cell-text);
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.allRows.map(id => state.rows.idRowsLookup.get(id)!);
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
- // Stores a committed edit under the id of the row that was edited, in the current store.
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
- let changed = false;
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.allRows.map(id => stateRef.current.rows.idRowsLookup.get(id)!),
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: Map<GridRowId, GridRowMeta>;
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: Map<GridRowId, GridRowMeta>;
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.map(getRowId);
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: Map<GridRowId, GridRowMeta>;
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: Map<GridRowId, GridRowMeta>;
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: Map<GridRowId, GridRowMeta>;
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: Map<GridRowId, GridRowMeta> = new 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<Map<GridRowId, GridRowMeta>>(() => {
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?: Map<GridRowId, GridRowMeta>;
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 stale ? live : selectedRowIds;
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: Map<GridRowId, GridRowMeta>;
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: Map<GridRowId, GridRowMeta>;
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.map(getRowId);
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
- return useMemo<LayoutResult<R>>(() => {
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?: Map<GridRowId, GridRowMeta>;
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?: Map<GridRowId, GridRowMeta>;
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: Map<GridRowId, GridRowMeta> | undefined,
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
- const rowMetaMap = useMemo<Map<GridRowId, GridRowMeta>>(() => {
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
- const variants = groupRowVariants.get(id);
443
- if (!variants) {
444
- // One object per row (not shared per depth): `params.rowMeta` is public, and a consumer that
445
- // writes to it must not change other rows.
446
- map.set(id, { hasChildren: false, treeDepth: node.depth, isGroupRow: false });
447
- return;
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
- *::before,
3
- *::after {
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 {
@@ -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.map(getRowId);
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); });