@jielga/tmdatagrid 2.0.0-beta.9 → 2.0.1

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 (157) hide show
  1. package/README.md +5 -212
  2. package/dist/index.d.ts +1281 -768
  3. package/dist/index.js +4607 -3250
  4. package/dist/index.js.map +1 -1
  5. package/dist/styles.css +1 -1
  6. package/docs/adding-rows.md +132 -0
  7. package/docs/anatomy.md +119 -0
  8. package/docs/card-view.md +108 -0
  9. package/docs/cell-selection.md +194 -0
  10. package/docs/column-layout.md +182 -0
  11. package/docs/column-menu.md +66 -0
  12. package/docs/columns.md +269 -0
  13. package/docs/components.md +311 -0
  14. package/docs/draft-store.md +242 -0
  15. package/docs/editing.md +303 -0
  16. package/docs/editors.md +250 -0
  17. package/docs/export.md +319 -0
  18. package/docs/filtering.md +362 -0
  19. package/docs/getting-started.md +123 -0
  20. package/docs/grouping.md +165 -0
  21. package/docs/loading-and-empty.md +92 -0
  22. package/docs/localization.md +79 -0
  23. package/docs/menu.md +143 -0
  24. package/docs/migrating-to-2.md +163 -0
  25. package/docs/pagination.md +144 -0
  26. package/docs/persistence.md +114 -0
  27. package/docs/portfolio-rebalancer.md +94 -0
  28. package/docs/query-builder.md +179 -0
  29. package/docs/quick-search.md +84 -0
  30. package/docs/row-details.md +115 -0
  31. package/docs/row-interaction.md +149 -0
  32. package/docs/row-pinning.md +132 -0
  33. package/docs/row-selection.md +136 -0
  34. package/docs/row-styling.md +133 -0
  35. package/docs/scrolling.md +112 -0
  36. package/docs/server-query.md +246 -0
  37. package/docs/server-side.md +206 -0
  38. package/docs/sorting.md +101 -0
  39. package/docs/styling.md +126 -0
  40. package/docs/summary-row.md +76 -0
  41. package/docs/testing.md +744 -0
  42. package/docs/toolbar.md +161 -0
  43. package/docs/use-tm-data-grid.md +361 -0
  44. package/package.json +22 -46
  45. package/skills/appearance/SKILL.md +72 -19
  46. package/skills/cell-selection/SKILL.md +49 -48
  47. package/skills/columns/SKILL.md +125 -70
  48. package/skills/columns/references/columns-api.md +59 -0
  49. package/skills/data/SKILL.md +112 -18
  50. package/skills/editing/SKILL.md +76 -42
  51. package/skills/editing/references/common-mistakes.md +77 -69
  52. package/skills/editing/references/editing-api.md +25 -20
  53. package/skills/editing/references/editors-and-validation.md +80 -18
  54. package/skills/filtering/SKILL.md +155 -41
  55. package/skills/getting-started/SKILL.md +116 -16
  56. package/skills/grouping/SKILL.md +31 -16
  57. package/skills/migrating-to-2/SKILL.md +244 -0
  58. package/skills/options/SKILL.md +24 -12
  59. package/skills/rows/SKILL.md +22 -18
  60. package/skills/rows/references/rows-api.md +10 -6
  61. package/skills/server-side/SKILL.md +170 -17
  62. package/skills/testing/SKILL.md +150 -32
  63. package/skills/testing-components/SKILL.md +230 -0
  64. package/skills/testing-editing/SKILL.md +240 -0
  65. package/src/{tmdatagrid/components → components}/TMDataGrid.tsx +38 -21
  66. package/src/{tmdatagrid/components → components}/TMDataGridCellEditor.tsx +54 -8
  67. package/src/{tmdatagrid/components → components}/TMDataGridColumnsPanel.module.css +5 -1
  68. package/src/{tmdatagrid/components → components}/TMDataGridColumnsPanel.tsx +47 -55
  69. package/src/{tmdatagrid/components → components}/TMDataGridDetailsColumn.tsx +5 -51
  70. package/src/{tmdatagrid/components → components}/TMDataGridDraftActions.tsx +41 -24
  71. package/src/{tmdatagrid/components → components}/TMDataGridEditColumn.tsx +12 -59
  72. package/src/{tmdatagrid/components → components}/TMDataGridEntryRows.tsx +164 -92
  73. package/src/components/TMDataGridExportPicker.module.css +77 -0
  74. package/src/components/TMDataGridExportPicker.tsx +234 -0
  75. package/src/components/TMDataGridFilterPanel.module.css +54 -0
  76. package/src/components/TMDataGridFilterPanel.tsx +348 -0
  77. package/src/{tmdatagrid/components → components}/TMDataGridFilterPills.tsx +15 -8
  78. package/src/components/TMDataGridFilterSurface.module.css +54 -0
  79. package/src/components/TMDataGridFilterSurface.tsx +167 -0
  80. package/src/{tmdatagrid/components → components}/TMDataGridFooter.tsx +53 -90
  81. package/src/{tmdatagrid/components → components}/TMDataGridGroupColumn.tsx +5 -69
  82. package/src/{tmdatagrid/components → components}/TMDataGridHeaderCell.module.css +12 -2
  83. package/src/{tmdatagrid/components → components}/TMDataGridHeaderCell.tsx +58 -21
  84. package/src/components/TMDataGridHeaderFilterRow.module.css +51 -0
  85. package/src/components/TMDataGridHeaderFilterRow.tsx +301 -0
  86. package/src/components/TMDataGridMenu.tsx +357 -0
  87. package/src/{tmdatagrid/components → components}/TMDataGridSelectColumn.tsx +11 -48
  88. package/src/{tmdatagrid/components → components}/TMDataGridTable.module.css +69 -56
  89. package/src/{tmdatagrid/components → components}/TMDataGridTable.tsx +240 -138
  90. package/src/{tmdatagrid/components → components}/TMDataGridToolbar.module.css +5 -0
  91. package/src/components/TMDataGridToolbar.tsx +181 -0
  92. package/src/{tmdatagrid/components → components}/editors/TMDataGridBooleanEditor.tsx +3 -3
  93. package/src/{tmdatagrid/components → components}/editors/TMDataGridDateEditor.tsx +3 -3
  94. package/src/{tmdatagrid/components → components}/editors/TMDataGridMultiSelectEditor.tsx +3 -3
  95. package/src/{tmdatagrid/components → components}/editors/TMDataGridNumberEditor.tsx +3 -3
  96. package/src/{tmdatagrid/components → components}/editors/TMDataGridSelectEditor.tsx +3 -3
  97. package/src/{tmdatagrid/components → components}/editors/TMDataGridStringEditor.tsx +3 -3
  98. package/src/{tmdatagrid/components → components}/editors/editorShared.ts +16 -2
  99. package/src/{tmdatagrid/components → components}/filters/DgAutocompleteFilter.tsx +5 -4
  100. package/src/{tmdatagrid/components → components}/filters/DgDateRangeFilter.tsx +22 -5
  101. package/src/{tmdatagrid/components → components}/filters/DgRangeSliderFilter.tsx +5 -1
  102. package/src/{tmdatagrid/components → components}/filters/DgTriStateFilter.tsx +5 -1
  103. package/src/{tmdatagrid/components → components}/filters/TMDataGridFilterValueInput.tsx +44 -28
  104. package/src/components/filters/controlLayout.ts +32 -0
  105. package/src/components/filters/filterControlFor.ts +65 -0
  106. package/src/components/generatedColumns.tsx +187 -0
  107. package/src/{tmdatagrid/components → components}/icons.ts +1 -0
  108. package/src/{tmdatagrid/components → components}/sticky.module.css +44 -0
  109. package/src/components/useHideableColumns.ts +52 -0
  110. package/src/{tmdatagrid/core → core}/autosize.ts +5 -2
  111. package/src/{tmdatagrid/core → core}/columnOptions.ts +60 -8
  112. package/src/{tmdatagrid/core → core}/columnOrdering.ts +33 -14
  113. package/src/{tmdatagrid/core → core}/columnUtils.ts +44 -5
  114. package/src/core/controlledStateSync.ts +108 -0
  115. package/src/core/deletedRows.ts +34 -0
  116. package/src/core/dom.ts +74 -0
  117. package/src/{tmdatagrid/core → core}/editEngine.ts +1107 -460
  118. package/src/core/export.ts +704 -0
  119. package/src/{tmdatagrid/core → core}/filterControls.ts +38 -1
  120. package/src/{tmdatagrid/core → core}/filterOperators.ts +64 -1
  121. package/src/core/filterSurface.ts +99 -0
  122. package/src/{tmdatagrid/core → core}/grouping.ts +21 -0
  123. package/src/{tmdatagrid/core → core}/labels.ts +51 -6
  124. package/src/{tmdatagrid/core → core}/labelsSv.ts +27 -6
  125. package/src/core/pageReset.ts +120 -0
  126. package/src/core/pagination.ts +81 -0
  127. package/src/{tmdatagrid/core → core}/persistence.ts +22 -5
  128. package/src/{tmdatagrid/core → core}/summary.ts +20 -4
  129. package/src/{tmdatagrid/index.ts → index.ts} +69 -35
  130. package/src/{tmdatagrid/useTMDataGrid.tsx → useTMDataGrid.tsx} +428 -109
  131. package/src/useTMDataGridExport.ts +78 -0
  132. package/src/tmdatagrid/components/TMDataGridFilterPanel.module.css +0 -35
  133. package/src/tmdatagrid/components/TMDataGridFilterPanel.tsx +0 -354
  134. package/src/tmdatagrid/components/TMDataGridToolbar.tsx +0 -161
  135. package/src/tmdatagrid/core/cellExport.ts +0 -320
  136. /package/src/{tmdatagrid/TMDataGridContext.ts → TMDataGridContext.ts} +0 -0
  137. /package/src/{tmdatagrid/components → components}/TMDataGrid.module.css +0 -0
  138. /package/src/{tmdatagrid/components → components}/TMDataGridDetailsColumn.module.css +0 -0
  139. /package/src/{tmdatagrid/components → components}/TMDataGridFilterPills.module.css +0 -0
  140. /package/src/{tmdatagrid/components → components}/TMDataGridFooter.module.css +0 -0
  141. /package/src/{tmdatagrid/components → components}/TMDataGridGroupColumn.module.css +0 -0
  142. /package/src/{tmdatagrid/components → components}/TMDataGridRowNumberColumn.tsx +0 -0
  143. /package/src/{tmdatagrid/components → components}/TMDataGridSearch.tsx +0 -0
  144. /package/src/{tmdatagrid/core → core}/capabilities.ts +0 -0
  145. /package/src/{tmdatagrid/core → core}/cellNavigation.ts +0 -0
  146. /package/src/{tmdatagrid/core → core}/cellRange.ts +0 -0
  147. /package/src/{tmdatagrid/core → core}/controlledState.ts +0 -0
  148. /package/src/{tmdatagrid/core → core}/draftCellContext.ts +0 -0
  149. /package/src/{tmdatagrid/core → core}/editorFocus.ts +0 -0
  150. /package/src/{tmdatagrid/core → core}/expanding.ts +0 -0
  151. /package/src/{tmdatagrid/core → core}/matchHighlight.ts +0 -0
  152. /package/src/{tmdatagrid/core → core}/quickSearch.ts +0 -0
  153. /package/src/{tmdatagrid/core → core}/resizePreview.ts +0 -0
  154. /package/src/{tmdatagrid/core → core}/rowPinning.ts +0 -0
  155. /package/src/{tmdatagrid/core → core}/rowSelection.ts +0 -0
  156. /package/src/{tmdatagrid/core → core}/sizes.ts +0 -0
  157. /package/src/{tmdatagrid/core → core}/useSettledTableState.ts +0 -0
@@ -1,4 +1,4 @@
1
- import { ActionIcon } from "@mantine/core";
1
+ import { ActionIcon, Tooltip } from "@mantine/core";
2
2
  import { FieldApi } from "@tanstack/react-form";
3
3
  import type { Cell, Row } from "@tanstack/react-table";
4
4
  import {
@@ -11,6 +11,7 @@ import {
11
11
  } from "react";
12
12
  import classes from "./TMDataGridTable.module.css";
13
13
  import { type TMDataGridRowData, useTMDataGridContext } from "../TMDataGridContext";
14
+ import { isButtonElement, isInputElement, isNode } from "../core/dom";
14
15
  import {
15
16
  getEditFieldName,
16
17
  normalizeFieldValidate,
@@ -27,6 +28,7 @@ import { TMDataGridMultiSelectEditor } from "./editors/TMDataGridMultiSelectEdit
27
28
  import { TMDataGridNumberEditor } from "./editors/TMDataGridNumberEditor";
28
29
  import { TMDataGridSelectEditor } from "./editors/TMDataGridSelectEditor";
29
30
  import { TMDataGridStringEditor } from "./editors/TMDataGridStringEditor";
31
+ import { useFieldError } from "./editors/editorShared";
30
32
 
31
33
  const BUILT_IN_EDITORS: Record<
32
34
  string,
@@ -138,6 +140,17 @@ export function TMDataGridCellEditor({
138
140
  });
139
141
  useEffect(() => field.mount(), [field]);
140
142
 
143
+ // The field's message, read here rather than in each editor: the tooltip
144
+ // below is the host's, so a column's own `meta.edit.editor` gets it too.
145
+ const error = useFieldError(field);
146
+ // When that tooltip is up, held here rather than left to Tooltip's own
147
+ // `events`: those bring floating-ui's dismiss interaction with them, which
148
+ // puts an `onKeyDown` on the anchor and stops Escape inside the editor
149
+ // before the host's handler below can cancel on it. A controlled `opened`
150
+ // turns the interaction off, so hover and focus are tracked here instead.
151
+ const [messageHovered, setMessageHovered] = useState(false);
152
+ const [messageFocused, setMessageFocused] = useState(false);
153
+
141
154
  // Type-to-edit: the seed replaces the value, as it does in a spreadsheet.
142
155
  // Only for the types where keystrokes are the input.
143
156
  useEffect(() => {
@@ -190,7 +203,7 @@ export function TMDataGridCellEditor({
190
203
  // Enter on the ✓ or ✕ is the button's own press, and does what a click on
191
204
  // it does: commit and stay, or cancel. Left to the branch below it would
192
205
  // commit and move down, and the same button would mean two things.
193
- if (event.key === "Enter" && event.target instanceof HTMLButtonElement) {
206
+ if (event.key === "Enter" && isButtonElement(event.target)) {
194
207
  event.stopPropagation();
195
208
  return;
196
209
  }
@@ -220,7 +233,7 @@ export function TMDataGridCellEditor({
220
233
  const next = tabbables[index + (event.shiftKey ? -1 : 1)];
221
234
  if (index >= 0 && next !== undefined) {
222
235
  next.focus();
223
- if (next instanceof HTMLInputElement) next.select();
236
+ if (isInputElement(next)) next.select();
224
237
  return;
225
238
  }
226
239
  deferAndClose(event.shiftKey ? "defer-shift-tab" : "defer-tab");
@@ -234,8 +247,12 @@ export function TMDataGridCellEditor({
234
247
  * Focus left the editor entirely - a click somewhere else. Under `"cell"`
235
248
  * that commits (Sheets): a row the user has walked away from is a row they
236
249
  * are done typing into, and where the commit lands is `editing.draft`'s
237
- * business. A commit blocked by validation keeps the form and its invalid
238
- * marker, with the editor closed.
250
+ * business. A refused commit - validation, or `onCommit` rejecting - leaves
251
+ * the editor where it is, invalid, with the value still in it: closed, the
252
+ * cell would render the refused value as if it had landed, and the message
253
+ * would go with the editor. The engine holds the same line when another
254
+ * row is opened over a refused one (see `begin`). The user fixes the value,
255
+ * or Escapes.
239
256
  */
240
257
  const handleBlur = (event: FocusEvent<HTMLDivElement>) => {
241
258
  if (closingRef.current) return;
@@ -244,7 +261,7 @@ export function TMDataGridCellEditor({
244
261
  // nothing there; the lane's buttons and the keys end the edit.
245
262
  if (isRowShaped) return;
246
263
  const next = event.relatedTarget;
247
- if (next instanceof Node && event.currentTarget.contains(next)) return;
264
+ if (isNode(next) && event.currentTarget.contains(next)) return;
248
265
  closingRef.current = true;
249
266
  if (features.editMode === "cellConfirm") {
250
267
  // The draft stays, dirty-marked, waiting for its ✓.
@@ -252,7 +269,8 @@ export function TMDataGridCellEditor({
252
269
  return;
253
270
  }
254
271
  void edit.commit(row.id).then((ok) => {
255
- if (!ok) edit.deactivate();
272
+ // Open for another try: the next blur or key has to be able to commit.
273
+ if (!ok) closingRef.current = false;
256
274
  });
257
275
  };
258
276
 
@@ -289,7 +307,35 @@ export function TMDataGridCellEditor({
289
307
  onMouseDown={(event) => event.stopPropagation()}
290
308
  onDoubleClick={(event) => event.stopPropagation()}
291
309
  >
292
- <Editor {...args} />
310
+ {/* The validation message, on the editor rather than under it: inside a
311
+ narrow column Mantine's inline error wraps, grows the row and is
312
+ clipped, so the input keeps the invalid border and the text comes up
313
+ beside it. The editor gets a box of its own because Tooltip needs a
314
+ child that takes a ref, which a `meta.edit.editor` need not forward. */}
315
+ <Tooltip
316
+ label={error}
317
+ opened={error !== undefined && (messageHovered || messageFocused)}
318
+ withArrow
319
+ position="bottom-start"
320
+ >
321
+ <div
322
+ className={classes.cellEditorField}
323
+ onMouseEnter={() => setMessageHovered(true)}
324
+ onMouseLeave={() => setMessageHovered(false)}
325
+ onFocus={() => setMessageFocused(true)}
326
+ onBlur={(event) => {
327
+ // Focus moving inside the editor - into an open dropdown, say - is
328
+ // not focus leaving it.
329
+ const next = event.relatedTarget;
330
+ if (isNode(next) && event.currentTarget.contains(next)) {
331
+ return;
332
+ }
333
+ setMessageFocused(false);
334
+ }}
335
+ >
336
+ <Editor {...args} />
337
+ </div>
338
+ </Tooltip>
293
339
  {/* cellConfirm's chrome: the draft only commits through the ✓ (or
294
340
  Enter), so the pair sits right beside the input. */}
295
341
  {features.editMode === "cellConfirm" && (
@@ -12,7 +12,11 @@
12
12
  display: flex;
13
13
  flex-direction: column;
14
14
  gap: var(--mantine-spacing-sm);
15
- padding: 0 var(--mantine-spacing-sm) var(--mantine-spacing-sm);
15
+ padding: var(--mantine-spacing-sm);
16
+ }
17
+
18
+ .columnsPanelSearch + * .columnsPanelList {
19
+ padding-top: 0;
16
20
  }
17
21
 
18
22
  .columnsPanelFooter {
@@ -1,4 +1,6 @@
1
1
  import {
2
+ Box,
3
+ type BoxProps,
2
4
  Button,
3
5
  Checkbox,
4
6
  ScrollArea,
@@ -6,36 +8,42 @@ import {
6
8
  TextInput,
7
9
  Tooltip,
8
10
  } from "@mantine/core";
9
- import { useSelector } from "@tanstack/react-store";
10
11
  import { useState } from "react";
11
12
  import classes from "./TMDataGridColumnsPanel.module.css";
12
13
  import { useTMDataGridContext } from "../TMDataGridContext";
13
- import { getColumnLabel } from "../core/columnUtils";
14
+ import {
15
+ getColumnLabel,
16
+ showColumnSearch,
17
+ type TMDataGridColumnSearchable,
18
+ } from "../core/columnUtils";
19
+ import { useHideableColumns } from "./useHideableColumns";
14
20
  import { SearchIcon } from "./icons";
15
21
 
16
22
  /**
17
- * The "Manage columns" surface. Reachable from every column header menu and
18
- * from the grid's burger menu, so it lives on its own and is rendered by
19
- * `TMDataGrid.ColumnsButton`.
23
+ * The "Manage columns" surface as plain controls, for a host that is not a
24
+ * menu - a Popover, a Drawer, an inline layout. `TMDataGrid.Menu.Columns` is
25
+ * the same chooser as menu items.
20
26
  */
21
- export function TMDataGridColumnsPanel() {
22
- const { table, labels, controlSize, resetSettings } = useTMDataGridContext();
27
+ /** Mantine's style props (`w={320}`, `p="sm"`) are set on the panel block. */
28
+ export type TMDataGridColumnsPanelProps = BoxProps & {
29
+ /**
30
+ * The search box over the list: `"auto"` (the default) from six hideable
31
+ * columns, `true` always, `false` never.
32
+ */
33
+ searchable?: TMDataGridColumnSearchable;
34
+ };
35
+
36
+ export function TMDataGridColumnsPanel({
37
+ className,
38
+ searchable = "auto",
39
+ ...others
40
+ }: TMDataGridColumnsPanelProps = {}) {
41
+ const { labels, controlSize, resetSettings } = useTMDataGridContext();
23
42
  const [search, setSearch] = useState("");
24
43
 
25
- const columnVisibility = useSelector(
26
- table.store,
27
- (state) => state.columnVisibility,
28
- );
44
+ const { columns, columnVisibility, shownCount, setAllVisible } =
45
+ useHideableColumns();
29
46
 
30
- // Only what can actually be hidden. A column with `enableHiding: false` is
31
- // left out rather than listed and disabled: a box that cannot be ticked only
32
- // invites the question, and every generated lane is one - the checkbox and
33
- // edit lanes hold the controls the grid needs, the tree column follows the
34
- // grouping state, the row-number gutter follows `enableRowNumbers`. None of
35
- // them is a setting.
36
- const columns = table
37
- .getAllLeafColumns()
38
- .filter((column) => column.getCanHide());
39
47
  const needle = search.trim().toLowerCase();
40
48
  const visibleInPanel = needle
41
49
  ? columns.filter((column) =>
@@ -43,41 +51,25 @@ export function TMDataGridColumnsPanel() {
43
51
  )
44
52
  : columns;
45
53
 
46
- const shownCount = columns.filter(
47
- (column) => columnVisibility[column.id] !== false,
48
- ).length;
49
-
50
- /**
51
- * Show or hide every listed column.
52
- *
53
- * Not `table.toggleAllColumnsVisible`, which writes a visibility entry for
54
- * *every* leaf column: showing all would publish the tree column - hidden
55
- * because nothing is grouped, not because the user hid it - and hiding all
56
- * would force the same column visible, since it writes `!getCanHide()` for
57
- * the columns it will not touch. Either way a lane the panel never listed
58
- * changes state, and persistence then keeps it that way.
59
- */
60
- const setAllVisible = (visible: boolean) => {
61
- table.setColumnVisibility((previous) => {
62
- const next = { ...previous };
63
- for (const column of columns) next[column.id] = visible;
64
- return next;
65
- });
66
- };
67
-
68
54
  return (
69
- <div data-dg-part="columns-panel" className={classes.columnsPanel}>
70
- <div className={classes.columnsPanelSearch}>
71
- <TextInput
72
- value={search}
73
- onChange={(event) => setSearch(event.currentTarget.value)}
74
- placeholder={labels.columnsSearchPlaceholder}
75
- leftSection={<SearchIcon size={16} stroke={1.6} />}
76
- size={controlSize}
77
- data-dg-part="columns-search"
78
- data-autofocus
79
- />
80
- </div>
55
+ <Box
56
+ data-dg-part="columns-panel"
57
+ className={[classes.columnsPanel, className].filter(Boolean).join(" ")}
58
+ {...others}
59
+ >
60
+ {showColumnSearch(searchable, columns.length) && (
61
+ <div className={classes.columnsPanelSearch}>
62
+ <TextInput
63
+ value={search}
64
+ onChange={(event) => setSearch(event.currentTarget.value)}
65
+ placeholder={labels.columnsSearchPlaceholder}
66
+ leftSection={<SearchIcon size={16} stroke={1.6} />}
67
+ size={controlSize}
68
+ data-dg-part="columns-search"
69
+ data-autofocus
70
+ />
71
+ </div>
72
+ )}
81
73
 
82
74
  <ScrollArea.Autosize mah={260} type="auto">
83
75
  <div className={classes.columnsPanelList}>
@@ -126,6 +118,6 @@ export function TMDataGridColumnsPanel() {
126
118
  </Button>
127
119
  </Tooltip>
128
120
  </div>
129
- </div>
121
+ </Box>
130
122
  );
131
123
  }
@@ -3,7 +3,7 @@ import {
3
3
  useBodyControlTabIndex,
4
4
  useTMDataGridContext,
5
5
  } from "../TMDataGridContext";
6
- import type { ColumnDef, Row, RowData } from "@tanstack/react-table";
6
+ import type { Row, RowData } from "@tanstack/react-table";
7
7
  import { useSelector } from "@tanstack/react-store";
8
8
  import classes from "./TMDataGridDetailsColumn.module.css";
9
9
  import { ChevronRightIcon } from "./icons";
@@ -20,7 +20,7 @@ export const DETAILS_COLUMN_ID = "__details__";
20
20
  * expand, so the React Compiler would cache the call along with it and the
21
21
  * chevron would never turn. Same reason the select checkbox subscribes.
22
22
  */
23
- function DetailsCell<TData extends RowData>({
23
+ export function DetailsCell<TData extends RowData>({
24
24
  row,
25
25
  }: {
26
26
  row: Row<TMDataGridFeatures, TData>;
@@ -51,7 +51,7 @@ function DetailsCell<TData extends RowData>({
51
51
  row.toggleExpanded();
52
52
  }}
53
53
  >
54
- <span className={classes.chevron} data-expanded={expanded}>
54
+ <span className={classes.chevron} data-expanded={expanded || undefined}>
55
55
  <ChevronRightIcon size={16} stroke={1.6} />
56
56
  </span>
57
57
  </UnstyledButton>
@@ -67,7 +67,7 @@ function DetailsCell<TData extends RowData>({
67
67
  * so it would unfold every group as well. Only the data rows are touched here;
68
68
  * whatever the tree was showing, it goes on showing. See resolveExpandAll.
69
69
  */
70
- function DetailsHeader<TData extends RowData>({
70
+ export function DetailsHeader<TData extends RowData>({
71
71
  table,
72
72
  }: {
73
73
  table: TMDataGridTable<TData>;
@@ -106,55 +106,9 @@ function DetailsHeader<TData extends RowData>({
106
106
  )
107
107
  }
108
108
  >
109
- <span className={classes.chevron} data-expanded={allExpanded}>
109
+ <span className={classes.chevron} data-expanded={allExpanded || undefined}>
110
110
  <ChevronRightIcon size={16} stroke={1.6} />
111
111
  </span>
112
112
  </UnstyledButton>
113
113
  );
114
114
  }
115
-
116
- /**
117
- * The generated details lane, prepended whenever `renderDetails` is set.
118
- *
119
- * Structural, like the checkbox and tree columns: fixed width, pinned to the
120
- * left after both of them, not hideable, not movable and not resizable. Moving
121
- * or hiding the toggle would leave rows with panels that cannot be opened.
122
- *
123
- * Last of the three because it acts on a single record.
124
- *
125
- * A second toggle elsewhere is supported: `row.toggleExpanded()` is the entire
126
- * interface, and this lane is only the control the grid ships.
127
- */
128
- export function createDetailsColumn<TData extends RowData>(
129
- label = "Details",
130
- ): ColumnDef<TMDataGridFeatures, TData, unknown> {
131
- return {
132
- id: DETAILS_COLUMN_ID,
133
- meta: {
134
- label,
135
- align: "center",
136
- // Structurally the last of the generated lanes.
137
- enableOrdering: false,
138
- },
139
- // A system lane: as wide as the control it holds and no wider. Fixed at
140
- // every scale - the control does not grow with the font size, so neither
141
- // should its track.
142
- size: 36,
143
- minSize: 36,
144
- maxSize: 36,
145
- enableResizing: false,
146
- enableSorting: false,
147
- enableColumnFilter: false,
148
- enableGlobalFilter: false,
149
- // Its visibility is not the user's to set: hiding it would strand every
150
- // panel behind a control that is no longer there.
151
- enableHiding: false,
152
- // Structurally pinned to the left; users shouldn't be able to move it.
153
- enablePinning: false,
154
- header: ({ table }) => <DetailsHeader table={table} />,
155
- cell: ({ row }) => <DetailsCell row={row} />,
156
- // Deliberately no `aggregatedCell`: on a group row every cell outside the
157
- // grouped column counts as aggregated, and blank is the right answer here -
158
- // groups expand into their rows, not into a panel.
159
- };
160
- }
@@ -3,7 +3,11 @@ import { useSelector } from "@tanstack/react-store";
3
3
  import { shallow } from "@tanstack/store";
4
4
  import type { ReactNode } from "react";
5
5
  import { useTMDataGridContext } from "../TMDataGridContext";
6
- import { getOpenRowIds } from "../core/editEngine";
6
+ import {
7
+ getOpenRowIds,
8
+ type TMDataGridCommitAllResult,
9
+ type TMDataGridSaveDraftsResult,
10
+ } from "../core/editEngine";
7
11
  import { getDisplayedRows } from "../core/rowSelection";
8
12
  import type {
9
13
  TMDataGridScrollAlign,
@@ -17,13 +21,20 @@ import type {
17
21
  */
18
22
  function useDraftCount(): number {
19
23
  const { edit } = useTMDataGridContext();
20
- return useSelector(
21
- edit.store,
22
- (state) =>
23
- state.committedRowIds.length +
24
+ return useSelector(edit.store, (state) => {
25
+ const deleted = new Set(state.deletedRowIds);
26
+ // An edited row that was then marked counts once: Save sends the
27
+ // deletion and leaves the edit behind.
28
+ const edited =
29
+ deleted.size === 0
30
+ ? state.committedRowIds.length
31
+ : state.committedRowIds.filter((rowId) => !deleted.has(rowId)).length;
32
+ return (
33
+ edited +
24
34
  state.newRows.filter((newRow) => newRow.committed).length +
25
- state.deletedRowIds.length,
26
- );
35
+ deleted.size
36
+ );
37
+ });
27
38
  }
28
39
 
29
40
  /**
@@ -55,15 +66,15 @@ function useOpenRowIds(): ReadonlyArray<string> {
55
66
  function EditSaveButton() {
56
67
  const { edit, labels } = useTMDataGridContext();
57
68
  const draftCount = useDraftCount();
58
- const isSubmitting = useSelector(edit.store, (state) =>
59
- state.openRowIds.some((rowId) => state.rows[rowId]?.isSubmitting === true),
60
- );
69
+ // Save sends the draft store, so its loading state is the save in flight,
70
+ // not an open row's submit.
71
+ const isSaving = useSelector(edit.store, (state) => state.isSaving);
61
72
 
62
73
  return (
63
74
  <Button
64
75
  size="compact-sm"
65
76
  disabled={draftCount === 0}
66
- loading={isSubmitting}
77
+ loading={isSaving}
67
78
  data-dg-part="save-all"
68
79
  data-draft-count={draftCount}
69
80
  onClick={() => void edit.saveDrafts()}
@@ -94,14 +105,14 @@ function EditDiscardButton() {
94
105
  const { edit, labels } = useTMDataGridContext();
95
106
  // Discard drops everything the grid is holding, open rows included, so it
96
107
  // stays live while anything is uncommitted.
97
- const pendingCount = useDraftCount() + useOpenCount();
108
+ const uncommittedCount = useDraftCount() + useOpenCount();
98
109
 
99
110
  return (
100
111
  <Button
101
112
  variant="subtle"
102
113
  color="gray"
103
114
  size="compact-sm"
104
- disabled={pendingCount === 0}
115
+ disabled={uncommittedCount === 0}
105
116
  data-dg-part="discard-all"
106
117
  onClick={() => edit.cancelAll()}
107
118
  >
@@ -139,21 +150,26 @@ export type TMDataGridDraftActionsState = {
139
150
  * in display order, so it need not be `openRowIds[0]`.
140
151
  */
141
152
  openRowIds: ReadonlyArray<string>;
142
- /**
143
- * @deprecated Was "everything uncommitted", which Save no longer sends.
144
- * Reads as `draftCount + openCount`; use whichever you meant.
145
- */
146
- pendingCount: number;
147
- /** Whether a submit is in flight. */
153
+ /** Whether a submit is in flight - any open row is submitting. */
148
154
  isSubmitting: boolean;
155
+ /** `true` while `saveDrafts` is in flight. */
156
+ isSaving: boolean;
149
157
  };
150
158
 
151
159
  /** What the edit chrome can do. */
152
160
  export type TMDataGridDraftActionsActions = {
153
- /** Saves the draft store. Open rows are left alone. */
154
- save: () => Promise<boolean>;
155
- /** Submits every open row, committing the ones that validate. */
156
- commitAll: () => Promise<boolean>;
161
+ /**
162
+ * Saves the draft store - `edit.saveDrafts()`. Open rows are left alone.
163
+ * Resolves which ids were saved, kept in the draft store for the next
164
+ * save, or reopened with an error; see {@link TMDataGridSaveDraftsResult}.
165
+ */
166
+ save: () => Promise<TMDataGridSaveDraftsResult>;
167
+ /**
168
+ * Submits every open row, committing the ones that validate -
169
+ * `edit.commitAll()`. Resolves which rows committed and which are still
170
+ * open; see {@link TMDataGridCommitAllResult}.
171
+ */
172
+ commitAll: () => Promise<TMDataGridCommitAllResult>;
157
173
  /** Drops everything - open form state and the draft store alike. */
158
174
  discard: () => void;
159
175
  /**
@@ -242,6 +258,7 @@ export function TMDataGridDraftActions({
242
258
  const isSubmitting = useSelector(edit.store, (state) =>
243
259
  state.openRowIds.some((rowId) => state.rows[rowId]?.isSubmitting === true),
244
260
  );
261
+ const isSaving = useSelector(edit.store, (state) => state.isSaving);
245
262
 
246
263
  if (!features.editing) return null;
247
264
 
@@ -251,8 +268,8 @@ export function TMDataGridDraftActions({
251
268
  draftCount,
252
269
  openCount: openRowIds.length,
253
270
  openRowIds,
254
- pendingCount: draftCount + openRowIds.length,
255
271
  isSubmitting,
272
+ isSaving,
256
273
  },
257
274
  actions: {
258
275
  save: () => edit.saveDrafts(),
@@ -1,6 +1,6 @@
1
1
  import { ActionIcon, Group, Loader, Tooltip } from "@mantine/core";
2
2
  import { useSelector } from "@tanstack/react-store";
3
- import type { ColumnDef, Row, RowData } from "@tanstack/react-table";
3
+ import type { Row, RowData } from "@tanstack/react-table";
4
4
  import classes from "./TMDataGridTable.module.css";
5
5
  import {
6
6
  useBodyControlTabIndex,
@@ -65,16 +65,16 @@ function RowStateIndicator({
65
65
  * | open | the mode's own controls: Save and Cancel | `editing.mode` |
66
66
  * | neither | the pencil (row mode) and the trash | `editing.mode` |
67
67
  *
68
- * A row error turns the Save red with the message in its tooltip - a
69
- * pathless `.refine()` has nowhere else to land, and a field error outlives
70
- * the editor that found it. A parked row never offers a save: it has had its
71
- * submit, and `TMDataGrid.DraftActions` is what sends it.
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
72
  *
73
73
  * The lane's controls take the tab index every body control does - see
74
74
  * useBodyControlTabIndex. The open row's save and cancel are the exception,
75
75
  * see where they are built.
76
76
  */
77
- function EditLaneCell<TData extends RowData>({
77
+ export function EditLaneCell<TData extends RowData>({
78
78
  row,
79
79
  }: {
80
80
  row: Row<TMDataGridFeatures, TData>;
@@ -186,10 +186,10 @@ function EditLaneCell<TData extends RowData>({
186
186
  }
187
187
  return (
188
188
  <Group gap={0} wrap="nowrap">
189
- <Tooltip label={labels.confirmNewRow} withArrow>
189
+ <Tooltip label={rowError ?? labels.confirmNewRow} withArrow>
190
190
  <ActionIcon
191
191
  variant="subtle"
192
- color="green"
192
+ color={hasErrors ? "red" : "green"}
193
193
  size="sm"
194
194
  tabIndex={tabIndex}
195
195
  aria-label={labels.confirmNewRow}
@@ -239,7 +239,7 @@ function EditLaneCell<TData extends RowData>({
239
239
  data-row-id={rowId}
240
240
  onClick={(event) => {
241
241
  event.stopPropagation();
242
- edit.deleteRow(rowId);
242
+ edit.restoreRow(rowId);
243
243
  }}
244
244
  >
245
245
  <RestoreIcon size={16} stroke={1.6} />
@@ -339,15 +339,9 @@ function EditLaneCell<TData extends RowData>({
339
339
 
340
340
  return (
341
341
  <Group gap={0} wrap="nowrap">
342
- {rowError === null ? (
343
- <Tooltip label={labels.saveRow} withArrow>
344
- {save}
345
- </Tooltip>
346
- ) : (
347
- <Tooltip label={rowError} withArrow>
348
- {save}
349
- </Tooltip>
350
- )}
342
+ <Tooltip label={rowError ?? labels.saveRow} withArrow>
343
+ {save}
344
+ </Tooltip>
351
345
  <Tooltip label={labels.cancelRowEdit} withArrow>
352
346
  <ActionIcon
353
347
  variant="subtle"
@@ -368,44 +362,3 @@ function EditLaneCell<TData extends RowData>({
368
362
  </Group>
369
363
  );
370
364
  }
371
-
372
- /**
373
- * The generated edit lane, appended and pinned right - the row's Save at the
374
- * end of the row under `mode: "row"`, the state marker and revert under
375
- * `editing.draft`, mirroring the checkbox lane's build on the left.
376
- */
377
- export function createEditColumn<TData extends RowData>(
378
- label = "Edit",
379
- /** A draft lane holds three controls where the rest hold two. */
380
- wide = false,
381
- ): ColumnDef<TMDataGridFeatures, TData, unknown> {
382
- const width = wide ? 88 : 64;
383
- return {
384
- id: EDIT_COLUMN_ID,
385
- meta: {
386
- label,
387
- align: "center",
388
- enableOrdering: false,
389
- },
390
- // Wide enough for the pair (or draft's trio) it holds while editing.
391
- size: width,
392
- minSize: width,
393
- maxSize: width,
394
- enableResizing: false,
395
- enableSorting: false,
396
- enableColumnFilter: false,
397
- enableGlobalFilter: false,
398
- // The row's Save, Cancel and Delete live here, so hiding the lane would
399
- // strand an open row with no way to commit or discard it. Same rule as the
400
- // checkbox lane: chrome the grid generates is not a user setting.
401
- enableHiding: false,
402
- // Structurally pinned to the right; not movable.
403
- enablePinning: false,
404
- header: () => null,
405
- cell: ({ row }) => <EditLaneCell row={row} />,
406
- // Group rows: same reasoning as the checkbox lane - without this the
407
- // cell renders blank on aggregated rows, but here blank is also correct,
408
- // so the aggregated cell renders the same (null for groups).
409
- aggregatedCell: ({ row }) => <EditLaneCell row={row} />,
410
- };
411
- }