@opencorestack/opengridx 3.3.0 → 3.4.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 (59) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/README.md +4 -1
  3. package/dist/ai.cjs +1 -0
  4. package/dist/ai.cjs.map +1 -0
  5. package/dist/ai.d.ts +166 -0
  6. package/dist/ai.es.js +1 -0
  7. package/dist/ai.es.js.map +1 -0
  8. package/dist/index.d.ts +148 -2
  9. package/dist/opengridx.css +1 -1
  10. package/dist/opengridx.es.js +6084 -5523
  11. package/dist/opengridx.es.js.map +1 -1
  12. package/dist/opengridx.umd.js +17 -15
  13. package/dist/opengridx.umd.js.map +1 -1
  14. package/docs/API_REFERENCE.md +98 -1
  15. package/docs/README.md +3 -1
  16. package/docs/architecture/datagrid-orchestration.md +10 -1
  17. package/docs/customization/theming.md +9 -0
  18. package/docs/features/ai-toolkit.md +119 -0
  19. package/docs/features/clipboard.md +88 -0
  20. package/docs/features/editing-reordering.md +50 -0
  21. package/docs/features/keyboard-navigation.md +11 -0
  22. package/docs/features/undo-redo.md +79 -0
  23. package/docs/roadmap.md +1 -1
  24. package/lib/ai/coerce.ts +90 -0
  25. package/lib/ai/columns.ts +90 -0
  26. package/lib/ai/index.ts +16 -0
  27. package/lib/ai/schema.ts +214 -0
  28. package/lib/ai/types.ts +88 -0
  29. package/lib/ai/validate.ts +286 -0
  30. package/lib/components/Cell/Cell.css +5 -0
  31. package/lib/components/Cell/GridEditInputCell.css +12 -4
  32. package/lib/components/Cell/GridEditInputCell.tsx +19 -12
  33. package/lib/components/ColumnVisibilityPanel/ColumnVisibilityPanel.css +14 -0
  34. package/lib/components/ColumnVisibilityPanel/ColumnVisibilityPanel.tsx +3 -2
  35. package/lib/components/DataGrid/DataGrid.tsx +62 -2
  36. package/lib/components/FilterPanel/FilterPanel.css +14 -0
  37. package/lib/components/FilterPanel/FilterPanel.tsx +3 -2
  38. package/lib/components/ListView/ListView.css +2 -4
  39. package/lib/components/ListView/ListViewRow.tsx +2 -2
  40. package/lib/components/Toolbar/GlobalSearch.css +16 -0
  41. package/lib/components/Toolbar/GlobalSearch.tsx +3 -2
  42. package/lib/components/ui/Checkbox.tsx +16 -5
  43. package/lib/components/ui/Input.css +43 -0
  44. package/lib/components/ui/Input.tsx +24 -5
  45. package/lib/hooks/core/gridApiPlaceholder.ts +8 -1
  46. package/lib/hooks/core/useDataGrid.ts +27 -0
  47. package/lib/hooks/core/useGridCellSelection.ts +30 -2
  48. package/lib/hooks/core/useGridClipboardPaste.ts +283 -0
  49. package/lib/hooks/core/useGridUndoRedo.ts +220 -0
  50. package/lib/hooks/features/useGridBatchEdit.ts +217 -0
  51. package/lib/hooks/features/useGridEditing.ts +24 -45
  52. package/lib/index.ts +7 -0
  53. package/lib/styles/opengridx.css +2 -1
  54. package/lib/types/index.ts +124 -0
  55. package/lib/utils/editing/commit.ts +69 -0
  56. package/lib/utils/editing/targets.ts +109 -0
  57. package/lib/utils/parsing.ts +252 -0
  58. package/llms.txt +55 -1
  59. package/package.json +8 -2
@@ -82,6 +82,35 @@ interface GridSelectedCell { id: GridRowId; field: string; value: unknown }
82
82
 
83
83
  Public CSS: `ogx__cell--range`, `ogx__cell--range-top` / `-bottom` / `-left` / `-right`, `ogx--range-dragging` (root, during a drag), `--ogx-range-background`, `--ogx-range-border`. Range cells carry `aria-selected="true"`.
84
84
 
85
+ #### Paste and Range Clear (v3.4)
86
+
87
+ | Prop | Type | Default | Description |
88
+ | :--- | :--- | :--- | :--- |
89
+ | `disableClipboardPaste` | `boolean` | `false` | With `cellSelection` on, Ctrl/Cmd+V pastes tab-separated text into the editable cells (through `valueSetter` and `processRowUpdate`, once per row). `true` turns the shortcut off; `apiRef.current.pasteText()` still works. See [Clipboard](features/clipboard.md#-paste-from-excel-and-google-sheets-v34). |
90
+ | `onClipboardPaste` | `(result: GridClipboardPasteResult) => void` | — | Fired after every paste, once every row has settled: `{ updated, failed, skipped, text }`. |
91
+ | `onBeforeClipboardPaste` | `(params: GridBeforeClipboardPasteParams) => boolean \| string` | — | Runs before a paste with `{ text, anchor }`. Return `false` to cancel, a string to paste instead. A throw cancels the paste. |
92
+ | `disableRangeClear` | `boolean` | `false` | With `cellSelection`, Delete / Backspace on a range of more than one cell empties its editable cells (`''` for strings, `null` otherwise) as one edit. `true` turns that off. |
93
+
94
+ #### Undo / Redo (v3.4)
95
+
96
+ | Prop | Type | Default | Description |
97
+ | :--- | :--- | :--- | :--- |
98
+ | `undoRedo` | `boolean \| GridUndoRedoOptions` | `false` | History of committed edits, pastes and range clears: Ctrl/Cmd+Z, Ctrl/Cmd+Shift+Z, Ctrl+Y (focus on a cell, no editor open) and `apiRef.undo()` / `redo()`. Undo and redo write through `valueSetter` and `processRowUpdate`; cells changed since are skipped (`'changed'`). `{ limit }`: actions kept, default 100. See [Undo & Redo](features/undo-redo.md). |
99
+ | `onHistoryChange` | `(params: GridHistoryChangeParams) => void` | — | Fired when `canUndo`, `canRedo` or `size` (actions that can be undone) changes. |
100
+
101
+ ```ts
102
+ type GridEditSkipReason = 'notEditable' | 'invalidValue' | 'changed' | 'missing';
103
+ interface GridBatchEditResult {
104
+ updated: GridRowId[]; // rows stored (processRowUpdate succeeded)
105
+ failed: { id: GridRowId; error: unknown }[]; // also sent to onProcessRowUpdateError
106
+ skipped: { id: GridRowId; field: string; reason: GridEditSkipReason }[];
107
+ }
108
+ interface GridClipboardPasteResult extends GridBatchEditResult { text: string }
109
+ interface GridBeforeClipboardPasteParams { text: string; anchor: GridCellCoordinates | null }
110
+ interface GridHistoryChangeParams { canUndo: boolean; canRedo: boolean; size: number }
111
+ interface GridUndoRedoOptions { limit?: number }
112
+ ```
113
+
85
114
  #### Column Visibility
86
115
 
87
116
  | Prop | Type | Default | Description |
@@ -329,6 +358,11 @@ Access these methods via the `apiRef` prop.
329
358
  | `clearCellSelection()` | `void` | Empty the cell selection (`reason: 'clear'`) (v3.3). |
330
359
  | `getSelectedCells()` | `GridSelectedCell[]` | Cells of the range in display order with values read through `valueGetter`. Synthetic rows and span-covered positions are left out (v3.3). |
331
360
  | `copySelectedCells()` | `Promise<void>` | Copy the range (the focused cell when nothing else is selected) as TSV without a header line. Resolves without writing when there is nothing to copy; rejects when the clipboard write fails (v3.3). |
361
+ | `pasteText(text, anchor?)` | `Promise<GridBatchEditResult>` | Paste tab-separated text as Ctrl/Cmd+V does, from `anchor` (default: the range's top-left cell, else the focused cell), with `onBeforeClipboardPaste` / `onClipboardPaste`. Works with `disableClipboardPaste` and without `cellSelection` (then pass `anchor` or focus a cell) (v3.4). |
362
+ | `undo()` | `Promise<GridBatchEditResult>` | Undo the last action (`undoRedo`); resolves once every row has settled (v3.4). |
363
+ | `redo()` | `Promise<GridBatchEditResult>` | Redo the last undone action (v3.4). |
364
+ | `canUndo()` / `canRedo()` | `boolean` | Whether there is an action to undo / redo (v3.4). |
365
+ | `clearHistory()` | `void` | Forget every undo and redo action (v3.4). |
332
366
  | `copySelectedRows()` | `Promise<void>` | Copy every selected row that passes the filter (other pages, collapsed groups and pinned rows included) as TSV, with the visible columns in screen order. Resolves without writing when no selected row is found; rejects when the clipboard write fails. |
333
367
 
334
368
  ---
@@ -385,6 +419,7 @@ Defines the behavior and appearance of a single column.
385
419
  | `valueOptions` | `Array<string \| number \| { value: unknown; label: string }>` | — | Allowed values for `type: 'singleSelect'` — the filter panel offers them as a (multi-)select, and the edit cell uses them; cells and exports show the option's label. |
386
420
  | `valueGetter` | `(params: GridValueGetterParams) => unknown` | — | Derive a computed value from the row object. Runs before `valueFormatter` and `renderCell`. Client-side sorting, column filters and the quick filter use this value, and editors start from it (double-click and Enter alike). Keep it pure: the quick filter caches each row's search text per row object and column set. A getter that throws for a row shows an error in that cell and reads as `undefined` for sorting, filtering, aggregation, grouping, pivot and list view (v3.0+, with a one-time development warning per column); before, it crashed the grid once the column was sorted, filtered or aggregated. |
387
421
  | `valueSetter` | `(params: GridValueSetterParams) => R` | — | v3.0+. Maps an edited value back onto the row (`{ value, row, field }` → updated row) when an edit is committed. Needed for editable `valueGetter` columns; without it the commit writes `row[field]` and a development warning is logged. |
422
+ | `valueParser` | `(text: string, params: GridValueParserParams) => unknown` | — | v3.4+. Turns pasted text into this column's value instead of the default parser for its `type` (`{ row, field, colDef }`). Throw or return `undefined` to skip the cell (`'invalidValue'`). See [Clipboard](features/clipboard.md#valueparser). |
388
423
  | `valueFormatter` | `(params: GridValueFormatterParams) => string` | — | Format the value into a display string (e.g. currency, dates). Does not affect editing, sorting or column filters; the quick filter also searches the formatted text. A formatter that throws leaves the value unformatted (the quick filter searches the raw value; v3.0+). |
389
424
 
390
425
  #### Rendering
@@ -433,6 +468,7 @@ Span values are floored; `Infinity` means "to the end"; `NaN`, `0` and negative
433
468
  | `groupingValueFormatter` | `(params: { field: string; value: unknown }) => string` | — | Custom formatter for group-header labels when this column is the active grouping field. Falls back to `"field: value"` when omitted or when it throws (v3.0+). |
434
469
  | `aggregable` | `boolean` | — | Whether this column can be aggregated. Unset, the toolbar's Summaries panel and the pivot panel's value fields offer it only for `type: 'number'` columns; `true` offers it for any type. `false` keeps it out of both panels, and an `aggregationModel` or `pivotModel` entry naming it is ignored (no footer total, group aggregate, pivot value or export total). |
435
470
  | `availableAggregationFunctions` | `string[]` | all built-ins | Restrict which aggregation functions are computed and available for this column (e.g. `['sum', 'avg']`). Functions outside this list are skipped even if set in `aggregationModel` or a pivot value field, the toolbar's Summaries panel and the pivot panel offer only these. |
471
+ | `aiExamples` | `unknown[]` | — | Example values put into the schema from `getGridAiSchema` (`@opencorestack/opengridx/ai`) to help a model write filters. **These values are sent to the model**: the schema never reads row data, so only what you list here leaves the app. Strings, numbers and booleans, at most 10. Not used by the grid itself (v3.4.0+). |
436
472
 
437
473
  ---
438
474
 
@@ -453,6 +489,32 @@ Every exporter takes `columns` typed for your rows (`GridColDef<R>[]`) or untype
453
489
 
454
490
  ---
455
491
 
492
+ ## 🤖 AI Toolkit (`@opencorestack/opengridx/ai`)
493
+
494
+ A separate entry point (v3.4.0+) with no React import and no dependencies; the core package never loads it. It does not call any AI service: the app sends the schema to its own model and validates the reply. Guide: [AI Toolkit](features/ai-toolkit.md).
495
+
496
+ ```ts
497
+ import { getGridAiSchema, validateGridAiState } from '@opencorestack/opengridx/ai';
498
+ import type { GridAiState, GridAiValidationError } from '@opencorestack/opengridx/ai';
499
+ ```
500
+
501
+ | Function | Return | Description |
502
+ | :--- | :--- | :--- |
503
+ | `getGridAiSchema(columns, options?)` | `GridAiSchema` | JSON Schema (draft 2020-12, `schemaVersion: 1`) of the `filterModel` (nested AND/OR groups up to 3 levels, `quickFilterValues`), `sortModel`, `rowGroupingModel`, `aggregationModel`, `pivotModel` and `columnVisibilityModel` the columns allow, honouring `filterable`, `sortable`, `groupable`, `hideable`, `aggregable`, `availableAggregationFunctions` and `valueOptions`. Operators per column come from the column type. Reads column definitions only, never rows; deterministic for the same input. |
504
+ | `validateGridAiState(json, columns, options?)` | `GridAiValidationResult` | `{ state, errors }`. `json` is an object or a JSON string. Drops unknown parts, fields, operators and enum values (each with an `{ path, message }` error), coerces values to the column type (`"1,200"` → `1200`, dates → `"YYYY-MM-DD"`), removes empty filter groups. Never throws. |
505
+
506
+ `GridAiSchemaOptions`: `include` / `exclude` (`GridAiPart[]`: `'filter' \| 'sort' \| 'grouping' \| 'aggregation' \| 'pivot' \| 'columnVisibility'`; `exclude` wins), `descriptions` (default `true`: each column's `headerName` and `description` go into the schema), `examples` (default `true`: the columns' `aiExamples` go into the schema). `GridAiValidateOptions` takes `include` / `exclude`; parts outside them are dropped with an error.
507
+
508
+ | Type | Description |
509
+ | :--- | :--- |
510
+ | `GridAiColumn` | The column properties the toolkit reads. A `GridColDef` (typed or untyped) is accepted as is. Fields starting with `__` (system columns) are skipped. |
511
+ | `GridAiSchema` | The returned JSON Schema object: `$schema`, `schemaVersion: 1`, `title`, `type: 'object'`, `properties` (one per offered part) and `$defs`. |
512
+ | `GridAiState` | The validated state: `filterModel?`, `sortModel?`, `rowGroupingModel?`, `aggregationModel?`, `pivotModel?`, `columnVisibilityModel?`, the same types the grid's props take. Only parts present in the reply are set. |
513
+ | `GridAiValidationError` | `{ path: string; message: string }`, e.g. `{ path: 'filterModel.items[2].field', message: 'Unknown field "revenue"' }`. At most 100 are listed. |
514
+ | `GridAiValidationResult` | `{ state: GridAiState; errors: GridAiValidationError[] }` |
515
+
516
+ ---
517
+
456
518
  ## 🧩 Standalone Components
457
519
 
458
520
  The grid's building blocks are exported with their props types (the props types since v3.0). `DataGrid` renders all of them itself; import them to build your own layout or slot.
@@ -469,7 +531,33 @@ The grid's building blocks are exported with their props types (the props types
469
531
  | `Cell` | `CellProps` | One body cell. See [Cell](components/cell.md). |
470
532
  | `CellSelectionStats` | `CellSelectionStatsProps` | The default status bar of `showCellSelectionStats` (v3.3), e.g. to wrap in a `cellSelectionStats` slot. |
471
533
  | `Skeleton` | `SkeletonProps` | Loading placeholder (`rows`, `columns`, both default 5). |
472
- | `Button`, `Input`, `Checkbox` | `ButtonProps`, `InputProps`, `CheckboxProps` | The grid's form controls. `Button` defaults to `type="button"`. |
534
+ | `Button`, `Input`, `Checkbox` | `ButtonProps`, `InputProps`, `CheckboxProps` | The grid's form controls. `Button` defaults to `type="button"`. See [`Input`](#input) and [`Checkbox`](#checkbox) below. |
535
+
536
+ ### `Input`
537
+ A text field: an `<input class="ogx-input">` inside a `<div class="ogx-input-wrapper">` that also holds the adornments. The grid's search boxes, filter value box and text / number / date cell editors render through it. Every other `<input>` attribute (`value`, `onChange`, `type`, `id`, `name`, `aria-*`, …) goes to the `<input>`.
538
+
539
+ | Prop | Type | Default | Description |
540
+ | :--- | :--- | :--- | :--- |
541
+ | `ref` | `Ref<HTMLInputElement>` | — | Reaches the `<input>` (`forwardRef`, React 18 and 19). Since v3.3.1. |
542
+ | `variant` | `'field' \| 'cell'` | `'field'` | `'field'`: bordered form field. `'cell'`: fills its container (a grid cell), no border, radius, background or focus shadow, cell font and the editor padding; adds `ogx-input-wrapper--cell`. Since v3.3.1. |
543
+ | `inputClassName` | `string` | — | Classes added to the `<input>` next to `ogx-input`. Since v3.3.1. |
544
+ | `className` | `string` | — | Classes added to the wrapper `<div>`. |
545
+ | `fullWidth` | `boolean` | `false` | Wrapper takes 100% width (`ogx-input-wrapper--full-width`). |
546
+ | `error` | `boolean` | `false` | `ogx-input-wrapper--error` (error border; inset error edge in the cell variant) and `aria-invalid="true"` on the `<input>`. |
547
+ | `startAdornment` / `endAdornment` | `ReactNode` | — | Content before / after the `<input>` (`ogx-input__adornment--start` / `--end`), e.g. a currency or a unit. |
548
+ | `disabled` | `boolean` | — | Disables the `<input>` and adds `ogx-input-wrapper--disabled`. |
549
+
550
+ ### `Checkbox`
551
+ A styled checkbox: a visually hidden `<input type="checkbox" class="ogx-checkbox__input">` and a drawn `.ogx-checkbox__box`, inside a `<label class="ogx-checkbox-wrapper">`. Other props (`checked`, `onChange`, `onMouseDown`, `tabIndex`, `aria-*`, …) go to the `<input>`. The grid's row checkboxes, column panel, list view and boolean cell editor use it.
552
+
553
+ | Prop | Type | Default | Description |
554
+ | :--- | :--- | :--- | :--- |
555
+ | `ref` | `Ref<HTMLInputElement>` | — | Reaches the `<input>` (`forwardRef`). Since v3.3.1. |
556
+ | `inputRef` | `Ref<HTMLInputElement>` | — | Also reaches the `<input>` (kept for compatibility). |
557
+ | `inputClassName` | `string` | — | Classes added to the `<input>` next to `ogx-checkbox__input`. Since v3.3.1. |
558
+ | `className` | `string` | — | Classes added to the wrapper `<label>`. |
559
+ | `indeterminate` | `boolean` | `false` | Shows the indeterminate mark and sets the input's `indeterminate`. |
560
+ | `label` | `string` | — | Visible label text after the box; also the accessible name. |
473
561
 
474
562
  ---
475
563
 
@@ -814,6 +902,15 @@ Passed to `valueSetter` (v3.0+).
814
902
  | `row` | `R` | The row as it was before the edit. |
815
903
  | `field` | `string` | The column field name. |
816
904
 
905
+ ### `GridValueParserParams<R>`
906
+ Passed to `valueParser` (v3.4+).
907
+
908
+ | Property | Type | Description |
909
+ | :--- | :--- | :--- |
910
+ | `row` | `R` | The row the text is pasted into, as stored now. |
911
+ | `field` | `string` | The column field name. |
912
+ | `colDef` | `GridColDef<R>` | The column. |
913
+
817
914
  ### `GridColumnOrderChangeParams`
818
915
  Passed to `onColumnOrderChange`.
819
916
 
package/docs/README.md CHANGED
@@ -36,8 +36,9 @@ In-depth guides on the grid's functional capabilities.
36
36
  - **[Editing & Reordering](features/editing-reordering.md)** - Inline edits and Drag-and-Drop
37
37
  - **[Loading & Performance](features/loading-states.md)** - Skeletons, Lazy, and Infinite loading
38
38
  - **[Row Selection](features/selection.md)** - Multi & single choice selection
39
- - **[Clipboard](features/clipboard.md)** - Copy and Paste grid data
39
+ - **[Clipboard](features/clipboard.md)** - Copy rows and ranges, paste from Excel / Sheets, clear a range with Delete
40
40
  - **[Cell Range Selection](features/cell-selection.md)** - Spreadsheet-style cell ranges, copy and a sum / average status bar
41
+ - **[Undo & Redo](features/undo-redo.md)** - Ctrl+Z / Ctrl+Shift+Z for edits, pastes and range clears
41
42
  - **[Pinning](features/pinning.md)** - Sticky columns and rows
42
43
  - **[State Persistence](features/state-persistence.md)** - Save/Restore grid configuration
43
44
  - **[Aggregation & Pivot](features/aggregation-pivot.md)** - Data summarization and analytics
@@ -49,6 +50,7 @@ In-depth guides on the grid's functional capabilities.
49
50
  - **[Infinite Scroll](features/infinite-scroll.md)** - Advanced performance loading
50
51
  - **[Data Source](features/data-source.md)** - Server-side fetching and SSR
51
52
  - **[Export Guide](features/export-guide.md)** - Excel, CSV, JSON, and Print
53
+ - **[AI Toolkit](features/ai-toolkit.md)** - JSON Schema of the grid state for your own model, and a validator for its replies (`@opencorestack/opengridx/ai`)
52
54
 
53
55
  ---
54
56
 
@@ -20,6 +20,11 @@ in an effect, so **keep the call order when you move or add a hook**. In particu
20
20
  - `useGridCellSelection` runs after `useGridSpanning` (it needs `getSpanOrigin` and the span caches) and
21
21
  before `useGridKeyboardNavigation`, which receives its Shift-key handlers. `useGridCellSelectionApi` runs
22
22
  after the keyboard hook because it reads and sets `focusedCell`.
23
+ - `useGridBatchEdit` is created before `useGridEditing` (single edits store through it, so an undo right after an
24
+ edit reads the stored row before the next render). The history hook needs the focus and selection hooks, so it runs
25
+ late (36c); commits reach it through `useGridEditCommitChannel` (12a), whose listener it sets in a layout effect.
26
+ `useGridClipboardPaste` (36b) and `useGridUndoRedo` listen for keys on the grid root with native listeners, which
27
+ fire before React's delegated `onKeyDown` of the viewport; they act only on cell targets with no editor open.
23
28
  - `useGridLiveRowSelection` prunes the selection, and `useGridPageCorrection` corrects a page past the
24
29
  end, in effects that run before the later hooks' effects.
25
30
 
@@ -40,7 +45,9 @@ in an effect, so **keep the call order when you move or add a hook**. In particu
40
45
  | 10 | `useGridRowIdOf` | `hooks/core/useGridRowIdOf.ts` | `getRowIdOf` and its stable event-handler twin |
41
46
  | 11 | `useGridApiRefBinding` | `hooks/core/useGridApiRefBinding.ts` | Binds the `apiRef` prop (layout effect) |
42
47
  | 12 | `useGridHierarchy` | `hooks/core/useGridHierarchy.ts` | `useTreeData` + `useRowGrouping`, the active handlers and `rowMetaMap` |
43
- | 13 | `useGridEditing` | `hooks/features/useGridEditing.ts` | Cell editing state and handlers |
48
+ | 12a | `useGridEditCommitChannel` | `hooks/core/useGridUndoRedo.ts` | Stable `emit` / `setListener` pair: the commit paths below report stored changes through `emit`, and `useGridUndoRedo` (36c) registers the listener (v3.4) |
49
+ | 12b | `useGridBatchEdit` | `hooks/features/useGridBatchEdit.ts` | Batch edits (v3.4): `applyEdits` runs `valueSetter` → `processRowUpdate` once per row (rows in parallel, failures isolated and sent to `onProcessRowUpdateError`) and stores every successful row in one `replaceRows` update; `getRow` sees rows stored since the last render; `storeRow` stores a single edit |
50
+ | 13 | `useGridEditing` | `hooks/features/useGridEditing.ts` | Cell editing state and handlers; stores through `useGridBatchEdit.storeRow` and reports the committed change to the channel |
44
51
  | 14 | `useGridLiveRowSelection` | `hooks/core/useGridLiveRowSelection.ts` | Selection without removed rows |
45
52
  | 15 | `useGridDataSource` | `hooks/features/useGridDataSource.ts` | Server fetching |
46
53
  | 16 | `useServerTreeChildren` | `hooks/features/useServerTreeChildren.ts` | Lazy server tree children |
@@ -67,6 +74,8 @@ in an effect, so **keep the call order when you move or add a hook**. In particu
67
74
  | 35 | `useGridKeyboardNavigation` | `hooks/core/useGridKeyboardNavigation.ts` | Focus and keyboard handling; takes `cellSelection.keyboard` (Shift+navigation, Escape, Ctrl/Cmd+A) |
68
75
  | 36 | `useGridPointerFocusHandlers` | `hooks/core/useGridFocusHandlers.ts` | Cell and header click focus |
69
76
  | 36a | `useGridCellSelectionApi` | `hooks/core/useGridCellSelectionApi.ts` | Keeps the range anchor and the focused cell together (layout effect: a new anchor moves focus; focus moved to another data cell collapses the range), clears a range whose corner is gone (`'dataChange'`), and installs `getCellSelectionModel`, `setCellSelectionModel`, `selectCellRange`, `clearCellSelection`, `getSelectedCells`, `copySelectedCells` (once per `apiRef`, reading the latest values from a ref) |
77
+ | 36b | `useGridClipboardPaste` | `hooks/core/useGridClipboardPaste.ts` | Paste and range clear (v3.4): `paste` and Delete/Backspace listeners on the grid root (only for cell targets, not editors or nested grids), TSV parsing (`utils/parsing.ts`), placement against the renderable rows and range columns (`utils/editing/targets.ts`), the batch edit, the new selection, `onBeforeClipboardPaste` / `onClipboardPaste`, and `apiRef.pasteText`. Inert without `cellSelection` except for `pasteText` |
78
+ | 36c | `useGridUndoRedo` | `hooks/core/useGridUndoRedo.ts` | History (v3.4, `undoRedo`): records the channel's changes, replays them as batch edits with an `expect` value per cell (stale cells skipped), queues undo/redo, selects and scrolls to the affected cells, Ctrl/Cmd+Z / Shift+Z / Ctrl+Y on the grid root, `onHistoryChange`, and `apiRef.undo` / `redo` / `canUndo` / `canRedo` / `clearHistory` |
70
79
  | 37 | `useGridColumnMenuHandlers` | `hooks/core/useGridHeaderHandlers.ts` | Column menu Hide / Pin (`pinColumnTo` in `utils/pinning`) |
71
80
  | 38 | `useGridAriaRows` | `hooks/core/useGridAriaRows.ts` | `aria-rowcount` and `aria-rowindex` bases |
72
81
  | 39 | `useGridSpanRowWindow` | `hooks/features/useGridSpanRenderWindow.ts` | Render window widened to whole row spans |
@@ -340,4 +340,13 @@ const purpleTheme: GridTheme = {
340
340
  - The dark mode overrides are in the same file under `@media (prefers-color-scheme: dark)`
341
341
  - The CSS entry point for the entire library is `lib/styles/opengridx.css` (single barrel import)
342
342
  - BEM class prefix: `ogx__` (e.g. `ogx__cell`, `ogx__header-cell`)
343
+ - **Form controls** — the toolbar search, column-panel search, filter value box and the text / number / date cell
344
+ editors render through the shared `Input` (v3.3.1+): their `<input>` keeps its own class (`ogx-global-search__input`,
345
+ `ogx-column-visibility-panel__search-input`, `ogx-filter__value-input`, `ogx__edit-input`) and also has `ogx-input`,
346
+ and sits inside a `div.ogx-input-wrapper`. Target it with a descendant selector (`.ogx-global-search input`), not a
347
+ child one (`.ogx-global-search > input`). Cell editors' wrappers also have `ogx-input-wrapper--cell` (fills the cell,
348
+ no border, background or focus shadow; `ogx-input-wrapper--error` adds an inset `--ogx-color-error` edge). The boolean
349
+ editor and list-view row checkbox are the shared `Checkbox` (`ogx-checkbox-wrapper`, `ogx-checkbox__box`; the `<input>`
350
+ keeps `ogx__edit-checkbox` and is visually hidden). `Input.css` loads first in `opengridx.css`, so component rules
351
+ override it at equal specificity.
343
352
  - Variable prefix: `--ogx-` (e.g. `--ogx-color-primary`)
@@ -0,0 +1,119 @@
1
+ # AI Toolkit: Grid Schema & Validator
2
+
3
+ *Since v3.4.0* · Demo: [AI: Grid Schema & Validator](https://opencorestack.github.io/OpenGridX/ai-schema)
4
+
5
+ Let users drive the grid in plain language ("paid orders over $10k in the North, biggest first") with the model your app already uses. The `@opencorestack/opengridx/ai` entry point gives you the two pieces the grid can own:
6
+
7
+ - **`getGridAiSchema(columns, options?)`** turns your column definitions into a JSON Schema (draft 2020-12) of the grid state a model may return: filters, sort, row grouping, aggregation, pivot and column visibility, each limited to what the columns allow.
8
+ - **`validateGridAiState(json, columns, options?)`** checks the model's reply against the columns and returns the state that is safe to apply, plus a list of what it dropped.
9
+
10
+ OpenGridX never calls an AI service and bundles no AI SDK. Your app sends the prompt, the schema and the current state to its own model (with structured output), and passes the reply to the validator.
11
+
12
+ ```ts
13
+ import { getGridAiSchema, validateGridAiState } from '@opencorestack/opengridx/ai';
14
+
15
+ const schema = getGridAiSchema(columns, { include: ['filter', 'sort', 'grouping', 'aggregation'] });
16
+
17
+ async function ask(prompt: string) {
18
+ // Your own endpoint calls your own model, with `schema` as the structured-output format.
19
+ const res = await fetch('/api/grid-assistant', {
20
+ method: 'POST',
21
+ headers: { 'Content-Type': 'application/json' },
22
+ body: JSON.stringify({ prompt, schema, currentState: { filterModel, sortModel } }),
23
+ });
24
+ const { state, errors } = validateGridAiState(await res.text(), columns);
25
+ if (errors.length) console.info('Dropped from the reply:', errors);
26
+ if (state.filterModel) setFilterModel(state.filterModel);
27
+ if (state.sortModel) setSortModel(state.sortModel);
28
+ if (state.rowGroupingModel) setRowGroupingModel(state.rowGroupingModel);
29
+ if (state.aggregationModel) setAggregationModel(state.aggregationModel);
30
+ }
31
+ ```
32
+
33
+ The entry point has no React import and no dependencies (about 4.5 kB gzipped). Importing `@opencorestack/opengridx` never loads it, and it works on a server too, so you can validate the reply in the same endpoint that calls the model.
34
+
35
+ ## Privacy rules
36
+
37
+ - **No row data by default.** The schema is built from column definitions only: `field`, `headerName`, `description`, `type`, the operators of the type, `valueOptions` and the allowed aggregation functions. The grid never reads a row for it.
38
+ - **`aiExamples` is real data.** `GridColDef.aiExamples` puts example values into the schema to help the model ("Acme" → `customer`). Those values are sent to the model. List only what you are happy to share; leave it out for anything personal. `options.examples: false` drops all of them.
39
+ - **`valueOptions` are sent.** The options of a `singleSelect` column become an `enum` in the schema. If an option list is itself sensitive, mark the column `filterable: false` or pass a copy of the columns without it.
40
+ - **Always validate before applying.** Model output is untrusted input. Apply `state`, never the raw reply; show `errors` (or log them) so users can see what was ignored. Show the change to the user and keep it easy to undo.
41
+ - **Check `schemaVersion`.** The schema carries `schemaVersion: 1`. It changes only when the shape of the schema changes, so you can cache prompts or fine-tuning data per version. The validator accepts a `schemaVersion` key in the reply and reports any value other than 1.
42
+
43
+ ## `getGridAiSchema(columns, options?)`
44
+
45
+ Returns a plain JSON object (no functions), deterministic for the same columns and options, so you can memoise it with the columns.
46
+
47
+ | Part (state key) | Option name | Shape | Taken from the columns |
48
+ | :--- | :--- | :--- | :--- |
49
+ | `filterModel` | `'filter'` | `GridFilterModel`: `items`, `logicOperator`, nested AND/OR groups up to 3 levels, `quickFilterValues` | Fields with `filterable !== false`; per field the operators of `getOperatorsForType(type)`; `value` typed per column type (number, `YYYY-MM-DD` string, boolean, `valueOptions` enum or a list of them for `isAnyOf`) |
50
+ | `sortModel` | `'sort'` | `GridSortItem[]` | Fields with `sortable !== false` |
51
+ | `rowGroupingModel` | `'grouping'` | `string[]` | Fields with `groupable !== false` |
52
+ | `aggregationModel` | `'aggregation'` | field → function | `type: 'number'` (or `aggregable: true`) columns get every built-in function, other columns `count`; never `aggregable: false`; limited by `availableAggregationFunctions` |
53
+ | `pivotModel` | `'pivot'` | `GridPivotModel` | Groupable fields as row and column labels; value fields as for aggregation (`sum`, `avg`, `count`, `min`, `max`) |
54
+ | `columnVisibilityModel` | `'columnVisibility'` | field → boolean | Fields with `hideable !== false` |
55
+
56
+ Each filter item is a `oneOf` per field (`field` is a `const`, `operator` an `enum`, `value` typed by the column), which the main structured-output APIs accept. A part that no column allows is left out of the schema.
57
+
58
+ | Option | Default | Description |
59
+ | :--- | :--- | :--- |
60
+ | `include` | every part | Parts to offer: `'filter' \| 'sort' \| 'grouping' \| 'aggregation' \| 'pivot' \| 'columnVisibility'` |
61
+ | `exclude` | none | Parts to leave out; wins over `include` |
62
+ | `descriptions` | `true` | Put each column's `headerName` and `description` into the schema, so the model can map the user's words ("revenue") to a field (`amt_usd`) |
63
+ | `examples` | `true` | Include the columns' `aiExamples`. Columns without `aiExamples` never send example values |
64
+
65
+ Columns whose field starts with `__` (the grid's own `__check__`, `__group__` …) are skipped, so `apiRef.current.getAllColumns()` can be passed as is. The `columns` parameter takes any object with the `GridColDef` properties above (`GridAiColumn`); renderers, getters and formatters are ignored.
66
+
67
+ Example for two columns:
68
+
69
+ ```ts
70
+ getGridAiSchema(
71
+ [
72
+ { field: 'amt_usd', headerName: 'Revenue', type: 'number' },
73
+ { field: 'status', headerName: 'Status', type: 'singleSelect', valueOptions: ['Open', 'Paid'], groupable: false },
74
+ ],
75
+ { include: ['filter', 'sort'], descriptions: false },
76
+ );
77
+ // {
78
+ // $schema: 'https://json-schema.org/draft/2020-12/schema', schemaVersion: 1, title: 'OpenGridX grid state',
79
+ // type: 'object', additionalProperties: false,
80
+ // properties: {
81
+ // filterModel: { type: 'object', additionalProperties: false, properties: {
82
+ // items: { type: 'array', items: { anyOf: [{ $ref: '#/$defs/filterItem' }, { $ref: '#/$defs/filterGroup1' }] } },
83
+ // logicOperator: { enum: ['and', 'or'] },
84
+ // quickFilterValues: { type: 'array', items: { type: 'string' } } } },
85
+ // sortModel: { type: 'array', items: { type: 'object', additionalProperties: false, required: ['field', 'sort'],
86
+ // properties: { field: { enum: ['amt_usd', 'status'] }, sort: { enum: ['asc', 'desc'] } } } },
87
+ // },
88
+ // $defs: {
89
+ // filterItem: { oneOf: [
90
+ // { type: 'object', required: ['field', 'operator'], additionalProperties: false, properties: {
91
+ // field: { const: 'amt_usd' }, operator: { enum: ['=', '!=', '>', '>=', '<', '<=', 'isEmpty', 'isNotEmpty'] },
92
+ // value: { type: 'number' } } },
93
+ // { type: 'object', required: ['field', 'operator'], additionalProperties: false, properties: {
94
+ // field: { const: 'status' }, operator: { enum: ['isAnyOf', 'is', 'not'] },
95
+ // value: { anyOf: [{ enum: ['Open', 'Paid'] }, { type: 'array', items: { enum: ['Open', 'Paid'] } }] } } },
96
+ // ] },
97
+ // filterGroup1: { … items: filterItem | filterGroup2 }, filterGroup2: { … }, filterGroup3: { … items: filterItem },
98
+ // },
99
+ // }
100
+ ```
101
+
102
+ ## `validateGridAiState(json, columns, options?)`
103
+
104
+ Returns `{ state, errors }`, where `state` is a `GridAiState` (`filterModel`, `sortModel`, `rowGroupingModel`, `aggregationModel`, `pivotModel`, `columnVisibilityModel`, each present only when the reply contained it) and `errors` is a list of `{ path, message }`, such as `{ path: 'filterModel.items[2].field', message: 'Unknown field "revenue"' }`.
105
+
106
+ - `json` may be an object or a JSON string. Bad JSON gives one error and an empty state.
107
+ - Unknown parts, unknown fields, fields a column does not allow (`filterable: false` …), operators that do not fit the column type, and functions or enum values outside the allowed list are **dropped**, each with an error. Parts outside `options.include` / `options.exclude` are dropped too.
108
+ - Values are coerced to the column type with the same rules as clipboard paste: `"1,200"`, `"$1,200"` → `1200`, `"12%"` → `0.12`; `"yes"` / `"no"` / `"1"` / `"0"` → booleans; dates (ISO or anything `Date.parse` reads) → the local day as `"YYYY-MM-DD"`, as the filter panel writes it; `singleSelect` values match an option value or label, case-insensitively, and become the option's value. A filter without a usable value is dropped.
109
+ - `asc` / `desc`, `and` / `or`, operators and function names are matched case-insensitively and written in their canonical spelling.
110
+ - A filter group left empty is removed; groups nested deeper than 3 levels are dropped.
111
+ - Lists are read up to 200 entries and at most 100 errors are listed, so a runaway reply stays cheap.
112
+ - It never throws, never returns a field the columns do not allow, and reads only own keys of the reply (never `__proto__`), so a reply cannot reach `Object.prototype`.
113
+
114
+ Applying an empty part is meaningful: `{ sortModel: [] }` clears the sort. Only apply the parts present in `state`.
115
+
116
+ ## Related
117
+
118
+ - [Filtering](filtering.md), [Sorting & Pagination](sorting-pagination.md), [Aggregation & Pivot](aggregation-pivot.md), [Tree Data & Grouping](tree-data-grouping.md): the models the schema describes.
119
+ - [API Reference: AI Toolkit](../API_REFERENCE.md)
@@ -101,3 +101,91 @@ ID Name Email Status
101
101
  ```
102
102
 
103
103
  When pasted into Excel or Google Sheets, this content will automatically be distributed into the correct cells.
104
+
105
+ ---
106
+
107
+ ## 📥 Paste from Excel and Google Sheets (v3.4)
108
+
109
+ With `cellSelection` on, **Ctrl/Cmd+V** pastes tab-separated text into the grid's **editable** cells. Each value is parsed for its column and saved like an edit: `valueSetter`, then `processRowUpdate` **once per row**, then the grid stores every row that succeeded in one update. Demo: **Paste from Excel** (`/clipboard-paste`).
110
+
111
+ ```tsx
112
+ <DataGrid
113
+ rows={rows}
114
+ columns={columns} // columns with editable: true
115
+ cellSelection
116
+ processRowUpdate={saveRow}
117
+ onClipboardPaste={({ updated, skipped, failed, text }) =>
118
+ toast(`${updated.length} rows updated, ${skipped.length} cells skipped`)}
119
+ />
120
+ ```
121
+
122
+ - Paste works while focus is in the grid and no editor is open. In an open editor, Ctrl/Cmd+V is the input's own paste.
123
+ - The grid reads the `paste` event (`text/plain`), so the browser asks for no clipboard permission.
124
+ - `disableClipboardPaste` turns the shortcut off. `apiRef.current.pasteText(text, anchor?)` still works, for a toolbar "Paste" button that reads the clipboard itself.
125
+
126
+ ### Where values land
127
+
128
+ | Clipboard | Selection | Result |
129
+ | :--- | :--- | :--- |
130
+ | One value | One cell | That cell |
131
+ | One value | A range | Every cell of the range (Excel's "fill selection") |
132
+ | A block | One cell | The block, from that cell to the right and down |
133
+ | A block | A range | The block from the range's top-left cell; cells past the range, the last row or the last column are dropped |
134
+
135
+ No rows are created. Only displayed rows are reached: rows on other pages and inside collapsed groups are not. After the paste the pasted area becomes the selection, and one Ctrl/Cmd+Z undoes all of it (with `undoRedo`).
136
+
137
+ ### How text is parsed
138
+
139
+ The clipboard is read as Excel and Sheets write it: tabs between columns, `\r\n` or `\n` between rows, quoted fields that may hold tabs, line breaks and doubled quotes. A trailing empty line is ignored. Each cell is then parsed for its column:
140
+
141
+ | Column type | Accepted | Otherwise |
142
+ | :--- | :--- | :--- |
143
+ | `string` (default), `image` | The text as is (an `image` column takes the URL) | — |
144
+ | `number` | `1234.5`, `1,234.5`, `-12`, `(12)`, `12%` (→ 0.12), `1e3`; currency symbols and spaces stripped | skipped, `'invalidValue'` |
145
+ | `date` | ISO `2026-10-07` (that local day), and what `Date.parse` accepts. The value keeps the kind the cell holds: a `Date`, a timestamp, or a `YYYY-MM-DD` string | skipped |
146
+ | `boolean` | `true`/`false`, `yes`/`no` (the grid's `Yes`/`No` labels), `1`/`0`, any case | skipped |
147
+ | `singleSelect` | An option value or an option label, any case; the option's value is stored | skipped |
148
+ | An empty cell | `null` for number, date, boolean and singleSelect; `''` for string | — |
149
+
150
+ `1,5` is not read as a number: in a decimal-comma locale use a `valueParser`.
151
+
152
+ ### `valueParser`
153
+
154
+ `GridColDef.valueParser(text, { row, field, colDef })` replaces the parser for one column. Throw (or return `undefined`) to reject the text: the cell is skipped with reason `'invalidValue'`.
155
+
156
+ ```tsx
157
+ {
158
+ field: 'price',
159
+ type: 'number',
160
+ editable: true,
161
+ // "1.234,5" → 1234.5
162
+ valueParser: (text) => {
163
+ const n = Number(text.replace(/[\s€]/g, '').split('.').join('').replace(',', '.'));
164
+ if (Number.isNaN(n)) throw new Error('not a number');
165
+ return n;
166
+ },
167
+ }
168
+ ```
169
+
170
+ ### What is skipped
171
+
172
+ `result.skipped` lists every cell that was not written, with a reason:
173
+
174
+ | Reason | When |
175
+ | :--- | :--- |
176
+ | `'notEditable'` | The column is not `editable`, `isCellEditable` returned false, the row is a synthetic row (group header, subtotal, tree auto-parent, pivot Grand Total), or the cell is covered by a span |
177
+ | `'invalidValue'` | The text does not parse for the column type, or `valueParser` rejected it |
178
+
179
+ A row whose `valueSetter` or `processRowUpdate` throws or rejects is listed in `result.failed` and reported to `onProcessRowUpdateError`, as for a single edit; the other rows are still saved. Cells that already hold the pasted value are left alone.
180
+
181
+ ### Events
182
+
183
+ - `onClipboardPaste(result)` fires after every paste, once every row has settled: `{ updated, failed, skipped, text }`.
184
+ - `onBeforeClipboardPaste({ text, anchor })` runs first. Return `false` to cancel the paste, or a string to paste instead (for example to strip a header line). A throw cancels the paste.
185
+ - Above 10,000 cells a paste logs a development warning.
186
+
187
+ ---
188
+
189
+ ## 🧹 Clear a range with Delete (v3.4)
190
+
191
+ With `cellSelection` on and a range of **more than one cell**, **Delete** or **Backspace** empties every editable cell of the range: `''` in string and image columns, `null` elsewhere. It is one batch edit through `valueSetter` and `processRowUpdate`, so one undo. Non-editable cells and synthetic rows are left alone. On a single cell nothing changes (Delete is not bound). `disableRangeClear` turns it off; `disableClipboardPaste` does not affect it.
@@ -115,6 +115,56 @@ const columns: GridColDef[] = [
115
115
 
116
116
  Enter and Escape keydowns that bubble out of a custom editor also commit and cancel. An error thrown by `renderEditCell` is contained to its cell (the cell shows `⚠`), like `renderCell`.
117
117
 
118
+ ### Building custom editors with `Input`
119
+ The built-in text, number and date editors are the exported `Input` component with `variant="cell"` (since v3.3.1). Use it in `renderEditCell` and a custom editor looks like the built-in ones: it fills the cell, has no border, background or focus ring of its own (the editing cell's outline shows), and uses the cell's font.
120
+
121
+ ```tsx
122
+ import { useEffect, useRef, useState } from 'react';
123
+ import { Input } from '@opencorestack/opengridx';
124
+ import type { GridColDef, GridRenderEditCellParams } from '@opencorestack/opengridx';
125
+
126
+ const isAmount = (t: string) => t.trim() !== '' && Number(t) >= 0;
127
+
128
+ function PriceEditor({ value, onValueChange, onCommit, onCancel }: GridRenderEditCellParams) {
129
+ const [text, setText] = useState(String(value ?? ''));
130
+ const ref = useRef<HTMLInputElement>(null);
131
+ useEffect(() => { ref.current?.focus(); ref.current?.select(); }, []);
132
+ const valid = isAmount(text);
133
+
134
+ return (
135
+ <Input
136
+ ref={ref} // reaches the <input>, on React 18 and 19
137
+ variant="cell"
138
+ startAdornment="$" // or endAdornment="kg"
139
+ error={!valid} // red inner edge + aria-invalid
140
+ inputMode="decimal"
141
+ value={text}
142
+ onChange={e => {
143
+ setText(e.target.value);
144
+ if (isAmount(e.target.value)) onValueChange(Number(e.target.value));
145
+ }}
146
+ onKeyDown={e => {
147
+ if (e.key === 'Enter') { e.stopPropagation(); if (valid) onCommit(); }
148
+ if (e.key === 'Escape') { e.stopPropagation(); onCancel(); }
149
+ }}
150
+ onBlur={() => (valid ? onCommit() : onCancel())}
151
+ />
152
+ );
153
+ }
154
+
155
+ const columns: GridColDef[] = [
156
+ { field: 'price', type: 'number', editable: true, renderEditCell: params => <PriceEditor {...params} /> },
157
+ ];
158
+ ```
159
+
160
+ - `ref` reaches the `<input>` (focus it and select its text when the editor opens, as the built-in editors do).
161
+ - `inputClassName` adds classes to the `<input>`; `className` goes to the `.ogx-input-wrapper` div around it.
162
+ - `error` adds `ogx-input-wrapper--error` (an inset `--ogx-color-error` edge in the cell variant, drawn over the cell outline) and `aria-invalid="true"`.
163
+ - Stop propagation of the Enter / Escape you handle yourself, or the grid also commits / cancels when they bubble out.
164
+ - The grid's boolean editor is the exported `Checkbox`; its `<input>` is visually hidden and `ref` / `inputClassName` reach it.
165
+
166
+ Live example: the *Custom Cell Editors* demo page (`/custom-editors`).
167
+
118
168
  ### Computed columns (`valueGetter` + `valueSetter`)
119
169
  An editable column with a `valueGetter` edits the derived value. Give it a `valueSetter` so the commit can write the value back to the fields it comes from; otherwise the commit writes `row[field]`, which the `valueGetter` ignores, and a development warning is logged.
120
170
 
@@ -37,6 +37,17 @@ With [`cellSelection`](cell-selection.md) on (v3.3), on a data cell:
37
37
  | **Ctrl+Shift+Home** / **Ctrl+Shift+End** (Cmd on macOS) | Extend to the first / last cell of the page. |
38
38
  | **Shift+PageUp** / **Shift+PageDown** | Extend one page up / down. |
39
39
  | **Escape** | Collapse the range to the anchor (when not editing). |
40
+ | **Ctrl+V** (Cmd+V) | Paste tab-separated text into the editable cells from the range's top-left cell (v3.4, see [Clipboard](clipboard.md#-paste-from-excel-and-google-sheets-v34)). Off with `disableClipboardPaste`. |
41
+ | **Delete** / **Backspace** | On a range of more than one cell: empty its editable cells in one edit (v3.4). Off with `disableRangeClear`. |
42
+
43
+ With `undoRedo` on (v3.4, see [Undo & Redo](undo-redo.md)), while focus is on a cell and no editor is open:
44
+
45
+ | Key | Action |
46
+ | :--- | :--- |
47
+ | **Ctrl+Z** (Cmd+Z) | Undo the last edit, paste or range clear. |
48
+ | **Ctrl+Shift+Z** (Cmd+Shift+Z) or **Ctrl+Y** | Redo. |
49
+
50
+ Inside an open editor these keys belong to the input (its own paste and undo).
40
51
 
41
52
  A plain arrow key after a range collapses it to the newly focused cell. Shift+Space still selects the row.
42
53
 
@@ -0,0 +1,79 @@
1
+ # Undo & Redo
2
+
3
+ `undoRedo` (v3.4) lets users undo and redo their cell edits, pastes and range clears. It is off by default: your app may already own Ctrl+Z, and undo writes data. Demo: **Undo & Redo** (`/undo-redo`).
4
+
5
+ ```tsx
6
+ import { DataGrid, useGridApiRef, type GridHistoryChangeParams } from '@opencorestack/opengridx';
7
+
8
+ function Budget({ rows, columns, saveRow }) {
9
+ const apiRef = useGridApiRef();
10
+ const [history, setHistory] = useState<GridHistoryChangeParams>({ canUndo: false, canRedo: false, size: 0 });
11
+
12
+ return (
13
+ <>
14
+ <button disabled={!history.canUndo} onClick={() => apiRef.current.undo()}>Undo</button>
15
+ <button disabled={!history.canRedo} onClick={() => apiRef.current.redo()}>Redo</button>
16
+ <DataGrid
17
+ rows={rows}
18
+ columns={columns}
19
+ apiRef={apiRef}
20
+ cellSelection // for paste and range clear; undo works without it
21
+ undoRedo // or { limit: 50 }; default 100 actions
22
+ onHistoryChange={setHistory}
23
+ processRowUpdate={saveRow} // undo and redo save through it too
24
+ />
25
+ </>
26
+ );
27
+ }
28
+ ```
29
+
30
+ ## Keys
31
+
32
+ | Key | Action |
33
+ | :--- | :--- |
34
+ | **Ctrl+Z** (Cmd+Z) | Undo |
35
+ | **Ctrl+Shift+Z** (Cmd+Shift+Z) or **Ctrl+Y** | Redo |
36
+
37
+ Only while focus is in the grid (a cell) and no editor is open. Inside an editor, a text field you render in a cell, or anywhere else on the page, the keys keep their native meaning (the input's own undo). After an undo or redo the affected cells become the selection (their bounding rectangle, with `cellSelection`; otherwise focus moves to the first one) and the first one is scrolled into view.
38
+
39
+ ## What is recorded
40
+
41
+ Each committed action is a list of cell changes `{ id, field, before, after }`:
42
+
43
+ | Action | Recorded |
44
+ | :--- | :--- |
45
+ | A single-cell edit | 1 change |
46
+ | A paste or a range clear | Every cell that was written (skipped cells are not) |
47
+ | Values `processRowUpdate` changed itself (a recomputed `total`) | Not recorded: only the fields the user changed. Your recomputation runs again on undo |
48
+
49
+ An action is recorded **only after `processRowUpdate` succeeded**, with `after` read from the stored row through `valueGetter`. A row that `processRowUpdate` rejected is not recorded, so history never claims a change your app refused. A change whose stored value equals the old one (for example normalised back by `processRowUpdate`) is not recorded either.
50
+
51
+ A new action after an undo clears the redo stack. Beyond `limit` actions the oldest is dropped.
52
+
53
+ **Not undoable:** sorting, filtering, column changes, row reordering, row selection and cell selection. These are view state, not data.
54
+
55
+ ## How undo writes
56
+
57
+ Undo writes the `before` values as a new batch edit, so `valueSetter` and `processRowUpdate` run as for any edit and your app persists the undo the way it persists edits. Redo writes the `after` values. One `processRowUpdate` call per row; rows run in parallel; a row that fails is reported to `onProcessRowUpdateError`, keeps its part of the action on the stack to try again, and does not stop the others.
58
+
59
+ ### Stale entries
60
+
61
+ If a cell no longer holds its recorded `after` value (it was edited again, or the `rows` prop brought new data), undo **skips that cell** with reason `'changed'` and applies the rest. A row that no longer exists is skipped with reason `'missing'`. Values are compared, not row objects, so an app that re-fetches its rows after saving keeps a working history. Redo checks the same way against the value undo wrote.
62
+
63
+ ```ts
64
+ const result = await apiRef.current.undo();
65
+ // { updated: [3, 7], failed: [], skipped: [{ id: 5, field: 'price', reason: 'changed' }] }
66
+ ```
67
+
68
+ ## API
69
+
70
+ | Member | Description |
71
+ | :--- | :--- |
72
+ | `undoRedo?: boolean \| { limit?: number }` | Turns history on. Default `false`; `limit` default 100. Turning it off clears the history. |
73
+ | `onHistoryChange?({ canUndo, canRedo, size })` | Fired when what can be undone or redone changes. `size` is the number of actions that can be undone. |
74
+ | `apiRef.current.undo()` | Undoes the last action. Resolves with a `GridBatchEditResult` once every row has settled. |
75
+ | `apiRef.current.redo()` | Redoes the last undone action. |
76
+ | `apiRef.current.canUndo()` / `canRedo()` | Whether there is something to undo / redo. |
77
+ | `apiRef.current.clearHistory()` | Forgets every action. |
78
+
79
+ Undo and redo calls run one after another, each on the rows the previous one stored.
package/docs/roadmap.md CHANGED
@@ -83,7 +83,7 @@ This document tracks the current status of features in `OpenGridX` and outlines
83
83
  | Release | Target | Headline | Also in it |
84
84
  | :--- | :--- | :--- | :--- |
85
85
  | **3.3.0** ✅ | shipped 7 Oct | **Cell range selection** (opt-in `cellSelection` prop): mouse drag, Shift+click, Shift+arrows; Ctrl+C / Cmd+C copies the block as TSV | Selection status bar (count, sum, average of selected cells); wrapped header text; React Compiler compatibility check |
86
- | **3.4.0** | 20 Nov | **Paste from Excel / Google Sheets** into the selection, through `processRowUpdate` (values parsed per column type, non-editable cells skipped, `onClipboardPasteError`). **Undo / redo** for cell edits and pastes (Ctrl+Z, Ctrl+Shift+Z / Ctrl+Y, `apiRef.undo()` / `redo()`) | **AI toolkit, part 1** in a new `@opencorestack/opengridx/ai` entry point: `getGridAiSchema(columns)` (JSON Schema of the filter, sort, grouping, aggregation, pivot and visibility models the columns allow) and `validateGridAiState(json, columns)` (drops unknown fields and operators, coerces values, returns errors) |
86
+ | **3.4.0** ✅ | shipped 8 Oct | **Paste from Excel / Google Sheets** into the selection, through `processRowUpdate` (values parsed per column type or `valueParser`, non-editable cells skipped, `onClipboardPaste` summary); **Delete clears a range**; built-in inputs and editors use the shared `Input` / `Checkbox` (`Input variant="cell"`). **Undo / redo** for cell edits and pastes (Ctrl+Z, Ctrl+Shift+Z / Ctrl+Y, `apiRef.undo()` / `redo()`) | **AI toolkit, part 1** in a new `@opencorestack/opengridx/ai` entry point: `getGridAiSchema(columns)` (JSON Schema of the filter, sort, grouping, aggregation, pivot and visibility models the columns allow) and `validateGridAiState(json, columns)` (drops unknown fields and operators, coerces values, returns errors) |
87
87
  | **3.5.0** | 15 Dec | **AI toolkit, part 2**: `aiAssistant={{ onPrompt }}` prompt panel that calls the app's own model, shows the proposed changes as removable chips and applies them with undo; `createGridAgentTools(apiRef)` (plain tool objects for CopilotKit, the Vercel AI SDK or WebMCP) | Header filter row; **fill handle** as a stretch goal (moves to Q1 2027 if 3.3–3.4 run late) |
88
88
  | Freeze | 19 Dec – 5 Jan | Bug fixes only | — |
89
89