@jielga/tmdatagrid 2.0.0-beta.2 → 2.0.0-beta.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (150) hide show
  1. package/README.md +5 -212
  2. package/dist/index.d.ts +1664 -632
  3. package/dist/index.js +5226 -3223
  4. package/dist/index.js.map +1 -1
  5. package/dist/styles.css +1 -1
  6. package/docs/anatomy.md +102 -0
  7. package/docs/cell-selection.md +154 -0
  8. package/docs/column-layout.md +204 -0
  9. package/docs/columns.md +262 -0
  10. package/docs/components.md +304 -0
  11. package/docs/editing.md +603 -0
  12. package/docs/editors.md +250 -0
  13. package/docs/export.md +326 -0
  14. package/docs/filtering.md +358 -0
  15. package/docs/getting-started.md +123 -0
  16. package/docs/grouping.md +165 -0
  17. package/docs/loading-and-empty.md +92 -0
  18. package/docs/localization.md +79 -0
  19. package/docs/menu.md +143 -0
  20. package/docs/pagination.md +144 -0
  21. package/docs/persistence.md +111 -0
  22. package/docs/portfolio-rebalancer.md +94 -0
  23. package/docs/query-builder.md +175 -0
  24. package/docs/quick-search.md +83 -0
  25. package/docs/row-details.md +113 -0
  26. package/docs/row-interaction.md +148 -0
  27. package/docs/row-pinning.md +132 -0
  28. package/docs/row-selection.md +134 -0
  29. package/docs/row-styling.md +133 -0
  30. package/docs/scrolling.md +111 -0
  31. package/docs/server-query.md +246 -0
  32. package/docs/server-side.md +206 -0
  33. package/docs/sorting.md +101 -0
  34. package/docs/styling.md +126 -0
  35. package/docs/summary-row.md +76 -0
  36. package/docs/testing.md +309 -0
  37. package/docs/toolbar.md +161 -0
  38. package/docs/use-tm-data-grid.md +361 -0
  39. package/package.json +21 -45
  40. package/skills/appearance/SKILL.md +70 -17
  41. package/skills/cell-selection/SKILL.md +70 -76
  42. package/skills/columns/SKILL.md +131 -32
  43. package/skills/data/SKILL.md +100 -23
  44. package/skills/editing/SKILL.md +217 -96
  45. package/skills/editing/references/common-mistakes.md +111 -24
  46. package/skills/editing/references/editing-api.md +63 -39
  47. package/skills/editing/references/editors-and-validation.md +77 -19
  48. package/skills/filtering/SKILL.md +148 -40
  49. package/skills/getting-started/SKILL.md +18 -16
  50. package/skills/grouping/SKILL.md +32 -15
  51. package/skills/options/SKILL.md +39 -9
  52. package/skills/rows/SKILL.md +22 -18
  53. package/skills/server-side/SKILL.md +170 -17
  54. package/skills/testing/SKILL.md +10 -7
  55. package/src/{tmdatagrid/TMDataGridContext.ts → TMDataGridContext.ts} +7 -19
  56. package/src/{tmdatagrid/components → components}/TMDataGrid.module.css +7 -1
  57. package/src/{tmdatagrid/components → components}/TMDataGrid.tsx +39 -23
  58. package/src/{tmdatagrid/components → components}/TMDataGridCellEditor.tsx +106 -38
  59. package/src/{tmdatagrid/components → components}/TMDataGridColumnsPanel.module.css +5 -1
  60. package/src/{tmdatagrid/components → components}/TMDataGridColumnsPanel.tsx +47 -55
  61. package/src/{tmdatagrid/components → components}/TMDataGridDetailsColumn.tsx +4 -4
  62. package/src/components/TMDataGridDraftActions.tsx +307 -0
  63. package/src/{tmdatagrid/components → components}/TMDataGridEditColumn.tsx +58 -50
  64. package/src/{tmdatagrid/components → components}/TMDataGridEntryRows.tsx +150 -115
  65. package/src/components/TMDataGridExportPicker.module.css +77 -0
  66. package/src/components/TMDataGridExportPicker.tsx +234 -0
  67. package/src/components/TMDataGridFilterPanel.module.css +54 -0
  68. package/src/components/TMDataGridFilterPanel.tsx +348 -0
  69. package/src/{tmdatagrid/components → components}/TMDataGridFilterPills.tsx +7 -5
  70. package/src/components/TMDataGridFilterSurface.module.css +54 -0
  71. package/src/components/TMDataGridFilterSurface.tsx +167 -0
  72. package/src/{tmdatagrid/components → components}/TMDataGridFooter.tsx +53 -13
  73. package/src/{tmdatagrid/components → components}/TMDataGridGroupColumn.tsx +4 -3
  74. package/src/{tmdatagrid/components → components}/TMDataGridHeaderCell.module.css +10 -0
  75. package/src/{tmdatagrid/components → components}/TMDataGridHeaderCell.tsx +100 -28
  76. package/src/components/TMDataGridHeaderFilterRow.module.css +51 -0
  77. package/src/components/TMDataGridHeaderFilterRow.tsx +301 -0
  78. package/src/components/TMDataGridMenu.tsx +354 -0
  79. package/src/{tmdatagrid/components → components}/TMDataGridSelectColumn.tsx +12 -7
  80. package/src/{tmdatagrid/components → components}/TMDataGridTable.module.css +90 -67
  81. package/src/{tmdatagrid/components → components}/TMDataGridTable.tsx +678 -156
  82. package/src/components/TMDataGridToolbar.module.css +21 -0
  83. package/src/components/TMDataGridToolbar.tsx +181 -0
  84. package/src/{tmdatagrid/components → components}/editors/TMDataGridBooleanEditor.tsx +3 -3
  85. package/src/{tmdatagrid/components → components}/editors/TMDataGridDateEditor.tsx +3 -3
  86. package/src/{tmdatagrid/components → components}/editors/TMDataGridMultiSelectEditor.tsx +3 -3
  87. package/src/components/editors/TMDataGridNumberEditor.tsx +70 -0
  88. package/src/{tmdatagrid/components → components}/editors/TMDataGridSelectEditor.tsx +3 -3
  89. package/src/{tmdatagrid/components → components}/editors/TMDataGridStringEditor.tsx +3 -3
  90. package/src/{tmdatagrid/components → components}/editors/editorShared.ts +17 -31
  91. package/src/{tmdatagrid/components → components}/filters/DgAutocompleteFilter.tsx +5 -4
  92. package/src/{tmdatagrid/components → components}/filters/DgDateRangeFilter.tsx +22 -5
  93. package/src/{tmdatagrid/components → components}/filters/DgRangeSliderFilter.tsx +5 -1
  94. package/src/{tmdatagrid/components → components}/filters/DgTriStateFilter.tsx +5 -1
  95. package/src/{tmdatagrid/components → components}/filters/TMDataGridFilterValueInput.tsx +44 -28
  96. package/src/components/filters/controlLayout.ts +32 -0
  97. package/src/components/filters/filterControlFor.ts +65 -0
  98. package/src/{tmdatagrid/components → components}/icons.ts +1 -0
  99. package/src/{tmdatagrid/components → components}/sticky.module.css +44 -0
  100. package/src/components/useHideableColumns.ts +52 -0
  101. package/src/{tmdatagrid/core → core}/autosize.ts +5 -2
  102. package/src/{tmdatagrid/core → core}/capabilities.ts +14 -6
  103. package/src/{tmdatagrid/core → core}/columnOptions.ts +46 -0
  104. package/src/{tmdatagrid/core → core}/columnOrdering.ts +33 -14
  105. package/src/{tmdatagrid/core → core}/columnUtils.ts +44 -5
  106. package/src/core/controlledState.ts +179 -0
  107. package/src/core/controlledStateSync.ts +108 -0
  108. package/src/core/deletedRows.ts +34 -0
  109. package/src/core/dom.ts +74 -0
  110. package/src/core/editEngine.ts +2476 -0
  111. package/src/{tmdatagrid/core → core}/editorFocus.ts +8 -4
  112. package/src/core/export.ts +843 -0
  113. package/src/{tmdatagrid/core → core}/filterControls.ts +38 -1
  114. package/src/{tmdatagrid/core → core}/filterOperators.ts +64 -1
  115. package/src/core/filterSurface.ts +99 -0
  116. package/src/{tmdatagrid/core → core}/labels.ts +66 -8
  117. package/src/{tmdatagrid/core → core}/labelsSv.ts +26 -3
  118. package/src/core/pageReset.ts +120 -0
  119. package/src/{tmdatagrid/core → core}/persistence.ts +22 -5
  120. package/src/core/resizePreview.ts +141 -0
  121. package/src/core/summary.ts +59 -0
  122. package/src/core/useSettledTableState.ts +36 -0
  123. package/src/{tmdatagrid/index.ts → index.ts} +75 -12
  124. package/src/{tmdatagrid/useTMDataGrid.tsx → useTMDataGrid.tsx} +734 -135
  125. package/src/useTMDataGridExport.ts +78 -0
  126. package/src/tmdatagrid/components/TMDataGridEditActions.tsx +0 -162
  127. package/src/tmdatagrid/components/TMDataGridFilterPanel.module.css +0 -35
  128. package/src/tmdatagrid/components/TMDataGridFilterPanel.tsx +0 -354
  129. package/src/tmdatagrid/components/TMDataGridToolbar.module.css +0 -12
  130. package/src/tmdatagrid/components/TMDataGridToolbar.tsx +0 -162
  131. package/src/tmdatagrid/components/editors/TMDataGridNumberEditor.tsx +0 -40
  132. package/src/tmdatagrid/core/cellExport.ts +0 -320
  133. package/src/tmdatagrid/core/editEngine.ts +0 -1006
  134. package/src/tmdatagrid/core/summary.ts +0 -35
  135. /package/src/{tmdatagrid/components → components}/TMDataGridDetailsColumn.module.css +0 -0
  136. /package/src/{tmdatagrid/components → components}/TMDataGridFilterPills.module.css +0 -0
  137. /package/src/{tmdatagrid/components → components}/TMDataGridFooter.module.css +0 -0
  138. /package/src/{tmdatagrid/components → components}/TMDataGridGroupColumn.module.css +0 -0
  139. /package/src/{tmdatagrid/components → components}/TMDataGridRowNumberColumn.tsx +0 -0
  140. /package/src/{tmdatagrid/components → components}/TMDataGridSearch.tsx +0 -0
  141. /package/src/{tmdatagrid/core → core}/cellNavigation.ts +0 -0
  142. /package/src/{tmdatagrid/core → core}/cellRange.ts +0 -0
  143. /package/src/{tmdatagrid/core → core}/draftCellContext.ts +0 -0
  144. /package/src/{tmdatagrid/core → core}/expanding.ts +0 -0
  145. /package/src/{tmdatagrid/core → core}/grouping.ts +0 -0
  146. /package/src/{tmdatagrid/core → core}/matchHighlight.ts +0 -0
  147. /package/src/{tmdatagrid/core → core}/quickSearch.ts +0 -0
  148. /package/src/{tmdatagrid/core → core}/rowPinning.ts +0 -0
  149. /package/src/{tmdatagrid/core → core}/rowSelection.ts +0 -0
  150. /package/src/{tmdatagrid/core → core}/sizes.ts +0 -0
@@ -0,0 +1,307 @@
1
+ import { Button, Group, Text } from "@mantine/core";
2
+ import { useSelector } from "@tanstack/react-store";
3
+ import { shallow } from "@tanstack/store";
4
+ import type { ReactNode } from "react";
5
+ import { useTMDataGridContext } from "../TMDataGridContext";
6
+ import { getOpenRowIds } from "../core/editEngine";
7
+ import { getDisplayedRows } from "../core/rowSelection";
8
+ import type {
9
+ TMDataGridScrollAlign,
10
+ TMDataGridScrollToRowArgs,
11
+ } from "../useTMDataGrid";
12
+
13
+ /**
14
+ * How many rows the draft store holds - parked edits, parked entry rows and
15
+ * deletion marks. This is what Save sends, so it is what Save counts: a row
16
+ * the user is still typing into is not in here.
17
+ */
18
+ function useDraftCount(): number {
19
+ const { edit } = useTMDataGridContext();
20
+ return useSelector(edit.store, (state) => {
21
+ const deleted = new Set(state.deletedRowIds);
22
+ // An edited row that was then marked counts once: Save sends the
23
+ // deletion and leaves the edit behind.
24
+ const edited =
25
+ deleted.size === 0
26
+ ? state.committedRowIds.length
27
+ : state.committedRowIds.filter((rowId) => !deleted.has(rowId)).length;
28
+ return (
29
+ edited +
30
+ state.newRows.filter((newRow) => newRow.committed).length +
31
+ deleted.size
32
+ );
33
+ });
34
+ }
35
+
36
+ /**
37
+ * How many rows are still open - edited or entered but not parked, so not
38
+ * part of the save. Surfaced beside Save so an undecided row is visible
39
+ * rather than silently left behind.
40
+ */
41
+ function useOpenCount(): number {
42
+ const { edit } = useTMDataGridContext();
43
+ return useSelector(edit.store, (state) => getOpenRowIds(state).length);
44
+ }
45
+
46
+ /**
47
+ * The ids behind {@link useOpenCount}. Shallow-compared: the array would
48
+ * otherwise be a new one on every edit-store publish, and it is handed to
49
+ * `renderActions`, where it may well end up in a dependency array.
50
+ */
51
+ function useOpenRowIds(): ReadonlyArray<string> {
52
+ const { edit } = useTMDataGridContext();
53
+ return useSelector(edit.store, getOpenRowIds, { compare: shallow });
54
+ }
55
+
56
+ /*
57
+ * Module scope, like the pagination controls and for the same reason: a
58
+ * component defined inside the render body is a new type every render, so
59
+ * React would remount the buttons underneath an interaction.
60
+ */
61
+
62
+ function EditSaveButton() {
63
+ const { edit, labels } = useTMDataGridContext();
64
+ const draftCount = useDraftCount();
65
+ // Save sends the draft store, so its loading state is the save in flight,
66
+ // not an open row's submit.
67
+ const isSaving = useSelector(edit.store, (state) => state.isSaving);
68
+
69
+ return (
70
+ <Button
71
+ size="compact-sm"
72
+ disabled={draftCount === 0}
73
+ loading={isSaving}
74
+ data-dg-part="save-all"
75
+ data-draft-count={draftCount}
76
+ onClick={() => void edit.saveDrafts()}
77
+ >
78
+ {labels.saveAllEdits(draftCount)}
79
+ </Button>
80
+ );
81
+ }
82
+
83
+ /** The count of rows left open, or nothing while every row is decided. */
84
+ function EditOpenRowsNote() {
85
+ const { labels } = useTMDataGridContext();
86
+ const openCount = useOpenCount();
87
+ if (openCount === 0) return null;
88
+ return (
89
+ <Text
90
+ size="xs"
91
+ c="dimmed"
92
+ data-dg-part="open-rows-note"
93
+ data-open-count={openCount}
94
+ >
95
+ {labels.editRowsStillOpen(openCount)}
96
+ </Text>
97
+ );
98
+ }
99
+
100
+ function EditDiscardButton() {
101
+ const { edit, labels } = useTMDataGridContext();
102
+ // Discard drops everything the grid is holding, open rows included, so it
103
+ // stays live while anything is uncommitted.
104
+ const pendingCount = useDraftCount() + useOpenCount();
105
+
106
+ return (
107
+ <Button
108
+ variant="subtle"
109
+ color="gray"
110
+ size="compact-sm"
111
+ disabled={pendingCount === 0}
112
+ data-dg-part="discard-all"
113
+ onClick={() => edit.cancelAll()}
114
+ >
115
+ {labels.discardAllEdits}
116
+ </Button>
117
+ );
118
+ }
119
+
120
+ const EDIT_ACTIONS_CONTROLS: TMDataGridDraftActionsControls = {
121
+ Save: EditSaveButton,
122
+ Discard: EditDiscardButton,
123
+ OpenRowsNote: EditOpenRowsNote,
124
+ };
125
+
126
+ /** What the edit chrome is showing. */
127
+ export type TMDataGridDraftActionsState = {
128
+ /**
129
+ * Rows in the draft store, which is what Save sends: committed edits,
130
+ * committed entry rows and deletion marks.
131
+ */
132
+ draftCount: number;
133
+ /**
134
+ * Rows still open - edited or entered but not committed, so not part of
135
+ * the save. They stay open across a save.
136
+ */
137
+ openCount: number;
138
+ /**
139
+ * The ids behind {@link openCount}, in the order the grid opened them - a
140
+ * sort, a filter or a page never moves this list. An entered row appears as
141
+ * its `tempId`.
142
+ *
143
+ * Narrower than `edit.state.openRowIds`, which is every row holding a form,
144
+ * the parked ones included. And note the ordering:
145
+ * {@link TMDataGridDraftActionsActions.scrollToFirstOpenRow} takes "first"
146
+ * in display order, so it need not be `openRowIds[0]`.
147
+ */
148
+ openRowIds: ReadonlyArray<string>;
149
+ /**
150
+ * @deprecated Was "everything uncommitted", which Save no longer sends.
151
+ * Reads as `draftCount + openCount`; use whichever you meant.
152
+ */
153
+ pendingCount: number;
154
+ /** Whether a submit is in flight - any open row is submitting. */
155
+ isSubmitting: boolean;
156
+ /** `true` while `saveDrafts` is in flight. */
157
+ isSaving: boolean;
158
+ };
159
+
160
+ /** What the edit chrome can do. */
161
+ export type TMDataGridDraftActionsActions = {
162
+ /** Saves the draft store. Open rows are left alone. */
163
+ save: () => Promise<boolean>;
164
+ /** Submits every open row, committing the ones that validate. */
165
+ commitAll: () => Promise<boolean>;
166
+ /** Drops everything - open form state and the draft store alike. */
167
+ discard: () => void;
168
+ /**
169
+ * `grid.scrollToRow`, so a control in here can take the user to a row
170
+ * without the grid being threaded down to it.
171
+ */
172
+ scrollToRow: (args: TMDataGridScrollToRowArgs) => boolean;
173
+ /**
174
+ * Scrolls to the first row still open, taking "first" in display order: the
175
+ * topmost open row under the current sort, filter and page. That need not
176
+ * be {@link TMDataGridDraftActionsState.openRowIds}`[0]`, which is the
177
+ * order the grid opened them in.
178
+ *
179
+ * Answers whether an open row could be reached. `false` when nothing is
180
+ * open, and when every open row is filtered out, on another page or
181
+ * collapsed inside a group. An open entry row answers `true` without
182
+ * scrolling - it is sticky under the header, so it is on screen already -
183
+ * and so does an open row pinned to an edge. The scroll goes through
184
+ * `scrollToRow`, so before `TMDataGrid.Table` has mounted there is nothing
185
+ * to scroll and the answer is `false`.
186
+ */
187
+ scrollToFirstOpenRow: (align?: TMDataGridScrollAlign) => boolean;
188
+ };
189
+
190
+ /** The pre-bound pieces of the built-in edit chrome. */
191
+ export type TMDataGridDraftActionsControls = {
192
+ /** Save, with the draft count, disabled while the draft store is empty. */
193
+ Save: () => ReactNode;
194
+ /** Discard, disabled while nothing is pending. */
195
+ Discard: () => ReactNode;
196
+ /** The "N rows still being edited" note, or nothing while there are none. */
197
+ OpenRowsNote: () => ReactNode;
198
+ };
199
+
200
+ /** What {@link TMDataGridDraftActionsProps.renderActions} is handed. */
201
+ export type TMDataGridDraftActionsSlotArgs = {
202
+ state: TMDataGridDraftActionsState;
203
+ actions: TMDataGridDraftActionsActions;
204
+ Controls: TMDataGridDraftActionsControls;
205
+ };
206
+
207
+ export type TMDataGridDraftActionsProps = {
208
+ /**
209
+ * Replaces the built-in Save/Discard pair, and is handed the pieces of it.
210
+ *
211
+ * ```tsx
212
+ * <TMDataGrid.DraftActions
213
+ * renderActions={({ state, Controls }) => (
214
+ * <Group>
215
+ * {state.draftCount > 0 && <Badge>{state.draftCount}</Badge>}
216
+ * <Controls.OpenRowsNote />
217
+ * <Controls.Save />
218
+ * <Controls.Discard />
219
+ * </Group>
220
+ * )}
221
+ * />
222
+ * ```
223
+ */
224
+ renderActions?: (args: TMDataGridDraftActionsSlotArgs) => ReactNode;
225
+ };
226
+
227
+ /**
228
+ * The draft store's toolbar chrome: Save with the store's count, Discard, and
229
+ * a note counting the rows still open. Save sends the store and leaves open
230
+ * rows alone, so it greys out while nothing is parked however much is being
231
+ * typed - the note is what makes those rows visible.
232
+ *
233
+ * Works under any `editing.mode`. The toolbar is declarative: the grid does
234
+ * not decide for you, so include this only when the grid runs a draft store -
235
+ * without `editing.draft` there is nothing to save and Save stays disabled.
236
+ *
237
+ * ```tsx
238
+ * <TMDataGrid.Toolbar>
239
+ * <TMDataGrid.SummaryCount />
240
+ * <TMDataGrid.Spacer />
241
+ * <TMDataGrid.DraftActions />
242
+ * </TMDataGrid.Toolbar>
243
+ * ```
244
+ */
245
+ export function TMDataGridDraftActions({
246
+ renderActions,
247
+ }: TMDataGridDraftActionsProps = {}) {
248
+ const { edit, features, table, scrollToRow } = useTMDataGridContext();
249
+ const draftCount = useDraftCount();
250
+ const openRowIds = useOpenRowIds();
251
+ const isSubmitting = useSelector(edit.store, (state) =>
252
+ state.openRowIds.some((rowId) => state.rows[rowId]?.isSubmitting === true),
253
+ );
254
+ const isSaving = useSelector(edit.store, (state) => state.isSaving);
255
+
256
+ if (!features.editing) return null;
257
+
258
+ if (renderActions) {
259
+ return renderActions({
260
+ state: {
261
+ draftCount,
262
+ openCount: openRowIds.length,
263
+ openRowIds,
264
+ pendingCount: draftCount + openRowIds.length,
265
+ isSubmitting,
266
+ isSaving,
267
+ },
268
+ actions: {
269
+ save: () => edit.saveDrafts(),
270
+ commitAll: () => edit.commitAll(),
271
+ discard: () => edit.cancelAll(),
272
+ scrollToRow,
273
+ scrollToFirstOpenRow: (align) => {
274
+ // Read through the store rather than closing over this render's
275
+ // arrays: display order is only decided here, at the click.
276
+ const state = edit.state;
277
+ const openIds = getOpenRowIds(state);
278
+ if (openIds.length === 0) return false;
279
+ const open = new Set(openIds);
280
+ const first = getDisplayedRows(table, features).find((row) =>
281
+ open.has(row.id),
282
+ );
283
+ if (first !== undefined) {
284
+ return scrollToRow({ rowId: first.id, align });
285
+ }
286
+ // Nothing left in the scrolling order, which does not mean nothing
287
+ // is visible: an entry row is sticky under the header, and a pinned
288
+ // row is parked at an edge - `scrollToRow` answers for that one.
289
+ return openIds.some(
290
+ (rowId) =>
291
+ state.newRows.some((newRow) => newRow.tempId === rowId) ||
292
+ scrollToRow({ rowId, align }),
293
+ );
294
+ },
295
+ },
296
+ Controls: EDIT_ACTIONS_CONTROLS,
297
+ });
298
+ }
299
+
300
+ return (
301
+ <Group gap="xs" wrap="nowrap">
302
+ <EditOpenRowsNote />
303
+ <EditSaveButton />
304
+ <EditDiscardButton />
305
+ </Group>
306
+ );
307
+ }
@@ -3,11 +3,11 @@ import { useSelector } from "@tanstack/react-store";
3
3
  import type { ColumnDef, Row, RowData } from "@tanstack/react-table";
4
4
  import classes from "./TMDataGridTable.module.css";
5
5
  import {
6
- useCellControlTabIndex,
6
+ useBodyControlTabIndex,
7
7
  useTMDataGridContext,
8
8
  } from "../TMDataGridContext";
9
9
  import type { TMDataGridFeatures } from "../useTMDataGrid";
10
- import { firstErrorText } from "./editors/editorShared";
10
+ import { firstErrorText } from "../core/editEngine";
11
11
  import {
12
12
  CheckIcon,
13
13
  CloseIcon,
@@ -20,10 +20,10 @@ import {
20
20
  export const EDIT_COLUMN_ID = "__edit__";
21
21
 
22
22
  /**
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.
23
+ * The draft store's change marker in the lane - what kind of change the row
24
+ * is holding for the save. Non-interactive; the buttons beside it act.
25
+ * `data-state` is the styling hook, `data-error` flips it red and puts the
26
+ * message in the tooltip in place of the state's name.
27
27
  */
28
28
  function RowStateIndicator({
29
29
  state,
@@ -56,13 +56,23 @@ function RowStateIndicator({
56
56
  }
57
57
 
58
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.
59
+ * One row's slot in the edit lane, driven off the edit store. Two things
60
+ * decide what it holds, and they are the two axes of `editing`:
61
+ *
62
+ * | Row is | Lane holds | From |
63
+ * | ------ | ---------- | ---- |
64
+ * | parked in the draft store | the change marker and the way back out - revert, discard or restore | `editing.draft` |
65
+ * | open | the mode's own controls: Save and Cancel | `editing.mode` |
66
+ * | neither | the pencil (row mode) and the trash | `editing.mode` |
67
+ *
68
+ * A row error turns the Save - or an entry row's ✓ - red with the message in
69
+ * its tooltip: a pathless `.refine()` has nowhere else to land, and a field
70
+ * error outlives the editor that found it. A parked row never offers a save:
71
+ * it has had its submit, and `TMDataGrid.DraftActions` is what sends it.
72
+ *
73
+ * The lane's controls take the tab index every body control does - see
74
+ * useBodyControlTabIndex. The open row's save and cancel are the exception,
75
+ * see where they are built.
66
76
  */
67
77
  function EditLaneCell<TData extends RowData>({
68
78
  row,
@@ -70,7 +80,7 @@ function EditLaneCell<TData extends RowData>({
70
80
  row: Row<TMDataGridFeatures, TData>;
71
81
  }) {
72
82
  const { edit, features, labels } = useTMDataGridContext();
73
- const tabIndex = useCellControlTabIndex();
83
+ const tabIndex = useBodyControlTabIndex();
74
84
  const rowId = row.id;
75
85
  const isOpen = useSelector(edit.store, (state) =>
76
86
  state.openRowIds.includes(rowId),
@@ -82,21 +92,28 @@ function EditLaneCell<TData extends RowData>({
82
92
  const isConfirmedNew = useSelector(
83
93
  edit.store,
84
94
  (state) =>
85
- state.newRows.find((newRow) => newRow.tempId === rowId)?.confirmed ===
95
+ state.newRows.find((newRow) => newRow.tempId === rowId)?.committed ===
86
96
  true,
87
97
  );
98
+ // Parked: the row passed its submit and is waiting for the save. A subset
99
+ // of the open rows, so it is asked about first - the two overlap.
100
+ const isCommitted = useSelector(edit.store, (state) =>
101
+ state.committedRowIds.includes(rowId),
102
+ );
88
103
  const isMarkedDeleted = useSelector(edit.store, (state) =>
89
104
  state.deletedRowIds.includes(rowId),
90
105
  );
91
106
 
92
107
  if (row.getIsGrouped()) return null;
93
108
 
94
- const isDraftMode = features.editMode === "draft";
95
109
  const hasErrors =
96
110
  projection !== undefined &&
97
111
  (projection.hasRowError || projection.errorFields.length > 0);
112
+ // The row's own message first - a pathless issue is about the row - then
113
+ // the first field's, which is what a failed commit left behind.
98
114
  const rowError = hasErrors
99
115
  ? (firstErrorText(edit.getForm(rowId)?.state.errors ?? []) ??
116
+ projection.errorMessages[0]?.message ??
100
117
  labels.editRowErrors)
101
118
  : null;
102
119
 
@@ -120,9 +137,9 @@ function EditLaneCell<TData extends RowData>({
120
137
  </Tooltip>
121
138
  ) : null;
122
139
 
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.
140
+ // An entry row: ✓ enters it - an add, or a park under `editing.draft` -
141
+ // and ✕ discards the entry. A parked entry waits for the save as a value
142
+ // row: marked new, re-openable, still discardable.
126
143
  if (isNew) {
127
144
  if (isConfirmedNew) {
128
145
  return (
@@ -169,10 +186,10 @@ function EditLaneCell<TData extends RowData>({
169
186
  }
170
187
  return (
171
188
  <Group gap={0} wrap="nowrap">
172
- <Tooltip label={labels.confirmNewRow} withArrow>
189
+ <Tooltip label={rowError ?? labels.confirmNewRow} withArrow>
173
190
  <ActionIcon
174
191
  variant="subtle"
175
- color="green"
192
+ color={hasErrors ? "red" : "green"}
176
193
  size="sm"
177
194
  tabIndex={tabIndex}
178
195
  aria-label={labels.confirmNewRow}
@@ -201,7 +218,7 @@ function EditLaneCell<TData extends RowData>({
201
218
  );
202
219
  }
203
220
 
204
- // A draft-mode deletion mark is a draft like any other - restorable in place.
221
+ // A deletion mark is a draft like any other - restorable in place.
205
222
  if (isMarkedDeleted) {
206
223
  return (
207
224
  <Group gap={0} wrap="nowrap">
@@ -222,7 +239,7 @@ function EditLaneCell<TData extends RowData>({
222
239
  data-row-id={rowId}
223
240
  onClick={(event) => {
224
241
  event.stopPropagation();
225
- edit.deleteRow(rowId);
242
+ edit.restoreRow(rowId);
226
243
  }}
227
244
  >
228
245
  <RestoreIcon size={16} stroke={1.6} />
@@ -235,16 +252,14 @@ function EditLaneCell<TData extends RowData>({
235
252
  // The pencil means "this row edits"; a row that does not gets nothing.
236
253
  if (!edit.canEditRow(row as never)) return null;
237
254
 
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;
255
+ if (projection?.isSubmitting) {
256
+ return <Loader size="xs" aria-label={labels.loading} />;
257
+ }
258
+
259
+ // Parked in the draft store: the mark, and the way back out. No trash
260
+ // beside the revert - the two undo-shaped icons were too easy to mistake
261
+ // for one another, so a drafted row reverts first, then deletes.
262
+ if (isCommitted) {
248
263
  return (
249
264
  <Group gap={0} wrap="nowrap">
250
265
  <RowStateIndicator
@@ -301,16 +316,15 @@ function EditLaneCell<TData extends RowData>({
301
316
  );
302
317
  }
303
318
 
304
- if (projection?.isSubmitting) {
305
- return <Loader size="xs" aria-label={labels.loading} />;
306
- }
307
-
319
+ // An open row is a form, and its save and cancel are the form's last two
320
+ // fields: Tab walks into them from the last editor. The closed lane's
321
+ // controls stay out of the tab order like every other control in a cell.
308
322
  const save = (
309
323
  <ActionIcon
310
324
  variant="subtle"
311
325
  color={hasErrors ? "red" : "green"}
312
326
  size="sm"
313
- tabIndex={tabIndex}
327
+ tabIndex={0}
314
328
  aria-label={labels.saveRow}
315
329
  data-dg-part="save-row"
316
330
  data-row-id={rowId}
@@ -325,21 +339,15 @@ function EditLaneCell<TData extends RowData>({
325
339
 
326
340
  return (
327
341
  <Group gap={0} wrap="nowrap">
328
- {rowError === null ? (
329
- <Tooltip label={labels.saveRow} withArrow>
330
- {save}
331
- </Tooltip>
332
- ) : (
333
- <Tooltip label={rowError} withArrow>
334
- {save}
335
- </Tooltip>
336
- )}
342
+ <Tooltip label={rowError ?? labels.saveRow} withArrow>
343
+ {save}
344
+ </Tooltip>
337
345
  <Tooltip label={labels.cancelRowEdit} withArrow>
338
346
  <ActionIcon
339
347
  variant="subtle"
340
348
  color="gray"
341
349
  size="sm"
342
- tabIndex={tabIndex}
350
+ tabIndex={0}
343
351
  aria-label={labels.cancelRowEdit}
344
352
  data-dg-part="cancel-row"
345
353
  data-row-id={rowId}
@@ -358,11 +366,11 @@ function EditLaneCell<TData extends RowData>({
358
366
  /**
359
367
  * The generated edit lane, appended and pinned right - the row's Save at the
360
368
  * 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.
369
+ * `editing.draft`, mirroring the checkbox lane's build on the left.
362
370
  */
363
371
  export function createEditColumn<TData extends RowData>(
364
372
  label = "Edit",
365
- /** Draft mode holds three controls where the others hold two. */
373
+ /** A draft lane holds three controls where the rest hold two. */
366
374
  wide = false,
367
375
  ): ColumnDef<TMDataGridFeatures, TData, unknown> {
368
376
  const width = wide ? 88 : 64;