@opencorestack/opengridx 3.3.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 +64 -0
- package/README.md +10 -1
- package/dist/ai.cjs +2 -0
- package/dist/ai.cjs.map +1 -0
- package/dist/ai.d.ts +366 -0
- package/dist/ai.es.js +2 -0
- package/dist/ai.es.js.map +1 -0
- package/dist/index.d.ts +396 -6
- package/dist/opengridx.css +1 -1
- package/dist/opengridx.es.js +8029 -6090
- package/dist/opengridx.es.js.map +1 -1
- package/dist/opengridx.umd.js +16 -14
- package/dist/opengridx.umd.js.map +1 -1
- package/docs/API_REFERENCE.md +182 -4
- package/docs/README.md +5 -1
- package/docs/architecture/datagrid-orchestration.md +19 -5
- 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 +12 -1
- package/docs/features/ai-toolkit.md +213 -0
- package/docs/features/cell-selection.md +8 -2
- package/docs/features/clipboard.md +88 -0
- package/docs/features/editing-reordering.md +50 -0
- 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 +31 -2
- package/docs/features/undo-redo.md +79 -0
- package/docs/roadmap.md +3 -4
- package/lib/ai/agentTools.ts +160 -0
- package/lib/ai/coerce.ts +90 -0
- package/lib/ai/columns.ts +90 -0
- package/lib/ai/index.ts +33 -0
- package/lib/ai/promptHandler.ts +109 -0
- package/lib/ai/schema.ts +214 -0
- package/lib/ai/types.ts +152 -0
- package/lib/ai/validate.ts +296 -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 +33 -0
- package/lib/components/Cell/Cell.tsx +6 -2
- package/lib/components/Cell/GridEditInputCell.css +12 -4
- package/lib/components/Cell/GridEditInputCell.tsx +19 -12
- package/lib/components/ColumnVisibilityPanel/ColumnVisibilityPanel.css +14 -0
- package/lib/components/ColumnVisibilityPanel/ColumnVisibilityPanel.tsx +3 -2
- package/lib/components/DataGrid/DataGrid.tsx +138 -5
- 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.css +14 -0
- package/lib/components/FilterPanel/FilterPanel.tsx +5 -11
- 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/ListView/ListView.css +2 -4
- package/lib/components/ListView/ListViewRow.tsx +2 -2
- package/lib/components/Toolbar/GlobalSearch.css +16 -0
- package/lib/components/Toolbar/GlobalSearch.tsx +3 -2
- package/lib/components/Toolbar/GridToolbar.tsx +51 -0
- package/lib/components/ui/Checkbox.tsx +16 -5
- package/lib/components/ui/Input.css +43 -0
- package/lib/components/ui/Input.tsx +24 -5
- package/lib/hooks/core/gridApiPlaceholder.ts +16 -1
- package/lib/hooks/core/gridToolbarHostContext.ts +5 -0
- package/lib/hooks/core/useDataGrid.ts +27 -0
- package/lib/hooks/core/useGridApiMethods.ts +39 -0
- package/lib/hooks/core/useGridAriaRows.ts +5 -3
- package/lib/hooks/core/useGridCellSelection.ts +107 -13
- package/lib/hooks/core/useGridClipboardPaste.ts +283 -0
- 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/core/useGridUndoRedo.ts +220 -0
- package/lib/hooks/features/useGridAiAssistant.ts +284 -0
- package/lib/hooks/features/useGridBatchEdit.ts +217 -0
- package/lib/hooks/features/useGridEditing.ts +24 -45
- package/lib/index.ts +10 -0
- package/lib/styles/opengridx.css +4 -1
- package/lib/theme/DataGridThemeProvider.tsx +2 -0
- package/lib/theme/types.ts +2 -0
- package/lib/types/index.ts +343 -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/commit.ts +69 -0
- package/lib/utils/editing/fill.ts +238 -0
- package/lib/utils/editing/targets.ts +109 -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/lib/utils/parsing.ts +252 -0
- package/llms.txt +97 -3
- package/package.json +8 -2
|
@@ -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 * {
|
|
@@ -66,6 +94,11 @@
|
|
|
66
94
|
outline-offset: -2px;
|
|
67
95
|
background-color: var(--ogx-grid-background);
|
|
68
96
|
}
|
|
97
|
+
/* An <Input variant="cell"> editor (the built-in text/number/date ones, or a renderEditCell) takes
|
|
98
|
+
the full cell height, so its error edge and click area cover the cell. */
|
|
99
|
+
.ogx__cell--editing > .ogx__cell-content:has(.ogx-input-wrapper--cell) {
|
|
100
|
+
align-self: stretch;
|
|
101
|
+
}
|
|
69
102
|
.ogx__cell--align-left {
|
|
70
103
|
justify-content: flex-start;
|
|
71
104
|
text-align: left;
|
|
@@ -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
|
}
|
|
@@ -28,6 +28,15 @@
|
|
|
28
28
|
outline: none;
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
+
/* The built-in editors render through <Input variant="cell">: keep their own font, colour and
|
|
32
|
+
padding over the variant's inherited ones (this file loads after Input.css). */
|
|
33
|
+
.ogx-input-wrapper--cell .ogx__edit-input {
|
|
34
|
+
padding: 0 10px;
|
|
35
|
+
font-family: var(--ogx-font-family, inherit);
|
|
36
|
+
font-size: var(--ogx-grid-cell-font-size, 14px);
|
|
37
|
+
color: var(--ogx-grid-row-text, #1e293b);
|
|
38
|
+
}
|
|
39
|
+
|
|
31
40
|
/* ── Number input — hide spinners ────────────────────────────────────── */
|
|
32
41
|
.ogx__edit-input--number::-webkit-inner-spin-button,
|
|
33
42
|
.ogx__edit-input--number::-webkit-outer-spin-button {
|
|
@@ -54,9 +63,8 @@
|
|
|
54
63
|
outline: 2px solid var(--ogx-color-primary, #2563eb);
|
|
55
64
|
outline-offset: -2px;
|
|
56
65
|
}
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
66
|
+
/* The editor is the shared Checkbox: `.ogx__edit-checkbox` sits on its visually hidden <input>,
|
|
67
|
+
the drawn box is `.ogx-checkbox__box`. */
|
|
68
|
+
.ogx__edit-boolean .ogx-checkbox-wrapper {
|
|
60
69
|
cursor: pointer;
|
|
61
|
-
accent-color: var(--ogx-color-primary, #2563eb);
|
|
62
70
|
}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { useEffect, useRef, useState } from 'react';
|
|
2
2
|
import type { KeyboardEvent } from 'react';
|
|
3
3
|
import type { GridColDef, GridRenderEditCellParams } from '../../types';
|
|
4
|
+
import { Input } from '../ui/Input';
|
|
5
|
+
import { Checkbox } from '../ui/Checkbox';
|
|
4
6
|
|
|
5
7
|
export type GridEditInputCellProps = GridRenderEditCellParams;
|
|
6
8
|
|
|
@@ -34,10 +36,11 @@ function TextEditor({ value, colDef, onValueChange, onCommit, onCancel }: GridEd
|
|
|
34
36
|
useEffect(() => { ref.current?.focus(); ref.current?.select(); }, []);
|
|
35
37
|
|
|
36
38
|
return (
|
|
37
|
-
<
|
|
39
|
+
<Input
|
|
38
40
|
ref={ref}
|
|
41
|
+
variant="cell"
|
|
39
42
|
type="text"
|
|
40
|
-
|
|
43
|
+
inputClassName="ogx__edit-input"
|
|
41
44
|
aria-label={editorLabel(colDef)}
|
|
42
45
|
value={local}
|
|
43
46
|
onChange={e => { setLocal(e.target.value); onValueChange(e.target.value); }}
|
|
@@ -54,10 +57,11 @@ function NumberEditor({ value, colDef, onValueChange, onCommit, onCancel }: Grid
|
|
|
54
57
|
useEffect(() => { ref.current?.focus(); ref.current?.select(); }, []);
|
|
55
58
|
|
|
56
59
|
return (
|
|
57
|
-
<
|
|
60
|
+
<Input
|
|
58
61
|
ref={ref}
|
|
62
|
+
variant="cell"
|
|
59
63
|
type="number"
|
|
60
|
-
|
|
64
|
+
inputClassName="ogx__edit-input ogx__edit-input--number"
|
|
61
65
|
aria-label={editorLabel(colDef)}
|
|
62
66
|
value={local}
|
|
63
67
|
onChange={e => {
|
|
@@ -83,16 +87,17 @@ function BooleanEditor({ value, colDef, onValueChange, onCommit, onCancel }: Gri
|
|
|
83
87
|
};
|
|
84
88
|
|
|
85
89
|
return (
|
|
86
|
-
|
|
87
|
-
|
|
90
|
+
// Safari does not focus checkboxes on click: mousedown moves focus to the cell, the blur
|
|
91
|
+
// commits the unchanged value and the editor unmounts before the click toggles it. The
|
|
92
|
+
// pointer lands on the drawn box (or around it), not the hidden input, so the whole editor
|
|
93
|
+
// keeps focus where it is.
|
|
94
|
+
<div className="ogx__edit-boolean" onMouseDown={(e) => e.preventDefault()}>
|
|
95
|
+
<Checkbox
|
|
88
96
|
ref={ref}
|
|
89
|
-
|
|
90
|
-
className="ogx__edit-checkbox"
|
|
97
|
+
inputClassName="ogx__edit-checkbox"
|
|
91
98
|
aria-label={editorLabel(colDef)}
|
|
92
99
|
checked={!!value}
|
|
93
100
|
onChange={handleChange}
|
|
94
|
-
// Safari does not focus checkboxes on click: mousedown moves focus to the cell, the
|
|
95
|
-
// blur commits the unchanged value and the editor unmounts before the click toggles it.
|
|
96
101
|
onMouseDown={(e) => e.preventDefault()}
|
|
97
102
|
onBlur={onCommit}
|
|
98
103
|
onKeyDown={makeKeyDownHandler(onCommit, onCancel, false)}
|
|
@@ -126,6 +131,7 @@ function SelectEditor({ value, colDef, onValueChange, onCommit, onCancel }: Grid
|
|
|
126
131
|
const showEmptyChoice = selectedIndex === -1;
|
|
127
132
|
|
|
128
133
|
return (
|
|
134
|
+
// Raw <select>: there is no shared Select component yet.
|
|
129
135
|
<select
|
|
130
136
|
ref={ref}
|
|
131
137
|
className="ogx__edit-select"
|
|
@@ -196,10 +202,11 @@ function DateEditor({ value, colDef, onValueChange, onCommit, onCancel }: GridEd
|
|
|
196
202
|
useEffect(() => { ref.current?.focus(); }, []);
|
|
197
203
|
|
|
198
204
|
return (
|
|
199
|
-
<
|
|
205
|
+
<Input
|
|
200
206
|
ref={ref}
|
|
207
|
+
variant="cell"
|
|
201
208
|
type="date"
|
|
202
|
-
|
|
209
|
+
inputClassName="ogx__edit-input"
|
|
203
210
|
aria-label={editorLabel(colDef)}
|
|
204
211
|
value={local}
|
|
205
212
|
onChange={e => { setLocal(e.target.value); onValueChange(fromDateInputValue(e.target.value, initialValue)); }}
|
|
@@ -30,6 +30,8 @@
|
|
|
30
30
|
}
|
|
31
31
|
|
|
32
32
|
.ogx-column-visibility-panel__search-input {
|
|
33
|
+
padding: revert;
|
|
34
|
+
line-height: revert;
|
|
33
35
|
border: none;
|
|
34
36
|
background: none;
|
|
35
37
|
outline: none;
|
|
@@ -39,6 +41,18 @@
|
|
|
39
41
|
flex: 1;
|
|
40
42
|
}
|
|
41
43
|
|
|
44
|
+
/* The search box renders through the shared Input: the container draws the box. */
|
|
45
|
+
.ogx-column-visibility-panel__search-container .ogx-input-wrapper {
|
|
46
|
+
flex: 1;
|
|
47
|
+
border: none;
|
|
48
|
+
border-radius: 0;
|
|
49
|
+
background: transparent;
|
|
50
|
+
transition: none;
|
|
51
|
+
}
|
|
52
|
+
.ogx-column-visibility-panel__search-container .ogx-input-wrapper:focus-within {
|
|
53
|
+
box-shadow: none;
|
|
54
|
+
}
|
|
55
|
+
|
|
42
56
|
.ogx-column-visibility-panel__search-input::placeholder {
|
|
43
57
|
color: var(--ogx-overlay-text, var(--ogx-color-gray-400));
|
|
44
58
|
opacity: 0.6;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { useState, useMemo, useRef, useId } from 'react';
|
|
2
2
|
import { Checkbox } from '../ui/Checkbox';
|
|
3
|
+
import { Input } from '../ui/Input';
|
|
3
4
|
import type { GridColDef, GridRowModel, GridValidRowModel } from '../../types';
|
|
4
5
|
|
|
5
6
|
|
|
@@ -115,9 +116,9 @@ export function ColumnVisibilityPanel<R extends GridValidRowModel = GridRowModel
|
|
|
115
116
|
<div className="ogx-column-visibility-panel__search-section">
|
|
116
117
|
<div className="ogx-column-visibility-panel__search-container">
|
|
117
118
|
<SearchIcon />
|
|
118
|
-
<
|
|
119
|
+
<Input
|
|
119
120
|
autoComplete="off"
|
|
120
|
-
|
|
121
|
+
inputClassName="ogx-column-visibility-panel__search-input"
|
|
121
122
|
placeholder="Search"
|
|
122
123
|
value={searchQuery}
|
|
123
124
|
onChange={(e) => setSearchQuery(e.target.value)}
|
|
@@ -27,6 +27,10 @@ import { useDataGrid } from '../../hooks/core/useDataGrid';
|
|
|
27
27
|
import { useRowReorder } from '../../hooks/useRowReorder';
|
|
28
28
|
import { useGridHierarchy } from '../../hooks/core/useGridHierarchy';
|
|
29
29
|
import { useGridEditing } from '../../hooks/features/useGridEditing';
|
|
30
|
+
import { useGridBatchEdit } from '../../hooks/features/useGridBatchEdit';
|
|
31
|
+
import { useGridClipboardPaste } from '../../hooks/core/useGridClipboardPaste';
|
|
32
|
+
import { useGridUndoRedo, useGridEditCommitChannel, UNDO_HISTORY_LIMIT } from '../../hooks/core/useGridUndoRedo';
|
|
33
|
+
import { useGridFillHandle } from '../../hooks/core/useGridFillHandle';
|
|
30
34
|
import { useGridSpanning } from '../../hooks/features/useGridSpanning';
|
|
31
35
|
import { useGridSpanRowWindow, useGridSpanColumnWindow } from '../../hooks/features/useGridSpanRenderWindow';
|
|
32
36
|
import { useColumnGroupReorderGuard } from '../../hooks/features/useColumnGroupReorderGuard';
|
|
@@ -40,7 +44,7 @@ import { GridLoadingOverlay } from './GridLoadingOverlay';
|
|
|
40
44
|
import { GridPinnedRows } from './GridPinnedRows';
|
|
41
45
|
import { GridVirtualRows } from './GridVirtualRows';
|
|
42
46
|
import { GridStandaloneColumnPanel } from './GridStandaloneColumnPanel';
|
|
43
|
-
import { resolveGridModes
|
|
47
|
+
import { resolveGridModes } from '../../utils/gridModes';
|
|
44
48
|
import { useGridGroupingColumn } from '../../hooks/core/useGridGroupingColumn';
|
|
45
49
|
import { useGridRowIdOf, getDefaultRowId } from '../../hooks/core/useGridRowIdOf';
|
|
46
50
|
import { useGridApiRefBinding } from '../../hooks/core/useGridApiRefBinding';
|
|
@@ -62,6 +66,9 @@ import { GridPaginationArea } from './GridPaginationArea';
|
|
|
62
66
|
import { GridLiveRegion } from './GridLiveRegion';
|
|
63
67
|
import { useGridAriaRows } from '../../hooks/core/useGridAriaRows';
|
|
64
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';
|
|
65
72
|
import type { DataGridProps, DataGridUntypedColumnsProps, GridValidRowModel, GridRowModel, GridRowId, GridColDef } from '../../types';
|
|
66
73
|
|
|
67
74
|
/**
|
|
@@ -80,6 +87,8 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
|
|
|
80
87
|
rowHeight: rowHeightProp,
|
|
81
88
|
headerHeight: headerHeightProp,
|
|
82
89
|
wrapHeaderText = false,
|
|
90
|
+
headerFilters = false,
|
|
91
|
+
headerFilterHeight = DEFAULT_HEADER_FILTER_HEIGHT,
|
|
83
92
|
autoHeight = false,
|
|
84
93
|
density,
|
|
85
94
|
checkboxSelection = false,
|
|
@@ -90,6 +99,14 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
|
|
|
90
99
|
cellSelectionModel: propCellSelectionModel,
|
|
91
100
|
onCellSelectionModelChange,
|
|
92
101
|
showCellSelectionStats = false,
|
|
102
|
+
disableClipboardPaste = false,
|
|
103
|
+
disableRangeClear = false,
|
|
104
|
+
onClipboardPaste,
|
|
105
|
+
onBeforeClipboardPaste,
|
|
106
|
+
undoRedo = false,
|
|
107
|
+
disableFillHandle = false,
|
|
108
|
+
onFill,
|
|
109
|
+
onHistoryChange,
|
|
93
110
|
rowSelectionModel: propRowSelectionModel,
|
|
94
111
|
onRowSelectionModelChange: propOnRowSelectionModelChange,
|
|
95
112
|
onRowClick,
|
|
@@ -129,6 +146,7 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
|
|
|
129
146
|
groupingColDef,
|
|
130
147
|
|
|
131
148
|
rowGroupingModel: propRowGroupingModel,
|
|
149
|
+
onRowGroupingModelChange,
|
|
132
150
|
|
|
133
151
|
aggregationModel: propAggregationModel,
|
|
134
152
|
onAggregationModelChange,
|
|
@@ -168,12 +186,14 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
|
|
|
168
186
|
localeText,
|
|
169
187
|
apiRef: propApiRef,
|
|
170
188
|
overscanRowCount = 3,
|
|
189
|
+
aiAssistant,
|
|
190
|
+
onAiAssistantApply,
|
|
191
|
+
onAiAssistantError,
|
|
171
192
|
} = props;
|
|
172
193
|
// Equal inline column definitions keep one identity, so the row passes memoised on it do not re-run.
|
|
173
194
|
const columns = useStableColumns(columnsProp);
|
|
174
195
|
|
|
175
196
|
const effectiveNoRowsLabel = localeText?.noRowsLabel ?? noRowsLabel;
|
|
176
|
-
const rowGroupingModel = propRowGroupingModel || EMPTY_ROW_GROUPING_MODEL;
|
|
177
197
|
|
|
178
198
|
const controlledState = useGridControlledState({
|
|
179
199
|
initialState,
|
|
@@ -183,6 +203,8 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
|
|
|
183
203
|
onFilterModelChange,
|
|
184
204
|
aggregationModel: propAggregationModel,
|
|
185
205
|
onAggregationModelChange,
|
|
206
|
+
rowGroupingModel: propRowGroupingModel,
|
|
207
|
+
onRowGroupingModelChange,
|
|
186
208
|
columnVisibilityModel: propColumnVisibilityModel,
|
|
187
209
|
onColumnVisibilityModelChange,
|
|
188
210
|
pinnedColumns: propPinnedColumns,
|
|
@@ -204,6 +226,7 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
|
|
|
204
226
|
selectedRowIds: selectionModelRowIds,
|
|
205
227
|
handleRowSelectionModelChange,
|
|
206
228
|
density: effectiveDensity,
|
|
229
|
+
rowGroupingModel, handleRowGroupingModelChange,
|
|
207
230
|
} = controlledState;
|
|
208
231
|
|
|
209
232
|
const pivot = useGridPivot({
|
|
@@ -217,6 +240,8 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
|
|
|
217
240
|
hierarchyRequested: treeData || rowGroupingModel.length > 0,
|
|
218
241
|
});
|
|
219
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;
|
|
220
245
|
const {
|
|
221
246
|
rowReordering, hierarchyRowGroupingModel, isTreeDataRequested, isTreeData,
|
|
222
247
|
isRowGrouping, isHierarchyEnabled, hasGroupingColumn, pagination, listView,
|
|
@@ -271,7 +296,7 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
|
|
|
271
296
|
syncRows: !dataSource,
|
|
272
297
|
});
|
|
273
298
|
const {
|
|
274
|
-
state, apiRef, setRows,
|
|
299
|
+
state, apiRef, setRows, replaceRows, setColumns, setDimensions, setDataSourceLoading, setDataSourceError,
|
|
275
300
|
setRowCount
|
|
276
301
|
} = gridData;
|
|
277
302
|
|
|
@@ -295,6 +320,18 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
|
|
|
295
320
|
getAggregationPosition, columnLookup, filterMode, sortingMode,
|
|
296
321
|
});
|
|
297
322
|
|
|
323
|
+
// Commits (single edits and batch edits) reach the undo/redo history created further down.
|
|
324
|
+
const editCommitChannel = useGridEditCommitChannel();
|
|
325
|
+
// Batch edits (paste, range clear, undo, redo): one processRowUpdate per row, one store update.
|
|
326
|
+
const batchEdit = useGridBatchEdit<R>({
|
|
327
|
+
rowsLookup: state.rows.idRowsLookup as unknown as ReadonlyMap<GridRowId, R>,
|
|
328
|
+
columns: activeColumns,
|
|
329
|
+
processRowUpdate,
|
|
330
|
+
onProcessRowUpdateError,
|
|
331
|
+
replaceRows: replaceRows as unknown as (rows: ReadonlyMap<GridRowId, R>) => void,
|
|
332
|
+
onCommitted: editCommitChannel.emit,
|
|
333
|
+
});
|
|
334
|
+
|
|
298
335
|
const editingHandlers = useGridEditing({
|
|
299
336
|
rows: effectiveRows,
|
|
300
337
|
getRowId: getRowIdOf,
|
|
@@ -306,8 +343,9 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
|
|
|
306
343
|
// after an awaited processRowUpdate, by which time the rows prop (or a dataSource
|
|
307
344
|
// fetch) may have replaced the rows this render saw. The returned row is stored as is
|
|
308
345
|
// (the grid never writes an id onto it), even if it does not carry its key itself.
|
|
309
|
-
|
|
346
|
+
batchEdit.storeRow(rowId, updatedRow);
|
|
310
347
|
},
|
|
348
|
+
onCommitted: (changes) => editCommitChannel.emit(changes, 'edit'),
|
|
311
349
|
});
|
|
312
350
|
|
|
313
351
|
// The selection without ids of removed rows. Only client-owned rows are pruned: a server page
|
|
@@ -437,6 +475,14 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
|
|
|
437
475
|
dataRows,
|
|
438
476
|
}),
|
|
439
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,
|
|
440
486
|
});
|
|
441
487
|
|
|
442
488
|
// Indices address the consumer's own rows, whatever the sort, filter, page or pinning.
|
|
@@ -574,6 +620,7 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
|
|
|
574
620
|
pinExpandColumn,
|
|
575
621
|
rowHeight: effectiveRowHeight,
|
|
576
622
|
cellSelection: cellSelection.keyboard,
|
|
623
|
+
headerFilters: showHeaderFilters,
|
|
577
624
|
});
|
|
578
625
|
|
|
579
626
|
const { handleCellClick, handleHeaderClick } = useGridPointerFocusHandlers<R>({
|
|
@@ -590,6 +637,61 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
|
|
|
590
637
|
containerRef,
|
|
591
638
|
});
|
|
592
639
|
|
|
640
|
+
// Ctrl/Cmd+V and Delete on a range (cellSelection); apiRef.pasteText.
|
|
641
|
+
useGridClipboardPaste<R>({
|
|
642
|
+
apiRef, containerRef, viewportRef,
|
|
643
|
+
selection: cellSelection,
|
|
644
|
+
disableClipboardPaste, disableRangeClear,
|
|
645
|
+
focusedCell,
|
|
646
|
+
allRenderableRows,
|
|
647
|
+
getRowId: getRowIdOf,
|
|
648
|
+
navigationColumns: navigationColumns as unknown as GridColDef<R>[],
|
|
649
|
+
isCellEditable,
|
|
650
|
+
rowMetaMap,
|
|
651
|
+
getSpanOrigin: spanning.getSpanOrigin,
|
|
652
|
+
isEditing: editingHandlers.editingCell !== null,
|
|
653
|
+
batchEdit,
|
|
654
|
+
onClipboardPaste,
|
|
655
|
+
onBeforeClipboardPaste,
|
|
656
|
+
});
|
|
657
|
+
|
|
658
|
+
// Undo / redo (undoRedo): history of committed edits, keys and apiRef methods.
|
|
659
|
+
useGridUndoRedo<R>({
|
|
660
|
+
apiRef,
|
|
661
|
+
enabled: Boolean(undoRedo),
|
|
662
|
+
limit: (typeof undoRedo === 'object' && undoRedo.limit) || UNDO_HISTORY_LIMIT,
|
|
663
|
+
onHistoryChange,
|
|
664
|
+
channel: editCommitChannel,
|
|
665
|
+
batchEdit,
|
|
666
|
+
isEditing: editingHandlers.editingCell !== null,
|
|
667
|
+
containerRef, viewportRef,
|
|
668
|
+
selection: cellSelection,
|
|
669
|
+
setFocusedCell,
|
|
670
|
+
allRenderableRows,
|
|
671
|
+
getRowId: getRowIdOf,
|
|
672
|
+
navigationColumns: navigationColumns as unknown as GridColDef<R>[],
|
|
673
|
+
layout,
|
|
674
|
+
rowHeight: effectiveRowHeight,
|
|
675
|
+
pinnedTopRowCount: pinnedTopRows.length,
|
|
676
|
+
});
|
|
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
|
+
|
|
593
695
|
const { handleHideColumn, handlePinColumn } = useGridColumnMenuHandlers({
|
|
594
696
|
columnVisibilityModel,
|
|
595
697
|
onColumnVisibilityModelChange: handleColumnVisibilityModelChange,
|
|
@@ -599,6 +701,7 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
|
|
|
599
701
|
|
|
600
702
|
const ariaRows = useGridAriaRows({
|
|
601
703
|
columnGroupingModel,
|
|
704
|
+
headerFilterRow: showHeaderFilters,
|
|
602
705
|
pinnedTopCount: pinnedTopRows.length,
|
|
603
706
|
pinnedBottomCount: pinnedBottomRows.length,
|
|
604
707
|
pagination,
|
|
@@ -653,6 +756,15 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
|
|
|
653
756
|
const columnsPanel = useGridColumnsPanel();
|
|
654
757
|
useGridStylesheetWarning(containerRef);
|
|
655
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
|
+
|
|
656
768
|
const NoRowsOverlaySlot = slots?.noRowsOverlay;
|
|
657
769
|
const LoadingOverlaySlot = slots?.loadingOverlay;
|
|
658
770
|
const loadingOverlay = LoadingOverlaySlot ? <LoadingOverlaySlot {...slotProps?.loadingOverlay} /> : undefined;
|
|
@@ -683,6 +795,12 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
|
|
|
683
795
|
disableColumnReorder, columnGroupingModel, moveColumn, resetColumnOrder,
|
|
684
796
|
forceColumnsOpen: columnsPanel.toolbarPanelRequested,
|
|
685
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,
|
|
686
804
|
});
|
|
687
805
|
|
|
688
806
|
// Props the row renderers (top-pinned, centre and bottom-pinned rows) all take.
|
|
@@ -704,6 +822,7 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
|
|
|
704
822
|
rowMetaMap,
|
|
705
823
|
cellRange: cellSelection.displayRange,
|
|
706
824
|
cellRangeHasRowSpan: cellSelection.hasRowSpan,
|
|
825
|
+
fillHandle: fillHandle.handle,
|
|
707
826
|
};
|
|
708
827
|
|
|
709
828
|
return (
|
|
@@ -714,7 +833,8 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
|
|
|
714
833
|
...style,
|
|
715
834
|
height: height ?? style?.height,
|
|
716
835
|
'--ogx-row-height': `${effectiveRowHeight}px`,
|
|
717
|
-
'--ogx-header-height': `${headerHeight}px
|
|
836
|
+
'--ogx-header-height': `${headerHeight}px`,
|
|
837
|
+
...(showHeaderFilters ? { '--ogx-header-filter-height': `${headerFilterHeight}px` } : undefined),
|
|
718
838
|
} as unknown as React.CSSProperties}
|
|
719
839
|
aria-busy={effectiveLoading}
|
|
720
840
|
>
|
|
@@ -724,6 +844,14 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
|
|
|
724
844
|
</GridToolbarHostContext.Provider>
|
|
725
845
|
)}
|
|
726
846
|
|
|
847
|
+
{aiAssistantPanel.panelProps && (
|
|
848
|
+
<GridAiAssistantArea
|
|
849
|
+
panelProps={aiAssistantPanel.panelProps}
|
|
850
|
+
slot={slots?.aiAssistantPanel}
|
|
851
|
+
slotProps={slotProps?.aiAssistantPanel}
|
|
852
|
+
/>
|
|
853
|
+
)}
|
|
854
|
+
|
|
727
855
|
{/* Used by the column menu's Manage columns when no GridToolbar can show the panel */}
|
|
728
856
|
<GridStandaloneColumnPanel<R>
|
|
729
857
|
isOpen={columnsPanel.standalonePanelOpen}
|
|
@@ -852,6 +980,10 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
|
|
|
852
980
|
onPinColumn={handlePinColumn}
|
|
853
981
|
wrapHeaderText={wrapHeaderText}
|
|
854
982
|
headerHeight={headerHeight}
|
|
983
|
+
headerFilters={showHeaderFilters}
|
|
984
|
+
filterModel={filterModel}
|
|
985
|
+
onFilterModelChange={handleFilterModelChange}
|
|
986
|
+
onOpenFilterPanel={columnsPanel.openFiltersPanel}
|
|
855
987
|
/>
|
|
856
988
|
|
|
857
989
|
<GridPinnedRows<R>
|
|
@@ -975,6 +1107,7 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
|
|
|
975
1107
|
dataRowCount={dataRows.length}
|
|
976
1108
|
filterModel={filterModel}
|
|
977
1109
|
cellSelectionAnnouncement={cellSelection.announcement}
|
|
1110
|
+
aiAssistantAnnouncement={aiAssistantPanel.announcement}
|
|
978
1111
|
/>
|
|
979
1112
|
|
|
980
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
|
}
|