@opencorestack/opengridx 3.7.1 → 3.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +41 -0
- package/dist/ai.cjs.map +1 -1
- package/dist/ai.es.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/opengridx.css +1 -1
- package/dist/opengridx.es.js +6043 -5979
- package/dist/opengridx.es.js.map +1 -1
- package/dist/opengridx.umd.js +16 -16
- package/dist/opengridx.umd.js.map +1 -1
- package/docs/API_REFERENCE.md +28 -0
- package/docs/features/filtering.md +2 -0
- package/lib/components/AiAssistant/GridAiAssistantPanel.css +2 -2
- package/lib/components/Cell/Cell.css +1 -3
- package/lib/components/ColumnGroupHeader/ColumnGroupHeader.css +1 -2
- package/lib/components/DataGrid/DataGrid.css +1 -28
- 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 +3 -2
- package/lib/components/FilterPanel/FilterPanel.tsx +7 -39
- package/lib/components/Header/Header.css +2 -11
- package/lib/components/HeaderFilter/HeaderFilterCell.tsx +6 -37
- package/lib/components/ListView/ListView.css +0 -16
- package/lib/components/Row/Row.tsx +4 -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 +4 -4
- package/lib/hooks/useRowGrouping.ts +19 -10
- package/lib/hooks/useRowReorder.ts +9 -3
- package/lib/styles/base/reset.css +6 -7
- package/lib/theme/__baseline__/default-theme.darwin.chromium.json +1 -1503
- package/lib/theme/__baseline__/default-theme.darwin.firefox.json +1 -1503
- package/lib/theme/__baseline__/default-theme.darwin.webkit.json +1 -1503
- package/lib/types/rows.ts +0 -2
- package/lib/types/state.ts +9 -0
- package/lib/utils/cellSelection.ts +0 -2
- package/lib/utils/editing/index.ts +1 -1
- package/lib/utils/editing/targets.ts +2 -3
- package/lib/utils/filtering/filterInput.ts +45 -0
- package/lib/utils/filtering/index.ts +0 -28
- 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/pinning/index.ts +0 -107
- package/lib/utils/rowIndex.ts +67 -0
- package/lib/utils/sorting/index.ts +42 -33
- package/lib/utils/values.ts +5 -1
- package/llms.txt +7 -3
- package/package.json +1 -1
- package/lib/theme/defaultStyles.ts +0 -0
package/docs/API_REFERENCE.md
CHANGED
|
@@ -546,6 +546,21 @@ In every exporter, a non-empty `selectedRows` takes precedence over `groupedRows
|
|
|
546
546
|
|
|
547
547
|
Every exporter takes `columns` typed for your rows (`GridColDef<R>[]`) or untyped (`GridColDef[]`), and its options also accept `getRowId?: (row: R) => GridRowId`. Pass the grid's `getRowId` when you use one: `selectedRows` holds those ids, and since v3.0 the grid no longer copies them onto `row.id`.
|
|
548
548
|
|
|
549
|
+
### Export option types
|
|
550
|
+
|
|
551
|
+
Each exporter's options are exported as a type. Fields common to all of them: `fileName`, `selectedRows`, `getRowId`, `aggregationResult`, `aggregationModel`, `groupedRows`.
|
|
552
|
+
|
|
553
|
+
| Type | For | Its own fields |
|
|
554
|
+
| :--- | :--- | :--- |
|
|
555
|
+
| `CsvExportOptions<R>` | `exportToCsv` | `includeHeaders`, `delimiter`, `escapeFormulas`, `bom` |
|
|
556
|
+
| `JsonExportOptions<R>` | `exportToJson` | `pretty` |
|
|
557
|
+
| `ExcelExportOptions<R>` | `exportToExcel` (HTML `.xls`) | `sheetName`, `includeHeaders`, `escapeFormulas` |
|
|
558
|
+
| `ExcelAdvancedExportOptions<R>` | `exportToExcelAdvanced` (`.xlsx`) | `sheets`, `columnStyles`, `headerFillColor`, `headerTextColor`, `headerFontSize`, `bodyFontSize` |
|
|
559
|
+
| `ExcelSheetDefinition` | one entry of `sheets` | `name`, `rows` (`'all'` / `'selected'`), `includeHeaders`, `includeSummary`, `autoFilter`, `frozenHeader`, `alternateRowColor`; `{ type: 'summary', name? }` adds a totals sheet |
|
|
560
|
+
| `ExcelColumnStyle` | a value of `columnStyles` (by field) | `numFmt` (e.g. `'$#,##0.00'`), `width`, `alignment`, `embedImage`, `imageWidth`, `imageHeight` |
|
|
561
|
+
|
|
562
|
+
`PdfExportOptions` and `PrintOptions` are described in [PDF Export](features/pdf-export.md) and the [Export Guide](features/export-guide.md).
|
|
563
|
+
|
|
549
564
|
---
|
|
550
565
|
|
|
551
566
|
## 🤖 AI Toolkit (`@opencorestack/opengridx/ai`)
|
|
@@ -652,6 +667,10 @@ Context provider for overriding the grid's visual system.
|
|
|
652
667
|
- **`overlays`**: colours (`background`, `text`, `border`, `shadow`, `item*`) and, v3.7, `fontFamily`, `fontWeight`, `fontSize` for menus and panels.
|
|
653
668
|
- **`toolbar`**, **`scrollbar`** (`thumbColor`, `trackColor`, `size`), **`skeleton`** (`baseColor`, `highlightColor`), **`transitions`**.
|
|
654
669
|
|
|
670
|
+
#### Theme types
|
|
671
|
+
|
|
672
|
+
`DataGridThemeProviderProps`: `theme: GridTheme`, `children`, `className?`, `style?` (both on the wrapper element). Each key of `GridTheme` has its own exported type, for building a theme in parts: `GridThemeColors` (with `GridThemeGrayScale` for `gray`), `GridThemeTypography`, `GridThemeSpacing`, `GridThemeBorders`, `GridThemeShadows`, `GridThemeGrid`, `GridThemeOverlays`, `GridThemeToolbar`, `GridThemeScrollbar`, `GridThemeSkeleton`, `GridThemeTransitions`. Every field is optional.
|
|
673
|
+
|
|
655
674
|
Presets: `darkTheme`, `roseTheme`, `emeraldTheme`, `amberTheme`, `compactTheme`. See [Theming](customization/theming.md).
|
|
656
675
|
|
|
657
676
|
---
|
|
@@ -836,6 +855,15 @@ If the key can change while the grid stays mounted, remount the grid with it (`<
|
|
|
836
855
|
|
|
837
856
|
## ⚙️ Interfaces & Types
|
|
838
857
|
|
|
858
|
+
### Small shared types
|
|
859
|
+
|
|
860
|
+
| Type | Definition | Used by |
|
|
861
|
+
| :--- | :--- | :--- |
|
|
862
|
+
| `GridAlignment` | `'left' \| 'center' \| 'right'` | `GridColDef.align`, `headerAlign` |
|
|
863
|
+
| `GridSortDirection` | `'asc' \| 'desc' \| null` | a column's sort state (`null`: unsorted) |
|
|
864
|
+
| `GridSortModel` | `{ field: string; sort: GridSortDirection }`: like `GridSortItem`, but `sort` may be `null` | a single column's sort state |
|
|
865
|
+
| `GridPinnedPosition` | `'left' \| 'right'` (logical: inline start / end in RTL) | column pinning |
|
|
866
|
+
|
|
839
867
|
### `GridSortItem`
|
|
840
868
|
```typescript
|
|
841
869
|
interface GridSortItem {
|
|
@@ -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
|
|
|
@@ -71,7 +71,7 @@
|
|
|
71
71
|
.ogx-ai-panel__suggestion {
|
|
72
72
|
padding: 3px 10px;
|
|
73
73
|
border: 1px solid var(--ogx-border-color, var(--ogx-grid-border-color));
|
|
74
|
-
border-radius:
|
|
74
|
+
border-radius: 999px;
|
|
75
75
|
background: var(--ogx-grid-header-background);
|
|
76
76
|
color: inherit;
|
|
77
77
|
font: inherit;
|
|
@@ -119,7 +119,7 @@
|
|
|
119
119
|
align-items: center;
|
|
120
120
|
gap: 4px;
|
|
121
121
|
padding: 2px 4px 2px 10px;
|
|
122
|
-
border-radius:
|
|
122
|
+
border-radius: 999px;
|
|
123
123
|
background: var(--ogx-color-primary-light, #e0e7ff);
|
|
124
124
|
color: var(--ogx-color-primary-dark, #1e3a8a);
|
|
125
125
|
}
|
|
@@ -27,9 +27,7 @@
|
|
|
27
27
|
outline: 2px solid var(--ogx-grid-cell-focus-border, var(--ogx-color-primary, #2563eb));
|
|
28
28
|
outline-offset: -2px;
|
|
29
29
|
}
|
|
30
|
-
|
|
31
|
-
cursor: text;
|
|
32
|
-
}
|
|
30
|
+
|
|
33
31
|
/*
|
|
34
32
|
* Cell range selection (cellSelection, v3.3). The tint is a background image, so it also covers
|
|
35
33
|
* pinned cells' own background; the outline is an inset box-shadow, so the layout never shifts.
|
|
@@ -67,7 +67,6 @@
|
|
|
67
67
|
}
|
|
68
68
|
|
|
69
69
|
/* ─── Dark-mode overrides ────────────────────────────────────────────────────*/
|
|
70
|
-
[data-theme="dark"] .ogx-col-group-cell__label
|
|
71
|
-
.ogx--dark .ogx-col-group-cell__label {
|
|
70
|
+
[data-theme="dark"] .ogx-col-group-cell__label {
|
|
72
71
|
color: var(--ogx-color-gray-300, #cbd5e1);
|
|
73
72
|
}
|
|
@@ -67,24 +67,7 @@
|
|
|
67
67
|
background: var(--ogx-grid-background);
|
|
68
68
|
box-shadow: 0 -4px 6px -1px rgba(0, 0, 0, 0.05);
|
|
69
69
|
}
|
|
70
|
-
|
|
71
|
-
opacity: 0.6;
|
|
72
|
-
pointer-events: none;
|
|
73
|
-
}
|
|
74
|
-
.ogx--loading::after {
|
|
75
|
-
content: '';
|
|
76
|
-
position: absolute;
|
|
77
|
-
top: 0;
|
|
78
|
-
left: 0;
|
|
79
|
-
right: 0;
|
|
80
|
-
bottom: 0;
|
|
81
|
-
background: color-mix(in srgb, var(--ogx-grid-background) 60%, transparent);
|
|
82
|
-
backdrop-filter: blur(2px);
|
|
83
|
-
display: flex;
|
|
84
|
-
align-items: center;
|
|
85
|
-
justify-content: center;
|
|
86
|
-
z-index: var(--ogx-z-index-modal);
|
|
87
|
-
}
|
|
70
|
+
|
|
88
71
|
/* No height prop: fill the container, as a block child of a sized box (height: 100%) or as a
|
|
89
72
|
flex item (flex: 1 with min-height: 0). In an auto-height parent 100% resolves to auto. */
|
|
90
73
|
.ogx--fill {
|
|
@@ -312,16 +295,6 @@
|
|
|
312
295
|
text-overflow: ellipsis;
|
|
313
296
|
}
|
|
314
297
|
|
|
315
|
-
.ogx--density-compact .ogx__cell,
|
|
316
|
-
.ogx--density-compact .ogx__header-cell {
|
|
317
|
-
padding: var(--ogx-spacing-xs) var(--ogx-spacing-sm);
|
|
318
|
-
font-size: var(--ogx-font-size-xs);
|
|
319
|
-
}
|
|
320
|
-
.ogx--density-comfortable .ogx__cell,
|
|
321
|
-
.ogx--density-comfortable .ogx__header-cell {
|
|
322
|
-
padding: var(--ogx-spacing-md) var(--ogx-spacing-lg);
|
|
323
|
-
font-size: var(--ogx-font-size-md);
|
|
324
|
-
}
|
|
325
298
|
.ogx__group-cell-content {
|
|
326
299
|
font-weight: 500;
|
|
327
300
|
text-align: start;
|
|
@@ -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;
|
|
@@ -228,7 +228,8 @@ export function GridVirtualRows<R extends GridRowModel>({
|
|
|
228
228
|
{Array.from({ length: infiniteScrollSkeletonCount }).map((_, i) => (
|
|
229
229
|
<Row<R>
|
|
230
230
|
key={`__skeleton_${i}__`}
|
|
231
|
-
row={{ id: `__skeleton_${i}__
|
|
231
|
+
row={{ id: `__skeleton_${i}__` } as unknown as R}
|
|
232
|
+
skeleton
|
|
232
233
|
columns={virtualColumns}
|
|
233
234
|
rowIndex={unpinnedRowsLength + i}
|
|
234
235
|
rowHeight={rowHeight}
|
|
@@ -7,9 +7,9 @@ import type {
|
|
|
7
7
|
GridFilterOperator
|
|
8
8
|
} from '../../types';
|
|
9
9
|
import { filterValueToInputText, getOperatorsForType, NO_VALUE_OPERATORS } from '../../utils/filtering';
|
|
10
|
+
import { filterValueFromInputText, getFilterValueOptions, isSameFilterValue, matchesFilterValueOption } from '../../utils/filtering/filterInput';
|
|
10
11
|
import { Input } from '../ui/Input';
|
|
11
12
|
|
|
12
|
-
|
|
13
13
|
export interface FilterPanelProps {
|
|
14
14
|
filterModel: GridFilterModel;
|
|
15
15
|
columns: GridColDef[];
|
|
@@ -29,17 +29,10 @@ function getActiveFilter(items: readonly FilterEntry[], field: string): GridFilt
|
|
|
29
29
|
return items.find((entry): entry is GridFilterItem => isFilterItem(entry) && entry.field === field);
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
-
function sameValue(a: unknown, b: unknown): boolean {
|
|
33
|
-
if (Object.is(a, b)) return true;
|
|
34
|
-
if (a instanceof Date && b instanceof Date) return a.getTime() === b.getTime();
|
|
35
|
-
if (Array.isArray(a) && Array.isArray(b)) return a.length === b.length && a.every((v, i) => sameValue(v, b[i]));
|
|
36
|
-
return false;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
32
|
function sameEntry(a: FilterEntry, b: FilterEntry): boolean {
|
|
40
33
|
if (a === b) return true;
|
|
41
34
|
if (isFilterItem(a) && isFilterItem(b)) {
|
|
42
|
-
return a.id === b.id && a.field === b.field && a.operator === b.operator &&
|
|
35
|
+
return a.id === b.id && a.field === b.field && a.operator === b.operator && isSameFilterValue(a.value, b.value);
|
|
43
36
|
}
|
|
44
37
|
if (!isFilterItem(a) && !isFilterItem(b)) {
|
|
45
38
|
return a.id === b.id && a.logicOperator === b.logicOperator && sameEntries(a.items, b.items);
|
|
@@ -52,36 +45,11 @@ function sameEntries(a: readonly FilterEntry[], b: readonly FilterEntry[]): bool
|
|
|
52
45
|
return a.length === b.length && a.every((entry, i) => sameEntry(entry, b[i]));
|
|
53
46
|
}
|
|
54
47
|
|
|
55
|
-
interface ValueOption {
|
|
56
|
-
key: string;
|
|
57
|
-
value: unknown;
|
|
58
|
-
label: string;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
function getValueOptions(col: GridColDef): ValueOption[] {
|
|
62
|
-
return (col.valueOptions ?? []).map((option, index) => (
|
|
63
|
-
option !== null && typeof option === 'object'
|
|
64
|
-
? { key: String(index), value: option.value, label: option.label }
|
|
65
|
-
: { key: String(index), value: option, label: String(option) }
|
|
66
|
-
));
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
function matchesOption(option: ValueOption, value: unknown): boolean {
|
|
70
|
-
if (Object.is(option.value, value)) return true;
|
|
71
|
-
return value != null && String(option.value).toLowerCase() === String(value).toLowerCase();
|
|
72
|
-
}
|
|
73
|
-
|
|
74
48
|
/** Text shown in the value input for a committed value. Never used to emit anything. */
|
|
75
49
|
function toInputText(value: unknown, col: GridColDef): string {
|
|
76
50
|
return filterValueToInputText(value, col.type);
|
|
77
51
|
}
|
|
78
52
|
|
|
79
|
-
/** The item value for typed text. isAnyOf takes a comma-separated list. */
|
|
80
|
-
function fromInputText(text: string, operator: GridFilterOperator): unknown {
|
|
81
|
-
if (operator === 'isAnyOf') return text.split(',').map(part => part.trim()).filter(part => part !== '');
|
|
82
|
-
return text;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
53
|
/** Carry a value over to a newly picked operator: lists for isAnyOf, scalars for the rest. */
|
|
86
54
|
function convertValueForOperator(value: unknown, operator: GridFilterOperator): unknown {
|
|
87
55
|
if (NO_VALUE_OPERATORS.has(operator)) return undefined;
|
|
@@ -144,7 +112,7 @@ const FilterRow: React.FC<{
|
|
|
144
112
|
const unchanged = current ? text === toInputText(current.value, colRef.current) : text.trim() === '';
|
|
145
113
|
if (unchanged) return;
|
|
146
114
|
const operator = current?.operator ?? defaultOperator;
|
|
147
|
-
onChangeRef.current(buildItem(operator,
|
|
115
|
+
onChangeRef.current(buildItem(operator, filterValueFromInputText(text, operator)));
|
|
148
116
|
}, [defaultOperator, buildItem]);
|
|
149
117
|
|
|
150
118
|
// Text still waiting for the debounce; flushed when the panel closes (unmounts) meanwhile.
|
|
@@ -172,7 +140,7 @@ const FilterRow: React.FC<{
|
|
|
172
140
|
|
|
173
141
|
const handleOperatorChange = (op: GridFilterOperator) => {
|
|
174
142
|
// Keep what the user typed even if the debounce has not committed it yet.
|
|
175
|
-
const value = draft !== null ?
|
|
143
|
+
const value = draft !== null ? filterValueFromInputText(draft, op) : item?.value;
|
|
176
144
|
setDraft(null);
|
|
177
145
|
onChange(buildItem(op, convertValueForOperator(value, op)));
|
|
178
146
|
};
|
|
@@ -183,7 +151,7 @@ const FilterRow: React.FC<{
|
|
|
183
151
|
};
|
|
184
152
|
|
|
185
153
|
const showValue = !NO_VALUE_OPERATORS.has(currentOperator);
|
|
186
|
-
const valueOptions =
|
|
154
|
+
const valueOptions = getFilterValueOptions(col);
|
|
187
155
|
const valueClassName = `ogx-filter__value-input${isActive ? ' ogx-filter__value-input--active' : ''}`;
|
|
188
156
|
const valueLabel = `Filter value for ${label}`;
|
|
189
157
|
|
|
@@ -215,7 +183,7 @@ const FilterRow: React.FC<{
|
|
|
215
183
|
className={`${valueClassName} ogx-filter__value-multiselect`}
|
|
216
184
|
multiple
|
|
217
185
|
size={Math.min(valueOptions.length, 4)}
|
|
218
|
-
value={valueOptions.filter(o => selected.some(v =>
|
|
186
|
+
value={valueOptions.filter(o => selected.some(v => matchesFilterValueOption(o, v))).map(o => o.key)}
|
|
219
187
|
onChange={(e) => {
|
|
220
188
|
const keys = new Set(Array.from(e.target.selectedOptions, option => option.value));
|
|
221
189
|
onChange(buildItem('isAnyOf', valueOptions.filter(o => keys.has(o.key)).map(o => o.value)));
|
|
@@ -226,7 +194,7 @@ const FilterRow: React.FC<{
|
|
|
226
194
|
</select>
|
|
227
195
|
);
|
|
228
196
|
} else if (showValue && col.type === 'singleSelect' && valueOptions.length > 0 && (currentOperator === 'is' || currentOperator === 'not')) {
|
|
229
|
-
const current = valueOptions.find(o =>
|
|
197
|
+
const current = valueOptions.find(o => matchesFilterValueOption(o, item?.value));
|
|
230
198
|
valueControl = (
|
|
231
199
|
<select
|
|
232
200
|
id={`${filterId}-val`}
|
|
@@ -207,12 +207,7 @@
|
|
|
207
207
|
background-color: var(--ogx-grid-header-hover-background);
|
|
208
208
|
z-index: 6;
|
|
209
209
|
}
|
|
210
|
-
|
|
211
|
-
outline: 2px solid var(--ogx-grid-cell-focus-border, var(--ogx-color-primary));
|
|
212
|
-
outline-offset: -2px;
|
|
213
|
-
background-color: var(--ogx-grid-header-hover-background);
|
|
214
|
-
z-index: 6;
|
|
215
|
-
}
|
|
210
|
+
|
|
216
211
|
.ogx__header-cell:focus, .ogx__header-cell:focus-visible {
|
|
217
212
|
outline: none !important;
|
|
218
213
|
}
|
|
@@ -241,8 +236,4 @@
|
|
|
241
236
|
background: var(--ogx-grid-header-hover-background);
|
|
242
237
|
color: var(--ogx-grid-header-text);
|
|
243
238
|
}
|
|
244
|
-
|
|
245
|
-
opacity: 1;
|
|
246
|
-
background: var(--ogx-color-primary-light);
|
|
247
|
-
color: var(--ogx-color-primary);
|
|
248
|
-
}
|
|
239
|
+
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
getHeaderFilterOperators,
|
|
11
11
|
type GridHeaderFilterState,
|
|
12
12
|
} from '../../utils/headerFilters';
|
|
13
|
+
import { filterValueFromInputText, getFilterValueOptions, isSameFilterValue, matchesFilterValueOption } from '../../utils/filtering/filterInput';
|
|
13
14
|
import { Input } from '../ui/Input';
|
|
14
15
|
import { HeaderFilterOperatorMenu } from './HeaderFilterOperatorMenu';
|
|
15
16
|
import type { HeaderFilterLocale } from '../../utils/headerFilterLocale';
|
|
@@ -29,42 +30,10 @@ export interface HeaderFilterCellProps {
|
|
|
29
30
|
locale: HeaderFilterLocale;
|
|
30
31
|
}
|
|
31
32
|
|
|
32
|
-
interface ValueOption {
|
|
33
|
-
key: string;
|
|
34
|
-
value: unknown;
|
|
35
|
-
label: string;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
function getValueOptions(col: GridColDef): ValueOption[] {
|
|
39
|
-
return (col.valueOptions ?? []).map((option, index) => (
|
|
40
|
-
option !== null && typeof option === 'object'
|
|
41
|
-
? { key: String(index), value: option.value, label: option.label }
|
|
42
|
-
: { key: String(index), value: option, label: String(option) }
|
|
43
|
-
));
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
function matchesOption(option: ValueOption, value: unknown): boolean {
|
|
47
|
-
const single = Array.isArray(value) ? value[0] : value;
|
|
48
|
-
if (Object.is(option.value, single)) return true;
|
|
49
|
-
return single != null && String(option.value).toLowerCase() === String(single).toLowerCase();
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
/** The item value for typed text. isAnyOf takes a comma-separated list. */
|
|
53
|
-
function fromInputText(text: string, operator: GridFilterOperator): unknown {
|
|
54
|
-
if (operator === 'isAnyOf') return text.split(',').map(part => part.trim()).filter(part => part !== '');
|
|
55
|
-
return text;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
function sameValue(a: unknown, b: unknown): boolean {
|
|
59
|
-
if (Object.is(a, b)) return true;
|
|
60
|
-
if (Array.isArray(a) && Array.isArray(b)) return a.length === b.length && a.every((v, i) => sameValue(v, b[i]));
|
|
61
|
-
return false;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
33
|
function sameItem(a: GridFilterItem | undefined, b: GridFilterItem | undefined): boolean {
|
|
65
34
|
if (a === b) return true;
|
|
66
35
|
if (!a || !b) return false;
|
|
67
|
-
return a.operator === b.operator &&
|
|
36
|
+
return a.operator === b.operator && isSameFilterValue(a.value, b.value);
|
|
68
37
|
}
|
|
69
38
|
|
|
70
39
|
function ClearIcon() {
|
|
@@ -81,7 +50,7 @@ export function HeaderFilterCell({ colDef, state, style, className, ariaColIndex
|
|
|
81
50
|
const label = colDef.headerName || colDef.field;
|
|
82
51
|
const item = state.kind === 'editable' ? state.item : undefined;
|
|
83
52
|
const operator: GridFilterOperator = item?.operator ?? getHeaderFilterDefaultOperator(colDef);
|
|
84
|
-
const valueOptions =
|
|
53
|
+
const valueOptions = getFilterValueOptions(colDef);
|
|
85
54
|
const usesSelect = colDef.type === 'boolean' || (colDef.type === 'singleSelect' && valueOptions.length > 0);
|
|
86
55
|
const showsValue = !NO_VALUE_OPERATORS.has(operator);
|
|
87
56
|
|
|
@@ -117,7 +86,7 @@ export function HeaderFilterCell({ colDef, state, style, className, ariaColIndex
|
|
|
117
86
|
const unchanged = current ? text === filterValueToInputText(current.value, col.type) : text.trim() === '';
|
|
118
87
|
if (unchanged) return;
|
|
119
88
|
const op = current?.operator ?? getHeaderFilterDefaultOperator(col);
|
|
120
|
-
const next = buildHeaderFilterItem(col, op,
|
|
89
|
+
const next = buildHeaderFilterItem(col, op, filterValueFromInputText(text, op));
|
|
121
90
|
setLastEmitted(next);
|
|
122
91
|
emit(col.field, next);
|
|
123
92
|
}, []);
|
|
@@ -173,7 +142,7 @@ export function HeaderFilterCell({ colDef, state, style, className, ariaColIndex
|
|
|
173
142
|
|
|
174
143
|
const selectOperator = (op: GridFilterOperator) => {
|
|
175
144
|
// Keep what was typed even if the debounce has not committed it yet.
|
|
176
|
-
const value = draft !== null ?
|
|
145
|
+
const value = draft !== null ? filterValueFromInputText(draft, op) : item?.value;
|
|
177
146
|
setDraft(null);
|
|
178
147
|
setMenuAnchor(null);
|
|
179
148
|
report(buildHeaderFilterItem(colDef, op, convertHeaderFilterValue(value, op)));
|
|
@@ -271,7 +240,7 @@ export function HeaderFilterCell({ colDef, state, style, className, ariaColIndex
|
|
|
271
240
|
</select>
|
|
272
241
|
);
|
|
273
242
|
} else if (usesSelect) {
|
|
274
|
-
const selected = valueOptions.find(o =>
|
|
243
|
+
const selected = valueOptions.find(o => matchesFilterValueOption(o, item?.value));
|
|
275
244
|
valueControl = (
|
|
276
245
|
<select
|
|
277
246
|
ref={selectRef}
|
|
@@ -109,22 +109,6 @@
|
|
|
109
109
|
color: var(--ogx-text-secondary, #9ca3af);
|
|
110
110
|
}
|
|
111
111
|
|
|
112
|
-
/* ─── Responsive: auto switch at ≤768px when using ogx--list-auto class ─── */
|
|
113
|
-
@media (max-width: 768px) {
|
|
114
|
-
.ogx--list-auto .ogx__viewport {
|
|
115
|
-
display: none;
|
|
116
|
-
}
|
|
117
|
-
.ogx--list-auto .ogx-list-view {
|
|
118
|
-
display: flex;
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
@media (min-width: 769px) {
|
|
123
|
-
.ogx--list-auto .ogx-list-view {
|
|
124
|
-
display: none;
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
|
|
128
112
|
/* Loading with no rows yet: a progress bar, or slots.loadingOverlay centred. */
|
|
129
113
|
.ogx-list-view__loading {
|
|
130
114
|
position: relative;
|
|
@@ -46,6 +46,8 @@ function mergeColSpan<R extends GridRowModel>(
|
|
|
46
46
|
|
|
47
47
|
export interface RowProps<R extends GridRowModel = GridRowModel> {
|
|
48
48
|
row: R;
|
|
49
|
+
/** A shimmer placeholder shown while infinite scroll loads more rows (`row` is unused). */
|
|
50
|
+
skeleton?: boolean;
|
|
49
51
|
/** The row's id (from getRowId). Defaults to `row.id`; the grid always passes it. */
|
|
50
52
|
rowId?: GridRowId;
|
|
51
53
|
columns: GridColDef<R>[];
|
|
@@ -108,6 +110,7 @@ export interface RowProps<R extends GridRowModel = GridRowModel> {
|
|
|
108
110
|
function RowImpl<R extends GridRowModel = GridRowModel>(props: RowProps<R>) {
|
|
109
111
|
const {
|
|
110
112
|
row,
|
|
113
|
+
skeleton = false,
|
|
111
114
|
rowId: rowIdProp,
|
|
112
115
|
columns,
|
|
113
116
|
rowIndex,
|
|
@@ -197,7 +200,7 @@ function RowImpl<R extends GridRowModel = GridRowModel>(props: RowProps<R>) {
|
|
|
197
200
|
}, [onEditStop, id]);
|
|
198
201
|
|
|
199
202
|
// ── Skeleton row (shown during infinite-scroll fetch) ─────────────────────
|
|
200
|
-
if (
|
|
203
|
+
if (skeleton) {
|
|
201
204
|
// Render shimmer placeholders — one per visible column, matching row height
|
|
202
205
|
const cellCount = Math.max(columns.length, 5);
|
|
203
206
|
return (
|
|
@@ -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 {
|