@jielga/tmdatagrid 2.0.0-beta.0 → 2.0.0-beta.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 (52) hide show
  1. package/README.md +32 -34
  2. package/dist/index.d.ts +164 -102
  3. package/dist/index.js +2060 -1705
  4. package/dist/index.js.map +1 -1
  5. package/dist/styles.css +1 -1
  6. package/package.json +1 -1
  7. package/skills/appearance/SKILL.md +37 -34
  8. package/skills/cell-selection/SKILL.md +19 -19
  9. package/skills/columns/SKILL.md +9 -9
  10. package/skills/data/SKILL.md +53 -53
  11. package/skills/editing/SKILL.md +152 -311
  12. package/skills/editing/references/common-mistakes.md +177 -0
  13. package/skills/editing/references/editing-api.md +56 -28
  14. package/skills/editing/references/editors-and-validation.md +26 -19
  15. package/skills/filtering/SKILL.md +51 -50
  16. package/skills/getting-started/SKILL.md +10 -10
  17. package/skills/grouping/SKILL.md +41 -43
  18. package/skills/options/SKILL.md +4 -4
  19. package/skills/rows/SKILL.md +59 -59
  20. package/skills/rows/references/rows-api.md +7 -7
  21. package/skills/server-side/SKILL.md +1 -1
  22. package/skills/testing/SKILL.md +11 -10
  23. package/src/tmdatagrid/components/TMDataGrid.module.css +13 -1
  24. package/src/tmdatagrid/components/TMDataGridCellEditor.tsx +20 -8
  25. package/src/tmdatagrid/components/TMDataGridColumnsPanel.tsx +29 -18
  26. package/src/tmdatagrid/components/TMDataGridDetailsColumn.tsx +5 -8
  27. package/src/tmdatagrid/components/TMDataGridEditActions.tsx +2 -2
  28. package/src/tmdatagrid/components/TMDataGridEditColumn.tsx +246 -86
  29. package/src/tmdatagrid/components/TMDataGridEntryRows.tsx +176 -77
  30. package/src/tmdatagrid/components/TMDataGridHeaderCell.tsx +11 -6
  31. package/src/tmdatagrid/components/TMDataGridSelectColumn.tsx +9 -5
  32. package/src/tmdatagrid/components/TMDataGridTable.module.css +47 -3
  33. package/src/tmdatagrid/components/TMDataGridTable.tsx +101 -40
  34. package/src/tmdatagrid/components/editors/TMDataGridNumberEditor.tsx +1 -1
  35. package/src/tmdatagrid/components/icons.ts +1 -0
  36. package/src/tmdatagrid/core/autosize.ts +30 -6
  37. package/src/tmdatagrid/core/capabilities.ts +15 -5
  38. package/src/tmdatagrid/core/cellExport.ts +6 -7
  39. package/src/tmdatagrid/core/cellNavigation.ts +2 -2
  40. package/src/tmdatagrid/core/cellRange.ts +6 -6
  41. package/src/tmdatagrid/core/columnOrdering.ts +30 -1
  42. package/src/tmdatagrid/core/columnUtils.ts +14 -0
  43. package/src/tmdatagrid/core/draftCellContext.ts +68 -0
  44. package/src/tmdatagrid/core/editEngine.ts +111 -37
  45. package/src/tmdatagrid/core/filterOperators.ts +6 -6
  46. package/src/tmdatagrid/core/labels.ts +13 -1
  47. package/src/tmdatagrid/core/labelsSv.ts +4 -0
  48. package/src/tmdatagrid/core/matchHighlight.ts +3 -3
  49. package/src/tmdatagrid/core/persistence.ts +3 -3
  50. package/src/tmdatagrid/core/rowSelection.ts +3 -3
  51. package/src/tmdatagrid/index.ts +3 -1
  52. package/src/tmdatagrid/useTMDataGrid.tsx +141 -99
@@ -0,0 +1,177 @@
1
+ # Editing - common mistakes
2
+
3
+ The failure modes that compile, raise no warning, and lose a user's typing.
4
+ Severity is how much data or trust the mistake costs.
5
+
6
+ ## CRITICAL Expecting the grid to write into `data`
7
+
8
+ The grid holds no copy of the rows. Without `editing.onCommit` an edit commits,
9
+ the draft clears, and the cell reverts to the value in `data`.
10
+
11
+ Wrong:
12
+
13
+ ```tsx
14
+ useTMDataGrid({
15
+ data: employees,
16
+ columns,
17
+ getRowId,
18
+ editing: { mode: "cell" },
19
+ });
20
+ ```
21
+
22
+ Correct: wire `editing.onCommit` to apply the change where the data lives, as
23
+ in the skill's Setup section.
24
+
25
+ Source: `src/docs/editing.md`, `src/tmdatagrid/core/editEngine.ts`.
26
+
27
+ ## CRITICAL `getRowId` built from the row index
28
+
29
+ `getRowId` accepts any string, so an index-based id compiles. Drafts are keyed
30
+ by it, so after a sort or a filter the draft is applied to whichever record now
31
+ sits at that index.
32
+
33
+ Wrong:
34
+
35
+ ```tsx
36
+ getRowId: (row, index) => String(index),
37
+ ```
38
+
39
+ Correct:
40
+
41
+ ```tsx
42
+ getRowId: (row) => String(row.id),
43
+ ```
44
+
45
+ Source: `src/tmdatagrid/useTMDataGrid.tsx` (`TMDataGridEditingCallbacks`).
46
+
47
+ ## HIGH A cell editor defined inside the component
48
+
49
+ `meta.edit.editor` is rendered as JSX, so its identity is its component type. An
50
+ inline arrow function is a new type on every render, which unmounts the open
51
+ editor and discards what was being typed.
52
+
53
+ Wrong:
54
+
55
+ ```tsx
56
+ meta: { edit: { editor: ({ field }) => <Slider value={field.state.value} /> } },
57
+ ```
58
+
59
+ Correct:
60
+
61
+ ```tsx
62
+ // Module scope, referenced by name.
63
+ const SalaryEditor: TMDataGridEditorComponent = ({ field, commit }) => (
64
+ <Slider
65
+ value={Number(field.state.value ?? 0)}
66
+ onChange={(next) => field.handleChange(next)}
67
+ onChangeEnd={() => void commit()}
68
+ />
69
+ );
70
+
71
+ meta: { edit: { editor: SalaryEditor } },
72
+ ```
73
+
74
+ Source: `src/docs/editors.md`, `src/tmdatagrid/core/editEngine.ts`.
75
+
76
+ ## HIGH A cross-field rule under `editing.mode: "cell"`
77
+
78
+ Under `"cell"` each cell commits on its own, so a rule spanning two columns
79
+ cannot be satisfied by either one: the first cell edited is rejected against the
80
+ other column's old value, and the row cannot be saved.
81
+
82
+ Correct: `editing.rowValidators` needs `mode: "row"` or `"draft"`, which
83
+ validate the whole row in one commit.
84
+
85
+ Source: `src/docs/editors.md` (Validation).
86
+
87
+ ## HIGH An `accessorFn` column that never opens an editor
88
+
89
+ Editability follows the data path, not the column. A column built on an accessor
90
+ function has no `accessorKey`, so it maps to nothing and stays read-only while
91
+ every other column edits. No warning is raised.
92
+
93
+ Wrong:
94
+
95
+ ```tsx
96
+ columnHelper.accessor((row) => `${row.firstName} ${row.lastName}`, {
97
+ id: "fullName",
98
+ header: "Full name",
99
+ });
100
+ ```
101
+
102
+ Correct:
103
+
104
+ ```tsx
105
+ columnHelper.accessor((row) => `${row.firstName} ${row.lastName}`, {
106
+ id: "fullName",
107
+ header: "Full name",
108
+ meta: { label: "Full name", edit: { field: "lastName" } },
109
+ });
110
+ ```
111
+
112
+ Source: `src/docs/editing.md` (Which cells edit).
113
+
114
+ ## HIGH Swallowing the error in `editing.onCommit`
115
+
116
+ The draft is dropped when `editing.onCommit` resolves. A `try/catch` that logs
117
+ the failure resolves it, so a save that failed on the server clears the editor
118
+ and the grid shows the old value as though nothing happened.
119
+
120
+ Wrong:
121
+
122
+ ```tsx
123
+ onCommit: async ({ rowId, changes }) => {
124
+ try {
125
+ await api.patch(rowId, changes);
126
+ } catch (error) {
127
+ console.error(error);
128
+ }
129
+ },
130
+ ```
131
+
132
+ Correct: no `catch` - let the rejection propagate, and the form stays open
133
+ with the error on the row.
134
+
135
+ Source: `src/tmdatagrid/useTMDataGrid.tsx` (`TMDataGridEditingCallbacks`).
136
+
137
+ ## HIGH Submitting an outer form while the grid holds a draft
138
+
139
+ The outer form's array contains only committed rows. Under any mode a mid-edit
140
+ row is invisible to it, and under `"draft"` every edit is until `submitAll()`,
141
+ so a form submit saves stale rows and collection rules skip pending values.
142
+
143
+ Correct:
144
+
145
+ ```tsx
146
+ const hasOpenDraft = useSelector(grid.edit.store, (s) => s.openRowIds.length > 0);
147
+
148
+ <Button type="submit" disabled={!canSubmit || hasOpenDraft}>Save</Button>
149
+ // or flush instead of blocking:
150
+ const flushed = await grid.edit.submitAll();
151
+ if (flushed) await form.handleSubmit();
152
+ ```
153
+
154
+ Source: `src/docs/query-builder.md` (Which mode, Submitting).
155
+
156
+ ## MEDIUM Reading a commit's result as the saved value
157
+
158
+ `edit.commit(rowId)` and `edit.submitAll()` resolve to a `boolean` saying
159
+ whether the form closed, and resolve `false` when validation or a rejected save
160
+ kept it open. Ignoring the result reports a save that did not happen.
161
+
162
+ ```tsx
163
+ const saved = await grid.edit.submitAll();
164
+ notifications.show({ message: saved ? "Saved" : "Some rows need attention" });
165
+ ```
166
+
167
+ Source: `src/tmdatagrid/core/editEngine.ts` (`TMDataGridEditApi`).
168
+
169
+ ## MEDIUM Expecting `editing.onRowDelete` to fire under draft
170
+
171
+ Under the immediate modes `edit.deleteRow` calls `editing.onRowDelete` at once.
172
+ Under `"draft"` it only toggles a deletion mark, so nothing is removed until
173
+ `submitAll`: the ids arrive as `deleted` in `editing.onCommitDrafts`, or, with
174
+ no such callback, in the per-row `editing.onRowDelete` loop. A confirmation
175
+ placed inside `editing.onRowDelete` therefore guards the save, not the trash.
176
+
177
+ Source: `src/docs/editing.md` (Adding and deleting rows).
@@ -7,24 +7,27 @@ kind.
7
7
 
8
8
  | Name | Type | Default | What it does |
9
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.
10
+ | `editing` | `TMDataGridEditingOptions` | off | The editing namespace. Setting it turns editing on. |
11
+ | `editing.mode` | `"cell" \| "cellConfirm" \| "row" \| "draft"` | – | Picks the commit policy. |
12
+ | `getRowId` | `(row) => string` | – | A TanStack table option, required once `editing` is set. Drafts are keyed by it. |
13
+ | `editing.isRowEditable` | `(row) => boolean` | – | Closes a whole row to editing, in every mode. |
14
+ | `editing.rowValidators` | `TMDataGridRowValidators` | – | Form-level validation. Cross-field rules live here. |
15
+ | `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. |
17
+ | `cellSelection` | `"none" \| "single" \| "range"` | `"single"` while `editing` is set | Editing turns the cell cursor on; set it explicitly to override. |
18
+
19
+ Passing any other member of `editing` without `mode` is a compile error, and
20
+ `onCommitDrafts` and `newRowsSticky` exist only in the `"draft"` branch of the
21
+ type.
19
22
 
20
23
  ## Callbacks
21
24
 
22
25
  | Name | Argument | What it does |
23
26
  | --- | --- | --- |
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. |
27
+ | `editing.onCommit` | `{ rowId, value, original, changes, source }` | Applies one row's change. Reject to keep the draft and show the error. |
28
+ | `editing.onCommitDrafts` | `{ rows, added, deleted }` | Draft mode only. One call for the entire save. Without it, `submitAll` loops `editing.onCommit`, `editing.onRowAdd` and `editing.onRowDelete`. |
29
+ | `editing.onRowAdd` | `{ tempId, value }` | Commits an entry row. Mint the real id here. |
30
+ | `editing.onRowDelete` | `{ rowId, row }` | Deletes a row under the immediate modes, and puts the trash can in the edit lane. |
28
31
 
29
32
  `changes` entries are `{ columnId, field, previous, next }`; `field` is the data
30
33
  path, which may be dotted.
@@ -46,18 +49,18 @@ path, which may be dotted.
46
49
 
47
50
  | Member | Signature | Notes |
48
51
  | --- | --- | --- |
49
- | `begin` | `({ rowId, columnId }) => void` | Row mode opens the whole row either way; `columnId` says which cell takes the caret, `null` (the pencil) its first editable one. |
50
- | `commit` | `(rowId) => Promise<boolean>` | `false` keeps the form open with its errors. |
52
+ | `begin` | `({ rowId, columnId }) => void` | Row mode opens the entire row either way. `columnId` selects which cell takes the caret; `null` (the pencil) uses its first editable one. On an entered new row it reopens the row, flipping `confirmed` back to `false`. |
53
+ | `commit` | `(rowId) => Promise<boolean>` | `false` keeps the form open with its errors. Under `"draft"` it validates and holds the draft: no consumer callback runs until `submitAll`. |
51
54
  | `cancel` | `(rowId) => void` | Drops one draft. |
52
55
  | `cancelAll` | `() => void` | Drops every draft. |
53
56
  | `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. |
57
+ | `submitAll` | `() => Promise<boolean>` | Draft mode's Save all. `true` when every row landed. |
55
58
  | `clearCell` | `(rowId, columnId) => Promise<boolean>` | What Delete does: writes the type's empty value and commits. |
56
59
  | `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. |
60
+ | `deleteRow` | `(rowId) => void` | `editing.onRowDelete` under the immediate modes, a deletion mark under draft mode. Toggles: a second call restores the row. |
61
+ | `canEditCell` | `(row, column) => boolean` | The check the built-in controls use. |
59
62
  | `canEditRow` | `(row) => boolean` | The pencil's gate. |
60
- | `canDeleteRows` | `() => boolean` | Whether delete chrome makes sense. |
63
+ | `canDeleteRows` | `() => boolean` | Whether the delete control should be shown. |
61
64
  | `getForm` | `(rowId) => TMDataGridRowEditForm \| undefined` | The row's live `FormApi`. |
62
65
  | `state` | `TMDataGridEditState` | Snapshot, for reads outside React. |
63
66
  | `store` | `Store<TMDataGridEditState>` | For `useSelector`. |
@@ -69,17 +72,23 @@ type TMDataGridEditState = {
69
72
  // The cell the last open gesture named - where the caret goes.
70
73
  active: { rowId: string; columnId: string | null } | null;
71
74
  // Rows with a live form. One under `"cell"`; as many as the user opened
72
- // under `"row"`, `"cellConfirm"` and `"batch"` - which rows are editing.
75
+ // under `"row"`, `"cellConfirm"` and `"draft"` - which rows are editing.
73
76
  openRowIds: ReadonlyArray<string>;
77
+ // One `TMDataGridEditRowProjection` per open row.
74
78
  rows: Record<
75
79
  string,
76
80
  {
77
81
  dirtyFields: ReadonlyArray<string>;
78
82
  errorFields: ReadonlyArray<string>;
83
+ hasRowError: boolean;
79
84
  isSubmitting: boolean;
85
+ // The row as drafted, reference-stable while no value changes.
86
+ values: TMDataGridRowData;
80
87
  }
81
88
  >;
82
- newRows: ReadonlyArray<{ tempId: string }>;
89
+ // `confirmed` is draft mode's "entered, awaiting Save all". Under the
90
+ // immediate modes a confirm commits through `onRowAdd`, so it stays `false`.
91
+ newRows: ReadonlyArray<{ tempId: string; confirmed: boolean }>;
83
92
  deletedRowIds: ReadonlyArray<string>;
84
93
  };
85
94
  ```
@@ -97,7 +106,7 @@ type TMDataGridEditState = {
97
106
  | `TMDataGridStringEditor` … `TMDataGridMultiSelectEditor` | Exports | The six built-in editors, for wrapping. |
98
107
 
99
108
  Types: `TMDataGridEditMode`, `TMDataGridEditApi`, `TMDataGridEditState`,
100
- `TMDataGridEditCommitArgs`, `TMDataGridEditCommitBatchArgs`,
109
+ `TMDataGridEditCommitArgs`, `TMDataGridEditCommitDraftsArgs`,
101
110
  `TMDataGridEditChange`, `TMDataGridEditorArgs`, `TMDataGridEditorComponent`,
102
111
  `TMDataGridEditField`, `TMDataGridEditRowProjection`, `TMDataGridFieldValidate`,
103
112
  `TMDataGridRowValidators`, `TMDataGridRowEditForm`, `TMDataGridRowAddArgs`,
@@ -108,20 +117,39 @@ Types: `TMDataGridEditMode`, `TMDataGridEditApi`, `TMDataGridEditState`,
108
117
  Generated, pinned right, id `EDIT_COLUMN_ID`. It appears when any of these
109
118
  holds:
110
119
 
111
- - `editMode: "row"` - the lane is Save and Cancel's home
112
- - `onRowDelete` is set - the trash can has somewhere to report to
113
- - `editMode: "batch"` **and** `onEditCommitBatch` is set
120
+ - `editing.mode: "row"` - the lane is Save and Cancel's home
121
+ - `editing.mode: "draft"` - the lane is the change indicator and the per-row
122
+ revert, and `editing.onCommitDrafts` is not required for it
123
+ - `editing.onRowDelete` is set - the trash can has somewhere to report to
124
+
125
+ `"cell"` and `"cellConfirm"` have no lane unless `editing.onRowDelete` asks for
126
+ one.
114
127
 
115
- `"cell"` and `"cellConfirm"` have no lane unless `onRowDelete` asks for one.
128
+ What the lane holds depends on the mode. Under `"row"` an open row shows
129
+ `save-row` and `cancel-row`; those two never render under `"draft"`. Under
130
+ `"draft"` every changed row shows `row-state`, whose `data-state` is `new`,
131
+ `edited` or `deleted`, together with `revert-row` on an edited row,
132
+ `restore-row` on one marked for deletion, and `edit-row` plus `discard-new-row`
133
+ on an entered new row. A row holding a dirty draft hides `delete-row`. Every
134
+ control carries a tooltip from the labels, `revertRow`, `rowStateNew`,
135
+ `rowStateEdited` and `rowStateDeleted` among them.
116
136
 
117
137
  ## Styling and test hooks
118
138
 
119
139
  | Name | Kind | What it is |
120
140
  | --- | --- | --- |
121
141
  | `--dg-entry-height` | CSS variable | Height of the sticky entry block. From `size`. |
122
- | `data-deleted` | Row attribute | On a row marked for deletion under batch. |
142
+ | `--dg-row-new-bg` | CSS variable | Background of an entered new row. A green tint. |
143
+ | `data-deleted` | Row attribute | On a row marked for deletion under draft mode. |
144
+ | `data-dirty` | Row attribute | On a body row holding a dirty draft. Also on the cell whose field is dirty. |
145
+ | `data-new` / `data-confirmed` | Entry row attributes | On an entry row; `data-confirmed` once it is entered, awaiting Save all. |
146
+ | `data-dg-entry-flow-block` | Attribute | The in-flow block above the body rows holding entered new rows, unless `editing.newRowsSticky` keeps them in the sticky entry block. |
123
147
  | `data-dg-part="editor-input"` | Part | The control inside an editing cell. |
124
- | `data-dg-part="save-row"` / `"cancel-row"` | Parts | The edit lane's buttons, with `data-row-id`. |
148
+ | `data-dg-part="save-row"` / `"cancel-row"` | Parts | The edit lane's buttons on an open row, with `data-row-id`. Row mode only. |
149
+ | `data-dg-part="row-state"` | Part | Draft mode's change marker, with `data-row-id` and `data-state` of `new`, `edited` or `deleted`. |
150
+ | `data-dg-part="revert-row"` / `"restore-row"` | Parts | Drops a row's draft, and undoes a deletion mark, with `data-row-id`. |
151
+ | `data-dg-part="edit-row"` / `"delete-row"` | Parts | The idle lane's pencil and trash, with `data-row-id`. `edit-row` also reopens an entered new row. |
152
+ | `data-dg-part="confirm-new-row"` / `"discard-new-row"` | Parts | An entry row's ✓ and ✕, with `data-row-id`. |
125
153
  | `data-dg-part="save-all"` / `"discard-all"` | Parts | `EditActions`. |
126
154
 
127
155
  See the `testing` skill for how to compose these into selectors.
@@ -27,15 +27,15 @@ columnHelper.accessor("department", {
27
27
  `meta.options` takes a static array, `"faceted"` (the distinct values present in
28
28
  the data), or a function of the table, column and row.
29
29
 
30
- ## The editor contract
30
+ ## The editor API
31
31
 
32
- `meta.edit.editor` fills the same slot the built-ins fill. It is a **component**,
33
- rendered as JSX, so hooks are legal inside it.
32
+ `meta.edit.editor` fills the same slot as the built-ins. It is a **component**,
33
+ rendered as JSX, so hooks may be used inside it.
34
34
 
35
35
  ```ts
36
36
  type TMDataGridEditorArgs = {
37
37
  field: TMDataGridEditField; // the live TanStack Form field
38
- form: TMDataGridRowEditForm; // the whole row's form, for sibling fields
38
+ form: TMDataGridRowEditForm; // the row's form, for sibling fields
39
39
  cell: Cell;
40
40
  row: Row;
41
41
  column: Column;
@@ -85,7 +85,8 @@ columnHelper.accessor("salary", {
85
85
  ## Wrapping a built-in
86
86
 
87
87
  The built-ins take `TMDataGridEditorArgs` as their props, so a custom editor can
88
- pass the whole object through and add around it rather than start over.
88
+ pass the whole object through and render around it instead of starting from
89
+ scratch.
89
90
 
90
91
  ```tsx
91
92
  import { Group, Text } from "@mantine/core";
@@ -186,22 +187,24 @@ column factories: it turns a bare schema into Form's validator shape.
186
187
 
187
188
  ## Row validation
188
189
 
189
- `rowValidators` is form-level, and where cross-field rules live.
190
+ `editing.rowValidators` is form-level, and where cross-field rules live.
190
191
 
191
192
  ```tsx
192
193
  const grid = useTMDataGrid({
193
194
  data: employees,
194
195
  columns,
195
196
  getRowId: (row) => String(row.id),
196
- editMode: "row",
197
- rowValidators: {
198
- onSubmit: z
199
- .object({ salary: z.number().positive(), status: z.string() })
200
- .refine((row) => row.status !== "Terminated" || row.salary === 0, {
201
- message: "A terminated employee has no salary",
202
- }),
197
+ editing: {
198
+ mode: "row",
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
+ onCommit,
203
207
  },
204
- onEditCommit,
205
208
  });
206
209
  ```
207
210
 
@@ -211,12 +214,12 @@ issue lands on the column whose `editField` is `"address.city"`.
211
214
 
212
215
  Cross-field rules need a mode that commits the whole row at once. Under `"cell"`
213
216
  each cell commits alone, so the rule is evaluated against the other column's
214
- unedited value and can never pass. Use `"row"` or `"batch"`.
217
+ unedited value and cannot pass. Use `"row"` or `"draft"`.
215
218
 
216
219
  ## Server-side errors
217
220
 
218
- `rowValidators.onSubmitAsync` returns TanStack Form's `{ form, fields }` shape,
219
- so an API's field errors land on the right cells without translation.
221
+ `editing.rowValidators.onSubmitAsync` returns TanStack Form's `{ form, fields }`
222
+ shape, so an API's field errors land on the right cells without translation.
220
223
 
221
224
  ```tsx
222
225
  rowValidators: {
@@ -232,15 +235,19 @@ rowValidators: {
232
235
  ```
233
236
 
234
237
  A commit blocked by validation keeps the editor open with the message on the
235
- input. A rejected `onEditCommit` keeps the draft too, with the error on the row.
238
+ input. A rejected `editing.onCommit` keeps the draft too, with the error on the
239
+ row.
236
240
 
237
241
  ## Where the state shows
238
242
 
239
243
  | Marker | Means |
240
244
  | --- | --- |
245
+ | 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 |
241
246
  | Blue cell corner | The field is dirty against its original value |
242
247
  | Red cell corner | The field carries a validation error |
243
248
  | Row error text | A pathless rule failed, or a commit was rejected |
249
+ | `data-dirty` on the row | The row holds a dirty draft |
244
250
 
245
251
  The same information is readable from `edit.store`: `rows[rowId].dirtyFields`,
246
- `rows[rowId].errorFields`, `rows[rowId].isSubmitting`.
252
+ `rows[rowId].errorFields`, `rows[rowId].hasRowError`,
253
+ `rows[rowId].isSubmitting`, and `rows[rowId].values` for the draft itself.
@@ -15,7 +15,7 @@ description: >
15
15
  metadata:
16
16
  type: core
17
17
  library: '@jielga/tmdatagrid'
18
- library_version: '2.0.0-beta.0'
18
+ library_version: '2.0.0-beta.2'
19
19
  sources:
20
20
  - 'Jielga/TMDataGrid:src/docs/filtering.md'
21
21
  - 'Jielga/TMDataGrid:src/docs/quick-search.md'
@@ -46,10 +46,10 @@ The model is therefore plain JSON: dates as ISO `YYYY-MM-DD`, booleans as
46
46
  cell is tested against) and `between` (a `[min, max]` pair, an empty string
47
47
  leaving that end open).
48
48
 
49
- A filter with an empty value **stays in state** so the panel keeps showing its
50
- row while the reader types. It matches every row, does not light the header
51
- indicator and gets no pill. `isFilterActive(value)` is the test for that state -
52
- presence in `columnFilters` is not.
49
+ A filter with an empty value **stays in state** so the panel keeps its row while
50
+ the user types. It matches every row, does not set the header indicator and
51
+ produces no pill. `isFilterActive(value)` tests for that state; presence in
52
+ `columnFilters` does not.
53
53
 
54
54
  ## Operators
55
55
 
@@ -66,12 +66,12 @@ presence in `columnFilters` is not.
66
66
  Every type also offers `isEmpty` and `isNotEmpty`, which ignore the value input.
67
67
 
68
68
  String comparisons are case-insensitive. Date comparisons are by calendar day,
69
- and `equals` on a `date` column plays the role of "is". On a `multiSelect`
69
+ so `equals` on a `date` column matches the same day. On a `multiSelect`
70
70
  column, whose cells hold arrays, `isAnyOf` is an intersection test and `isNoneOf`
71
71
  its complement. `between` is inclusive at both ends.
72
72
 
73
73
  `meta.filter.defaultOperator` sets which one a fresh filter opens on, and must be
74
- one of the type's own:
74
+ one of the operators that type offers:
75
75
 
76
76
  ```tsx
77
77
  columnHelper.accessor("salary", {
@@ -85,10 +85,10 @@ columnHelper.accessor("salary", {
85
85
  `TMDataGrid.FilterPanel` is column, operator and value rows under a "Filters"
86
86
  header, above "Add filter" and "Clear all". It is rendered by
87
87
  `TMDataGrid.Table`; Escape and a click outside close it, with `FilterButton`
88
- exempt from the click-away so it stays a toggle. Closing only hides it - the
88
+ exempt from the click-away so it stays a toggle. Closing only hides it; the
89
89
  filters stay. **Clear all** drops every filter, half-typed ones included.
90
90
 
91
- `TMDataGrid.FilterPills` takes the grid as an `api` prop rather than reading
91
+ `TMDataGrid.FilterPills` takes the grid as an `api` prop instead of reading
92
92
  context, so active filters can live in a page header or anywhere else:
93
93
 
94
94
  ```tsx
@@ -97,7 +97,7 @@ import { TMDataGridFilterPills } from "@jielga/tmdatagrid";
97
97
  <TMDataGridFilterPills api={grid} onPillClick={(columnId) => focus(columnId)} />;
98
98
  ```
99
99
 
100
- One pill per **active** filter - `First name: Sofia ✕` - where ✕ clears it and a
100
+ One pill per **active** filter, `First name: Sofia ✕`, where ✕ clears it and a
101
101
  click on the label reopens the panel on its column. The label spells the
102
102
  operator out unless it is the type's default: `Age is greater than 30`, but
103
103
  `First name: Sofia`. `openColumnFilter(api, columnId)` does the same reopening
@@ -123,15 +123,15 @@ meta: {
123
123
  ```
124
124
 
125
125
  Pair the range-shaped ones with `defaultOperator: "between"` so the filter
126
- opens on them. For operators outside their shape every built-in falls back to
127
- `TMDataGridFilterValueInput`, which is exported so a custom control can take the
128
- same escape.
126
+ opens on them. For an operator they do not cover, every built-in falls back to
127
+ `TMDataGridFilterValueInput`, which is exported so custom controls can fall back
128
+ the same way.
129
129
 
130
- `meta.filter.control` is a **component**, rendered as JSX, so hooks are legal
131
- inside. It is a **value-only contract**: it reads `operator` to shape itself and
132
- writes the bare value through `onChange`, and the grid composes the stored
133
- `{ operator, value }` around it. The column and operator dropdowns stay the
134
- panel's.
130
+ `meta.filter.control` is a **component**, rendered as JSX, so hooks may be used
131
+ inside. It handles the value only: it reads `operator` to shape itself and
132
+ writes the bare value through `onChange`, and the grid stores the
133
+ `{ operator, value }` pair around it. The column and operator dropdowns remain
134
+ the panel's.
135
135
 
136
136
  ```tsx
137
137
  import type { TMDataGridFilterControlComponent } from "@jielga/tmdatagrid";
@@ -160,16 +160,17 @@ meta: { type: "select", filter: { control: StatusFilter } }
160
160
  ```
161
161
 
162
162
  `args.options` arrives pre-resolved for a column that declares `meta.options` or
163
- is select-shaped; `args.table` is the escape hatch for a control that must reach
164
- further.
163
+ is select-shaped. `args.table` is available to a control that needs more than
164
+ that.
165
165
 
166
- To give a column its own *matching* rather than its own control, set `filterFn`
167
- on the column definition. The grid only provides `"tmDataGrid"` as the default.
166
+ To give a column its own *matching* instead of its own control, set `filterFn`
167
+ on the column definition. The grid provides only `"tmDataGrid"`, which is the
168
+ default.
168
169
 
169
170
  ## Quick search
170
171
 
171
- `TMDataGrid.Search` is a debounced input writing TanStack's `globalFilter`. No
172
- option turns it on: you render it or you do not.
172
+ `TMDataGrid.Search` is a debounced input writing TanStack's `globalFilter`.
173
+ There is no option to turn it on: render the component, or do not.
173
174
 
174
175
  ```tsx
175
176
  <TMDataGrid.Toolbar>
@@ -177,37 +178,37 @@ option turns it on: you render it or you do not.
177
178
  </TMDataGrid.Toolbar>
178
179
  ```
179
180
 
180
- Matching is **fuzzy by default**: typos and skipped characters are forgiven, and
181
+ Matching is **fuzzy by default**: typos and skipped characters still match, and
181
182
  while the search is the only thing narrowing the grid (no sort, no grouping) the
182
- rows order by match quality, best first. That ordering is derived, never written
183
- into `sorting` - no column claims `aria-sort`, nothing lands in the persisted
184
- slices, and the reader's next sort click takes over just by existing.
183
+ rows are ordered by match quality, best first. That ordering is derived and
184
+ never written into `sorting`: no column takes `aria-sort`, nothing is written to
185
+ the persisted slices, and the next sort click replaces it.
185
186
 
186
187
  `quickSearchMode: "contains"` restores plain substring matching. An explicit
187
188
  `globalFilterFn` overrides both, and switches the rank ordering off with it.
188
189
  `fuzzyGlobalFilterFn` is exported for a custom input over the same matching.
189
190
 
190
- `enableMatchHighlighting: true` marks the matched slice of a cell's text, under
191
+ `enableMatchHighlighting: true` marks the matched part of a cell's text, under
191
192
  the quick search or a `contains` / `startsWith` / `endsWith` column filter. What
192
- gets marked is the contiguous, case-insensitive occurrence, so a fuzzy
193
- typo-match with no contiguous occurrence shows no highlight. It applies to
194
- **default-rendered cells only**: a column with its own `cell` renderer opts out
195
- by existing.
193
+ is marked is the contiguous, case-insensitive occurrence, so a fuzzy typo-match
194
+ with no contiguous occurrence is not highlighted. It applies to
195
+ **default-rendered cells only**: a column with its own `cell` renderer is
196
+ excluded.
196
197
 
197
198
  ## Turning it off
198
199
 
199
200
  `enableColumnFilters: false` removes the panel, the button and the menu item;
200
201
  `enableColumnFilter: false` on a column takes that column out.
201
202
  `enableGlobalFilter: false` removes the search input, or on a column removes its
202
- participation in the search. Generated lanes already opt out.
203
+ participation in the search. The generated lanes are already excluded.
203
204
 
204
205
  ## Common mistakes
205
206
 
206
207
  ### CRITICAL Treating presence in `columnFilters` as an active filter
207
208
 
208
- A half-typed filter stays in state on purpose, so the panel keeps its row while
209
- the reader types. Counting entries reports filters that are narrowing nothing,
210
- and forwarding them to a server sends `{ operator: "contains", value: "" }` as a
209
+ A half-typed filter stays in state deliberately, so the panel keeps its row
210
+ while the user types. Counting entries reports filters that narrow nothing, and
211
+ forwarding them to a server sends `{ operator: "contains", value: "" }` as a
211
212
  real constraint.
212
213
 
213
214
  Wrong:
@@ -251,8 +252,8 @@ Source: `src/tmdatagrid/core/filterOperators.ts`.
251
252
 
252
253
  `getColumnType` defaults to `"string"`, so a numeric column that omits
253
254
  `meta: { type: "number" }` offers only the string operators. `greaterThan` and
254
- `between` never appear in the panel, and comparisons run as text - `"9"` sorts
255
- above `"10"`.
255
+ `between` never appear in the panel, and comparisons run as text, where `"9"`
256
+ sorts above `"10"`.
256
257
 
257
258
  Source: `src/docs/filtering.md` (Operators).
258
259
 
@@ -275,9 +276,9 @@ Source: `src/docs/filtering.md` (Writing your own).
275
276
 
276
277
  ### MEDIUM Writing the whole filter from a custom control
277
278
 
278
- The contract is value-only: `onChange` takes the bare value, and the grid
279
- composes `{ operator, value }` around it. Passing an object writes a filter
280
- whose `value` is itself an object, which no operator can match against.
279
+ A control handles the value only: `onChange` takes the bare value, and the grid
280
+ stores `{ operator, value }` around it. Passing an object writes a filter whose
281
+ `value` is itself an object, which no operator can match against.
281
282
 
282
283
  Wrong:
283
284
 
@@ -295,18 +296,18 @@ Source: `src/docs/filtering.md` (Writing your own).
295
296
 
296
297
  ### MEDIUM Expecting match highlighting in a custom cell
297
298
 
298
- The grid replicates the default value-to-string render with marks added, and
299
- never rummages inside a custom renderer's output. A column with a `cell`
300
- renderer shows no marks however `enableMatchHighlighting` is set. Equality
301
- operators highlight nothing either.
299
+ The grid reproduces the default value-to-string render with marks added, and
300
+ does not modify a custom renderer's output. A column with a `cell` renderer
301
+ shows no marks whatever `enableMatchHighlighting` is set to. Equality operators
302
+ highlight nothing either.
302
303
 
303
304
  Source: `src/docs/quick-search.md` (Match highlighting).
304
305
 
305
306
  ### MEDIUM Expecting fuzzy ranking to survive a sort
306
307
 
307
308
  The rank ordering applies only while the search is the sole narrowing. Any sort
308
- or grouping takes over, by design - the ordering is derived and never written
309
- into `sorting`, so there is nothing to clear afterwards.
309
+ or grouping replaces it. The ordering is derived and never written into
310
+ `sorting`, so there is nothing to clear afterwards.
310
311
 
311
312
  Source: `src/docs/quick-search.md` (Fuzzy by default).
312
313
 
@@ -329,11 +330,11 @@ Source: `src/docs/quick-search.md` (Fuzzy by default).
329
330
  | `TMDataGrid.FilterPills` | Component | `api`, `size`, `showClearAll`, `onPillClick`, `className` | – | Active filters as removable pills, renderable anywhere. |
330
331
  | `TMDataGrid.Search` | Component | `placeholder`, `debounce` (`250`), `w` (`220`) | – | The debounced quick-search input. |
331
332
  | `openColumnFilter` | Export | `(api, columnId) => void` | – | Opens the panel on a column. |
332
- | `isFilterActive` | Export | `(value) => boolean` | – | Whether a filter value is doing anything. |
333
+ | `isFilterActive` | Export | `(value) => boolean` | – | Whether a filter value narrows anything. |
333
334
  | `getOperatorsForType` | Export | `(type) => operators` | – | The operator list a type offers. |
334
335
  | `FILTER_OPERATOR_LABELS` | Export | record | – | The label shown for each operator. |
335
336
  | `formatFilterLabel` | Export | `({ label, type, filter }) => string` | – | The one-line description used on the pills. |
336
- | `emptyValueForOperator` · `operatorNeedsValue` · `operatorTakesArrayValue` · `operatorTakesRangeValue` | Exports | – | – | What shape of value an operator wants. |
337
+ | `emptyValueForOperator` · `operatorNeedsValue` · `operatorTakesArrayValue` · `operatorTakesRangeValue` | Exports | – | – | What shape of value an operator expects. |
337
338
  | `TMDataGridFilterValueInput` | Export | component | – | The default value control, for falling back to. |
338
339
  | `DgRangeSliderFilter` · `DgDateRangeFilter` · `DgAutocompleteFilter` · `DgTriStateFilter` | Exports | components | – | The four ready-made controls. |
339
340
  | `fuzzyGlobalFilterFn` | Export | filter fn | – | The default matcher, for a custom input. |