@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.
Files changed (101) hide show
  1. package/CHANGELOG.md +64 -0
  2. package/README.md +10 -1
  3. package/dist/ai.cjs +2 -0
  4. package/dist/ai.cjs.map +1 -0
  5. package/dist/ai.d.ts +366 -0
  6. package/dist/ai.es.js +2 -0
  7. package/dist/ai.es.js.map +1 -0
  8. package/dist/index.d.ts +396 -6
  9. package/dist/opengridx.css +1 -1
  10. package/dist/opengridx.es.js +8029 -6090
  11. package/dist/opengridx.es.js.map +1 -1
  12. package/dist/opengridx.umd.js +16 -14
  13. package/dist/opengridx.umd.js.map +1 -1
  14. package/docs/API_REFERENCE.md +182 -4
  15. package/docs/README.md +5 -1
  16. package/docs/architecture/datagrid-orchestration.md +19 -5
  17. package/docs/components/filter-panel.md +1 -0
  18. package/docs/components/header.md +4 -1
  19. package/docs/components/toolbar.md +7 -0
  20. package/docs/customization/theming.md +12 -1
  21. package/docs/features/ai-toolkit.md +213 -0
  22. package/docs/features/cell-selection.md +8 -2
  23. package/docs/features/clipboard.md +88 -0
  24. package/docs/features/editing-reordering.md +50 -0
  25. package/docs/features/fill-handle.md +125 -0
  26. package/docs/features/filtering.md +3 -0
  27. package/docs/features/header-filters.md +138 -0
  28. package/docs/features/keyboard-navigation.md +31 -2
  29. package/docs/features/undo-redo.md +79 -0
  30. package/docs/roadmap.md +3 -4
  31. package/lib/ai/agentTools.ts +160 -0
  32. package/lib/ai/coerce.ts +90 -0
  33. package/lib/ai/columns.ts +90 -0
  34. package/lib/ai/index.ts +33 -0
  35. package/lib/ai/promptHandler.ts +109 -0
  36. package/lib/ai/schema.ts +214 -0
  37. package/lib/ai/types.ts +152 -0
  38. package/lib/ai/validate.ts +296 -0
  39. package/lib/components/AiAssistant/GridAiAssistantArea.tsx +26 -0
  40. package/lib/components/AiAssistant/GridAiAssistantPanel.css +171 -0
  41. package/lib/components/AiAssistant/GridAiAssistantPanel.tsx +145 -0
  42. package/lib/components/AiAssistant/useSpeechInput.ts +74 -0
  43. package/lib/components/Cell/Cell.css +33 -0
  44. package/lib/components/Cell/Cell.tsx +6 -2
  45. package/lib/components/Cell/GridEditInputCell.css +12 -4
  46. package/lib/components/Cell/GridEditInputCell.tsx +19 -12
  47. package/lib/components/ColumnVisibilityPanel/ColumnVisibilityPanel.css +14 -0
  48. package/lib/components/ColumnVisibilityPanel/ColumnVisibilityPanel.tsx +3 -2
  49. package/lib/components/DataGrid/DataGrid.tsx +138 -5
  50. package/lib/components/DataGrid/GridLiveRegion.tsx +5 -2
  51. package/lib/components/DataGrid/GridPinnedRows.tsx +5 -1
  52. package/lib/components/DataGrid/GridVirtualRows.tsx +5 -1
  53. package/lib/components/FilterPanel/FilterPanel.css +14 -0
  54. package/lib/components/FilterPanel/FilterPanel.tsx +5 -11
  55. package/lib/components/Header/Header.tsx +44 -23
  56. package/lib/components/Header/headerColumnStyle.ts +28 -0
  57. package/lib/components/HeaderFilter/HeaderFilter.css +160 -0
  58. package/lib/components/HeaderFilter/HeaderFilterCell.tsx +370 -0
  59. package/lib/components/HeaderFilter/HeaderFilterOperatorMenu.tsx +124 -0
  60. package/lib/components/HeaderFilter/HeaderFilterRow.tsx +157 -0
  61. package/lib/components/ListView/ListView.css +2 -4
  62. package/lib/components/ListView/ListViewRow.tsx +2 -2
  63. package/lib/components/Toolbar/GlobalSearch.css +16 -0
  64. package/lib/components/Toolbar/GlobalSearch.tsx +3 -2
  65. package/lib/components/Toolbar/GridToolbar.tsx +51 -0
  66. package/lib/components/ui/Checkbox.tsx +16 -5
  67. package/lib/components/ui/Input.css +43 -0
  68. package/lib/components/ui/Input.tsx +24 -5
  69. package/lib/hooks/core/gridApiPlaceholder.ts +16 -1
  70. package/lib/hooks/core/gridToolbarHostContext.ts +5 -0
  71. package/lib/hooks/core/useDataGrid.ts +27 -0
  72. package/lib/hooks/core/useGridApiMethods.ts +39 -0
  73. package/lib/hooks/core/useGridAriaRows.ts +5 -3
  74. package/lib/hooks/core/useGridCellSelection.ts +107 -13
  75. package/lib/hooks/core/useGridClipboardPaste.ts +283 -0
  76. package/lib/hooks/core/useGridColumnsPanel.ts +24 -1
  77. package/lib/hooks/core/useGridControlledState.ts +27 -0
  78. package/lib/hooks/core/useGridFillHandle.ts +323 -0
  79. package/lib/hooks/core/useGridKeyboardNavigation.ts +29 -10
  80. package/lib/hooks/core/useGridToolbarProps.ts +19 -0
  81. package/lib/hooks/core/useGridUndoRedo.ts +220 -0
  82. package/lib/hooks/features/useGridAiAssistant.ts +284 -0
  83. package/lib/hooks/features/useGridBatchEdit.ts +217 -0
  84. package/lib/hooks/features/useGridEditing.ts +24 -45
  85. package/lib/index.ts +10 -0
  86. package/lib/styles/opengridx.css +4 -1
  87. package/lib/theme/DataGridThemeProvider.tsx +2 -0
  88. package/lib/theme/types.ts +2 -0
  89. package/lib/types/index.ts +343 -1
  90. package/lib/utils/aiAssistant.ts +205 -0
  91. package/lib/utils/aiBrand.ts +17 -0
  92. package/lib/utils/cellSelection.ts +25 -5
  93. package/lib/utils/editing/commit.ts +69 -0
  94. package/lib/utils/editing/fill.ts +238 -0
  95. package/lib/utils/editing/targets.ts +109 -0
  96. package/lib/utils/filtering/index.ts +14 -0
  97. package/lib/utils/focus/index.ts +9 -3
  98. package/lib/utils/headerFilters.ts +189 -0
  99. package/lib/utils/parsing.ts +252 -0
  100. package/llms.txt +97 -3
  101. 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. @since v3.3
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
- .ogx__edit-checkbox {
58
- width: 18px;
59
- height: 18px;
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
- <input
39
+ <Input
38
40
  ref={ref}
41
+ variant="cell"
39
42
  type="text"
40
- className="ogx__edit-input"
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
- <input
60
+ <Input
58
61
  ref={ref}
62
+ variant="cell"
59
63
  type="number"
60
- className="ogx__edit-input ogx__edit-input--number"
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
- <div className="ogx__edit-boolean">
87
- <input
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
- type="checkbox"
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
- <input
205
+ <Input
200
206
  ref={ref}
207
+ variant="cell"
201
208
  type="date"
202
- className="ogx__edit-input"
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
- <input
119
+ <Input
119
120
  autoComplete="off"
120
- className="ogx-column-visibility-panel__search-input"
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, EMPTY_ROW_GROUPING_MODEL } from '../../utils/gridModes';
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, replaceRow, setColumns, setDimensions, setDataSourceLoading, setDataSourceError,
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
- replaceRow(rowId, updatedRow);
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
- * and the size of the selected cell range.
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
  }