@opencorestack/opengridx 3.4.0 → 3.5.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 +33 -0
- package/README.md +6 -0
- package/dist/ai.cjs +2 -1
- package/dist/ai.cjs.map +1 -1
- package/dist/ai.d.ts +203 -3
- package/dist/ai.es.js +2 -1
- package/dist/ai.es.js.map +1 -1
- package/dist/index.d.ts +248 -4
- package/dist/opengridx.css +1 -1
- package/dist/opengridx.es.js +7104 -5726
- 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 +88 -7
- package/docs/README.md +3 -1
- package/docs/architecture/datagrid-orchestration.md +9 -4
- package/docs/components/filter-panel.md +1 -0
- package/docs/components/header.md +4 -1
- package/docs/components/toolbar.md +7 -0
- package/docs/customization/theming.md +3 -1
- package/docs/features/ai-toolkit.md +99 -5
- package/docs/features/cell-selection.md +8 -2
- package/docs/features/fill-handle.md +125 -0
- package/docs/features/filtering.md +3 -0
- package/docs/features/header-filters.md +138 -0
- package/docs/features/keyboard-navigation.md +20 -2
- package/docs/roadmap.md +2 -3
- package/lib/ai/agentTools.ts +160 -0
- package/lib/ai/index.ts +19 -2
- package/lib/ai/promptHandler.ts +109 -0
- package/lib/ai/types.ts +89 -25
- package/lib/ai/validate.ts +10 -0
- package/lib/components/AiAssistant/GridAiAssistantArea.tsx +26 -0
- package/lib/components/AiAssistant/GridAiAssistantPanel.css +171 -0
- package/lib/components/AiAssistant/GridAiAssistantPanel.tsx +145 -0
- package/lib/components/AiAssistant/useSpeechInput.ts +74 -0
- package/lib/components/Cell/Cell.css +28 -0
- package/lib/components/Cell/Cell.tsx +6 -2
- package/lib/components/DataGrid/DataGrid.tsx +76 -3
- package/lib/components/DataGrid/GridLiveRegion.tsx +5 -2
- package/lib/components/DataGrid/GridPinnedRows.tsx +5 -1
- package/lib/components/DataGrid/GridVirtualRows.tsx +5 -1
- package/lib/components/FilterPanel/FilterPanel.tsx +2 -9
- package/lib/components/Header/Header.tsx +44 -23
- package/lib/components/Header/headerColumnStyle.ts +28 -0
- package/lib/components/HeaderFilter/HeaderFilter.css +160 -0
- package/lib/components/HeaderFilter/HeaderFilterCell.tsx +370 -0
- package/lib/components/HeaderFilter/HeaderFilterOperatorMenu.tsx +124 -0
- package/lib/components/HeaderFilter/HeaderFilterRow.tsx +157 -0
- package/lib/components/Toolbar/GridToolbar.tsx +51 -0
- package/lib/hooks/core/gridApiPlaceholder.ts +8 -0
- package/lib/hooks/core/gridToolbarHostContext.ts +5 -0
- package/lib/hooks/core/useGridApiMethods.ts +39 -0
- package/lib/hooks/core/useGridAriaRows.ts +5 -3
- package/lib/hooks/core/useGridCellSelection.ts +77 -11
- package/lib/hooks/core/useGridColumnsPanel.ts +24 -1
- package/lib/hooks/core/useGridControlledState.ts +27 -0
- package/lib/hooks/core/useGridFillHandle.ts +323 -0
- package/lib/hooks/core/useGridKeyboardNavigation.ts +29 -10
- package/lib/hooks/core/useGridToolbarProps.ts +19 -0
- package/lib/hooks/features/useGridAiAssistant.ts +284 -0
- package/lib/hooks/features/useGridBatchEdit.ts +2 -2
- package/lib/index.ts +3 -0
- package/lib/styles/opengridx.css +2 -0
- package/lib/theme/DataGridThemeProvider.tsx +2 -0
- package/lib/theme/types.ts +2 -0
- package/lib/types/index.ts +219 -1
- package/lib/utils/aiAssistant.ts +205 -0
- package/lib/utils/aiBrand.ts +17 -0
- package/lib/utils/cellSelection.ts +25 -5
- package/lib/utils/editing/fill.ts +238 -0
- package/lib/utils/filtering/index.ts +14 -0
- package/lib/utils/focus/index.ts +9 -3
- package/lib/utils/headerFilters.ts +189 -0
- package/llms.txt +44 -4
- package/package.json +1 -1
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react';
|
|
2
|
+
import { attempt } from '../../utils/attempt';
|
|
3
|
+
|
|
4
|
+
/** The part of the Web Speech API's SpeechRecognition the panel uses. */
|
|
5
|
+
interface SpeechRecognitionLike {
|
|
6
|
+
lang: string;
|
|
7
|
+
interimResults: boolean;
|
|
8
|
+
maxAlternatives: number;
|
|
9
|
+
onresult: ((event: { results: ArrayLike<ArrayLike<{ transcript: string }>> }) => void) | null;
|
|
10
|
+
onend: (() => void) | null;
|
|
11
|
+
onerror: (() => void) | null;
|
|
12
|
+
start: () => void;
|
|
13
|
+
stop: () => void;
|
|
14
|
+
abort: () => void;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
type SpeechRecognitionConstructor = new () => SpeechRecognitionLike;
|
|
18
|
+
|
|
19
|
+
function getSpeechRecognition(): SpeechRecognitionConstructor | null {
|
|
20
|
+
if (typeof window === 'undefined') return null;
|
|
21
|
+
const w = window as unknown as { SpeechRecognition?: SpeechRecognitionConstructor; webkitSpeechRecognition?: SpeechRecognitionConstructor };
|
|
22
|
+
return w.SpeechRecognition ?? w.webkitSpeechRecognition ?? null;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Dictation for the prompt input. The browser asks for the microphone only when `toggle` starts it
|
|
27
|
+
* (a click), never on load; without speech recognition `toggle` does nothing.
|
|
28
|
+
*/
|
|
29
|
+
export function useSpeechInput(onText: (text: string) => void): { listening: boolean; toggle: () => void } {
|
|
30
|
+
const [listening, setListening] = useState(false);
|
|
31
|
+
const recognitionRef = useRef<SpeechRecognitionLike | null>(null);
|
|
32
|
+
const onTextRef = useRef(onText);
|
|
33
|
+
useLayoutEffect(() => {
|
|
34
|
+
onTextRef.current = onText;
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
useEffect(() => () => {
|
|
38
|
+
const recognition = recognitionRef.current;
|
|
39
|
+
recognitionRef.current = null;
|
|
40
|
+
if (recognition) attempt(() => recognition.abort());
|
|
41
|
+
}, []);
|
|
42
|
+
|
|
43
|
+
const toggle = useCallback(() => {
|
|
44
|
+
const running = recognitionRef.current;
|
|
45
|
+
if (running) {
|
|
46
|
+
attempt(() => running.stop());
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
const Recognition = getSpeechRecognition();
|
|
50
|
+
if (!Recognition) return;
|
|
51
|
+
const created = attempt(() => new Recognition());
|
|
52
|
+
if (!created.ok) return;
|
|
53
|
+
const recognition = created.value;
|
|
54
|
+
recognition.lang = typeof navigator === 'undefined' ? 'en-US' : navigator.language;
|
|
55
|
+
recognition.interimResults = false;
|
|
56
|
+
recognition.maxAlternatives = 1;
|
|
57
|
+
recognition.onresult = (event) => {
|
|
58
|
+
const first = event.results[0];
|
|
59
|
+
const transcript = first && first[0] ? first[0].transcript : '';
|
|
60
|
+
if (transcript) onTextRef.current(transcript);
|
|
61
|
+
};
|
|
62
|
+
const done = () => {
|
|
63
|
+
if (recognitionRef.current === recognition) recognitionRef.current = null;
|
|
64
|
+
setListening(false);
|
|
65
|
+
};
|
|
66
|
+
recognition.onend = done;
|
|
67
|
+
recognition.onerror = done;
|
|
68
|
+
recognitionRef.current = recognition;
|
|
69
|
+
setListening(true);
|
|
70
|
+
if (!attempt(() => recognition.start()).ok) done();
|
|
71
|
+
}, []);
|
|
72
|
+
|
|
73
|
+
return { listening, toggle };
|
|
74
|
+
}
|
|
@@ -52,6 +52,34 @@
|
|
|
52
52
|
.ogx__cell--range-right {
|
|
53
53
|
--ogx-range-edge-right: inset -2px 0 0 0 var(--ogx-range-border);
|
|
54
54
|
}
|
|
55
|
+
/*
|
|
56
|
+
* Fill handle (v3.5): the square on the bottom-right cell of the range. The cell clips its overflow,
|
|
57
|
+
* so the 24px hit area lies inside the corner and the visible square sits in its corner.
|
|
58
|
+
* touch-action: none lets a touch drag on it fill instead of scrolling.
|
|
59
|
+
*/
|
|
60
|
+
.ogx__cell-fill-handle {
|
|
61
|
+
position: absolute;
|
|
62
|
+
right: 0;
|
|
63
|
+
bottom: 0;
|
|
64
|
+
width: 24px;
|
|
65
|
+
height: 24px;
|
|
66
|
+
max-width: 100%;
|
|
67
|
+
max-height: 100%;
|
|
68
|
+
cursor: crosshair;
|
|
69
|
+
touch-action: none;
|
|
70
|
+
z-index: 1;
|
|
71
|
+
}
|
|
72
|
+
.ogx__cell-fill-handle::after {
|
|
73
|
+
content: '';
|
|
74
|
+
position: absolute;
|
|
75
|
+
right: 1px;
|
|
76
|
+
bottom: 1px;
|
|
77
|
+
width: 7px;
|
|
78
|
+
height: 7px;
|
|
79
|
+
box-sizing: border-box;
|
|
80
|
+
background-color: var(--ogx-fill-handle-color, var(--ogx-range-border));
|
|
81
|
+
border: 1px solid var(--ogx-grid-background, #fff);
|
|
82
|
+
}
|
|
55
83
|
/* While a pointer drag selects cells, the browser must not select text as well. */
|
|
56
84
|
.ogx--range-dragging,
|
|
57
85
|
.ogx--range-dragging * {
|
|
@@ -7,7 +7,7 @@ import { GridEditInputCell } from './GridEditInputCell';
|
|
|
7
7
|
import { CellErrorBoundary } from './CellErrorBoundary';
|
|
8
8
|
import { formatValueByType } from '../../utils/values';
|
|
9
9
|
import { attempt } from '../../utils/attempt';
|
|
10
|
-
import { RANGE_BOTTOM, RANGE_CELL, RANGE_LEFT, RANGE_RIGHT, RANGE_TOP } from '../../utils/cellSelection';
|
|
10
|
+
import { RANGE_BOTTOM, RANGE_CELL, RANGE_FILL_HANDLE, RANGE_LEFT, RANGE_RIGHT, RANGE_TOP } from '../../utils/cellSelection';
|
|
11
11
|
|
|
12
12
|
export interface CellProps<R extends GridRowModel = GridRowModel> {
|
|
13
13
|
onCellClick?: (params: GridCellParams<R>) => void;
|
|
@@ -50,7 +50,8 @@ export interface CellProps<R extends GridRowModel = GridRowModel> {
|
|
|
50
50
|
valueError?: unknown;
|
|
51
51
|
/**
|
|
52
52
|
* Place of the cell in the selected cell range (`cellSelection`), as bit flags: in range 1, top
|
|
53
|
-
* edge 2, bottom 4, left 8, right 16. 0 or absent: not in a range.
|
|
53
|
+
* edge 2, bottom 4, left 8, right 16; 32 draws the fill handle (v3.5). 0 or absent: not in a range.
|
|
54
|
+
* @since v3.3
|
|
54
55
|
*/
|
|
55
56
|
rangeFlags?: number;
|
|
56
57
|
}
|
|
@@ -341,6 +342,9 @@ function CellImpl<R extends GridRowModel = GridRowModel>(props: CellProps<R>) {
|
|
|
341
342
|
formattedValue
|
|
342
343
|
)}
|
|
343
344
|
</div>
|
|
345
|
+
{(rangeFlags & RANGE_FILL_HANDLE) !== 0 && !isEditing && (
|
|
346
|
+
<div className="ogx__cell-fill-handle" aria-hidden="true" />
|
|
347
|
+
)}
|
|
344
348
|
</div>
|
|
345
349
|
);
|
|
346
350
|
}
|
|
@@ -30,6 +30,7 @@ import { useGridEditing } from '../../hooks/features/useGridEditing';
|
|
|
30
30
|
import { useGridBatchEdit } from '../../hooks/features/useGridBatchEdit';
|
|
31
31
|
import { useGridClipboardPaste } from '../../hooks/core/useGridClipboardPaste';
|
|
32
32
|
import { useGridUndoRedo, useGridEditCommitChannel, UNDO_HISTORY_LIMIT } from '../../hooks/core/useGridUndoRedo';
|
|
33
|
+
import { useGridFillHandle } from '../../hooks/core/useGridFillHandle';
|
|
33
34
|
import { useGridSpanning } from '../../hooks/features/useGridSpanning';
|
|
34
35
|
import { useGridSpanRowWindow, useGridSpanColumnWindow } from '../../hooks/features/useGridSpanRenderWindow';
|
|
35
36
|
import { useColumnGroupReorderGuard } from '../../hooks/features/useColumnGroupReorderGuard';
|
|
@@ -43,7 +44,7 @@ import { GridLoadingOverlay } from './GridLoadingOverlay';
|
|
|
43
44
|
import { GridPinnedRows } from './GridPinnedRows';
|
|
44
45
|
import { GridVirtualRows } from './GridVirtualRows';
|
|
45
46
|
import { GridStandaloneColumnPanel } from './GridStandaloneColumnPanel';
|
|
46
|
-
import { resolveGridModes
|
|
47
|
+
import { resolveGridModes } from '../../utils/gridModes';
|
|
47
48
|
import { useGridGroupingColumn } from '../../hooks/core/useGridGroupingColumn';
|
|
48
49
|
import { useGridRowIdOf, getDefaultRowId } from '../../hooks/core/useGridRowIdOf';
|
|
49
50
|
import { useGridApiRefBinding } from '../../hooks/core/useGridApiRefBinding';
|
|
@@ -65,6 +66,9 @@ import { GridPaginationArea } from './GridPaginationArea';
|
|
|
65
66
|
import { GridLiveRegion } from './GridLiveRegion';
|
|
66
67
|
import { useGridAriaRows } from '../../hooks/core/useGridAriaRows';
|
|
67
68
|
import { useGridToolbarProps } from '../../hooks/core/useGridToolbarProps';
|
|
69
|
+
import { useGridAiAssistant } from '../../hooks/features/useGridAiAssistant';
|
|
70
|
+
import { GridAiAssistantArea } from '../AiAssistant/GridAiAssistantArea';
|
|
71
|
+
import { DEFAULT_HEADER_FILTER_HEIGHT } from '../../utils/headerFilters';
|
|
68
72
|
import type { DataGridProps, DataGridUntypedColumnsProps, GridValidRowModel, GridRowModel, GridRowId, GridColDef } from '../../types';
|
|
69
73
|
|
|
70
74
|
/**
|
|
@@ -83,6 +87,8 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
|
|
|
83
87
|
rowHeight: rowHeightProp,
|
|
84
88
|
headerHeight: headerHeightProp,
|
|
85
89
|
wrapHeaderText = false,
|
|
90
|
+
headerFilters = false,
|
|
91
|
+
headerFilterHeight = DEFAULT_HEADER_FILTER_HEIGHT,
|
|
86
92
|
autoHeight = false,
|
|
87
93
|
density,
|
|
88
94
|
checkboxSelection = false,
|
|
@@ -98,6 +104,8 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
|
|
|
98
104
|
onClipboardPaste,
|
|
99
105
|
onBeforeClipboardPaste,
|
|
100
106
|
undoRedo = false,
|
|
107
|
+
disableFillHandle = false,
|
|
108
|
+
onFill,
|
|
101
109
|
onHistoryChange,
|
|
102
110
|
rowSelectionModel: propRowSelectionModel,
|
|
103
111
|
onRowSelectionModelChange: propOnRowSelectionModelChange,
|
|
@@ -138,6 +146,7 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
|
|
|
138
146
|
groupingColDef,
|
|
139
147
|
|
|
140
148
|
rowGroupingModel: propRowGroupingModel,
|
|
149
|
+
onRowGroupingModelChange,
|
|
141
150
|
|
|
142
151
|
aggregationModel: propAggregationModel,
|
|
143
152
|
onAggregationModelChange,
|
|
@@ -177,12 +186,14 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
|
|
|
177
186
|
localeText,
|
|
178
187
|
apiRef: propApiRef,
|
|
179
188
|
overscanRowCount = 3,
|
|
189
|
+
aiAssistant,
|
|
190
|
+
onAiAssistantApply,
|
|
191
|
+
onAiAssistantError,
|
|
180
192
|
} = props;
|
|
181
193
|
// Equal inline column definitions keep one identity, so the row passes memoised on it do not re-run.
|
|
182
194
|
const columns = useStableColumns(columnsProp);
|
|
183
195
|
|
|
184
196
|
const effectiveNoRowsLabel = localeText?.noRowsLabel ?? noRowsLabel;
|
|
185
|
-
const rowGroupingModel = propRowGroupingModel || EMPTY_ROW_GROUPING_MODEL;
|
|
186
197
|
|
|
187
198
|
const controlledState = useGridControlledState({
|
|
188
199
|
initialState,
|
|
@@ -192,6 +203,8 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
|
|
|
192
203
|
onFilterModelChange,
|
|
193
204
|
aggregationModel: propAggregationModel,
|
|
194
205
|
onAggregationModelChange,
|
|
206
|
+
rowGroupingModel: propRowGroupingModel,
|
|
207
|
+
onRowGroupingModelChange,
|
|
195
208
|
columnVisibilityModel: propColumnVisibilityModel,
|
|
196
209
|
onColumnVisibilityModelChange,
|
|
197
210
|
pinnedColumns: propPinnedColumns,
|
|
@@ -213,6 +226,7 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
|
|
|
213
226
|
selectedRowIds: selectionModelRowIds,
|
|
214
227
|
handleRowSelectionModelChange,
|
|
215
228
|
density: effectiveDensity,
|
|
229
|
+
rowGroupingModel, handleRowGroupingModelChange,
|
|
216
230
|
} = controlledState;
|
|
217
231
|
|
|
218
232
|
const pivot = useGridPivot({
|
|
@@ -226,6 +240,8 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
|
|
|
226
240
|
hierarchyRequested: treeData || rowGroupingModel.length > 0,
|
|
227
241
|
});
|
|
228
242
|
const isPivotActive = pivot.isActive;
|
|
243
|
+
// The header filter row filters the source columns; pivot columns are generated, so it is off then.
|
|
244
|
+
const showHeaderFilters = headerFilters && !isPivotActive;
|
|
229
245
|
const {
|
|
230
246
|
rowReordering, hierarchyRowGroupingModel, isTreeDataRequested, isTreeData,
|
|
231
247
|
isRowGrouping, isHierarchyEnabled, hasGroupingColumn, pagination, listView,
|
|
@@ -459,6 +475,14 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
|
|
|
459
475
|
dataRows,
|
|
460
476
|
}),
|
|
461
477
|
visibleColumns: renderOrderedColumns as unknown as GridColDef[],
|
|
478
|
+
rowGroupingModel,
|
|
479
|
+
onRowGroupingModelChange: handleRowGroupingModelChange,
|
|
480
|
+
aggregationModel,
|
|
481
|
+
onAggregationModelChange: handleAggregationModelChange,
|
|
482
|
+
columnVisibilityModel,
|
|
483
|
+
onColumnVisibilityModelChange: handleColumnVisibilityModelChange,
|
|
484
|
+
pivotModel: currentPivotModel,
|
|
485
|
+
onPivotModelChange: handlePivotModelChange,
|
|
462
486
|
});
|
|
463
487
|
|
|
464
488
|
// Indices address the consumer's own rows, whatever the sort, filter, page or pinning.
|
|
@@ -596,6 +620,7 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
|
|
|
596
620
|
pinExpandColumn,
|
|
597
621
|
rowHeight: effectiveRowHeight,
|
|
598
622
|
cellSelection: cellSelection.keyboard,
|
|
623
|
+
headerFilters: showHeaderFilters,
|
|
599
624
|
});
|
|
600
625
|
|
|
601
626
|
const { handleCellClick, handleHeaderClick } = useGridPointerFocusHandlers<R>({
|
|
@@ -650,6 +675,23 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
|
|
|
650
675
|
pinnedTopRowCount: pinnedTopRows.length,
|
|
651
676
|
});
|
|
652
677
|
|
|
678
|
+
// Fill handle (cellSelection): drag the range's corner, Ctrl/Cmd+D and Ctrl/Cmd+R.
|
|
679
|
+
const fillHandle = useGridFillHandle<R>({
|
|
680
|
+
containerRef, viewportRef,
|
|
681
|
+
selection: cellSelection,
|
|
682
|
+
disableFillHandle,
|
|
683
|
+
isEditing: editingHandlers.editingCell !== null,
|
|
684
|
+
allRenderableRows,
|
|
685
|
+
getRowId: getRowIdOf,
|
|
686
|
+
navigationColumns: navigationColumns as unknown as GridColDef<R>[],
|
|
687
|
+
columnIndexMap,
|
|
688
|
+
isCellEditable,
|
|
689
|
+
rowMetaMap,
|
|
690
|
+
getSpanOrigin: spanning.getSpanOrigin,
|
|
691
|
+
batchEdit,
|
|
692
|
+
onFill,
|
|
693
|
+
});
|
|
694
|
+
|
|
653
695
|
const { handleHideColumn, handlePinColumn } = useGridColumnMenuHandlers({
|
|
654
696
|
columnVisibilityModel,
|
|
655
697
|
onColumnVisibilityModelChange: handleColumnVisibilityModelChange,
|
|
@@ -659,6 +701,7 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
|
|
|
659
701
|
|
|
660
702
|
const ariaRows = useGridAriaRows({
|
|
661
703
|
columnGroupingModel,
|
|
704
|
+
headerFilterRow: showHeaderFilters,
|
|
662
705
|
pinnedTopCount: pinnedTopRows.length,
|
|
663
706
|
pinnedBottomCount: pinnedBottomRows.length,
|
|
664
707
|
pagination,
|
|
@@ -713,6 +756,15 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
|
|
|
713
756
|
const columnsPanel = useGridColumnsPanel();
|
|
714
757
|
useGridStylesheetWarning(containerRef);
|
|
715
758
|
|
|
759
|
+
// The aiAssistant prompt panel (v3.5): applies validated replies through apiRef; apiRef.openAiAssistant.
|
|
760
|
+
const aiAssistantPanel = useGridAiAssistant({
|
|
761
|
+
apiRef,
|
|
762
|
+
options: aiAssistant,
|
|
763
|
+
columns: activeColumns as unknown as GridColDef[],
|
|
764
|
+
onApply: onAiAssistantApply,
|
|
765
|
+
onError: onAiAssistantError,
|
|
766
|
+
});
|
|
767
|
+
|
|
716
768
|
const NoRowsOverlaySlot = slots?.noRowsOverlay;
|
|
717
769
|
const LoadingOverlaySlot = slots?.loadingOverlay;
|
|
718
770
|
const loadingOverlay = LoadingOverlaySlot ? <LoadingOverlaySlot {...slotProps?.loadingOverlay} /> : undefined;
|
|
@@ -743,6 +795,12 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
|
|
|
743
795
|
disableColumnReorder, columnGroupingModel, moveColumn, resetColumnOrder,
|
|
744
796
|
forceColumnsOpen: columnsPanel.toolbarPanelRequested,
|
|
745
797
|
onColumnsPanelClose: columnsPanel.closeToolbarPanel,
|
|
798
|
+
aiAssistantEnabled: aiAssistantPanel.enabled,
|
|
799
|
+
aiAssistantOpen: aiAssistantPanel.open,
|
|
800
|
+
onAiAssistantToggle: aiAssistantPanel.togglePanel,
|
|
801
|
+
aiAssistantLabel: localeText?.aiAssistantButton,
|
|
802
|
+
forceFiltersOpen: columnsPanel.toolbarFiltersRequested,
|
|
803
|
+
onFiltersPanelClose: columnsPanel.closeToolbarFiltersPanel,
|
|
746
804
|
});
|
|
747
805
|
|
|
748
806
|
// Props the row renderers (top-pinned, centre and bottom-pinned rows) all take.
|
|
@@ -764,6 +822,7 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
|
|
|
764
822
|
rowMetaMap,
|
|
765
823
|
cellRange: cellSelection.displayRange,
|
|
766
824
|
cellRangeHasRowSpan: cellSelection.hasRowSpan,
|
|
825
|
+
fillHandle: fillHandle.handle,
|
|
767
826
|
};
|
|
768
827
|
|
|
769
828
|
return (
|
|
@@ -774,7 +833,8 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
|
|
|
774
833
|
...style,
|
|
775
834
|
height: height ?? style?.height,
|
|
776
835
|
'--ogx-row-height': `${effectiveRowHeight}px`,
|
|
777
|
-
'--ogx-header-height': `${headerHeight}px
|
|
836
|
+
'--ogx-header-height': `${headerHeight}px`,
|
|
837
|
+
...(showHeaderFilters ? { '--ogx-header-filter-height': `${headerFilterHeight}px` } : undefined),
|
|
778
838
|
} as unknown as React.CSSProperties}
|
|
779
839
|
aria-busy={effectiveLoading}
|
|
780
840
|
>
|
|
@@ -784,6 +844,14 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
|
|
|
784
844
|
</GridToolbarHostContext.Provider>
|
|
785
845
|
)}
|
|
786
846
|
|
|
847
|
+
{aiAssistantPanel.panelProps && (
|
|
848
|
+
<GridAiAssistantArea
|
|
849
|
+
panelProps={aiAssistantPanel.panelProps}
|
|
850
|
+
slot={slots?.aiAssistantPanel}
|
|
851
|
+
slotProps={slotProps?.aiAssistantPanel}
|
|
852
|
+
/>
|
|
853
|
+
)}
|
|
854
|
+
|
|
787
855
|
{/* Used by the column menu's Manage columns when no GridToolbar can show the panel */}
|
|
788
856
|
<GridStandaloneColumnPanel<R>
|
|
789
857
|
isOpen={columnsPanel.standalonePanelOpen}
|
|
@@ -912,6 +980,10 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
|
|
|
912
980
|
onPinColumn={handlePinColumn}
|
|
913
981
|
wrapHeaderText={wrapHeaderText}
|
|
914
982
|
headerHeight={headerHeight}
|
|
983
|
+
headerFilters={showHeaderFilters}
|
|
984
|
+
filterModel={filterModel}
|
|
985
|
+
onFilterModelChange={handleFilterModelChange}
|
|
986
|
+
onOpenFilterPanel={columnsPanel.openFiltersPanel}
|
|
915
987
|
/>
|
|
916
988
|
|
|
917
989
|
<GridPinnedRows<R>
|
|
@@ -1035,6 +1107,7 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
|
|
|
1035
1107
|
dataRowCount={dataRows.length}
|
|
1036
1108
|
filterModel={filterModel}
|
|
1037
1109
|
cellSelectionAnnouncement={cellSelection.announcement}
|
|
1110
|
+
aiAssistantAnnouncement={aiAssistantPanel.announcement}
|
|
1038
1111
|
/>
|
|
1039
1112
|
|
|
1040
1113
|
{showLoadingOverRows && (
|
|
@@ -13,13 +13,15 @@ interface GridLiveRegionProps {
|
|
|
13
13
|
filterModel: GridFilterModel | undefined;
|
|
14
14
|
/** The size of the selected cell range (`cellSelection`), already debounced. */
|
|
15
15
|
cellSelectionAnnouncement?: string;
|
|
16
|
+
/** The `aiAssistant` status line ("Thinking…", "Applied 3 changes", an error). */
|
|
17
|
+
aiAssistantAnnouncement?: string;
|
|
16
18
|
}
|
|
17
19
|
|
|
18
20
|
/**
|
|
19
21
|
* Polite status announcements: loading, a load error, no rows, or the row count of an active filter;
|
|
20
|
-
*
|
|
22
|
+
* the size of the selected cell range; and the AI assistant's status.
|
|
21
23
|
*/
|
|
22
|
-
export function GridLiveRegion({ loading, error, noRowsLabel, filteredRowCount, dataRowCount, filterModel, cellSelectionAnnouncement }: GridLiveRegionProps) {
|
|
24
|
+
export function GridLiveRegion({ loading, error, noRowsLabel, filteredRowCount, dataRowCount, filterModel, cellSelectionAnnouncement, aiAssistantAnnouncement }: GridLiveRegionProps) {
|
|
23
25
|
return (
|
|
24
26
|
<div className="ogx-aria-live-status" role="status" aria-live="polite">
|
|
25
27
|
{loading ? 'Loading data...' : ''}
|
|
@@ -32,6 +34,7 @@ export function GridLiveRegion({ loading, error, noRowsLabel, filteredRowCount,
|
|
|
32
34
|
: ''
|
|
33
35
|
)}
|
|
34
36
|
{cellSelectionAnnouncement ? <span className="ogx-aria-live-status__cells"> {cellSelectionAnnouncement}</span> : null}
|
|
37
|
+
{aiAssistantAnnouncement ? <span className="ogx-aria-live-status__ai"> {aiAssistantAnnouncement}</span> : null}
|
|
35
38
|
</div>
|
|
36
39
|
);
|
|
37
40
|
}
|
|
@@ -17,6 +17,7 @@ import type {
|
|
|
17
17
|
import type { CellColSpanInfo, RowSpanningCaches } from '../../hooks/features/useGridSpanning';
|
|
18
18
|
import type { GridDisplayCellRange } from '../../hooks/core/useGridCellSelection';
|
|
19
19
|
import { getRowCellRange } from '../../utils/cellSelection';
|
|
20
|
+
import type { GridFillHandlePosition } from '../../utils/cellSelection';
|
|
20
21
|
import type { GridEditingState } from '../../hooks/features/useGridEditing';
|
|
21
22
|
|
|
22
23
|
interface PinnedRowEditingHandlers {
|
|
@@ -64,6 +65,8 @@ export interface GridPinnedRowsProps<R extends GridRowModel> {
|
|
|
64
65
|
cellRange?: GridDisplayCellRange | null;
|
|
65
66
|
/** The grid has row spans: rows tell their cells how far the range bottom is. */
|
|
66
67
|
cellRangeHasRowSpan?: boolean;
|
|
68
|
+
/** Where the fill handle is drawn (v3.5), or null. */
|
|
69
|
+
fillHandle?: GridFillHandlePosition | null;
|
|
67
70
|
/** Pinned rows are edited like any other row. */
|
|
68
71
|
editingHandlers?: PinnedRowEditingHandlers;
|
|
69
72
|
isCellEditable?: (params: GridCellParams<R>) => boolean;
|
|
@@ -111,6 +114,7 @@ export function GridPinnedRows<R extends GridRowModel>({
|
|
|
111
114
|
rowReordering = false,
|
|
112
115
|
cellRange,
|
|
113
116
|
cellRangeHasRowSpan = false,
|
|
117
|
+
fillHandle,
|
|
114
118
|
}: GridPinnedRowsProps<R>) {
|
|
115
119
|
if (rows.length === 0) return null;
|
|
116
120
|
|
|
@@ -156,7 +160,7 @@ export function GridPinnedRows<R extends GridRowModel>({
|
|
|
156
160
|
onEditCellValueChange={editingHandlers?.setEditCellValue}
|
|
157
161
|
isCellEditable={isCellEditable}
|
|
158
162
|
rowReordering={rowReordering}
|
|
159
|
-
cellRange={getRowCellRange(cellRange, rowIndex, cellRangeHasRowSpan)}
|
|
163
|
+
cellRange={getRowCellRange(cellRange, rowIndex, cellRangeHasRowSpan, fillHandle)}
|
|
160
164
|
/>
|
|
161
165
|
);
|
|
162
166
|
})}
|
|
@@ -20,6 +20,7 @@ import type { GridEditingState } from '../../hooks/features/useGridEditing';
|
|
|
20
20
|
import type { CellColSpanInfo, RowSpanningCaches } from '../../hooks/features/useGridSpanning';
|
|
21
21
|
import type { GridDisplayCellRange } from '../../hooks/core/useGridCellSelection';
|
|
22
22
|
import { getRowCellRange } from '../../utils/cellSelection';
|
|
23
|
+
import type { GridFillHandlePosition } from '../../utils/cellSelection';
|
|
23
24
|
|
|
24
25
|
interface RowEditingHandlers {
|
|
25
26
|
editingCell: GridEditingState['editingCell'];
|
|
@@ -77,6 +78,8 @@ export interface GridVirtualRowsProps<R extends GridRowModel> {
|
|
|
77
78
|
cellRange?: GridDisplayCellRange | null;
|
|
78
79
|
/** The grid has row spans: rows tell their cells how far the range bottom is. */
|
|
79
80
|
cellRangeHasRowSpan?: boolean;
|
|
81
|
+
/** Where the fill handle is drawn (v3.5), or null. */
|
|
82
|
+
fillHandle?: GridFillHandlePosition | null;
|
|
80
83
|
}
|
|
81
84
|
|
|
82
85
|
export function GridVirtualRows<R extends GridRowModel>({
|
|
@@ -123,6 +126,7 @@ export function GridVirtualRows<R extends GridRowModel>({
|
|
|
123
126
|
columnIndexMap,
|
|
124
127
|
cellRange,
|
|
125
128
|
cellRangeHasRowSpan = false,
|
|
129
|
+
fillHandle,
|
|
126
130
|
}: GridVirtualRowsProps<R>) {
|
|
127
131
|
const skeletonColumns: GridColDef<R>[] = baseColumns.length > 0
|
|
128
132
|
? baseColumns
|
|
@@ -214,7 +218,7 @@ export function GridVirtualRows<R extends GridRowModel>({
|
|
|
214
218
|
rowSpanningCaches={rowSpanningCaches}
|
|
215
219
|
rowHeight={rowHeight}
|
|
216
220
|
rowMeta={rowMetaMap.get(id)}
|
|
217
|
-
cellRange={getRowCellRange(cellRange, actualIndex, cellRangeHasRowSpan)}
|
|
221
|
+
cellRange={getRowCellRange(cellRange, actualIndex, cellRangeHasRowSpan, fillHandle)}
|
|
218
222
|
/>
|
|
219
223
|
))
|
|
220
224
|
)}
|
|
@@ -6,8 +6,7 @@ import type {
|
|
|
6
6
|
GridColDef,
|
|
7
7
|
GridFilterOperator
|
|
8
8
|
} from '../../types';
|
|
9
|
-
import { getOperatorsForType, NO_VALUE_OPERATORS } from '../../utils/filtering';
|
|
10
|
-
import { toLocalDateString } from '../../utils/values';
|
|
9
|
+
import { filterValueToInputText, getOperatorsForType, NO_VALUE_OPERATORS } from '../../utils/filtering';
|
|
11
10
|
import { Input } from '../ui/Input';
|
|
12
11
|
|
|
13
12
|
|
|
@@ -74,13 +73,7 @@ function matchesOption(option: ValueOption, value: unknown): boolean {
|
|
|
74
73
|
|
|
75
74
|
/** Text shown in the value input for a committed value. Never used to emit anything. */
|
|
76
75
|
function toInputText(value: unknown, col: GridColDef): string {
|
|
77
|
-
|
|
78
|
-
if (col.type === 'date' || value instanceof Date) {
|
|
79
|
-
const day = toLocalDateString(value);
|
|
80
|
-
return day || (typeof value === 'string' ? value : '');
|
|
81
|
-
}
|
|
82
|
-
if (Array.isArray(value)) return value.map(v => String(v)).join(', ');
|
|
83
|
-
return String(value);
|
|
76
|
+
return filterValueToInputText(value, col.type);
|
|
84
77
|
}
|
|
85
78
|
|
|
86
79
|
/** The item value for typed text. isAnyOf takes a comma-separated list. */
|
|
@@ -2,12 +2,14 @@ import React, { useId } from 'react';
|
|
|
2
2
|
import { Checkbox } from '../ui/Checkbox';
|
|
3
3
|
import { ColumnResizeHandle } from '../ColumnResizeHandle/ColumnResizeHandle';
|
|
4
4
|
import { clampResizeWidth } from '../ColumnResizeHandle/clampResizeWidth';
|
|
5
|
-
import type { GridColDef, GridRowModel, GridSortDirection, GridColumnPinning, GridAggregationModel, GridColumnGroupingModel } from '../../types';
|
|
5
|
+
import type { GridColDef, GridRowModel, GridSortDirection, GridColumnPinning, GridAggregationModel, GridColumnGroupingModel, GridFilterModel } from '../../types';
|
|
6
6
|
import { isColumnPinned, calculatePinnedPositions, getPinnedEdgeFields } from '../../utils/pinning';
|
|
7
7
|
import { getRenderedColumnWidth } from '../../utils/columnWidth';
|
|
8
8
|
import { buildColumnGroupRow, getColumnGroupDepth, getColumnGroupPaths } from '../../utils/columnGroups';
|
|
9
9
|
import { ColumnMenu } from './ColumnMenu';
|
|
10
10
|
import { getHeaderLineClamp, shouldWrapHeaderText } from '../../utils/headerWrap';
|
|
11
|
+
import { getHeaderColumnStyle } from './headerColumnStyle';
|
|
12
|
+
import { HeaderFilterRow } from '../HeaderFilter/HeaderFilterRow';
|
|
11
13
|
|
|
12
14
|
|
|
13
15
|
function MenuIcon() {
|
|
@@ -60,8 +62,11 @@ export interface HeaderProps<R extends GridRowModel = GridRowModel> {
|
|
|
60
62
|
onDragOver?: (field: string) => (event: React.DragEvent) => void;
|
|
61
63
|
onDragEnd?: () => void;
|
|
62
64
|
onDrop?: (targetField: string) => (event: React.DragEvent) => void;
|
|
63
|
-
/**
|
|
64
|
-
|
|
65
|
+
/**
|
|
66
|
+
* The grid's focus position; `id: null` means a header cell is focused, and `headerFilter: true`
|
|
67
|
+
* a cell of the header filter row.
|
|
68
|
+
*/
|
|
69
|
+
focusedCell?: { id: string | number | null; field: string; headerFilter?: boolean } | null;
|
|
65
70
|
onHeaderClick?: (field: string) => void;
|
|
66
71
|
onSortAdd?: (field: string, direction: GridSortDirection) => void;
|
|
67
72
|
multiSort?: boolean;
|
|
@@ -74,6 +79,14 @@ export interface HeaderProps<R extends GridRowModel = GridRowModel> {
|
|
|
74
79
|
wrapHeaderText?: boolean;
|
|
75
80
|
/** Header row height in pixels; sets how many lines a wrapped title may take. Defaults to 56. */
|
|
76
81
|
headerHeight?: number;
|
|
82
|
+
/** Renders the header filter row under the column headers (`DataGridProps.headerFilters`). */
|
|
83
|
+
headerFilters?: boolean;
|
|
84
|
+
/** The filter model the header filter row reads. */
|
|
85
|
+
filterModel?: GridFilterModel;
|
|
86
|
+
/** Receives the filter model after a header filter changes. */
|
|
87
|
+
onFilterModelChange?: (model: GridFilterModel) => void;
|
|
88
|
+
/** Opens the filter panel; a header filter cell showing "Custom filter" calls it. */
|
|
89
|
+
onOpenFilterPanel?: () => void;
|
|
77
90
|
}
|
|
78
91
|
|
|
79
92
|
export function Header<R extends GridRowModel = GridRowModel>(props: HeaderProps<R>) {
|
|
@@ -112,6 +125,10 @@ export function Header<R extends GridRowModel = GridRowModel>(props: HeaderProps
|
|
|
112
125
|
columnIndexMap,
|
|
113
126
|
wrapHeaderText = false,
|
|
114
127
|
headerHeight = 56,
|
|
128
|
+
headerFilters = false,
|
|
129
|
+
filterModel,
|
|
130
|
+
onFilterModelChange,
|
|
131
|
+
onOpenFilterPanel,
|
|
115
132
|
} = props;
|
|
116
133
|
|
|
117
134
|
const selectAllId = useId();
|
|
@@ -148,7 +165,7 @@ export function Header<R extends GridRowModel = GridRowModel>(props: HeaderProps
|
|
|
148
165
|
setMenuOpenParams({ colDef, anchorEl: anchor });
|
|
149
166
|
};
|
|
150
167
|
|
|
151
|
-
const isHeaderFocused = (field: string) => focusedCell?.id === null && focusedCell.field === field;
|
|
168
|
+
const isHeaderFocused = (field: string) => focusedCell?.id === null && !focusedCell.headerFilter && focusedCell.field === field;
|
|
152
169
|
|
|
153
170
|
// aria-colindex: system columns first (reorder, expand, checkbox), then the data columns.
|
|
154
171
|
const reorderColIndex = rowReordering ? 1 : 0;
|
|
@@ -437,30 +454,12 @@ export function Header<R extends GridRowModel = GridRowModel>(props: HeaderProps
|
|
|
437
454
|
colDef.headerClassName
|
|
438
455
|
].filter(Boolean).join(' ');
|
|
439
456
|
|
|
440
|
-
const effectiveWidth = columnWidths[colDef.field] ?? colDef.width ?? 100;
|
|
441
|
-
|
|
442
457
|
const style: React.CSSProperties = {
|
|
443
|
-
|
|
444
|
-
minWidth: colDef.minWidth,
|
|
445
|
-
maxWidth: colDef.maxWidth,
|
|
446
|
-
flexGrow: colDef.flex ?? 0,
|
|
447
|
-
flexShrink: 0,
|
|
448
|
-
flexBasis: colDef.flex ? 'auto' : 'auto',
|
|
449
|
-
boxSizing: 'border-box',
|
|
450
|
-
position: isPinned ? 'sticky' : 'relative',
|
|
458
|
+
...getHeaderColumnStyle(colDef, columnWidths, pinnedPosition, pinnedPosition ? pinnedPositions[colDef.field] : undefined),
|
|
451
459
|
zIndex: colDef.zIndex,
|
|
452
460
|
opacity: isDragging ? 0.5 : 1
|
|
453
461
|
};
|
|
454
462
|
|
|
455
|
-
if (isPinned && pinnedPosition) {
|
|
456
|
-
const offset = pinnedPositions[colDef.field];
|
|
457
|
-
if (pinnedPosition === 'left') {
|
|
458
|
-
style.left = offset;
|
|
459
|
-
} else {
|
|
460
|
-
style.right = offset;
|
|
461
|
-
}
|
|
462
|
-
}
|
|
463
|
-
|
|
464
463
|
const headerContent = colDef.renderHeader
|
|
465
464
|
? colDef.renderHeader({ field: colDef.field, colDef: colDef as unknown as GridColDef, colIndex })
|
|
466
465
|
: colDef.headerName || colDef.field;
|
|
@@ -565,6 +564,28 @@ export function Header<R extends GridRowModel = GridRowModel>(props: HeaderProps
|
|
|
565
564
|
/>
|
|
566
565
|
)}
|
|
567
566
|
</div>{/* end .ogx__header */}
|
|
567
|
+
|
|
568
|
+
{/* ── Header filter row ── */}
|
|
569
|
+
{headerFilters && (
|
|
570
|
+
<HeaderFilterRow
|
|
571
|
+
columns={columns as unknown as GridColDef[]}
|
|
572
|
+
columnWidths={columnWidths}
|
|
573
|
+
pinnedColumns={pinnedColumns}
|
|
574
|
+
pinnedPositions={pinnedPositions}
|
|
575
|
+
pinnedEdges={pinnedEdges}
|
|
576
|
+
rowReordering={rowReordering}
|
|
577
|
+
hasDetailPanel={hasDetailPanel}
|
|
578
|
+
checkboxSelection={checkboxSelection}
|
|
579
|
+
pinExpandColumn={pinExpandColumn}
|
|
580
|
+
pinCheckboxColumn={pinCheckboxColumn}
|
|
581
|
+
columnIndexMap={columnIndexMap}
|
|
582
|
+
ariaRowIndex={groupDepth + 2}
|
|
583
|
+
filterModel={filterModel}
|
|
584
|
+
onFilterModelChange={onFilterModelChange}
|
|
585
|
+
onOpenFilterPanel={onOpenFilterPanel}
|
|
586
|
+
focusedField={focusedCell?.id === null && focusedCell.headerFilter ? focusedCell.field : null}
|
|
587
|
+
/>
|
|
588
|
+
)}
|
|
568
589
|
</div>
|
|
569
590
|
);
|
|
570
591
|
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type React from 'react';
|
|
2
|
+
import type { GridColDef, GridRowModel } from '../../types';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Width, flex and sticky offset of a data column's cell in a header row. The column header row
|
|
6
|
+
* and the header filter row both use it, so their cells line up whatever the pinning and widths.
|
|
7
|
+
*/
|
|
8
|
+
export function getHeaderColumnStyle<R extends GridRowModel>(
|
|
9
|
+
colDef: GridColDef<R>,
|
|
10
|
+
columnWidths: Record<string, number>,
|
|
11
|
+
pinnedPosition: 'left' | 'right' | null,
|
|
12
|
+
pinnedOffset: number | undefined,
|
|
13
|
+
): React.CSSProperties {
|
|
14
|
+
const style: React.CSSProperties = {
|
|
15
|
+
width: columnWidths[colDef.field] ?? colDef.width ?? 100,
|
|
16
|
+
minWidth: colDef.minWidth,
|
|
17
|
+
maxWidth: colDef.maxWidth,
|
|
18
|
+
flexGrow: colDef.flex ?? 0,
|
|
19
|
+
flexShrink: 0,
|
|
20
|
+
flexBasis: 'auto',
|
|
21
|
+
boxSizing: 'border-box',
|
|
22
|
+
position: pinnedPosition ? 'sticky' : 'relative',
|
|
23
|
+
};
|
|
24
|
+
if (pinnedPosition === 'left') style.left = pinnedOffset;
|
|
25
|
+
else if (pinnedPosition === 'right') style.right = pinnedOffset;
|
|
26
|
+
return style;
|
|
27
|
+
}
|
|
28
|
+
|