@jielga/tmdatagrid 2.0.0-beta.9 → 2.0.0
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/README.md +5 -212
- package/dist/index.d.ts +1281 -768
- package/dist/index.js +4607 -3250
- package/dist/index.js.map +1 -1
- package/dist/styles.css +1 -1
- package/docs/adding-rows.md +132 -0
- package/docs/anatomy.md +119 -0
- package/docs/card-view.md +108 -0
- package/docs/cell-selection.md +194 -0
- package/docs/column-layout.md +182 -0
- package/docs/column-menu.md +66 -0
- package/docs/columns.md +268 -0
- package/docs/components.md +311 -0
- package/docs/draft-store.md +242 -0
- package/docs/editing.md +303 -0
- package/docs/editors.md +250 -0
- package/docs/export.md +319 -0
- package/docs/filtering.md +362 -0
- package/docs/getting-started.md +123 -0
- package/docs/grouping.md +165 -0
- package/docs/loading-and-empty.md +92 -0
- package/docs/localization.md +79 -0
- package/docs/menu.md +143 -0
- package/docs/migrating-to-2.md +163 -0
- package/docs/pagination.md +144 -0
- package/docs/persistence.md +114 -0
- package/docs/portfolio-rebalancer.md +94 -0
- package/docs/query-builder.md +179 -0
- package/docs/quick-search.md +84 -0
- package/docs/row-details.md +115 -0
- package/docs/row-interaction.md +149 -0
- package/docs/row-pinning.md +132 -0
- package/docs/row-selection.md +136 -0
- package/docs/row-styling.md +133 -0
- package/docs/scrolling.md +112 -0
- package/docs/server-query.md +246 -0
- package/docs/server-side.md +206 -0
- package/docs/sorting.md +101 -0
- package/docs/styling.md +126 -0
- package/docs/summary-row.md +76 -0
- package/docs/testing.md +744 -0
- package/docs/toolbar.md +161 -0
- package/docs/use-tm-data-grid.md +361 -0
- package/package.json +22 -46
- package/skills/appearance/SKILL.md +72 -19
- package/skills/cell-selection/SKILL.md +46 -47
- package/skills/columns/SKILL.md +90 -34
- package/skills/data/SKILL.md +86 -16
- package/skills/editing/SKILL.md +67 -40
- package/skills/editing/references/common-mistakes.md +77 -69
- package/skills/editing/references/editing-api.md +22 -19
- package/skills/editing/references/editors-and-validation.md +24 -17
- package/skills/filtering/SKILL.md +148 -40
- package/skills/getting-started/SKILL.md +17 -15
- package/skills/grouping/SKILL.md +31 -16
- package/skills/options/SKILL.md +7 -7
- package/skills/rows/SKILL.md +22 -18
- package/skills/server-side/SKILL.md +170 -17
- package/skills/testing/SKILL.md +150 -32
- package/skills/testing-components/SKILL.md +230 -0
- package/skills/testing-editing/SKILL.md +240 -0
- package/src/{tmdatagrid/components → components}/TMDataGrid.tsx +38 -21
- package/src/{tmdatagrid/components → components}/TMDataGridCellEditor.tsx +54 -8
- package/src/{tmdatagrid/components → components}/TMDataGridColumnsPanel.module.css +5 -1
- package/src/{tmdatagrid/components → components}/TMDataGridColumnsPanel.tsx +47 -55
- package/src/{tmdatagrid/components → components}/TMDataGridDetailsColumn.tsx +5 -51
- package/src/{tmdatagrid/components → components}/TMDataGridDraftActions.tsx +41 -24
- package/src/{tmdatagrid/components → components}/TMDataGridEditColumn.tsx +12 -59
- package/src/{tmdatagrid/components → components}/TMDataGridEntryRows.tsx +164 -92
- package/src/components/TMDataGridExportPicker.module.css +77 -0
- package/src/components/TMDataGridExportPicker.tsx +234 -0
- package/src/components/TMDataGridFilterPanel.module.css +54 -0
- package/src/components/TMDataGridFilterPanel.tsx +348 -0
- package/src/{tmdatagrid/components → components}/TMDataGridFilterPills.tsx +15 -8
- package/src/components/TMDataGridFilterSurface.module.css +54 -0
- package/src/components/TMDataGridFilterSurface.tsx +167 -0
- package/src/{tmdatagrid/components → components}/TMDataGridFooter.tsx +53 -90
- package/src/{tmdatagrid/components → components}/TMDataGridGroupColumn.tsx +5 -69
- package/src/{tmdatagrid/components → components}/TMDataGridHeaderCell.module.css +12 -2
- package/src/{tmdatagrid/components → components}/TMDataGridHeaderCell.tsx +58 -21
- package/src/components/TMDataGridHeaderFilterRow.module.css +51 -0
- package/src/components/TMDataGridHeaderFilterRow.tsx +301 -0
- package/src/components/TMDataGridMenu.tsx +357 -0
- package/src/{tmdatagrid/components → components}/TMDataGridSelectColumn.tsx +11 -48
- package/src/{tmdatagrid/components → components}/TMDataGridTable.module.css +69 -56
- package/src/{tmdatagrid/components → components}/TMDataGridTable.tsx +240 -138
- package/src/{tmdatagrid/components → components}/TMDataGridToolbar.module.css +5 -0
- package/src/components/TMDataGridToolbar.tsx +181 -0
- package/src/{tmdatagrid/components → components}/editors/TMDataGridBooleanEditor.tsx +3 -3
- package/src/{tmdatagrid/components → components}/editors/TMDataGridDateEditor.tsx +3 -3
- package/src/{tmdatagrid/components → components}/editors/TMDataGridMultiSelectEditor.tsx +3 -3
- package/src/{tmdatagrid/components → components}/editors/TMDataGridNumberEditor.tsx +3 -3
- package/src/{tmdatagrid/components → components}/editors/TMDataGridSelectEditor.tsx +3 -3
- package/src/{tmdatagrid/components → components}/editors/TMDataGridStringEditor.tsx +3 -3
- package/src/{tmdatagrid/components → components}/editors/editorShared.ts +16 -2
- package/src/{tmdatagrid/components → components}/filters/DgAutocompleteFilter.tsx +5 -4
- package/src/{tmdatagrid/components → components}/filters/DgDateRangeFilter.tsx +22 -5
- package/src/{tmdatagrid/components → components}/filters/DgRangeSliderFilter.tsx +5 -1
- package/src/{tmdatagrid/components → components}/filters/DgTriStateFilter.tsx +5 -1
- package/src/{tmdatagrid/components → components}/filters/TMDataGridFilterValueInput.tsx +44 -28
- package/src/components/filters/controlLayout.ts +32 -0
- package/src/components/filters/filterControlFor.ts +65 -0
- package/src/components/generatedColumns.tsx +187 -0
- package/src/{tmdatagrid/components → components}/icons.ts +1 -0
- package/src/{tmdatagrid/components → components}/sticky.module.css +44 -0
- package/src/components/useHideableColumns.ts +52 -0
- package/src/{tmdatagrid/core → core}/autosize.ts +5 -2
- package/src/{tmdatagrid/core → core}/columnOptions.ts +60 -8
- package/src/{tmdatagrid/core → core}/columnOrdering.ts +33 -14
- package/src/{tmdatagrid/core → core}/columnUtils.ts +44 -5
- package/src/core/controlledStateSync.ts +108 -0
- package/src/core/deletedRows.ts +34 -0
- package/src/core/dom.ts +74 -0
- package/src/{tmdatagrid/core → core}/editEngine.ts +1107 -460
- package/src/core/export.ts +704 -0
- package/src/{tmdatagrid/core → core}/filterControls.ts +38 -1
- package/src/{tmdatagrid/core → core}/filterOperators.ts +64 -1
- package/src/core/filterSurface.ts +99 -0
- package/src/{tmdatagrid/core → core}/grouping.ts +21 -0
- package/src/{tmdatagrid/core → core}/labels.ts +51 -6
- package/src/{tmdatagrid/core → core}/labelsSv.ts +27 -6
- package/src/core/pageReset.ts +120 -0
- package/src/core/pagination.ts +81 -0
- package/src/{tmdatagrid/core → core}/persistence.ts +22 -5
- package/src/{tmdatagrid/core → core}/summary.ts +20 -4
- package/src/{tmdatagrid/index.ts → index.ts} +69 -35
- package/src/{tmdatagrid/useTMDataGrid.tsx → useTMDataGrid.tsx} +428 -109
- package/src/useTMDataGridExport.ts +78 -0
- package/src/tmdatagrid/components/TMDataGridFilterPanel.module.css +0 -35
- package/src/tmdatagrid/components/TMDataGridFilterPanel.tsx +0 -354
- package/src/tmdatagrid/components/TMDataGridToolbar.tsx +0 -161
- package/src/tmdatagrid/core/cellExport.ts +0 -320
- /package/src/{tmdatagrid/TMDataGridContext.ts → TMDataGridContext.ts} +0 -0
- /package/src/{tmdatagrid/components → components}/TMDataGrid.module.css +0 -0
- /package/src/{tmdatagrid/components → components}/TMDataGridDetailsColumn.module.css +0 -0
- /package/src/{tmdatagrid/components → components}/TMDataGridFilterPills.module.css +0 -0
- /package/src/{tmdatagrid/components → components}/TMDataGridFooter.module.css +0 -0
- /package/src/{tmdatagrid/components → components}/TMDataGridGroupColumn.module.css +0 -0
- /package/src/{tmdatagrid/components → components}/TMDataGridRowNumberColumn.tsx +0 -0
- /package/src/{tmdatagrid/components → components}/TMDataGridSearch.tsx +0 -0
- /package/src/{tmdatagrid/core → core}/capabilities.ts +0 -0
- /package/src/{tmdatagrid/core → core}/cellNavigation.ts +0 -0
- /package/src/{tmdatagrid/core → core}/cellRange.ts +0 -0
- /package/src/{tmdatagrid/core → core}/controlledState.ts +0 -0
- /package/src/{tmdatagrid/core → core}/draftCellContext.ts +0 -0
- /package/src/{tmdatagrid/core → core}/editorFocus.ts +0 -0
- /package/src/{tmdatagrid/core → core}/expanding.ts +0 -0
- /package/src/{tmdatagrid/core → core}/matchHighlight.ts +0 -0
- /package/src/{tmdatagrid/core → core}/quickSearch.ts +0 -0
- /package/src/{tmdatagrid/core → core}/resizePreview.ts +0 -0
- /package/src/{tmdatagrid/core → core}/rowPinning.ts +0 -0
- /package/src/{tmdatagrid/core → core}/rowSelection.ts +0 -0
- /package/src/{tmdatagrid/core → core}/sizes.ts +0 -0
- /package/src/{tmdatagrid/core → core}/useSettledTableState.ts +0 -0
|
@@ -5,7 +5,7 @@ import {
|
|
|
5
5
|
type AnyFormApi,
|
|
6
6
|
type StandardSchemaV1,
|
|
7
7
|
} from "@tanstack/react-form";
|
|
8
|
-
import {
|
|
8
|
+
import { Store } from "@tanstack/store";
|
|
9
9
|
import type { Cell, Column, Row, RowData } from "@tanstack/react-table";
|
|
10
10
|
import type { ComponentType } from "react";
|
|
11
11
|
import type { TMDataGridRowData } from "../TMDataGridContext";
|
|
@@ -115,8 +115,10 @@ export type TMDataGridTableValidateArgs<
|
|
|
115
115
|
/**
|
|
116
116
|
* `editing.tableValidators` - rules that need the other rows: no duplicate
|
|
117
117
|
* keys, no overlapping ranges, allocations summing to a total. Run at every
|
|
118
|
-
* commit, after the row's own validators, and again per
|
|
119
|
-
* `saveDrafts
|
|
118
|
+
* commit, after the row's own validators, and again per committed row during
|
|
119
|
+
* `saveDrafts`, the only rules that run there - a committed row a later edit
|
|
120
|
+
* invalidated is reopened with the error, and the save reports it in
|
|
121
|
+
* `reopened`.
|
|
120
122
|
*
|
|
121
123
|
* Return nothing to pass, a message, or Form's `{ form, fields }` shape;
|
|
122
124
|
* pathed issues land on the committing row's cells, pathless ones on the row.
|
|
@@ -194,20 +196,22 @@ export type TMDataGridEditState = {
|
|
|
194
196
|
*/
|
|
195
197
|
active: { rowId: string; columnId: string | null } | null;
|
|
196
198
|
/**
|
|
197
|
-
*
|
|
198
|
-
*
|
|
199
|
-
*
|
|
199
|
+
* Every row the grid is holding work for: open rows, whose form is still
|
|
200
|
+
* undecided, and committed rows, whose values wait in the draft store. In
|
|
201
|
+
* the order the rows first entered; a reopen keeps a row's place. A row is
|
|
202
|
+
* *open* when it is in here and not in {@link committedRowIds}, or, for an
|
|
203
|
+
* entry row, not flagged `committed` in {@link newRows}.
|
|
200
204
|
*/
|
|
201
205
|
openRowIds: ReadonlyArray<string>;
|
|
202
206
|
rows: Record<string, TMDataGridEditRowProjection>;
|
|
203
207
|
/**
|
|
204
|
-
* The draft store's edit slice: existing rows
|
|
205
|
-
*
|
|
206
|
-
*
|
|
207
|
-
*
|
|
208
|
+
* The draft store's edit slice: existing rows that passed their commit and
|
|
209
|
+
* wait for `saveDrafts`. A committed row is data, not a form: its values
|
|
210
|
+
* are in {@link committedValues}, and `begin` on one of these builds a
|
|
211
|
+
* fresh form from them and takes the row back out.
|
|
208
212
|
*
|
|
209
|
-
* Only `editing.draft`
|
|
210
|
-
*
|
|
213
|
+
* Only `editing.draft` commits into the store. Without it a commit goes
|
|
214
|
+
* straight to the consumer, so this stays empty.
|
|
211
215
|
*/
|
|
212
216
|
committedRowIds: ReadonlyArray<string>;
|
|
213
217
|
/**
|
|
@@ -223,12 +227,21 @@ export type TMDataGridEditState = {
|
|
|
223
227
|
/**
|
|
224
228
|
* Rows being created, not yet in `data`. `committed` is the draft store's
|
|
225
229
|
* add slice: the entry row passed its submit and renders as a value row
|
|
226
|
-
* until `begin` re-opens it. Without
|
|
227
|
-
* `onRowAdd` and the entry is
|
|
230
|
+
* from {@link committedValues} until `begin` re-opens it. Without
|
|
231
|
+
* `editing.draft` a commit adds through `onRowAdd` and the entry is
|
|
232
|
+
* dropped, so it never turns `true`.
|
|
228
233
|
*/
|
|
229
234
|
newRows: ReadonlyArray<{ tempId: string; committed: boolean }>;
|
|
230
235
|
/** The draft store's delete slice: rows marked deleted, awaiting the save. */
|
|
231
236
|
deletedRowIds: ReadonlyArray<string>;
|
|
237
|
+
/**
|
|
238
|
+
* `true` while `saveDrafts` is in flight - from the call until the
|
|
239
|
+
* consumer's callbacks (`onSaveDrafts`, or the per-row `onCommit` /
|
|
240
|
+
* `onRowAdd` / `onRowDelete` loop) have settled. Concurrent `saveDrafts`
|
|
241
|
+
* calls join the same run, so it flips once per run. It stays `false` for
|
|
242
|
+
* a save that finds nothing to send.
|
|
243
|
+
*/
|
|
244
|
+
isSaving: boolean;
|
|
232
245
|
};
|
|
233
246
|
|
|
234
247
|
const EMPTY_EDIT_STATE: TMDataGridEditState = {
|
|
@@ -239,29 +252,54 @@ const EMPTY_EDIT_STATE: TMDataGridEditState = {
|
|
|
239
252
|
committedValues: {},
|
|
240
253
|
newRows: [],
|
|
241
254
|
deletedRowIds: [],
|
|
255
|
+
isSaving: false,
|
|
242
256
|
};
|
|
243
257
|
|
|
244
258
|
/**
|
|
245
259
|
* The rows still *open*: holding a live form nobody has decided yet.
|
|
246
260
|
*
|
|
247
261
|
* Narrower than {@link TMDataGridEditState.openRowIds}, which is every row
|
|
248
|
-
*
|
|
249
|
-
*
|
|
250
|
-
* always counts, an existing one only once a value has moved.
|
|
262
|
+
* the grid holds, the committed ones included. A row qualifies here when it
|
|
263
|
+
* is not committed in the draft store and there is something to lose - an
|
|
264
|
+
* entered row always counts, an existing one only once a value has moved.
|
|
251
265
|
*
|
|
252
266
|
* The order is the engine's: the order the forms were opened.
|
|
253
267
|
*/
|
|
254
268
|
export function getOpenRowIds(
|
|
255
269
|
state: TMDataGridEditState,
|
|
256
270
|
): ReadonlyArray<string> {
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
271
|
+
const committed = new Set(state.committedRowIds);
|
|
272
|
+
const entered = new Map(
|
|
273
|
+
state.newRows.map((newRow) => [newRow.tempId, newRow.committed]),
|
|
274
|
+
);
|
|
275
|
+
return state.openRowIds.filter((rowId) => {
|
|
276
|
+
if (committed.has(rowId)) return false;
|
|
277
|
+
const entryCommitted = entered.get(rowId);
|
|
278
|
+
if (entryCommitted !== undefined) return !entryCommitted;
|
|
279
|
+
return (state.rows[rowId]?.dirtyFields.length ?? 0) > 0;
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Whether the grid holds anything a navigation would lose: an open row with
|
|
285
|
+
* a moved value, an entry row, the draft store (edits, new rows, deletion
|
|
286
|
+
* marks), or a save still in flight.
|
|
287
|
+
*
|
|
288
|
+
* A pure selector over the edit state, so `useSelector(edit.store,
|
|
289
|
+
* hasPendingEdits)` re-renders only when the answer flips, and a navigation
|
|
290
|
+
* blocker can read `hasPendingEdits(edit.state)` at the moment it is asked.
|
|
291
|
+
* `isSaving` counts because the default per-row loop takes the deletion
|
|
292
|
+
* marks out of the store before its `onRowDelete` calls settle.
|
|
293
|
+
*/
|
|
294
|
+
export function hasPendingEdits(state: TMDataGridEditState): boolean {
|
|
295
|
+
return (
|
|
296
|
+
state.isSaving ||
|
|
297
|
+
state.committedRowIds.length > 0 ||
|
|
298
|
+
state.deletedRowIds.length > 0 ||
|
|
299
|
+
state.newRows.length > 0 ||
|
|
300
|
+
state.openRowIds.some(
|
|
301
|
+
(rowId) => (state.rows[rowId]?.dirtyFields.length ?? 0) > 0,
|
|
302
|
+
)
|
|
265
303
|
);
|
|
266
304
|
}
|
|
267
305
|
|
|
@@ -348,15 +386,16 @@ export type TMDataGridEditValueMap = (
|
|
|
348
386
|
* `meta.type` and `meta.options` stay outside this namespace on purpose: one
|
|
349
387
|
* declaration of each feeds the cell editor and the filter panel alike.
|
|
350
388
|
*/
|
|
351
|
-
export type TMDataGridColumnEditOptions
|
|
389
|
+
export type TMDataGridColumnEditOptions<
|
|
390
|
+
TData extends RowData = TMDataGridRowData,
|
|
391
|
+
> = {
|
|
352
392
|
/**
|
|
353
393
|
* Whether this column's cells take edits, once `editMode` is on. `false`
|
|
354
394
|
* switches the column off outright; a predicate decides per row. Defaults
|
|
355
395
|
* to editable for any column that maps to a field - see {@link field}.
|
|
396
|
+
* `row` is typed by the column helper the column was declared with.
|
|
356
397
|
*/
|
|
357
|
-
enabled?:
|
|
358
|
-
| boolean
|
|
359
|
-
| ((row: Row<TMDataGridFeatures, TMDataGridRowData>) => boolean);
|
|
398
|
+
enabled?: boolean | ((row: Row<TMDataGridFeatures, TData>) => boolean);
|
|
360
399
|
/**
|
|
361
400
|
* The data path this column edits, when it is not the `accessorKey` - the
|
|
362
401
|
* only way a column built on `accessorFn` becomes editable. Dot paths reach
|
|
@@ -407,10 +446,6 @@ export type TMDataGridSaveDraftsArgs<TData extends RowData> = {
|
|
|
407
446
|
created: Array<TMDataGridRowAddArgs<TData>>;
|
|
408
447
|
/** Ids marked deleted while the drafts accumulated. */
|
|
409
448
|
deleted: Array<string>;
|
|
410
|
-
/** @deprecated Renamed to {@link updated}. Removed in a later beta. */
|
|
411
|
-
rows: Array<TMDataGridEditCommitArgs<TData>>;
|
|
412
|
-
/** @deprecated Renamed to {@link created}. Removed in a later beta. */
|
|
413
|
-
added: Array<TMDataGridRowAddArgs<TData>>;
|
|
414
449
|
};
|
|
415
450
|
|
|
416
451
|
/**
|
|
@@ -429,7 +464,7 @@ export type TMDataGridSaveOutcomes = boolean | Record<string, boolean>;
|
|
|
429
464
|
* with nothing beyond the state itself - a failed edit keeps `data-draft`,
|
|
430
465
|
* a failed deletion keeps `data-deleted` - so the display is the consumer's.
|
|
431
466
|
*/
|
|
432
|
-
export type
|
|
467
|
+
export type TMDataGridSaveDraftsResponse = {
|
|
433
468
|
/** Keyed by `rowId`. */
|
|
434
469
|
updated?: TMDataGridSaveOutcomes;
|
|
435
470
|
/** Keyed by `tempId`. */
|
|
@@ -448,13 +483,6 @@ function isSaved(
|
|
|
448
483
|
return outcomes[id] !== false;
|
|
449
484
|
}
|
|
450
485
|
|
|
451
|
-
/**
|
|
452
|
-
* @deprecated Renamed to {@link TMDataGridSaveDraftsArgs} - the payload is
|
|
453
|
-
* the draft store being saved, not a commit. Removed in a later beta.
|
|
454
|
-
*/
|
|
455
|
-
export type TMDataGridEditCommitDraftsArgs<TData extends RowData> =
|
|
456
|
-
TMDataGridSaveDraftsArgs<TData>;
|
|
457
|
-
|
|
458
486
|
/** What the engine reads fresh on every call - see `createEditEngine`. */
|
|
459
487
|
export type TMDataGridEditEngineContext = {
|
|
460
488
|
table: TMDataGridTable<TMDataGridRowData>;
|
|
@@ -476,8 +504,8 @@ export type TMDataGridEditEngineContext = {
|
|
|
476
504
|
args: TMDataGridSaveDraftsArgs<TMDataGridRowData>,
|
|
477
505
|
) =>
|
|
478
506
|
| void
|
|
479
|
-
|
|
|
480
|
-
| Promise<void |
|
|
507
|
+
| TMDataGridSaveDraftsResponse
|
|
508
|
+
| Promise<void | TMDataGridSaveDraftsResponse>;
|
|
481
509
|
/**
|
|
482
510
|
* Seed values for `addRow`, under the values it is called with. A function
|
|
483
511
|
* is called per added row.
|
|
@@ -537,6 +565,8 @@ export type TMDataGridAddRowsOptions = {
|
|
|
537
565
|
|
|
538
566
|
/** What `edit.addRows` reports back. Every added row is in exactly one list. */
|
|
539
567
|
export type TMDataGridAddRowsResult = {
|
|
568
|
+
/** `true` when every added row committed - `open` is empty. */
|
|
569
|
+
ok: boolean;
|
|
540
570
|
/** Temp ids that committed - parked as drafts, or added outright. */
|
|
541
571
|
committed: Array<string>;
|
|
542
572
|
/**
|
|
@@ -546,6 +576,68 @@ export type TMDataGridAddRowsResult = {
|
|
|
546
576
|
open: Array<string>;
|
|
547
577
|
};
|
|
548
578
|
|
|
579
|
+
/**
|
|
580
|
+
* What `edit.commitAll` reports back. Every row that was open at the call is
|
|
581
|
+
* in exactly one list; ids are row ids, and temp ids for entry rows.
|
|
582
|
+
*/
|
|
583
|
+
export type TMDataGridCommitAllResult = {
|
|
584
|
+
/** `true` when every open row committed - `open` is empty. */
|
|
585
|
+
ok: boolean;
|
|
586
|
+
/**
|
|
587
|
+
* Rows whose `commit` resolved `true` - into the draft store under
|
|
588
|
+
* `editing.draft`, out to the consumer without it, or closed with nothing
|
|
589
|
+
* to save.
|
|
590
|
+
*/
|
|
591
|
+
committed: Array<string>;
|
|
592
|
+
/**
|
|
593
|
+
* Rows still open, carrying their errors: validation failed, or without
|
|
594
|
+
* `editing.draft` the consumer's `onCommit` / `onRowAdd` threw.
|
|
595
|
+
*/
|
|
596
|
+
open: Array<string>;
|
|
597
|
+
};
|
|
598
|
+
|
|
599
|
+
/**
|
|
600
|
+
* What `edit.saveDrafts` reports back. Ids are row ids, and temp ids for new
|
|
601
|
+
* rows; edits, new rows and deletions are mixed in each list. Every id the
|
|
602
|
+
* save took from the draft store is in exactly one list. Rows open at the
|
|
603
|
+
* call are not part of the save and are in none.
|
|
604
|
+
*/
|
|
605
|
+
export type TMDataGridSaveDraftsResult = {
|
|
606
|
+
/** `true` when nothing was kept or reopened. */
|
|
607
|
+
ok: boolean;
|
|
608
|
+
/** Left the draft store - the consumer accepted it. */
|
|
609
|
+
saved: Array<string>;
|
|
610
|
+
/**
|
|
611
|
+
* Still in the draft store, still committed, and sent again by the next
|
|
612
|
+
* save: an id `onSaveDrafts` reported as failed, every id it was sent
|
|
613
|
+
* when it threw, or, without `onSaveDrafts`, a deletion whose
|
|
614
|
+
* `onRowDelete` threw.
|
|
615
|
+
*/
|
|
616
|
+
kept: Array<string>;
|
|
617
|
+
/**
|
|
618
|
+
* Out of the draft store and open again, carrying an error: a table rule
|
|
619
|
+
* now rejects the row, or, without `onSaveDrafts`, the row's own
|
|
620
|
+
* `onCommit` / `onRowAdd` threw.
|
|
621
|
+
*/
|
|
622
|
+
reopened: Array<string>;
|
|
623
|
+
};
|
|
624
|
+
|
|
625
|
+
/**
|
|
626
|
+
* Splits ids by the `commit` each one got - the shape `addRows` and
|
|
627
|
+
* `commitAll` share. `results[index]` answers for `ids[index]`; order is kept.
|
|
628
|
+
*/
|
|
629
|
+
function splitCommitted(
|
|
630
|
+
ids: ReadonlyArray<string>,
|
|
631
|
+
results: ReadonlyArray<boolean>,
|
|
632
|
+
): { ok: boolean; committed: Array<string>; open: Array<string> } {
|
|
633
|
+
const committed: Array<string> = [];
|
|
634
|
+
const open: Array<string> = [];
|
|
635
|
+
ids.forEach((id, index) => {
|
|
636
|
+
(results[index] ? committed : open).push(id);
|
|
637
|
+
});
|
|
638
|
+
return { ok: open.length === 0, committed, open };
|
|
639
|
+
}
|
|
640
|
+
|
|
549
641
|
/** One row of {@link TMDataGridEditApi.getRows}. */
|
|
550
642
|
export type TMDataGridEditRowSnapshot<
|
|
551
643
|
TData extends RowData = TMDataGridRowData,
|
|
@@ -563,9 +655,11 @@ export type TMDataGridEditRowSnapshot<
|
|
|
563
655
|
/**
|
|
564
656
|
* The engine plus its store - `api.edit`.
|
|
565
657
|
*
|
|
566
|
-
* "One row, one form": `getForm` hands out the same
|
|
567
|
-
* editors write through, so a consumer can render it in
|
|
568
|
-
* panel and share values, dirty state and errors with
|
|
658
|
+
* "One row, one form" while a row is open: `getForm` hands out the same
|
|
659
|
+
* `FormApi` the inline editors write through, so a consumer can render it in
|
|
660
|
+
* a drawer or a detail panel and share values, dirty state and errors with
|
|
661
|
+
* the cells. A committed row has no form - it is data in the draft store -
|
|
662
|
+
* so `getForm` returns `undefined` for it until `begin` reopens it.
|
|
569
663
|
*/
|
|
570
664
|
export type TMDataGridEditApi<
|
|
571
665
|
TData extends RowData = TMDataGridRowData,
|
|
@@ -574,13 +668,17 @@ export type TMDataGridEditApi<
|
|
|
574
668
|
store: Store<TMDataGridEditState>;
|
|
575
669
|
/** Current snapshot, for reads outside React. */
|
|
576
670
|
readonly state: TMDataGridEditState;
|
|
577
|
-
/**
|
|
671
|
+
/**
|
|
672
|
+
* rowId → the open row's live form, the source of truth for everything
|
|
673
|
+
* mid-edit. `undefined` for a row that is not open, a committed row
|
|
674
|
+
* included; `begin` reopens one.
|
|
675
|
+
*/
|
|
578
676
|
getForm: (rowId: string) => TMDataGridRowEditForm | undefined;
|
|
579
677
|
/**
|
|
580
|
-
* The row as shown: its draft values where
|
|
581
|
-
*
|
|
582
|
-
* such row exists. A deletion mark does not
|
|
583
|
-
* `state.deletedRowIds` for that.
|
|
678
|
+
* The row as shown: its draft values where the grid holds any - an open
|
|
679
|
+
* form's, or the committed values in the draft store - else what `data`
|
|
680
|
+
* says. `undefined` when no such row exists. A deletion mark does not
|
|
681
|
+
* change the answer; check `state.deletedRowIds` for that.
|
|
584
682
|
*/
|
|
585
683
|
getRowValues: (rowId: string) => TData | undefined;
|
|
586
684
|
/**
|
|
@@ -629,24 +727,33 @@ export type TMDataGridEditApi<
|
|
|
629
727
|
* Submits every open row, as if each had been OK'd: a row that validates
|
|
630
728
|
* commits (into the draft store with `editing.draft` on, straight to the
|
|
631
729
|
* consumer without it), a row that fails stays open with its errors.
|
|
632
|
-
*
|
|
633
|
-
*
|
|
730
|
+
* Under `editing.draft` it sends nothing to the consumer by itself - that
|
|
731
|
+
* is `saveDrafts`.
|
|
732
|
+
*
|
|
733
|
+
* Resolves which rows went which way: `committed` and `open` hold every
|
|
734
|
+
* row that was open at the call, each in exactly one list, and `ok` is
|
|
735
|
+
* `true` when `open` is empty. See {@link TMDataGridCommitAllResult}.
|
|
634
736
|
*/
|
|
635
|
-
commitAll: () => Promise<
|
|
737
|
+
commitAll: () => Promise<TMDataGridCommitAllResult>;
|
|
636
738
|
/**
|
|
637
739
|
* Flushes the draft store: every committed edit, added row and deletion
|
|
638
740
|
* mark reaches the consumer, through `onSaveDrafts` in one call when it is
|
|
639
741
|
* set, or row by row through `onCommit` / `onRowAdd` / `onRowDelete`.
|
|
640
742
|
*
|
|
641
743
|
* Rows still open are left alone - they keep their form state and stay
|
|
642
|
-
* open
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
*
|
|
647
|
-
*
|
|
744
|
+
* open, and are in no list of the result.
|
|
745
|
+
*
|
|
746
|
+
* Resolves what happened to each id it sent: `saved` left the draft store,
|
|
747
|
+
* `kept` is still in it, committed, for the next save - an id
|
|
748
|
+
* `onSaveDrafts` reported as failed, every id when it threw, or a
|
|
749
|
+
* deletion whose per-row `onRowDelete` threw - and
|
|
750
|
+
* `reopened` is open again with an error, because a table rule now
|
|
751
|
+
* rejects it or its per-row `onCommit` / `onRowAdd` threw. `ok` is `true`
|
|
752
|
+
* when nothing was kept or reopened, and an empty store resolves `ok` with
|
|
753
|
+
* empty lists. A call while a save is in flight joins it and resolves the
|
|
754
|
+
* same result. See {@link TMDataGridSaveDraftsResult}.
|
|
648
755
|
*/
|
|
649
|
-
|
|
756
|
+
saveDrafts: () => Promise<TMDataGridSaveDraftsResult>;
|
|
650
757
|
/** Writes the type's empty value into a cell and commits it - Delete. */
|
|
651
758
|
clearCell: (rowId: string, columnId: string) => Promise<boolean>;
|
|
652
759
|
/**
|
|
@@ -691,15 +798,19 @@ export type TMDataGridEditApi<
|
|
|
691
798
|
*/
|
|
692
799
|
addRow: (values?: Partial<TData>) => string;
|
|
693
800
|
/**
|
|
694
|
-
* Opens entry rows for a list of records at once - one
|
|
801
|
+
* Opens entry rows for a list of records at once - one publish for the
|
|
695
802
|
* batch, where a loop over `addRow` is one per row. Each row is seeded over
|
|
696
803
|
* `newRowDefaults` exactly as `addRow` does.
|
|
697
804
|
*
|
|
698
|
-
* `commit: true` submits
|
|
699
|
-
*
|
|
700
|
-
*
|
|
701
|
-
*
|
|
702
|
-
*
|
|
805
|
+
* `commit: true` submits the rows too, which is what an import wants: rows
|
|
806
|
+
* that validate commit, and rows that fail stay open in the entry block
|
|
807
|
+
* carrying their errors, for the user to fix. The result says which went
|
|
808
|
+
* which way - `committed` and `open` hold every added row, each in exactly
|
|
809
|
+
* one list, and `ok` is `true` when `open` is empty. Under
|
|
810
|
+
* `editing.draft` the rows validate together and land in the draft store
|
|
811
|
+
* in the same publish as the add - the grid renders once,
|
|
812
|
+
* whatever the count. Without it each valid row goes out through
|
|
813
|
+
* `onRowAdd`, one at a time and in order.
|
|
703
814
|
*/
|
|
704
815
|
addRows: (
|
|
705
816
|
rows: ReadonlyArray<Partial<TData>>,
|
|
@@ -707,11 +818,26 @@ export type TMDataGridEditApi<
|
|
|
707
818
|
) => Promise<TMDataGridAddRowsResult>;
|
|
708
819
|
/**
|
|
709
820
|
* Deletes a row: `onRowDelete` straight away, or under `editing.draft` a
|
|
710
|
-
*
|
|
711
|
-
*
|
|
712
|
-
*
|
|
821
|
+
* mark in `deletedRowIds` - the row renders struck through until
|
|
822
|
+
* `saveDrafts` reports it. Idempotent: deleting a marked row again leaves
|
|
823
|
+
* it marked, and {@link restoreRow} is the undo. On an entry row,
|
|
824
|
+
* committed or not, it just discards the entry; an id the grid does not
|
|
825
|
+
* know is a no-op.
|
|
713
826
|
*/
|
|
714
827
|
deleteRow: (rowId: string) => void;
|
|
828
|
+
/**
|
|
829
|
+
* {@link deleteRow} for several rows in one call - one notification for
|
|
830
|
+
* the batch, for a bulk action over a selection. Because `deleteRow` is
|
|
831
|
+
* idempotent and ignores unknown ids, the list may be passed exactly as
|
|
832
|
+
* the selection stands - already-marked rows stay marked, duplicates and
|
|
833
|
+
* stale ids do nothing.
|
|
834
|
+
*/
|
|
835
|
+
deleteRows: (rowIds: ReadonlyArray<string>) => void;
|
|
836
|
+
/**
|
|
837
|
+
* Removes a row's deletion mark - the lane's Restore. A no-op on a row
|
|
838
|
+
* that is not marked, and outside `editing.draft`, where no marks exist.
|
|
839
|
+
*/
|
|
840
|
+
restoreRow: (rowId: string) => void;
|
|
715
841
|
/** Whether delete chrome makes sense - the lane's trash gate. */
|
|
716
842
|
canDeleteRows: () => boolean;
|
|
717
843
|
};
|
|
@@ -835,22 +961,36 @@ async function runFieldValidator(
|
|
|
835
961
|
* form keeps its values, meta and errors; scroll back and the editor
|
|
836
962
|
* re-mounts over the same form.
|
|
837
963
|
*/
|
|
964
|
+
/**
|
|
965
|
+
* The engine as the hook holds it: the public `edit` API plus the verbs the
|
|
966
|
+
* grid calls on its own behalf and does not document.
|
|
967
|
+
*/
|
|
968
|
+
export type TMDataGridEditEngine = TMDataGridEditApi & {
|
|
969
|
+
/** See the implementation. `rowsById` is the core row model's. */
|
|
970
|
+
forgetMissingRows: (rowsById: Record<string, unknown>) => void;
|
|
971
|
+
/**
|
|
972
|
+
* Whether the row carries a deletion mark - the set behind
|
|
973
|
+
* `state.deletedRowIds`, for a check per row from a predicate the table
|
|
974
|
+
* calls per row.
|
|
975
|
+
*/
|
|
976
|
+
isRowDeleted: (rowId: string) => boolean;
|
|
977
|
+
};
|
|
978
|
+
|
|
838
979
|
export function createEditEngine(
|
|
839
980
|
getContext: () => TMDataGridEditEngineContext,
|
|
840
|
-
):
|
|
981
|
+
): TMDataGridEditEngine {
|
|
841
982
|
const store = new Store<TMDataGridEditState>(EMPTY_EDIT_STATE);
|
|
842
983
|
|
|
984
|
+
/**
|
|
985
|
+
* One open row's editor state. Only an open row has one: a row that
|
|
986
|
+
* commits into the draft store hands its values to `committed` and its
|
|
987
|
+
* form is dropped, so nothing in here describes a committed row.
|
|
988
|
+
*/
|
|
843
989
|
type FormEntry = {
|
|
844
990
|
form: TMDataGridRowEditForm;
|
|
845
991
|
original: TMDataGridRowData;
|
|
846
992
|
/** An entry-block row - a form with no backing row yet. */
|
|
847
993
|
isNew: boolean;
|
|
848
|
-
/**
|
|
849
|
-
* In the draft store: this row's form passed its submit and is parked,
|
|
850
|
-
* waiting for `saveDrafts`. Mirrored into the state's `committedRowIds`
|
|
851
|
-
* (existing rows) or `newRows[].committed` (entry rows).
|
|
852
|
-
*/
|
|
853
|
-
committed: boolean;
|
|
854
994
|
/** Set by the wrapped onSubmit when the consumer's commit resolved. */
|
|
855
995
|
lastSubmitOk: boolean;
|
|
856
996
|
/**
|
|
@@ -862,31 +1002,35 @@ export function createEditEngine(
|
|
|
862
1002
|
submitErrors: Array<{ field: string; value: unknown; message: string }>;
|
|
863
1003
|
/**
|
|
864
1004
|
* The park: this submit validates and puts the row in the draft store
|
|
865
|
-
* instead of calling the consumer. Set by `commit` per attempt
|
|
866
|
-
*
|
|
867
|
-
* what closes the per-row escape hatches (the lane's ✓,
|
|
868
|
-
* Delete-to-clear) at the engine.
|
|
1005
|
+
* instead of calling the consumer. Set by `commit` per attempt, from
|
|
1006
|
+
* `editing.draft`.
|
|
869
1007
|
*/
|
|
870
1008
|
parkOnly: boolean;
|
|
871
1009
|
/** A commit already running - Enter and blur race on the same edit. */
|
|
872
1010
|
pendingCommit: Promise<boolean> | null;
|
|
873
1011
|
unsubscribe: () => void;
|
|
874
|
-
unmount: () => void;
|
|
875
1012
|
};
|
|
876
1013
|
const forms = new Map<string, FormEntry>();
|
|
877
|
-
let newRowCounter = 0;
|
|
878
|
-
/** Lets `commit` tell a `saveDrafts` flush apart from a lone commit. */
|
|
879
|
-
let savingDrafts = false;
|
|
880
1014
|
|
|
881
1015
|
/**
|
|
882
|
-
*
|
|
883
|
-
*
|
|
884
|
-
*
|
|
1016
|
+
* One committed row in the draft store. A committed row is plain data:
|
|
1017
|
+
* it passed its validators, the user left it, and nothing can change its
|
|
1018
|
+
* values until a reopen builds a form back from this. Which is what keeps
|
|
1019
|
+
* an import of ten thousand rows to a few megabytes - a live `FormApi` per
|
|
1020
|
+
* row is some eight kilobytes each - and what makes the store serializable.
|
|
885
1021
|
*/
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
1022
|
+
type Committed = {
|
|
1023
|
+
/** The data row, or the entry row's seed - what `changes` diffs against. */
|
|
1024
|
+
original: TMDataGridRowData;
|
|
1025
|
+
/** The committed values; `committedValues[rowId]` holds this same object. */
|
|
1026
|
+
values: TMDataGridRowData;
|
|
1027
|
+
/** `diff(original, values)` as it stood at commit time. */
|
|
1028
|
+
dirtyFields: Array<string>;
|
|
1029
|
+
isNew: boolean;
|
|
1030
|
+
};
|
|
1031
|
+
const committed = new Map<string, Committed>();
|
|
1032
|
+
let newRowCounter = 0;
|
|
1033
|
+
const NEW_ROW_ID_PREFIX = "__new__";
|
|
890
1034
|
|
|
891
1035
|
/**
|
|
892
1036
|
* The column's half of the rule, with no row in hand: it is a consumer
|
|
@@ -946,49 +1090,62 @@ export function createEditEngine(
|
|
|
946
1090
|
return Object.keys(fields).length > 0 ? fields : undefined;
|
|
947
1091
|
};
|
|
948
1092
|
|
|
1093
|
+
/**
|
|
1094
|
+
* The draft the grid is holding for a row: an open form's live values, or
|
|
1095
|
+
* a committed row's snapshot. `undefined` for a row it holds nothing for.
|
|
1096
|
+
*/
|
|
1097
|
+
const draftValues = (rowId: string): TMDataGridRowData | undefined => {
|
|
1098
|
+
const entry = forms.get(rowId);
|
|
1099
|
+
if (entry !== undefined) {
|
|
1100
|
+
return entry.form.state.values as TMDataGridRowData;
|
|
1101
|
+
}
|
|
1102
|
+
return committed.get(rowId)?.values;
|
|
1103
|
+
};
|
|
1104
|
+
|
|
949
1105
|
/**
|
|
950
1106
|
* The collection as a table validator sees it: every data row overlaid
|
|
951
|
-
* with its draft where
|
|
952
|
-
* marked deleted removed. The committing row
|
|
953
|
-
* submitted
|
|
954
|
-
*
|
|
1107
|
+
* with its draft where the grid holds one, entry rows appended, and rows
|
|
1108
|
+
* marked deleted removed. The committing row, when there is one,
|
|
1109
|
+
* contributes the values being submitted rather than what `data` or the
|
|
1110
|
+
* draft store still says. Built from the core row model, so it is
|
|
1111
|
+
* unfiltered and never contains group rows.
|
|
955
1112
|
*/
|
|
956
|
-
const mergedRows = (
|
|
957
|
-
rowId: string
|
|
958
|
-
value: TMDataGridRowData
|
|
959
|
-
isNew: boolean
|
|
960
|
-
): Array<{ rowId: string; value: TMDataGridRowData }> => {
|
|
961
|
-
const deleted =
|
|
1113
|
+
const mergedRows = (committing?: {
|
|
1114
|
+
rowId: string;
|
|
1115
|
+
value: TMDataGridRowData;
|
|
1116
|
+
isNew: boolean;
|
|
1117
|
+
}): Array<{ rowId: string; value: TMDataGridRowData }> => {
|
|
1118
|
+
const deleted = working.deletedRowIds;
|
|
962
1119
|
const rows: Array<{ rowId: string; value: TMDataGridRowData }> = [];
|
|
963
1120
|
const model = getContext().table.getCoreRowModel();
|
|
964
1121
|
for (const row of model.flatRows) {
|
|
965
1122
|
if (deleted.has(row.id)) continue;
|
|
966
|
-
if (row.id === rowId) {
|
|
967
|
-
rows.push({ rowId, value });
|
|
1123
|
+
if (row.id === committing?.rowId) {
|
|
1124
|
+
rows.push({ rowId: row.id, value: committing.value });
|
|
968
1125
|
continue;
|
|
969
1126
|
}
|
|
970
|
-
const held = forms.get(row.id);
|
|
971
1127
|
rows.push({
|
|
972
1128
|
rowId: row.id,
|
|
973
|
-
value:
|
|
974
|
-
held === undefined
|
|
975
|
-
? (row.original as TMDataGridRowData)
|
|
976
|
-
: (held.form.state.values as TMDataGridRowData),
|
|
1129
|
+
value: draftValues(row.id) ?? (row.original as TMDataGridRowData),
|
|
977
1130
|
});
|
|
978
1131
|
}
|
|
979
1132
|
// Entry rows the table does not hold: the open ones, and the committed
|
|
980
1133
|
// ones under `newRowsSticky`. A committed row in flow is in `data`
|
|
981
1134
|
// already, so it was listed above.
|
|
982
|
-
for (const
|
|
983
|
-
if (
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
1135
|
+
for (const tempId of working.newRows.keys()) {
|
|
1136
|
+
if (tempId === committing?.rowId || tempId in model.rowsById) continue;
|
|
1137
|
+
if (deleted.has(tempId)) continue;
|
|
1138
|
+
const values = draftValues(tempId);
|
|
1139
|
+
if (values === undefined) continue;
|
|
1140
|
+
rows.push({ rowId: tempId, value: values });
|
|
1141
|
+
}
|
|
1142
|
+
if (
|
|
1143
|
+
committing !== undefined &&
|
|
1144
|
+
committing.isNew &&
|
|
1145
|
+
!(committing.rowId in model.rowsById)
|
|
1146
|
+
) {
|
|
1147
|
+
rows.push({ rowId: committing.rowId, value: committing.value });
|
|
990
1148
|
}
|
|
991
|
-
if (isNew && !(rowId in model.rowsById)) rows.push({ rowId, value });
|
|
992
1149
|
return rows;
|
|
993
1150
|
};
|
|
994
1151
|
|
|
@@ -996,11 +1153,17 @@ export function createEditEngine(
|
|
|
996
1153
|
* Runs `editing.tableValidators` for one commit. `onSubmit` first; its
|
|
997
1154
|
* failure stands and `onSubmitAsync` is not consulted, mirroring how the
|
|
998
1155
|
* column triggers short-circuit.
|
|
1156
|
+
*
|
|
1157
|
+
* `rows` is the collection to judge against, for the caller that has one
|
|
1158
|
+
* already: a Save pass runs the rules over rows whose values are frozen,
|
|
1159
|
+
* so building the collection once there turns an O(N²) pass into an O(N)
|
|
1160
|
+
* one. Left out, this builds the view for the committing row itself.
|
|
999
1161
|
*/
|
|
1000
1162
|
const runTableValidators = async (
|
|
1001
1163
|
value: TMDataGridRowData,
|
|
1002
1164
|
rowId: string,
|
|
1003
1165
|
isNew: boolean,
|
|
1166
|
+
rows?: ReadonlyArray<{ rowId: string; value: TMDataGridRowData }>,
|
|
1004
1167
|
): Promise<unknown> => {
|
|
1005
1168
|
const validators = getContext().tableValidators;
|
|
1006
1169
|
if (
|
|
@@ -1013,7 +1176,7 @@ export function createEditEngine(
|
|
|
1013
1176
|
value,
|
|
1014
1177
|
rowId,
|
|
1015
1178
|
isNew,
|
|
1016
|
-
rows: mergedRows(rowId, value, isNew),
|
|
1179
|
+
rows: rows ?? mergedRows({ rowId, value, isNew }),
|
|
1017
1180
|
};
|
|
1018
1181
|
const sync = validators.onSubmit?.(args);
|
|
1019
1182
|
if (isValidationError(sync)) return sync;
|
|
@@ -1094,15 +1257,16 @@ export function createEditEngine(
|
|
|
1094
1257
|
};
|
|
1095
1258
|
};
|
|
1096
1259
|
|
|
1260
|
+
/** What moved between a row as it was and a row as drafted. */
|
|
1097
1261
|
const diff = (
|
|
1098
|
-
|
|
1262
|
+
original: TMDataGridRowData,
|
|
1099
1263
|
values: TMDataGridRowData,
|
|
1100
1264
|
): Array<TMDataGridEditChange> => {
|
|
1101
1265
|
const changes: Array<TMDataGridEditChange> = [];
|
|
1102
1266
|
for (const column of editableColumns()) {
|
|
1103
1267
|
const field = getEditFieldName(column);
|
|
1104
1268
|
if (field === null) continue;
|
|
1105
|
-
const previous: unknown = getBy(
|
|
1269
|
+
const previous: unknown = getBy(original, field);
|
|
1106
1270
|
const next: unknown = getBy(values, field);
|
|
1107
1271
|
if (!sameValue(previous, next)) {
|
|
1108
1272
|
changes.push({ columnId: column.id, field, previous, next });
|
|
@@ -1134,7 +1298,7 @@ export function createEditEngine(
|
|
|
1134
1298
|
...kept.map(({ field, message }) => ({ field, message })),
|
|
1135
1299
|
];
|
|
1136
1300
|
return {
|
|
1137
|
-
dirtyFields: diff(entry, values).map((change) => change.field),
|
|
1301
|
+
dirtyFields: diff(entry.original, values).map((change) => change.field),
|
|
1138
1302
|
errorFields: errorMessages.map((error) => error.field),
|
|
1139
1303
|
errorMessages,
|
|
1140
1304
|
hasRowError: hasAnyError(state.errors),
|
|
@@ -1143,143 +1307,314 @@ export function createEditEngine(
|
|
|
1143
1307
|
};
|
|
1144
1308
|
};
|
|
1145
1309
|
|
|
1310
|
+
/**
|
|
1311
|
+
* The same projection for a committed row, built from its snapshot. There
|
|
1312
|
+
* is no form to read and nothing left to decide: the values passed every
|
|
1313
|
+
* rule on the way in, so the error slots are empty and stay empty until a
|
|
1314
|
+
* reopen puts the row back into form state.
|
|
1315
|
+
*/
|
|
1316
|
+
const projectCommitted = (
|
|
1317
|
+
snapshot: Committed,
|
|
1318
|
+
): TMDataGridEditRowProjection => ({
|
|
1319
|
+
dirtyFields: snapshot.dirtyFields,
|
|
1320
|
+
errorFields: [],
|
|
1321
|
+
errorMessages: [],
|
|
1322
|
+
hasRowError: false,
|
|
1323
|
+
isSubmitting: false,
|
|
1324
|
+
values: snapshot.values,
|
|
1325
|
+
});
|
|
1326
|
+
|
|
1327
|
+
/**
|
|
1328
|
+
* The engine's own copy of the state, in maps and sets, so a write is O(1)
|
|
1329
|
+
* where the published shape would copy an array or a record. Every write
|
|
1330
|
+
* lands here; `publish` materializes it into `store`, rebuilding only the
|
|
1331
|
+
* slices that changed - a keystroke in an open editor leaves
|
|
1332
|
+
* `committedValues` and `newRows` identity-stable, which is what keeps the
|
|
1333
|
+
* table from rebuilding its row model under a caret.
|
|
1334
|
+
*
|
|
1335
|
+
* Holding the publish is what makes a batch verb scale. `addRows`,
|
|
1336
|
+
* `commitAll`, `saveDrafts` and `cancelAll` each touch every row they
|
|
1337
|
+
* hold, and an immutable write per row was quadratic: each one copied
|
|
1338
|
+
* `rows` and `committedValues`, rebuilt `openRowIds` and woke every
|
|
1339
|
+
* subscriber, so an import of ten thousand rows spent minutes copying and
|
|
1340
|
+
* re-rendering. Held, a run's writes stay O(1) each and the grid renders
|
|
1341
|
+
* once when it ends.
|
|
1342
|
+
*/
|
|
1343
|
+
const working = {
|
|
1344
|
+
active: null as TMDataGridEditState["active"],
|
|
1345
|
+
/**
|
|
1346
|
+
* Every row the grid holds work for, open or committed, in the order the
|
|
1347
|
+
* rows first entered. Its own registry rather than `forms.keys()` plus
|
|
1348
|
+
* the committed ids: rows commit in whatever order their async
|
|
1349
|
+
* validators resolve, and the published order is the order the user
|
|
1350
|
+
* opened them in. A re-add is a no-op on a Set, so a reopen keeps a
|
|
1351
|
+
* row's place.
|
|
1352
|
+
*/
|
|
1353
|
+
openRowIds: new Set<string>(),
|
|
1354
|
+
rows: new Map<string, TMDataGridEditRowProjection>(),
|
|
1355
|
+
committedRowIds: new Set<string>(),
|
|
1356
|
+
committedValues: new Map<string, TMDataGridRowData>(),
|
|
1357
|
+
/** `tempId` to its committed flag, in the order the rows were opened. */
|
|
1358
|
+
newRows: new Map<string, boolean>(),
|
|
1359
|
+
deletedRowIds: new Set<string>(),
|
|
1360
|
+
isSaving: false,
|
|
1361
|
+
};
|
|
1362
|
+
/** The slices the next publish has to rebuild. */
|
|
1363
|
+
const dirty = new Set<keyof TMDataGridEditState>();
|
|
1364
|
+
/** Forms whose projection is stale - projected once each, at publish. */
|
|
1365
|
+
const staleRows = new Set<string>();
|
|
1366
|
+
/** Above zero, `publish` waits for the run holding it to end. */
|
|
1367
|
+
let holdDepth = 0;
|
|
1368
|
+
|
|
1369
|
+
// Rows the engine took out of the table, and rows that only lost their
|
|
1370
|
+
// selection - see flushRowState.
|
|
1371
|
+
const pendingLeft = new Set<string>();
|
|
1372
|
+
const pendingUnselect = new Set<string>();
|
|
1373
|
+
|
|
1374
|
+
/**
|
|
1375
|
+
* Drops rows the engine took out of the table from the table state keyed
|
|
1376
|
+
* by row id: `rowSelection`, `expanded` and `rowPinning`. A row that was
|
|
1377
|
+
* only marked for deletion loses its selection alone - it is still there,
|
|
1378
|
+
* pinned or open as it was.
|
|
1379
|
+
*
|
|
1380
|
+
* TanStack never prunes those maps itself: `toggleAllRowsSelected(false)`
|
|
1381
|
+
* unticks only rows still in the model, `getIsSomeRowsSelected` counts
|
|
1382
|
+
* every key, and `expanded` is persisted with whatever ids it holds. An id
|
|
1383
|
+
* left behind keeps the header box indeterminate, flips "select all" back
|
|
1384
|
+
* to selecting, and shows a consumer reading the keys a row that is gone.
|
|
1385
|
+
* Two kinds of row leave through the engine: an entry row, discarded or
|
|
1386
|
+
* saved - its temp id never comes back - and a marked row whose deletion
|
|
1387
|
+
* the consumer has acted on. Run from `publish`, so a bulk delete or a
|
|
1388
|
+
* save prunes in one update per slice.
|
|
1389
|
+
*/
|
|
1390
|
+
const flushRowState = () => {
|
|
1391
|
+
if (pendingLeft.size === 0 && pendingUnselect.size === 0) return;
|
|
1392
|
+
const left = [...pendingLeft];
|
|
1393
|
+
const unselect = [...left, ...pendingUnselect];
|
|
1394
|
+
pendingLeft.clear();
|
|
1395
|
+
pendingUnselect.clear();
|
|
1396
|
+
const table = getContext().table;
|
|
1397
|
+
const { rowSelection, expanded, rowPinning } = table.store.state;
|
|
1398
|
+
const has = (map: object, rowId: string) =>
|
|
1399
|
+
Object.prototype.hasOwnProperty.call(map, rowId);
|
|
1400
|
+
if (unselect.some((rowId) => has(rowSelection, rowId))) {
|
|
1401
|
+
table.setRowSelection((old) => {
|
|
1402
|
+
const next = { ...old };
|
|
1403
|
+
for (const rowId of unselect) delete next[rowId];
|
|
1404
|
+
return next;
|
|
1405
|
+
});
|
|
1406
|
+
}
|
|
1407
|
+
if (left.length === 0) return;
|
|
1408
|
+
if (expanded !== true && left.some((rowId) => has(expanded, rowId))) {
|
|
1409
|
+
table.setExpanded((old) => {
|
|
1410
|
+
if (old === true) return old;
|
|
1411
|
+
const next = { ...old };
|
|
1412
|
+
for (const rowId of left) delete next[rowId];
|
|
1413
|
+
return next;
|
|
1414
|
+
});
|
|
1415
|
+
}
|
|
1416
|
+
const gone = new Set(left);
|
|
1417
|
+
if (
|
|
1418
|
+
rowPinning.top.some((rowId) => gone.has(rowId)) ||
|
|
1419
|
+
rowPinning.bottom.some((rowId) => gone.has(rowId))
|
|
1420
|
+
) {
|
|
1421
|
+
table.setRowPinning((old) => ({
|
|
1422
|
+
top: old.top.filter((rowId) => !gone.has(rowId)),
|
|
1423
|
+
bottom: old.bottom.filter((rowId) => !gone.has(rowId)),
|
|
1424
|
+
}));
|
|
1425
|
+
}
|
|
1426
|
+
};
|
|
1427
|
+
|
|
1428
|
+
const publish = () => {
|
|
1429
|
+
if (holdDepth > 0) return;
|
|
1430
|
+
flushRowState();
|
|
1431
|
+
if (staleRows.size > 0) {
|
|
1432
|
+
for (const rowId of staleRows) {
|
|
1433
|
+
const entry = forms.get(rowId);
|
|
1434
|
+
if (entry !== undefined) working.rows.set(rowId, project(entry));
|
|
1435
|
+
// A row whose form is gone because it committed keeps the projection
|
|
1436
|
+
// `releaseForm` wrote from its snapshot; only a row that left the
|
|
1437
|
+
// grid loses it.
|
|
1438
|
+
else if (!committed.has(rowId)) working.rows.delete(rowId);
|
|
1439
|
+
}
|
|
1440
|
+
staleRows.clear();
|
|
1441
|
+
dirty.add("rows");
|
|
1442
|
+
}
|
|
1443
|
+
if (dirty.size === 0) return;
|
|
1444
|
+
const next = { ...store.state };
|
|
1445
|
+
if (dirty.has("active")) next.active = working.active;
|
|
1446
|
+
if (dirty.has("openRowIds")) next.openRowIds = [...working.openRowIds];
|
|
1447
|
+
if (dirty.has("rows")) next.rows = Object.fromEntries(working.rows);
|
|
1448
|
+
if (dirty.has("committedRowIds")) {
|
|
1449
|
+
next.committedRowIds = [...working.committedRowIds];
|
|
1450
|
+
}
|
|
1451
|
+
if (dirty.has("committedValues")) {
|
|
1452
|
+
next.committedValues = Object.fromEntries(working.committedValues);
|
|
1453
|
+
}
|
|
1454
|
+
if (dirty.has("newRows")) {
|
|
1455
|
+
next.newRows = [...working.newRows].map(([tempId, committed]) => ({
|
|
1456
|
+
tempId,
|
|
1457
|
+
committed,
|
|
1458
|
+
}));
|
|
1459
|
+
}
|
|
1460
|
+
if (dirty.has("deletedRowIds")) {
|
|
1461
|
+
next.deletedRowIds = [...working.deletedRowIds];
|
|
1462
|
+
}
|
|
1463
|
+
if (dirty.has("isSaving")) next.isSaving = working.isSaving;
|
|
1464
|
+
dirty.clear();
|
|
1465
|
+
store.setState(() => next);
|
|
1466
|
+
};
|
|
1467
|
+
|
|
1468
|
+
/** Marks slices changed and publishes - the ordinary single write. */
|
|
1469
|
+
const touch = (...slices: Array<keyof TMDataGridEditState>) => {
|
|
1470
|
+
for (const slice of slices) dirty.add(slice);
|
|
1471
|
+
publish();
|
|
1472
|
+
};
|
|
1473
|
+
|
|
1474
|
+
/** Runs `body` with the publish held: one render for the whole run. */
|
|
1475
|
+
const held = async <T,>(body: () => Promise<T>): Promise<T> => {
|
|
1476
|
+
holdDepth += 1;
|
|
1477
|
+
try {
|
|
1478
|
+
return await body();
|
|
1479
|
+
} finally {
|
|
1480
|
+
holdDepth -= 1;
|
|
1481
|
+
publish();
|
|
1482
|
+
}
|
|
1483
|
+
};
|
|
1484
|
+
|
|
1485
|
+
/** {@link held} for a synchronous run. */
|
|
1486
|
+
const heldSync = <T,>(body: () => T): T => {
|
|
1487
|
+
holdDepth += 1;
|
|
1488
|
+
try {
|
|
1489
|
+
return body();
|
|
1490
|
+
} finally {
|
|
1491
|
+
holdDepth -= 1;
|
|
1492
|
+
publish();
|
|
1493
|
+
}
|
|
1494
|
+
};
|
|
1495
|
+
|
|
1146
1496
|
const publishRow = (rowId: string) => {
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
const rows = { ...prev.rows };
|
|
1150
|
-
if (entry === undefined) delete rows[rowId];
|
|
1151
|
-
else rows[rowId] = project(entry);
|
|
1152
|
-
return { ...prev, rows, openRowIds: [...forms.keys()] };
|
|
1153
|
-
});
|
|
1497
|
+
staleRows.add(rowId);
|
|
1498
|
+
publish();
|
|
1154
1499
|
};
|
|
1155
1500
|
|
|
1156
1501
|
const setActive = (active: TMDataGridEditState["active"]) => {
|
|
1157
|
-
|
|
1502
|
+
if (working.active === active) return;
|
|
1503
|
+
working.active = active;
|
|
1504
|
+
touch("active");
|
|
1158
1505
|
};
|
|
1159
1506
|
|
|
1160
1507
|
/**
|
|
1161
|
-
*
|
|
1162
|
-
* not only the first - `setCellValue` on a
|
|
1163
|
-
*
|
|
1164
|
-
*
|
|
1165
|
-
* the next decision.
|
|
1508
|
+
* Publishes a committed row's values from its snapshot. Refreshed on every
|
|
1509
|
+
* commit, not only the first - `setCellValue` on a committed row reopens
|
|
1510
|
+
* it, writes and commits it again, and the table has to see the second
|
|
1511
|
+
* value as well as the first. A reopen leaves the published values alone:
|
|
1512
|
+
* the table keeps showing the last decided ones until the next decision.
|
|
1513
|
+
* Marks the slice; the caller publishes.
|
|
1166
1514
|
*/
|
|
1167
|
-
const snapshotCommitted = (
|
|
1168
|
-
|
|
1169
|
-
rowId
|
|
1170
|
-
|
|
1171
|
-
const values = forms.get(rowId)?.form.state.values as
|
|
1172
|
-
| TMDataGridRowData
|
|
1173
|
-
| undefined;
|
|
1174
|
-
if (values === undefined || prev.committedValues[rowId] === values) {
|
|
1175
|
-
return prev.committedValues;
|
|
1515
|
+
const snapshotCommitted = (rowId: string) => {
|
|
1516
|
+
const values = committed.get(rowId)?.values;
|
|
1517
|
+
if (values === undefined || working.committedValues.get(rowId) === values) {
|
|
1518
|
+
return;
|
|
1176
1519
|
}
|
|
1177
|
-
|
|
1520
|
+
working.committedValues.set(rowId, values);
|
|
1521
|
+
dirty.add("committedValues");
|
|
1178
1522
|
};
|
|
1179
1523
|
|
|
1180
|
-
const setNewRowCommitted = (tempId: string,
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
committedValues === prev.committedValues
|
|
1190
|
-
) {
|
|
1191
|
-
return prev;
|
|
1192
|
-
}
|
|
1193
|
-
return {
|
|
1194
|
-
...prev,
|
|
1195
|
-
committedValues,
|
|
1196
|
-
newRows:
|
|
1197
|
-
target.committed === committed
|
|
1198
|
-
? prev.newRows
|
|
1199
|
-
: prev.newRows.map((newRow) =>
|
|
1200
|
-
newRow.tempId === tempId ? { ...newRow, committed } : newRow,
|
|
1201
|
-
),
|
|
1202
|
-
};
|
|
1203
|
-
});
|
|
1524
|
+
const setNewRowCommitted = (tempId: string, isCommitted: boolean) => {
|
|
1525
|
+
const current = working.newRows.get(tempId);
|
|
1526
|
+
if (current === undefined) return;
|
|
1527
|
+
if (isCommitted) snapshotCommitted(tempId);
|
|
1528
|
+
if (current !== isCommitted) {
|
|
1529
|
+
working.newRows.set(tempId, isCommitted);
|
|
1530
|
+
dirty.add("newRows");
|
|
1531
|
+
}
|
|
1532
|
+
publish();
|
|
1204
1533
|
};
|
|
1205
1534
|
|
|
1206
1535
|
/**
|
|
1207
1536
|
* Moves an existing row across the line between form state and the draft
|
|
1208
|
-
* store. The values
|
|
1209
|
-
*
|
|
1210
|
-
*
|
|
1537
|
+
* store. The values are the snapshot's; this records which side the row is
|
|
1538
|
+
* on and publishes them for the table on the way in. On the way out it
|
|
1539
|
+
* leaves `committedValues` standing, so the row keeps the place the last
|
|
1540
|
+
* decision gave it while the next one is being typed.
|
|
1211
1541
|
*/
|
|
1212
|
-
const setCommitted = (rowId: string,
|
|
1213
|
-
|
|
1214
|
-
if (
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
if (has === committed && committedValues === prev.committedValues) {
|
|
1221
|
-
return prev;
|
|
1222
|
-
}
|
|
1223
|
-
return {
|
|
1224
|
-
...prev,
|
|
1225
|
-
committedValues,
|
|
1226
|
-
committedRowIds:
|
|
1227
|
-
has === committed
|
|
1228
|
-
? prev.committedRowIds
|
|
1229
|
-
: committed
|
|
1230
|
-
? [...prev.committedRowIds, rowId]
|
|
1231
|
-
: prev.committedRowIds.filter((id) => id !== rowId),
|
|
1232
|
-
};
|
|
1233
|
-
});
|
|
1234
|
-
};
|
|
1235
|
-
|
|
1236
|
-
const withoutCommittedValues = (
|
|
1237
|
-
prev: TMDataGridEditState,
|
|
1238
|
-
rowId: string,
|
|
1239
|
-
): TMDataGridEditState["committedValues"] => {
|
|
1240
|
-
if (!(rowId in prev.committedValues)) return prev.committedValues;
|
|
1241
|
-
const { [rowId]: _dropped, ...rest } = prev.committedValues;
|
|
1242
|
-
return rest;
|
|
1542
|
+
const setCommitted = (rowId: string, isCommitted: boolean) => {
|
|
1543
|
+
if (isCommitted) snapshotCommitted(rowId);
|
|
1544
|
+
if (working.committedRowIds.has(rowId) !== isCommitted) {
|
|
1545
|
+
if (isCommitted) working.committedRowIds.add(rowId);
|
|
1546
|
+
else working.committedRowIds.delete(rowId);
|
|
1547
|
+
dirty.add("committedRowIds");
|
|
1548
|
+
}
|
|
1549
|
+
publish();
|
|
1243
1550
|
};
|
|
1244
1551
|
|
|
1245
|
-
|
|
1552
|
+
/**
|
|
1553
|
+
* Drops the form of a row that has just committed, leaving every trace of
|
|
1554
|
+
* the row itself: it is in the draft store now, and its projection is the
|
|
1555
|
+
* snapshot's. The form is what goes - eight kilobytes a row that nothing
|
|
1556
|
+
* can write to any more.
|
|
1557
|
+
*/
|
|
1558
|
+
const releaseForm = (rowId: string) => {
|
|
1246
1559
|
const entry = forms.get(rowId);
|
|
1247
1560
|
if (entry === undefined) return;
|
|
1248
1561
|
entry.unsubscribe();
|
|
1249
|
-
entry.unmount();
|
|
1250
1562
|
forms.delete(rowId);
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1563
|
+
staleRows.delete(rowId);
|
|
1564
|
+
const snapshot = committed.get(rowId);
|
|
1565
|
+
if (snapshot !== undefined) {
|
|
1566
|
+
working.rows.set(rowId, projectCommitted(snapshot));
|
|
1567
|
+
dirty.add("rows");
|
|
1568
|
+
}
|
|
1569
|
+
publish();
|
|
1570
|
+
};
|
|
1571
|
+
|
|
1572
|
+
/** The row leaves the grid: its form, its snapshot and every flag go. */
|
|
1573
|
+
const forget = (rowId: string) => {
|
|
1574
|
+
heldSync(() => {
|
|
1575
|
+
const isNew =
|
|
1576
|
+
forms.get(rowId)?.isNew ?? committed.get(rowId)?.isNew ?? false;
|
|
1577
|
+
releaseForm(rowId);
|
|
1578
|
+
committed.delete(rowId);
|
|
1579
|
+
if (working.openRowIds.delete(rowId)) dirty.add("openRowIds");
|
|
1580
|
+
if (working.rows.delete(rowId)) dirty.add("rows");
|
|
1581
|
+
if (working.committedRowIds.delete(rowId)) dirty.add("committedRowIds");
|
|
1582
|
+
if (working.committedValues.delete(rowId)) dirty.add("committedValues");
|
|
1583
|
+
if (isNew && working.newRows.delete(rowId)) dirty.add("newRows");
|
|
1584
|
+
// An entry row leaves the table for good: discarded, or saved and
|
|
1585
|
+
// coming back under the consumer's own id. Its selection, expansion
|
|
1586
|
+
// and pin go with it - see flushRowState.
|
|
1587
|
+
if (isNew) pendingLeft.add(rowId);
|
|
1588
|
+
if (working.active?.rowId === rowId) {
|
|
1589
|
+
working.active = null;
|
|
1590
|
+
dirty.add("active");
|
|
1591
|
+
}
|
|
1265
1592
|
});
|
|
1266
1593
|
};
|
|
1267
1594
|
|
|
1595
|
+
/**
|
|
1596
|
+
* Opens a row: a form over `original`, which is the row as it was when the
|
|
1597
|
+
* editing began and what a commit's `changes` are diffed against.
|
|
1598
|
+
*
|
|
1599
|
+
* `values` is what the form starts out holding when that is not `original`
|
|
1600
|
+
* - a reopen or a demotion, where the row already has committed values.
|
|
1601
|
+
* They are written before the mount validation runs, so a rule judging the
|
|
1602
|
+
* row on the way in sees what the user last decided, not the seed.
|
|
1603
|
+
*/
|
|
1268
1604
|
const createForm = (
|
|
1269
1605
|
rowId: string,
|
|
1270
1606
|
original: TMDataGridRowData,
|
|
1271
1607
|
isNew: boolean,
|
|
1608
|
+
values?: TMDataGridRowData,
|
|
1272
1609
|
): FormEntry => {
|
|
1273
1610
|
const entry: FormEntry = {
|
|
1274
1611
|
original,
|
|
1275
1612
|
isNew,
|
|
1276
|
-
committed: false,
|
|
1277
1613
|
lastSubmitOk: false,
|
|
1278
1614
|
submitErrors: [],
|
|
1279
1615
|
parkOnly: false,
|
|
1280
1616
|
pendingCommit: null,
|
|
1281
1617
|
unsubscribe: () => {},
|
|
1282
|
-
unmount: () => {},
|
|
1283
1618
|
form: new FormApi({
|
|
1284
1619
|
defaultValues: original,
|
|
1285
1620
|
// The consumer's vocabulary is Form's own; the cast erases the
|
|
@@ -1294,12 +1629,8 @@ export function createEditEngine(
|
|
|
1294
1629
|
tempId: rowId,
|
|
1295
1630
|
value: value as TMDataGridRowData,
|
|
1296
1631
|
};
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
entry.lastSubmitOk = true;
|
|
1300
|
-
return;
|
|
1301
|
-
}
|
|
1302
|
-
// Parked: validated and held for Save all - no consumer call.
|
|
1632
|
+
// Into the draft store: validated and held for Save - no
|
|
1633
|
+
// consumer call, and `commit` takes it from here.
|
|
1303
1634
|
if (entry.parkOnly) {
|
|
1304
1635
|
entry.lastSubmitOk = true;
|
|
1305
1636
|
return;
|
|
@@ -1315,7 +1646,7 @@ export function createEditEngine(
|
|
|
1315
1646
|
}
|
|
1316
1647
|
return;
|
|
1317
1648
|
}
|
|
1318
|
-
const changes = diff(entry, value as TMDataGridRowData);
|
|
1649
|
+
const changes = diff(entry.original, value as TMDataGridRowData);
|
|
1319
1650
|
const args: TMDataGridEditCommitArgs<TMDataGridRowData> = {
|
|
1320
1651
|
rowId,
|
|
1321
1652
|
value: value as TMDataGridRowData,
|
|
@@ -1323,12 +1654,8 @@ export function createEditEngine(
|
|
|
1323
1654
|
changes,
|
|
1324
1655
|
source: getContext().editMode,
|
|
1325
1656
|
};
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
entry.lastSubmitOk = true;
|
|
1329
|
-
return;
|
|
1330
|
-
}
|
|
1331
|
-
// Parked: validated and held for Save all - no consumer call.
|
|
1657
|
+
// Into the draft store: validated and held for Save - no consumer
|
|
1658
|
+
// call, and `commit` takes it from here.
|
|
1332
1659
|
if (entry.parkOnly) {
|
|
1333
1660
|
entry.lastSubmitOk = true;
|
|
1334
1661
|
return;
|
|
@@ -1346,22 +1673,118 @@ export function createEditEngine(
|
|
|
1346
1673
|
},
|
|
1347
1674
|
}) as TMDataGridRowEditForm,
|
|
1348
1675
|
};
|
|
1349
|
-
|
|
1676
|
+
// One write for the lot rather than a `setFieldValue` per field, which
|
|
1677
|
+
// would churn the field meta and run the change rules on the way past.
|
|
1678
|
+
// `keepDefaultValues` is what keeps `original` the thing the row is
|
|
1679
|
+
// dirty against.
|
|
1680
|
+
if (values !== undefined) {
|
|
1681
|
+
entry.form.reset(values as never, { keepDefaultValues: true });
|
|
1682
|
+
}
|
|
1683
|
+
// Not `form.mount()`. What mount does is devtools wiring - three `window`
|
|
1684
|
+
// listeners per form and a state broadcast on every change - and a grid
|
|
1685
|
+
// holding ten thousand row forms cannot afford it: every form's every
|
|
1686
|
+
// change would fan out to every other form's listeners. The one thing
|
|
1687
|
+
// the engine wants from it is the `onMount` pass.
|
|
1688
|
+
if (getContext().rowValidators?.onMount !== undefined) {
|
|
1689
|
+
entry.form.validateSync("mount");
|
|
1690
|
+
}
|
|
1350
1691
|
const subscription = entry.form.store.subscribe(() => publishRow(rowId));
|
|
1351
1692
|
entry.unsubscribe = () => subscription.unsubscribe();
|
|
1352
1693
|
forms.set(rowId, entry);
|
|
1353
|
-
|
|
1694
|
+
staleRows.add(rowId);
|
|
1695
|
+
// A Set add is idempotent, so a reopened row keeps the place it had.
|
|
1696
|
+
if (!working.openRowIds.has(rowId)) {
|
|
1697
|
+
working.openRowIds.add(rowId);
|
|
1698
|
+
dirty.add("openRowIds");
|
|
1699
|
+
}
|
|
1700
|
+
publish();
|
|
1354
1701
|
return entry;
|
|
1355
1702
|
};
|
|
1356
1703
|
|
|
1704
|
+
/**
|
|
1705
|
+
* Takes a committed row back out of the draft store and into form state: a
|
|
1706
|
+
* fresh form over the row as it was, holding the values the user last
|
|
1707
|
+
* decided. `undefined` when the row is not committed.
|
|
1708
|
+
*
|
|
1709
|
+
* `committedValues` is left standing on purpose - the table keeps showing
|
|
1710
|
+
* the last decided values, so the row holds its place in the sort while
|
|
1711
|
+
* the next cell is typed into.
|
|
1712
|
+
*/
|
|
1713
|
+
const reopen = (rowId: string): FormEntry | undefined => {
|
|
1714
|
+
const snapshot = committed.get(rowId);
|
|
1715
|
+
if (snapshot === undefined) return undefined;
|
|
1716
|
+
committed.delete(rowId);
|
|
1717
|
+
const entry = createForm(
|
|
1718
|
+
rowId,
|
|
1719
|
+
snapshot.original,
|
|
1720
|
+
snapshot.isNew,
|
|
1721
|
+
snapshot.values,
|
|
1722
|
+
);
|
|
1723
|
+
if (snapshot.isNew) setNewRowCommitted(rowId, false);
|
|
1724
|
+
else setCommitted(rowId, false);
|
|
1725
|
+
return entry;
|
|
1726
|
+
};
|
|
1727
|
+
|
|
1728
|
+
/**
|
|
1729
|
+
* A committed row that failed at Save: reopened, with the error on it.
|
|
1730
|
+
* That is what the user has to act on, and "committed" means validated, so
|
|
1731
|
+
* a row carrying an error cannot stay in the store.
|
|
1732
|
+
*
|
|
1733
|
+
* The error is read the way TanStack Form's own `normalizeError` reads a
|
|
1734
|
+
* validator's return: an object naming `form` or `fields` splits into a
|
|
1735
|
+
* row-level message and pathed ones, anything else is a row-level message.
|
|
1736
|
+
*/
|
|
1737
|
+
const demote = (rowId: string, error: unknown) => {
|
|
1738
|
+
const snapshot = committed.get(rowId);
|
|
1739
|
+
// No snapshot: `begin` took the row back out while a per-row save was
|
|
1740
|
+
// still waiting on the consumer. The row is open already, and the
|
|
1741
|
+
// answer lands on the form it has rather than being lost.
|
|
1742
|
+
const entry = snapshot === undefined ? forms.get(rowId) : reopen(rowId);
|
|
1743
|
+
if (entry === undefined) return;
|
|
1744
|
+
const values =
|
|
1745
|
+
snapshot?.values ?? (entry.form.state.values as TMDataGridRowData);
|
|
1746
|
+
const shape =
|
|
1747
|
+
typeof error === "object" && error !== null
|
|
1748
|
+
? (error as { form?: unknown; fields?: Record<string, unknown> })
|
|
1749
|
+
: undefined;
|
|
1750
|
+
const split = shape !== undefined && ("fields" in shape || "form" in shape);
|
|
1751
|
+
const fields = split ? shape.fields : undefined;
|
|
1752
|
+
const formError = split ? shape.form : error;
|
|
1753
|
+
if (fields !== undefined) {
|
|
1754
|
+
// The shape `takeFieldErrors` produces, which `project` keeps on the
|
|
1755
|
+
// row for as long as the value the rule tripped over stands.
|
|
1756
|
+
entry.submitErrors = Object.entries(fields).flatMap(([field, issue]) =>
|
|
1757
|
+
isValidationError(issue)
|
|
1758
|
+
? [
|
|
1759
|
+
{
|
|
1760
|
+
field,
|
|
1761
|
+
value: getBy(values, field),
|
|
1762
|
+
message: firstErrorText(issue) ?? "",
|
|
1763
|
+
},
|
|
1764
|
+
]
|
|
1765
|
+
: [],
|
|
1766
|
+
);
|
|
1767
|
+
}
|
|
1768
|
+
if (isValidationError(formError)) {
|
|
1769
|
+
entry.form.setErrorMap({ onSubmit: formError } as never);
|
|
1770
|
+
}
|
|
1771
|
+
publishRow(rowId);
|
|
1772
|
+
};
|
|
1773
|
+
|
|
1357
1774
|
const canEditRow = (row: ErasedRow): boolean => {
|
|
1358
1775
|
if (row.getIsGrouped()) return false;
|
|
1776
|
+
// A deletion-marked row is read-only until it is restored: an edit on a
|
|
1777
|
+
// row the save is about to delete has nowhere to go. The mark is checked
|
|
1778
|
+
// here, in the engine, so the keyboard and the verbs agree with the
|
|
1779
|
+
// pointer, which the table's CSS blocks.
|
|
1780
|
+
if (working.deletedRowIds.has(row.id)) return false;
|
|
1359
1781
|
return getContext().isRowEditable?.(row) !== false;
|
|
1360
1782
|
};
|
|
1361
1783
|
|
|
1362
1784
|
const canEditCell = (row: ErasedRow, column: ErasedColumn): boolean => {
|
|
1363
1785
|
const context = getContext();
|
|
1364
1786
|
if (row.getIsGrouped()) return false;
|
|
1787
|
+
if (working.deletedRowIds.has(row.id)) return false;
|
|
1365
1788
|
if (!isColumnEditable(column)) return false;
|
|
1366
1789
|
if (!isColumnEditableForRow(column, row)) return false;
|
|
1367
1790
|
if (context.isRowEditable !== undefined && !context.isRowEditable(row)) {
|
|
@@ -1401,6 +1824,8 @@ export function createEditEngine(
|
|
|
1401
1824
|
|
|
1402
1825
|
const commit = async (rowId: string): Promise<boolean> => {
|
|
1403
1826
|
const entry = forms.get(rowId);
|
|
1827
|
+
// No form: the row is committed already, or the grid never held it.
|
|
1828
|
+
// Either way there is nothing left to decide.
|
|
1404
1829
|
if (entry === undefined) return true;
|
|
1405
1830
|
// Enter and blur race on the same edit; the second caller joins the
|
|
1406
1831
|
// first's commit instead of submitting the row twice.
|
|
@@ -1409,9 +1834,10 @@ export function createEditEngine(
|
|
|
1409
1834
|
// Not for a new row: adding an untouched entry is still an add.
|
|
1410
1835
|
if (
|
|
1411
1836
|
!entry.isNew &&
|
|
1412
|
-
diff(entry, entry.form.state.values as TMDataGridRowData)
|
|
1837
|
+
diff(entry.original, entry.form.state.values as TMDataGridRowData)
|
|
1838
|
+
.length === 0
|
|
1413
1839
|
) {
|
|
1414
|
-
|
|
1840
|
+
forget(rowId);
|
|
1415
1841
|
return true;
|
|
1416
1842
|
}
|
|
1417
1843
|
// Field errors the *form's* validator planted are cleared before the
|
|
@@ -1421,13 +1847,18 @@ export function createEditEngine(
|
|
|
1421
1847
|
clearFormSourcedFieldErrors(entry.form);
|
|
1422
1848
|
entry.lastSubmitOk = false;
|
|
1423
1849
|
entry.submitErrors = [];
|
|
1424
|
-
entry.parkOnly = getContext().draft
|
|
1850
|
+
entry.parkOnly = getContext().draft;
|
|
1425
1851
|
entry.pendingCommit = (async () => {
|
|
1426
1852
|
try {
|
|
1427
1853
|
await entry.form.handleSubmit();
|
|
1428
1854
|
} finally {
|
|
1429
1855
|
entry.pendingCommit = null;
|
|
1430
1856
|
}
|
|
1857
|
+
// Dropped while the submit was in flight - cancel or deleteRow won the
|
|
1858
|
+
// race. The form is gone, so there is nothing to park or drop; marking
|
|
1859
|
+
// the row committed now would plant an id in `committedRowIds` that no
|
|
1860
|
+
// save or discard could ever clear.
|
|
1861
|
+
if (forms.get(rowId) !== entry) return true;
|
|
1431
1862
|
if (!entry.lastSubmitOk) {
|
|
1432
1863
|
// Snapshot before the caller closes the editor: the field errors go
|
|
1433
1864
|
// with it, and the row is about to be left carrying them.
|
|
@@ -1436,49 +1867,67 @@ export function createEditEngine(
|
|
|
1436
1867
|
return false;
|
|
1437
1868
|
}
|
|
1438
1869
|
if (entry.parkOnly) {
|
|
1439
|
-
// Into the draft store: the
|
|
1440
|
-
//
|
|
1441
|
-
//
|
|
1442
|
-
//
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1870
|
+
// Into the draft store, as data: the values are decided, so they are
|
|
1871
|
+
// snapshotted and the form goes. An entry row renders as a value
|
|
1872
|
+
// row, an edited row as its draft - both until `begin` builds a form
|
|
1873
|
+
// back from the snapshot, or `saveDrafts` flushes it.
|
|
1874
|
+
heldSync(() => {
|
|
1875
|
+
const values = entry.form.state.values as TMDataGridRowData;
|
|
1876
|
+
committed.set(rowId, {
|
|
1877
|
+
original: entry.original,
|
|
1878
|
+
values,
|
|
1879
|
+
dirtyFields: diff(entry.original, values).map(
|
|
1880
|
+
(change) => change.field,
|
|
1881
|
+
),
|
|
1882
|
+
isNew: entry.isNew,
|
|
1883
|
+
});
|
|
1884
|
+
if (entry.isNew) setNewRowCommitted(rowId, true);
|
|
1885
|
+
else setCommitted(rowId, true);
|
|
1886
|
+
if (working.active?.rowId === rowId) setActive(null);
|
|
1887
|
+
releaseForm(rowId);
|
|
1888
|
+
});
|
|
1449
1889
|
return true;
|
|
1450
1890
|
}
|
|
1451
|
-
|
|
1891
|
+
forget(rowId);
|
|
1452
1892
|
return true;
|
|
1453
1893
|
})();
|
|
1454
1894
|
return entry.pendingCommit;
|
|
1455
1895
|
};
|
|
1456
1896
|
|
|
1457
1897
|
const beginOn = (rowId: string, columnId: string | null) => {
|
|
1458
|
-
//
|
|
1459
|
-
//
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1898
|
+
// Held: a reopen builds a form, takes the row out of the draft store and
|
|
1899
|
+
// names the caret's cell, and the grid should see all of it at once.
|
|
1900
|
+
heldSync(() => {
|
|
1901
|
+
// An entry row has no backing row in the table. An open one is already
|
|
1902
|
+
// in its editors; a committed one is re-armed from its snapshot, which
|
|
1903
|
+
// with `editing.draft` on is how an entered row is edited again.
|
|
1904
|
+
if (forms.get(rowId)?.isNew === true) {
|
|
1905
|
+
setActive({ rowId, columnId });
|
|
1906
|
+
return;
|
|
1907
|
+
}
|
|
1908
|
+
if (committed.get(rowId)?.isNew === true) {
|
|
1909
|
+
reopen(rowId);
|
|
1910
|
+
setActive({ rowId, columnId });
|
|
1911
|
+
return;
|
|
1912
|
+
}
|
|
1913
|
+
const row = getRow(rowId);
|
|
1914
|
+
if (row === undefined) return;
|
|
1915
|
+
if (columnId !== null) {
|
|
1916
|
+
const column = getContext().table.getColumn(columnId);
|
|
1917
|
+
if (column === undefined || !canEditCell(row, column)) return;
|
|
1918
|
+
} else if (!canEditRow(row)) {
|
|
1919
|
+
return;
|
|
1920
|
+
}
|
|
1921
|
+
// Reopening a committed row takes it back out of the draft store: what
|
|
1922
|
+
// the user is now editing is undecided again, and `saveDrafts` must not
|
|
1923
|
+
// send it until it is committed afresh. It reopens from the snapshot,
|
|
1924
|
+
// never from `row.original` - the table is showing the draft, and the
|
|
1925
|
+
// row as it was is what a commit's `changes` are diffed against.
|
|
1926
|
+
if (!forms.has(rowId) && reopen(rowId) === undefined) {
|
|
1927
|
+
createForm(rowId, row.original, false);
|
|
1928
|
+
}
|
|
1465
1929
|
setActive({ rowId, columnId });
|
|
1466
|
-
|
|
1467
|
-
}
|
|
1468
|
-
const row = getRow(rowId);
|
|
1469
|
-
if (row === undefined) return;
|
|
1470
|
-
if (columnId !== null) {
|
|
1471
|
-
const column = getContext().table.getColumn(columnId);
|
|
1472
|
-
if (column === undefined || !canEditCell(row, column)) return;
|
|
1473
|
-
} else if (getContext().isRowEditable?.(row) === false) {
|
|
1474
|
-
return;
|
|
1475
|
-
}
|
|
1476
|
-
if (!forms.has(rowId)) createForm(rowId, row.original, false);
|
|
1477
|
-
// Reopening a committed row takes it back out of the draft store: what
|
|
1478
|
-
// the user is now editing is undecided again, and `saveDrafts` must not
|
|
1479
|
-
// send it until it is committed afresh.
|
|
1480
|
-
else if (forms.get(rowId)?.committed === true) setCommitted(rowId, false);
|
|
1481
|
-
setActive({ rowId, columnId });
|
|
1930
|
+
});
|
|
1482
1931
|
};
|
|
1483
1932
|
|
|
1484
1933
|
const begin: TMDataGridEditApi["begin"] = ({ rowId, columnId }) => {
|
|
@@ -1492,15 +1941,14 @@ export function createEditEngine(
|
|
|
1492
1941
|
// so a second row opening can neither discard the first nor be refused
|
|
1493
1942
|
// by it - nothing about one row's form bears on another's |
|
|
1494
1943
|
//
|
|
1495
|
-
// A
|
|
1496
|
-
// the save, so it
|
|
1944
|
+
// A committed row is not "open": it has had its submit and is waiting
|
|
1945
|
+
// for the save, so it holds no form and the sweep never sees it. An
|
|
1497
1946
|
// entry row is skipped too - it is row-shaped in every mode, and its ✓
|
|
1498
1947
|
// is the decision, so the sweep must not add a half-typed row.
|
|
1499
1948
|
if (editMode === "cell") {
|
|
1500
|
-
const openElsewhere = [...forms.keys()].find(
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
});
|
|
1949
|
+
const openElsewhere = [...forms.keys()].find(
|
|
1950
|
+
(id) => id !== rowId && forms.get(id)?.isNew !== true,
|
|
1951
|
+
);
|
|
1504
1952
|
if (openElsewhere !== undefined) {
|
|
1505
1953
|
void commit(openElsewhere).then((ok) => {
|
|
1506
1954
|
if (ok) beginOn(rowId, columnId);
|
|
@@ -1512,7 +1960,7 @@ export function createEditEngine(
|
|
|
1512
1960
|
};
|
|
1513
1961
|
|
|
1514
1962
|
const cancel = (rowId: string) => {
|
|
1515
|
-
|
|
1963
|
+
forget(rowId);
|
|
1516
1964
|
};
|
|
1517
1965
|
|
|
1518
1966
|
const deactivate = () => {
|
|
@@ -1520,27 +1968,34 @@ export function createEditEngine(
|
|
|
1520
1968
|
};
|
|
1521
1969
|
|
|
1522
1970
|
const cancelAll = () => {
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
...
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1971
|
+
heldSync(() => {
|
|
1972
|
+
// Open rows and committed ones alike - `openRowIds` is both.
|
|
1973
|
+
for (const rowId of [...working.openRowIds]) forget(rowId);
|
|
1974
|
+
committed.clear();
|
|
1975
|
+
working.active = null;
|
|
1976
|
+
working.committedRowIds.clear();
|
|
1977
|
+
working.committedValues.clear();
|
|
1978
|
+
working.deletedRowIds.clear();
|
|
1979
|
+
dirty.add("active");
|
|
1980
|
+
dirty.add("committedRowIds");
|
|
1981
|
+
dirty.add("committedValues");
|
|
1982
|
+
dirty.add("deletedRowIds");
|
|
1983
|
+
});
|
|
1531
1984
|
};
|
|
1532
1985
|
|
|
1533
|
-
|
|
1986
|
+
/** Opens one entry row - the form and its `newRows` entry, in one write. */
|
|
1987
|
+
const openEntryRow = (values?: TMDataGridRowData): string => {
|
|
1534
1988
|
newRowCounter += 1;
|
|
1535
|
-
const tempId =
|
|
1989
|
+
const tempId = `${NEW_ROW_ID_PREFIX}${newRowCounter}`;
|
|
1536
1990
|
createForm(tempId, seedNewRow(values), true);
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
newRows: [...prev.newRows, { tempId, committed: false }],
|
|
1540
|
-
}));
|
|
1991
|
+
working.newRows.set(tempId, false);
|
|
1992
|
+
dirty.add("newRows");
|
|
1541
1993
|
return tempId;
|
|
1542
1994
|
};
|
|
1543
1995
|
|
|
1996
|
+
const addRow = (values?: TMDataGridRowData): string =>
|
|
1997
|
+
heldSync(() => openEntryRow(values));
|
|
1998
|
+
|
|
1544
1999
|
/** Seeds one entry row's values - `newRowDefaults` under `values`. */
|
|
1545
2000
|
const seedNewRow = (values?: TMDataGridRowData): TMDataGridRowData => {
|
|
1546
2001
|
const { newRowDefaults } = getContext();
|
|
@@ -1556,56 +2011,74 @@ export function createEditEngine(
|
|
|
1556
2011
|
const addRows = async (
|
|
1557
2012
|
rows: ReadonlyArray<TMDataGridRowData>,
|
|
1558
2013
|
options?: TMDataGridAddRowsOptions,
|
|
1559
|
-
): Promise<TMDataGridAddRowsResult> =>
|
|
1560
|
-
|
|
1561
|
-
//
|
|
1562
|
-
//
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
const tempId = `__new__${newRowCounter}`;
|
|
1568
|
-
createForm(tempId, seedNewRow(values), true);
|
|
1569
|
-
tempIds.push(tempId);
|
|
2014
|
+
): Promise<TMDataGridAddRowsResult> =>
|
|
2015
|
+
// One publish for the whole import - the entry block and the table learn
|
|
2016
|
+
// of the rows once, committed, rather than once per row on the way in
|
|
2017
|
+
// and once more per commit.
|
|
2018
|
+
held(async () => {
|
|
2019
|
+
const tempIds = rows.map((values) => openEntryRow(values));
|
|
2020
|
+
if (options?.commit !== true) {
|
|
2021
|
+
return { ok: tempIds.length === 0, committed: [], open: tempIds };
|
|
1570
2022
|
}
|
|
1571
|
-
store.setState((prev) => ({
|
|
1572
|
-
...prev,
|
|
1573
|
-
newRows: [
|
|
1574
|
-
...prev.newRows,
|
|
1575
|
-
...tempIds.map((tempId) => ({ tempId, committed: false })),
|
|
1576
|
-
],
|
|
1577
|
-
}));
|
|
1578
|
-
});
|
|
1579
|
-
|
|
1580
|
-
if (options?.commit !== true) return { committed: [], open: tempIds };
|
|
1581
2023
|
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
2024
|
+
// A row that fails validation stays open carrying its errors.
|
|
2025
|
+
//
|
|
2026
|
+
// Parked, the rows are submitted together: Form runs a submit's async
|
|
2027
|
+
// validators off a timer, so one row after another is a timer per row
|
|
2028
|
+
// - minutes for an import, once the browser clamps nested timers.
|
|
2029
|
+
// Nothing but the engine hears a parked commit, so the rows may
|
|
2030
|
+
// validate side by side. Out to `onRowAdd`, one at a time and in
|
|
2031
|
+
// order, so the consumer sees the rows as the file had them.
|
|
2032
|
+
const results = getContext().draft
|
|
2033
|
+
? await Promise.all(tempIds.map((tempId) => commit(tempId)))
|
|
2034
|
+
: [];
|
|
2035
|
+
if (!getContext().draft) {
|
|
2036
|
+
for (const tempId of tempIds) results.push(await commit(tempId));
|
|
2037
|
+
}
|
|
2038
|
+
return splitCommitted(tempIds, results);
|
|
2039
|
+
});
|
|
1592
2040
|
|
|
1593
2041
|
const deleteRow = (rowId: string) => {
|
|
1594
|
-
|
|
1595
|
-
//
|
|
1596
|
-
if (
|
|
1597
|
-
|
|
2042
|
+
// Deleting an unsaved entry row is just discarding the entry, whether it
|
|
2043
|
+
// is still open or already committed into the draft store.
|
|
2044
|
+
if (
|
|
2045
|
+
forms.get(rowId)?.isNew === true ||
|
|
2046
|
+
committed.get(rowId)?.isNew === true
|
|
2047
|
+
) {
|
|
2048
|
+
forget(rowId);
|
|
1598
2049
|
return;
|
|
1599
2050
|
}
|
|
1600
2051
|
const context = getContext();
|
|
1601
2052
|
if (context.draft) {
|
|
1602
|
-
//
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
2053
|
+
// Idempotent: a marked row stays marked - `restoreRow` is the undo.
|
|
2054
|
+
if (working.deletedRowIds.has(rowId)) return;
|
|
2055
|
+
// Only a consumer row can be marked. A deletion mark is what
|
|
2056
|
+
// `saveDrafts` reports to the server, so an id it cannot act on - an
|
|
2057
|
+
// engine temp id, a record gone from `data`, an id the grid never
|
|
2058
|
+
// knew - must not live on as a mark inflating the draft count. Entry
|
|
2059
|
+
// rows are dropped above, never marked; the prefix check also catches
|
|
2060
|
+
// one already dropped that a stale selection or a double-fired
|
|
2061
|
+
// handler names again, while the table's data still shows it for one
|
|
2062
|
+
// render. The core model, so a filtered-out row still takes its mark.
|
|
2063
|
+
if (
|
|
2064
|
+
rowId.startsWith(NEW_ROW_ID_PREFIX) ||
|
|
2065
|
+
!(rowId in context.table.getCoreRowModel().rowsById)
|
|
2066
|
+
) {
|
|
2067
|
+
return;
|
|
2068
|
+
}
|
|
2069
|
+
heldSync(() => {
|
|
2070
|
+
// An editor open on the row loses: what was being typed into a row
|
|
2071
|
+
// the user then deleted is not worth keeping, and a marked row has
|
|
2072
|
+
// no editor. A committed edit stays under the mark, so Restore
|
|
2073
|
+
// brings the row back as it was edited.
|
|
2074
|
+
if (forms.has(rowId)) forget(rowId);
|
|
2075
|
+
working.deletedRowIds.add(rowId);
|
|
2076
|
+
dirty.add("deletedRowIds");
|
|
2077
|
+
// Not selectable while marked - see `enableRowSelection` in the
|
|
2078
|
+
// hook - so the selection lets it go now, the way a row leaving the
|
|
2079
|
+
// table would. See flushRowState.
|
|
2080
|
+
pendingUnselect.add(rowId);
|
|
2081
|
+
});
|
|
1609
2082
|
return;
|
|
1610
2083
|
}
|
|
1611
2084
|
const row = getRow(rowId);
|
|
@@ -1614,6 +2087,19 @@ export function createEditEngine(
|
|
|
1614
2087
|
void context.onRowDelete?.({ rowId, row });
|
|
1615
2088
|
};
|
|
1616
2089
|
|
|
2090
|
+
const deleteRows = (rowIds: ReadonlyArray<string>) => {
|
|
2091
|
+
// One publish for the batch - each id still goes through `deleteRow`,
|
|
2092
|
+
// so entry rows drop and everything else marks or no-ops by the same
|
|
2093
|
+
// rules.
|
|
2094
|
+
heldSync(() => {
|
|
2095
|
+
for (const rowId of rowIds) deleteRow(rowId);
|
|
2096
|
+
});
|
|
2097
|
+
};
|
|
2098
|
+
|
|
2099
|
+
const restoreRow = (rowId: string) => {
|
|
2100
|
+
if (working.deletedRowIds.delete(rowId)) touch("deletedRowIds");
|
|
2101
|
+
};
|
|
2102
|
+
|
|
1617
2103
|
const canDeleteRows = (): boolean => {
|
|
1618
2104
|
const context = getContext();
|
|
1619
2105
|
if (context.draft) {
|
|
@@ -1625,151 +2111,275 @@ export function createEditEngine(
|
|
|
1625
2111
|
return context.onRowDelete !== undefined;
|
|
1626
2112
|
};
|
|
1627
2113
|
|
|
1628
|
-
/** The
|
|
1629
|
-
const
|
|
1630
|
-
|
|
1631
|
-
if (deleted.length > 0) {
|
|
1632
|
-
store.setState((prev) => ({ ...prev, deletedRowIds: [] }));
|
|
1633
|
-
}
|
|
1634
|
-
return deleted;
|
|
1635
|
-
};
|
|
2114
|
+
/** The draft store's rows, in the order the rows entered the grid. */
|
|
2115
|
+
const committedIds = (): Array<string> =>
|
|
2116
|
+
[...working.openRowIds].filter((rowId) => committed.has(rowId));
|
|
1636
2117
|
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
// submit and must not be put through a second one here.
|
|
1644
|
-
const openIds = [...forms.keys()].filter(
|
|
1645
|
-
(rowId) => forms.get(rowId)?.committed !== true,
|
|
2118
|
+
const commitAll = async (): Promise<TMDataGridCommitAllResult> => {
|
|
2119
|
+
// Every form there is - a row in the draft store holds none, so this is
|
|
2120
|
+
// the open rows and nothing else.
|
|
2121
|
+
const openIds = [...forms.keys()];
|
|
2122
|
+
const results = await held(() =>
|
|
2123
|
+
Promise.all(openIds.map((rowId) => commit(rowId))),
|
|
1646
2124
|
);
|
|
1647
|
-
|
|
1648
|
-
return results.every(Boolean);
|
|
2125
|
+
return splitCommitted(openIds, results);
|
|
1649
2126
|
};
|
|
1650
2127
|
|
|
1651
2128
|
/**
|
|
1652
2129
|
* The in-flight save. A second call while `onSaveDrafts` awaits would
|
|
1653
2130
|
* re-collect the same payload and send it again - a double-clicked Save
|
|
1654
2131
|
* would create every pending entry row twice - so concurrent calls join
|
|
1655
|
-
* this promise instead of starting a save of their own.
|
|
2132
|
+
* this promise, and its result, instead of starting a save of their own.
|
|
1656
2133
|
*/
|
|
1657
|
-
let saveInFlight: Promise<
|
|
2134
|
+
let saveInFlight: Promise<TMDataGridSaveDraftsResult> | null = null;
|
|
1658
2135
|
|
|
1659
|
-
const saveDrafts = (): Promise<
|
|
2136
|
+
const saveDrafts = (): Promise<TMDataGridSaveDraftsResult> => {
|
|
1660
2137
|
if (saveInFlight !== null) return saveInFlight;
|
|
1661
|
-
// While this runs, `commit` really commits - `editing.draft` parks
|
|
1662
|
-
// otherwise. Single-threaded flag, same idiom as the collectors.
|
|
1663
|
-
savingDrafts = true;
|
|
1664
2138
|
saveInFlight = saveDraftsInner().finally(() => {
|
|
1665
|
-
savingDrafts = false;
|
|
1666
2139
|
saveInFlight = null;
|
|
2140
|
+
// Guarded: a save that found nothing to send never raised the flag,
|
|
2141
|
+
// so it publishes nothing either.
|
|
2142
|
+
if (working.isSaving) {
|
|
2143
|
+
working.isSaving = false;
|
|
2144
|
+
touch("isSaving");
|
|
2145
|
+
}
|
|
1667
2146
|
});
|
|
1668
2147
|
return saveInFlight;
|
|
1669
2148
|
};
|
|
1670
2149
|
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
2150
|
+
/** What one committed row hands `onEditCommit` and the payload's `updated`. */
|
|
2151
|
+
const commitArgsFor = (
|
|
2152
|
+
rowId: string,
|
|
2153
|
+
snapshot: Committed,
|
|
2154
|
+
): TMDataGridEditCommitArgs<TMDataGridRowData> => ({
|
|
2155
|
+
rowId,
|
|
2156
|
+
value: snapshot.values,
|
|
2157
|
+
original: snapshot.original,
|
|
2158
|
+
changes: diff(snapshot.original, snapshot.values),
|
|
2159
|
+
source: getContext().editMode,
|
|
2160
|
+
});
|
|
2161
|
+
|
|
2162
|
+
const saveDraftsInner = async (): Promise<TMDataGridSaveDraftsResult> => {
|
|
2163
|
+
const deletedIds = [...working.deletedRowIds];
|
|
2164
|
+
// A marked row's edit is not sent - the save deletes the row. The edit
|
|
2165
|
+
// stays under the mark for Restore and leaves with the row once the
|
|
2166
|
+
// deletion is saved; a rejected deletion keeps both.
|
|
2167
|
+
const ids = committedIds().filter(
|
|
2168
|
+
(rowId) => !working.deletedRowIds.has(rowId),
|
|
2169
|
+
);
|
|
2170
|
+
// Every id the save takes out of the store lands in exactly one list,
|
|
2171
|
+
// edits, new rows and deletions mixed, in the store's order.
|
|
2172
|
+
const saved: Array<string> = [];
|
|
2173
|
+
const kept: Array<string> = [];
|
|
2174
|
+
const reopened: Array<string> = [];
|
|
2175
|
+
const report = (): TMDataGridSaveDraftsResult => ({
|
|
2176
|
+
ok: kept.length === 0 && reopened.length === 0,
|
|
2177
|
+
saved,
|
|
2178
|
+
kept,
|
|
2179
|
+
reopened,
|
|
2180
|
+
});
|
|
1674
2181
|
// Nothing decided: open rows are not this verb's business, so a grid
|
|
1675
2182
|
// mid-edit with an empty draft store saves cleanly and stays as it is.
|
|
1676
|
-
if (
|
|
2183
|
+
if (ids.length === 0 && deletedIds.length === 0) return report();
|
|
2184
|
+
|
|
2185
|
+
working.isSaving = true;
|
|
2186
|
+
touch("isSaving");
|
|
2187
|
+
|
|
2188
|
+
// Only the table rules run here, and only once per row. A committed
|
|
2189
|
+
// row's values passed its column and row validators on the way in and
|
|
2190
|
+
// cannot have moved since, so re-running those could only say the same
|
|
2191
|
+
// thing. Table rules see the *other* rows, which later commits do move,
|
|
2192
|
+
// so a draft another edit has invalidated is caught here - and nowhere
|
|
2193
|
+
// else, because checking every committed row at every commit would be a
|
|
2194
|
+
// pass over the store per keystroke.
|
|
2195
|
+
//
|
|
2196
|
+
// The collection is built once for the pass: every value in it is frozen
|
|
2197
|
+
// while the pass runs, so N rows share one view instead of building one
|
|
2198
|
+
// each.
|
|
2199
|
+
const rows = mergedRows();
|
|
1677
2200
|
|
|
1678
2201
|
if (getContext().onSaveDrafts === undefined) {
|
|
1679
2202
|
// The default: the per-row loop - edits through `onEditCommit`, entry
|
|
1680
|
-
// rows through `onRowAdd`, marked deletions through `onRowDelete`.
|
|
1681
|
-
|
|
1682
|
-
|
|
2203
|
+
// rows through `onRowAdd`, marked deletions through `onRowDelete`. The
|
|
2204
|
+
// rows go out side by side, as one `onSaveDrafts` call would.
|
|
2205
|
+
const results = await held(() =>
|
|
2206
|
+
Promise.all(
|
|
2207
|
+
ids.map(async (rowId): Promise<"saved" | "reopened" | null> => {
|
|
2208
|
+
const snapshot = committed.get(rowId);
|
|
2209
|
+
if (snapshot === undefined) return null;
|
|
2210
|
+
const invalid = await runTableValidators(
|
|
2211
|
+
snapshot.values,
|
|
2212
|
+
rowId,
|
|
2213
|
+
snapshot.isNew,
|
|
2214
|
+
rows,
|
|
2215
|
+
);
|
|
2216
|
+
if (isValidationError(invalid)) {
|
|
2217
|
+
demote(rowId, invalid);
|
|
2218
|
+
return "reopened";
|
|
2219
|
+
}
|
|
2220
|
+
try {
|
|
2221
|
+
if (snapshot.isNew) {
|
|
2222
|
+
await getContext().onRowAdd?.({
|
|
2223
|
+
tempId: rowId,
|
|
2224
|
+
value: snapshot.values,
|
|
2225
|
+
});
|
|
2226
|
+
} else {
|
|
2227
|
+
await getContext().onEditCommit?.(
|
|
2228
|
+
commitArgsFor(rowId, snapshot),
|
|
2229
|
+
);
|
|
2230
|
+
}
|
|
2231
|
+
} catch (error) {
|
|
2232
|
+
// The same answer a rejected commit gets outside the draft
|
|
2233
|
+
// store: the row is open again, carrying the message.
|
|
2234
|
+
demote(
|
|
2235
|
+
rowId,
|
|
2236
|
+
error instanceof Error ? error.message : String(error),
|
|
2237
|
+
);
|
|
2238
|
+
return "reopened";
|
|
2239
|
+
}
|
|
2240
|
+
forget(rowId);
|
|
2241
|
+
return "saved";
|
|
2242
|
+
}),
|
|
2243
|
+
),
|
|
1683
2244
|
);
|
|
1684
|
-
|
|
2245
|
+
ids.forEach((rowId, index) => {
|
|
2246
|
+
const outcome = results[index];
|
|
2247
|
+
if (outcome === "saved") saved.push(rowId);
|
|
2248
|
+
else if (outcome === "reopened") reopened.push(rowId);
|
|
2249
|
+
});
|
|
2250
|
+
// A mark leaves once its `onRowDelete` resolved, and the row's
|
|
2251
|
+
// selection and the edit held under the mark go with it in one publish -
|
|
2252
|
+
// see flushRowState and saveDraftsInner's `ids`. A throw keeps the mark
|
|
2253
|
+
// for the next save, as a deletion `onSaveDrafts` refuses is kept.
|
|
2254
|
+
const deletedOk: Array<string> = [];
|
|
2255
|
+
for (const rowId of deletedIds) {
|
|
1685
2256
|
const row = getRow(rowId);
|
|
1686
|
-
|
|
1687
|
-
|
|
2257
|
+
try {
|
|
2258
|
+
if (row !== undefined) {
|
|
2259
|
+
await getContext().onRowDelete?.({ rowId, row });
|
|
2260
|
+
}
|
|
2261
|
+
deletedOk.push(rowId);
|
|
2262
|
+
} catch {
|
|
2263
|
+
kept.push(rowId);
|
|
1688
2264
|
}
|
|
1689
2265
|
}
|
|
1690
|
-
|
|
2266
|
+
heldSync(() => {
|
|
2267
|
+
let deletionsChanged = false;
|
|
2268
|
+
for (const rowId of deletedOk) {
|
|
2269
|
+
if (working.deletedRowIds.delete(rowId)) deletionsChanged = true;
|
|
2270
|
+
pendingLeft.add(rowId);
|
|
2271
|
+
if (committed.has(rowId)) forget(rowId);
|
|
2272
|
+
}
|
|
2273
|
+
if (deletionsChanged) dirty.add("deletedRowIds");
|
|
2274
|
+
});
|
|
2275
|
+
saved.push(...deletedOk);
|
|
2276
|
+
return report();
|
|
1691
2277
|
}
|
|
1692
2278
|
|
|
1693
|
-
// One consumer call for the lot.
|
|
1694
|
-
//
|
|
1695
|
-
//
|
|
1696
|
-
//
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
2279
|
+
// One consumer call for the lot. The valid rows travel together, and
|
|
2280
|
+
// only a resolved save drops them - a rejected save keeps every draft,
|
|
2281
|
+
// deletions included. A row the table rules now reject is reopened with
|
|
2282
|
+
// the error and left behind for the user to fix.
|
|
2283
|
+
type CommitArgs = TMDataGridEditCommitArgs<TMDataGridRowData>;
|
|
2284
|
+
type AddArgs = TMDataGridRowAddArgs<TMDataGridRowData>;
|
|
2285
|
+
const collected: Array<CommitArgs> = [];
|
|
2286
|
+
const added: Array<AddArgs> = [];
|
|
2287
|
+
// The rows sent, edits and new rows mixed in the store's order - the
|
|
2288
|
+
// order the result reports them in.
|
|
2289
|
+
const sent: Array<{ id: string; isNew: boolean }> = [];
|
|
2290
|
+
await held(async () => {
|
|
2291
|
+
const results = await Promise.all(
|
|
2292
|
+
ids.map((rowId) => {
|
|
2293
|
+
const snapshot = committed.get(rowId);
|
|
2294
|
+
if (snapshot === undefined) return undefined;
|
|
2295
|
+
return runTableValidators(
|
|
2296
|
+
snapshot.values,
|
|
2297
|
+
rowId,
|
|
2298
|
+
snapshot.isNew,
|
|
2299
|
+
rows,
|
|
2300
|
+
);
|
|
2301
|
+
}),
|
|
2302
|
+
);
|
|
2303
|
+
// In the store's order, which is the order the rows entered the grid.
|
|
2304
|
+
ids.forEach((rowId, index) => {
|
|
2305
|
+
const snapshot = committed.get(rowId);
|
|
2306
|
+
if (snapshot === undefined) return;
|
|
2307
|
+
if (isValidationError(results[index])) {
|
|
2308
|
+
demote(rowId, results[index]);
|
|
2309
|
+
reopened.push(rowId);
|
|
2310
|
+
return;
|
|
2311
|
+
}
|
|
2312
|
+
sent.push({ id: rowId, isNew: snapshot.isNew });
|
|
2313
|
+
if (snapshot.isNew) {
|
|
2314
|
+
added.push({ tempId: rowId, value: snapshot.values });
|
|
2315
|
+
} else {
|
|
2316
|
+
collected.push(commitArgsFor(rowId, snapshot));
|
|
2317
|
+
}
|
|
2318
|
+
});
|
|
2319
|
+
});
|
|
1714
2320
|
const deleted = deletedIds;
|
|
1715
2321
|
if (collected.length > 0 || added.length > 0 || deleted.length > 0) {
|
|
1716
|
-
let
|
|
2322
|
+
let response: void | TMDataGridSaveDraftsResponse;
|
|
1717
2323
|
try {
|
|
1718
|
-
|
|
2324
|
+
response = await getContext().onSaveDrafts?.({
|
|
1719
2325
|
updated: collected,
|
|
1720
2326
|
created: added,
|
|
1721
2327
|
deleted,
|
|
1722
|
-
// The pre-2.0 names, still filled - see TMDataGridSaveDraftsArgs.
|
|
1723
|
-
rows: collected,
|
|
1724
|
-
added,
|
|
1725
2328
|
});
|
|
1726
2329
|
} catch {
|
|
1727
|
-
|
|
2330
|
+
// A thrown save keeps everything it was sent, for the next save.
|
|
2331
|
+
kept.push(...sent.map((row) => row.id), ...deleted);
|
|
2332
|
+
return report();
|
|
1728
2333
|
}
|
|
1729
2334
|
|
|
1730
|
-
// Nothing returned saves the lot. A
|
|
2335
|
+
// Nothing returned saves the lot. A response names what failed; those
|
|
1731
2336
|
// keep their drafts, committed, so the next save retries them.
|
|
1732
|
-
const outcomes =
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
)
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
2337
|
+
const outcomes = response ?? {};
|
|
2338
|
+
|
|
2339
|
+
// The saved rows leave the store in one publish.
|
|
2340
|
+
heldSync(() => {
|
|
2341
|
+
for (const { id, isNew } of sent) {
|
|
2342
|
+
if (isSaved(isNew ? outcomes.created : outcomes.updated, id)) {
|
|
2343
|
+
forget(id);
|
|
2344
|
+
saved.push(id);
|
|
2345
|
+
} else {
|
|
2346
|
+
kept.push(id);
|
|
2347
|
+
}
|
|
2348
|
+
}
|
|
2349
|
+
let deletionsChanged = false;
|
|
2350
|
+
for (const id of deleted) {
|
|
2351
|
+
if (!isSaved(outcomes.deleted, id)) {
|
|
2352
|
+
kept.push(id);
|
|
2353
|
+
continue;
|
|
2354
|
+
}
|
|
2355
|
+
saved.push(id);
|
|
2356
|
+
if (working.deletedRowIds.delete(id)) deletionsChanged = true;
|
|
2357
|
+
// The consumer has deleted the record; the row is on its way out
|
|
2358
|
+
// of `data`, and its selection goes now - see flushRowState. So
|
|
2359
|
+
// does the edit held under the mark.
|
|
2360
|
+
pendingLeft.add(id);
|
|
2361
|
+
if (committed.has(id)) forget(id);
|
|
2362
|
+
}
|
|
2363
|
+
if (deletionsChanged) dirty.add("deletedRowIds");
|
|
2364
|
+
});
|
|
1757
2365
|
}
|
|
1758
|
-
return
|
|
1759
|
-
};
|
|
1760
|
-
|
|
1761
|
-
/** @deprecated The old one-shot save - `commitAll` then `saveDrafts`. */
|
|
1762
|
-
const submitAll = async (): Promise<boolean> => {
|
|
1763
|
-
const committedOk = await commitAll();
|
|
1764
|
-
const savedOk = await saveDrafts();
|
|
1765
|
-
return committedOk && savedOk;
|
|
2366
|
+
return report();
|
|
1766
2367
|
};
|
|
1767
2368
|
|
|
1768
2369
|
/**
|
|
1769
2370
|
* The write path behind `clearCell`, `setCellValue` and `setRowValues`:
|
|
1770
2371
|
* fill the row's own form and commit it, exactly as a ✓ on an open editor
|
|
1771
2372
|
* would. Uses the form already open on the row when there is one, so a
|
|
1772
|
-
* programmatic write joins an edit in progress rather than discarding it
|
|
2373
|
+
* programmatic write joins an edit in progress rather than discarding it;
|
|
2374
|
+
* a committed row is reopened from its snapshot and commits afresh.
|
|
2375
|
+
*
|
|
2376
|
+
* Only that last case runs under a hold. Reopened, the row is out of the
|
|
2377
|
+
* draft store until it commits again, and its markers must not flicker
|
|
2378
|
+
* off for the length of the validators. The hold is safe there because a
|
|
2379
|
+
* committed row exists only under `editing.draft`, where the commit parks
|
|
2380
|
+
* and never waits on the consumer. An open row's commit without `draft`
|
|
2381
|
+
* awaits `onEditCommit`, and holding the engine's publish for a server
|
|
2382
|
+
* round trip would stall every other row's markers and the caret.
|
|
1773
2383
|
*
|
|
1774
2384
|
* No editor is involved, so `meta.edit.mapValue` does not run - the caller
|
|
1775
2385
|
* writes the stored value itself. Validation is untouched: this is the same
|
|
@@ -1783,12 +2393,24 @@ export function createEditEngine(
|
|
|
1783
2393
|
): Promise<boolean> => {
|
|
1784
2394
|
const row = getRow(rowId);
|
|
1785
2395
|
if (row === undefined) return false;
|
|
2396
|
+
// Read-only while marked - see canEditRow.
|
|
2397
|
+
if (working.deletedRowIds.has(rowId)) return false;
|
|
1786
2398
|
if (writes.length === 0) return true;
|
|
1787
|
-
const
|
|
1788
|
-
|
|
1789
|
-
|
|
2399
|
+
const write = (entry: FormEntry): Promise<boolean> => {
|
|
2400
|
+
for (const { field, value } of writes) {
|
|
2401
|
+
entry.form.setFieldValue(field as never, value as never);
|
|
2402
|
+
}
|
|
2403
|
+
return commit(rowId);
|
|
2404
|
+
};
|
|
2405
|
+
const open = forms.get(rowId);
|
|
2406
|
+
if (open !== undefined) return write(open);
|
|
2407
|
+
if (!committed.has(rowId)) {
|
|
2408
|
+
return write(createForm(rowId, row.original, false));
|
|
1790
2409
|
}
|
|
1791
|
-
return
|
|
2410
|
+
return held(async () => {
|
|
2411
|
+
const entry = reopen(rowId);
|
|
2412
|
+
return entry === undefined ? false : write(entry);
|
|
2413
|
+
});
|
|
1792
2414
|
};
|
|
1793
2415
|
|
|
1794
2416
|
/** The field a cell writes to, or `null` when that cell takes no edit. */
|
|
@@ -1840,49 +2462,73 @@ export function createEditEngine(
|
|
|
1840
2462
|
return writeFields(rowId, writes);
|
|
1841
2463
|
};
|
|
1842
2464
|
|
|
1843
|
-
const getRowValues = (rowId: string): TMDataGridRowData | undefined =>
|
|
1844
|
-
|
|
1845
|
-
//
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
};
|
|
2465
|
+
const getRowValues = (rowId: string): TMDataGridRowData | undefined =>
|
|
2466
|
+
// An entry row exists only as a draft, open or committed, so this covers
|
|
2467
|
+
// it too.
|
|
2468
|
+
draftValues(rowId) ??
|
|
2469
|
+
(getRow(rowId)?.original as TMDataGridRowData | undefined);
|
|
1849
2470
|
|
|
1850
2471
|
const getRows = (): ReadonlyArray<TMDataGridEditRowSnapshot> => {
|
|
1851
|
-
const deleted =
|
|
2472
|
+
const deleted = working.deletedRowIds;
|
|
1852
2473
|
const rows: Array<TMDataGridEditRowSnapshot> = [];
|
|
1853
2474
|
const model = getContext().table.getCoreRowModel();
|
|
1854
2475
|
for (const row of model.flatRows) {
|
|
1855
|
-
const held = forms.get(row.id);
|
|
1856
2476
|
rows.push({
|
|
1857
2477
|
rowId: row.id,
|
|
1858
|
-
value:
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
: (held.form.state.values as TMDataGridRowData),
|
|
1862
|
-
isNew: held?.isNew === true,
|
|
2478
|
+
value: draftValues(row.id) ?? (row.original as TMDataGridRowData),
|
|
2479
|
+
isNew:
|
|
2480
|
+
forms.get(row.id)?.isNew ?? committed.get(row.id)?.isNew ?? false,
|
|
1863
2481
|
deleted: deleted.has(row.id),
|
|
1864
2482
|
});
|
|
1865
2483
|
}
|
|
1866
2484
|
// Entry rows the table does not hold - see mergedRows.
|
|
1867
|
-
for (const
|
|
1868
|
-
if (
|
|
1869
|
-
const
|
|
1870
|
-
if (
|
|
2485
|
+
for (const tempId of working.newRows.keys()) {
|
|
2486
|
+
if (tempId in model.rowsById) continue;
|
|
2487
|
+
const values = draftValues(tempId);
|
|
2488
|
+
if (values === undefined) continue;
|
|
1871
2489
|
rows.push({
|
|
1872
|
-
rowId:
|
|
1873
|
-
value:
|
|
2490
|
+
rowId: tempId,
|
|
2491
|
+
value: values,
|
|
1874
2492
|
isNew: true,
|
|
1875
|
-
deleted: deleted.has(
|
|
2493
|
+
deleted: deleted.has(tempId),
|
|
1876
2494
|
});
|
|
1877
2495
|
}
|
|
1878
2496
|
return rows;
|
|
1879
2497
|
};
|
|
1880
2498
|
|
|
2499
|
+
/**
|
|
2500
|
+
* Drops what the store holds for records that left `data`: a refetch that
|
|
2501
|
+
* no longer returns a row takes its open form, its committed edit and its
|
|
2502
|
+
* deletion mark with it. The server has nothing for the save to update or
|
|
2503
|
+
* delete, and a row nobody can see must not sit in the Save count. Entry
|
|
2504
|
+
* rows are the engine's own and stay. Called by the hook whenever the rows
|
|
2505
|
+
* as shown change, with the core row model's `rowsById`; not called where
|
|
2506
|
+
* the grid does not own the result set, since there a row missing from
|
|
2507
|
+
* `data` is on another page, not gone.
|
|
2508
|
+
*/
|
|
2509
|
+
const forgetMissingRows = (rowsById: Record<string, unknown>) => {
|
|
2510
|
+
heldSync(() => {
|
|
2511
|
+
// Open rows and committed ones alike - `openRowIds` is both.
|
|
2512
|
+
for (const rowId of [...working.openRowIds]) {
|
|
2513
|
+
const isNew =
|
|
2514
|
+
forms.get(rowId)?.isNew ?? committed.get(rowId)?.isNew ?? false;
|
|
2515
|
+
if (!isNew && !(rowId in rowsById)) forget(rowId);
|
|
2516
|
+
}
|
|
2517
|
+
for (const rowId of [...working.deletedRowIds]) {
|
|
2518
|
+
if (rowId in rowsById) continue;
|
|
2519
|
+
working.deletedRowIds.delete(rowId);
|
|
2520
|
+
dirty.add("deletedRowIds");
|
|
2521
|
+
}
|
|
2522
|
+
});
|
|
2523
|
+
};
|
|
2524
|
+
|
|
1881
2525
|
return {
|
|
1882
2526
|
store,
|
|
1883
2527
|
get state() {
|
|
1884
2528
|
return store.state;
|
|
1885
2529
|
},
|
|
2530
|
+
forgetMissingRows,
|
|
2531
|
+
isRowDeleted: (rowId) => working.deletedRowIds.has(rowId),
|
|
1886
2532
|
getForm: (rowId) => forms.get(rowId)?.form,
|
|
1887
2533
|
getRowValues,
|
|
1888
2534
|
getRows,
|
|
@@ -1896,13 +2542,14 @@ export function createEditEngine(
|
|
|
1896
2542
|
cancelAll,
|
|
1897
2543
|
commitAll,
|
|
1898
2544
|
saveDrafts,
|
|
1899
|
-
submitAll,
|
|
1900
2545
|
clearCell,
|
|
1901
2546
|
setCellValue,
|
|
1902
2547
|
setRowValues,
|
|
1903
2548
|
addRow,
|
|
1904
2549
|
addRows,
|
|
1905
2550
|
deleteRow,
|
|
2551
|
+
deleteRows,
|
|
2552
|
+
restoreRow,
|
|
1906
2553
|
canDeleteRows,
|
|
1907
2554
|
};
|
|
1908
2555
|
}
|