@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
@@ -22,7 +22,7 @@ useTMDataGrid({
22
22
  Correct: wire `editing.onCommit` to apply the change where the data lives, as
23
23
  in the skill's Setup section.
24
24
 
25
- Source: `src/docs/editing.md`, `src/tmdatagrid/core/editEngine.ts`.
25
+ Source: `packages/tmdatagrid/docs/editing.md`, `packages/tmdatagrid/src/core/editEngine.ts`.
26
26
 
27
27
  ## CRITICAL `getRowId` built from the row index
28
28
 
@@ -42,7 +42,7 @@ Correct:
42
42
  getRowId: (row) => String(row.id),
43
43
  ```
44
44
 
45
- Source: `src/tmdatagrid/useTMDataGrid.tsx` (`TMDataGridEditingCallbacks`).
45
+ Source: `packages/tmdatagrid/src/useTMDataGrid.tsx` (`TMDataGridEditingCallbacks`).
46
46
 
47
47
  ## HIGH A cell editor defined inside the component
48
48
 
@@ -71,7 +71,7 @@ const SalaryEditor: TMDataGridEditorComponent = ({ field, commit }) => (
71
71
  meta: { edit: { editor: SalaryEditor } },
72
72
  ```
73
73
 
74
- Source: `src/docs/editors.md`, `src/tmdatagrid/core/editEngine.ts`.
74
+ Source: `packages/tmdatagrid/docs/editors.md`, `packages/tmdatagrid/src/core/editEngine.ts`.
75
75
 
76
76
  ## HIGH A cross-field rule under `editing.mode: "cell"`
77
77
 
@@ -79,10 +79,10 @@ Under `"cell"` each cell commits on its own, so a rule spanning two columns
79
79
  cannot be satisfied by either one: the first cell edited is rejected against the
80
80
  other column's old value, and the row cannot be saved.
81
81
 
82
- Correct: `editing.rowValidators` needs `mode: "row"` or `"draft"`, which
83
- validate the whole row in one commit.
82
+ Correct: `editing.rowValidators` needs `mode: "row"`, which validates the
83
+ whole row in one commit.
84
84
 
85
- Source: `src/docs/editors.md` (Validation).
85
+ Source: `packages/tmdatagrid/docs/editors.md` (Validation).
86
86
 
87
87
  ## HIGH An `accessorFn` column that never opens an editor
88
88
 
@@ -109,7 +109,7 @@ columnHelper.accessor((row) => `${row.firstName} ${row.lastName}`, {
109
109
  });
110
110
  ```
111
111
 
112
- Source: `src/docs/editing.md` (Which cells edit).
112
+ Source: `packages/tmdatagrid/docs/editing.md` (Which cells edit).
113
113
 
114
114
  ## HIGH Swallowing the error in `editing.onCommit`
115
115
 
@@ -120,9 +120,9 @@ and the grid shows the old value as though nothing happened.
120
120
  Wrong:
121
121
 
122
122
  ```tsx
123
- onCommit: async ({ rowId, changes }) => {
123
+ onCommit: async ({ rowId, value }) => {
124
124
  try {
125
- await api.patch(rowId, changes);
125
+ await api.put(rowId, value);
126
126
  } catch (error) {
127
127
  console.error(error);
128
128
  }
@@ -132,12 +132,13 @@ onCommit: async ({ rowId, changes }) => {
132
132
  Correct: no `catch` - let the rejection propagate, and the form stays open
133
133
  with the error on the row.
134
134
 
135
- Source: `src/tmdatagrid/useTMDataGrid.tsx` (`TMDataGridEditingCallbacks`).
135
+ Source: `packages/tmdatagrid/src/useTMDataGrid.tsx` (`TMDataGridEditingCallbacks`).
136
136
 
137
137
  ## HIGH Submitting an outer form while the grid holds a draft
138
138
 
139
139
  The outer form's array contains only committed rows. Under any mode a mid-edit
140
- row is invisible to it, and under `"draft"` every edit is until `submitAll()`,
140
+ row is invisible to it, and under `draft: true` every edit is until
141
+ `saveDrafts()`,
141
142
  so a form submit saves stale rows and collection rules skip pending values.
142
143
 
143
144
  Correct:
@@ -146,32 +147,118 @@ Correct:
146
147
  const hasOpenDraft = useSelector(grid.edit.store, (s) => s.openRowIds.length > 0);
147
148
 
148
149
  <Button type="submit" disabled={!canSubmit || hasOpenDraft}>Save</Button>
149
- // or flush instead of blocking:
150
- const flushed = await grid.edit.submitAll();
150
+ // or commit and flush instead of blocking:
151
+ await grid.edit.commitAll();
152
+ const flushed = await grid.edit.saveDrafts();
151
153
  if (flushed) await form.handleSubmit();
152
154
  ```
153
155
 
154
- Source: `src/docs/query-builder.md` (Which mode, Submitting).
156
+ Source: `packages/tmdatagrid/docs/query-builder.md` (Which mode, Submitting).
157
+
158
+ ## HIGH A bulk write built from `begin`, `getForm` and `commit`
159
+
160
+ `edit.setCellValue(rowId, columnId, value)` writes one cell and commits its row, so a fill over the selection is one call per row.
161
+ Reaching instead for `begin`, then `getForm(rowId)?.setFieldValue(...)`, then `commit` looks equivalent and is not: under `editing.mode: "cell"`, `begin` defers behind a pending commit whenever another row is open, so `getForm(rowId)` returns `undefined` immediately after it and the optional chain writes nothing.
162
+ The `commit` that follows finds no form and resolves `true`, reporting a write that never happened.
163
+
164
+ Wrong:
165
+
166
+ ```tsx
167
+ for (const row of grid.table.getSelectedRowModel().rows) {
168
+ grid.edit.begin({ rowId: row.id, columnId: "targetPct" });
169
+ grid.edit.getForm(row.id)?.setFieldValue("targetPct", next(row.original));
170
+ await grid.edit.commit(row.id);
171
+ }
172
+ ```
173
+
174
+ Correct:
175
+
176
+ ```tsx
177
+ for (const row of grid.table.getSelectedRowModel().rows) {
178
+ await grid.edit.setCellValue(row.id, "targetPct", next(row.original));
179
+ }
180
+
181
+ // Several cells of one row: one commit, one draft entry.
182
+ await grid.edit.setRowValues(rowId, { status: "Closed", closedOn: today() });
183
+ ```
184
+
185
+ Both write the stored value through the row's form, so `meta.edit.mapValue` does not run - no editor is involved - while `meta.edit.validate` does, at the commit.
186
+ A refused value leaves the row open carrying its errors and the call resolves `false`, so read the result rather than assuming the fill landed.
187
+
188
+ Source: `packages/tmdatagrid/src/core/editEngine.ts` (`begin`, `writeFields`).
189
+
190
+ ## MEDIUM A computed column frozen while a row is edited
191
+
192
+ A held draft is displayed by the column that owns the field. A column computed
193
+ from other fields - `accessorFn` or `display` - reads `row.original`, which is
194
+ `data`, so it keeps showing the saved record while the values it derives from
195
+ are being typed.
196
+
197
+ Correct: read the drafted row from `edit.store` inside the cell renderer:
198
+
199
+ ```tsx
200
+ function useDraftedRow(rowId: string, original: Product): Product {
201
+ const { edit } = useTMDataGridContext();
202
+ const values = useSelector(edit.store, (state) => state.rows[rowId]?.values);
203
+ return (values as Product | undefined) ?? original;
204
+ }
205
+ ```
206
+
207
+ Source: `packages/tmdatagrid/docs/editing.md` (Draft lifetime).
155
208
 
156
209
  ## MEDIUM Reading a commit's result as the saved value
157
210
 
158
- `edit.commit(rowId)` and `edit.submitAll()` resolve to a `boolean` saying
159
- whether the form closed, and resolve `false` when validation or a rejected save
160
- kept it open. Ignoring the result reports a save that did not happen.
211
+ `edit.commit(rowId)`, `edit.commitAll()` and `edit.saveDrafts()` resolve to a
212
+ `boolean` saying whether everything landed, and resolve `false` when validation
213
+ or a rejected save kept a row open. Ignoring the result reports a save that did
214
+ not happen.
161
215
 
162
216
  ```tsx
163
- const saved = await grid.edit.submitAll();
217
+ const saved = await grid.edit.saveDrafts();
164
218
  notifications.show({ message: saved ? "Saved" : "Some rows need attention" });
165
219
  ```
166
220
 
167
- Source: `src/tmdatagrid/core/editEngine.ts` (`TMDataGridEditApi`).
221
+ Source: `packages/tmdatagrid/src/core/editEngine.ts` (`TMDataGridEditApi`).
168
222
 
169
- ## MEDIUM Expecting `editing.onRowDelete` to fire under draft
223
+ ## MEDIUM Expecting `editing.onRowDelete` to fire under a draft store
170
224
 
171
- Under the immediate modes `edit.deleteRow` calls `editing.onRowDelete` at once.
172
- Under `"draft"` it only toggles a deletion mark, so nothing is removed until
173
- `submitAll`: the ids arrive as `deleted` in `editing.onCommitDrafts`, or, with
225
+ Without `editing.draft`, `edit.deleteRow` calls `editing.onRowDelete` at once.
226
+ Under `draft: true` it only toggles a deletion mark, so nothing is removed until
227
+ `saveDrafts`: the ids arrive as `deleted` in `editing.onSaveDrafts`, or, with
174
228
  no such callback, in the per-row `editing.onRowDelete` loop. A confirmation
175
229
  placed inside `editing.onRowDelete` therefore guards the save, not the trash.
176
230
 
177
- Source: `src/docs/editing.md` (Adding and deleting rows).
231
+ Source: `packages/tmdatagrid/docs/editing.md` (Adding and deleting rows).
232
+
233
+ ## MEDIUM A custom editor that binds no invalid state
234
+
235
+ The message is the host's: it shows in a tooltip on the editor whatever the
236
+ editor is. The invalid styling is the editor's own, and one that binds nothing
237
+ keeps its normal border while the commit is refused, so the only marks on
238
+ screen are the tooltip and `data-invalid` on the cell.
239
+
240
+ Wrong:
241
+
242
+ ```tsx
243
+ const SalaryEditor: TMDataGridEditorComponent = ({ field, commit }) => (
244
+ <Slider value={field.state.value} onChange={field.handleChange} onChangeEnd={() => void commit()} />
245
+ );
246
+ ```
247
+
248
+ Correct:
249
+
250
+ ```tsx
251
+ const SalaryEditor: TMDataGridEditorComponent = ({ field, commit }) => {
252
+ const hasError = field.state.meta.errors.length > 0;
253
+ return (
254
+ <Slider
255
+ value={field.state.value}
256
+ onChange={field.handleChange}
257
+ onChangeEnd={() => void commit()}
258
+ error={hasError}
259
+ />
260
+ );
261
+ };
262
+ ```
263
+
264
+ Source: `packages/tmdatagrid/docs/editors.md` (Writing your own).
@@ -8,26 +8,30 @@ kind.
8
8
  | Name | Type | Default | What it does |
9
9
  | --- | --- | --- | --- |
10
10
  | `editing` | `TMDataGridEditingOptions` | off | The editing namespace. Setting it turns editing on. |
11
- | `editing.mode` | `"cell" \| "cellConfirm" \| "row" \| "draft"` | – | Picks the commit policy. |
11
+ | `editing.mode` | `"cell" \| "cellConfirm" \| "row"` | – | What counts as a commit, and which controls trigger it. |
12
+ | `editing.draft` | `boolean` | `false` | Where a commit goes. On, it is held in the grid's draft store until `edit.saveDrafts()`. |
12
13
  | `getRowId` | `(row) => string` | – | A TanStack table option, required once `editing` is set. Drafts are keyed by it. |
14
+ | `editing.columns` | `ReadonlyArray<string>` | every column mapping to a data path | The column ids that take edits. Gates before `meta.edit`, never past it: a column left out takes no edits whatever its meta says, and a listed column still answers to its `meta.edit.enabled`. Also decides which cells an entry row opens. |
13
15
  | `editing.isRowEditable` | `(row) => boolean` | – | Closes a whole row to editing, in every mode. |
14
16
  | `editing.rowValidators` | `TMDataGridRowValidators` | – | Form-level validation. Cross-field rules live here. |
17
+ | `editing.tableValidators` | `TMDataGridTableValidators` | – | Cross-row rules, handed the collection with every draft overlaid. |
15
18
  | `editing.newRowDefaults` | `TData \| (() => TData)` | – | Seeds the entry row's form. A function is called per added row. |
16
- | `editing.newRowsSticky` | `boolean` | `false` | Draft mode only. Keeps entered new rows pinned in the entry block until Save all, instead of letting them scroll with the body. |
19
+ | `editing.newRowsSticky` | `boolean` | `false` | `draft: true` only. Keeps committed entry rows in the sticky entry block, out of the body's sort and out of the row count, until the save. Off, a committed entry row becomes a body row. |
17
20
  | `cellSelection` | `"none" \| "single" \| "range"` | `"single"` while `editing` is set | Editing turns the cell cursor on; set it explicitly to override. |
18
21
 
19
22
  Passing any other member of `editing` without `mode` is a compile error, and
20
- `onCommitDrafts` and `newRowsSticky` exist only in the `"draft"` branch of the
21
- type.
23
+ `onSaveDrafts` and `newRowsSticky` exist only in the `draft: true` branch of
24
+ the type.
22
25
 
23
26
  ## Callbacks
24
27
 
25
28
  | Name | Argument | What it does |
26
29
  | --- | --- | --- |
27
30
  | `editing.onCommit` | `{ rowId, value, original, changes, source }` | Applies one row's change. Reject to keep the draft and show the error. |
28
- | `editing.onCommitDrafts` | `{ rows, added, deleted }` | Draft mode only. One call for the entire save. Without it, `submitAll` loops `editing.onCommit`, `editing.onRowAdd` and `editing.onRowDelete`. |
31
+ | `editing.onSaveDrafts` | `{ updated, created, deleted }` | `draft: true` only. One call for the whole draft store. Without it, `saveDrafts` loops `editing.onCommit`, `editing.onRowAdd` and `editing.onRowDelete`. |
32
+ | `editing.onCommitDrafts` | `{ updated, created, deleted }` | **Deprecated** - renamed to `onSaveDrafts`. Still honoured; the new name wins if both are set. |
29
33
  | `editing.onRowAdd` | `{ tempId, value }` | Commits an entry row. Mint the real id here. |
30
- | `editing.onRowDelete` | `{ rowId, row }` | Deletes a row under the immediate modes, and puts the trash can in the edit lane. |
34
+ | `editing.onRowDelete` | `{ rowId, row }` | Deletes a row. Shows the trash; under `draft: true`, `onSaveDrafts` shows it too. |
31
35
 
32
36
  `changes` entries are `{ columnId, field, previous, next }`; `field` is the data
33
37
  path, which may be dotted.
@@ -49,19 +53,25 @@ path, which may be dotted.
49
53
 
50
54
  | Member | Signature | Notes |
51
55
  | --- | --- | --- |
52
- | `begin` | `({ rowId, columnId }) => void` | Row mode opens the entire row either way. `columnId` selects which cell takes the caret; `null` (the pencil) uses its first editable one. On an entered new row it reopens the row, flipping `confirmed` back to `false`. |
53
- | `commit` | `(rowId) => Promise<boolean>` | `false` keeps the form open with its errors. Under `"draft"` it validates and holds the draft: no consumer callback runs until `submitAll`. |
56
+ | `begin` | `({ rowId, columnId }) => void` | Row mode opens the entire row either way. `columnId` selects which cell takes the caret; `null` (the pencil) uses its first editable one. On a committed row it reopens it, taking it back out of the draft store. |
57
+ | `commit` | `(rowId) => Promise<boolean>` | The OK gesture: submits the row's form. `false` keeps it open with its errors, and the message outlives the editor that found it. Under `draft: true` a pass commits the row into the draft store - no consumer callback runs until `saveDrafts`. Column rules run whether or not an editor is mounted. |
58
+ | `commitAll` | `() => Promise<boolean>` | Submits every open row. Rows that fail stay open. `false` when one did. |
59
+ | `saveDrafts` | `() => Promise<boolean>` | Sends the draft store, re-running `editing.tableValidators` only; a committed row that fails is reopened with its errors. Open rows are left alone and stay open. |
54
60
  | `cancel` | `(rowId) => void` | Drops one draft. |
55
61
  | `cancelAll` | `() => void` | Drops every draft. |
56
62
  | `deactivate` | `() => void` | Closes the editor without touching the draft, as blur does under `"cellConfirm"`. |
57
- | `submitAll` | `() => Promise<boolean>` | Draft mode's Save all. `true` when every row landed. |
63
+ | `submitAll` | `() => Promise<boolean>` | **Deprecated** - `commitAll()` then `saveDrafts()`. |
58
64
  | `clearCell` | `(rowId, columnId) => Promise<boolean>` | What Delete does: writes the type's empty value and commits. |
59
- | `addRow` | `() => string` | Opens an entry row, returns its `tempId`. |
60
- | `deleteRow` | `(rowId) => void` | `editing.onRowDelete` under the immediate modes, a deletion mark under draft mode. Toggles: a second call restores the row. |
61
- | `canEditCell` | `(row, column) => boolean` | The check the built-in controls use. |
65
+ | `setCellValue` | `(rowId, columnId, value) => Promise<boolean>` | Writes one cell and commits the row, with no editor - toolbar actions and bulk fills. The row need not be mounted; a row inside a collapsed group takes the write. `value` is the stored value, so `meta.edit.mapValue` does not run and `meta.edit.validate` does. Under `draft: true` the row is committed into the draft store like any hand-made edit. `false` when the cell takes no edit, or when validation refused the value and left the row open with its errors. |
66
+ | `setRowValues` | `(rowId, values) => Promise<boolean>` | `setCellValue` for several cells of one row in a single commit - one consumer call and one draft entry. Keys are column ids. All or nothing: if any named cell takes no edit, nothing is written and it resolves `false`. |
67
+ | `addRow` | `(values?) => string` | Opens an entry row, returns its `tempId`. `values` overrides `editing.newRowDefaults` key by key for that row; with no argument the row is `newRowDefaults` alone. |
68
+ | `addRows` | `(rows, options?) => Promise<{ committed, open }>` | Opens a batch in one write, each row seeded as `addRow` seeds. `{ commit: true }` submits each as it lands - valid rows commit, invalid ones stay open with their errors. |
69
+ | `deleteRow` | `(rowId) => void` | `editing.onRowDelete`, or a deletion mark under `draft: true`. Toggles: a second call restores the row. |
70
+ | `canEditCell` | `(row, column) => boolean` | The check the built-in controls use. Both halves: the column's and the row's. |
62
71
  | `canEditRow` | `(row) => boolean` | The pencil's gate. |
72
+ | `isColumnEditable` | `(column) => boolean` | The column's half alone, with no row in hand: it maps to a field, `editing.columns` lists it when that is set, and `meta.edit.enabled` is not `false`. |
63
73
  | `canDeleteRows` | `() => boolean` | Whether the delete control should be shown. |
64
- | `getForm` | `(rowId) => TMDataGridRowEditForm \| undefined` | The row's live `FormApi`. |
74
+ | `getForm` | `(rowId) => TMDataGridRowEditForm \| undefined` | The open row's live `FormApi`; `undefined` for a committed row. |
65
75
  | `state` | `TMDataGridEditState` | Snapshot, for reads outside React. |
66
76
  | `store` | `Store<TMDataGridEditState>` | For `useSelector`. |
67
77
 
@@ -71,10 +81,10 @@ path, which may be dotted.
71
81
  type TMDataGridEditState = {
72
82
  // The cell the last open gesture named - where the caret goes.
73
83
  active: { rowId: string; columnId: string | null } | null;
74
- // Rows with a live form. One under `"cell"`; as many as the user opened
75
- // under `"row"`, `"cellConfirm"` and `"draft"` - which rows are editing.
84
+ // Every row the grid holds work for: open rows and committed rows. A row
85
+ // is *open* when it is in here and not in `committedRowIds`.
76
86
  openRowIds: ReadonlyArray<string>;
77
- // One `TMDataGridEditRowProjection` per open row.
87
+ // One `TMDataGridEditRowProjection` per row in `openRowIds`.
78
88
  rows: Record<
79
89
  string,
80
90
  {
@@ -86,10 +96,20 @@ type TMDataGridEditState = {
86
96
  values: TMDataGridRowData;
87
97
  }
88
98
  >;
89
- // `confirmed` is draft mode's "entered, awaiting Save all". Under the
90
- // immediate modes a confirm commits through `onRowAdd`, so it stays `false`.
91
- newRows: ReadonlyArray<{ tempId: string; confirmed: boolean }>;
99
+ // The draft store's edit slice: existing rows that passed their commit,
100
+ // held as values for `saveDrafts`. Empty without `editing.draft`.
101
+ committedRowIds: ReadonlyArray<string>;
102
+ // What a committed row *is* to the table: the draft store's values per row,
103
+ // snapshotted at each commit and kept across a reopen, so the row holds its
104
+ // place until it commits again or is dropped. The grid feeds these to the
105
+ // table in place of the consumer's records.
106
+ committedValues: Readonly<Record<string, TMDataGridRowData>>;
107
+ // `committed` is "in the store, awaiting the save". Without `editing.draft`
108
+ // a commit adds through `onRowAdd`, so it stays `false`.
109
+ newRows: ReadonlyArray<{ tempId: string; committed: boolean }>;
92
110
  deletedRowIds: ReadonlyArray<string>;
111
+ // `true` while `saveDrafts` is in flight.
112
+ isSaving: boolean;
93
113
  };
94
114
  ```
95
115
 
@@ -97,8 +117,8 @@ type TMDataGridEditState = {
97
117
 
98
118
  | Name | Kind | What it is |
99
119
  | --- | --- | --- |
100
- | `TMDataGrid.EditActions` | Component | Save with the pending count, and Discard. Any mode; renders nothing while editing is off. Takes `renderActions` over `{ state, actions, Controls }`. |
101
- | `TMDataGridEditActions` | Export | The same component, for use outside the namespace. |
120
+ | `TMDataGrid.DraftActions` | Component | Save with the draft-store count, Discard, and a note counting the rows still open. Any mode; renders nothing while editing is off. Takes `renderActions` over `{ state, actions, Controls }` - `state.openRowIds` and `actions.scrollToFirstOpenRow(align?)` reach the rows left open. |
121
+ | `TMDataGridDraftActions` | Export | The same component, for use outside the namespace. |
102
122
  | `EDIT_COLUMN_ID` | Export | `"__edit__"`, the generated edit lane's id. |
103
123
  | `clearedValueForType` | Export | `(type) => unknown` - what Delete writes per column type. |
104
124
  | `getEditFieldName` | Export | `(column) => string` - the data path a column's edits write to. |
@@ -118,20 +138,24 @@ Generated, pinned right, id `EDIT_COLUMN_ID`. It appears when any of these
118
138
  holds:
119
139
 
120
140
  - `editing.mode: "row"` - the lane is Save and Cancel's home
121
- - `editing.mode: "draft"` - the lane is the change indicator and the per-row
122
- revert, and `editing.onCommitDrafts` is not required for it
141
+ - `editing.draft` - the lane is the change indicator and the per-row revert,
142
+ and `editing.onSaveDrafts` is not required for it
123
143
  - `editing.onRowDelete` is set - the trash can has somewhere to report to
124
144
 
125
- `"cell"` and `"cellConfirm"` have no lane unless `editing.onRowDelete` asks for
126
- one.
145
+ `"cell"` and `"cellConfirm"` have no lane unless `editing.draft` or
146
+ `editing.onRowDelete` asks for one.
127
147
 
128
- What the lane holds depends on the mode. Under `"row"` an open row shows
129
- `save-row` and `cancel-row`; those two never render under `"draft"`. Under
130
- `"draft"` every changed row shows `row-state`, whose `data-state` is `new`,
131
- `edited` or `deleted`, together with `revert-row` on an edited row,
132
- `restore-row` on one marked for deletion, and `edit-row` plus `discard-new-row`
133
- on an entered new row. A row holding a dirty draft hides `delete-row`. Every
134
- control carries a tooltip from the labels, `revertRow`, `rowStateNew`,
148
+ The trash itself shows when the deletion has somewhere to report to:
149
+ `onRowDelete` is set, or under `draft: true`, `onSaveDrafts` is - a deletion
150
+ mark is part of the save.
151
+
152
+ What the lane holds follows the row's state, one axis each. An open row shows
153
+ the mode's own controls, `save-row` and `cancel-row`. A committed row shows
154
+ `row-state` instead, whose `data-state` is `new`, `edited` or `deleted`,
155
+ together with `revert-row` on an edited row, `restore-row` on one marked for
156
+ deletion, and `edit-row` plus `discard-new-row` on an entered new row - never a
157
+ save, which the engine would only commit again. A committed row hides `delete-row`.
158
+ Every control carries a tooltip from the labels, `revertRow`, `rowStateNew`,
135
159
  `rowStateEdited` and `rowStateDeleted` among them.
136
160
 
137
161
  ## Styling and test hooks
@@ -139,17 +163,17 @@ control carries a tooltip from the labels, `revertRow`, `rowStateNew`,
139
163
  | Name | Kind | What it is |
140
164
  | --- | --- | --- |
141
165
  | `--dg-entry-height` | CSS variable | Height of the sticky entry block. From `size`. |
142
- | `--dg-row-new-bg` | CSS variable | Background of an entered new row. A green tint. |
143
- | `data-deleted` | Row attribute | On a row marked for deletion under draft mode. |
166
+ | `--dg-row-new-bg` | CSS variable | Background of a committed new row, in the body or the entry block. A green tint. |
167
+ | `data-deleted` | Row attribute | On a row marked for deletion under `draft: true`. |
144
168
  | `data-dirty` | Row attribute | On a body row holding a dirty draft. Also on the cell whose field is dirty. |
145
- | `data-new` / `data-confirmed` | Entry row attributes | On an entry row; `data-confirmed` once it is entered, awaiting Save all. |
146
- | `data-dg-entry-flow-block` | Attribute | The in-flow block above the body rows holding entered new rows, unless `editing.newRowsSticky` keeps them in the sticky entry block. |
169
+ | `data-new` | Row attribute | On a body row that is a committed new row, and on an entry row. |
170
+ | `data-committed` | Entry row attribute | On an entry row once it is committed, awaiting the save. Seen only under `editing.newRowsSticky`. |
147
171
  | `data-dg-part="editor-input"` | Part | The control inside an editing cell. |
148
- | `data-dg-part="save-row"` / `"cancel-row"` | Parts | The edit lane's buttons on an open row, with `data-row-id`. Row mode only. |
149
- | `data-dg-part="row-state"` | Part | Draft mode's change marker, with `data-row-id` and `data-state` of `new`, `edited` or `deleted`. |
172
+ | `data-dg-part="save-row"` / `"cancel-row"` | Parts | The edit lane's buttons on an open row, with `data-row-id`. |
173
+ | `data-dg-part="row-state"` | Part | The draft store's change marker, with `data-row-id` and `data-state` of `new`, `edited` or `deleted`. |
150
174
  | `data-dg-part="revert-row"` / `"restore-row"` | Parts | Drops a row's draft, and undoes a deletion mark, with `data-row-id`. |
151
175
  | `data-dg-part="edit-row"` / `"delete-row"` | Parts | The idle lane's pencil and trash, with `data-row-id`. `edit-row` also reopens an entered new row. |
152
176
  | `data-dg-part="confirm-new-row"` / `"discard-new-row"` | Parts | An entry row's ✓ and ✕, with `data-row-id`. |
153
- | `data-dg-part="save-all"` / `"discard-all"` | Parts | `EditActions`. |
177
+ | `data-dg-part="save-all"` / `"discard-all"` | Parts | `DraftActions`. |
154
178
 
155
179
  See the `testing` skill for how to compose these into selectors.
@@ -8,14 +8,20 @@ bad value being committed. Both follow from the column.
8
8
  `meta.type` picks one, `meta.options` feeds the two select editors from the same
9
9
  declaration the filter panel reads.
10
10
 
11
- | `meta.type` | Editor | Export |
12
- | --- | --- | --- |
13
- | `"string"` (default) | Text input | `TMDataGridStringEditor` |
14
- | `"number"` | Number input | `TMDataGridNumberEditor` |
15
- | `"boolean"` | Checkbox | `TMDataGridBooleanEditor` |
16
- | `"date"` | Native `<input type="date">`, ISO `YYYY-MM-DD` | `TMDataGridDateEditor` |
17
- | `"select"` | Searchable select, commits on pick under `"cell"` | `TMDataGridSelectEditor` |
18
- | `"multiSelect"` | Multi-select, same source | `TMDataGridMultiSelectEditor` |
11
+ | `meta.type` | Editor | Writes | Export |
12
+ | --- | --- | --- | --- |
13
+ | `"string"` (default) | Text input | `string` | `TMDataGridStringEditor` |
14
+ | `"number"` | Number input | `number`, or `null` while the cell is empty or the text is not yet a number | `TMDataGridNumberEditor` |
15
+ | `"boolean"` | Checkbox | `boolean` | `TMDataGridBooleanEditor` |
16
+ | `"date"` | Native `<input type="date">` | A `Date`, or the ISO `"YYYY-MM-DD"` string; `null` when cleared | `TMDataGridDateEditor` |
17
+ | `"select"` | Searchable select, commits on pick under `"cell"` | `string \| null` | `TMDataGridSelectEditor` |
18
+ | `"multiSelect"` | Multi-select, same source | `string[]` | `TMDataGridMultiSelectEditor` |
19
+
20
+ **Writes** is the value the editor puts into the draft: what `meta.edit.mapValue` is handed, what `meta.edit.validate` checks, and what a commit carries in `value` and in `changes[].next`.
21
+
22
+ The number editor writes `null` rather than `NaN` while the text does not parse, so a half-typed number leaves the field empty instead of committing a number no rule can describe.
23
+ The date editor picks between its two types once, when it opens, from what the cell held: a `Date` cell keeps receiving `Date`s and a string cell keeps receiving `"YYYY-MM-DD"` strings, so clearing and retyping cannot flip the type.
24
+ A validator for a date column has to accept whichever of the two that column's data holds.
19
25
 
20
26
  ```tsx
21
27
  columnHelper.accessor("department", {
@@ -57,6 +63,12 @@ Bind any control to `field` exactly as inside any TanStack Form:
57
63
  `field.state.value`, `field.state.meta.errors`, `field.handleChange`,
58
64
  `field.handleBlur`.
59
65
 
66
+ The message itself is the host's: it shows in a tooltip on the editor, opened
67
+ by focus and by hover, for a custom editor as much as a built-in one. What an
68
+ editor binds is the invalid state - the built-ins pass a boolean to the input's
69
+ `error` prop for the border alone, and an editor that binds nothing looks
70
+ unchanged while the commit is refused.
71
+
60
72
  ```tsx
61
73
  import { Slider } from "@mantine/core";
62
74
  import type { TMDataGridEditorComponent } from "@jielga/tmdatagrid";
@@ -155,6 +167,9 @@ Left unmapped on purpose:
155
167
  edited, mark a pristine row dirty and swallow the select-all.
156
168
  - `edit.clearCell()`, the Delete key: it writes the type's empty value through
157
169
  the form, so there is no input to map.
170
+ - `edit.setCellValue()` and `edit.setRowValues()`: they write through the form
171
+ too, and the caller passes the stored value itself. `meta.edit.validate` still
172
+ runs on the commit.
158
173
  - An editor calling `field.setValue`. `handleChange` is the mapped path.
159
174
 
160
175
  The built-in string and number editors restore the caret after a mapped write,
@@ -178,8 +193,13 @@ meta: { edit: { validate: z.string().min(2, "At least two characters") } }
178
193
  // Object form: pick the trigger.
179
194
  meta: { edit: { validate: { onBlur: z.string().email("Not an email address") } } }
180
195
 
181
- // A plain function works too.
182
- meta: { edit: { validate: ({ value }) => (value > 0 ? undefined : "Must be positive") } }
196
+ // A plain function works too. `value` is typed `never`, so annotate the parameter.
197
+ meta: {
198
+ edit: {
199
+ validate: ({ value }: { value: unknown }) =>
200
+ typeof value === "number" && value > 0 ? undefined : "Must be positive",
201
+ },
202
+ }
183
203
  ```
184
204
 
185
205
  `normalizeFieldValidate(validate)` is exported for consumers building their own
@@ -209,12 +229,46 @@ const grid = useTMDataGrid({
209
229
  ```
210
230
 
211
231
  Issues with a path land on the matching column's cell; pathless issues land on
212
- the row. A nested schema's issues follow the same rule, so a `address.city`
213
- issue lands on the column whose `editField` is `"address.city"`.
232
+ the row, where the message shows in the edit lane's tooltip on the open
233
+ row's ✓. A nested schema's issues follow the
234
+ same rule, so a `address.city` issue lands on the column whose `editField` is
235
+ `"address.city"`.
214
236
 
215
237
  Cross-field rules need a mode that commits the whole row at once. Under `"cell"`
216
238
  each cell commits alone, so the rule is evaluated against the other column's
217
- unedited value and cannot pass. Use `"row"` or `"draft"`.
239
+ unedited value and cannot pass. Use `"row"`.
240
+
241
+ ## Cross-row rules
242
+
243
+ `editing.tableValidators` holds the rules that need the other rows. Its
244
+ `onSubmit` / `onSubmitAsync` receive `{ value, rowId, isNew, rows }`:
245
+ `value` is the committing row as drafted, and `rows` is
246
+ `Array<{ rowId, value }>` - the collection as it would stand if the commit
247
+ landed, with every draft overlaid, entry rows appended and deletion-marked
248
+ rows removed. `rows` is unfiltered and never contains group rows.
249
+
250
+ ```tsx
251
+ editing: {
252
+ mode: "cell",
253
+ draft: true,
254
+ tableValidators: {
255
+ onSubmit: ({ value, rowId, rows }) =>
256
+ rows.some((r) => r.rowId !== rowId && r.value.code === value.code)
257
+ ? { fields: { code: "Codes must be unique" } }
258
+ : undefined,
259
+ },
260
+ }
261
+ ```
262
+
263
+ The result is the `rowValidators` vocabulary: nothing passes, a string is a
264
+ row-level message, `{ form, fields }` lands pathed issues on the committing
265
+ row's cells. `onSubmit` runs first, and its failure stands without
266
+ `onSubmitAsync` running. Errors land on the committing row only.
267
+
268
+ The rules run at every commit - typed, ✓, `edit.setCellValue`, an entry
269
+ row's - after the row's own validators, and again for every committed row during
270
+ `saveDrafts`, the only rules that run there: a committed row that a later edit
271
+ invalidated is reopened with its errors, and the save resolves `false`.
218
272
 
219
273
  ## Server-side errors
220
274
 
@@ -234,9 +288,9 @@ rowValidators: {
234
288
  },
235
289
  ```
236
290
 
237
- A commit blocked by validation keeps the editor open with the message on the
238
- input. A rejected `editing.onCommit` keeps the draft too, with the error on the
239
- row.
291
+ A commit blocked by validation keeps the editor open, invalid, with the message
292
+ in its tooltip. A rejected `editing.onCommit` keeps the draft too, with the
293
+ error on the row.
240
294
 
241
295
  ## Where the state shows
242
296
 
@@ -245,9 +299,13 @@ row.
245
299
  | The cell's own value | A held draft is displayed: the cell renders the draft value through the column's `cell` renderer, in every mode |
246
300
  | Blue cell corner | The field is dirty against its original value |
247
301
  | Red cell corner | The field carries a validation error |
248
- | Row error text | A pathless rule failed, or a commit was rejected |
302
+ | Field error message | In a tooltip on the open editor, shown while the input has focus and on hover |
303
+ | Row error text | A pathless rule failed, or a commit was rejected. In the lane's tooltip, on the open row's ✓ |
249
304
  | `data-dirty` on the row | The row holds a dirty draft |
250
305
 
251
306
  The same information is readable from `edit.store`: `rows[rowId].dirtyFields`,
252
- `rows[rowId].errorFields`, `rows[rowId].hasRowError`,
253
- `rows[rowId].isSubmitting`, and `rows[rowId].values` for the draft itself.
307
+ `rows[rowId].errorFields`, `rows[rowId].errorMessages` (`{ field, message }`
308
+ pairs), `rows[rowId].hasRowError`, `rows[rowId].isSubmitting`, and
309
+ `rows[rowId].values` for the draft itself. The pathless message's text is not
310
+ in the store; read it from `edit.getForm(rowId)?.state.errors` on the open
311
+ row that carries it.