@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
@@ -111,12 +111,12 @@ export type TMDataGridLabels = {
111
111
  cancelRowEdit: string;
112
112
  /** Save tooltip when field errors block the row, with no row message. */
113
113
  editRowErrors: string;
114
- /** `EditActions`' Save, with the count of rows in the draft store. */
114
+ /** `DraftActions`' Save, with the count of rows in the draft store. */
115
115
  saveAllEdits: (rows: number) => string;
116
- /** `EditActions`' Discard. */
116
+ /** `DraftActions`' Discard. */
117
117
  discardAllEdits: string;
118
118
  /**
119
- * `EditActions`' note about rows still open - edited but not committed, so
119
+ * `DraftActions`' note about rows still open - edited but not committed, so
120
120
  * not part of the save. Shown only while there are any.
121
121
  */
122
122
  editRowsStillOpen: (rows: number) => string;
@@ -18,6 +18,10 @@ export type TMDataGridAggregationName = keyof typeof aggregationFns;
18
18
  * Filtered rather than paginated rows, so the total covers everything the
19
19
  * filters left (all pages), and follows the filters as they change. Reuses
20
20
  * TanStack's registered aggregation functions; `fn` defaults to `"sum"`.
21
+ *
22
+ * Every data row counts once. Grouping builds its group rows from this model
23
+ * rather than into it, so a grouped grid totals its records, not its records
24
+ * plus their subtotals; a tree counts parents and children alike.
21
25
  */
22
26
  export function aggregateColumn<TData extends RowData>({
23
27
  table,
@@ -29,7 +33,11 @@ export function aggregateColumn<TData extends RowData>({
29
33
  fn?: TMDataGridAggregationName;
30
34
  }): unknown {
31
35
  const erased = table as unknown as TMDataGridTable<TMDataGridRowData>;
32
- const rows = erased.getFilteredRowModel().rows;
33
- // Leaf and child rows are the same set here: the filtered model is flat.
36
+ // `flatRows`, not `rows`: the filtered model nests under `getSubRows`, and
37
+ // the top-level array would total a tree's roots and drop every child.
38
+ // Grouping does not nest it - the group rows are built from this model, not
39
+ // held in it - so a grouped grid is unaffected either way.
40
+ const rows = erased.getFilteredRowModel().flatRows;
41
+ // Leaf and child rows are the same set here: this is already every row.
34
42
  return aggregationFns[fn](columnId, rows, rows);
35
43
  }
@@ -67,13 +67,13 @@ export {
67
67
  type TMDataGridOptionsSource,
68
68
  } from "./core/columnOptions";
69
69
  export {
70
- TMDataGridEditActions,
71
- type TMDataGridEditActionsActions,
72
- type TMDataGridEditActionsControls,
73
- type TMDataGridEditActionsProps,
74
- type TMDataGridEditActionsSlotArgs,
75
- type TMDataGridEditActionsState,
76
- } from "./components/TMDataGridEditActions";
70
+ TMDataGridDraftActions,
71
+ type TMDataGridDraftActionsActions,
72
+ type TMDataGridDraftActionsControls,
73
+ type TMDataGridDraftActionsProps,
74
+ type TMDataGridDraftActionsSlotArgs,
75
+ type TMDataGridDraftActionsState,
76
+ } from "./components/TMDataGridDraftActions";
77
77
  export {
78
78
  clearedValueForType,
79
79
  getEditFieldName,
@@ -101,6 +101,8 @@ export {
101
101
  type TMDataGridRowDeleteArgs,
102
102
  type TMDataGridRowEditForm,
103
103
  type TMDataGridRowValidators,
104
+ type TMDataGridTableValidateArgs,
105
+ type TMDataGridTableValidators,
104
106
  } from "./core/editEngine";
105
107
  export type {
106
108
  TMDataGridColumnFilterOptions,
@@ -59,6 +59,7 @@ import {
59
59
  type TMDataGridRowAddArgs,
60
60
  type TMDataGridRowDeleteArgs,
61
61
  type TMDataGridRowValidators,
62
+ type TMDataGridTableValidators,
62
63
  } from "./core/editEngine";
63
64
  import {
64
65
  emptyValueForOperator,
@@ -461,6 +462,27 @@ type TMDataGridEditingCallbacks<TData extends RowData> = {
461
462
  * Pathed issues land on the matching columns; pathless ones on the row.
462
463
  */
463
464
  rowValidators?: TMDataGridRowValidators;
465
+ /**
466
+ * Rules that need the other rows - no duplicate keys, no overlapping
467
+ * ranges, allocations summing to a total. Handed the committing row and
468
+ * `rows`, the collection as it would stand if the commit landed: every
469
+ * draft overlaid, entry rows appended, deletion-marked rows removed.
470
+ *
471
+ * ```tsx
472
+ * tableValidators: {
473
+ * onSubmit: ({ value, rowId, rows }) =>
474
+ * rows.some((r) => r.rowId !== rowId && r.value.code === value.code)
475
+ * ? { fields: { code: "Duplicate code" } }
476
+ * : undefined,
477
+ * }
478
+ * ```
479
+ *
480
+ * Runs at every commit, after the row's own validators, and again per
481
+ * parked row during `saveDrafts` - a draft another edit has invalidated
482
+ * blocks the save. Pathed issues land on the committing row's cells,
483
+ * pathless ones on the row.
484
+ */
485
+ tableValidators?: TMDataGridTableValidators<TData>;
464
486
  /** Rows the pencil skips - `false` keeps a row read-only in every mode. */
465
487
  isRowEditable?: (row: Row<TMDataGridFeatures, TData>) => boolean;
466
488
  /**
@@ -481,36 +503,44 @@ type TMDataGridEditingCallbacks<TData extends RowData> = {
481
503
  */
482
504
  newRowDefaults?: TData | (() => TData);
483
505
  /**
484
- * Called when an entry row commits: `Enter` or the lane's ✓ under the
485
- * immediate modes, `submitAll` under draft. Create the record and let it
486
- * arrive back through `data`; the engine's `tempId` never leaves the grid.
506
+ * Called when an entry row commits: `Enter` or the lane's ✓, or
507
+ * `saveDrafts` under `editing.draft`. Create the record and let it arrive
508
+ * back through `data`; the engine's `tempId` never leaves the grid.
487
509
  */
488
510
  onRowAdd?: (args: TMDataGridRowAddArgs<TData>) => void | Promise<void>;
489
511
  /**
490
- * Called by `edit.deleteRow` under the immediate modes - confirmation, if
491
- * any, belongs in here. Under draft, deletions accumulate in
492
- * `edit.state.deletedRowIds` instead and are reported by `submitAll`.
493
- * Setting this also puts the trash can in the edit lane.
512
+ * Called by `edit.deleteRow` - confirmation, if any, belongs in here. Under
513
+ * `editing.draft` deletions accumulate in `edit.state.deletedRowIds`
514
+ * instead and are reported by `saveDrafts`. Setting this also puts the
515
+ * trash can in the edit lane.
494
516
  */
495
517
  onRowDelete?: (args: TMDataGridRowDeleteArgs<TData>) => void | Promise<void>;
496
518
  };
497
519
 
498
520
  /**
499
521
  * The `editing` option: one object that turns editing on and holds
500
- * everything about it. `mode` picks what counts as a commit and which
501
- * controls trigger it; the other members act within that mode.
522
+ * everything about it. Two axes, and they are independent: `mode` picks what
523
+ * counts as a commit, `draft` picks where that commit goes.
524
+ *
525
+ * | Mode | Commit | Cancel | Controls |
526
+ * | ---- | ------ | ------ | -------- |
527
+ * | `"cell"` | Enter, Tab, blur - Sheets | Escape | none |
528
+ * | `"cellConfirm"` | ✓ or Enter; Tab and blur keep the draft | ✕ or Escape | ✓ / ✕ beside the input |
529
+ * | `"row"` | Save in the edit lane, or Enter | Cancel, or Escape | generated edit lane |
530
+ *
531
+ * | `draft` | Where a commit goes |
532
+ * | ------- | ------------------- |
533
+ * | `false` (default) | Out as it happens - `onCommit`, `onRowAdd`, `onRowDelete` |
534
+ * | `true` | Into the grid's draft store; `edit.saveDrafts()` sends the lot |
502
535
  *
503
- * | Mode | Commit | Cancel |
504
- * | ---- | ------ | ------ |
505
- * | `"cell"` | Enter, Tab, blur - Sheets | Escape |
506
- * | `"cellConfirm"` | ✓ or Enter only; blur keeps the draft | ✕ or Escape |
507
- * | `"row"` | Save in the edit lane, or Ctrl+Enter | Cancel, or Escape |
508
- * | `"draft"` | `edit.commit(rowId)` into the draft store, `edit.saveDrafts()` out | `edit.cancelAll()` |
536
+ * So `{ mode: "row", draft: true }` is "edit a row, the lane's ✓ parks it,
537
+ * the toolbar's Save sends every parked row at once", and
538
+ * `{ mode: "cell", draft: true }` is the same store filled cell by cell.
509
539
  *
510
540
  * Setting `editing` makes `getRowId` required - drafts are keyed by row id,
511
541
  * and the index fallback would name a different record after any sort - and
512
- * `onSaveDrafts` exists only under `mode: "draft"`, the one mode with a draft
513
- * store to save.
542
+ * `onSaveDrafts` exists only under `draft: true`, the one configuration with
543
+ * a draft store to save.
514
544
  *
515
545
  * The object may be written inline: the callbacks are read through a ref
516
546
  * every render, so its identity does not matter.
@@ -522,12 +552,35 @@ type TMDataGridEditingCallbacks<TData extends RowData> = {
522
552
  * override the rest.
523
553
  */
524
554
  export type TMDataGridEditingOptions<TData extends RowData> =
525
- TMDataGridEditingCallbacks<TData> &
526
- (
555
+ TMDataGridEditingCallbacks<TData> & {
556
+ /** What counts as a commit. See the table above. */
557
+ mode: TMDataGridEditMode;
558
+ /**
559
+ * The column ids that take edits, by id. Unset - the default - every
560
+ * column mapping to a data path is editable, which is what a grid whose
561
+ * columns are mostly the record itself wants.
562
+ *
563
+ * Set it for the other shape: a grid of reference data with one or two
564
+ * columns the user maintains, where naming those is shorter and harder to
565
+ * get wrong than switching every other column off one by one.
566
+ *
567
+ * This gates before `meta.edit`, never past it: a column left out takes no
568
+ * edits whatever its own meta says, and a column listed here still answers
569
+ * to its `meta.edit.enabled`.
570
+ */
571
+ columns?: ReadonlyArray<string>;
572
+ } & (
527
573
  | {
528
- mode: "draft";
529
574
  /**
530
- * Draft mode's save: called once by `edit.saveDrafts()` with the
575
+ * Commits park in the grid's draft store instead of reaching the
576
+ * consumer, and leave together through `edit.saveDrafts()`. The
577
+ * edit lane gains the change markers and the per-row revert, the
578
+ * trash marks a row for deletion rather than deleting it, and
579
+ * `TMDataGrid.DraftActions` gets something to save.
580
+ */
581
+ draft: true;
582
+ /**
583
+ * The bulk save: called once by `edit.saveDrafts()` with the
531
584
  * whole draft store - committed edits, added rows and deletion
532
585
  * marks - so a server can apply it as one transaction. Without it,
533
586
  * `saveDrafts` falls back to the per-row
@@ -555,21 +608,22 @@ export type TMDataGridEditingOptions<TData extends RowData> =
555
608
  | TMDataGridSaveDraftsResult
556
609
  | Promise<void | TMDataGridSaveDraftsResult>;
557
610
  /**
558
- * Keep committed entry rows pinned in the sticky entry block until
559
- * the draft store is saved. Off by default: a committed row joins
560
- * the scrolling flow above the body rows instead - the block a row
561
- * is *typed* into is always sticky, but committed rows scroll, so
562
- * committing many cannot fill the viewport with sticky chrome.
611
+ * Keep parked entry rows pinned in the sticky entry block until
612
+ * the draft store is saved. Off by default: a parked row joins the
613
+ * scrolling flow above the body rows instead - the block a row is
614
+ * *typed* into is always sticky, but parked rows scroll, so
615
+ * entering many cannot fill the viewport with sticky chrome.
563
616
  */
564
617
  newRowsSticky?: boolean;
565
618
  }
566
619
  | {
567
- mode: Exclude<TMDataGridEditMode, "draft">;
568
- /** Only `"draft"` has a draft store to save - see the other branch. */
620
+ /** Every commit reaches the consumer as it happens. The default. */
621
+ draft?: false;
622
+ /** Only `draft: true` has a store to save - see the other branch. */
569
623
  onSaveDrafts?: never;
570
624
  /** @deprecated See {@link onSaveDrafts}. */
571
625
  onCommitDrafts?: never;
572
- /** Committed entry rows exist only under `"draft"` - see there. */
626
+ /** Parked entry rows exist only under `draft: true` - see there. */
573
627
  newRowsSticky?: never;
574
628
  }
575
629
  );
@@ -919,6 +973,7 @@ export function useTMDataGrid<TData extends RowData>({
919
973
  // The one place `editing` is unpacked - the engine and the flags keep their
920
974
  // own vocabulary (`editMode`, `onEditCommit`), so the mapping lives here.
921
975
  const editMode = editing?.mode;
976
+ const editDraft = editing?.draft === true;
922
977
 
923
978
  // Derived up here, rather than just before the return, because the rest of the
924
979
  // hook needs `selectColumn` - one place decides what each mode means.
@@ -949,14 +1004,12 @@ export function useTMDataGrid<TData extends RowData>({
949
1004
  const detailsColumnEnabled = renderDetails !== undefined;
950
1005
  // Row mode's Save sits at the end of the row - the lane is its chrome. It
951
1006
  // also appears wherever the trash can has somewhere to report to, and
952
- // always under draft mode, where it is the change marker and the per-row
953
- // revert - with or without `onCommitDrafts`, since the per-row `submitAll`
954
- // fallback is a first-class configuration.
1007
+ // always under `draft`, where it is the change marker and the per-row
1008
+ // revert - with or without `onSaveDrafts`, since the per-row fallback is a
1009
+ // first-class configuration.
955
1010
  const editColumnEnabled =
956
1011
  editing !== undefined &&
957
- (editing.mode === "row" ||
958
- editing.mode === "draft" ||
959
- editing.onRowDelete !== undefined);
1012
+ (editing.mode === "row" || editDraft || editing.onRowDelete !== undefined);
960
1013
 
961
1014
  // The generated lanes bake `meta.label` into their definitions, so the memo
962
1015
  // depends on the strings rather than on the labels object - a fresh
@@ -967,9 +1020,9 @@ export function useTMDataGrid<TData extends RowData>({
967
1020
  const editColumnLabel = labels.editColumnLabel;
968
1021
  const rowNumberColumnLabel = labels.rowNumberColumnLabel;
969
1022
  const rowNumbersEnabled = features.rowNumbers;
970
- // Draft mode's lane holds three controls (state icon, undo, trash) where
971
- // the other modes hold two - it gets the wider track.
972
- const editIsDraftMode = editing?.mode === "draft";
1023
+ // A draft lane holds three controls (state icon, undo, trash) where the
1024
+ // rest hold two - it gets the wider track.
1025
+ const editWideLane = editDraft;
973
1026
 
974
1027
  const columns = useMemo(() => {
975
1028
  const base = withTMDataGridDefaults<TData>(
@@ -1000,7 +1053,7 @@ export function useTMDataGrid<TData extends RowData>({
1000
1053
  ...base,
1001
1054
  // Last and pinned right - the row's Save belongs at the end of the row.
1002
1055
  ...(editColumnEnabled
1003
- ? [createEditColumn<TData>(editColumnLabel, editIsDraftMode)]
1056
+ ? [createEditColumn<TData>(editColumnLabel, editWideLane)]
1004
1057
  : []),
1005
1058
  ];
1006
1059
  }, [
@@ -1010,7 +1063,7 @@ export function useTMDataGrid<TData extends RowData>({
1010
1063
  detailsColumnEnabled,
1011
1064
  groupColumnEnabled,
1012
1065
  editColumnEnabled,
1013
- editIsDraftMode,
1066
+ editWideLane,
1014
1067
  rowNumberColumnLabel,
1015
1068
  selectColumnLabel,
1016
1069
  groupColumnLabel,
@@ -1186,7 +1239,11 @@ export function useTMDataGrid<TData extends RowData>({
1186
1239
  editContextRef.current = {
1187
1240
  table: table as unknown as TMDataGridTable<TMDataGridRowData>,
1188
1241
  editMode: editMode ?? "cell",
1242
+ draft: editDraft,
1189
1243
  rowValidators: editing?.rowValidators,
1244
+ tableValidators: editing?.tableValidators as
1245
+ TMDataGridEditEngineContext["tableValidators"],
1246
+ editableColumnIds: editing?.columns,
1190
1247
  isRowEditable:
1191
1248
  editing?.isRowEditable as TMDataGridEditEngineContext["isRowEditable"],
1192
1249
  onEditCommit:
@@ -1209,15 +1266,18 @@ export function useTMDataGrid<TData extends RowData>({
1209
1266
  ) as unknown as TMDataGridEditApi<TData>,
1210
1267
  );
1211
1268
 
1212
- // Switching modes mid-flight drops every draft: the policies disagree about
1213
- // what an open form means, and carrying one across is how a draft parked
1214
- // under "draft" silently commits under "cell".
1215
- const previousEditModeRef = useRef(editMode);
1269
+ // Switching either axis mid-flight drops every draft: the policies disagree
1270
+ // about what an open form means, and carrying one across is how a row
1271
+ // parked for `saveDrafts` silently commits to the consumer instead.
1272
+ const previousEditPolicyRef = useRef({ editMode, editDraft });
1216
1273
  useEffect(() => {
1217
- if (previousEditModeRef.current === editMode) return;
1218
- previousEditModeRef.current = editMode;
1274
+ const previous = previousEditPolicyRef.current;
1275
+ if (previous.editMode === editMode && previous.editDraft === editDraft) {
1276
+ return;
1277
+ }
1278
+ previousEditPolicyRef.current = { editMode, editDraft };
1219
1279
  edit.cancelAll();
1220
- }, [editMode, edit]);
1280
+ }, [editMode, editDraft, edit]);
1221
1281
 
1222
1282
  // Editing without stable ids points every draft at whatever record slides
1223
1283
  // into that index after a sort. Loud, once, in development.
@@ -1,233 +0,0 @@
1
- import { Button, Group, Text } from "@mantine/core";
2
- import { useSelector } from "@tanstack/react-store";
3
- import type { ReactNode } from "react";
4
- import { useTMDataGridContext } from "../TMDataGridContext";
5
-
6
- /**
7
- * How many rows the draft store holds - committed edits, committed entry
8
- * rows and deletion marks. This is what Save sends, so it is what Save
9
- * counts: a row the user is still typing into is not in here.
10
- */
11
- function useDraftCount(): number {
12
- const { edit } = useTMDataGridContext();
13
- return useSelector(
14
- edit.store,
15
- (state) =>
16
- state.committedRowIds.length +
17
- state.newRows.filter((newRow) => newRow.committed).length +
18
- state.deletedRowIds.length,
19
- );
20
- }
21
-
22
- /**
23
- * How many rows are still open - edited or entered but not committed, so not
24
- * part of the save. Surfaced beside Save so an uncommitted row is visible
25
- * rather than silently left behind.
26
- */
27
- function useOpenCount(): number {
28
- const { edit } = useTMDataGridContext();
29
- return useSelector(
30
- edit.store,
31
- (state) =>
32
- state.openRowIds.filter(
33
- (rowId) =>
34
- !state.committedRowIds.includes(rowId) &&
35
- !state.newRows.some(
36
- (newRow) => newRow.tempId === rowId && newRow.committed,
37
- ) &&
38
- (state.newRows.some((newRow) => newRow.tempId === rowId) ||
39
- (state.rows[rowId]?.dirtyFields.length ?? 0) > 0),
40
- ).length,
41
- );
42
- }
43
-
44
- /*
45
- * Module scope, like the pagination controls and for the same reason: a
46
- * component defined inside the render body is a new type every render, so
47
- * React would remount the buttons underneath an interaction.
48
- */
49
-
50
- function EditSaveButton() {
51
- const { edit, labels } = useTMDataGridContext();
52
- const draftCount = useDraftCount();
53
- const isSubmitting = useSelector(edit.store, (state) =>
54
- state.openRowIds.some((rowId) => state.rows[rowId]?.isSubmitting === true),
55
- );
56
-
57
- return (
58
- <Button
59
- size="compact-sm"
60
- disabled={draftCount === 0}
61
- loading={isSubmitting}
62
- data-dg-part="save-all"
63
- data-draft-count={draftCount}
64
- onClick={() => void edit.saveDrafts()}
65
- >
66
- {labels.saveAllEdits(draftCount)}
67
- </Button>
68
- );
69
- }
70
-
71
- /** The count of rows left open, or nothing while every row is decided. */
72
- function EditOpenRowsNote() {
73
- const { labels } = useTMDataGridContext();
74
- const openCount = useOpenCount();
75
- if (openCount === 0) return null;
76
- return (
77
- <Text
78
- size="xs"
79
- c="dimmed"
80
- data-dg-part="open-rows-note"
81
- data-open-count={openCount}
82
- >
83
- {labels.editRowsStillOpen(openCount)}
84
- </Text>
85
- );
86
- }
87
-
88
- function EditDiscardButton() {
89
- const { edit, labels } = useTMDataGridContext();
90
- // Discard drops everything the grid is holding, open rows included, so it
91
- // stays live while anything is uncommitted.
92
- const pendingCount = useDraftCount() + useOpenCount();
93
-
94
- return (
95
- <Button
96
- variant="subtle"
97
- color="gray"
98
- size="compact-sm"
99
- disabled={pendingCount === 0}
100
- data-dg-part="discard-all"
101
- onClick={() => edit.cancelAll()}
102
- >
103
- {labels.discardAllEdits}
104
- </Button>
105
- );
106
- }
107
-
108
- const EDIT_ACTIONS_CONTROLS: TMDataGridEditActionsControls = {
109
- Save: EditSaveButton,
110
- Discard: EditDiscardButton,
111
- OpenRowsNote: EditOpenRowsNote,
112
- };
113
-
114
- /** What the edit chrome is showing. */
115
- export type TMDataGridEditActionsState = {
116
- /**
117
- * Rows in the draft store, which is what Save sends: committed edits,
118
- * committed entry rows and deletion marks.
119
- */
120
- draftCount: number;
121
- /**
122
- * Rows still open - edited or entered but not committed, so not part of
123
- * the save. They stay open across a save.
124
- */
125
- openCount: number;
126
- /**
127
- * @deprecated Was "everything uncommitted", which Save no longer sends.
128
- * Reads as `draftCount + openCount`; use whichever you meant.
129
- */
130
- pendingCount: number;
131
- /** Whether a submit is in flight. */
132
- isSubmitting: boolean;
133
- };
134
-
135
- /** What the edit chrome can do. */
136
- export type TMDataGridEditActionsActions = {
137
- /** Saves the draft store. Open rows are left alone. */
138
- save: () => Promise<boolean>;
139
- /** Submits every open row, committing the ones that validate. */
140
- commitAll: () => Promise<boolean>;
141
- /** Drops everything - open form state and the draft store alike. */
142
- discard: () => void;
143
- };
144
-
145
- /** The pre-bound pieces of the built-in edit chrome. */
146
- export type TMDataGridEditActionsControls = {
147
- /** Save, with the draft count, disabled while the draft store is empty. */
148
- Save: () => ReactNode;
149
- /** Discard, disabled while nothing is pending. */
150
- Discard: () => ReactNode;
151
- /** The "N rows still being edited" note, or nothing while there are none. */
152
- OpenRowsNote: () => ReactNode;
153
- };
154
-
155
- /** What {@link TMDataGridEditActionsProps.renderActions} is handed. */
156
- export type TMDataGridEditActionsSlotArgs = {
157
- state: TMDataGridEditActionsState;
158
- actions: TMDataGridEditActionsActions;
159
- Controls: TMDataGridEditActionsControls;
160
- };
161
-
162
- export type TMDataGridEditActionsProps = {
163
- /**
164
- * Replaces the built-in Save/Discard pair, and is handed the pieces of it.
165
- *
166
- * ```tsx
167
- * <TMDataGrid.EditActions
168
- * renderActions={({ state, Controls }) => (
169
- * <Group>
170
- * {state.draftCount > 0 && <Badge>{state.draftCount}</Badge>}
171
- * <Controls.OpenRowsNote />
172
- * <Controls.Save />
173
- * <Controls.Discard />
174
- * </Group>
175
- * )}
176
- * />
177
- * ```
178
- */
179
- renderActions?: (args: TMDataGridEditActionsSlotArgs) => ReactNode;
180
- };
181
-
182
- /**
183
- * Draft mode's toolbar chrome: Save with the draft-store count, Discard, and
184
- * a note counting rows still open. Save sends the draft store and leaves open
185
- * rows alone, so it greys out while nothing is committed however much is
186
- * being typed - the note is what makes those rows visible. Works under any
187
- * `editing.mode` and renders nothing while editing is off.
188
- *
189
- * ```tsx
190
- * <TMDataGrid.Toolbar>
191
- * <TMDataGrid.SummaryCount />
192
- * <TMDataGrid.Spacer />
193
- * <TMDataGrid.EditActions />
194
- * </TMDataGrid.Toolbar>
195
- * ```
196
- */
197
- export function TMDataGridEditActions({
198
- renderActions,
199
- }: TMDataGridEditActionsProps = {}) {
200
- const { edit, features } = useTMDataGridContext();
201
- const draftCount = useDraftCount();
202
- const openCount = useOpenCount();
203
- const isSubmitting = useSelector(edit.store, (state) =>
204
- state.openRowIds.some((rowId) => state.rows[rowId]?.isSubmitting === true),
205
- );
206
-
207
- if (!features.editing) return null;
208
-
209
- if (renderActions) {
210
- return renderActions({
211
- state: {
212
- draftCount,
213
- openCount,
214
- pendingCount: draftCount + openCount,
215
- isSubmitting,
216
- },
217
- actions: {
218
- save: () => edit.saveDrafts(),
219
- commitAll: () => edit.commitAll(),
220
- discard: () => edit.cancelAll(),
221
- },
222
- Controls: EDIT_ACTIONS_CONTROLS,
223
- });
224
- }
225
-
226
- return (
227
- <Group gap="xs" wrap="nowrap">
228
- <EditOpenRowsNote />
229
- <EditSaveButton />
230
- <EditDiscardButton />
231
- </Group>
232
- );
233
- }