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