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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/dist/index.d.ts +60 -23
  2. package/dist/index.js +2020 -1704
  3. package/dist/index.js.map +1 -1
  4. package/dist/styles.css +1 -1
  5. package/package.json +1 -1
  6. package/skills/appearance/SKILL.md +1 -1
  7. package/skills/cell-selection/SKILL.md +1 -1
  8. package/skills/columns/SKILL.md +2 -2
  9. package/skills/data/SKILL.md +1 -1
  10. package/skills/editing/SKILL.md +68 -213
  11. package/skills/editing/references/common-mistakes.md +177 -0
  12. package/skills/editing/references/editing-api.md +39 -13
  13. package/skills/editing/references/editors-and-validation.md +5 -2
  14. package/skills/filtering/SKILL.md +1 -1
  15. package/skills/getting-started/SKILL.md +1 -1
  16. package/skills/grouping/SKILL.md +1 -1
  17. package/skills/options/SKILL.md +2 -2
  18. package/skills/rows/SKILL.md +1 -1
  19. package/skills/rows/references/rows-api.md +1 -1
  20. package/skills/server-side/SKILL.md +1 -1
  21. package/skills/testing/SKILL.md +3 -2
  22. package/src/tmdatagrid/components/TMDataGrid.module.css +7 -0
  23. package/src/tmdatagrid/components/TMDataGridCellEditor.tsx +20 -8
  24. package/src/tmdatagrid/components/TMDataGridEditActions.tsx +1 -1
  25. package/src/tmdatagrid/components/TMDataGridEditColumn.tsx +242 -86
  26. package/src/tmdatagrid/components/TMDataGridEntryRows.tsx +176 -77
  27. package/src/tmdatagrid/components/TMDataGridTable.module.css +47 -3
  28. package/src/tmdatagrid/components/TMDataGridTable.tsx +44 -4
  29. package/src/tmdatagrid/components/icons.ts +1 -0
  30. package/src/tmdatagrid/core/capabilities.ts +10 -0
  31. package/src/tmdatagrid/core/draftCellContext.ts +68 -0
  32. package/src/tmdatagrid/core/editEngine.ts +106 -32
  33. package/src/tmdatagrid/core/labels.ts +13 -1
  34. package/src/tmdatagrid/core/labelsSv.ts +4 -0
  35. package/src/tmdatagrid/index.ts +1 -1
  36. package/src/tmdatagrid/useTMDataGrid.tsx +39 -20
@@ -22,7 +22,7 @@ import type { TMDataGridSize } from "./sizes";
22
22
  * How commits happen - one axis, each mode a thin policy over the same
23
23
  * engine. See `editMode` on `UseTMDataGridOptions`.
24
24
  */
25
- export type TMDataGridEditMode = "cell" | "cellConfirm" | "row" | "batch";
25
+ export type TMDataGridEditMode = "cell" | "cellConfirm" | "row" | "draft";
26
26
 
27
27
  /**
28
28
  * One editing row's live form. TanStack Form's `FormApi`, not a wrapper: the
@@ -113,6 +113,12 @@ export type TMDataGridEditRowProjection = {
113
113
  /** A row-level error - a pathless `.refine()`, or a rejected commit. */
114
114
  hasRowError: boolean;
115
115
  isSubmitting: boolean;
116
+ /**
117
+ * The row as drafted - the form's `state.values`. Reference-stable while no
118
+ * value changes, so a cell may subscribe to it and repaint only when its
119
+ * row's draft actually moves.
120
+ */
121
+ values: TMDataGridRowData;
116
122
  };
117
123
 
118
124
  /**
@@ -130,13 +136,18 @@ export type TMDataGridEditState = {
130
136
  active: { rowId: string; columnId: string | null } | null;
131
137
  /**
132
138
  * Rows with a live form. In cell mode at most one; in row, cellConfirm and
133
- * batch, as many as the user opened - which rows are editing.
139
+ * draft, as many as the user opened - which rows are editing.
134
140
  */
135
141
  openRowIds: ReadonlyArray<string>;
136
142
  rows: Record<string, TMDataGridEditRowProjection>;
137
- /** Phase 4 - rows being created, not yet in `data`. */
138
- newRows: ReadonlyArray<{ tempId: string }>;
139
- /** Phase 4 - rows marked deleted under batch mode. */
143
+ /**
144
+ * Rows being created, not yet in `data`. `confirmed` is draft mode's
145
+ * "entered, awaiting Save all": the entry row renders as a value row until
146
+ * `begin` re-opens it. Under the immediate modes a confirm commits through
147
+ * `onRowAdd` and the entry is dropped, so there it never turns `true`.
148
+ */
149
+ newRows: ReadonlyArray<{ tempId: string; confirmed: boolean }>;
150
+ /** Rows marked deleted under draft mode. */
140
151
  deletedRowIds: ReadonlyArray<string>;
141
152
  };
142
153
 
@@ -278,13 +289,13 @@ export type TMDataGridRowDeleteArgs<TData extends RowData> = {
278
289
  row: Row<TMDataGridFeatures, TData>;
279
290
  };
280
291
 
281
- /** What `submitAll` hands `onEditCommitBatch` - everything pending at once. */
282
- export type TMDataGridEditCommitBatchArgs<TData extends RowData> = {
292
+ /** What `submitAll` hands `onEditCommitDrafts` - everything pending at once. */
293
+ export type TMDataGridEditCommitDraftsArgs<TData extends RowData> = {
283
294
  /** Every valid dirty existing row. */
284
295
  rows: Array<TMDataGridEditCommitArgs<TData>>;
285
296
  /** Every valid new row from the entry block. */
286
297
  added: Array<TMDataGridRowAddArgs<TData>>;
287
- /** Ids marked deleted while the batch accumulated. */
298
+ /** Ids marked deleted while the drafts accumulated. */
288
299
  deleted: Array<string>;
289
300
  };
290
301
 
@@ -297,8 +308,8 @@ export type TMDataGridEditEngineContext = {
297
308
  onEditCommit?: (
298
309
  args: TMDataGridEditCommitArgs<TMDataGridRowData>,
299
310
  ) => void | Promise<void>;
300
- onEditCommitBatch?: (
301
- args: TMDataGridEditCommitBatchArgs<TMDataGridRowData>,
311
+ onEditCommitDrafts?: (
312
+ args: TMDataGridEditCommitDraftsArgs<TMDataGridRowData>,
302
313
  ) => void | Promise<void>;
303
314
  /** Seed values for `addRow`. A function is called per added row. */
304
315
  newRowDefaults?: TMDataGridRowData | (() => TMDataGridRowData);
@@ -387,7 +398,7 @@ export type TMDataGridEditApi = {
387
398
  deactivate: () => void;
388
399
  /** Drops every draft. */
389
400
  cancelAll: () => void;
390
- /** Commits every open row - batch mode's save. `true` when all landed. */
401
+ /** Commits every open row - draft mode's save. `true` when all landed. */
391
402
  submitAll: () => Promise<boolean>;
392
403
  /** Writes the type's empty value into a cell and commits it - Delete. */
393
404
  clearCell: (rowId: string, columnId: string) => Promise<boolean>;
@@ -395,12 +406,12 @@ export type TMDataGridEditApi = {
395
406
  * Opens a new entry row (the sticky block under the header) seeded from
396
407
  * `newRowDefaults`. Returns its temporary id - a form with no backing row
397
408
  * yet. Committing it calls `onRowAdd` (immediate modes) or joins
398
- * `submitAll`'s `added` (batch).
409
+ * `submitAll`'s `added` (draft).
399
410
  */
400
411
  addRow: () => string;
401
412
  /**
402
413
  * Deletes a row: `onRowDelete` straight away under the immediate modes;
403
- * under batch it toggles the id in `deletedRowIds` - the row renders
414
+ * under draft it toggles the id in `deletedRowIds` - the row renders
404
415
  * struck through until `submitAll` reports it. On an uncommitted entry
405
416
  * row it just discards the entry.
406
417
  */
@@ -467,6 +478,13 @@ export function createEditEngine(
467
478
  isNew: boolean;
468
479
  /** Set by the wrapped onSubmit when the consumer's commit resolved. */
469
480
  lastSubmitOk: boolean;
481
+ /**
482
+ * Draft mode's park: this submit validates and holds the draft in the
483
+ * grid instead of calling the consumer. Set by `commit` per attempt -
484
+ * `true` only under draft outside `submitAll`, which is what closes the
485
+ * per-row escape hatches (the lane's ✓, Delete-to-clear) at the engine.
486
+ */
487
+ parkOnly: boolean;
470
488
  /** A commit already running - Enter and blur race on the same edit. */
471
489
  pendingCommit: Promise<boolean> | null;
472
490
  unsubscribe: () => void;
@@ -474,15 +492,17 @@ export function createEditEngine(
474
492
  };
475
493
  const forms = new Map<string, FormEntry>();
476
494
  let newRowCounter = 0;
495
+ /** Lets `commit` tell `submitAll`'s per-row loop apart from a lone call. */
496
+ let submitAllRunning = false;
477
497
 
478
498
  /**
479
- * While `submitAll` runs with an `onEditCommitBatch`, each row's wrapped
499
+ * While `submitAll` runs with an `onEditCommitDrafts`, each row's wrapped
480
500
  * onSubmit contributes its args here instead of calling `onEditCommit` -
481
501
  * validation stays per row (Form's), the consumer call becomes one.
482
502
  */
483
- let batchCollector: Array<TMDataGridEditCommitArgs<TMDataGridRowData>> | null =
503
+ let draftCollector: Array<TMDataGridEditCommitArgs<TMDataGridRowData>> | null =
484
504
  null;
485
- let batchAddCollector: Array<TMDataGridRowAddArgs<TMDataGridRowData>> | null =
505
+ let draftAddCollector: Array<TMDataGridRowAddArgs<TMDataGridRowData>> | null =
486
506
  null;
487
507
 
488
508
  const editableColumns = (): Array<ErasedColumn> =>
@@ -530,6 +550,7 @@ export function createEditEngine(
530
550
  errorFields,
531
551
  hasRowError: hasAnyError(state.errors),
532
552
  isSubmitting: state.isSubmitting,
553
+ values: state.values as TMDataGridRowData,
533
554
  };
534
555
  };
535
556
 
@@ -547,6 +568,19 @@ export function createEditEngine(
547
568
  store.setState((prev) => ({ ...prev, active }));
548
569
  };
549
570
 
571
+ const setNewRowConfirmed = (tempId: string, confirmed: boolean) => {
572
+ store.setState((prev) => {
573
+ const target = prev.newRows.find((newRow) => newRow.tempId === tempId);
574
+ if (target === undefined || target.confirmed === confirmed) return prev;
575
+ return {
576
+ ...prev,
577
+ newRows: prev.newRows.map((newRow) =>
578
+ newRow.tempId === tempId ? { ...newRow, confirmed } : newRow,
579
+ ),
580
+ };
581
+ });
582
+ };
583
+
550
584
  const drop = (rowId: string) => {
551
585
  const entry = forms.get(rowId);
552
586
  if (entry === undefined) return;
@@ -578,6 +612,7 @@ export function createEditEngine(
578
612
  original,
579
613
  isNew,
580
614
  lastSubmitOk: false,
615
+ parkOnly: false,
581
616
  pendingCommit: null,
582
617
  unsubscribe: () => {},
583
618
  unmount: () => {},
@@ -594,8 +629,13 @@ export function createEditEngine(
594
629
  tempId: rowId,
595
630
  value: value as TMDataGridRowData,
596
631
  };
597
- if (batchAddCollector !== null) {
598
- batchAddCollector.push(addArgs);
632
+ if (draftAddCollector !== null) {
633
+ draftAddCollector.push(addArgs);
634
+ entry.lastSubmitOk = true;
635
+ return;
636
+ }
637
+ // Parked: validated and held for Save all - no consumer call.
638
+ if (entry.parkOnly) {
599
639
  entry.lastSubmitOk = true;
600
640
  return;
601
641
  }
@@ -618,8 +658,13 @@ export function createEditEngine(
618
658
  changes,
619
659
  source: getContext().editMode,
620
660
  };
621
- if (batchCollector !== null) {
622
- batchCollector.push(args);
661
+ if (draftCollector !== null) {
662
+ draftCollector.push(args);
663
+ entry.lastSubmitOk = true;
664
+ return;
665
+ }
666
+ // Parked: validated and held for Save all - no consumer call.
667
+ if (entry.parkOnly) {
623
668
  entry.lastSubmitOk = true;
624
669
  return;
625
670
  }
@@ -691,6 +736,7 @@ export function createEditEngine(
691
736
  // block `canSubmit` forever. The validator puts back whatever still holds.
692
737
  clearFormSourcedFieldErrors(entry.form);
693
738
  entry.lastSubmitOk = false;
739
+ entry.parkOnly = getContext().editMode === "draft" && !submitAllRunning;
694
740
  entry.pendingCommit = (async () => {
695
741
  try {
696
742
  await entry.form.handleSubmit();
@@ -698,6 +744,15 @@ export function createEditEngine(
698
744
  entry.pendingCommit = null;
699
745
  }
700
746
  if (!entry.lastSubmitOk) return false;
747
+ if (entry.parkOnly) {
748
+ // The draft stays in the grid until Save all. An entry row becomes
749
+ // "entered": rendered as a value row until `begin` re-opens it.
750
+ if (entry.isNew) setNewRowConfirmed(rowId, true);
751
+ store.setState((prev) =>
752
+ prev.active?.rowId === rowId ? { ...prev, active: null } : prev,
753
+ );
754
+ return true;
755
+ }
701
756
  drop(rowId);
702
757
  return true;
703
758
  })();
@@ -705,6 +760,14 @@ export function createEditEngine(
705
760
  };
706
761
 
707
762
  const beginOn = (rowId: string, columnId: string | null) => {
763
+ // An entry row has no backing row in the table; opening one re-arms its
764
+ // editors - under draft, that is how a confirmed row is edited again.
765
+ const entryForm = forms.get(rowId);
766
+ if (entryForm?.isNew === true) {
767
+ setNewRowConfirmed(rowId, false);
768
+ setActive({ rowId, columnId });
769
+ return;
770
+ }
708
771
  const row = getRow(rowId);
709
772
  if (row === undefined) return;
710
773
  if (columnId !== null) {
@@ -724,7 +787,7 @@ export function createEditEngine(
724
787
  // | Mode | Another row open |
725
788
  // | ---- | ---------------- |
726
789
  // | cell | committed - Sheets; a failed commit keeps holding the edit |
727
- // | row, cellConfirm, batch | accumulates. Every draft waits for its own
790
+ // | row, cellConfirm, draft | accumulates. Every draft waits for its own
728
791
  // save, so a second row opening can neither discard the first nor be
729
792
  // refused by it - nothing about one row's form bears on another's |
730
793
  if (editMode === "cell") {
@@ -763,7 +826,7 @@ export function createEditEngine(
763
826
  createForm(tempId, defaults, true);
764
827
  store.setState((prev) => ({
765
828
  ...prev,
766
- newRows: [...prev.newRows, { tempId }],
829
+ newRows: [...prev.newRows, { tempId, confirmed: false }],
767
830
  }));
768
831
  return tempId;
769
832
  };
@@ -776,7 +839,7 @@ export function createEditEngine(
776
839
  return;
777
840
  }
778
841
  const context = getContext();
779
- if (context.editMode === "batch") {
842
+ if (context.editMode === "draft") {
780
843
  // A toggle: the second press unmarks - the mark is a draft too.
781
844
  store.setState((prev) => ({
782
845
  ...prev,
@@ -794,10 +857,10 @@ export function createEditEngine(
794
857
 
795
858
  const canDeleteRows = (): boolean => {
796
859
  const context = getContext();
797
- if (context.editMode === "batch") {
860
+ if (context.editMode === "draft") {
798
861
  return (
799
862
  context.onRowDelete !== undefined ||
800
- context.onEditCommitBatch !== undefined
863
+ context.onEditCommitDrafts !== undefined
801
864
  );
802
865
  }
803
866
  return context.onRowDelete !== undefined;
@@ -813,7 +876,18 @@ export function createEditEngine(
813
876
  };
814
877
 
815
878
  const submitAll = async (): Promise<boolean> => {
816
- if (getContext().onEditCommitBatch === undefined) {
879
+ // While this runs, `commit` really commits - under draft it parks
880
+ // otherwise. Single-threaded flag, same idiom as the collectors.
881
+ submitAllRunning = true;
882
+ try {
883
+ return await submitAllInner();
884
+ } finally {
885
+ submitAllRunning = false;
886
+ }
887
+ };
888
+
889
+ const submitAllInner = async (): Promise<boolean> => {
890
+ if (getContext().onEditCommitDrafts === undefined) {
817
891
  // The default: the per-row loop - edits through `onEditCommit`, entry
818
892
  // rows through `onRowAdd`, marked deletions through `onRowDelete`.
819
893
  const results = await Promise.all(
@@ -830,12 +904,12 @@ export function createEditEngine(
830
904
 
831
905
  // One consumer call for the lot. Validation stays Form's, per row: rows
832
906
  // that fail keep their forms and markers; the valid ones travel
833
- // together, and only a resolved batch drops them - a rejected save keeps
907
+ // together, and only a resolved save drops them - a rejected save keeps
834
908
  // every draft, deletions included.
835
909
  const collected: Array<TMDataGridEditCommitArgs<TMDataGridRowData>> = [];
836
910
  const added: Array<TMDataGridRowAddArgs<TMDataGridRowData>> = [];
837
- batchCollector = collected;
838
- batchAddCollector = added;
911
+ draftCollector = collected;
912
+ draftAddCollector = added;
839
913
  let allValid = true;
840
914
  try {
841
915
  for (const rowId of [...forms.keys()]) {
@@ -853,13 +927,13 @@ export function createEditEngine(
853
927
  if (!entry.lastSubmitOk) allValid = false;
854
928
  }
855
929
  } finally {
856
- batchCollector = null;
857
- batchAddCollector = null;
930
+ draftCollector = null;
931
+ draftAddCollector = null;
858
932
  }
859
933
  const deleted = [...store.state.deletedRowIds];
860
934
  if (collected.length > 0 || added.length > 0 || deleted.length > 0) {
861
935
  try {
862
- await getContext().onEditCommitBatch?.({ rows: collected, added, deleted });
936
+ await getContext().onEditCommitDrafts?.({ rows: collected, added, deleted });
863
937
  } catch {
864
938
  return false;
865
939
  }
@@ -121,8 +121,16 @@ export type TMDataGridLabels = {
121
121
  discardNewRow: string;
122
122
  /** The lane's trash can. */
123
123
  deleteRow: string;
124
- /** The lane's undo on a row marked deleted (batch). */
124
+ /** The lane's undo on a row marked deleted (draft). */
125
125
  restoreRow: string;
126
+ /** The lane's undo on a row with a dirty draft (draft) - drops it. */
127
+ revertRow: string;
128
+ /** The lane's state icon on a confirmed entry row (draft). */
129
+ rowStateNew: string;
130
+ /** The lane's state icon on a row with a dirty draft (draft). */
131
+ rowStateEdited: string;
132
+ /** The lane's state icon on a row marked deleted (draft). */
133
+ rowStateDeleted: string;
126
134
 
127
135
  // Cell selection menu
128
136
  cellCount: (count: number) => string;
@@ -239,6 +247,10 @@ export const TMDATAGRID_LABELS_EN: TMDataGridLabels = {
239
247
  discardNewRow: "Discard new row",
240
248
  deleteRow: "Delete row",
241
249
  restoreRow: "Restore row",
250
+ revertRow: "Revert changes",
251
+ rowStateNew: "New row",
252
+ rowStateEdited: "Edited row",
253
+ rowStateDeleted: "Marked for deletion",
242
254
 
243
255
  cellCount: (count) => (count === 1 ? "1 cell" : `${count} cells`),
244
256
  copy: "Copy",
@@ -107,6 +107,10 @@ export const TMDATAGRID_LABELS_SV: TMDataGridLabels = {
107
107
  discardNewRow: "Släng ny rad",
108
108
  deleteRow: "Ta bort rad",
109
109
  restoreRow: "Återställ rad",
110
+ revertRow: "Ångra ändringar",
111
+ rowStateNew: "Ny rad",
112
+ rowStateEdited: "Ändrad rad",
113
+ rowStateDeleted: "Markerad för borttagning",
110
114
 
111
115
  cellCount: (count) => (count === 1 ? "1 cell" : `${count} celler`),
112
116
  copy: "Kopiera",
@@ -82,7 +82,7 @@ export {
82
82
  type TMDataGridEditApi,
83
83
  type TMDataGridEditChange,
84
84
  type TMDataGridEditCommitArgs,
85
- type TMDataGridEditCommitBatchArgs,
85
+ type TMDataGridEditCommitDraftsArgs,
86
86
  type TMDataGridEditField,
87
87
  type TMDataGridEditMode,
88
88
  type TMDataGridEditorArgs,
@@ -51,7 +51,7 @@ import {
51
51
  createEditEngine,
52
52
  type TMDataGridEditApi,
53
53
  type TMDataGridEditCommitArgs,
54
- type TMDataGridEditCommitBatchArgs,
54
+ type TMDataGridEditCommitDraftsArgs,
55
55
  type TMDataGridEditEngineContext,
56
56
  type TMDataGridColumnEditOptions,
57
57
  type TMDataGridEditMode,
@@ -475,13 +475,13 @@ type TMDataGridEditingCallbacks<TData extends RowData> = {
475
475
  newRowDefaults?: TData | (() => TData);
476
476
  /**
477
477
  * Called when an entry row commits: `Enter` or the lane's ✓ under the
478
- * immediate modes, `submitAll` under batch. Create the record and let it
478
+ * immediate modes, `submitAll` under draft. Create the record and let it
479
479
  * arrive back through `data`; the engine's `tempId` never leaves the grid.
480
480
  */
481
481
  onRowAdd?: (args: TMDataGridRowAddArgs<TData>) => void | Promise<void>;
482
482
  /**
483
483
  * Called by `edit.deleteRow` under the immediate modes - confirmation, if
484
- * any, belongs in here. Under batch, deletions accumulate in
484
+ * any, belongs in here. Under draft, deletions accumulate in
485
485
  * `edit.state.deletedRowIds` instead and are reported by `submitAll`.
486
486
  * Setting this also puts the trash can in the edit lane.
487
487
  */
@@ -498,11 +498,11 @@ type TMDataGridEditingCallbacks<TData extends RowData> = {
498
498
  * | `"cell"` | Enter, Tab, blur - Sheets | Escape |
499
499
  * | `"cellConfirm"` | ✓ or Enter only; blur keeps the draft | ✕ or Escape |
500
500
  * | `"row"` | Save in the edit lane, or Ctrl+Enter | Cancel, or Escape |
501
- * | `"batch"` | `edit.submitAll()` | `edit.cancelAll()` |
501
+ * | `"draft"` | `edit.submitAll()` | `edit.cancelAll()` |
502
502
  *
503
503
  * Setting `editing` makes `getRowId` required - drafts are keyed by row id,
504
504
  * and the index fallback would name a different record after any sort - and
505
- * `onCommitBatch` exists only under `mode: "batch"`, the one mode whose
505
+ * `onCommitDrafts` exists only under `mode: "draft"`, the one mode whose
506
506
  * `submitAll` calls it.
507
507
  *
508
508
  * The object may be written inline: the callbacks are read through a ref
@@ -518,21 +518,31 @@ export type TMDataGridEditingOptions<TData extends RowData> =
518
518
  TMDataGridEditingCallbacks<TData> &
519
519
  (
520
520
  | {
521
- mode: "batch";
521
+ mode: "draft";
522
522
  /**
523
- * Batch mode's save, called once by `edit.submitAll()` with every
523
+ * Draft mode's save, called once by `edit.submitAll()` with every
524
524
  * valid dirty row. Without it, `submitAll` falls back to the per-row
525
525
  * {@link TMDataGridEditingCallbacks.onCommit} loop. Rows failing
526
526
  * validation stay open either way; a rejection keeps every draft.
527
527
  */
528
- onCommitBatch?: (
529
- args: TMDataGridEditCommitBatchArgs<TData>,
528
+ onCommitDrafts?: (
529
+ args: TMDataGridEditCommitDraftsArgs<TData>,
530
530
  ) => void | Promise<void>;
531
+ /**
532
+ * Keep confirmed entry rows pinned in the sticky entry block until
533
+ * Save all. Off by default: a confirmed row joins the scrolling
534
+ * flow above the body rows instead - the block a row is *typed*
535
+ * into is always sticky, but entered rows scroll, so entering many
536
+ * cannot fill the viewport with sticky chrome.
537
+ */
538
+ newRowsSticky?: boolean;
531
539
  }
532
540
  | {
533
- mode: Exclude<TMDataGridEditMode, "batch">;
534
- /** Only `"batch"`'s `submitAll` ever calls it - see the other branch. */
535
- onCommitBatch?: never;
541
+ mode: Exclude<TMDataGridEditMode, "draft">;
542
+ /** Only `"draft"`'s `submitAll` ever calls it - see the other branch. */
543
+ onCommitDrafts?: never;
544
+ /** Confirmed entry rows exist only under `"draft"` - see there. */
545
+ newRowsSticky?: never;
536
546
  }
537
547
  );
538
548
 
@@ -910,12 +920,15 @@ export function useTMDataGrid<TData extends RowData>({
910
920
  // `renderDetails` has nothing for it to open.
911
921
  const detailsColumnEnabled = renderDetails !== undefined;
912
922
  // Row mode's Save sits at the end of the row - the lane is its chrome. It
913
- // also appears wherever the trash can has somewhere to report to.
923
+ // also appears wherever the trash can has somewhere to report to, and
924
+ // always under draft mode, where it is the change marker and the per-row
925
+ // revert - with or without `onCommitDrafts`, since the per-row `submitAll`
926
+ // fallback is a first-class configuration.
914
927
  const editColumnEnabled =
915
928
  editing !== undefined &&
916
929
  (editing.mode === "row" ||
917
- editing.onRowDelete !== undefined ||
918
- (editing.mode === "batch" && editing.onCommitBatch !== undefined));
930
+ editing.mode === "draft" ||
931
+ editing.onRowDelete !== undefined);
919
932
 
920
933
  // The generated lanes bake `meta.label` into their definitions, so the memo
921
934
  // depends on the strings rather than on the labels object - a fresh
@@ -926,6 +939,9 @@ export function useTMDataGrid<TData extends RowData>({
926
939
  const editColumnLabel = labels.editColumnLabel;
927
940
  const rowNumberColumnLabel = labels.rowNumberColumnLabel;
928
941
  const rowNumbersEnabled = features.rowNumbers;
942
+ // Draft mode's lane holds three controls (state icon, undo, trash) where
943
+ // the other modes hold two - it gets the wider track.
944
+ const editIsDraftMode = editing?.mode === "draft";
929
945
 
930
946
  const columns = useMemo(() => {
931
947
  const base = withTMDataGridDefaults<TData>(
@@ -955,7 +971,9 @@ export function useTMDataGrid<TData extends RowData>({
955
971
  : []),
956
972
  ...base,
957
973
  // Last and pinned right - the row's Save belongs at the end of the row.
958
- ...(editColumnEnabled ? [createEditColumn<TData>(editColumnLabel)] : []),
974
+ ...(editColumnEnabled
975
+ ? [createEditColumn<TData>(editColumnLabel, editIsDraftMode)]
976
+ : []),
959
977
  ];
960
978
  }, [
961
979
  options.columns,
@@ -964,6 +982,7 @@ export function useTMDataGrid<TData extends RowData>({
964
982
  detailsColumnEnabled,
965
983
  groupColumnEnabled,
966
984
  editColumnEnabled,
985
+ editIsDraftMode,
967
986
  rowNumberColumnLabel,
968
987
  selectColumnLabel,
969
988
  groupColumnLabel,
@@ -1090,8 +1109,8 @@ export function useTMDataGrid<TData extends RowData>({
1090
1109
  editing?.isRowEditable as TMDataGridEditEngineContext["isRowEditable"],
1091
1110
  onEditCommit:
1092
1111
  editing?.onCommit as TMDataGridEditEngineContext["onEditCommit"],
1093
- onEditCommitBatch:
1094
- editing?.onCommitBatch as TMDataGridEditEngineContext["onEditCommitBatch"],
1112
+ onEditCommitDrafts:
1113
+ editing?.onCommitDrafts as TMDataGridEditEngineContext["onEditCommitDrafts"],
1095
1114
  newRowDefaults:
1096
1115
  editing?.newRowDefaults as TMDataGridEditEngineContext["newRowDefaults"],
1097
1116
  onRowAdd: editing?.onRowAdd as TMDataGridEditEngineContext["onRowAdd"],
@@ -1103,8 +1122,8 @@ export function useTMDataGrid<TData extends RowData>({
1103
1122
  );
1104
1123
 
1105
1124
  // Switching modes mid-flight drops every draft: the policies disagree about
1106
- // what an open form means, and carrying one across is how a "batch" draft
1107
- // silently commits under "cell".
1125
+ // what an open form means, and carrying one across is how a draft parked
1126
+ // under "draft" silently commits under "cell".
1108
1127
  const previousEditModeRef = useRef(editMode);
1109
1128
  useEffect(() => {
1110
1129
  if (previousEditModeRef.current === editMode) return;