@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.
Files changed (36) hide show
  1. package/dist/index.d.ts +60 -23
  2. package/dist/index.js +2020 -1704
  3. package/dist/index.js.map +1 -1
  4. package/dist/styles.css +1 -1
  5. package/package.json +1 -1
  6. package/skills/appearance/SKILL.md +1 -1
  7. package/skills/cell-selection/SKILL.md +1 -1
  8. package/skills/columns/SKILL.md +2 -2
  9. package/skills/data/SKILL.md +1 -1
  10. package/skills/editing/SKILL.md +68 -213
  11. package/skills/editing/references/common-mistakes.md +177 -0
  12. package/skills/editing/references/editing-api.md +39 -13
  13. package/skills/editing/references/editors-and-validation.md +5 -2
  14. package/skills/filtering/SKILL.md +1 -1
  15. package/skills/getting-started/SKILL.md +1 -1
  16. package/skills/grouping/SKILL.md +1 -1
  17. package/skills/options/SKILL.md +2 -2
  18. package/skills/rows/SKILL.md +1 -1
  19. package/skills/rows/references/rows-api.md +1 -1
  20. package/skills/server-side/SKILL.md +1 -1
  21. package/skills/testing/SKILL.md +3 -2
  22. package/src/tmdatagrid/components/TMDataGrid.module.css +7 -0
  23. package/src/tmdatagrid/components/TMDataGridCellEditor.tsx +20 -8
  24. package/src/tmdatagrid/components/TMDataGridEditActions.tsx +1 -1
  25. package/src/tmdatagrid/components/TMDataGridEditColumn.tsx +242 -86
  26. package/src/tmdatagrid/components/TMDataGridEntryRows.tsx +176 -77
  27. package/src/tmdatagrid/components/TMDataGridTable.module.css +47 -3
  28. package/src/tmdatagrid/components/TMDataGridTable.tsx +44 -4
  29. package/src/tmdatagrid/components/icons.ts +1 -0
  30. package/src/tmdatagrid/core/capabilities.ts +10 -0
  31. package/src/tmdatagrid/core/draftCellContext.ts +68 -0
  32. package/src/tmdatagrid/core/editEngine.ts +106 -32
  33. package/src/tmdatagrid/core/labels.ts +13 -1
  34. package/src/tmdatagrid/core/labelsSv.ts +4 -0
  35. package/src/tmdatagrid/index.ts +1 -1
  36. 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
- * One row's slot in the edit lane. Three states, driven off the edit store:
22
- * a pencil while idle, Save/Cancel while the row's form is open, a loader
23
- * while its commit is in flight. A row error turns the Save red and puts the
24
- * message in its tooltip - the pathless `.refine()` has nowhere else to land.
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
- // An entry row: ✓ adds it (not under batch, where submitAll does), ✕
48
- // discards the entry.
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
- {features.editMode !== "batch" && (
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
- <ActionIcon
67
- variant="subtle"
68
- color="gray"
69
- size="sm"
70
- tabIndex={tabIndex}
71
- aria-label={labels.discardNewRow}
72
- data-dg-part="discard-new-row"
73
- data-row-id={rowId}
74
- onClick={() => edit.cancel(rowId)}
75
- >
76
- <CloseIcon size={16} stroke={1.6} />
77
- </ActionIcon>
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 batch deletion mark is a draft like any other - restorable in place.
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
- {features.editMode === "row" && (
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.editRow}
116
- data-dg-part="edit-row"
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.begin({ rowId, columnId: null });
225
+ edit.deleteRow(rowId);
121
226
  }}
122
227
  >
123
- <PencilIcon size={16} stroke={1.6} />
228
+ <RestoreIcon size={16} stroke={1.6} />
124
229
  </ActionIcon>
125
- )}
126
- {edit.canDeleteRows() && (
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.deleteRow}
133
- data-dg-part="delete-row"
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.deleteRow(rowId);
267
+ edit.cancel(rowId);
138
268
  }}
139
269
  >
140
- <TrashIcon size={16} stroke={1.6} />
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
- save
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
- <ActionIcon
187
- variant="subtle"
188
- color="gray"
189
- size="sm"
190
- tabIndex={tabIndex}
191
- aria-label={labels.cancelRowEdit}
192
- data-dg-part="cancel-row"
193
- data-row-id={rowId}
194
- onClick={(event) => {
195
- event.stopPropagation();
196
- edit.cancel(rowId);
197
- }}
198
- >
199
- <CloseIcon size={16} stroke={1.6} />
200
- </ActionIcon>
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 under
207
- * `editing: { mode: "row" }` - the row's Save at the end of the row, mirroring the
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 Save/Cancel pair it holds while editing.
221
- size: 64,
222
- minSize: 64,
223
- maxSize: 64,
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,