@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.
- package/dist/index.d.ts +339 -152
- package/dist/index.js +2602 -2450
- package/dist/index.js.map +1 -1
- package/dist/styles.css +1 -1
- package/package.json +1 -1
- package/skills/appearance/SKILL.md +1 -1
- package/skills/cell-selection/SKILL.md +1 -1
- package/skills/columns/SKILL.md +44 -2
- package/skills/data/SKILL.md +14 -10
- package/skills/editing/SKILL.md +140 -68
- package/skills/editing/references/common-mistakes.md +98 -8
- package/skills/editing/references/editing-api.md +40 -30
- package/skills/editing/references/editors-and-validation.md +65 -14
- package/skills/filtering/SKILL.md +1 -1
- package/skills/getting-started/SKILL.md +3 -3
- package/skills/grouping/SKILL.md +5 -3
- package/skills/options/SKILL.md +2 -2
- package/skills/rows/SKILL.md +7 -3
- package/skills/server-side/SKILL.md +1 -1
- package/skills/testing/SKILL.md +1 -1
- package/src/tmdatagrid/components/TMDataGrid.tsx +2 -2
- package/src/tmdatagrid/components/TMDataGridCellEditor.tsx +33 -34
- package/src/tmdatagrid/components/TMDataGridDraftActions.tsx +296 -0
- package/src/tmdatagrid/components/TMDataGridEditColumn.tsx +40 -33
- package/src/tmdatagrid/components/TMDataGridEntryRows.tsx +7 -7
- package/src/tmdatagrid/components/TMDataGridTable.tsx +27 -5
- package/src/tmdatagrid/components/TMDataGridToolbar.module.css +4 -0
- package/src/tmdatagrid/components/TMDataGridToolbar.tsx +8 -10
- package/src/tmdatagrid/components/editors/TMDataGridNumberEditor.tsx +34 -4
- package/src/tmdatagrid/components/editors/editorShared.ts +1 -29
- package/src/tmdatagrid/core/capabilities.ts +14 -6
- package/src/tmdatagrid/core/editEngine.ts +472 -96
- package/src/tmdatagrid/core/labels.ts +3 -3
- package/src/tmdatagrid/core/summary.ts +10 -2
- package/src/tmdatagrid/index.ts +9 -7
- package/src/tmdatagrid/useTMDataGrid.tsx +107 -47
- 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
|
-
/** `
|
|
114
|
+
/** `DraftActions`' Save, with the count of rows in the draft store. */
|
|
115
115
|
saveAllEdits: (rows: number) => string;
|
|
116
|
-
/** `
|
|
116
|
+
/** `DraftActions`' Discard. */
|
|
117
117
|
discardAllEdits: string;
|
|
118
118
|
/**
|
|
119
|
-
* `
|
|
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
|
-
|
|
33
|
-
//
|
|
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
|
}
|
package/src/tmdatagrid/index.ts
CHANGED
|
@@ -67,13 +67,13 @@ export {
|
|
|
67
67
|
type TMDataGridOptionsSource,
|
|
68
68
|
} from "./core/columnOptions";
|
|
69
69
|
export {
|
|
70
|
-
|
|
71
|
-
type
|
|
72
|
-
type
|
|
73
|
-
type
|
|
74
|
-
type
|
|
75
|
-
type
|
|
76
|
-
} from "./components/
|
|
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
|
|
485
|
-
*
|
|
486
|
-
*
|
|
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`
|
|
491
|
-
*
|
|
492
|
-
*
|
|
493
|
-
*
|
|
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.
|
|
501
|
-
*
|
|
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
|
-
*
|
|
504
|
-
*
|
|
505
|
-
*
|
|
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 `
|
|
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
|
-
*
|
|
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
|
|
559
|
-
* the draft store is saved. Off by default: a
|
|
560
|
-
*
|
|
561
|
-
*
|
|
562
|
-
*
|
|
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
|
-
|
|
568
|
-
|
|
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
|
-
/**
|
|
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
|
|
953
|
-
// revert - with or without `
|
|
954
|
-
//
|
|
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
|
-
//
|
|
971
|
-
//
|
|
972
|
-
const
|
|
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,
|
|
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
|
-
|
|
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
|
|
1213
|
-
// what an open form means, and carrying one across is how a
|
|
1214
|
-
//
|
|
1215
|
-
const
|
|
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
|
-
|
|
1218
|
-
|
|
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
|
-
}
|