@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.
- package/README.md +32 -34
- package/dist/index.d.ts +164 -102
- package/dist/index.js +2060 -1705
- package/dist/index.js.map +1 -1
- package/dist/styles.css +1 -1
- package/package.json +1 -1
- package/skills/appearance/SKILL.md +37 -34
- package/skills/cell-selection/SKILL.md +19 -19
- package/skills/columns/SKILL.md +9 -9
- package/skills/data/SKILL.md +53 -53
- package/skills/editing/SKILL.md +152 -311
- package/skills/editing/references/common-mistakes.md +177 -0
- package/skills/editing/references/editing-api.md +56 -28
- package/skills/editing/references/editors-and-validation.md +26 -19
- package/skills/filtering/SKILL.md +51 -50
- package/skills/getting-started/SKILL.md +10 -10
- package/skills/grouping/SKILL.md +41 -43
- package/skills/options/SKILL.md +4 -4
- package/skills/rows/SKILL.md +59 -59
- package/skills/rows/references/rows-api.md +7 -7
- package/skills/server-side/SKILL.md +1 -1
- package/skills/testing/SKILL.md +11 -10
- package/src/tmdatagrid/components/TMDataGrid.module.css +13 -1
- package/src/tmdatagrid/components/TMDataGridCellEditor.tsx +20 -8
- package/src/tmdatagrid/components/TMDataGridColumnsPanel.tsx +29 -18
- package/src/tmdatagrid/components/TMDataGridDetailsColumn.tsx +5 -8
- package/src/tmdatagrid/components/TMDataGridEditActions.tsx +2 -2
- package/src/tmdatagrid/components/TMDataGridEditColumn.tsx +246 -86
- package/src/tmdatagrid/components/TMDataGridEntryRows.tsx +176 -77
- package/src/tmdatagrid/components/TMDataGridHeaderCell.tsx +11 -6
- package/src/tmdatagrid/components/TMDataGridSelectColumn.tsx +9 -5
- package/src/tmdatagrid/components/TMDataGridTable.module.css +47 -3
- package/src/tmdatagrid/components/TMDataGridTable.tsx +101 -40
- package/src/tmdatagrid/components/editors/TMDataGridNumberEditor.tsx +1 -1
- package/src/tmdatagrid/components/icons.ts +1 -0
- package/src/tmdatagrid/core/autosize.ts +30 -6
- package/src/tmdatagrid/core/capabilities.ts +15 -5
- package/src/tmdatagrid/core/cellExport.ts +6 -7
- package/src/tmdatagrid/core/cellNavigation.ts +2 -2
- package/src/tmdatagrid/core/cellRange.ts +6 -6
- package/src/tmdatagrid/core/columnOrdering.ts +30 -1
- package/src/tmdatagrid/core/columnUtils.ts +14 -0
- package/src/tmdatagrid/core/draftCellContext.ts +68 -0
- package/src/tmdatagrid/core/editEngine.ts +111 -37
- package/src/tmdatagrid/core/filterOperators.ts +6 -6
- package/src/tmdatagrid/core/labels.ts +13 -1
- package/src/tmdatagrid/core/labelsSv.ts +4 -0
- package/src/tmdatagrid/core/matchHighlight.ts +3 -3
- package/src/tmdatagrid/core/persistence.ts +3 -3
- package/src/tmdatagrid/core/rowSelection.ts +3 -3
- package/src/tmdatagrid/index.ts +3 -1
- 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
|
-
| `
|
|
11
|
-
| `
|
|
12
|
-
| `
|
|
13
|
-
| `
|
|
14
|
-
| `
|
|
15
|
-
| `
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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
|
-
| `
|
|
25
|
-
| `
|
|
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
|
|
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>` |
|
|
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
|
|
58
|
-
| `canEditCell` | `(row, column) => boolean` | The
|
|
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
|
|
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 `"
|
|
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
|
-
|
|
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`, `
|
|
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
|
-
- `
|
|
112
|
-
- `
|
|
113
|
-
|
|
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
|
-
|
|
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
|
-
|
|
|
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
|
|
30
|
+
## The editor API
|
|
31
31
|
|
|
32
|
-
`meta.edit.editor` fills the same slot the built-ins
|
|
33
|
-
rendered as JSX, so hooks
|
|
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
|
|
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
|
|
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
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
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
|
|
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 }`
|
|
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 `
|
|
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].
|
|
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.
|
|
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
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
127
|
-
`TMDataGridFilterValueInput`, which is exported so
|
|
128
|
-
same
|
|
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
|
|
131
|
-
inside. It
|
|
132
|
-
writes the bare value through `onChange`, and the grid
|
|
133
|
-
`{ operator, value }` around it. The column and operator dropdowns
|
|
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
|
|
164
|
-
|
|
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*
|
|
167
|
-
on the column definition. The grid only
|
|
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`.
|
|
172
|
-
option
|
|
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
|
|
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
|
|
183
|
-
into `sorting
|
|
184
|
-
slices, and the
|
|
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
|
|
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
|
-
|
|
193
|
-
|
|
194
|
-
**default-rendered cells only**: a column with its own `cell` renderer
|
|
195
|
-
|
|
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.
|
|
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
|
|
209
|
-
the
|
|
210
|
-
|
|
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
|
|
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
|
-
|
|
279
|
-
|
|
280
|
-
|
|
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
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
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
|
|
309
|
-
|
|
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
|
|
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
|
|
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. |
|