@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
@@ -74,6 +74,7 @@
74
74
  font-family: var(--ogx-font-family, system-ui, sans-serif);
75
75
  font-size: var(--ogx-font-size-sm, 13px);
76
76
  color: var(--ogx-toolbar-input-text, var(--ogx-grid-row-text, #1e293b));
77
+ line-height: revert;
77
78
  outline: none;
78
79
  opacity: 0;
79
80
  pointer-events: none;
@@ -88,6 +89,21 @@
88
89
  font-size: var(--ogx-font-size-sm, 13px);
89
90
  }
90
91
 
92
+ /* The input renders through the shared Input: its wrapper is the flex item and the container
93
+ draws the box, so the wrapper has none of its own. */
94
+ .ogx-global-search .ogx-input-wrapper {
95
+ flex: 1 1 auto;
96
+ min-width: 0;
97
+ height: 100%;
98
+ border: none;
99
+ border-radius: 0;
100
+ background: transparent;
101
+ transition: none;
102
+ }
103
+ .ogx-global-search .ogx-input-wrapper:focus-within {
104
+ box-shadow: none;
105
+ }
106
+
91
107
  /* ─── Clear button ────────────────────────────────────────────────────────── */
92
108
  .ogx-global-search__clear {
93
109
  position: absolute;
@@ -1,4 +1,5 @@
1
1
  import React, { useState, useEffect, useRef, useCallback, useLayoutEffect } from 'react';
2
+ import { Input } from '../ui/Input';
2
3
 
3
4
 
4
5
  interface GlobalSearchProps {
@@ -143,9 +144,9 @@ export function GlobalSearch({ value = '', onChange, placeholder = 'Search...',
143
144
  <div className="ogx-global-search__icon-wrapper">
144
145
  <SearchIcon />
145
146
  </div>
146
- <input
147
+ <Input
147
148
  ref={inputRef}
148
- className="ogx-global-search__input"
149
+ inputClassName="ogx-global-search__input"
149
150
  value={localValue}
150
151
  onChange={(e) => setLocalValue(e.target.value)}
151
152
  onFocus={handleFocus}
@@ -10,6 +10,7 @@ import { GlobalSearch } from './GlobalSearch';
10
10
  import { ColumnVisibilityPanel } from '../ColumnVisibilityPanel/ColumnVisibilityPanel';
11
11
  import { FilterPanel } from '../FilterPanel/FilterPanel';
12
12
  import { GridTooltip } from '../Tooltip/Tooltip';
13
+ import { AiSparkleIcon } from '../AiAssistant/GridAiAssistantPanel';
13
14
 
14
15
  /** Props passed to a custom toolbar button renderer. */
15
16
  export interface ToolbarButtonRenderProps {
@@ -48,6 +49,10 @@ export interface GridToolbarProps {
48
49
  onColumnOrderReset?: () => void;
49
50
  forceColumnsOpen?: boolean;
50
51
  onColumnsPanelClose?: () => void;
52
+ /** Opens the filter panel when it turns true (the grid passes it for the header filter row). @since v3.5 */
53
+ forceFiltersOpen?: boolean;
54
+ /** Called whenever the filter panel closes. @since v3.5 */
55
+ onFiltersPanelClose?: () => void;
51
56
  children?: React.ReactNode;
52
57
  rightContent?: React.ReactNode;
53
58
  style?: React.CSSProperties;
@@ -80,6 +85,16 @@ export interface GridToolbarProps {
80
85
  className?: string;
81
86
  /** Show columns with `hideable: false` in the Columns panel as disabled rows. Default: false. */
82
87
  showNonHideableColumns?: boolean;
88
+ /**
89
+ * Opens or closes the `aiAssistant` panel. The grid passes it when `aiAssistant` is set; the
90
+ * **Ask AI** button is shown only with it. Receives the button, which gets focus back on close.
91
+ * @since v3.5
92
+ */
93
+ onAiAssistantToggle?: (trigger: HTMLElement | null) => void;
94
+ /** Whether the `aiAssistant` panel is open. @since v3.5 */
95
+ aiAssistantOpen?: boolean;
96
+ /** Label of the Ask AI button (`localeText.aiAssistantButton`). Default: `"Ask AI"`. @since v3.5 */
97
+ aiAssistantLabel?: string;
83
98
  }
84
99
 
85
100
  const BUILT_IN_AGGREGATION_FUNCTIONS = Object.keys(AGGREGATION_FUNCTIONS) as BuiltInAggFn[];
@@ -492,6 +507,8 @@ export function GridToolbar({
492
507
  onColumnOrderReset,
493
508
  forceColumnsOpen,
494
509
  onColumnsPanelClose,
510
+ forceFiltersOpen,
511
+ onFiltersPanelClose,
495
512
  children,
496
513
  rightContent,
497
514
  style,
@@ -502,6 +519,9 @@ export function GridToolbar({
502
519
  renderQuickFilter,
503
520
  className,
504
521
  showNonHideableColumns,
522
+ onAiAssistantToggle,
523
+ aiAssistantOpen = false,
524
+ aiAssistantLabel = 'Ask AI',
505
525
  }: GridToolbarProps) {
506
526
  // At most one panel is open. Every way the Columns panel closes (its button, a custom
507
527
  // button, another panel opening, click-outside, Escape) reports onColumnsPanelClose, so the
@@ -514,6 +534,7 @@ export function GridToolbar({
514
534
 
515
535
  const switchPanel = (next: ToolbarPanel | null) => {
516
536
  if (openPanel === 'columns' && next !== 'columns') onColumnsPanelClose?.();
537
+ if (openPanel === 'filters' && next !== 'filters') onFiltersPanelClose?.();
517
538
  setOpenPanel(next);
518
539
  };
519
540
  const togglePanel = (panel: ToolbarPanel) => switchPanel(openPanel === panel ? null : panel);
@@ -526,6 +547,12 @@ export function GridToolbar({
526
547
  setSeenForceColumnsOpen(forceColumnsOpen);
527
548
  if (forceColumnsOpen) setOpenPanel('columns');
528
549
  }
550
+ // forceFiltersOpen turning true opens the filter panel, the same way.
551
+ const [seenForceFiltersOpen, setSeenForceFiltersOpen] = useState(forceFiltersOpen);
552
+ if (seenForceFiltersOpen !== forceFiltersOpen) {
553
+ setSeenForceFiltersOpen(forceFiltersOpen);
554
+ if (forceFiltersOpen && onFilterModelChange) setOpenPanel('filters');
555
+ }
529
556
 
530
557
  // Tell the grid this toolbar shows the Columns panel for the column menu; it can only do so
531
558
  // when the grid's forceColumnsOpen reaches it and the Columns panel is enabled.
@@ -535,8 +562,14 @@ export function GridToolbar({
535
562
  if (!toolbarHost || !hostsColumnsPanel) return undefined;
536
563
  return toolbarHost.registerColumnsPanel();
537
564
  }, [toolbarHost, hostsColumnsPanel]);
565
+ const hostsFiltersPanel = forceFiltersOpen !== undefined && Boolean(onFilterModelChange);
566
+ useEffect(() => {
567
+ if (!toolbarHost || !hostsFiltersPanel) return undefined;
568
+ return toolbarHost.registerFiltersPanel();
569
+ }, [toolbarHost, hostsFiltersPanel]);
538
570
 
539
571
  const aggButtonRef = useRef<HTMLButtonElement>(null);
572
+ const aiButtonRef = useRef<HTMLButtonElement>(null);
540
573
  const pivotButtonRef = useRef<HTMLButtonElement>(null);
541
574
  const colsButtonRef = useRef<HTMLButtonElement>(null);
542
575
  const filterButtonRef = useRef<HTMLButtonElement>(null);
@@ -642,6 +675,24 @@ export function GridToolbar({
642
675
 
643
676
  {/* Right: built-in buttons */}
644
677
  <div className="ogx-toolbar__actions">
678
+ {onAiAssistantToggle && (
679
+ <div className="ogx-toolbar__dropdown-wrapper" style={{ marginRight: 4 }}>
680
+ <button
681
+ type="button"
682
+ ref={aiButtonRef}
683
+ className={`ogx-toolbar__icon-btn ogx-toolbar__ai-btn${aiAssistantOpen ? ' ogx-toolbar__icon-btn--active' : ''}`}
684
+ aria-haspopup="dialog"
685
+ aria-expanded={aiAssistantOpen}
686
+ onClick={() => {
687
+ switchPanel(null);
688
+ onAiAssistantToggle(aiButtonRef.current);
689
+ }}
690
+ >
691
+ <AiSparkleIcon />
692
+ {aiAssistantLabel}
693
+ </button>
694
+ </div>
695
+ )}
645
696
  {onFilterModelChange && (
646
697
  <div className="ogx-toolbar__dropdown-wrapper" style={{ marginRight: 4 }}>
647
698
  {renderQuickFilter
@@ -4,7 +4,10 @@ import React from 'react';
4
4
  export interface CheckboxProps extends Omit<React.InputHTMLAttributes<HTMLInputElement>, 'type'> {
5
5
  indeterminate?: boolean;
6
6
  label?: string;
7
+ /** A ref to the `<input>`; the forwarded `ref` reaches it as well. */
7
8
  inputRef?: React.Ref<HTMLInputElement>;
9
+ /** Classes added to the `<input>` itself, next to `ogx-checkbox__input`. `className` goes to the wrapper `<label>`. */
10
+ inputClassName?: string;
8
11
  }
9
12
 
10
13
  /** Points a callback or object ref at `element`. Outside the component, so it does not mutate a prop in render scope. */
@@ -16,13 +19,18 @@ function assignRef(ref: React.Ref<HTMLInputElement> | undefined, element: HTMLIn
16
19
  }
17
20
  }
18
21
 
19
- export const Checkbox: React.FC<CheckboxProps> = ({
22
+ /**
23
+ * A styled checkbox: a visually hidden `<input type="checkbox">` inside a `<label>`. Other props
24
+ * (`onChange`, `onMouseDown`, `tabIndex`, `aria-*`, …) go to the `<input>`, and so does `ref`.
25
+ */
26
+ export const Checkbox = React.forwardRef<HTMLInputElement, CheckboxProps>(function Checkbox({
20
27
  indeterminate = false,
21
28
  label,
22
29
  className = '',
30
+ inputClassName = '',
23
31
  inputRef,
24
32
  ...props
25
- }) => {
33
+ }, forwardedRef) {
26
34
  const internalRef = React.useRef<HTMLInputElement>(null);
27
35
 
28
36
  // Combine refs
@@ -30,7 +38,8 @@ export const Checkbox: React.FC<CheckboxProps> = ({
30
38
  internalRef.current = element;
31
39
 
32
40
  assignRef(inputRef, element);
33
- }, [inputRef]);
41
+ assignRef(forwardedRef, element);
42
+ }, [inputRef, forwardedRef]);
34
43
 
35
44
  React.useEffect(() => {
36
45
  if (internalRef.current) {
@@ -51,7 +60,7 @@ export const Checkbox: React.FC<CheckboxProps> = ({
51
60
  <input
52
61
  ref={setRef}
53
62
  type="checkbox"
54
- className="ogx-checkbox__input"
63
+ className={['ogx-checkbox__input', inputClassName].filter(Boolean).join(' ')}
55
64
  {...props}
56
65
  />
57
66
  <span className="ogx-checkbox__box">
@@ -69,4 +78,6 @@ export const Checkbox: React.FC<CheckboxProps> = ({
69
78
  {label && <span className="ogx-checkbox__label">{label}</span>}
70
79
  </label>
71
80
  );
72
- };
81
+ });
82
+
83
+ Checkbox.displayName = 'Checkbox';
@@ -57,3 +57,46 @@
57
57
  .ogx-input-wrapper--disabled .ogx-input {
58
58
  cursor: not-allowed;
59
59
  }
60
+
61
+ /* ── Cell variant: an editor that fills a grid cell ───────────────────── */
62
+ .ogx-input-wrapper--cell {
63
+ /* Not positioned: the editing cell's focus outline must stay painted over the editor. */
64
+ position: static;
65
+ display: flex;
66
+ width: 100%;
67
+ height: 100%;
68
+ border: none;
69
+ border-radius: 0;
70
+ background: transparent;
71
+ box-shadow: none;
72
+ transition: none;
73
+ }
74
+ .ogx-input-wrapper--cell:hover:not(.ogx-input-wrapper--disabled),
75
+ .ogx-input-wrapper--cell:focus-within:not(.ogx-input-wrapper--disabled) {
76
+ box-shadow: none;
77
+ }
78
+ .ogx-input-wrapper--cell .ogx-input {
79
+ width: 100%;
80
+ min-width: 0;
81
+ height: 100%;
82
+ padding: 0 10px;
83
+ font-family: inherit;
84
+ font-size: inherit;
85
+ line-height: normal;
86
+ color: inherit;
87
+ }
88
+ .ogx-input-wrapper--cell .ogx-input__adornment--start {
89
+ padding-left: 10px;
90
+ }
91
+ .ogx-input-wrapper--cell .ogx-input__adornment--start + .ogx-input {
92
+ padding-left: 4px;
93
+ }
94
+ .ogx-input-wrapper--cell .ogx-input__adornment--end {
95
+ padding-right: 10px;
96
+ }
97
+ .ogx-input-wrapper--cell.ogx-input-wrapper--error,
98
+ .ogx-input-wrapper--cell.ogx-input-wrapper--error:focus-within:not(.ogx-input-wrapper--disabled) {
99
+ /* Positioned, so the error edge is painted over the cell's focus outline. */
100
+ position: relative;
101
+ box-shadow: inset 0 0 0 2px var(--ogx-color-error);
102
+ }
@@ -1,4 +1,3 @@
1
-
2
1
  import React from 'react';
3
2
 
4
3
  export interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {
@@ -6,24 +5,40 @@ export interface InputProps extends React.InputHTMLAttributes<HTMLInputElement>
6
5
  error?: boolean;
7
6
  startAdornment?: React.ReactNode;
8
7
  endAdornment?: React.ReactNode;
8
+ /** Classes added to the `<input>` itself, next to `ogx-input`. `className` goes to the wrapper. */
9
+ inputClassName?: string;
10
+ /**
11
+ * `'field'` (default): a bordered form field. `'cell'`: fills a grid cell with no border,
12
+ * background or focus ring, in the cell's font, for use in `renderEditCell`.
13
+ */
14
+ variant?: 'field' | 'cell';
9
15
  }
10
16
 
11
- export const Input: React.FC<InputProps> = ({
17
+ /**
18
+ * A text field: an `<input>` inside an `.ogx-input-wrapper` that holds the adornments.
19
+ * `ref` reaches the `<input>` (React 18 and 19).
20
+ */
21
+ export const Input = React.forwardRef<HTMLInputElement, InputProps>(function Input({
12
22
  fullWidth = false,
13
23
  error = false,
14
24
  startAdornment,
15
25
  endAdornment,
16
26
  className = '',
27
+ inputClassName = '',
28
+ variant = 'field',
17
29
  ...props
18
- }) => {
30
+ }, ref) {
19
31
  const wrapperClassNames = [
20
32
  'ogx-input-wrapper',
33
+ variant === 'cell' && 'ogx-input-wrapper--cell',
21
34
  fullWidth && 'ogx-input-wrapper--full-width',
22
35
  error && 'ogx-input-wrapper--error',
23
36
  props.disabled && 'ogx-input-wrapper--disabled',
24
37
  className
25
38
  ].filter(Boolean).join(' ');
26
39
 
40
+ const inputClassNames = ['ogx-input', inputClassName].filter(Boolean).join(' ');
41
+
27
42
  return (
28
43
  <div className={wrapperClassNames}>
29
44
  {startAdornment && (
@@ -32,7 +47,9 @@ export const Input: React.FC<InputProps> = ({
32
47
  </div>
33
48
  )}
34
49
  <input
35
- className="ogx-input"
50
+ ref={ref}
51
+ className={inputClassNames}
52
+ aria-invalid={error ? true : undefined}
36
53
  {...props}
37
54
  />
38
55
  {endAdornment && (
@@ -42,4 +59,6 @@ export const Input: React.FC<InputProps> = ({
42
59
  )}
43
60
  </div>
44
61
  );
45
- };
62
+ });
63
+
64
+ Input.displayName = 'Input';
@@ -1,6 +1,7 @@
1
- import type { GridApi, GridRowModel, GridValidRowModel } from '../../types';
1
+ import type { GridApi, GridBatchEditResult, GridRowModel, GridValidRowModel } from '../../types';
2
2
 
3
3
  const noop = () => {};
4
+ const emptyEditResult = (): Promise<GridBatchEditResult> => Promise.resolve({ updated: [], failed: [], skipped: [] });
4
5
 
5
6
  /**
6
7
  * A complete GridApi whose methods are safe no-ops. `useGridApiRef()` starts with one so
@@ -38,5 +39,19 @@ export function createGridApiPlaceholder<R extends GridValidRowModel = GridRowMo
38
39
  clearCellSelection: noop,
39
40
  getSelectedCells: () => [],
40
41
  copySelectedCells: () => Promise.resolve(),
42
+ pasteText: emptyEditResult,
43
+ undo: emptyEditResult,
44
+ redo: emptyEditResult,
45
+ canUndo: () => false,
46
+ canRedo: () => false,
47
+ clearHistory: noop,
48
+ setSortModel: noop,
49
+ setRowGroupingModel: noop,
50
+ setAggregationModel: noop,
51
+ setColumnVisibilityModel: noop,
52
+ setPivotModel: noop,
53
+ getGridAiState: () => ({}),
54
+ openAiAssistant: noop,
55
+ closeAiAssistant: noop,
41
56
  };
42
57
  }
@@ -9,6 +9,11 @@ import { createContext } from 'react';
9
9
  export interface GridToolbarHost {
10
10
  /** Registers a toolbar that can show the Columns panel. Returns the function that unregisters it. */
11
11
  registerColumnsPanel: () => () => void;
12
+ /**
13
+ * Registers a toolbar that can show the filter panel when the header filter row asks for it
14
+ * (through `forceFiltersOpen`). Returns the function that unregisters it.
15
+ */
16
+ registerFiltersPanel: () => () => void;
12
17
  }
13
18
 
14
19
  export const GridToolbarHostContext = createContext<GridToolbarHost | null>(null);
@@ -18,6 +18,7 @@ type GetRowId = (row: GridRowModel) => GridRowId;
18
18
  type GridAction =
19
19
  | { type: 'SET_ROWS'; payload: GridRowModel[] | ((prev: GridRowModel[]) => GridRowModel[]); getRowId: GetRowId }
20
20
  | { type: 'REPLACE_ROW'; id: GridRowId; row: GridRowModel }
21
+ | { type: 'REPLACE_ROWS'; rows: ReadonlyMap<GridRowId, GridRowModel> }
21
22
  | { type: 'SET_COLUMNS'; payload: GridColDef[] }
22
23
  | { type: 'SET_DIMENSIONS'; payload: { viewportWidth: number; viewportHeight: number } }
23
24
  | { type: 'SET_DATASOURCE_LOADING'; payload: boolean }
@@ -134,6 +135,26 @@ function gridReducer(state: GridState, action: GridAction): GridState {
134
135
  };
135
136
  }
136
137
 
138
+ case 'REPLACE_ROWS': {
139
+ // A batch edit (paste, range clear, undo, redo): every stored row in one update.
140
+ let changed = false;
141
+ const idRowsLookup = new Map(state.rows.idRowsLookup);
142
+ const idByRow = new Map(state.rows.idByRow);
143
+ for (const [id, row] of action.rows) {
144
+ const previous = idRowsLookup.get(id);
145
+ if (previous === undefined) continue;
146
+ changed = true;
147
+ idRowsLookup.set(id, row);
148
+ idByRow.delete(previous);
149
+ idByRow.set(row, id);
150
+ }
151
+ if (!changed) return state;
152
+ return {
153
+ ...state,
154
+ rows: { ...state.rows, idRowsLookup, idByRow },
155
+ };
156
+ }
157
+
137
158
  case 'SET_COLUMNS': {
138
159
  const columnLookup = new Map<string, GridColDef>();
139
160
  const orderedFields: string[] = [];
@@ -270,6 +291,11 @@ export function useDataGrid<R extends GridRowModel = GridRowModel>(params: UseDa
270
291
  dispatch({ type: 'REPLACE_ROW', id, row });
271
292
  }, []);
272
293
 
294
+ /** Replaces several stored rows in one update (a batch edit); ids no longer stored are ignored. */
295
+ const replaceRows = useCallback((rowsById: ReadonlyMap<GridRowId, GridRowModel>) => {
296
+ if (rowsById.size > 0) dispatch({ type: 'REPLACE_ROWS', rows: rowsById });
297
+ }, []);
298
+
273
299
  const setColumns = useCallback((newColumns: GridColDef[]) => {
274
300
  dispatch({ type: 'SET_COLUMNS', payload: newColumns });
275
301
  }, []);
@@ -307,6 +333,7 @@ export function useDataGrid<R extends GridRowModel = GridRowModel>(params: UseDa
307
333
  apiRef,
308
334
  setRows,
309
335
  replaceRow,
336
+ replaceRows,
310
337
  setColumns,
311
338
  setDimensions,
312
339
  setDataSourceLoading,
@@ -1,9 +1,13 @@
1
1
  import { useLayoutEffect, useRef } from 'react';
2
2
  import type {
3
+ GridAggregationModel,
3
4
  GridApi,
4
5
  GridColDef,
6
+ GridColumnVisibilityModel,
5
7
  GridFilterModel,
6
8
  GridPaginationModel,
9
+ GridPivotModel,
10
+ GridRowGroupingModel,
7
11
  GridRowId,
8
12
  GridRowModel,
9
13
  GridSortItem,
@@ -45,6 +49,16 @@ export interface UseGridApiMethodsParams {
45
49
  getAllFilteredRows: () => GridRowModel[];
46
50
  /** Columns on screen, in display order. */
47
51
  visibleColumns: GridColDef[];
52
+
53
+ // v3.5: the remaining models, for the setters and getGridAiState.
54
+ rowGroupingModel: GridRowGroupingModel;
55
+ onRowGroupingModelChange: (model: GridRowGroupingModel) => void;
56
+ aggregationModel: GridAggregationModel;
57
+ onAggregationModelChange: (model: GridAggregationModel) => void;
58
+ columnVisibilityModel: GridColumnVisibilityModel;
59
+ onColumnVisibilityModelChange: (model: GridColumnVisibilityModel) => void;
60
+ pivotModel: GridPivotModel;
61
+ onPivotModelChange: (model: GridPivotModel) => void;
48
62
  }
49
63
 
50
64
  /**
@@ -111,5 +125,30 @@ export function useGridApiMethods(params: UseGridApiMethodsParams): void {
111
125
  api.getVisibleRows = () => latest().getVisibleRows();
112
126
  api.getAllFilteredRows = () => latest().getAllFilteredRows();
113
127
  api.getVisibleColumns = () => latest().visibleColumns;
128
+
129
+ api.setSortModel = (model) => {
130
+ latestRef.current = { ...latest(), sortModel: model };
131
+ latest().onSortModelChange(model);
132
+ };
133
+ api.setRowGroupingModel = (model) => {
134
+ latestRef.current = { ...latest(), rowGroupingModel: model };
135
+ latest().onRowGroupingModelChange(model);
136
+ };
137
+ api.setAggregationModel = (model) => {
138
+ latestRef.current = { ...latest(), aggregationModel: model };
139
+ latest().onAggregationModelChange(model);
140
+ };
141
+ api.setColumnVisibilityModel = (model) => {
142
+ latestRef.current = { ...latest(), columnVisibilityModel: model };
143
+ latest().onColumnVisibilityModelChange(model);
144
+ };
145
+ api.setPivotModel = (model) => {
146
+ latestRef.current = { ...latest(), pivotModel: model };
147
+ latest().onPivotModelChange(model);
148
+ };
149
+ api.getGridAiState = () => {
150
+ const { filterModel, sortModel, rowGroupingModel, aggregationModel, pivotModel, columnVisibilityModel } = latest();
151
+ return { filterModel, sortModel, rowGroupingModel, aggregationModel, pivotModel, columnVisibilityModel };
152
+ };
114
153
  }, [apiRef]);
115
154
  }
@@ -5,6 +5,8 @@ import type { GridColumnGroupingModel, GridPaginationModel } from '../../types';
5
5
 
6
6
  export interface UseGridAriaRowsParams {
7
7
  columnGroupingModel: GridColumnGroupingModel | undefined;
8
+ /** Whether the header filter row (`headerFilters`) is rendered: one more header row. */
9
+ headerFilterRow?: boolean;
8
10
  pinnedTopCount: number;
9
11
  pinnedBottomCount: number;
10
12
  pagination: boolean;
@@ -19,13 +21,13 @@ export interface UseGridAriaRowsParams {
19
21
  /** `aria-rowcount` and the `aria-rowindex` bases of the pinned and centre rows. */
20
22
  export function useGridAriaRows(params: UseGridAriaRowsParams): GridAriaRowLayout {
21
23
  const {
22
- columnGroupingModel, pinnedTopCount, pinnedBottomCount, pagination, paginationMode, paginationModel,
24
+ columnGroupingModel, headerFilterRow = false, pinnedTopCount, pinnedBottomCount, pagination, paginationMode, paginationModel,
23
25
  paginationRowCount, paginatedUnpinnedRowCount, sortedUnpinnedRowCount,
24
26
  } = params;
25
27
  const { page, pageSize } = paginationModel;
26
28
 
27
29
  return useMemo(() => getAriaRowLayout({
28
- headerRowCount: 1 + getColumnGroupDepth(columnGroupingModel),
30
+ headerRowCount: 1 + getColumnGroupDepth(columnGroupingModel) + (headerFilterRow ? 1 : 0),
29
31
  pinnedTopCount,
30
32
  pinnedBottomCount,
31
33
  pageRowCount: pagination ? paginatedUnpinnedRowCount : sortedUnpinnedRowCount,
@@ -33,6 +35,6 @@ export function useGridAriaRows(params: UseGridAriaRowsParams): GridAriaRowLayou
33
35
  ? Math.max(paginationRowCount, paginatedUnpinnedRowCount)
34
36
  : sortedUnpinnedRowCount,
35
37
  pageOffset: pagination ? page * pageSize : 0,
36
- }), [columnGroupingModel, pinnedTopCount, pinnedBottomCount, pagination, paginatedUnpinnedRowCount,
38
+ }), [columnGroupingModel, headerFilterRow, pinnedTopCount, pinnedBottomCount, pagination, paginatedUnpinnedRowCount,
37
39
  sortedUnpinnedRowCount, paginationMode, paginationRowCount, page, pageSize]);
38
40
  }