@jielga/tmdatagrid 2.0.0-beta.4 → 2.0.0-beta.7

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 (37) hide show
  1. package/dist/index.d.ts +339 -152
  2. package/dist/index.js +2602 -2450
  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 +44 -2
  9. package/skills/data/SKILL.md +14 -10
  10. package/skills/editing/SKILL.md +140 -68
  11. package/skills/editing/references/common-mistakes.md +98 -8
  12. package/skills/editing/references/editing-api.md +40 -30
  13. package/skills/editing/references/editors-and-validation.md +65 -14
  14. package/skills/filtering/SKILL.md +1 -1
  15. package/skills/getting-started/SKILL.md +3 -3
  16. package/skills/grouping/SKILL.md +5 -3
  17. package/skills/options/SKILL.md +2 -2
  18. package/skills/rows/SKILL.md +7 -3
  19. package/skills/server-side/SKILL.md +1 -1
  20. package/skills/testing/SKILL.md +1 -1
  21. package/src/tmdatagrid/components/TMDataGrid.tsx +2 -2
  22. package/src/tmdatagrid/components/TMDataGridCellEditor.tsx +33 -34
  23. package/src/tmdatagrid/components/TMDataGridDraftActions.tsx +296 -0
  24. package/src/tmdatagrid/components/TMDataGridEditColumn.tsx +40 -33
  25. package/src/tmdatagrid/components/TMDataGridEntryRows.tsx +7 -7
  26. package/src/tmdatagrid/components/TMDataGridTable.tsx +27 -5
  27. package/src/tmdatagrid/components/TMDataGridToolbar.module.css +4 -0
  28. package/src/tmdatagrid/components/TMDataGridToolbar.tsx +8 -10
  29. package/src/tmdatagrid/components/editors/TMDataGridNumberEditor.tsx +34 -4
  30. package/src/tmdatagrid/components/editors/editorShared.ts +1 -29
  31. package/src/tmdatagrid/core/capabilities.ts +14 -6
  32. package/src/tmdatagrid/core/editEngine.ts +472 -96
  33. package/src/tmdatagrid/core/labels.ts +3 -3
  34. package/src/tmdatagrid/core/summary.ts +10 -2
  35. package/src/tmdatagrid/index.ts +9 -7
  36. package/src/tmdatagrid/useTMDataGrid.tsx +107 -47
  37. package/src/tmdatagrid/components/TMDataGridEditActions.tsx +0 -233
@@ -364,9 +364,10 @@ function TMDataGridBodyCell({
364
364
  <span className={classes.cellContent}>
365
365
  {(() => {
366
366
  // A row with a live form shows its draft, not `data`: the column's
367
- // own renderer over a context reading the form's values. Parked
368
- // drafts - draft mode, cellConfirm's kept-on-blur - are the point;
369
- // group cells are excluded because no group row ever has a form.
367
+ // own renderer over a context reading the form's values. The
368
+ // drafts that outlive their editor - parked ones, cellConfirm's
369
+ // kept ones - are the point; group cells are excluded because no
370
+ // group row ever has a form.
370
371
  const draftContext =
371
372
  draftValues !== undefined &&
372
373
  contentOverride === undefined &&
@@ -969,6 +970,21 @@ export function TMDataGridTable<TData extends RowData = TMDataGridRowData>({
969
970
  initialRect: { height: 600, width: 1200 },
970
971
  });
971
972
 
973
+ /**
974
+ * A live `size` (or `meta.rowHeight`) change moves what `estimateSize`
975
+ * answers, but the virtualizer's measurement cache does not list that option
976
+ * among its dependencies - by its own docs, changing the estimate means
977
+ * calling `measure()`. Without this the spacers keep the old height until
978
+ * something else invalidates the cache, so the scroll range and the window
979
+ * disagree with the rows actually painted.
980
+ */
981
+ const prevRowHeightRef = useRef(rowHeight);
982
+ useEffect(() => {
983
+ if (prevRowHeightRef.current === rowHeight) return;
984
+ prevRowHeightRef.current = rowHeight;
985
+ virtualizer.measure();
986
+ }, [rowHeight, virtualizer]);
987
+
972
988
  const virtualItems = virtualizer.getVirtualItems();
973
989
  const paddingTop = virtualItems[0]?.start ?? 0;
974
990
  const paddingBottom =
@@ -1539,7 +1555,7 @@ export function TMDataGridTable<TData extends RowData = TMDataGridRowData>({
1539
1555
  close: TMDataGridCellEditorClose,
1540
1556
  ) => {
1541
1557
  // Where the keyboard goes: Enter moves down, Tab to the next editable
1542
- // cell (the deferring draft variants move the same way, draft in tow) -
1558
+ // cell (cellConfirm's deferring variants move the same way, draft kept) -
1543
1559
  // everything else, and a saved row, goes back to where the edit was.
1544
1560
  const move =
1545
1561
  close.via === "enter"
@@ -2163,8 +2179,14 @@ export function TMDataGridTable<TData extends RowData = TMDataGridRowData>({
2163
2179
  // open, so every row holding a form shows its editors. Which of
2164
2180
  // them holds the caret is not decided here - see
2165
2181
  // placeEditCaret, which places it once per gesture.
2182
+ //
2183
+ // A parked row is not one of them. It keeps its form, so it is
2184
+ // still "open", but it has been decided: it renders its draft
2185
+ // through the column's own renderer until `begin` reopens it.
2166
2186
  const rowEditing =
2167
- features.editMode === "row" && editOpenRowIds.includes(row.id);
2187
+ features.editMode === "row" &&
2188
+ editOpenRowIds.includes(row.id) &&
2189
+ !editDraftRowIds.includes(row.id);
2168
2190
  // Cell selection takes the body's tab stop off the row and puts
2169
2191
  // it on a cell - two stops per row would make Tab a way of
2170
2192
  // walking the grid, which is what the arrow keys are for. Space
@@ -10,3 +10,7 @@
10
10
  .toolbarSpacer {
11
11
  flex: 1;
12
12
  }
13
+
14
+ .summaryCount {
15
+ white-space: nowrap;
16
+ }
@@ -54,22 +54,20 @@ export function TMDataGridSummaryCount({ children }: { children?: ReactNode }) {
54
54
  const { table, controlSize } = useTMDataGridContext();
55
55
  useSelector(table.store);
56
56
 
57
- if (children !== undefined) {
58
- return (
59
- <Text span size={controlSize} c="dimmed" data-dg-part="summary-count">
60
- {children}
61
- </Text>
62
- );
63
- }
64
-
65
57
  const shown = table.getRowCount();
66
58
  const total =
67
59
  table.options.meta?.totalRowCount ??
68
60
  table.getPreFilteredRowModel().rows.length;
69
61
 
70
62
  return (
71
- <Text span size={controlSize} c="dimmed" data-dg-part="summary-count">
72
- {shown} / {total}
63
+ <Text
64
+ span
65
+ size={controlSize}
66
+ c="dimmed"
67
+ className={classes.summaryCount}
68
+ data-dg-part="summary-count"
69
+ >
70
+ {children !== undefined ? children : `${shown} / ${total}`}
73
71
  </Text>
74
72
  );
75
73
  }
@@ -1,4 +1,5 @@
1
1
  import { NumberInput } from "@mantine/core";
2
+ import { useState } from "react";
2
3
  import type { TMDataGridEditorArgs } from "../../core/editEngine";
3
4
  import { getColumnLabel } from "../../core/columnUtils";
4
5
  import { useTMDataGridContext } from "../../TMDataGridContext";
@@ -15,6 +16,19 @@ export function TMDataGridNumberEditor({ field, column, size }: TMDataGridEditor
15
16
  const value = useFieldValue(field);
16
17
  const error = useFieldError(field);
17
18
  const { inputRef, remember } = useCaretKeeper();
19
+ /**
20
+ * The last text Mantine reported instead of a number - "-" or "1e" on the
21
+ * way to one. Held so a half-typed number stays on screen while the field
22
+ * itself holds no value.
23
+ */
24
+ const [typed, setTyped] = useState<string | null>(null);
25
+ // The field is the source of truth: the text survives only while it still
26
+ // describes what the field holds, so a write from outside the editor -
27
+ // `edit.setCellValue`, a revert - replaces it rather than being masked.
28
+ const parsedTyped = typed === null || typed === "" ? null : Number(typed);
29
+ const textDescribesField = Number.isFinite(parsedTyped)
30
+ ? parsedTyped === value
31
+ : value === null;
18
32
  return (
19
33
  <NumberInput
20
34
  ref={inputRef}
@@ -24,13 +38,29 @@ export function TMDataGridNumberEditor({ field, column, size }: TMDataGridEditor
24
38
  hideControls
25
39
  aria-label={labels.editCell(getColumnLabel(column))}
26
40
  data-dg-part="editor-input"
27
- value={typeof value === "number" ? value : ""}
41
+ value={
42
+ typed !== null && textDescribesField
43
+ ? typed
44
+ : typeof value === "number"
45
+ ? value
46
+ : ""
47
+ }
28
48
  onChange={(next) => {
29
49
  if (inputRef.current !== null) remember(inputRef.current);
30
- // Mantine reports a number once one can be parsed, and a string while
31
- // it cannot. An empty cell is `null`, meaning no value.
50
+ if (typeof next === "number") {
51
+ setTyped(null);
52
+ field.handleChange(next);
53
+ return;
54
+ }
55
+ // Mantine reports a number once one can be parsed and a string while
56
+ // it cannot: "" for an empty cell, partial input on the way to a
57
+ // number. Anything that does not parse leaves the field empty rather
58
+ // than `NaN` - which is a number to every validator and would commit
59
+ // as one, with no message a user could act on.
60
+ setTyped(next);
61
+ const parsed = Number(next);
32
62
  field.handleChange(
33
- typeof next === "number" ? next : next === "" ? null : Number(next),
63
+ next !== "" && Number.isFinite(parsed) ? parsed : null,
34
64
  );
35
65
  }}
36
66
  onBlur={field.handleBlur}
@@ -1,34 +1,6 @@
1
1
  import { useStore } from "@tanstack/react-form";
2
2
  import { useLayoutEffect, useRef, type RefObject } from "react";
3
- import type { TMDataGridEditField } from "../../core/editEngine";
4
-
5
- /**
6
- * The first error as text, whatever shape the validator produced - TanStack
7
- * Form keeps errors verbatim, so a Zod issue arrives as `{ message }`, a
8
- * plain function's return arrives as it was, and a form-level schema's
9
- * pathless issues arrive keyed under an empty path (`{ "": [issues] }`).
10
- * This digs until it finds a message.
11
- */
12
- export function firstErrorText(error: unknown): string | undefined {
13
- if (error === null || error === undefined) return undefined;
14
- if (typeof error === "string") return error;
15
- if (Array.isArray(error)) {
16
- for (const entry of error) {
17
- const text = firstErrorText(entry);
18
- if (text !== undefined) return text;
19
- }
20
- return undefined;
21
- }
22
- if (typeof error === "object") {
23
- const message = (error as { message?: unknown }).message;
24
- if (typeof message === "string") return message;
25
- for (const value of Object.values(error)) {
26
- const text = firstErrorText(value);
27
- if (text !== undefined) return text;
28
- }
29
- }
30
- return undefined;
31
- }
3
+ import { firstErrorText, type TMDataGridEditField } from "../../core/editEngine";
32
4
 
33
5
  /** The field's live value - one subscription, on the field's own store. */
34
6
  export function useFieldValue(field: TMDataGridEditField): unknown {
@@ -120,13 +120,20 @@ export type TMDataGridFeatureFlags = {
120
120
  grouping: boolean;
121
121
  /** Whether cells can be edited at all - the `editing` option was set. */
122
122
  editing: boolean;
123
- /** The commit policy, or `null` while editing is off. */
123
+ /** What counts as a commit, or `null` while editing is off. */
124
124
  editMode: TMDataGridEditMode | null;
125
125
  /**
126
- * Whether a confirmed entry row stays pinned in the sticky entry block
127
- * until Save all - `editing.newRowsSticky`, draft mode only. Off by
128
- * default: a confirmed row joins the scrolling flow instead, so entering
129
- * many rows cannot fill the viewport with sticky chrome.
126
+ * Where a commit goes - `editing.draft`. On, commits park in the grid's
127
+ * draft store and leave through `edit.saveDrafts()`; off, each one reaches
128
+ * the consumer as it happens.
129
+ */
130
+ editDraft: boolean;
131
+ /**
132
+ * Whether a parked entry row stays pinned in the sticky entry block until
133
+ * the draft store is saved - `editing.newRowsSticky`, which needs
134
+ * `editing.draft`. Off by default: a parked row joins the scrolling flow
135
+ * instead, so entering many rows cannot fill the viewport with sticky
136
+ * chrome.
130
137
  */
131
138
  editNewRowsSticky: boolean;
132
139
  /** The generated row-number gutter - `enableRowNumbers`. Off by default. */
@@ -211,8 +218,9 @@ export function readFeatureFlags<TData extends RowData>(
211
218
  grouping: options.enableGrouping ?? options.manualPagination !== true,
212
219
  editing: options.editing !== undefined,
213
220
  editMode: options.editing?.mode ?? null,
221
+ editDraft: options.editing?.draft === true,
214
222
  editNewRowsSticky:
215
- options.editing?.mode === "draft" &&
223
+ options.editing?.draft === true &&
216
224
  options.editing.newRowsSticky === true,
217
225
  rowNumbers: options.enableRowNumbers === true,
218
226
  // A predicate counts as on - some rows may still pin.