@jielga/tmdatagrid 2.0.0-beta.2 → 2.0.0-beta.21

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 (150) hide show
  1. package/README.md +5 -212
  2. package/dist/index.d.ts +1664 -632
  3. package/dist/index.js +5226 -3223
  4. package/dist/index.js.map +1 -1
  5. package/dist/styles.css +1 -1
  6. package/docs/anatomy.md +102 -0
  7. package/docs/cell-selection.md +154 -0
  8. package/docs/column-layout.md +204 -0
  9. package/docs/columns.md +262 -0
  10. package/docs/components.md +304 -0
  11. package/docs/editing.md +603 -0
  12. package/docs/editors.md +250 -0
  13. package/docs/export.md +326 -0
  14. package/docs/filtering.md +358 -0
  15. package/docs/getting-started.md +123 -0
  16. package/docs/grouping.md +165 -0
  17. package/docs/loading-and-empty.md +92 -0
  18. package/docs/localization.md +79 -0
  19. package/docs/menu.md +143 -0
  20. package/docs/pagination.md +144 -0
  21. package/docs/persistence.md +111 -0
  22. package/docs/portfolio-rebalancer.md +94 -0
  23. package/docs/query-builder.md +175 -0
  24. package/docs/quick-search.md +83 -0
  25. package/docs/row-details.md +113 -0
  26. package/docs/row-interaction.md +148 -0
  27. package/docs/row-pinning.md +132 -0
  28. package/docs/row-selection.md +134 -0
  29. package/docs/row-styling.md +133 -0
  30. package/docs/scrolling.md +111 -0
  31. package/docs/server-query.md +246 -0
  32. package/docs/server-side.md +206 -0
  33. package/docs/sorting.md +101 -0
  34. package/docs/styling.md +126 -0
  35. package/docs/summary-row.md +76 -0
  36. package/docs/testing.md +309 -0
  37. package/docs/toolbar.md +161 -0
  38. package/docs/use-tm-data-grid.md +361 -0
  39. package/package.json +21 -45
  40. package/skills/appearance/SKILL.md +70 -17
  41. package/skills/cell-selection/SKILL.md +70 -76
  42. package/skills/columns/SKILL.md +131 -32
  43. package/skills/data/SKILL.md +100 -23
  44. package/skills/editing/SKILL.md +217 -96
  45. package/skills/editing/references/common-mistakes.md +111 -24
  46. package/skills/editing/references/editing-api.md +63 -39
  47. package/skills/editing/references/editors-and-validation.md +77 -19
  48. package/skills/filtering/SKILL.md +148 -40
  49. package/skills/getting-started/SKILL.md +18 -16
  50. package/skills/grouping/SKILL.md +32 -15
  51. package/skills/options/SKILL.md +39 -9
  52. package/skills/rows/SKILL.md +22 -18
  53. package/skills/server-side/SKILL.md +170 -17
  54. package/skills/testing/SKILL.md +10 -7
  55. package/src/{tmdatagrid/TMDataGridContext.ts → TMDataGridContext.ts} +7 -19
  56. package/src/{tmdatagrid/components → components}/TMDataGrid.module.css +7 -1
  57. package/src/{tmdatagrid/components → components}/TMDataGrid.tsx +39 -23
  58. package/src/{tmdatagrid/components → components}/TMDataGridCellEditor.tsx +106 -38
  59. package/src/{tmdatagrid/components → components}/TMDataGridColumnsPanel.module.css +5 -1
  60. package/src/{tmdatagrid/components → components}/TMDataGridColumnsPanel.tsx +47 -55
  61. package/src/{tmdatagrid/components → components}/TMDataGridDetailsColumn.tsx +4 -4
  62. package/src/components/TMDataGridDraftActions.tsx +307 -0
  63. package/src/{tmdatagrid/components → components}/TMDataGridEditColumn.tsx +58 -50
  64. package/src/{tmdatagrid/components → components}/TMDataGridEntryRows.tsx +150 -115
  65. package/src/components/TMDataGridExportPicker.module.css +77 -0
  66. package/src/components/TMDataGridExportPicker.tsx +234 -0
  67. package/src/components/TMDataGridFilterPanel.module.css +54 -0
  68. package/src/components/TMDataGridFilterPanel.tsx +348 -0
  69. package/src/{tmdatagrid/components → components}/TMDataGridFilterPills.tsx +7 -5
  70. package/src/components/TMDataGridFilterSurface.module.css +54 -0
  71. package/src/components/TMDataGridFilterSurface.tsx +167 -0
  72. package/src/{tmdatagrid/components → components}/TMDataGridFooter.tsx +53 -13
  73. package/src/{tmdatagrid/components → components}/TMDataGridGroupColumn.tsx +4 -3
  74. package/src/{tmdatagrid/components → components}/TMDataGridHeaderCell.module.css +10 -0
  75. package/src/{tmdatagrid/components → components}/TMDataGridHeaderCell.tsx +100 -28
  76. package/src/components/TMDataGridHeaderFilterRow.module.css +51 -0
  77. package/src/components/TMDataGridHeaderFilterRow.tsx +301 -0
  78. package/src/components/TMDataGridMenu.tsx +354 -0
  79. package/src/{tmdatagrid/components → components}/TMDataGridSelectColumn.tsx +12 -7
  80. package/src/{tmdatagrid/components → components}/TMDataGridTable.module.css +90 -67
  81. package/src/{tmdatagrid/components → components}/TMDataGridTable.tsx +678 -156
  82. package/src/components/TMDataGridToolbar.module.css +21 -0
  83. package/src/components/TMDataGridToolbar.tsx +181 -0
  84. package/src/{tmdatagrid/components → components}/editors/TMDataGridBooleanEditor.tsx +3 -3
  85. package/src/{tmdatagrid/components → components}/editors/TMDataGridDateEditor.tsx +3 -3
  86. package/src/{tmdatagrid/components → components}/editors/TMDataGridMultiSelectEditor.tsx +3 -3
  87. package/src/components/editors/TMDataGridNumberEditor.tsx +70 -0
  88. package/src/{tmdatagrid/components → components}/editors/TMDataGridSelectEditor.tsx +3 -3
  89. package/src/{tmdatagrid/components → components}/editors/TMDataGridStringEditor.tsx +3 -3
  90. package/src/{tmdatagrid/components → components}/editors/editorShared.ts +17 -31
  91. package/src/{tmdatagrid/components → components}/filters/DgAutocompleteFilter.tsx +5 -4
  92. package/src/{tmdatagrid/components → components}/filters/DgDateRangeFilter.tsx +22 -5
  93. package/src/{tmdatagrid/components → components}/filters/DgRangeSliderFilter.tsx +5 -1
  94. package/src/{tmdatagrid/components → components}/filters/DgTriStateFilter.tsx +5 -1
  95. package/src/{tmdatagrid/components → components}/filters/TMDataGridFilterValueInput.tsx +44 -28
  96. package/src/components/filters/controlLayout.ts +32 -0
  97. package/src/components/filters/filterControlFor.ts +65 -0
  98. package/src/{tmdatagrid/components → components}/icons.ts +1 -0
  99. package/src/{tmdatagrid/components → components}/sticky.module.css +44 -0
  100. package/src/components/useHideableColumns.ts +52 -0
  101. package/src/{tmdatagrid/core → core}/autosize.ts +5 -2
  102. package/src/{tmdatagrid/core → core}/capabilities.ts +14 -6
  103. package/src/{tmdatagrid/core → core}/columnOptions.ts +46 -0
  104. package/src/{tmdatagrid/core → core}/columnOrdering.ts +33 -14
  105. package/src/{tmdatagrid/core → core}/columnUtils.ts +44 -5
  106. package/src/core/controlledState.ts +179 -0
  107. package/src/core/controlledStateSync.ts +108 -0
  108. package/src/core/deletedRows.ts +34 -0
  109. package/src/core/dom.ts +74 -0
  110. package/src/core/editEngine.ts +2476 -0
  111. package/src/{tmdatagrid/core → core}/editorFocus.ts +8 -4
  112. package/src/core/export.ts +843 -0
  113. package/src/{tmdatagrid/core → core}/filterControls.ts +38 -1
  114. package/src/{tmdatagrid/core → core}/filterOperators.ts +64 -1
  115. package/src/core/filterSurface.ts +99 -0
  116. package/src/{tmdatagrid/core → core}/labels.ts +66 -8
  117. package/src/{tmdatagrid/core → core}/labelsSv.ts +26 -3
  118. package/src/core/pageReset.ts +120 -0
  119. package/src/{tmdatagrid/core → core}/persistence.ts +22 -5
  120. package/src/core/resizePreview.ts +141 -0
  121. package/src/core/summary.ts +59 -0
  122. package/src/core/useSettledTableState.ts +36 -0
  123. package/src/{tmdatagrid/index.ts → index.ts} +75 -12
  124. package/src/{tmdatagrid/useTMDataGrid.tsx → useTMDataGrid.tsx} +734 -135
  125. package/src/useTMDataGridExport.ts +78 -0
  126. package/src/tmdatagrid/components/TMDataGridEditActions.tsx +0 -162
  127. package/src/tmdatagrid/components/TMDataGridFilterPanel.module.css +0 -35
  128. package/src/tmdatagrid/components/TMDataGridFilterPanel.tsx +0 -354
  129. package/src/tmdatagrid/components/TMDataGridToolbar.module.css +0 -12
  130. package/src/tmdatagrid/components/TMDataGridToolbar.tsx +0 -162
  131. package/src/tmdatagrid/components/editors/TMDataGridNumberEditor.tsx +0 -40
  132. package/src/tmdatagrid/core/cellExport.ts +0 -320
  133. package/src/tmdatagrid/core/editEngine.ts +0 -1006
  134. package/src/tmdatagrid/core/summary.ts +0 -35
  135. /package/src/{tmdatagrid/components → components}/TMDataGridDetailsColumn.module.css +0 -0
  136. /package/src/{tmdatagrid/components → components}/TMDataGridFilterPills.module.css +0 -0
  137. /package/src/{tmdatagrid/components → components}/TMDataGridFooter.module.css +0 -0
  138. /package/src/{tmdatagrid/components → components}/TMDataGridGroupColumn.module.css +0 -0
  139. /package/src/{tmdatagrid/components → components}/TMDataGridRowNumberColumn.tsx +0 -0
  140. /package/src/{tmdatagrid/components → components}/TMDataGridSearch.tsx +0 -0
  141. /package/src/{tmdatagrid/core → core}/cellNavigation.ts +0 -0
  142. /package/src/{tmdatagrid/core → core}/cellRange.ts +0 -0
  143. /package/src/{tmdatagrid/core → core}/draftCellContext.ts +0 -0
  144. /package/src/{tmdatagrid/core → core}/expanding.ts +0 -0
  145. /package/src/{tmdatagrid/core → core}/grouping.ts +0 -0
  146. /package/src/{tmdatagrid/core → core}/matchHighlight.ts +0 -0
  147. /package/src/{tmdatagrid/core → core}/quickSearch.ts +0 -0
  148. /package/src/{tmdatagrid/core → core}/rowPinning.ts +0 -0
  149. /package/src/{tmdatagrid/core → core}/rowSelection.ts +0 -0
  150. /package/src/{tmdatagrid/core → core}/sizes.ts +0 -0
@@ -1,29 +1,30 @@
1
1
  ---
2
2
  name: editing
3
3
  description: >
4
- Edit cells and rows in TMDataGrid. Covers the editing option with its four
5
- mode policies (cell, cellConfirm, row, draft), the required getRowId,
6
- editing.onCommit and editing.onCommitDrafts, why the grid never mutates data,
7
- per-column gating with meta.edit.enabled and meta.edit.field, the six built-in
8
- editors picked by meta.type, custom editors through meta.edit.editor,
9
- per-keystroke value mapping with meta.edit.mapValue, field validation with
10
- meta.edit.validate and cross-field rules with editing.rowValidators (Standard
11
- Schema and Zod), adding and deleting rows with edit.addRow,
12
- editing.newRowDefaults, editing.onRowAdd and editing.onRowDelete, the
13
- generated edit lane, TMDataGrid.EditActions with its renderActions slot, and
14
- the public edit engine (begin, commit, cancel, submitAll, getForm, store).
15
- Load when making a grid editable, choosing an edit mode, wiring a save,
16
- writing a cell editor, validating an edit, or when cells will not open.
4
+ Edit cells and rows in TMDataGrid. Covers the editing option and its two axes
5
+ (mode: cell, cellConfirm, row; draft), the required getRowId, editing.onCommit
6
+ and editing.onSaveDrafts, why the grid never mutates data, gating with
7
+ editing.columns, meta.edit.enabled and meta.edit.field, the built-in editors
8
+ picked by meta.type, custom editors via meta.edit.editor, value mapping with
9
+ meta.edit.mapValue, validation at every level - meta.edit.validate,
10
+ cross-field editing.rowValidators, cross-row editing.tableValidators over the
11
+ draft-overlaid collection - adding and deleting rows (edit.addRow,
12
+ newRowDefaults, onRowAdd, onRowDelete), the edit lane, TMDataGrid.DraftActions
13
+ and renderActions, and the edit engine (begin, commit, commitAll, saveDrafts,
14
+ addRows, setCellValue, setRowValues, getForm, store). Load when making a grid
15
+ editable, choosing an edit mode, wiring a save, writing a cell editor,
16
+ validating an edit, writing cells from a toolbar action or bulk fill, or when
17
+ cells will not open.
17
18
  metadata:
18
19
  type: core
19
20
  library: '@jielga/tmdatagrid'
20
- library_version: '2.0.0-beta.2'
21
+ library_version: '2.0.0-beta.21'
21
22
  sources:
22
- - 'Jielga/TMDataGrid:src/docs/editing.md'
23
- - 'Jielga/TMDataGrid:src/docs/query-builder.md'
24
- - 'Jielga/TMDataGrid:src/docs/editors.md'
25
- - 'Jielga/TMDataGrid:src/tmdatagrid/core/editEngine.ts'
26
- - 'Jielga/TMDataGrid:src/tmdatagrid/useTMDataGrid.tsx'
23
+ - 'Jielga/TMDataGrid:packages/tmdatagrid/docs/editing.md'
24
+ - 'Jielga/TMDataGrid:packages/tmdatagrid/docs/query-builder.md'
25
+ - 'Jielga/TMDataGrid:packages/tmdatagrid/docs/editors.md'
26
+ - 'Jielga/TMDataGrid:packages/tmdatagrid/src/core/editEngine.ts'
27
+ - 'Jielga/TMDataGrid:packages/tmdatagrid/src/useTMDataGrid.tsx'
27
28
  ---
28
29
 
29
30
  # TMDataGrid - Editing
@@ -33,9 +34,10 @@ happen. Three facts decide every wiring question below:
33
34
 
34
35
  - **The grid never mutates `data`.** `editing.onCommit` applies the change
35
36
  wherever the data lives, and the updated rows arrive back through `data`.
36
- - **One row, one form.** Each editing row gets its own TanStack Form, keyed by
37
- row id and living outside the DOM, so a draft survives scrolling, sorting and
38
- filtering.
37
+ - **One row, one form, while it is open.** Each row being edited gets its own
38
+ TanStack Form, keyed by row id and living outside the DOM, so a draft
39
+ survives scrolling, sorting and filtering. A committed row holds values, not
40
+ a form.
39
41
  - **`getRowId` is required** once `editing` is set, and it must be the record's
40
42
  own identity. Drafts are keyed by it.
41
43
 
@@ -69,31 +71,35 @@ const grid = useTMDataGrid({
69
71
  ```
70
72
 
71
73
  Two rules are compile errors, not options that silently do nothing: `editing`
72
- requires `getRowId`, and `editing.onCommitDrafts` exists only under
73
- `editing.mode: "draft"`. Draft _without_ `onCommitDrafts` is fine: `submitAll`
74
- falls back to the per-row `editing.onCommit` loop.
74
+ requires `getRowId`, and `editing.onSaveDrafts` exists only under
75
+ `editing.draft: true`. A draft store _without_ `onSaveDrafts` is fine:
76
+ `saveDrafts` falls back to the per-row `editing.onCommit` loop.
75
77
 
76
- ## The four modes
78
+ ## The two axes
77
79
 
78
- All four use the same engine and the same forms. `editing.mode` sets what counts
79
- as a commit, and which controls trigger it.
80
+ `editing.mode` sets what counts as a commit; `editing.draft` sets where that commit goes.
81
+ They are independent, and every pair is legal.
80
82
 
81
83
  | Mode | Commits on | Cancels on | Controls |
82
84
  | --- | --- | --- | --- |
83
- | `"cell"` | Enter, Tab, blur | Escape | none |
84
- | `"cellConfirm"` | ✓ or Enter; blur keeps the draft | ✕ or Escape | ✓ / ✕ beside the input |
85
- | `"row"` | Save in the edit lane, or Ctrl+Enter | Cancel, or Escape | generated edit lane |
86
- | `"draft"` | `edit.submitAll()` | `edit.cancelAll()`, or per row in the lane | `TMDataGrid.EditActions` + the edit lane |
85
+ | `"cell"` | Enter, Tab, leaving the cell | Escape | none |
86
+ | `"cellConfirm"` | ✓ or Enter; Tab walks input, ✓, ✕ and then leaves, keeping the draft | ✕ or Escape | ✓ / ✕ beside the input |
87
+ | `"row"` | Save in the edit lane, or Enter | Cancel, or Escape | generated edit lane |
87
88
 
88
- Which to pick: `"cell"` for saved-as-you-go spreadsheet feel; `"cellConfirm"`
89
- when a stray click must not fire a request; `"row"` when the row is the unit of
90
- the save or a rule spans two columns; `"draft"` for many edits sent as one
91
- transaction.
89
+ Leaving a cell commits it only once the value passes: a refused commit keeps the editor open, invalid, with the message in its tooltip, until the value is fixed or Escape drops it.
90
+
91
+ | `editing.draft` | Where a commit goes |
92
+ | --- | --- |
93
+ | `false` (default) | Straight out: `onCommit`, `onRowAdd`, `onRowDelete` |
94
+ | `true` | Into the grid's draft store, until `edit.saveDrafts()` sends the lot |
95
+
96
+ Which to pick: `"cell"` for spreadsheet feel; `"cellConfirm"` when a stray click must not fire a request; `"row"` when the row is the unit of the save or a rule spans two columns.
97
+ Add `draft: true` for many edits sent as one transaction - `{ mode: "row", draft: true }` commits a whole row from the lane's ✓, `{ mode: "cell", draft: true }` commits a row as the caret leaves it.
92
98
 
93
99
  An editor opens on double-click, or with the cell cursor on the cell: Enter, F2,
94
100
  or typing, where the first character replaces the value. Delete or Backspace
95
- clears the value and commits without opening an editor; under `"draft"` the
96
- cleared value is held with the other drafts instead. Editing implies cell
101
+ clears the value and commits without opening an editor; under `draft: true`
102
+ the cleared value is held with the other drafts instead. Editing implies cell
97
103
  selection: `cellSelection` defaults to `"single"` while `editing` is set. The
98
104
  grid places the caret in the cell that was opened, and `edit.addRow()` places it
99
105
  in the new row's first editable cell, so a `meta.edit.editor` receives focus
@@ -104,13 +110,14 @@ the cell clicked, opens every editable cell of the row, and ✓ saves them as on
104
110
  commit. Rows accumulate: opening a second row leaves the first open, and each
105
111
  row's ✓ and ✕ act on that row alone.
106
112
 
107
- Under `"draft"` nothing reaches a callback until `submitAll`. Enter and Tab hold
108
- the draft, Tab moving on to the next editable cell, Escape drops that one draft,
109
- and drafts accumulate across rows, surviving filters, sorts and scrolling.
110
- `edit.commit(rowId)` validates and holds the draft too, so there is no per-row
111
- escape hatch to the consumer. A held draft is displayed: the cell renders the
112
- draft value through the column's own `cell` renderer, with the blue corner
113
- marking it dirty and `data-dirty` on the row.
113
+ Under `draft: true` nothing reaches a callback until `saveDrafts`.
114
+ The mode's own commit gesture puts the row in the draft store instead of sending it, Escape drops that one draft, and committed rows accumulate.
115
+ `edit.commit(rowId)` goes to the draft store too, so there is no per-row escape hatch to the consumer.
116
+ A committed row is displayed: the cell renders the draft value through the column's own `cell` renderer, with the blue corner marking it dirty and `data-dirty` on the row.
117
+ It is a row like any other to the table: sorting, filtering, quick search, grouping, aggregates, export, selection, the row counts, `edit.getRows()` and `editing.tableValidators` all read its draft values, and the row callbacks receive it with the draft as `row.original`.
118
+ A committed row that stops matching a filter leaves the view, and the Save bar still counts it.
119
+ `data` itself is never modified, and only top-level rows are overlaid - `getSubRows` children keep their `data` values.
120
+ An entry row is row-shaped in every mode - every editable cell open at once, the browser's Tab, and the lane's ✓ to enter it.
114
121
 
115
122
  The edit lane is the change indicator and the per-row undo: an edited row shows
116
123
  a pencil icon and Revert, which drops that row's draft; a new row a plus icon, a
@@ -119,11 +126,23 @@ Restore. A row holding a dirty draft hides the trash - revert first, then
119
126
  delete. A row blocked by validation turns its icon red with the message in the
120
127
  tooltip.
121
128
 
122
- `submitAll` then commits every pending change: through the per-row
129
+ `saveDrafts` then sends the draft store: through the per-row
123
130
  `editing.onCommit` / `editing.onRowAdd` / `editing.onRowDelete` loop by default,
124
- or through one `editing.onCommitDrafts({ rows, added, deleted })` call when that
125
- is set. Rows failing validation stay open either way, and a rejected save keeps
126
- every draft.
131
+ or through one `editing.onSaveDrafts({ updated, created, deleted })` call when
132
+ that is set. `updated` entries carry a `rowId`, `created` entries a `tempId`, `deleted` is
133
+ a list of row ids. Rows failing validation stay open either way.
134
+
135
+ `onSaveDrafts` decides how much of the store is cleared: returning nothing
136
+ saves everything, throwing saves nothing, and returning
137
+ `{ updated, created, deleted }` saves everything except the ids reported
138
+ `false`. Each key takes `false` for the whole bucket or a map of id to result;
139
+ an unnamed id saved. A kept row stays committed, so the next `saveDrafts()`
140
+ retries it, and `saveDrafts()` resolves `false` when anything was kept.
141
+
142
+ Rows carry `data-dirty` (values typed in), `data-draft` (committed, waiting for
143
+ Save), `data-deleted` and `data-new` - a committed new row in the body, or an
144
+ entry row in the block. The grid paints none of them; use `rowStyle` /
145
+ `rowClassName` or the attributes to highlight what is pending.
127
146
 
128
147
  ## What a commit receives
129
148
 
@@ -144,6 +163,7 @@ records, so `accessorKey: "address.city"` edits `values.address.city`.
144
163
 
145
164
  | Gate | Effect |
146
165
  | --- | --- |
166
+ | `editing.columns: ["targetPct"]` | Only the named columns edit |
147
167
  | `meta.edit.enabled: false` | The column never edits |
148
168
  | `meta.edit.enabled: (row) => boolean` | Per row, per column |
149
169
  | `meta.edit.field: "lastName"` | The path an `accessorFn` column writes to |
@@ -152,6 +172,11 @@ records, so `accessorKey: "address.city"` edits `values.address.city`.
152
172
  Group rows and the generated lanes (checkbox, row number, details, edit) never
153
173
  edit.
154
174
 
175
+ `editing.columns` lists the column ids that take edits; unset, the default, every column mapping to a data path is editable.
176
+ It gates before `meta.edit`, never past it: a column left out takes no edits whatever its own meta says, and a listed column still answers to its `meta.edit.enabled`.
177
+ The same list decides which cells an entry row opens.
178
+ `edit.isColumnEditable(column)` answers the column's half of the question with no row in hand, for a toolbar or a menu, and `edit.canEditCell(row, column)` asks both halves.
179
+
155
180
  ```tsx
156
181
  // Computed column writing back to a real field, and per-row gating.
157
182
  columnHelper.accessor((row) => `${row.firstName} ${row.lastName}`, {
@@ -170,7 +195,7 @@ columnHelper.accessor("salary", {
170
195
  `meta.type` picks the editor - `"string"` (the default), `"number"`,
171
196
  `"boolean"`, `"date"`, `"select"` and `"multiSelect"` - and `meta.options` feeds
172
197
  the two select editors from the same declaration the filter panel reads. Each
173
- one, with the export that wraps it, is in
198
+ one, with the value it writes into the draft and the export that wraps it, is in
174
199
  [references/editors-and-validation.md](references/editors-and-validation.md#the-built-in-editors).
175
200
 
176
201
  `meta.edit.editor` replaces one, `meta.edit.validate` guards the field, and
@@ -183,7 +208,7 @@ import { z } from "zod";
183
208
  // Field level, on the column. A bare schema means { onChange: schema }.
184
209
  meta: { edit: { validate: z.string().min(2, "At least two characters") } }
185
210
 
186
- // Form level, inside `editing`. Cross-field rules, under "row" or "draft".
211
+ // Form level, inside `editing`. Cross-field rules, under "row".
187
212
  rowValidators: {
188
213
  onSubmit: z
189
214
  .object({ salary: z.number().positive(), status: z.string() })
@@ -194,7 +219,31 @@ rowValidators: {
194
219
  ```
195
220
 
196
221
  Pathed issues land on the matching cells, pathless ones on the row, and cell
197
- corners mark both: blue for a dirty draft, red for a validation error.
222
+ corners mark both: blue for a dirty draft, red for a validation error. A
223
+ field's message shows in a tooltip on the open editor, which the host renders
224
+ for a custom editor as much as a built-in one; the plain-function form of a
225
+ validator types `value` as `never`, so annotate the parameter -
226
+ `({ value }: { value: unknown })`.
227
+
228
+ `editing.tableValidators` carries the rules that need the other rows - no
229
+ duplicate keys, no overlapping ranges, shares summing to a total. Its
230
+ `onSubmit` / `onSubmitAsync` receive `{ value, rowId, isNew, rows }`, where
231
+ `rows` is the collection as it would stand if the commit landed: every draft
232
+ overlaid, committed new rows among them, the entry rows the table does not hold
233
+ appended, deletion-marked rows removed. Each row appears once. Same result
234
+ vocabulary as `rowValidators`; errors land on the committing row. The rules
235
+ re-run per committed row during `saveDrafts`, the only validation that runs
236
+ there: a committed row a later edit invalidated is reopened with its errors
237
+ and the save resolves `false`.
238
+
239
+ ```tsx
240
+ tableValidators: {
241
+ onSubmit: ({ value, rowId, rows }) =>
242
+ rows.some((r) => r.rowId !== rowId && r.value.code === value.code)
243
+ ? { fields: { code: "Codes must be unique" } }
244
+ : undefined,
245
+ }
246
+ ```
198
247
 
199
248
  `meta.edit.mapValue` rewrites a value instead of rejecting it: uppercase a code,
200
249
  strip spaces from an IBAN, clamp a number. It runs on every write an editor
@@ -210,18 +259,23 @@ Detail for all three: [references/editors-and-validation.md](references/editors-
210
259
  ## Adding and deleting rows
211
260
 
212
261
  `edit.addRow()` opens an entry row in a sticky block under the header, seeded
213
- from `editing.newRowDefaults`. Enter, or the lane's ✓, commits the add through
214
- `editing.onRowAdd` under the immediate modes; under draft mode it enters the
215
- row, which is validated, held with the other drafts, and reported in
216
- `submitAll`'s `added`. Escape, or ✕, discards the entry.
217
-
218
- Under `"draft"` an entered row renders as a value row with no inputs, marked
219
- `data-new` and `data-confirmed` and tinted with `--dg-row-new-bg`. By default it
220
- joins the scrolling flow above the body rows; `editing.newRowsSticky: true`
221
- keeps entered rows pinned in the entry block until Save all. Double-click, or
222
- the lane's pencil, reopens it; ✕ removes it. To limit how many entry rows are
223
- open at once, gate the Add button on
224
- `useSelector(grid.edit.store, (s) => s.newRows.some((n) => !n.confirmed))`.
262
+ from `editing.newRowDefaults`. `edit.addRow(values)` overrides that seed key by
263
+ key, so `addRow()` opens the `newRowDefaults` row and `addRow(values)` opens it
264
+ with those fields filled in - pass a whole row to duplicate it. Enter, or the
265
+ lane's ✓, commits the add through `editing.onRowAdd`; under `draft: true` it
266
+ commits the row into the draft store, validated, and
267
+ `saveDrafts` reports it in `added`. Escape, or ✕, discards the entry. An entry
268
+ row never OK'd is not part of a save - it stays open.
269
+
270
+ Under `draft: true` a committed entry row leaves the entry block and becomes a
271
+ body row with no inputs, marked `data-new` and `data-draft`, tinted with
272
+ `--dg-row-new-bg`, and sorted, filtered and counted with the rest on the values
273
+ it was entered with. `editing.newRowsSticky: true` keeps committed rows in the
274
+ entry block until the save instead, out of the body's sort and out of the row
275
+ count. Double-click, or the lane's pencil, reopens it back into the entry block;
276
+ ✕ removes it. To limit how many entry rows are open at once, gate the Add
277
+ button on
278
+ `useSelector(grid.edit.store, (s) => s.newRows.some((n) => !n.committed))`.
225
279
 
226
280
  ```tsx
227
281
  const grid = useTMDataGrid({
@@ -229,48 +283,76 @@ const grid = useTMDataGrid({
229
283
  columns,
230
284
  getRowId: (row) => String(row.id),
231
285
  editing: {
232
- mode: "draft",
286
+ mode: "row",
287
+ draft: true,
233
288
  newRowDefaults: () => ({ id: 0, firstName: "", salary: 30_000 }),
234
- onCommitDrafts: async ({ rows, added, deleted }) => {
235
- await api.saveBatch({ rows, added, deleted });
289
+ onSaveDrafts: async ({ updated, created, deleted }) => {
290
+ await api.saveBatch({ updated, created, deleted });
236
291
  },
237
292
  },
238
293
  });
239
294
 
240
295
  <TMDataGrid.Toolbar>
241
296
  <Button onClick={() => grid.edit.addRow()}>Add row</Button>
297
+ <Button onClick={() => grid.edit.addRow({ salary: 50_000 })}>
298
+ Add senior
299
+ </Button>
242
300
  <TMDataGrid.Spacer />
243
- <TMDataGrid.EditActions />
301
+ <TMDataGrid.DraftActions />
244
302
  </TMDataGrid.Toolbar>;
245
303
  ```
246
304
 
247
- `edit.deleteRow(rowId)` calls `editing.onRowDelete({ rowId, row })` immediately
248
- under the immediate modes, so put any confirmation inside that callback. Under
249
- draft mode it toggles a mark instead: the row renders struck through and inert
250
- (`data-deleted`), the lane shows Restore, and `submitAll` reports the ids in
305
+ `edit.addRows(rows, options?)` opens a batch in one write. `{ commit: true }`
306
+ submits each row as it lands - the import case: valid rows are committed,
307
+ invalid ones stay open in the entry block with their errors, and the result
308
+ (`{ committed, open }`) says which went which way. Column rules are enforced
309
+ even though the rows never had an editor on screen, because the engine runs
310
+ `meta.edit.validate` itself at commit.
311
+
312
+ ```tsx
313
+ const { committed, open } = await grid.edit.addRows(parsed, { commit: true });
314
+ if (open.length > 0) notify(`${open.length} rows need attention`);
315
+ await grid.edit.saveDrafts();
316
+ ```
317
+
318
+ `edit.deleteRow(rowId)` calls `editing.onRowDelete({ rowId, row })`
319
+ immediately, so put any confirmation inside that callback. Under `draft: true`
320
+ it toggles a mark instead: the row renders struck through and inert
321
+ (`data-deleted`), the lane shows Restore, and `saveDrafts` reports the ids in
251
322
  `deleted`. You apply adds and deletes, the same as edits. The engine's `tempId`
252
323
  (`__new__1`, …) does not need to become a real id.
253
324
 
254
325
  ## The built-in controls
255
326
 
256
- The generated edit lane (`EDIT_COLUMN_ID`, pinned right) appears when
257
- `editing.mode` is `"row"` or `"draft"`, or when `editing.onRowDelete` is set.
258
- Nothing else adds it, and `"cell"` mode renders no controls of its own. Under
259
- `"row"` it holds Save and Cancel while a row is open; under `"draft"` it holds
260
- the row-state marker with Revert and Restore, and Save and Cancel never appear.
327
+ The generated edit lane (`EDIT_COLUMN_ID`, pinned right) appears when `editing.mode` is `"row"`, when `editing.draft` is on, or when `editing.onRowDelete` is set.
328
+ Nothing else adds it.
329
+ It holds one thing per axis: the mode's own controls while a row is open - Save and Cancel under `"row"` - and, once a row is committed, the row-state marker with Revert or Restore.
330
+ A committed row never offers a save.
331
+ The trash shows when the deletion has somewhere to report to: `onRowDelete` is set, or under `draft: true`, `onSaveDrafts` is.
332
+ If validation blocks a row, its marker - or the open row's ✓ - turns red with the message in the tooltip, which is where a pathless `rowValidators` message shows.
261
333
  Every control carries a tooltip from the labels.
262
334
 
263
- `TMDataGrid.EditActions` is Save with the pending count plus Discard. It greys
264
- out while nothing is pending, spins while a submit is in flight, renders nothing
335
+ `TMDataGrid.DraftActions` is Save with the draft-store count, Discard, and a
336
+ note counting the rows still open. Save greys out while the store is empty
337
+ however much is being typed, spins while a submit is in flight, renders nothing
265
338
  while editing is off, and works under any mode, not only draft.
266
339
 
267
- `renderActions` replaces the pair and hands over its pieces:
340
+ `renderActions` replaces the set and hands over its pieces:
268
341
 
269
342
  ```tsx
270
- <TMDataGrid.EditActions
271
- renderActions={({ state, Controls }) => (
343
+ <TMDataGrid.DraftActions
344
+ renderActions={({ state, actions, Controls }) => (
272
345
  <Group>
273
- {state.pendingCount > 0 && <Badge>{state.pendingCount}</Badge>}
346
+ {state.draftCount > 0 && <Badge>{state.draftCount}</Badge>}
347
+ <Button
348
+ disabled={state.openCount === 0}
349
+ onClick={() => {
350
+ actions.scrollToFirstOpenRow("center");
351
+ }}
352
+ >
353
+ Go to open row
354
+ </Button>
355
+ <Controls.OpenRowsNote />
274
356
  <Controls.Save />
275
357
  <Controls.Discard />
276
358
  </Group>
@@ -278,21 +360,55 @@ while editing is off, and works under any mode, not only draft.
278
360
  />
279
361
  ```
280
362
 
281
- `state` is `{ pendingCount, isSubmitting }`, `actions` is `{ save, discard }`.
363
+ `state` is
364
+ `{ draftCount, openCount, openRowIds, pendingCount, isSubmitting, isSaving }` -
365
+ `pendingCount` deprecated, reading as `draftCount + openCount`. `actions` is
366
+ `{ save, commitAll, discard, scrollToRow, scrollToFirstOpenRow }`, and
367
+ `Controls` is `{ Save, Discard, OpenRowsNote }`.
368
+
369
+ The grid is always virtualized, so an open row far down the list has no element
370
+ to scroll to. `actions.scrollToFirstOpenRow(align?)` moves the virtualizer to
371
+ the topmost open row and answers whether it could be reached; `false` means
372
+ every open row is filtered out, on another page or collapsed in a group. An
373
+ open entry row answers `true` without scrolling - the entry block is sticky, so
374
+ it is on screen already.
375
+
376
+ The two orderings differ: `state.openRowIds` is the order the grid opened the
377
+ rows, `scrollToFirstOpenRow` is display order. `openRowIds[0]` need not be the
378
+ row it reaches.
282
379
 
283
380
  ## The engine: `edit`
284
381
 
285
382
  `grid.edit` is public, and everything the built-in controls do goes through it:
286
- `begin` and `commit`, `cancel` / `cancelAll`, `submitAll`, `addRow` /
383
+ `begin` and `commit`, `cancel` / `cancelAll`, `commitAll` / `saveDrafts`,
384
+ `setCellValue` / `setRowValues` / `clearCell`, `addRow` / `addRows` /
287
385
  `deleteRow`, `getForm`, and `store` for `useSelector` (an example is under
288
386
  [Submitting an outer form](#high-submitting-an-outer-form-while-the-grid-holds-a-draft)).
289
- Every member with its signature, the gates, `clearCell`, `deactivate`, and the
290
- `edit.store` shape are in
387
+ `edit.store` publishes each open or committed row's drafted values as
388
+ `rows[rowId].values`, which is what a computed cell or a cross-row check reads
389
+ - `useTMDataGridContext()` reaches the engine from inside a cell renderer.
390
+ Every member with its signature, the gates, `isColumnEditable`, `deactivate`,
391
+ and the `edit.store` shape are in
291
392
  [references/editing-api.md](references/editing-api.md#the-edit-engine).
292
393
 
293
- `getForm` exposes the row's form: render it in a drawer and it shares values,
294
- dirty state and errors with the inline cells, because it is the same
295
- `FormApi`.
394
+ `getForm` exposes an open row's form: render it in a drawer and it shares
395
+ values, dirty state and errors with the inline cells, because it is the same
396
+ `FormApi`. It is `undefined` for a committed row, which holds values and no
397
+ form; `begin` reopens the row with a form seeded from them.
398
+
399
+ `edit.setCellValue(rowId, columnId, value)` writes one cell and commits its row with no editor open, which is what a toolbar action or a bulk fill wants.
400
+ The row need not be mounted, so a selected row inside a collapsed group takes the write like any other.
401
+ `edit.setRowValues(rowId, values)` does several cells of one row in a single commit, keyed by column id, all or nothing.
402
+
403
+ ```tsx
404
+ for (const row of grid.table.getSelectedRowModel().rows) {
405
+ await grid.edit.setCellValue(row.id, "targetPct", equalWeight(row.original));
406
+ }
407
+ ```
408
+
409
+ Under `draft: true` each row is committed into the draft store like any hand-made edit, with the same change markers and the same per-row revert, and the basket leaves through `saveDrafts`.
410
+ `value` is the stored value: no editor runs, so `meta.edit.mapValue` does not run either, while `meta.edit.validate` does.
411
+ Both resolve `false` when the cell takes no edit - no such row or column, `editing.columns` excludes it, `meta.edit.enabled` is off, or the row is not editable - and when validation refuses the value, which leaves the row open carrying its errors.
296
412
 
297
413
  ## Inside an outer form
298
414
 
@@ -304,10 +420,12 @@ approval, map by row id (never index), and assign negative ids to new rows.
304
420
 
305
421
  The validation split follows from what each side can see: **a rule decidable
306
422
  from one row belongs to the grid (`meta.edit.validate`,
307
- `editing.rowValidators`); a rule needing the other rows or the collection ("has
308
- rows", "no duplicates") belongs to the form's field validator.** A row's form
309
- cannot see the array, and `edit.store` publishes field names but not values, so
310
- the form cannot see a draft.
423
+ `editing.rowValidators`), and a rule needing the other rows belongs to
424
+ `editing.tableValidators`, which is handed the collection with every draft
425
+ overlaid.** A collection rule may live in the outer form's field validator
426
+ instead, where the submit gate is the form's own; `edit.store` publishes each
427
+ row's drafted values as `rows[rowId].values` for a rule there that must count
428
+ pending values.
311
429
 
312
430
  ## Common mistakes
313
431
 
@@ -319,12 +437,15 @@ are in [references/common-mistakes.md](references/common-mistakes.md).
319
437
  | CRITICAL | Expecting the grid to write into `data` - without `editing.onCommit` the cell reverts |
320
438
  | CRITICAL | `getRowId` built from the row index - drafts follow the index, not the record |
321
439
  | HIGH | A cell editor defined inside the component - a new type per render unmounts the editor |
322
- | HIGH | A cross-field rule under `mode: "cell"` - `rowValidators` needs `"row"` or `"draft"` |
440
+ | HIGH | A cross-field rule under `mode: "cell"` - `rowValidators` needs `"row"` |
323
441
  | HIGH | An `accessorFn` column with no `meta.edit.field` - it maps to nothing and stays read-only |
324
442
  | HIGH | Swallowing the error in `editing.onCommit` - a resolved catch drops the draft |
325
443
  | HIGH | Submitting an outer form while the grid holds a draft - it saves stale rows |
444
+ | HIGH | A bulk write built from `begin` + `getForm` + `commit` - `getForm` can be `undefined`; `edit.setCellValue` is the write |
445
+ | MEDIUM | A computed column frozen while a row is edited - `accessorFn` reads `data`; read the draft from `edit.store`'s `rows[rowId].values` |
326
446
  | MEDIUM | Reading a commit's result as the saved value - it is a `boolean` about the form |
327
- | MEDIUM | Expecting `editing.onRowDelete` to fire under draft - the mark waits for `submitAll` |
447
+ | MEDIUM | Expecting `editing.onRowDelete` to fire under draft - the mark waits for `saveDrafts` |
448
+ | MEDIUM | A custom editor that binds no invalid state - the host shows the message in a tooltip, but the control keeps its normal border; bind a boolean to `error` |
328
449
 
329
450
  ## References
330
451