@jielga/tmdatagrid 1.0.0 → 1.0.2

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 (78) hide show
  1. package/README.md +8 -8
  2. package/dist/index.d.ts +225 -225
  3. package/dist/index.js +58 -50
  4. package/dist/index.js.map +1 -1
  5. package/dist/styles.css +1 -1
  6. package/package.json +3 -2
  7. package/skills/appearance/SKILL.md +322 -0
  8. package/skills/cell-selection/SKILL.md +240 -0
  9. package/skills/columns/SKILL.md +261 -86
  10. package/skills/data/SKILL.md +289 -0
  11. package/skills/editing/SKILL.md +492 -0
  12. package/skills/editing/references/editing-api.md +124 -0
  13. package/skills/editing/references/editors-and-validation.md +198 -0
  14. package/skills/filtering/SKILL.md +344 -0
  15. package/skills/getting-started/SKILL.md +48 -27
  16. package/skills/grouping/SKILL.md +264 -0
  17. package/skills/options/SKILL.md +31 -20
  18. package/skills/rows/SKILL.md +369 -0
  19. package/skills/rows/references/rows-api.md +117 -0
  20. package/skills/server-side/SKILL.md +7 -7
  21. package/skills/testing/SKILL.md +12 -12
  22. package/src/tmdatagrid/TMDataGridContext.ts +2 -2
  23. package/src/tmdatagrid/components/TMDataGrid.module.css +2 -2
  24. package/src/tmdatagrid/components/TMDataGrid.tsx +5 -5
  25. package/src/tmdatagrid/components/TMDataGridCellEditor.tsx +4 -4
  26. package/src/tmdatagrid/components/TMDataGridColumnsPanel.tsx +2 -2
  27. package/src/tmdatagrid/components/TMDataGridDetailsColumn.tsx +6 -6
  28. package/src/tmdatagrid/components/TMDataGridEditActions.tsx +2 -2
  29. package/src/tmdatagrid/components/TMDataGridEditColumn.tsx +4 -4
  30. package/src/tmdatagrid/components/TMDataGridEntryRows.tsx +6 -6
  31. package/src/tmdatagrid/components/TMDataGridFilterPanel.tsx +6 -6
  32. package/src/tmdatagrid/components/TMDataGridFilterPills.module.css +2 -2
  33. package/src/tmdatagrid/components/TMDataGridFilterPills.tsx +1 -1
  34. package/src/tmdatagrid/components/TMDataGridFooter.module.css +1 -1
  35. package/src/tmdatagrid/components/TMDataGridFooter.tsx +12 -6
  36. package/src/tmdatagrid/components/TMDataGridGroupColumn.module.css +1 -1
  37. package/src/tmdatagrid/components/TMDataGridGroupColumn.tsx +5 -5
  38. package/src/tmdatagrid/components/TMDataGridHeaderCell.module.css +8 -8
  39. package/src/tmdatagrid/components/TMDataGridHeaderCell.tsx +12 -12
  40. package/src/tmdatagrid/components/TMDataGridRowNumberColumn.tsx +4 -4
  41. package/src/tmdatagrid/components/TMDataGridSearch.tsx +5 -5
  42. package/src/tmdatagrid/components/TMDataGridSelectColumn.tsx +8 -8
  43. package/src/tmdatagrid/components/TMDataGridTable.module.css +18 -18
  44. package/src/tmdatagrid/components/TMDataGridTable.tsx +116 -116
  45. package/src/tmdatagrid/components/TMDataGridToolbar.tsx +5 -5
  46. package/src/tmdatagrid/components/editors/TMDataGridBooleanEditor.tsx +1 -1
  47. package/src/tmdatagrid/components/editors/TMDataGridDateEditor.tsx +2 -2
  48. package/src/tmdatagrid/components/editors/TMDataGridMultiSelectEditor.tsx +1 -1
  49. package/src/tmdatagrid/components/editors/TMDataGridNumberEditor.tsx +1 -1
  50. package/src/tmdatagrid/components/editors/TMDataGridSelectEditor.tsx +2 -2
  51. package/src/tmdatagrid/components/editors/editorShared.ts +3 -3
  52. package/src/tmdatagrid/components/filters/DgDateRangeFilter.tsx +1 -1
  53. package/src/tmdatagrid/components/filters/DgRangeSliderFilter.tsx +1 -1
  54. package/src/tmdatagrid/components/filters/DgTriStateFilter.tsx +1 -1
  55. package/src/tmdatagrid/components/filters/TMDataGridFilterValueInput.tsx +2 -2
  56. package/src/tmdatagrid/components/sticky.module.css +8 -8
  57. package/src/tmdatagrid/core/autosize.ts +4 -4
  58. package/src/tmdatagrid/core/capabilities.ts +17 -17
  59. package/src/tmdatagrid/core/cellExport.ts +13 -13
  60. package/src/tmdatagrid/core/cellNavigation.ts +6 -6
  61. package/src/tmdatagrid/core/cellRange.ts +4 -4
  62. package/src/tmdatagrid/core/columnOptions.ts +2 -2
  63. package/src/tmdatagrid/core/columnOrdering.ts +2 -2
  64. package/src/tmdatagrid/core/columnUtils.ts +3 -3
  65. package/src/tmdatagrid/core/editEngine.ts +49 -49
  66. package/src/tmdatagrid/core/expanding.ts +5 -5
  67. package/src/tmdatagrid/core/filterControls.ts +5 -5
  68. package/src/tmdatagrid/core/filterOperators.ts +14 -14
  69. package/src/tmdatagrid/core/labels.ts +8 -8
  70. package/src/tmdatagrid/core/matchHighlight.ts +4 -4
  71. package/src/tmdatagrid/core/persistence.ts +8 -8
  72. package/src/tmdatagrid/core/quickSearch.ts +8 -8
  73. package/src/tmdatagrid/core/rowPinning.ts +3 -3
  74. package/src/tmdatagrid/core/rowSelection.ts +12 -12
  75. package/src/tmdatagrid/core/sizes.ts +1 -1
  76. package/src/tmdatagrid/core/summary.ts +3 -3
  77. package/src/tmdatagrid/useTMDataGrid.tsx +79 -79
  78. package/skills/features/SKILL.md +0 -352
@@ -0,0 +1,492 @@
1
+ ---
2
+ name: editing
3
+ description: >
4
+ Edit cells and rows in TMDataGrid. Covers editMode with its four policies
5
+ (cell, cellConfirm, row, batch), the required getRowId, onEditCommit and
6
+ onEditCommitBatch, why the grid never mutates data, per-column gating with
7
+ meta.editable and meta.editField, the six built-in editors picked by
8
+ meta.type, custom editors through meta.editor, field validation with
9
+ meta.validate and cross-field rules with rowValidators (Standard Schema and
10
+ Zod), adding and deleting rows with edit.addRow, newRowDefaults, onRowAdd,
11
+ onRowDelete, the generated edit lane, TMDataGrid.EditActions, and the public
12
+ edit engine (begin, commit, cancel, submitAll, getForm, store). Load when
13
+ making a grid editable, choosing an edit mode, wiring a save, writing a cell
14
+ editor, validating an edit, or when cells will not open for editing.
15
+ metadata:
16
+ type: core
17
+ library: '@jielga/tmdatagrid'
18
+ library_version: '1.0.2'
19
+ sources:
20
+ - 'Jielga/TMDataGrid:src/docs/editing.md'
21
+ - 'Jielga/TMDataGrid:src/docs/editors.md'
22
+ - 'Jielga/TMDataGrid:src/tmdatagrid/core/editEngine.ts'
23
+ - 'Jielga/TMDataGrid:src/tmdatagrid/useTMDataGrid.tsx'
24
+ ---
25
+
26
+ # TMDataGrid - Editing
27
+
28
+ `editMode` turns editing on and picks how commits happen. Three facts decide
29
+ every wiring question below:
30
+
31
+ - **The grid never mutates `data`.** `onEditCommit` applies the change wherever
32
+ the data actually lives, and the new rows arrive back through `data`.
33
+ - **One row, one form.** Each editing row gets its own TanStack Form, keyed by
34
+ row id and living outside the DOM, so a draft survives scrolling, sorting and
35
+ filtering.
36
+ - **`getRowId` is required** once `editMode` is set, and it must be the record's
37
+ own identity. Drafts are keyed by it.
38
+
39
+ `@tanstack/react-form` becomes a peer dependency once editing is used.
40
+
41
+ ## Setup
42
+
43
+ ```tsx
44
+ import { useState } from "react";
45
+ import {
46
+ createTMDataGridColumnHelper,
47
+ TMDataGrid,
48
+ useTMDataGrid,
49
+ } from "@jielga/tmdatagrid";
50
+
51
+ type Employee = { id: number; firstName: string; salary: number };
52
+
53
+ const columnHelper = createTMDataGridColumnHelper<Employee>();
54
+
55
+ const columns = columnHelper.columns([
56
+ columnHelper.accessor("firstName", { header: "First name", minSize: 140 }),
57
+ columnHelper.accessor("salary", {
58
+ header: "Salary",
59
+ minSize: 130,
60
+ meta: { type: "number", align: "right" },
61
+ }),
62
+ ]);
63
+
64
+ export function Employees({ initial }: { initial: Array<Employee> }) {
65
+ const [employees, setEmployees] = useState(initial);
66
+
67
+ const grid = useTMDataGrid({
68
+ data: employees,
69
+ columns,
70
+ getRowId: (row) => String(row.id),
71
+ editMode: "cell",
72
+ // The grid writes nothing: apply the change, and the edited row arrives
73
+ // back through `data`.
74
+ onEditCommit: ({ rowId, value }) =>
75
+ setEmployees((previous) =>
76
+ previous.map((employee) =>
77
+ String(employee.id) === rowId ? value : employee,
78
+ ),
79
+ ),
80
+ });
81
+
82
+ return (
83
+ <TMDataGrid {...grid} style={{ flex: 1, minHeight: 0 }}>
84
+ <TMDataGrid.Table<Employee> />
85
+ </TMDataGrid>
86
+ );
87
+ }
88
+ ```
89
+
90
+ The editing options travel together, and the types say so: passing any of them
91
+ without `editMode` is a compile error, `getRowId` stops being optional the
92
+ moment `editMode` is set, and `onEditCommitBatch` exists only under
93
+ `editMode: "batch"`.
94
+
95
+ ## The four modes
96
+
97
+ One axis, four policies over the same engine.
98
+
99
+ | Mode | Commits on | Cancels on | Chrome |
100
+ | --- | --- | --- | --- |
101
+ | `"cell"` | Enter, Tab, blur | Escape | none |
102
+ | `"cellConfirm"` | ✓ or Enter; blur keeps the draft | ✕ or Escape | ✓ / ✕ beside the input |
103
+ | `"row"` | Save in the edit lane, or Ctrl+Enter | Cancel, or Escape | generated edit lane |
104
+ | `"batch"` | `edit.submitAll()` | `edit.cancelAll()` | `TMDataGrid.EditActions` |
105
+
106
+ Which to pick:
107
+
108
+ - One field at a time, saved as you go, spreadsheet feel: `"cell"`.
109
+ - The same, but every commit costs a request you do not want fired by a stray
110
+ click: `"cellConfirm"`.
111
+ - The row is the unit of the save, or a rule spans two columns: `"row"`.
112
+ - Many edits reviewed and sent as one transaction: `"batch"`.
113
+
114
+ An editor opens on double-click, or with the cell cursor on the cell: Enter, F2,
115
+ or just typing, where the character replaces the value. Delete or Backspace
116
+ clears the value and commits without opening an editor. Editing brings cell
117
+ selection with it: `cellSelection` defaults to `"single"` while `editMode` is
118
+ set.
119
+
120
+ Under `"batch"` nothing leaves the grid until `submitAll`. Enter and Tab park
121
+ the draft, dirty-marked, Escape drops that one draft, and drafts accumulate
122
+ across rows. `submitAll` then commits every open row: through the per-row
123
+ `onEditCommit` loop by default, or through one
124
+ `onEditCommitBatch({ rows, added, deleted })` call when that is set. Rows
125
+ failing validation stay open either way, and a rejected batch keeps every
126
+ draft.
127
+
128
+ ## What a commit receives
129
+
130
+ `onEditCommit` is handed `{ rowId, value, original, changes, source }`: `value`
131
+ is the whole edited row for consumers who save records, `changes` the per-field
132
+ diff (`columnId`, `field`, `previous`, `next`) for consumers who PATCH, one
133
+ entry in cell mode.
134
+
135
+ The engine drops the draft only when `onEditCommit` resolves. A slow save keeps
136
+ the draft on screen with a busy marker; a **rejection keeps the form open** with
137
+ the error on the row, which is how a failed save stays visible.
138
+
139
+ ## Which cells edit
140
+
141
+ A column is editable when it maps to a data path: its `accessorKey`, or
142
+ `meta.editField` for a column built on `accessorFn`. Dot paths reach into nested
143
+ records, so `accessorKey: "address.city"` edits `values.address.city`.
144
+
145
+ | Gate | Effect |
146
+ | --- | --- |
147
+ | `meta.editable: false` | The column never edits |
148
+ | `meta.editable: (row) => boolean` | Per row, per column |
149
+ | `meta.editField: "lastName"` | The path an `accessorFn` column writes to |
150
+ | `isRowEditable: (row) => boolean` | The whole row, in every mode |
151
+
152
+ Group rows and the generated lanes (checkbox, row number, details, edit) never
153
+ edit.
154
+
155
+ ```tsx
156
+ // Computed column: writes back to a real field.
157
+ columnHelper.accessor((row) => `${row.firstName} ${row.lastName}`, {
158
+ id: "fullName",
159
+ header: "Full name",
160
+ meta: { label: "Full name", editField: "lastName" },
161
+ });
162
+
163
+ // Editable only while the employee is active.
164
+ columnHelper.accessor("salary", {
165
+ header: "Salary",
166
+ meta: {
167
+ type: "number",
168
+ align: "right",
169
+ editable: (row) => row.original.status !== "Terminated",
170
+ },
171
+ });
172
+ ```
173
+
174
+ ## Editors and validation
175
+
176
+ `meta.type` picks the editor, and `meta.options` feeds the select editors from
177
+ the same declaration the filter panel reads.
178
+
179
+ | `meta.type` | Editor |
180
+ | --- | --- |
181
+ | `"string"` (default) | Text input |
182
+ | `"number"` | Number input |
183
+ | `"boolean"` | Checkbox |
184
+ | `"date"` | Native `<input type="date">`, ISO `YYYY-MM-DD` |
185
+ | `"select"` | Searchable select from `meta.options`; commits on pick under `"cell"` |
186
+ | `"multiSelect"` | Multi-select, same source |
187
+
188
+ `meta.editor` replaces one, `meta.validate` guards the field, and
189
+ `rowValidators` carries cross-field rules. The validators are TanStack Form's
190
+ own, Standard Schema included, so a Zod schema passes straight through:
191
+
192
+ ```tsx
193
+ import { z } from "zod";
194
+
195
+ // Field level, on the column. A bare schema means { onChange: schema }.
196
+ meta: { validate: z.string().min(2, "At least two characters") }
197
+
198
+ // Form level, on the hook. Cross-field rules, under "row" or "batch".
199
+ rowValidators: {
200
+ onSubmit: z
201
+ .object({ salary: z.number().positive(), status: z.string() })
202
+ .refine((row) => row.status !== "Terminated" || row.salary === 0, {
203
+ message: "A terminated employee has no salary",
204
+ }),
205
+ }
206
+ ```
207
+
208
+ Pathed issues land on the matching cells, pathless ones on the row. Cell corners
209
+ mark state meanwhile: blue for a dirty draft, red for a validation error.
210
+
211
+ The editor contract, wrapping a built-in, and async server-side field errors:
212
+ [references/editors-and-validation.md](references/editors-and-validation.md).
213
+
214
+ ## Adding and deleting rows
215
+
216
+ `edit.addRow()` opens an entry row in a sticky block under the header, seeded
217
+ from `newRowDefaults`. Enter, or the lane's ✓, commits the add through
218
+ `onRowAdd` under the immediate modes; batch parks it for `submitAll`. Escape, or
219
+ ✕, discards it.
220
+
221
+ ```tsx
222
+ const grid = useTMDataGrid({
223
+ data: employees,
224
+ columns,
225
+ getRowId: (row) => String(row.id),
226
+ editMode: "batch",
227
+ newRowDefaults: () => ({ id: 0, firstName: "", salary: 30_000 }),
228
+ onEditCommitBatch: async ({ rows, added, deleted }) => {
229
+ await api.saveBatch({ rows, added, deleted });
230
+ },
231
+ });
232
+
233
+ <TMDataGrid.Toolbar>
234
+ <Button onClick={() => grid.edit.addRow()}>Add row</Button>
235
+ <TMDataGrid.Spacer />
236
+ <TMDataGrid.EditActions />
237
+ </TMDataGrid.Toolbar>;
238
+ ```
239
+
240
+ `edit.deleteRow(rowId)` calls `onRowDelete({ rowId, row })` straight away under
241
+ the immediate modes, so a confirmation belongs inside that callback. Under batch
242
+ it toggles a mark instead: the row renders struck through and inert
243
+ (`data-deleted`), the lane's trash becomes a restore, and `submitAll` reports
244
+ the ids in `deleted`.
245
+
246
+ Adds and deletes are applied by you, the same as edits. The engine's `tempId`
247
+ (`__new__1`, `__new__2`, …) never needs to become a real id: mint one when you
248
+ create the record.
249
+
250
+ ## The chrome
251
+
252
+ The generated edit lane (`EDIT_COLUMN_ID`, pinned right) appears when `editMode`
253
+ is `"row"`, or `onRowDelete` is set, or `editMode` is `"batch"` with
254
+ `onEditCommitBatch` set. Nothing else conjures it, and `"cell"` mode has no
255
+ chrome at all by design.
256
+
257
+ `TMDataGrid.EditActions` is Save with the pending count plus Discard. It greys
258
+ out while nothing is pending, spins while a submit is in flight, renders nothing
259
+ while editing is off, and works under any mode, not only batch.
260
+
261
+ ## The engine: `edit`
262
+
263
+ `grid.edit` is public, and everything the chrome does goes through it.
264
+
265
+ | Member | Does |
266
+ | --- | --- |
267
+ | `edit.begin({ rowId, columnId })` | Opens an editor. `columnId: null` opens the whole row |
268
+ | `edit.commit(rowId)` | Commits one row. `Promise<boolean>`, `false` when blocked |
269
+ | `edit.cancel(rowId)` / `edit.cancelAll()` | Drops one draft, or every draft |
270
+ | `edit.submitAll()` | Commits every open row. `Promise<boolean>` |
271
+ | `edit.addRow()` / `edit.deleteRow(rowId)` | Rows in and out |
272
+ | `edit.getForm(rowId)` | The row's live TanStack Form `FormApi` |
273
+ | `edit.store` | The projection store, for `useSelector` |
274
+
275
+ ```tsx
276
+ import { useSelector } from "@tanstack/react-store";
277
+
278
+ const pendingCount = useSelector(
279
+ grid.edit.store,
280
+ (state) => state.openRowIds.length + state.deletedRowIds.length,
281
+ );
282
+ ```
283
+
284
+ `edit.store` holds `active`, `openRowIds`, `rows` (per row `dirtyFields`,
285
+ `errorFields`, `isSubmitting`), `newRows` and `deletedRowIds`. The full member
286
+ list, `clearCell`, `deactivate` and the gates included, is in
287
+ [references/editing-api.md](references/editing-api.md).
288
+
289
+ `getForm` is the *one row, one form* promise made public: render that same form
290
+ in a drawer and it shares values, dirty state and errors with the inline cells,
291
+ because it is the same `FormApi`.
292
+
293
+ ## Common mistakes
294
+
295
+ ### CRITICAL Expecting the grid to write into `data`
296
+
297
+ The grid holds no copy of the rows. Without `onEditCommit` an edit commits, the
298
+ draft clears, and the cell snaps back to the value in `data`, which looks like
299
+ the edit was silently discarded.
300
+
301
+ Wrong:
302
+
303
+ ```tsx
304
+ useTMDataGrid({ data: employees, columns, getRowId, editMode: "cell" });
305
+ ```
306
+
307
+ Correct:
308
+
309
+ ```tsx
310
+ useTMDataGrid({
311
+ data: employees,
312
+ columns,
313
+ getRowId,
314
+ editMode: "cell",
315
+ onEditCommit: ({ rowId, value }) =>
316
+ setEmployees((previous) =>
317
+ previous.map((employee) =>
318
+ String(employee.id) === rowId ? value : employee,
319
+ ),
320
+ ),
321
+ });
322
+ ```
323
+
324
+ Source: `src/docs/editing.md`, `src/tmdatagrid/core/editEngine.ts`.
325
+
326
+ ### CRITICAL `getRowId` built from the row index
327
+
328
+ `getRowId` satisfies the compiler with any string, so an index-based id passes.
329
+ Drafts are keyed by it, so after a sort or a filter the draft is applied to
330
+ whichever record now sits at that index.
331
+
332
+ Wrong:
333
+
334
+ ```tsx
335
+ getRowId: (row, index) => String(index),
336
+ ```
337
+
338
+ Correct:
339
+
340
+ ```tsx
341
+ getRowId: (row) => String(row.id),
342
+ ```
343
+
344
+ Source: `src/tmdatagrid/useTMDataGrid.tsx` (`TMDataGridEditingCallbacks`).
345
+
346
+ ### HIGH A cell editor defined inside the component
347
+
348
+ `meta.editor` is rendered as JSX, so its identity is its component type. An
349
+ inline arrow is a new type on every render, which unmounts the open editor and
350
+ loses what was being typed.
351
+
352
+ Wrong:
353
+
354
+ ```tsx
355
+ meta: { editor: ({ field }) => <Slider value={field.state.value} /> },
356
+ ```
357
+
358
+ Correct:
359
+
360
+ ```tsx
361
+ // Module scope, referenced by name.
362
+ const SalaryEditor: TMDataGridEditorComponent = ({ field, commit }) => (
363
+ <Slider
364
+ value={Number(field.state.value ?? 0)}
365
+ onChange={(next) => field.handleChange(next)}
366
+ onChangeEnd={() => void commit()}
367
+ />
368
+ );
369
+
370
+ meta: { editor: SalaryEditor },
371
+ ```
372
+
373
+ Source: `src/docs/editors.md`, `src/tmdatagrid/core/editEngine.ts`.
374
+
375
+ ### HIGH A cross-field rule under `editMode: "cell"`
376
+
377
+ Under `"cell"` each cell commits on its own, so a rule spanning two columns can
378
+ never be satisfied by either one: the first cell edited is rejected against the
379
+ other column's old value, and the row cannot be saved at all.
380
+
381
+ Wrong:
382
+
383
+ ```tsx
384
+ useTMDataGrid({ editMode: "cell", rowValidators: { onSubmit: endAfterStart } });
385
+ ```
386
+
387
+ Correct:
388
+
389
+ ```tsx
390
+ // "row" (or "batch") validates the whole row in one commit.
391
+ useTMDataGrid({ editMode: "row", rowValidators: { onSubmit: endAfterStart } });
392
+ ```
393
+
394
+ Source: `src/docs/editors.md` (cross-field rules want a mode that commits the
395
+ whole row at once).
396
+
397
+ ### HIGH An `accessorFn` column that never opens an editor
398
+
399
+ Editability follows the data path, not the column. A column built on an accessor
400
+ function has no `accessorKey`, so it maps to nothing and stays read-only while
401
+ every other column edits. No warning is raised.
402
+
403
+ Wrong:
404
+
405
+ ```tsx
406
+ columnHelper.accessor((row) => `${row.firstName} ${row.lastName}`, {
407
+ id: "fullName",
408
+ header: "Full name",
409
+ });
410
+ ```
411
+
412
+ Correct:
413
+
414
+ ```tsx
415
+ columnHelper.accessor((row) => `${row.firstName} ${row.lastName}`, {
416
+ id: "fullName",
417
+ header: "Full name",
418
+ meta: { label: "Full name", editField: "lastName" },
419
+ });
420
+ ```
421
+
422
+ Source: `src/docs/editing.md` (Which cells edit).
423
+
424
+ ### HIGH Swallowing the error in `onEditCommit`
425
+
426
+ The draft is dropped when `onEditCommit` resolves. A `try/catch` that logs the
427
+ failure resolves it, so a save that failed on the server clears the editor and
428
+ the grid shows the old value as though nothing happened.
429
+
430
+ Wrong:
431
+
432
+ ```tsx
433
+ onEditCommit: async ({ rowId, changes }) => {
434
+ try {
435
+ await api.patch(rowId, changes);
436
+ } catch (error) {
437
+ console.error(error);
438
+ }
439
+ },
440
+ ```
441
+
442
+ Correct:
443
+
444
+ ```tsx
445
+ // Reject: the form stays open with the error on the row.
446
+ onEditCommit: async ({ rowId, changes }) => {
447
+ await api.patch(rowId, changes);
448
+ },
449
+ ```
450
+
451
+ Source: `src/tmdatagrid/useTMDataGrid.tsx` (`onEditCommit`).
452
+
453
+ ### MEDIUM Reading a commit's result as the saved value
454
+
455
+ `edit.commit(rowId)` and `edit.submitAll()` resolve to a `boolean` saying
456
+ whether the form closed, and resolve `false` when validation or a rejected save
457
+ kept it open. Ignoring the result reports a save that did not happen.
458
+
459
+ Wrong:
460
+
461
+ ```tsx
462
+ await grid.edit.submitAll();
463
+ notifications.show({ message: "Saved" });
464
+ ```
465
+
466
+ Correct:
467
+
468
+ ```tsx
469
+ const saved = await grid.edit.submitAll();
470
+ notifications.show({ message: saved ? "Saved" : "Some rows need attention" });
471
+ ```
472
+
473
+ Source: `src/tmdatagrid/core/editEngine.ts` (`TMDataGridEditApi`).
474
+
475
+ ### MEDIUM Expecting `onRowDelete` to fire under batch
476
+
477
+ Under the immediate modes `edit.deleteRow` calls `onRowDelete` at once. Under
478
+ `"batch"` it only toggles a deletion mark, and the ids arrive later in
479
+ `submitAll`'s `deleted`, so a batch grid that deletes through `onRowDelete`
480
+ alone never removes anything. Handle `deleted` in `onEditCommitBatch`.
481
+
482
+ Source: `src/docs/editing.md` (Adding and deleting rows).
483
+
484
+ ## References
485
+
486
+ - [Editors and validation](references/editors-and-validation.md) - the editor
487
+ contract, wrapping a built-in, field and row validators, server-side errors.
488
+ - [Editing API](references/editing-api.md) - every option, callback, column meta
489
+ field, export, CSS variable and data attribute editing owns.
490
+
491
+ See also: the `columns` skill for `meta.type` and the shared `meta.options`
492
+ source, and the `testing` skill for driving editors from a test.
@@ -0,0 +1,124 @@
1
+ # Editing API
2
+
3
+ Reference for the `editing` skill. Everything editing owns, in one table per
4
+ kind.
5
+
6
+ ## Hook options
7
+
8
+ | Name | Type | Default | What it does |
9
+ | --- | --- | --- | --- |
10
+ | `editMode` | `"cell" \| "cellConfirm" \| "row" \| "batch"` | off | Turns editing on and picks the commit policy. |
11
+ | `getRowId` | `(row) => string` | – | A TanStack table option, required once `editMode` is set. Drafts are keyed by it. |
12
+ | `isRowEditable` | `(row) => boolean` | – | Closes a whole row to editing, in every mode. |
13
+ | `rowValidators` | `TMDataGridRowValidators` | – | Form-level validation. Cross-field rules live here. |
14
+ | `newRowDefaults` | `TData \| (() => TData)` | – | Seeds the entry row's form. A function is called per added row. |
15
+ | `cellSelection` | `"none" \| "single" \| "range"` | `"single"` while editing | Editing turns the cell cursor on; set it explicitly to override. |
16
+
17
+ Passing any editing option without `editMode` is a compile error, and
18
+ `onEditCommitBatch` exists only in the `"batch"` branch of the type.
19
+
20
+ ## Callbacks
21
+
22
+ | Name | Argument | What it does |
23
+ | --- | --- | --- |
24
+ | `onEditCommit` | `{ rowId, value, original, changes, source }` | Applies one row's change. Reject to keep the draft and show the error. |
25
+ | `onEditCommitBatch` | `{ rows, added, deleted }` | Batch only. One call for the whole save. Without it, `submitAll` loops `onEditCommit`. |
26
+ | `onRowAdd` | `{ tempId, value }` | Commits an entry row. Mint the real id here. |
27
+ | `onRowDelete` | `{ rowId, row }` | Deletes a row under the immediate modes, and puts the trash can in the edit lane. |
28
+
29
+ `changes` entries are `{ columnId, field, previous, next }`; `field` is the data
30
+ path, which may be dotted.
31
+
32
+ ## Column meta
33
+
34
+ | Name | Type | Default | What it does |
35
+ | --- | --- | --- | --- |
36
+ | `meta.type` | `TMDataGridColumnType` | `"string"` | Picks the built-in editor, and the filter operators. |
37
+ | `meta.options` | `TMDataGridOptionsSource` | – | Choices for `select` / `multiSelect`, shared with the filter panel. Array, `"faceted"`, or a function. |
38
+ | `meta.editable` | `boolean \| ((row) => boolean)` | editable where a field maps | Whether this column's cells edit. |
39
+ | `meta.editField` | `string` | The `accessorKey` | The data path an edit writes to. The only way an `accessorFn` column edits. |
40
+ | `meta.editor` | `TMDataGridEditorComponent` | By `meta.type` | Replaces the cell editor. Define at module scope. |
41
+ | `meta.validate` | `TMDataGridFieldValidate` | – | Field-level validation. A bare schema means `onChange`. |
42
+
43
+ ## The `edit` engine
44
+
45
+ `grid.edit`, type `TMDataGridEditApi`.
46
+
47
+ | Member | Signature | Notes |
48
+ | --- | --- | --- |
49
+ | `begin` | `({ rowId, columnId }) => void` | `columnId: null` opens the whole row (row mode). |
50
+ | `commit` | `(rowId) => Promise<boolean>` | `false` keeps the form open with its errors. |
51
+ | `cancel` | `(rowId) => void` | Drops one draft. |
52
+ | `cancelAll` | `() => void` | Drops every draft. |
53
+ | `deactivate` | `() => void` | Closes the editor without touching the draft, as blur does under `"cellConfirm"`. |
54
+ | `submitAll` | `() => Promise<boolean>` | Batch's save. `true` when every row landed. |
55
+ | `clearCell` | `(rowId, columnId) => Promise<boolean>` | What Delete does: writes the type's empty value and commits. |
56
+ | `addRow` | `() => string` | Opens an entry row, returns its `tempId`. |
57
+ | `deleteRow` | `(rowId) => void` | `onRowDelete` under the immediate modes, a deletion mark under batch. |
58
+ | `canEditCell` | `(row, column) => boolean` | The gate the chrome uses. |
59
+ | `canEditRow` | `(row) => boolean` | The pencil's gate. |
60
+ | `canDeleteRows` | `() => boolean` | Whether delete chrome makes sense. |
61
+ | `getForm` | `(rowId) => TMDataGridRowEditForm \| undefined` | The row's live `FormApi`. |
62
+ | `state` | `TMDataGridEditState` | Snapshot, for reads outside React. |
63
+ | `store` | `Store<TMDataGridEditState>` | For `useSelector`. |
64
+
65
+ `TMDataGridEditState`:
66
+
67
+ ```ts
68
+ type TMDataGridEditState = {
69
+ active: { rowId: string; columnId: string | null } | null;
70
+ openRowIds: ReadonlyArray<string>;
71
+ rows: Record<
72
+ string,
73
+ {
74
+ dirtyFields: ReadonlyArray<string>;
75
+ errorFields: ReadonlyArray<string>;
76
+ isSubmitting: boolean;
77
+ }
78
+ >;
79
+ newRows: ReadonlyArray<{ tempId: string }>;
80
+ deletedRowIds: ReadonlyArray<string>;
81
+ };
82
+ ```
83
+
84
+ ## Components and exports
85
+
86
+ | Name | Kind | What it is |
87
+ | --- | --- | --- |
88
+ | `TMDataGrid.EditActions` | Component | Save with the pending count, and Discard. Any mode; renders nothing while editing is off. |
89
+ | `TMDataGridEditActions` | Export | The same component, for use outside the namespace. |
90
+ | `EDIT_COLUMN_ID` | Export | `"__edit__"`, the generated edit lane's id. |
91
+ | `clearedValueForType` | Export | `(type) => unknown` - what Delete writes per column type. |
92
+ | `getEditFieldName` | Export | `(column) => string` - the data path a column's edits write to. |
93
+ | `normalizeFieldValidate` | Export | `(validate) => validators` - a bare schema into Form's shape. |
94
+ | `TMDataGridStringEditor` … `TMDataGridMultiSelectEditor` | Exports | The six built-in editors, for wrapping. |
95
+
96
+ Types: `TMDataGridEditMode`, `TMDataGridEditApi`, `TMDataGridEditState`,
97
+ `TMDataGridEditCommitArgs`, `TMDataGridEditCommitBatchArgs`,
98
+ `TMDataGridEditChange`, `TMDataGridEditorArgs`, `TMDataGridEditorComponent`,
99
+ `TMDataGridEditField`, `TMDataGridEditRowProjection`, `TMDataGridFieldValidate`,
100
+ `TMDataGridRowValidators`, `TMDataGridRowEditForm`, `TMDataGridRowAddArgs`,
101
+ `TMDataGridRowDeleteArgs`, `TMDataGridEditingOptions`.
102
+
103
+ ## The edit lane
104
+
105
+ Generated, pinned right, id `EDIT_COLUMN_ID`. It appears when any of these
106
+ holds:
107
+
108
+ - `editMode: "row"` - the lane is Save and Cancel's home
109
+ - `onRowDelete` is set - the trash can has somewhere to report to
110
+ - `editMode: "batch"` **and** `onEditCommitBatch` is set
111
+
112
+ `"cell"` and `"cellConfirm"` have no lane unless `onRowDelete` asks for one.
113
+
114
+ ## Styling and test hooks
115
+
116
+ | Name | Kind | What it is |
117
+ | --- | --- | --- |
118
+ | `--dg-entry-height` | CSS variable | Height of the sticky entry block. From `size`. |
119
+ | `data-deleted` | Row attribute | On a row marked for deletion under batch. |
120
+ | `data-dg-part="editor-input"` | Part | The control inside an editing cell. |
121
+ | `data-dg-part="save-row"` / `"cancel-row"` | Parts | The edit lane's buttons, with `data-row-id`. |
122
+ | `data-dg-part="save-all"` / `"discard-all"` | Parts | `EditActions`. |
123
+
124
+ See the `testing` skill for how to compose these into selectors.