@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
|
@@ -19,10 +19,21 @@ import type { TMDataGridColumnType } from "./filterOperators";
|
|
|
19
19
|
import type { TMDataGridSize } from "./sizes";
|
|
20
20
|
|
|
21
21
|
/**
|
|
22
|
-
*
|
|
23
|
-
* engine.
|
|
22
|
+
* What counts as a commit - one axis, each mode a thin policy over the same
|
|
23
|
+
* engine. Where a commit *goes* is the other axis, `editing.draft`: out to
|
|
24
|
+
* the consumer, or into the draft store. See `editing` on
|
|
25
|
+
* `UseTMDataGridOptions`.
|
|
26
|
+
*
|
|
27
|
+
* | Mode | Enter | Tab | Focus leaves | Escape |
|
|
28
|
+
* | ---- | ----- | --- | ------------ | ------ |
|
|
29
|
+
* | `"cell"` | commits | commits, caret moves on | commits | cancels |
|
|
30
|
+
* | `"cellConfirm"` | commits | keeps the draft, caret moves on | keeps the draft | cancels |
|
|
31
|
+
* | `"row"` | commits the row | the browser's, along the row | nothing | cancels the row |
|
|
32
|
+
*
|
|
33
|
+
* An entry row is row-shaped in every mode - every editable cell open at
|
|
34
|
+
* once, the browser's Tab between them, and an explicit ✓.
|
|
24
35
|
*/
|
|
25
|
-
export type TMDataGridEditMode = "cell" | "cellConfirm" | "row"
|
|
36
|
+
export type TMDataGridEditMode = "cell" | "cellConfirm" | "row";
|
|
26
37
|
|
|
27
38
|
/**
|
|
28
39
|
* One editing row's live form. TanStack Form's `FormApi`, not a wrapper: the
|
|
@@ -82,6 +93,45 @@ export type TMDataGridRowValidators = {
|
|
|
82
93
|
onSubmitAsyncDebounceMs?: number;
|
|
83
94
|
};
|
|
84
95
|
|
|
96
|
+
/** What `editing.tableValidators` is handed for one commit. */
|
|
97
|
+
export type TMDataGridTableValidateArgs<
|
|
98
|
+
TData extends RowData = TMDataGridRowData,
|
|
99
|
+
> = {
|
|
100
|
+
/** The committing row, as drafted. */
|
|
101
|
+
value: TData;
|
|
102
|
+
/** Its id - `addRow`'s temp id for an entry row. */
|
|
103
|
+
rowId: string;
|
|
104
|
+
/** Whether the row is an entry row, not yet in `data`. */
|
|
105
|
+
isNew: boolean;
|
|
106
|
+
/**
|
|
107
|
+
* The collection as it would stand if this commit landed: every data row
|
|
108
|
+
* overlaid with its draft where one is held (this row's `value` included),
|
|
109
|
+
* entry rows appended, deletion-marked rows removed. Unfiltered - a rule
|
|
110
|
+
* sees the whole collection whatever the view shows.
|
|
111
|
+
*/
|
|
112
|
+
rows: ReadonlyArray<{ rowId: string; value: TData }>;
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* `editing.tableValidators` - rules that need the other rows: no duplicate
|
|
117
|
+
* keys, no overlapping ranges, allocations summing to a total. Run at every
|
|
118
|
+
* commit, after the row's own validators, and again per parked row during
|
|
119
|
+
* `saveDrafts` - so a draft invalidated by a later edit blocks the save.
|
|
120
|
+
*
|
|
121
|
+
* Return nothing to pass, a message, or Form's `{ form, fields }` shape;
|
|
122
|
+
* pathed issues land on the committing row's cells, pathless ones on the row.
|
|
123
|
+
* `onSubmit` runs first, and its failure stands without `onSubmitAsync`
|
|
124
|
+
* running.
|
|
125
|
+
*/
|
|
126
|
+
export type TMDataGridTableValidators<
|
|
127
|
+
TData extends RowData = TMDataGridRowData,
|
|
128
|
+
> = {
|
|
129
|
+
onSubmit?: (args: TMDataGridTableValidateArgs<TData>) => unknown;
|
|
130
|
+
onSubmitAsync?: (
|
|
131
|
+
args: TMDataGridTableValidateArgs<TData>,
|
|
132
|
+
) => unknown | Promise<unknown>;
|
|
133
|
+
};
|
|
134
|
+
|
|
85
135
|
/** One field's before/after across a commit. */
|
|
86
136
|
export type TMDataGridEditChange = {
|
|
87
137
|
/** Column the field maps back to, for consumers thinking in columns. */
|
|
@@ -108,8 +158,17 @@ export type TMDataGridEditCommitArgs<TData extends RowData> = {
|
|
|
108
158
|
export type TMDataGridEditRowProjection = {
|
|
109
159
|
/** Field names whose value differs from the original. */
|
|
110
160
|
dirtyFields: ReadonlyArray<string>;
|
|
111
|
-
/** Field names carrying a validation error. */
|
|
161
|
+
/** Field names carrying a validation error, live or from a failed commit. */
|
|
112
162
|
errorFields: ReadonlyArray<string>;
|
|
163
|
+
/**
|
|
164
|
+
* Those errors as text: what each cell editor is showing, plus what the
|
|
165
|
+
* row's last failed commit found. The second half is the reason this
|
|
166
|
+
* exists - Form clears a field's errors when its editor unmounts, so a row
|
|
167
|
+
* left invalid would go back to looking like an ordinary edited row. Such
|
|
168
|
+
* a message is dropped as soon as its field's value moves: the fix is what
|
|
169
|
+
* clears the mark.
|
|
170
|
+
*/
|
|
171
|
+
errorMessages: ReadonlyArray<{ field: string; message: string }>;
|
|
113
172
|
/** A row-level error - a pathless `.refine()`, or a rejected commit. */
|
|
114
173
|
hasRowError: boolean;
|
|
115
174
|
isSubmitting: boolean;
|
|
@@ -147,19 +206,18 @@ export type TMDataGridEditState = {
|
|
|
147
206
|
* values stay in the row's form, this records which side of the line the
|
|
148
207
|
* row is on. `begin` on one of these takes it back out, into form state.
|
|
149
208
|
*
|
|
150
|
-
* Only draft
|
|
151
|
-
* consumer and
|
|
209
|
+
* Only `editing.draft` parks. Without it a commit goes straight to the
|
|
210
|
+
* consumer and the form is dropped, so this stays empty.
|
|
152
211
|
*/
|
|
153
212
|
committedRowIds: ReadonlyArray<string>;
|
|
154
213
|
/**
|
|
155
214
|
* Rows being created, not yet in `data`. `committed` is the draft store's
|
|
156
215
|
* add slice: the entry row passed its submit and renders as a value row
|
|
157
|
-
* until `begin` re-opens it.
|
|
158
|
-
*
|
|
159
|
-
* `true`.
|
|
216
|
+
* until `begin` re-opens it. Without `editing.draft` a commit adds through
|
|
217
|
+
* `onRowAdd` and the entry is dropped, so it never turns `true`.
|
|
160
218
|
*/
|
|
161
219
|
newRows: ReadonlyArray<{ tempId: string; committed: boolean }>;
|
|
162
|
-
/** The draft store's delete slice: rows marked deleted
|
|
220
|
+
/** The draft store's delete slice: rows marked deleted, awaiting the save. */
|
|
163
221
|
deletedRowIds: ReadonlyArray<string>;
|
|
164
222
|
};
|
|
165
223
|
|
|
@@ -172,6 +230,30 @@ const EMPTY_EDIT_STATE: TMDataGridEditState = {
|
|
|
172
230
|
deletedRowIds: [],
|
|
173
231
|
};
|
|
174
232
|
|
|
233
|
+
/**
|
|
234
|
+
* The rows still *open*: holding a live form nobody has decided yet.
|
|
235
|
+
*
|
|
236
|
+
* Narrower than {@link TMDataGridEditState.openRowIds}, which is every row
|
|
237
|
+
* with a form, the parked ones included. A row qualifies here when it is not
|
|
238
|
+
* parked in the draft store and there is something to lose - an entered row
|
|
239
|
+
* always counts, an existing one only once a value has moved.
|
|
240
|
+
*
|
|
241
|
+
* The order is the engine's: the order the forms were opened.
|
|
242
|
+
*/
|
|
243
|
+
export function getOpenRowIds(
|
|
244
|
+
state: TMDataGridEditState,
|
|
245
|
+
): ReadonlyArray<string> {
|
|
246
|
+
return state.openRowIds.filter(
|
|
247
|
+
(rowId) =>
|
|
248
|
+
!state.committedRowIds.includes(rowId) &&
|
|
249
|
+
!state.newRows.some(
|
|
250
|
+
(newRow) => newRow.tempId === rowId && newRow.committed,
|
|
251
|
+
) &&
|
|
252
|
+
(state.newRows.some((newRow) => newRow.tempId === rowId) ||
|
|
253
|
+
(state.rows[rowId]?.dirtyFields.length ?? 0) > 0),
|
|
254
|
+
);
|
|
255
|
+
}
|
|
256
|
+
|
|
175
257
|
type ErasedRow = Row<TMDataGridFeatures, TMDataGridRowData>;
|
|
176
258
|
type ErasedColumn = Column<TMDataGridFeatures, TMDataGridRowData, unknown>;
|
|
177
259
|
|
|
@@ -289,7 +371,7 @@ export type TMDataGridColumnEditOptions = {
|
|
|
289
371
|
mapValue?: TMDataGridEditValueMap;
|
|
290
372
|
};
|
|
291
373
|
|
|
292
|
-
/** A new row being committed - `onRowAdd`, and `
|
|
374
|
+
/** A new row being committed - `onRowAdd`, and `saveDrafts`'s `created`. */
|
|
293
375
|
export type TMDataGridRowAddArgs<TData extends RowData> = {
|
|
294
376
|
/** The engine's placeholder id; the real id is the consumer's to mint. */
|
|
295
377
|
tempId: string;
|
|
@@ -366,7 +448,15 @@ export type TMDataGridEditCommitDraftsArgs<TData extends RowData> =
|
|
|
366
448
|
export type TMDataGridEditEngineContext = {
|
|
367
449
|
table: TMDataGridTable<TMDataGridRowData>;
|
|
368
450
|
editMode: TMDataGridEditMode;
|
|
451
|
+
/** `editing.draft` - whether a commit parks instead of reaching out. */
|
|
452
|
+
draft: boolean;
|
|
369
453
|
rowValidators?: TMDataGridRowValidators;
|
|
454
|
+
tableValidators?: TMDataGridTableValidators;
|
|
455
|
+
/**
|
|
456
|
+
* `editing.columns` - the allowlist, or `undefined` for "every column that
|
|
457
|
+
* maps to a field".
|
|
458
|
+
*/
|
|
459
|
+
editableColumnIds?: ReadonlyArray<string>;
|
|
370
460
|
isRowEditable?: (row: ErasedRow) => boolean;
|
|
371
461
|
onEditCommit?: (
|
|
372
462
|
args: TMDataGridEditCommitArgs<TMDataGridRowData>,
|
|
@@ -468,6 +558,13 @@ export type TMDataGridEditApi<
|
|
|
468
558
|
canEditCell: (row: ErasedRow, column: ErasedColumn) => boolean;
|
|
469
559
|
/** Whether the row takes edits at all - the edit lane's pencil gate. */
|
|
470
560
|
canEditRow: (row: ErasedRow) => boolean;
|
|
561
|
+
/**
|
|
562
|
+
* Whether the column takes edits at all, with no row in hand: it maps to a
|
|
563
|
+
* field, `editing.columns` lists it if that option is set, and
|
|
564
|
+
* `meta.edit.enabled` is not `false`. A per-row `enabled` predicate is the
|
|
565
|
+
* row's half of the question - `canEditCell` asks both.
|
|
566
|
+
*/
|
|
567
|
+
isColumnEditable: (column: ErasedColumn) => boolean;
|
|
471
568
|
/**
|
|
472
569
|
* Opens an editor. In row mode the whole row opens either way and `columnId`
|
|
473
570
|
* only says which cell takes the caret; `null` (the lane's pencil) leaves it
|
|
@@ -491,10 +588,10 @@ export type TMDataGridEditApi<
|
|
|
491
588
|
cancelAll: () => void;
|
|
492
589
|
/**
|
|
493
590
|
* Submits every open row, as if each had been OK'd: a row that validates
|
|
494
|
-
* commits (into the draft store
|
|
495
|
-
*
|
|
496
|
-
* `true` when every row committed.
|
|
497
|
-
*
|
|
591
|
+
* commits (into the draft store with `editing.draft` on, straight to the
|
|
592
|
+
* consumer without it), a row that fails stays open with its errors.
|
|
593
|
+
* `true` when every row committed. Under `editing.draft` it sends nothing
|
|
594
|
+
* to the consumer by itself - that is `saveDrafts`.
|
|
498
595
|
*/
|
|
499
596
|
commitAll: () => Promise<boolean>;
|
|
500
597
|
/**
|
|
@@ -513,13 +610,45 @@ export type TMDataGridEditApi<
|
|
|
513
610
|
submitAll: () => Promise<boolean>;
|
|
514
611
|
/** Writes the type's empty value into a cell and commits it - Delete. */
|
|
515
612
|
clearCell: (rowId: string, columnId: string) => Promise<boolean>;
|
|
613
|
+
/**
|
|
614
|
+
* Writes one cell and commits the row - a typed edit without the typing,
|
|
615
|
+
* for toolbar actions and bulk fills. Under `editing.draft` the row parks
|
|
616
|
+
* in the draft store exactly as a hand-made edit does, so it carries the
|
|
617
|
+
* same change markers and the same per-row revert.
|
|
618
|
+
*
|
|
619
|
+
* The row need not be mounted; a row inside a collapsed group takes the
|
|
620
|
+
* write. Resolves `false` when the cell takes no edit - no such row or
|
|
621
|
+
* column, `editing.columns` excludes it, `meta.edit.enabled` is off, or the
|
|
622
|
+
* row is not editable - and when validation refuses the value, which leaves
|
|
623
|
+
* the row open carrying its errors.
|
|
624
|
+
*
|
|
625
|
+
* `value` is the stored value: no editor runs, so `meta.edit.mapValue`
|
|
626
|
+
* does not either. `meta.edit.validate` does.
|
|
627
|
+
*/
|
|
628
|
+
setCellValue: (
|
|
629
|
+
rowId: string,
|
|
630
|
+
columnId: string,
|
|
631
|
+
value: unknown,
|
|
632
|
+
) => Promise<boolean>;
|
|
633
|
+
/**
|
|
634
|
+
* {@link setCellValue} for several cells of one row, in a single commit -
|
|
635
|
+
* one consumer call and one draft entry rather than one per column. Keys
|
|
636
|
+
* are column ids.
|
|
637
|
+
*
|
|
638
|
+
* All or nothing: if any named cell takes no edit, nothing is written and
|
|
639
|
+
* this resolves `false`.
|
|
640
|
+
*/
|
|
641
|
+
setRowValues: (
|
|
642
|
+
rowId: string,
|
|
643
|
+
values: Record<string, unknown>,
|
|
644
|
+
) => Promise<boolean>;
|
|
516
645
|
/**
|
|
517
646
|
* Opens a new entry row (the sticky block under the header) seeded from
|
|
518
647
|
* `newRowDefaults`. `values` overrides that seed key by key, so
|
|
519
648
|
* `addRow()` opens a blank row and `addRow({ status: "draft" })` opens one
|
|
520
649
|
* that starts filled in. Returns its temporary id - a form with no backing
|
|
521
|
-
* row yet. Committing it calls `onRowAdd
|
|
522
|
-
*
|
|
650
|
+
* row yet. Committing it calls `onRowAdd`, or parks it for `saveDrafts`
|
|
651
|
+
* under `editing.draft`.
|
|
523
652
|
*/
|
|
524
653
|
addRow: (values?: Partial<TData>) => string;
|
|
525
654
|
/**
|
|
@@ -528,8 +657,8 @@ export type TMDataGridEditApi<
|
|
|
528
657
|
* `newRowDefaults` exactly as `addRow` does.
|
|
529
658
|
*
|
|
530
659
|
* `commit: true` submits each row as it lands, which is what an import
|
|
531
|
-
* wants: rows that validate commit (parked in the draft store under
|
|
532
|
-
*
|
|
660
|
+
* wants: rows that validate commit (parked in the draft store under
|
|
661
|
+
* `editing.draft`, added through `onRowAdd` without it - once per row),
|
|
533
662
|
* and rows that fail stay open in the entry block carrying their errors,
|
|
534
663
|
* for the user to fix. The result says which went which way.
|
|
535
664
|
*/
|
|
@@ -538,10 +667,10 @@ export type TMDataGridEditApi<
|
|
|
538
667
|
options?: TMDataGridAddRowsOptions,
|
|
539
668
|
) => Promise<TMDataGridAddRowsResult>;
|
|
540
669
|
/**
|
|
541
|
-
* Deletes a row: `onRowDelete` straight away under
|
|
542
|
-
*
|
|
543
|
-
*
|
|
544
|
-
*
|
|
670
|
+
* Deletes a row: `onRowDelete` straight away, or under `editing.draft` a
|
|
671
|
+
* toggle of the id in `deletedRowIds` - the row renders struck through
|
|
672
|
+
* until `saveDrafts` reports it. On an uncommitted entry row it just
|
|
673
|
+
* discards the entry.
|
|
545
674
|
*/
|
|
546
675
|
deleteRow: (rowId: string) => void;
|
|
547
676
|
/** Whether delete chrome makes sense - the lane's trash gate. */
|
|
@@ -584,6 +713,34 @@ function hasAnyError(errors: ReadonlyArray<unknown>): boolean {
|
|
|
584
713
|
return errors.some((error) => error !== undefined && error !== null);
|
|
585
714
|
}
|
|
586
715
|
|
|
716
|
+
/**
|
|
717
|
+
* The first error as text, whatever shape the validator produced - TanStack
|
|
718
|
+
* Form keeps errors verbatim, so a Zod issue arrives as `{ message }`, a
|
|
719
|
+
* plain function's return arrives as it was, and a form-level schema's
|
|
720
|
+
* pathless issues arrive keyed under an empty path (`{ "": [issues] }`).
|
|
721
|
+
* This digs until it finds a message.
|
|
722
|
+
*/
|
|
723
|
+
export function firstErrorText(error: unknown): string | undefined {
|
|
724
|
+
if (error === null || error === undefined) return undefined;
|
|
725
|
+
if (typeof error === "string") return error;
|
|
726
|
+
if (Array.isArray(error)) {
|
|
727
|
+
for (const entry of error) {
|
|
728
|
+
const text = firstErrorText(entry);
|
|
729
|
+
if (text !== undefined) return text;
|
|
730
|
+
}
|
|
731
|
+
return undefined;
|
|
732
|
+
}
|
|
733
|
+
if (typeof error === "object") {
|
|
734
|
+
const message = (error as { message?: unknown }).message;
|
|
735
|
+
if (typeof message === "string") return message;
|
|
736
|
+
for (const value of Object.values(error)) {
|
|
737
|
+
const text = firstErrorText(value);
|
|
738
|
+
if (text !== undefined) return text;
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
return undefined;
|
|
742
|
+
}
|
|
743
|
+
|
|
587
744
|
/**
|
|
588
745
|
* The triggers a submit runs, in the order Form would: a value that fails its
|
|
589
746
|
* column's `onChange` rule fails the submit too.
|
|
@@ -658,10 +815,18 @@ export function createEditEngine(
|
|
|
658
815
|
/** Set by the wrapped onSubmit when the consumer's commit resolved. */
|
|
659
816
|
lastSubmitOk: boolean;
|
|
660
817
|
/**
|
|
661
|
-
*
|
|
662
|
-
*
|
|
663
|
-
*
|
|
664
|
-
*
|
|
818
|
+
* What the last failed commit found, field by field, with the value it
|
|
819
|
+
* found it on. Form clears a field's errors when its editor unmounts, so
|
|
820
|
+
* without this a row that failed on the way out would keep its draft and
|
|
821
|
+
* lose the reason. `project` drops an entry once its value has moved.
|
|
822
|
+
*/
|
|
823
|
+
submitErrors: Array<{ field: string; value: unknown; message: string }>;
|
|
824
|
+
/**
|
|
825
|
+
* The park: this submit validates and puts the row in the draft store
|
|
826
|
+
* instead of calling the consumer. Set by `commit` per attempt - `true`
|
|
827
|
+
* only while `editing.draft` is on and outside `saveDrafts`, which is
|
|
828
|
+
* what closes the per-row escape hatches (the lane's ✓,
|
|
829
|
+
* Delete-to-clear) at the engine.
|
|
665
830
|
*/
|
|
666
831
|
parkOnly: boolean;
|
|
667
832
|
/** A commit already running - Enter and blur race on the same edit. */
|
|
@@ -684,15 +849,22 @@ export function createEditEngine(
|
|
|
684
849
|
let draftAddCollector: Array<TMDataGridRowAddArgs<TMDataGridRowData>> | null =
|
|
685
850
|
null;
|
|
686
851
|
|
|
852
|
+
/**
|
|
853
|
+
* The column's half of the rule, with no row in hand: it is a consumer
|
|
854
|
+
* column mapping to a field, `editing.columns` lists it if that is set, and
|
|
855
|
+
* `meta.edit.enabled` has not switched it off outright. A per-row predicate
|
|
856
|
+
* on `enabled` is the row's half and is not asked here.
|
|
857
|
+
*/
|
|
858
|
+
const isColumnEditable = (column: ErasedColumn): boolean => {
|
|
859
|
+
if (isControlColumn(column.id)) return false;
|
|
860
|
+
if (getEditFieldName(column) === null) return false;
|
|
861
|
+
const allowed = getContext().editableColumnIds;
|
|
862
|
+
if (allowed !== undefined && !allowed.includes(column.id)) return false;
|
|
863
|
+
return !isColumnEditSwitchedOff(column);
|
|
864
|
+
};
|
|
865
|
+
|
|
687
866
|
const editableColumns = (): Array<ErasedColumn> =>
|
|
688
|
-
getContext()
|
|
689
|
-
.table.getAllLeafColumns()
|
|
690
|
-
.filter(
|
|
691
|
-
(column) =>
|
|
692
|
-
!isControlColumn(column.id) &&
|
|
693
|
-
!isColumnEditSwitchedOff(column) &&
|
|
694
|
-
getEditFieldName(column) !== null,
|
|
695
|
-
);
|
|
867
|
+
getContext().table.getAllLeafColumns().filter(isColumnEditable);
|
|
696
868
|
|
|
697
869
|
/**
|
|
698
870
|
* Every column's `meta.edit.validate`, run against a row's values.
|
|
@@ -736,9 +908,124 @@ export function createEditEngine(
|
|
|
736
908
|
};
|
|
737
909
|
|
|
738
910
|
/**
|
|
739
|
-
* The
|
|
740
|
-
*
|
|
741
|
-
*
|
|
911
|
+
* The collection as a table validator sees it: every data row overlaid
|
|
912
|
+
* with its draft where a form holds one, entry rows appended, and rows
|
|
913
|
+
* marked deleted removed. The committing row contributes the values being
|
|
914
|
+
* submitted, not what `data` still says. Built from the core row model, so
|
|
915
|
+
* it is unfiltered and never contains group rows.
|
|
916
|
+
*/
|
|
917
|
+
const mergedRows = (
|
|
918
|
+
rowId: string,
|
|
919
|
+
value: TMDataGridRowData,
|
|
920
|
+
isNew: boolean,
|
|
921
|
+
): Array<{ rowId: string; value: TMDataGridRowData }> => {
|
|
922
|
+
const deleted = new Set(store.state.deletedRowIds);
|
|
923
|
+
const rows: Array<{ rowId: string; value: TMDataGridRowData }> = [];
|
|
924
|
+
for (const row of getContext().table.getCoreRowModel().flatRows) {
|
|
925
|
+
if (deleted.has(row.id)) continue;
|
|
926
|
+
if (row.id === rowId) {
|
|
927
|
+
rows.push({ rowId, value });
|
|
928
|
+
continue;
|
|
929
|
+
}
|
|
930
|
+
const held = forms.get(row.id);
|
|
931
|
+
rows.push({
|
|
932
|
+
rowId: row.id,
|
|
933
|
+
value:
|
|
934
|
+
held === undefined
|
|
935
|
+
? (row.original as TMDataGridRowData)
|
|
936
|
+
: (held.form.state.values as TMDataGridRowData),
|
|
937
|
+
});
|
|
938
|
+
}
|
|
939
|
+
for (const newRow of store.state.newRows) {
|
|
940
|
+
if (newRow.tempId === rowId) continue;
|
|
941
|
+
const held = forms.get(newRow.tempId);
|
|
942
|
+
if (held === undefined || deleted.has(newRow.tempId)) continue;
|
|
943
|
+
rows.push({
|
|
944
|
+
rowId: newRow.tempId,
|
|
945
|
+
value: held.form.state.values as TMDataGridRowData,
|
|
946
|
+
});
|
|
947
|
+
}
|
|
948
|
+
if (isNew) rows.push({ rowId, value });
|
|
949
|
+
return rows;
|
|
950
|
+
};
|
|
951
|
+
|
|
952
|
+
/**
|
|
953
|
+
* Runs `editing.tableValidators` for one commit. `onSubmit` first; its
|
|
954
|
+
* failure stands and `onSubmitAsync` is not consulted, mirroring how the
|
|
955
|
+
* column triggers short-circuit.
|
|
956
|
+
*/
|
|
957
|
+
const runTableValidators = async (
|
|
958
|
+
value: TMDataGridRowData,
|
|
959
|
+
rowId: string,
|
|
960
|
+
isNew: boolean,
|
|
961
|
+
): Promise<unknown> => {
|
|
962
|
+
const validators = getContext().tableValidators;
|
|
963
|
+
if (
|
|
964
|
+
validators?.onSubmit === undefined &&
|
|
965
|
+
validators?.onSubmitAsync === undefined
|
|
966
|
+
) {
|
|
967
|
+
return undefined;
|
|
968
|
+
}
|
|
969
|
+
const args: TMDataGridTableValidateArgs = {
|
|
970
|
+
value,
|
|
971
|
+
rowId,
|
|
972
|
+
isNew,
|
|
973
|
+
rows: mergedRows(rowId, value, isNew),
|
|
974
|
+
};
|
|
975
|
+
const sync = validators.onSubmit?.(args);
|
|
976
|
+
if (isValidationError(sync)) return sync;
|
|
977
|
+
return await validators.onSubmitAsync?.(args);
|
|
978
|
+
};
|
|
979
|
+
|
|
980
|
+
/**
|
|
981
|
+
* One submit pass's results folded into Form's error shape. A single
|
|
982
|
+
* source passes through untouched (its own shape preserved); several merge
|
|
983
|
+
* into `{ form, fields }`, the later source winning a field collision and
|
|
984
|
+
* the form slot alike.
|
|
985
|
+
*/
|
|
986
|
+
const mergeSubmitResults = (
|
|
987
|
+
results: ReadonlyArray<unknown>,
|
|
988
|
+
fields: Record<string, unknown> | undefined,
|
|
989
|
+
): unknown => {
|
|
990
|
+
const errors = results.filter(isValidationError);
|
|
991
|
+
if (errors.length === 0) {
|
|
992
|
+
return fields === undefined ? undefined : { fields };
|
|
993
|
+
}
|
|
994
|
+
if (errors.length === 1) {
|
|
995
|
+
const only = errors[0];
|
|
996
|
+
if (fields === undefined) return only;
|
|
997
|
+
if (typeof only === "object" && only !== null) {
|
|
998
|
+
const shape = only as { fields?: Record<string, unknown> };
|
|
999
|
+
return { ...shape, fields: { ...fields, ...(shape.fields ?? {}) } };
|
|
1000
|
+
}
|
|
1001
|
+
return { form: only, fields };
|
|
1002
|
+
}
|
|
1003
|
+
let form: unknown;
|
|
1004
|
+
let mergedFields: Record<string, unknown> = { ...(fields ?? {}) };
|
|
1005
|
+
for (const error of errors) {
|
|
1006
|
+
if (typeof error === "object" && error !== null) {
|
|
1007
|
+
const shape = error as {
|
|
1008
|
+
form?: unknown;
|
|
1009
|
+
fields?: Record<string, unknown>;
|
|
1010
|
+
};
|
|
1011
|
+
if (isValidationError(shape.form)) form = shape.form;
|
|
1012
|
+
mergedFields = { ...mergedFields, ...(shape.fields ?? {}) };
|
|
1013
|
+
} else {
|
|
1014
|
+
form = error;
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
1017
|
+
const merged: { form?: unknown; fields?: Record<string, unknown> } = {};
|
|
1018
|
+
if (form !== undefined) merged.form = form;
|
|
1019
|
+
if (Object.keys(mergedFields).length > 0) merged.fields = mergedFields;
|
|
1020
|
+
return merged;
|
|
1021
|
+
};
|
|
1022
|
+
|
|
1023
|
+
/**
|
|
1024
|
+
* The row's `validators`, with column validation and the table rules folded
|
|
1025
|
+
* into the submit pass. The consumer's own `rowValidators` are passed
|
|
1026
|
+
* through untouched except for `onSubmitAsync`, which now also carries the
|
|
1027
|
+
* column rules and `editing.tableValidators` - later sources win a merge,
|
|
1028
|
+
* so the order is columns, then the table rules, then the row's own.
|
|
742
1029
|
*/
|
|
743
1030
|
const composeValidators = (
|
|
744
1031
|
rowId: string,
|
|
@@ -752,30 +1039,14 @@ export function createEditEngine(
|
|
|
752
1039
|
return {
|
|
753
1040
|
...rowValidators,
|
|
754
1041
|
onSubmitAsync: async (args: { value: unknown }) => {
|
|
1042
|
+
const value = args.value as TMDataGridRowData;
|
|
755
1043
|
const fromConsumer =
|
|
756
1044
|
consumerAsync === undefined
|
|
757
1045
|
? undefined
|
|
758
1046
|
: await (consumerAsync as (a: unknown) => unknown)(args);
|
|
759
|
-
const
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
isNew,
|
|
763
|
-
);
|
|
764
|
-
if (fields === undefined) return fromConsumer;
|
|
765
|
-
if (!isValidationError(fromConsumer)) return { fields };
|
|
766
|
-
// The consumer's row-level result stands alongside the column
|
|
767
|
-
// errors: theirs keeps whatever shape it had, ours lands on fields.
|
|
768
|
-
if (typeof fromConsumer === "object" && fromConsumer !== null) {
|
|
769
|
-
const consumerShape = fromConsumer as {
|
|
770
|
-
form?: unknown;
|
|
771
|
-
fields?: Record<string, unknown>;
|
|
772
|
-
};
|
|
773
|
-
return {
|
|
774
|
-
...consumerShape,
|
|
775
|
-
fields: { ...fields, ...(consumerShape.fields ?? {}) },
|
|
776
|
-
};
|
|
777
|
-
}
|
|
778
|
-
return { form: fromConsumer, fields };
|
|
1047
|
+
const fromTable = await runTableValidators(value, rowId, isNew);
|
|
1048
|
+
const fields = await validateColumnFields(value, rowId, isNew);
|
|
1049
|
+
return mergeSubmitResults([fromTable, fromConsumer], fields);
|
|
779
1050
|
},
|
|
780
1051
|
};
|
|
781
1052
|
};
|
|
@@ -799,23 +1070,33 @@ export function createEditEngine(
|
|
|
799
1070
|
|
|
800
1071
|
const project = (entry: FormEntry): TMDataGridEditRowProjection => {
|
|
801
1072
|
const state = entry.form.state;
|
|
802
|
-
const
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
)
|
|
810
|
-
|
|
1073
|
+
const values = state.values as TMDataGridRowData;
|
|
1074
|
+
const live = Object.entries(state.fieldMeta).flatMap(([name, meta]) => {
|
|
1075
|
+
// The empty path is a pathless (row-level) issue's landing spot - it
|
|
1076
|
+
// belongs to `hasRowError`, not to any cell's marker.
|
|
1077
|
+
if (name === "") return [];
|
|
1078
|
+
const errors = (meta as { errors: ReadonlyArray<unknown> }).errors;
|
|
1079
|
+
if (!hasAnyError(errors)) return [];
|
|
1080
|
+
return [{ field: name, message: firstErrorText(errors) ?? "" }];
|
|
1081
|
+
});
|
|
1082
|
+
// What a closed editor left behind, minus everything the user has since
|
|
1083
|
+
// changed - editing the field is the answer to its message.
|
|
1084
|
+
const kept = entry.submitErrors.filter(
|
|
1085
|
+
(error) =>
|
|
1086
|
+
!live.some((current) => current.field === error.field) &&
|
|
1087
|
+
sameValue(getBy(values, error.field), error.value),
|
|
1088
|
+
);
|
|
1089
|
+
const errorMessages = [
|
|
1090
|
+
...live,
|
|
1091
|
+
...kept.map(({ field, message }) => ({ field, message })),
|
|
1092
|
+
];
|
|
811
1093
|
return {
|
|
812
|
-
dirtyFields: diff(entry,
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
errorFields,
|
|
1094
|
+
dirtyFields: diff(entry, values).map((change) => change.field),
|
|
1095
|
+
errorFields: errorMessages.map((error) => error.field),
|
|
1096
|
+
errorMessages,
|
|
816
1097
|
hasRowError: hasAnyError(state.errors),
|
|
817
1098
|
isSubmitting: state.isSubmitting,
|
|
818
|
-
values
|
|
1099
|
+
values,
|
|
819
1100
|
};
|
|
820
1101
|
};
|
|
821
1102
|
|
|
@@ -898,6 +1179,7 @@ export function createEditEngine(
|
|
|
898
1179
|
isNew,
|
|
899
1180
|
committed: false,
|
|
900
1181
|
lastSubmitOk: false,
|
|
1182
|
+
submitErrors: [],
|
|
901
1183
|
parkOnly: false,
|
|
902
1184
|
pendingCommit: null,
|
|
903
1185
|
unsubscribe: () => {},
|
|
@@ -984,8 +1266,7 @@ export function createEditEngine(
|
|
|
984
1266
|
const canEditCell = (row: ErasedRow, column: ErasedColumn): boolean => {
|
|
985
1267
|
const context = getContext();
|
|
986
1268
|
if (row.getIsGrouped()) return false;
|
|
987
|
-
if (
|
|
988
|
-
if (getEditFieldName(column) === null) return false;
|
|
1269
|
+
if (!isColumnEditable(column)) return false;
|
|
989
1270
|
if (!isColumnEditableForRow(column, row)) return false;
|
|
990
1271
|
if (context.isRowEditable !== undefined && !context.isRowEditable(row)) {
|
|
991
1272
|
return false;
|
|
@@ -1002,6 +1283,26 @@ export function createEditEngine(
|
|
|
1002
1283
|
}
|
|
1003
1284
|
};
|
|
1004
1285
|
|
|
1286
|
+
/**
|
|
1287
|
+
* The row's field errors as they stand, with the value each was found on.
|
|
1288
|
+
* Read straight after a failed submit, while every editor is still mounted
|
|
1289
|
+
* and Form still holds what its validators said.
|
|
1290
|
+
*/
|
|
1291
|
+
const takeFieldErrors = (
|
|
1292
|
+
entry: FormEntry,
|
|
1293
|
+
): Array<{ field: string; value: unknown; message: string }> => {
|
|
1294
|
+
const values = entry.form.state.values as TMDataGridRowData;
|
|
1295
|
+
return Object.entries(entry.form.state.fieldMeta).flatMap(
|
|
1296
|
+
([field, meta]) => {
|
|
1297
|
+
if (field === "") return [];
|
|
1298
|
+
const errors = (meta as { errors: ReadonlyArray<unknown> }).errors;
|
|
1299
|
+
const message = hasAnyError(errors) ? firstErrorText(errors) : undefined;
|
|
1300
|
+
if (message === undefined) return [];
|
|
1301
|
+
return [{ field, value: getBy(values, field), message }];
|
|
1302
|
+
},
|
|
1303
|
+
);
|
|
1304
|
+
};
|
|
1305
|
+
|
|
1005
1306
|
const commit = async (rowId: string): Promise<boolean> => {
|
|
1006
1307
|
const entry = forms.get(rowId);
|
|
1007
1308
|
if (entry === undefined) return true;
|
|
@@ -1023,14 +1324,21 @@ export function createEditEngine(
|
|
|
1023
1324
|
// block `canSubmit` forever. The validator puts back whatever still holds.
|
|
1024
1325
|
clearFormSourcedFieldErrors(entry.form);
|
|
1025
1326
|
entry.lastSubmitOk = false;
|
|
1026
|
-
entry.
|
|
1327
|
+
entry.submitErrors = [];
|
|
1328
|
+
entry.parkOnly = getContext().draft && !savingDrafts;
|
|
1027
1329
|
entry.pendingCommit = (async () => {
|
|
1028
1330
|
try {
|
|
1029
1331
|
await entry.form.handleSubmit();
|
|
1030
1332
|
} finally {
|
|
1031
1333
|
entry.pendingCommit = null;
|
|
1032
1334
|
}
|
|
1033
|
-
if (!entry.lastSubmitOk)
|
|
1335
|
+
if (!entry.lastSubmitOk) {
|
|
1336
|
+
// Snapshot before the caller closes the editor: the field errors go
|
|
1337
|
+
// with it, and the row is about to be left carrying them.
|
|
1338
|
+
entry.submitErrors = takeFieldErrors(entry);
|
|
1339
|
+
publishRow(rowId);
|
|
1340
|
+
return false;
|
|
1341
|
+
}
|
|
1034
1342
|
if (entry.parkOnly) {
|
|
1035
1343
|
// Into the draft store: the row's values stay in its form, and the
|
|
1036
1344
|
// grid records that they are decided. An entry row renders as a
|
|
@@ -1052,7 +1360,8 @@ export function createEditEngine(
|
|
|
1052
1360
|
|
|
1053
1361
|
const beginOn = (rowId: string, columnId: string | null) => {
|
|
1054
1362
|
// An entry row has no backing row in the table; opening one re-arms its
|
|
1055
|
-
// editors -
|
|
1363
|
+
// editors - with `editing.draft` on, that is how a parked row is edited
|
|
1364
|
+
// again.
|
|
1056
1365
|
const entryForm = forms.get(rowId);
|
|
1057
1366
|
if (entryForm?.isNew === true) {
|
|
1058
1367
|
entryForm.committed = false;
|
|
@@ -1083,11 +1392,19 @@ export function createEditEngine(
|
|
|
1083
1392
|
// | Mode | Another row open |
|
|
1084
1393
|
// | ---- | ---------------- |
|
|
1085
1394
|
// | cell | committed - Sheets; a failed commit keeps holding the edit |
|
|
1086
|
-
// | row, cellConfirm
|
|
1087
|
-
//
|
|
1088
|
-
//
|
|
1395
|
+
// | row, cellConfirm | accumulates. Every draft waits for its own save,
|
|
1396
|
+
// so a second row opening can neither discard the first nor be refused
|
|
1397
|
+
// by it - nothing about one row's form bears on another's |
|
|
1398
|
+
//
|
|
1399
|
+
// A parked row is not "open": it has had its submit and is waiting for
|
|
1400
|
+
// the save, so it is skipped rather than put through a second one. An
|
|
1401
|
+
// entry row is skipped too - it is row-shaped in every mode, and its ✓
|
|
1402
|
+
// is the decision, so the sweep must not add a half-typed row.
|
|
1089
1403
|
if (editMode === "cell") {
|
|
1090
|
-
const openElsewhere = [...forms.keys()].find((id) =>
|
|
1404
|
+
const openElsewhere = [...forms.keys()].find((id) => {
|
|
1405
|
+
const other = forms.get(id);
|
|
1406
|
+
return id !== rowId && other?.committed !== true && other?.isNew !== true;
|
|
1407
|
+
});
|
|
1091
1408
|
if (openElsewhere !== undefined) {
|
|
1092
1409
|
void commit(openElsewhere).then((ok) => {
|
|
1093
1410
|
if (ok) beginOn(rowId, columnId);
|
|
@@ -1184,7 +1501,7 @@ export function createEditEngine(
|
|
|
1184
1501
|
return;
|
|
1185
1502
|
}
|
|
1186
1503
|
const context = getContext();
|
|
1187
|
-
if (context.
|
|
1504
|
+
if (context.draft) {
|
|
1188
1505
|
// A toggle: the second press unmarks - the mark is a draft too.
|
|
1189
1506
|
store.setState((prev) => ({
|
|
1190
1507
|
...prev,
|
|
@@ -1202,7 +1519,7 @@ export function createEditEngine(
|
|
|
1202
1519
|
|
|
1203
1520
|
const canDeleteRows = (): boolean => {
|
|
1204
1521
|
const context = getContext();
|
|
1205
|
-
if (context.
|
|
1522
|
+
if (context.draft) {
|
|
1206
1523
|
return (
|
|
1207
1524
|
context.onRowDelete !== undefined ||
|
|
1208
1525
|
context.onSaveDrafts !== undefined
|
|
@@ -1244,7 +1561,7 @@ export function createEditEngine(
|
|
|
1244
1561
|
|
|
1245
1562
|
const saveDrafts = (): Promise<boolean> => {
|
|
1246
1563
|
if (saveInFlight !== null) return saveInFlight;
|
|
1247
|
-
// While this runs, `commit` really commits -
|
|
1564
|
+
// While this runs, `commit` really commits - `editing.draft` parks
|
|
1248
1565
|
// otherwise. Single-threaded flag, same idiom as the collectors.
|
|
1249
1566
|
savingDrafts = true;
|
|
1250
1567
|
saveInFlight = saveDraftsInner().finally(() => {
|
|
@@ -1351,25 +1668,81 @@ export function createEditEngine(
|
|
|
1351
1668
|
return committedOk && savedOk;
|
|
1352
1669
|
};
|
|
1353
1670
|
|
|
1354
|
-
|
|
1671
|
+
/**
|
|
1672
|
+
* The write path behind `clearCell`, `setCellValue` and `setRowValues`:
|
|
1673
|
+
* fill the row's own form and commit it, exactly as a ✓ on an open editor
|
|
1674
|
+
* would. Uses the form already open on the row when there is one, so a
|
|
1675
|
+
* programmatic write joins an edit in progress rather than discarding it.
|
|
1676
|
+
*
|
|
1677
|
+
* No editor is involved, so `meta.edit.mapValue` does not run - the caller
|
|
1678
|
+
* writes the stored value itself. Validation is untouched: this is the same
|
|
1679
|
+
* `commit`, so `meta.edit.validate` and `editing.rowValidators` still decide
|
|
1680
|
+
* whether the row lands, and a failure leaves the row open carrying its
|
|
1681
|
+
* errors.
|
|
1682
|
+
*/
|
|
1683
|
+
const writeFields = async (
|
|
1355
1684
|
rowId: string,
|
|
1356
|
-
|
|
1685
|
+
writes: ReadonlyArray<{ field: string; value: unknown }>,
|
|
1357
1686
|
): Promise<boolean> => {
|
|
1358
|
-
const context = getContext();
|
|
1359
1687
|
const row = getRow(rowId);
|
|
1360
|
-
|
|
1361
|
-
if (
|
|
1362
|
-
if (!canEditCell(row, column)) return false;
|
|
1363
|
-
const field = getEditFieldName(column);
|
|
1364
|
-
if (field === null) return false;
|
|
1688
|
+
if (row === undefined) return false;
|
|
1689
|
+
if (writes.length === 0) return true;
|
|
1365
1690
|
const entry = forms.get(rowId) ?? createForm(rowId, row.original, false);
|
|
1366
|
-
|
|
1367
|
-
field as never,
|
|
1368
|
-
|
|
1369
|
-
);
|
|
1691
|
+
for (const { field, value } of writes) {
|
|
1692
|
+
entry.form.setFieldValue(field as never, value as never);
|
|
1693
|
+
}
|
|
1370
1694
|
return commit(rowId);
|
|
1371
1695
|
};
|
|
1372
1696
|
|
|
1697
|
+
/** The field a cell writes to, or `null` when that cell takes no edit. */
|
|
1698
|
+
const writableField = (rowId: string, columnId: string): string | null => {
|
|
1699
|
+
const row = getRow(rowId);
|
|
1700
|
+
const column = getContext().table.getColumn(columnId);
|
|
1701
|
+
if (row === undefined || column === undefined) return null;
|
|
1702
|
+
if (!canEditCell(row, column)) return null;
|
|
1703
|
+
return getEditFieldName(column);
|
|
1704
|
+
};
|
|
1705
|
+
|
|
1706
|
+
const clearCell = async (
|
|
1707
|
+
rowId: string,
|
|
1708
|
+
columnId: string,
|
|
1709
|
+
): Promise<boolean> => {
|
|
1710
|
+
const column = getContext().table.getColumn(columnId);
|
|
1711
|
+
const field = writableField(rowId, columnId);
|
|
1712
|
+
if (field === null || column === undefined) return false;
|
|
1713
|
+
return writeFields(rowId, [
|
|
1714
|
+
{
|
|
1715
|
+
field,
|
|
1716
|
+
value: clearedValueForType(column.columnDef.meta?.type ?? "string"),
|
|
1717
|
+
},
|
|
1718
|
+
]);
|
|
1719
|
+
};
|
|
1720
|
+
|
|
1721
|
+
const setCellValue = async (
|
|
1722
|
+
rowId: string,
|
|
1723
|
+
columnId: string,
|
|
1724
|
+
value: unknown,
|
|
1725
|
+
): Promise<boolean> => {
|
|
1726
|
+
const field = writableField(rowId, columnId);
|
|
1727
|
+
if (field === null) return false;
|
|
1728
|
+
return writeFields(rowId, [{ field, value }]);
|
|
1729
|
+
};
|
|
1730
|
+
|
|
1731
|
+
const setRowValues = async (
|
|
1732
|
+
rowId: string,
|
|
1733
|
+
values: Record<string, unknown>,
|
|
1734
|
+
): Promise<boolean> => {
|
|
1735
|
+
const writes: Array<{ field: string; value: unknown }> = [];
|
|
1736
|
+
for (const [columnId, value] of Object.entries(values)) {
|
|
1737
|
+
const field = writableField(rowId, columnId);
|
|
1738
|
+
// All or nothing: one cell that takes no edit fails the call before
|
|
1739
|
+
// anything is written, so a bulk action cannot half-apply unnoticed.
|
|
1740
|
+
if (field === null) return false;
|
|
1741
|
+
writes.push({ field, value });
|
|
1742
|
+
}
|
|
1743
|
+
return writeFields(rowId, writes);
|
|
1744
|
+
};
|
|
1745
|
+
|
|
1373
1746
|
return {
|
|
1374
1747
|
store,
|
|
1375
1748
|
get state() {
|
|
@@ -1378,6 +1751,7 @@ export function createEditEngine(
|
|
|
1378
1751
|
getForm: (rowId) => forms.get(rowId)?.form,
|
|
1379
1752
|
canEditCell,
|
|
1380
1753
|
canEditRow,
|
|
1754
|
+
isColumnEditable,
|
|
1381
1755
|
begin,
|
|
1382
1756
|
commit,
|
|
1383
1757
|
cancel,
|
|
@@ -1387,6 +1761,8 @@ export function createEditEngine(
|
|
|
1387
1761
|
saveDrafts,
|
|
1388
1762
|
submitAll,
|
|
1389
1763
|
clearCell,
|
|
1764
|
+
setCellValue,
|
|
1765
|
+
setRowValues,
|
|
1390
1766
|
addRow,
|
|
1391
1767
|
addRows,
|
|
1392
1768
|
deleteRow,
|