@jielga/tmdatagrid 2.0.0-beta.4 → 2.0.0-beta.7

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 (37) hide show
  1. package/dist/index.d.ts +339 -152
  2. package/dist/index.js +2602 -2450
  3. package/dist/index.js.map +1 -1
  4. package/dist/styles.css +1 -1
  5. package/package.json +1 -1
  6. package/skills/appearance/SKILL.md +1 -1
  7. package/skills/cell-selection/SKILL.md +1 -1
  8. package/skills/columns/SKILL.md +44 -2
  9. package/skills/data/SKILL.md +14 -10
  10. package/skills/editing/SKILL.md +140 -68
  11. package/skills/editing/references/common-mistakes.md +98 -8
  12. package/skills/editing/references/editing-api.md +40 -30
  13. package/skills/editing/references/editors-and-validation.md +65 -14
  14. package/skills/filtering/SKILL.md +1 -1
  15. package/skills/getting-started/SKILL.md +3 -3
  16. package/skills/grouping/SKILL.md +5 -3
  17. package/skills/options/SKILL.md +2 -2
  18. package/skills/rows/SKILL.md +7 -3
  19. package/skills/server-side/SKILL.md +1 -1
  20. package/skills/testing/SKILL.md +1 -1
  21. package/src/tmdatagrid/components/TMDataGrid.tsx +2 -2
  22. package/src/tmdatagrid/components/TMDataGridCellEditor.tsx +33 -34
  23. package/src/tmdatagrid/components/TMDataGridDraftActions.tsx +296 -0
  24. package/src/tmdatagrid/components/TMDataGridEditColumn.tsx +40 -33
  25. package/src/tmdatagrid/components/TMDataGridEntryRows.tsx +7 -7
  26. package/src/tmdatagrid/components/TMDataGridTable.tsx +27 -5
  27. package/src/tmdatagrid/components/TMDataGridToolbar.module.css +4 -0
  28. package/src/tmdatagrid/components/TMDataGridToolbar.tsx +8 -10
  29. package/src/tmdatagrid/components/editors/TMDataGridNumberEditor.tsx +34 -4
  30. package/src/tmdatagrid/components/editors/editorShared.ts +1 -29
  31. package/src/tmdatagrid/core/capabilities.ts +14 -6
  32. package/src/tmdatagrid/core/editEngine.ts +472 -96
  33. package/src/tmdatagrid/core/labels.ts +3 -3
  34. package/src/tmdatagrid/core/summary.ts +10 -2
  35. package/src/tmdatagrid/index.ts +9 -7
  36. package/src/tmdatagrid/useTMDataGrid.tsx +107 -47
  37. package/src/tmdatagrid/components/TMDataGridEditActions.tsx +0 -233
@@ -73,14 +73,52 @@ meta: { edit: { editor: SalaryEditor } },
73
73
 
74
74
  Source: `src/docs/editors.md`, `src/tmdatagrid/core/editEngine.ts`.
75
75
 
76
+ ## HIGH A custom editor that binds no error text
77
+
78
+ The built-in editors bind the field's first error to the input's `error` prop.
79
+ A custom editor that binds nothing still blocks the commit, but all the user
80
+ sees is `data-invalid` on the cell - the row stays open with no message
81
+ anywhere on screen, which reads as a broken grid rather than a rejected value.
82
+
83
+ Wrong:
84
+
85
+ ```tsx
86
+ const SalaryEditor: TMDataGridEditorComponent = ({ field, commit }) => (
87
+ <Slider value={field.state.value} onChange={field.handleChange} onChangeEnd={() => void commit()} />
88
+ );
89
+ ```
90
+
91
+ Correct:
92
+
93
+ ```tsx
94
+ const SalaryEditor: TMDataGridEditorComponent = ({ field, commit }) => {
95
+ const error = field.state.meta.errors
96
+ .map((e) => (typeof e === "string" ? e : e?.message))
97
+ .find(Boolean);
98
+ return (
99
+ <Slider
100
+ value={field.state.value}
101
+ onChange={field.handleChange}
102
+ onChangeEnd={() => void commit()}
103
+ error={error}
104
+ />
105
+ );
106
+ };
107
+ ```
108
+
109
+ An entry of `field.state.meta.errors` is a string from a function validator,
110
+ or an issue carrying a `message` from a schema.
111
+
112
+ Source: `src/docs/editors.md` (Writing your own).
113
+
76
114
  ## HIGH A cross-field rule under `editing.mode: "cell"`
77
115
 
78
116
  Under `"cell"` each cell commits on its own, so a rule spanning two columns
79
117
  cannot be satisfied by either one: the first cell edited is rejected against the
80
118
  other column's old value, and the row cannot be saved.
81
119
 
82
- Correct: `editing.rowValidators` needs `mode: "row"` or `"draft"`, which
83
- validate the whole row in one commit.
120
+ Correct: `editing.rowValidators` needs `mode: "row"`, which validates the
121
+ whole row in one commit.
84
122
 
85
123
  Source: `src/docs/editors.md` (Validation).
86
124
 
@@ -120,9 +158,9 @@ and the grid shows the old value as though nothing happened.
120
158
  Wrong:
121
159
 
122
160
  ```tsx
123
- onCommit: async ({ rowId, changes }) => {
161
+ onCommit: async ({ rowId, value }) => {
124
162
  try {
125
- await api.patch(rowId, changes);
163
+ await api.put(rowId, value);
126
164
  } catch (error) {
127
165
  console.error(error);
128
166
  }
@@ -137,7 +175,8 @@ Source: `src/tmdatagrid/useTMDataGrid.tsx` (`TMDataGridEditingCallbacks`).
137
175
  ## HIGH Submitting an outer form while the grid holds a draft
138
176
 
139
177
  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 `saveDrafts()`,
178
+ row is invisible to it, and under `draft: true` every edit is until
179
+ `saveDrafts()`,
141
180
  so a form submit saves stale rows and collection rules skip pending values.
142
181
 
143
182
  Correct:
@@ -154,6 +193,57 @@ if (flushed) await form.handleSubmit();
154
193
 
155
194
  Source: `src/docs/query-builder.md` (Which mode, Submitting).
156
195
 
196
+ ## HIGH A bulk write built from `begin`, `getForm` and `commit`
197
+
198
+ `edit.setCellValue(rowId, columnId, value)` writes one cell and commits its row, so a fill over the selection is one call per row.
199
+ 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.
200
+ The `commit` that follows finds no form and resolves `true`, reporting a write that never happened.
201
+
202
+ Wrong:
203
+
204
+ ```tsx
205
+ for (const row of grid.table.getSelectedRowModel().rows) {
206
+ grid.edit.begin({ rowId: row.id, columnId: "targetPct" });
207
+ grid.edit.getForm(row.id)?.setFieldValue("targetPct", next(row.original));
208
+ await grid.edit.commit(row.id);
209
+ }
210
+ ```
211
+
212
+ Correct:
213
+
214
+ ```tsx
215
+ for (const row of grid.table.getSelectedRowModel().rows) {
216
+ await grid.edit.setCellValue(row.id, "targetPct", next(row.original));
217
+ }
218
+
219
+ // Several cells of one row: one commit, one draft entry.
220
+ await grid.edit.setRowValues(rowId, { status: "Closed", closedOn: today() });
221
+ ```
222
+
223
+ 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.
224
+ 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.
225
+
226
+ Source: `src/tmdatagrid/core/editEngine.ts` (`begin`, `writeFields`).
227
+
228
+ ## MEDIUM A computed column frozen while a row is edited
229
+
230
+ A held draft is displayed by the column that owns the field. A column computed
231
+ from other fields - `accessorFn` or `display` - reads `row.original`, which is
232
+ `data`, so it keeps showing the saved record while the values it derives from
233
+ are being typed.
234
+
235
+ Correct: read the drafted row from `edit.store` inside the cell renderer:
236
+
237
+ ```tsx
238
+ function useDraftedRow(rowId: string, original: Product): Product {
239
+ const { edit } = useTMDataGridContext();
240
+ const values = useSelector(edit.store, (state) => state.rows[rowId]?.values);
241
+ return (values as Product | undefined) ?? original;
242
+ }
243
+ ```
244
+
245
+ Source: `src/docs/editing.md` (Draft lifetime).
246
+
157
247
  ## MEDIUM Reading a commit's result as the saved value
158
248
 
159
249
  `edit.commit(rowId)`, `edit.commitAll()` and `edit.saveDrafts()` resolve to a
@@ -168,10 +258,10 @@ notifications.show({ message: saved ? "Saved" : "Some rows need attention" });
168
258
 
169
259
  Source: `src/tmdatagrid/core/editEngine.ts` (`TMDataGridEditApi`).
170
260
 
171
- ## MEDIUM Expecting `editing.onRowDelete` to fire under draft
261
+ ## MEDIUM Expecting `editing.onRowDelete` to fire under a draft store
172
262
 
173
- Under the immediate modes `edit.deleteRow` calls `editing.onRowDelete` at once.
174
- Under `"draft"` it only toggles a deletion mark, so nothing is removed until
263
+ Without `editing.draft`, `edit.deleteRow` calls `editing.onRowDelete` at once.
264
+ Under `draft: true` it only toggles a deletion mark, so nothing is removed until
175
265
  `saveDrafts`: the ids arrive as `deleted` in `editing.onSaveDrafts`, or, with
176
266
  no such callback, in the per-row `editing.onRowDelete` loop. A confirmation
177
267
  placed inside `editing.onRowDelete` therefore guards the save, not the trash.
@@ -8,27 +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 parks 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 entered new rows pinned in the entry block until the save, instead of letting them scroll with the body. |
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
- `onSaveDrafts` 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.onSaveDrafts` | `{ rows, added, deleted }` | Draft mode only. One call for the whole draft store. Without it, `saveDrafts` loops `editing.onCommit`, `editing.onRowAdd` and `editing.onRowDelete`. |
29
- | `editing.onCommitDrafts` | `{ rows, added, deleted }` | **Deprecated** - renamed to `onSaveDrafts`. Still honoured; the new name wins if both are set. |
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. |
30
33
  | `editing.onRowAdd` | `{ tempId, value }` | Commits an entry row. Mint the real id here. |
31
- | `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. |
32
35
 
33
36
  `changes` entries are `{ columnId, field, previous, next }`; `field` is the data
34
37
  path, which may be dotted.
@@ -51,7 +54,7 @@ path, which may be dotted.
51
54
  | Member | Signature | Notes |
52
55
  | --- | --- | --- |
53
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. |
54
- | `commit` | `(rowId) => Promise<boolean>` | The OK gesture: submits the row's form. `false` keeps it open with its errors. Under `"draft"` a pass puts the row in the draft store - no consumer callback runs until `saveDrafts`. Column rules run whether or not an editor is mounted. |
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 parks the row - no consumer callback runs until `saveDrafts`. Column rules run whether or not an editor is mounted. |
55
58
  | `commitAll` | `() => Promise<boolean>` | Submits every open row. Rows that fail stay open. `false` when one did. |
56
59
  | `saveDrafts` | `() => Promise<boolean>` | Sends the draft store. Open rows are left alone and stay open. |
57
60
  | `cancel` | `(rowId) => void` | Drops one draft. |
@@ -59,11 +62,14 @@ path, which may be dotted.
59
62
  | `deactivate` | `() => void` | Closes the editor without touching the draft, as blur does under `"cellConfirm"`. |
60
63
  | `submitAll` | `() => Promise<boolean>` | **Deprecated** - `commitAll()` then `saveDrafts()`. |
61
64
  | `clearCell` | `(rowId, columnId) => Promise<boolean>` | What Delete does: writes the type's empty value and commits. |
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 parks 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`. |
62
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. |
63
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. |
64
- | `deleteRow` | `(rowId) => void` | `editing.onRowDelete` under the immediate modes, a deletion mark under draft mode. Toggles: a second call restores the row. |
65
- | `canEditCell` | `(row, column) => boolean` | The check the built-in controls use. |
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. |
66
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`. |
67
73
  | `canDeleteRows` | `() => boolean` | Whether the delete control should be shown. |
68
74
  | `getForm` | `(rowId) => TMDataGridRowEditForm \| undefined` | The row's live `FormApi`. |
69
75
  | `state` | `TMDataGridEditState` | Snapshot, for reads outside React. |
@@ -91,10 +97,10 @@ type TMDataGridEditState = {
91
97
  }
92
98
  >;
93
99
  // The draft store's edit slice: existing rows whose form passed its submit,
94
- // parked for `saveDrafts`. Empty outside `"draft"`.
100
+ // parked for `saveDrafts`. Empty without `editing.draft`.
95
101
  committedRowIds: ReadonlyArray<string>;
96
- // `committed` is draft mode's "in the store, awaiting the save". Under the
97
- // immediate modes a commit adds through `onRowAdd`, so it stays `false`.
102
+ // `committed` is "in the store, awaiting the save". Without `editing.draft`
103
+ // a commit adds through `onRowAdd`, so it stays `false`.
98
104
  newRows: ReadonlyArray<{ tempId: string; committed: boolean }>;
99
105
  deletedRowIds: ReadonlyArray<string>;
100
106
  };
@@ -104,8 +110,8 @@ type TMDataGridEditState = {
104
110
 
105
111
  | Name | Kind | What it is |
106
112
  | --- | --- | --- |
107
- | `TMDataGrid.EditActions` | Component | Save with the pending count, and Discard. Any mode; renders nothing while editing is off. Takes `renderActions` over `{ state, actions, Controls }`. |
108
- | `TMDataGridEditActions` | Export | The same component, for use outside the namespace. |
113
+ | `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. |
114
+ | `TMDataGridDraftActions` | Export | The same component, for use outside the namespace. |
109
115
  | `EDIT_COLUMN_ID` | Export | `"__edit__"`, the generated edit lane's id. |
110
116
  | `clearedValueForType` | Export | `(type) => unknown` - what Delete writes per column type. |
111
117
  | `getEditFieldName` | Export | `(column) => string` - the data path a column's edits write to. |
@@ -125,20 +131,24 @@ Generated, pinned right, id `EDIT_COLUMN_ID`. It appears when any of these
125
131
  holds:
126
132
 
127
133
  - `editing.mode: "row"` - the lane is Save and Cancel's home
128
- - `editing.mode: "draft"` - the lane is the change indicator and the per-row
129
- revert, and `editing.onSaveDrafts` is not required for it
134
+ - `editing.draft` - the lane is the change indicator and the per-row revert,
135
+ and `editing.onSaveDrafts` is not required for it
130
136
  - `editing.onRowDelete` is set - the trash can has somewhere to report to
131
137
 
132
- `"cell"` and `"cellConfirm"` have no lane unless `editing.onRowDelete` asks for
133
- one.
138
+ `"cell"` and `"cellConfirm"` have no lane unless `editing.draft` or
139
+ `editing.onRowDelete` asks for one.
134
140
 
135
- What the lane holds depends on the mode. Under `"row"` an open row shows
136
- `save-row` and `cancel-row`; those two never render under `"draft"`. Under
137
- `"draft"` every changed row shows `row-state`, whose `data-state` is `new`,
138
- `edited` or `deleted`, together with `revert-row` on an edited row,
139
- `restore-row` on one marked for deletion, and `edit-row` plus `discard-new-row`
140
- on an entered new row. A row holding a dirty draft hides `delete-row`. Every
141
- control carries a tooltip from the labels, `revertRow`, `rowStateNew`,
141
+ The trash itself shows when the deletion has somewhere to report to:
142
+ `onRowDelete` is set, or under `draft: true`, `onSaveDrafts` is - a deletion
143
+ mark is part of the save.
144
+
145
+ What the lane holds follows the row's state, one axis each. An open row shows
146
+ the mode's own controls, `save-row` and `cancel-row`. A parked row shows
147
+ `row-state` instead, whose `data-state` is `new`, `edited` or `deleted`,
148
+ together with `revert-row` on an edited row, `restore-row` on one marked for
149
+ deletion, and `edit-row` plus `discard-new-row` on an entered new row - never a
150
+ save, which the engine would only park again. A parked row hides `delete-row`.
151
+ Every control carries a tooltip from the labels, `revertRow`, `rowStateNew`,
142
152
  `rowStateEdited` and `rowStateDeleted` among them.
143
153
 
144
154
  ## Styling and test hooks
@@ -147,16 +157,16 @@ control carries a tooltip from the labels, `revertRow`, `rowStateNew`,
147
157
  | --- | --- | --- |
148
158
  | `--dg-entry-height` | CSS variable | Height of the sticky entry block. From `size`. |
149
159
  | `--dg-row-new-bg` | CSS variable | Background of an entered new row. A green tint. |
150
- | `data-deleted` | Row attribute | On a row marked for deletion under draft mode. |
160
+ | `data-deleted` | Row attribute | On a row marked for deletion under `draft: true`. |
151
161
  | `data-dirty` | Row attribute | On a body row holding a dirty draft. Also on the cell whose field is dirty. |
152
162
  | `data-new` / `data-committed` | Entry row attributes | On an entry row; `data-committed` once it is committed, awaiting the save. |
153
163
  | `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. |
154
164
  | `data-dg-part="editor-input"` | Part | The control inside an editing cell. |
155
- | `data-dg-part="save-row"` / `"cancel-row"` | Parts | The edit lane's buttons on an open row, with `data-row-id`. Row mode only. |
156
- | `data-dg-part="row-state"` | Part | Draft mode's change marker, with `data-row-id` and `data-state` of `new`, `edited` or `deleted`. |
165
+ | `data-dg-part="save-row"` / `"cancel-row"` | Parts | The edit lane's buttons on an open row, with `data-row-id`. |
166
+ | `data-dg-part="row-state"` | Part | The draft store's change marker, with `data-row-id` and `data-state` of `new`, `edited` or `deleted`. |
157
167
  | `data-dg-part="revert-row"` / `"restore-row"` | Parts | Drops a row's draft, and undoes a deletion mark, with `data-row-id`. |
158
168
  | `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. |
159
169
  | `data-dg-part="confirm-new-row"` / `"discard-new-row"` | Parts | An entry row's ✓ and ✕, with `data-row-id`. |
160
- | `data-dg-part="save-all"` / `"discard-all"` | Parts | `EditActions`. |
170
+ | `data-dg-part="save-all"` / `"discard-all"` | Parts | `DraftActions`. |
161
171
 
162
172
  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
+ Binding `field.state.meta.errors` is what shows a refused commit: the built-in
67
+ editors pass the first error to the input's `error` prop, and an editor that
68
+ binds nothing leaves a blocked save as `data-invalid` on the cell with no
69
+ message on screen. An entry is a string from a function validator, or an issue
70
+ carrying a `message` from a schema.
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,
@@ -209,12 +224,46 @@ const grid = useTMDataGrid({
209
224
  ```
210
225
 
211
226
  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"`.
227
+ the row, where the message shows in the edit lane's tooltip - on the open
228
+ row's ✓, and on the parked row's marker. A nested schema's issues follow the
229
+ same rule, so a `address.city` issue lands on the column whose `editField` is
230
+ `"address.city"`.
214
231
 
215
232
  Cross-field rules need a mode that commits the whole row at once. Under `"cell"`
216
233
  each cell commits alone, so the rule is evaluated against the other column's
217
- unedited value and cannot pass. Use `"row"` or `"draft"`.
234
+ unedited value and cannot pass. Use `"row"`.
235
+
236
+ ## Cross-row rules
237
+
238
+ `editing.tableValidators` holds the rules that need the other rows. Its
239
+ `onSubmit` / `onSubmitAsync` receive `{ value, rowId, isNew, rows }`:
240
+ `value` is the committing row as drafted, and `rows` is
241
+ `Array<{ rowId, value }>` - the collection as it would stand if the commit
242
+ landed, with every draft overlaid, entry rows appended and deletion-marked
243
+ rows removed. `rows` is unfiltered and never contains group rows.
244
+
245
+ ```tsx
246
+ editing: {
247
+ mode: "cell",
248
+ draft: true,
249
+ tableValidators: {
250
+ onSubmit: ({ value, rowId, rows }) =>
251
+ rows.some((r) => r.rowId !== rowId && r.value.code === value.code)
252
+ ? { fields: { code: "Codes must be unique" } }
253
+ : undefined,
254
+ },
255
+ }
256
+ ```
257
+
258
+ The result is the `rowValidators` vocabulary: nothing passes, a string is a
259
+ row-level message, `{ form, fields }` lands pathed issues on the committing
260
+ row's cells. `onSubmit` runs first, and its failure stands without
261
+ `onSubmitAsync` running. Errors land on the committing row only.
262
+
263
+ The rules run at every commit - typed, ✓, `edit.setCellValue`, an entry
264
+ row's - after the row's own validators, and again for every parked row during
265
+ `saveDrafts`: a draft that a later edit invalidated fails there, keeps its
266
+ markers, and the save resolves `false`.
218
267
 
219
268
  ## Server-side errors
220
269
 
@@ -245,9 +294,11 @@ row.
245
294
  | 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
295
  | Blue cell corner | The field is dirty against its original value |
247
296
  | Red cell corner | The field carries a validation error |
248
- | Row error text | A pathless rule failed, or a commit was rejected |
297
+ | Row error text | A pathless rule failed, or a commit was rejected. In the lane's tooltip: the open row's ✓, or the parked row's marker |
249
298
  | `data-dirty` on the row | The row holds a dirty draft |
250
299
 
251
300
  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.
301
+ `rows[rowId].errorFields`, `rows[rowId].errorMessages` (`{ field, message }`
302
+ pairs), `rows[rowId].hasRowError`, `rows[rowId].isSubmitting`, and
303
+ `rows[rowId].values` for the draft itself. The pathless message's text is not
304
+ in the store; read it from `edit.getForm(rowId)?.state.errors`.
@@ -15,7 +15,7 @@ description: >
15
15
  metadata:
16
16
  type: core
17
17
  library: '@jielga/tmdatagrid'
18
- library_version: '2.0.0-beta.4'
18
+ library_version: '2.0.0-beta.7'
19
19
  sources:
20
20
  - 'Jielga/TMDataGrid:src/docs/filtering.md'
21
21
  - 'Jielga/TMDataGrid:src/docs/quick-search.md'
@@ -4,14 +4,14 @@ description: >
4
4
  Set up TMDataGrid, a compound React data grid built on TanStack Table v9 and
5
5
  Mantine. Covers useTMDataGrid, the TMDataGrid root, context, the component
6
6
  catalog (Table, Footer, Toolbar, Spacer, SummaryCount, Search,
7
- LoadingIndicator, EditActions, FilterButton, ColumnsButton, FilterPanel,
7
+ LoadingIndicator, DraftActions, FilterButton, ColumnsButton, FilterPanel,
8
8
  FilterPills, ColumnsPanel), the size scale and the bounded-height layout
9
9
  requirement. Load when adding a grid, choosing which parts to render, or when
10
10
  rows do not appear.
11
11
  metadata:
12
12
  type: core
13
13
  library: '@jielga/tmdatagrid'
14
- library_version: '2.0.0-beta.4'
14
+ library_version: '2.0.0-beta.7'
15
15
  sources:
16
16
  - 'Jielga/TMDataGrid:src/docs/getting-started.md'
17
17
  - 'Jielga/TMDataGrid:src/docs/anatomy.md'
@@ -156,7 +156,7 @@ flex column.
156
156
  | `TMDataGrid.SummaryCount` | `children` | Visible rows out of total. |
157
157
  | `TMDataGrid.Search` | `placeholder`, `debounce` (default `250`), `w` (default `220`) | Quick search over every column, debounced into `globalFilter`. Renders nothing under `enableGlobalFilter: false`. |
158
158
  | `TMDataGrid.LoadingIndicator` | - | Small spinner while `meta.loading` is `true` and rows stay on screen. |
159
- | `TMDataGrid.EditActions` | `renderActions` | Save with the pending count, and Discard. Renders nothing while editing is off - see the `editing` skill. |
159
+ | `TMDataGrid.DraftActions` | `renderActions` | Save with the pending count, and Discard. Renders nothing while editing is off - see the `editing` skill. |
160
160
  | `TMDataGrid.FilterButton` | - | Toggles filter panel. Renders nothing if no column is filterable. |
161
161
  | `TMDataGrid.ColumnsButton` | - | Opens column manager. Renders nothing if no column is hideable. |
162
162
  | `TMDataGrid.FilterPanel` | - | Rendered by `.Table`; exported for custom layouts. Header close button, Escape, click-away, "Add filter" and "Clear all". |
@@ -14,7 +14,7 @@ description: >
14
14
  metadata:
15
15
  type: core
16
16
  library: '@jielga/tmdatagrid'
17
- library_version: '2.0.0-beta.4'
17
+ library_version: '2.0.0-beta.7'
18
18
  sources:
19
19
  - 'Jielga/TMDataGrid:src/docs/grouping.md'
20
20
  - 'Jielga/TMDataGrid:src/docs/summary-row.md'
@@ -86,7 +86,9 @@ would pass a row that looks real but is the wrong one. Group rows therefore:
86
86
  - do not fire `onRowClick` or the cell handlers
87
87
  - cannot be highlighted, pinned, or given a details panel
88
88
  - never edit
89
- - carry `data-grouped` and `data-depth`, with `--dg-row-group-bg` behind them
89
+ - carry `data-grouped="true"` and `data-depth`, with `--dg-row-group-bg`
90
+ behind them. `data-grouped` is on every row, `"true"` or `"false"`, so match
91
+ the value rather than the bare attribute
90
92
 
91
93
  A group row's checkbox selects every record under it at any depth, including
92
94
  records inside collapsed sub-groups, showing a tick once all are selected and a
@@ -256,7 +258,7 @@ Source: `src/docs/grouping.md` (Server-side grids).
256
258
  | `isPagingActive` | Export | `(table, features) => boolean` | – | Whether the pager is slicing anything. `false` while grouped. |
257
259
  | `--dg-row-group-bg` | CSS variable | colour | Themed | Group row background. |
258
260
  | `--dg-summary-height` | CSS variable | length | From `size` | Height of the summary row. |
259
- | `data-grouped` · `data-depth` | Data attributes | – | – | On group rows, and the nesting level on every row. |
261
+ | `data-grouped` · `data-depth` | Data attributes | – | – | `"true"` on group rows (published on every row), and the nesting level on every row. |
260
262
 
261
263
  See also: the `rows` skill for selection and the details lane, and the `data`
262
264
  skill for the pager grouping suspends.
@@ -14,7 +14,7 @@ description: >
14
14
  metadata:
15
15
  type: core
16
16
  library: '@jielga/tmdatagrid'
17
- library_version: '2.0.0-beta.4'
17
+ library_version: '2.0.0-beta.7'
18
18
  sources:
19
19
  - 'Jielga/TMDataGrid:src/docs/use-tm-data-grid.md'
20
20
  - 'Jielga/TMDataGrid:src/tmdatagrid/useTMDataGrid.tsx'
@@ -66,7 +66,7 @@ rather than forwarded to TanStack.
66
66
  | `state` | `Partial<TableState>` | – | Controlled state. Each slice requires its `onXChange` - see below. |
67
67
  | `meta` | `TMDataGridTableMeta` | `{}` | Grid configuration, see below. |
68
68
  | `persist` | `TMDataGridPersistence` | – | State persistence, see below. |
69
- | `editing` | `TMDataGridEditingOptions` | off | Turns editing on. `mode` (`"cell" \| "cellConfirm" \| "row" \| "draft"`) picks the commit policy; the object also holds `onCommit`, `onSaveDrafts`, `rowValidators`, `isRowEditable`, `newRowDefaults`, `newRowsSticky`, `onRowAdd` and `onRowDelete` - see the `editing` skill. |
69
+ | `editing` | `TMDataGridEditingOptions` | off | Turns editing on. Two axes: `mode` (`"cell" \| "cellConfirm" \| "row"`) picks what counts as a commit, `draft` picks whether it parks in the draft store; the object also holds `onCommit`, `onSaveDrafts`, `rowValidators`, `isRowEditable`, `newRowDefaults`, `newRowsSticky`, `onRowAdd` and `onRowDelete` - see the `editing` skill. |
70
70
  | `labels` | `TMDataGridLabelsOverride` | English | Overrides for the grid's strings and `aria-label`s. |
71
71
 
72
72
  ### Default initial state
@@ -17,7 +17,7 @@ description: >
17
17
  metadata:
18
18
  type: core
19
19
  library: '@jielga/tmdatagrid'
20
- library_version: '2.0.0-beta.4'
20
+ library_version: '2.0.0-beta.7'
21
21
  sources:
22
22
  - 'Jielga/TMDataGrid:src/docs/row-selection.md'
23
23
  - 'Jielga/TMDataGrid:src/docs/row-interaction.md'
@@ -194,7 +194,7 @@ Returning an empty list leaves the column with no menu button at all.
194
194
  striped
195
195
  rowStyle={(row) =>
196
196
  row.original.status === "Terminated"
197
- ? { "--row-bg": "var(--mantine-color-red-0)" }
197
+ ? { "--row-bg": "color-mix(in srgb, var(--mantine-color-red-6) 12%, transparent)" }
198
198
  : undefined
199
199
  }
200
200
  rowClassName={(row) => (row.original.overdue ? classes.overdue : undefined)}
@@ -210,7 +210,11 @@ are not striped.
210
210
 
211
211
  Rows carry `data-selected`, `data-selected-bg`, `data-highlighted`,
212
212
  `data-grouped`, `data-depth`, `data-context-menu` and `data-row-id`, so a
213
- stylesheet can target any of it without a callback.
213
+ stylesheet can target any of it without a callback. The boolean attributes are
214
+ published on every row as `"true"` or `"false"`, so match the value
215
+ (`[data-grouped="true"]`), not the bare attribute. Pick a `--row-bg` that
216
+ reads under both colour schemes - a `-0` Mantine shade is near-white and
217
+ unreadable in dark mode; mix a mid shade into transparency instead.
214
218
 
215
219
  ## The details panel
216
220
 
@@ -10,7 +10,7 @@ description: >
10
10
  metadata:
11
11
  type: core
12
12
  library: '@jielga/tmdatagrid'
13
- library_version: '2.0.0-beta.4'
13
+ library_version: '2.0.0-beta.7'
14
14
  sources:
15
15
  - 'Jielga/TMDataGrid:src/docs/server-side.md'
16
16
  - 'Jielga/TMDataGrid:src/tmdatagrid/useTMDataGrid.tsx'
@@ -11,7 +11,7 @@ description: >
11
11
  metadata:
12
12
  type: core
13
13
  library: '@jielga/tmdatagrid'
14
- library_version: '2.0.0-beta.4'
14
+ library_version: '2.0.0-beta.7'
15
15
  sources:
16
16
  - 'Jielga/TMDataGrid:src/docs/testing.md'
17
17
  - 'Jielga/TMDataGrid:src/tmdatagrid/components/TMDataGrid.tsx'
@@ -3,7 +3,7 @@ import { type CSSProperties, type ReactNode, useMemo } from "react";
3
3
  import classes from "./TMDataGrid.module.css";
4
4
  import { TMDataGridColumnsPanel } from "./TMDataGridColumnsPanel";
5
5
  import { TMDataGridContextProvider } from "../TMDataGridContext";
6
- import { TMDataGridEditActions } from "./TMDataGridEditActions";
6
+ import { TMDataGridDraftActions } from "./TMDataGridDraftActions";
7
7
  import { TMDataGridFilterPanel } from "./TMDataGridFilterPanel";
8
8
  import { TMDataGridFilterPills } from "./TMDataGridFilterPills";
9
9
  import { TMDataGridFooter } from "./TMDataGridFooter";
@@ -151,7 +151,7 @@ export const TMDataGrid = Object.assign(TMDataGridRoot, {
151
151
  SummaryCount: TMDataGridSummaryCount,
152
152
  LoadingIndicator: TMDataGridLoadingIndicator,
153
153
  Search: TMDataGridSearch,
154
- EditActions: TMDataGridEditActions,
154
+ DraftActions: TMDataGridDraftActions,
155
155
  ColumnsButton: TMDataGridColumnsButton,
156
156
  FilterButton: TMDataGridFilterButton,
157
157
  Table: TMDataGridTable,