@jielga/tmdatagrid 2.0.0-beta.1 → 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/dist/index.d.ts +60 -23
- package/dist/index.js +2020 -1704
- package/dist/index.js.map +1 -1
- package/dist/styles.css +1 -1
- package/package.json +1 -1
- package/skills/appearance/SKILL.md +1 -1
- package/skills/cell-selection/SKILL.md +1 -1
- package/skills/columns/SKILL.md +2 -2
- package/skills/data/SKILL.md +1 -1
- package/skills/editing/SKILL.md +68 -213
- package/skills/editing/references/common-mistakes.md +177 -0
- package/skills/editing/references/editing-api.md +39 -13
- package/skills/editing/references/editors-and-validation.md +5 -2
- package/skills/filtering/SKILL.md +1 -1
- package/skills/getting-started/SKILL.md +1 -1
- package/skills/grouping/SKILL.md +1 -1
- package/skills/options/SKILL.md +2 -2
- package/skills/rows/SKILL.md +1 -1
- package/skills/rows/references/rows-api.md +1 -1
- package/skills/server-side/SKILL.md +1 -1
- package/skills/testing/SKILL.md +3 -2
- package/src/tmdatagrid/components/TMDataGrid.module.css +7 -0
- package/src/tmdatagrid/components/TMDataGridCellEditor.tsx +20 -8
- package/src/tmdatagrid/components/TMDataGridEditActions.tsx +1 -1
- package/src/tmdatagrid/components/TMDataGridEditColumn.tsx +242 -86
- package/src/tmdatagrid/components/TMDataGridEntryRows.tsx +176 -77
- package/src/tmdatagrid/components/TMDataGridTable.module.css +47 -3
- package/src/tmdatagrid/components/TMDataGridTable.tsx +44 -4
- package/src/tmdatagrid/components/icons.ts +1 -0
- package/src/tmdatagrid/core/capabilities.ts +10 -0
- package/src/tmdatagrid/core/draftCellContext.ts +68 -0
- package/src/tmdatagrid/core/editEngine.ts +106 -32
- package/src/tmdatagrid/core/labels.ts +13 -1
- package/src/tmdatagrid/core/labelsSv.ts +4 -0
- package/src/tmdatagrid/index.ts +1 -1
- package/src/tmdatagrid/useTMDataGrid.tsx +39 -20
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { ActionIcon, Group, Loader, Tooltip } from "@mantine/core";
|
|
2
2
|
import { useSelector } from "@tanstack/react-store";
|
|
3
3
|
import type { ColumnDef, Row, RowData } from "@tanstack/react-table";
|
|
4
|
+
import classes from "./TMDataGridTable.module.css";
|
|
4
5
|
import {
|
|
5
6
|
useCellControlTabIndex,
|
|
6
7
|
useTMDataGridContext,
|
|
@@ -11,6 +12,7 @@ import {
|
|
|
11
12
|
CheckIcon,
|
|
12
13
|
CloseIcon,
|
|
13
14
|
PencilIcon,
|
|
15
|
+
PlusIcon,
|
|
14
16
|
RestoreIcon,
|
|
15
17
|
TrashIcon,
|
|
16
18
|
} from "./icons";
|
|
@@ -18,10 +20,49 @@ import {
|
|
|
18
20
|
export const EDIT_COLUMN_ID = "__edit__";
|
|
19
21
|
|
|
20
22
|
/**
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
23
|
+
* Draft mode's change marker in the lane - what kind of uncommitted change
|
|
24
|
+
* the row carries. Non-interactive; the buttons beside it act. `data-state`
|
|
25
|
+
* is the styling hook, `data-error` flips it red and puts the message in the
|
|
26
|
+
* tooltip in place of the state's name.
|
|
27
|
+
*/
|
|
28
|
+
function RowStateIndicator({
|
|
29
|
+
state,
|
|
30
|
+
rowId,
|
|
31
|
+
label,
|
|
32
|
+
error,
|
|
33
|
+
}: {
|
|
34
|
+
state: "new" | "edited" | "deleted";
|
|
35
|
+
rowId: string;
|
|
36
|
+
label: string;
|
|
37
|
+
error: string | null;
|
|
38
|
+
}) {
|
|
39
|
+
const Icon =
|
|
40
|
+
state === "new" ? PlusIcon : state === "edited" ? PencilIcon : TrashIcon;
|
|
41
|
+
return (
|
|
42
|
+
<Tooltip label={error ?? label} withArrow>
|
|
43
|
+
<span
|
|
44
|
+
role="img"
|
|
45
|
+
aria-label={label}
|
|
46
|
+
data-dg-part="row-state"
|
|
47
|
+
data-state={state}
|
|
48
|
+
data-row-id={rowId}
|
|
49
|
+
data-error={error !== null || undefined}
|
|
50
|
+
className={classes.rowStateIndicator}
|
|
51
|
+
>
|
|
52
|
+
<Icon size={16} stroke={1.6} />
|
|
53
|
+
</span>
|
|
54
|
+
</Tooltip>
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* One row's slot in the edit lane, driven off the edit store. Outside draft
|
|
60
|
+
* mode: a pencil while idle, Save/Cancel while the row's form is open, a
|
|
61
|
+
* loader while its commit is in flight; a row error turns the Save red with
|
|
62
|
+
* the message in its tooltip - the pathless `.refine()` has nowhere else to
|
|
63
|
+
* land. Under draft mode the lane is the change marker and the per-row
|
|
64
|
+
* revert instead: a state icon (new/edited/deleted) beside undo, discard or
|
|
65
|
+
* restore - never a per-row save, which the engine would only park anyway.
|
|
25
66
|
*/
|
|
26
67
|
function EditLaneCell<TData extends RowData>({
|
|
27
68
|
row,
|
|
@@ -38,18 +79,97 @@ function EditLaneCell<TData extends RowData>({
|
|
|
38
79
|
const isNew = useSelector(edit.store, (state) =>
|
|
39
80
|
state.newRows.some((newRow) => newRow.tempId === rowId),
|
|
40
81
|
);
|
|
82
|
+
const isConfirmedNew = useSelector(
|
|
83
|
+
edit.store,
|
|
84
|
+
(state) =>
|
|
85
|
+
state.newRows.find((newRow) => newRow.tempId === rowId)?.confirmed ===
|
|
86
|
+
true,
|
|
87
|
+
);
|
|
41
88
|
const isMarkedDeleted = useSelector(edit.store, (state) =>
|
|
42
89
|
state.deletedRowIds.includes(rowId),
|
|
43
90
|
);
|
|
44
91
|
|
|
45
92
|
if (row.getIsGrouped()) return null;
|
|
46
93
|
|
|
47
|
-
|
|
48
|
-
|
|
94
|
+
const isDraftMode = features.editMode === "draft";
|
|
95
|
+
const hasErrors =
|
|
96
|
+
projection !== undefined &&
|
|
97
|
+
(projection.hasRowError || projection.errorFields.length > 0);
|
|
98
|
+
const rowError = hasErrors
|
|
99
|
+
? (firstErrorText(edit.getForm(rowId)?.state.errors ?? []) ??
|
|
100
|
+
labels.editRowErrors)
|
|
101
|
+
: null;
|
|
102
|
+
|
|
103
|
+
const trash = edit.canDeleteRows() ? (
|
|
104
|
+
<Tooltip label={labels.deleteRow} withArrow>
|
|
105
|
+
<ActionIcon
|
|
106
|
+
variant="subtle"
|
|
107
|
+
color="gray"
|
|
108
|
+
size="sm"
|
|
109
|
+
tabIndex={tabIndex}
|
|
110
|
+
aria-label={labels.deleteRow}
|
|
111
|
+
data-dg-part="delete-row"
|
|
112
|
+
data-row-id={rowId}
|
|
113
|
+
onClick={(event) => {
|
|
114
|
+
event.stopPropagation();
|
|
115
|
+
edit.deleteRow(rowId);
|
|
116
|
+
}}
|
|
117
|
+
>
|
|
118
|
+
<TrashIcon size={16} stroke={1.6} />
|
|
119
|
+
</ActionIcon>
|
|
120
|
+
</Tooltip>
|
|
121
|
+
) : null;
|
|
122
|
+
|
|
123
|
+
// An entry row: ✓ enters it - an add under the immediate modes, a parked
|
|
124
|
+
// confirm under draft - and ✕ discards the entry. A confirmed entry waits
|
|
125
|
+
// for Save all as a value row: marked new, re-openable, still discardable.
|
|
49
126
|
if (isNew) {
|
|
127
|
+
if (isConfirmedNew) {
|
|
128
|
+
return (
|
|
129
|
+
<Group gap={0} wrap="nowrap">
|
|
130
|
+
<RowStateIndicator
|
|
131
|
+
state="new"
|
|
132
|
+
rowId={rowId}
|
|
133
|
+
label={labels.rowStateNew}
|
|
134
|
+
error={rowError}
|
|
135
|
+
/>
|
|
136
|
+
<Tooltip label={labels.editRow} withArrow>
|
|
137
|
+
<ActionIcon
|
|
138
|
+
variant="subtle"
|
|
139
|
+
color="gray"
|
|
140
|
+
size="sm"
|
|
141
|
+
tabIndex={tabIndex}
|
|
142
|
+
aria-label={labels.editRow}
|
|
143
|
+
data-dg-part="edit-row"
|
|
144
|
+
data-row-id={rowId}
|
|
145
|
+
onClick={(event) => {
|
|
146
|
+
event.stopPropagation();
|
|
147
|
+
edit.begin({ rowId, columnId: null });
|
|
148
|
+
}}
|
|
149
|
+
>
|
|
150
|
+
<PencilIcon size={16} stroke={1.6} />
|
|
151
|
+
</ActionIcon>
|
|
152
|
+
</Tooltip>
|
|
153
|
+
<Tooltip label={labels.discardNewRow} withArrow>
|
|
154
|
+
<ActionIcon
|
|
155
|
+
variant="subtle"
|
|
156
|
+
color="gray"
|
|
157
|
+
size="sm"
|
|
158
|
+
tabIndex={tabIndex}
|
|
159
|
+
aria-label={labels.discardNewRow}
|
|
160
|
+
data-dg-part="discard-new-row"
|
|
161
|
+
data-row-id={rowId}
|
|
162
|
+
onClick={() => edit.cancel(rowId)}
|
|
163
|
+
>
|
|
164
|
+
<CloseIcon size={16} stroke={1.6} />
|
|
165
|
+
</ActionIcon>
|
|
166
|
+
</Tooltip>
|
|
167
|
+
</Group>
|
|
168
|
+
);
|
|
169
|
+
}
|
|
50
170
|
return (
|
|
51
171
|
<Group gap={0} wrap="nowrap">
|
|
52
|
-
{
|
|
172
|
+
<Tooltip label={labels.confirmNewRow} withArrow>
|
|
53
173
|
<ActionIcon
|
|
54
174
|
variant="subtle"
|
|
55
175
|
color="green"
|
|
@@ -62,84 +182,121 @@ function EditLaneCell<TData extends RowData>({
|
|
|
62
182
|
>
|
|
63
183
|
<CheckIcon size={16} stroke={2} />
|
|
64
184
|
</ActionIcon>
|
|
65
|
-
|
|
66
|
-
<
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
185
|
+
</Tooltip>
|
|
186
|
+
<Tooltip label={labels.discardNewRow} withArrow>
|
|
187
|
+
<ActionIcon
|
|
188
|
+
variant="subtle"
|
|
189
|
+
color="gray"
|
|
190
|
+
size="sm"
|
|
191
|
+
tabIndex={tabIndex}
|
|
192
|
+
aria-label={labels.discardNewRow}
|
|
193
|
+
data-dg-part="discard-new-row"
|
|
194
|
+
data-row-id={rowId}
|
|
195
|
+
onClick={() => edit.cancel(rowId)}
|
|
196
|
+
>
|
|
197
|
+
<CloseIcon size={16} stroke={1.6} />
|
|
198
|
+
</ActionIcon>
|
|
199
|
+
</Tooltip>
|
|
78
200
|
</Group>
|
|
79
201
|
);
|
|
80
202
|
}
|
|
81
203
|
|
|
82
|
-
// A
|
|
204
|
+
// A draft-mode deletion mark is a draft like any other - restorable in place.
|
|
83
205
|
if (isMarkedDeleted) {
|
|
84
|
-
return (
|
|
85
|
-
<ActionIcon
|
|
86
|
-
variant="subtle"
|
|
87
|
-
color="gray"
|
|
88
|
-
size="sm"
|
|
89
|
-
tabIndex={tabIndex}
|
|
90
|
-
aria-label={labels.restoreRow}
|
|
91
|
-
data-dg-part="restore-row"
|
|
92
|
-
data-row-id={rowId}
|
|
93
|
-
onClick={(event) => {
|
|
94
|
-
event.stopPropagation();
|
|
95
|
-
edit.deleteRow(rowId);
|
|
96
|
-
}}
|
|
97
|
-
>
|
|
98
|
-
<RestoreIcon size={16} stroke={1.6} />
|
|
99
|
-
</ActionIcon>
|
|
100
|
-
);
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
// The pencil means "this row edits"; a row that does not gets nothing.
|
|
104
|
-
if (!edit.canEditRow(row as never)) return null;
|
|
105
|
-
|
|
106
|
-
if (!isOpen) {
|
|
107
206
|
return (
|
|
108
207
|
<Group gap={0} wrap="nowrap">
|
|
109
|
-
|
|
208
|
+
<RowStateIndicator
|
|
209
|
+
state="deleted"
|
|
210
|
+
rowId={rowId}
|
|
211
|
+
label={labels.rowStateDeleted}
|
|
212
|
+
error={null}
|
|
213
|
+
/>
|
|
214
|
+
<Tooltip label={labels.restoreRow} withArrow>
|
|
110
215
|
<ActionIcon
|
|
111
216
|
variant="subtle"
|
|
112
217
|
color="gray"
|
|
113
218
|
size="sm"
|
|
114
219
|
tabIndex={tabIndex}
|
|
115
|
-
aria-label={labels.
|
|
116
|
-
data-dg-part="
|
|
220
|
+
aria-label={labels.restoreRow}
|
|
221
|
+
data-dg-part="restore-row"
|
|
117
222
|
data-row-id={rowId}
|
|
118
223
|
onClick={(event) => {
|
|
119
224
|
event.stopPropagation();
|
|
120
|
-
edit.
|
|
225
|
+
edit.deleteRow(rowId);
|
|
121
226
|
}}
|
|
122
227
|
>
|
|
123
|
-
<
|
|
228
|
+
<RestoreIcon size={16} stroke={1.6} />
|
|
124
229
|
</ActionIcon>
|
|
125
|
-
|
|
126
|
-
|
|
230
|
+
</Tooltip>
|
|
231
|
+
</Group>
|
|
232
|
+
);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// The pencil means "this row edits"; a row that does not gets nothing.
|
|
236
|
+
if (!edit.canEditRow(row as never)) return null;
|
|
237
|
+
|
|
238
|
+
// Draft mode never offers a per-row save - the engine would only park it,
|
|
239
|
+
// and Save all is the commit. The lane is the state marker and the revert.
|
|
240
|
+
// No trash beside the revert: the two undo-shaped icons were too easy to
|
|
241
|
+
// mistake for one another, so a dirty row reverts first, then deletes.
|
|
242
|
+
if (isDraftMode) {
|
|
243
|
+
if (projection?.isSubmitting) {
|
|
244
|
+
return <Loader size="xs" aria-label={labels.loading} />;
|
|
245
|
+
}
|
|
246
|
+
const isDirty = (projection?.dirtyFields.length ?? 0) > 0;
|
|
247
|
+
if (!isDirty) return trash;
|
|
248
|
+
return (
|
|
249
|
+
<Group gap={0} wrap="nowrap">
|
|
250
|
+
<RowStateIndicator
|
|
251
|
+
state="edited"
|
|
252
|
+
rowId={rowId}
|
|
253
|
+
label={labels.rowStateEdited}
|
|
254
|
+
error={rowError}
|
|
255
|
+
/>
|
|
256
|
+
<Tooltip label={labels.revertRow} withArrow>
|
|
127
257
|
<ActionIcon
|
|
128
258
|
variant="subtle"
|
|
129
259
|
color="gray"
|
|
130
260
|
size="sm"
|
|
131
261
|
tabIndex={tabIndex}
|
|
132
|
-
aria-label={labels.
|
|
133
|
-
data-dg-part="
|
|
262
|
+
aria-label={labels.revertRow}
|
|
263
|
+
data-dg-part="revert-row"
|
|
134
264
|
data-row-id={rowId}
|
|
135
265
|
onClick={(event) => {
|
|
136
266
|
event.stopPropagation();
|
|
137
|
-
edit.
|
|
267
|
+
edit.cancel(rowId);
|
|
138
268
|
}}
|
|
139
269
|
>
|
|
140
|
-
<
|
|
270
|
+
<RestoreIcon size={16} stroke={1.6} />
|
|
141
271
|
</ActionIcon>
|
|
272
|
+
</Tooltip>
|
|
273
|
+
</Group>
|
|
274
|
+
);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
if (!isOpen) {
|
|
278
|
+
return (
|
|
279
|
+
<Group gap={0} wrap="nowrap">
|
|
280
|
+
{features.editMode === "row" && (
|
|
281
|
+
<Tooltip label={labels.editRow} withArrow>
|
|
282
|
+
<ActionIcon
|
|
283
|
+
variant="subtle"
|
|
284
|
+
color="gray"
|
|
285
|
+
size="sm"
|
|
286
|
+
tabIndex={tabIndex}
|
|
287
|
+
aria-label={labels.editRow}
|
|
288
|
+
data-dg-part="edit-row"
|
|
289
|
+
data-row-id={rowId}
|
|
290
|
+
onClick={(event) => {
|
|
291
|
+
event.stopPropagation();
|
|
292
|
+
edit.begin({ rowId, columnId: null });
|
|
293
|
+
}}
|
|
294
|
+
>
|
|
295
|
+
<PencilIcon size={16} stroke={1.6} />
|
|
296
|
+
</ActionIcon>
|
|
297
|
+
</Tooltip>
|
|
142
298
|
)}
|
|
299
|
+
{trash}
|
|
143
300
|
</Group>
|
|
144
301
|
);
|
|
145
302
|
}
|
|
@@ -148,14 +305,6 @@ function EditLaneCell<TData extends RowData>({
|
|
|
148
305
|
return <Loader size="xs" aria-label={labels.loading} />;
|
|
149
306
|
}
|
|
150
307
|
|
|
151
|
-
const hasErrors =
|
|
152
|
-
projection !== undefined &&
|
|
153
|
-
(projection.hasRowError || projection.errorFields.length > 0);
|
|
154
|
-
const rowError = hasErrors
|
|
155
|
-
? (firstErrorText(edit.getForm(rowId)?.state.errors ?? []) ??
|
|
156
|
-
labels.editRowErrors)
|
|
157
|
-
: null;
|
|
158
|
-
|
|
159
308
|
const save = (
|
|
160
309
|
<ActionIcon
|
|
161
310
|
variant="subtle"
|
|
@@ -177,39 +326,46 @@ function EditLaneCell<TData extends RowData>({
|
|
|
177
326
|
return (
|
|
178
327
|
<Group gap={0} wrap="nowrap">
|
|
179
328
|
{rowError === null ? (
|
|
180
|
-
|
|
329
|
+
<Tooltip label={labels.saveRow} withArrow>
|
|
330
|
+
{save}
|
|
331
|
+
</Tooltip>
|
|
181
332
|
) : (
|
|
182
333
|
<Tooltip label={rowError} withArrow>
|
|
183
334
|
{save}
|
|
184
335
|
</Tooltip>
|
|
185
336
|
)}
|
|
186
|
-
<
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
event
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
337
|
+
<Tooltip label={labels.cancelRowEdit} withArrow>
|
|
338
|
+
<ActionIcon
|
|
339
|
+
variant="subtle"
|
|
340
|
+
color="gray"
|
|
341
|
+
size="sm"
|
|
342
|
+
tabIndex={tabIndex}
|
|
343
|
+
aria-label={labels.cancelRowEdit}
|
|
344
|
+
data-dg-part="cancel-row"
|
|
345
|
+
data-row-id={rowId}
|
|
346
|
+
onClick={(event) => {
|
|
347
|
+
event.stopPropagation();
|
|
348
|
+
edit.cancel(rowId);
|
|
349
|
+
}}
|
|
350
|
+
>
|
|
351
|
+
<CloseIcon size={16} stroke={1.6} />
|
|
352
|
+
</ActionIcon>
|
|
353
|
+
</Tooltip>
|
|
201
354
|
</Group>
|
|
202
355
|
);
|
|
203
356
|
}
|
|
204
357
|
|
|
205
358
|
/**
|
|
206
|
-
* The generated edit lane, appended and pinned right
|
|
207
|
-
*
|
|
208
|
-
* checkbox lane's build on the left.
|
|
359
|
+
* The generated edit lane, appended and pinned right - the row's Save at the
|
|
360
|
+
* end of the row under `mode: "row"`, the state marker and revert under
|
|
361
|
+
* `mode: "draft"`, mirroring the checkbox lane's build on the left.
|
|
209
362
|
*/
|
|
210
363
|
export function createEditColumn<TData extends RowData>(
|
|
211
364
|
label = "Edit",
|
|
365
|
+
/** Draft mode holds three controls where the others hold two. */
|
|
366
|
+
wide = false,
|
|
212
367
|
): ColumnDef<TMDataGridFeatures, TData, unknown> {
|
|
368
|
+
const width = wide ? 88 : 64;
|
|
213
369
|
return {
|
|
214
370
|
id: EDIT_COLUMN_ID,
|
|
215
371
|
meta: {
|
|
@@ -217,10 +373,10 @@ export function createEditColumn<TData extends RowData>(
|
|
|
217
373
|
align: "center",
|
|
218
374
|
enableOrdering: false,
|
|
219
375
|
},
|
|
220
|
-
// Wide enough for the
|
|
221
|
-
size:
|
|
222
|
-
minSize:
|
|
223
|
-
maxSize:
|
|
376
|
+
// Wide enough for the pair (or draft's trio) it holds while editing.
|
|
377
|
+
size: width,
|
|
378
|
+
minSize: width,
|
|
379
|
+
maxSize: width,
|
|
224
380
|
enableResizing: false,
|
|
225
381
|
enableSorting: false,
|
|
226
382
|
enableColumnFilter: false,
|