@jielga/tmdatagrid 2.0.0-beta.8 → 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 +1323 -796
- package/dist/index.js +4719 -3193
- 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 +69 -78
- package/skills/columns/SKILL.md +90 -34
- package/skills/data/SKILL.md +86 -16
- package/skills/editing/SKILL.md +83 -50
- package/skills/editing/references/common-mistakes.md +77 -69
- package/skills/editing/references/editing-api.md +31 -23
- 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 +8 -8
- 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/TMDataGridContext.ts → TMDataGridContext.ts} +7 -19
- package/src/{tmdatagrid/components → components}/TMDataGrid.module.css +7 -1
- package/src/{tmdatagrid/components → components}/TMDataGrid.tsx +38 -21
- package/src/{tmdatagrid/components → components}/TMDataGridCellEditor.tsx +73 -7
- 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 +9 -55
- package/src/{tmdatagrid/components → components}/TMDataGridDraftActions.tsx +41 -24
- package/src/{tmdatagrid/components → components}/TMDataGridEditColumn.tsx +23 -63
- package/src/components/TMDataGridEntryRows.tsx +354 -0
- 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 +9 -72
- 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 +15 -53
- package/src/{tmdatagrid/components → components}/TMDataGridTable.module.css +88 -65
- package/src/{tmdatagrid/components → components}/TMDataGridTable.tsx +579 -165
- 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}/capabilities.ts +5 -5
- 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 +1172 -388
- package/src/{tmdatagrid/core → core}/editorFocus.ts +8 -4
- 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} +70 -36
- package/src/{tmdatagrid/useTMDataGrid.tsx → useTMDataGrid.tsx} +534 -123
- package/src/useTMDataGridExport.ts +78 -0
- package/src/tmdatagrid/components/TMDataGridEntryRows.tsx +0 -298
- 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/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}/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}/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,31 +196,52 @@ 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>;
|
|
217
|
+
/**
|
|
218
|
+
* The draft store's values, per row - what a committed row *is* to the
|
|
219
|
+
* table. Snapshotted when a row commits (existing and entry rows alike)
|
|
220
|
+
* and kept across a reopen until the row commits again or is dropped, so
|
|
221
|
+
* a row keeps its place in the sort while a second cell is being typed
|
|
222
|
+
* into. The hook feeds these into the table's `data` in place of the
|
|
223
|
+
* consumer's records, which is how sorting, filtering, grouping and
|
|
224
|
+
* aggregates see a draft.
|
|
225
|
+
*/
|
|
226
|
+
committedValues: Readonly<Record<string, TMDataGridRowData>>;
|
|
213
227
|
/**
|
|
214
228
|
* Rows being created, not yet in `data`. `committed` is the draft store's
|
|
215
229
|
* add slice: the entry row passed its submit and renders as a value row
|
|
216
|
-
* until `begin` re-opens it. Without
|
|
217
|
-
* `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`.
|
|
218
233
|
*/
|
|
219
234
|
newRows: ReadonlyArray<{ tempId: string; committed: boolean }>;
|
|
220
235
|
/** The draft store's delete slice: rows marked deleted, awaiting the save. */
|
|
221
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;
|
|
222
245
|
};
|
|
223
246
|
|
|
224
247
|
const EMPTY_EDIT_STATE: TMDataGridEditState = {
|
|
@@ -226,31 +249,57 @@ const EMPTY_EDIT_STATE: TMDataGridEditState = {
|
|
|
226
249
|
openRowIds: [],
|
|
227
250
|
rows: {},
|
|
228
251
|
committedRowIds: [],
|
|
252
|
+
committedValues: {},
|
|
229
253
|
newRows: [],
|
|
230
254
|
deletedRowIds: [],
|
|
255
|
+
isSaving: false,
|
|
231
256
|
};
|
|
232
257
|
|
|
233
258
|
/**
|
|
234
259
|
* The rows still *open*: holding a live form nobody has decided yet.
|
|
235
260
|
*
|
|
236
261
|
* Narrower than {@link TMDataGridEditState.openRowIds}, which is every row
|
|
237
|
-
*
|
|
238
|
-
*
|
|
239
|
-
* 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.
|
|
240
265
|
*
|
|
241
266
|
* The order is the engine's: the order the forms were opened.
|
|
242
267
|
*/
|
|
243
268
|
export function getOpenRowIds(
|
|
244
269
|
state: TMDataGridEditState,
|
|
245
270
|
): ReadonlyArray<string> {
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
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
|
+
)
|
|
254
303
|
);
|
|
255
304
|
}
|
|
256
305
|
|
|
@@ -337,15 +386,16 @@ export type TMDataGridEditValueMap = (
|
|
|
337
386
|
* `meta.type` and `meta.options` stay outside this namespace on purpose: one
|
|
338
387
|
* declaration of each feeds the cell editor and the filter panel alike.
|
|
339
388
|
*/
|
|
340
|
-
export type TMDataGridColumnEditOptions
|
|
389
|
+
export type TMDataGridColumnEditOptions<
|
|
390
|
+
TData extends RowData = TMDataGridRowData,
|
|
391
|
+
> = {
|
|
341
392
|
/**
|
|
342
393
|
* Whether this column's cells take edits, once `editMode` is on. `false`
|
|
343
394
|
* switches the column off outright; a predicate decides per row. Defaults
|
|
344
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.
|
|
345
397
|
*/
|
|
346
|
-
enabled?:
|
|
347
|
-
| boolean
|
|
348
|
-
| ((row: Row<TMDataGridFeatures, TMDataGridRowData>) => boolean);
|
|
398
|
+
enabled?: boolean | ((row: Row<TMDataGridFeatures, TData>) => boolean);
|
|
349
399
|
/**
|
|
350
400
|
* The data path this column edits, when it is not the `accessorKey` - the
|
|
351
401
|
* only way a column built on `accessorFn` becomes editable. Dot paths reach
|
|
@@ -396,10 +446,6 @@ export type TMDataGridSaveDraftsArgs<TData extends RowData> = {
|
|
|
396
446
|
created: Array<TMDataGridRowAddArgs<TData>>;
|
|
397
447
|
/** Ids marked deleted while the drafts accumulated. */
|
|
398
448
|
deleted: Array<string>;
|
|
399
|
-
/** @deprecated Renamed to {@link updated}. Removed in a later beta. */
|
|
400
|
-
rows: Array<TMDataGridEditCommitArgs<TData>>;
|
|
401
|
-
/** @deprecated Renamed to {@link created}. Removed in a later beta. */
|
|
402
|
-
added: Array<TMDataGridRowAddArgs<TData>>;
|
|
403
449
|
};
|
|
404
450
|
|
|
405
451
|
/**
|
|
@@ -418,7 +464,7 @@ export type TMDataGridSaveOutcomes = boolean | Record<string, boolean>;
|
|
|
418
464
|
* with nothing beyond the state itself - a failed edit keeps `data-draft`,
|
|
419
465
|
* a failed deletion keeps `data-deleted` - so the display is the consumer's.
|
|
420
466
|
*/
|
|
421
|
-
export type
|
|
467
|
+
export type TMDataGridSaveDraftsResponse = {
|
|
422
468
|
/** Keyed by `rowId`. */
|
|
423
469
|
updated?: TMDataGridSaveOutcomes;
|
|
424
470
|
/** Keyed by `tempId`. */
|
|
@@ -437,13 +483,6 @@ function isSaved(
|
|
|
437
483
|
return outcomes[id] !== false;
|
|
438
484
|
}
|
|
439
485
|
|
|
440
|
-
/**
|
|
441
|
-
* @deprecated Renamed to {@link TMDataGridSaveDraftsArgs} - the payload is
|
|
442
|
-
* the draft store being saved, not a commit. Removed in a later beta.
|
|
443
|
-
*/
|
|
444
|
-
export type TMDataGridEditCommitDraftsArgs<TData extends RowData> =
|
|
445
|
-
TMDataGridSaveDraftsArgs<TData>;
|
|
446
|
-
|
|
447
486
|
/** What the engine reads fresh on every call - see `createEditEngine`. */
|
|
448
487
|
export type TMDataGridEditEngineContext = {
|
|
449
488
|
table: TMDataGridTable<TMDataGridRowData>;
|
|
@@ -465,8 +504,8 @@ export type TMDataGridEditEngineContext = {
|
|
|
465
504
|
args: TMDataGridSaveDraftsArgs<TMDataGridRowData>,
|
|
466
505
|
) =>
|
|
467
506
|
| void
|
|
468
|
-
|
|
|
469
|
-
| Promise<void |
|
|
507
|
+
| TMDataGridSaveDraftsResponse
|
|
508
|
+
| Promise<void | TMDataGridSaveDraftsResponse>;
|
|
470
509
|
/**
|
|
471
510
|
* Seed values for `addRow`, under the values it is called with. A function
|
|
472
511
|
* is called per added row.
|
|
@@ -526,6 +565,8 @@ export type TMDataGridAddRowsOptions = {
|
|
|
526
565
|
|
|
527
566
|
/** What `edit.addRows` reports back. Every added row is in exactly one list. */
|
|
528
567
|
export type TMDataGridAddRowsResult = {
|
|
568
|
+
/** `true` when every added row committed - `open` is empty. */
|
|
569
|
+
ok: boolean;
|
|
529
570
|
/** Temp ids that committed - parked as drafts, or added outright. */
|
|
530
571
|
committed: Array<string>;
|
|
531
572
|
/**
|
|
@@ -535,12 +576,90 @@ export type TMDataGridAddRowsResult = {
|
|
|
535
576
|
open: Array<string>;
|
|
536
577
|
};
|
|
537
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
|
+
|
|
641
|
+
/** One row of {@link TMDataGridEditApi.getRows}. */
|
|
642
|
+
export type TMDataGridEditRowSnapshot<
|
|
643
|
+
TData extends RowData = TMDataGridRowData,
|
|
644
|
+
> = {
|
|
645
|
+
/** The row's id - `addRow`'s temp id for an entry row. */
|
|
646
|
+
rowId: string;
|
|
647
|
+
/** The row as shown: its draft where a form holds one, else `data`'s value. */
|
|
648
|
+
value: TData;
|
|
649
|
+
/** An entry row, not yet in `data`. */
|
|
650
|
+
isNew: boolean;
|
|
651
|
+
/** Marked deleted, awaiting `saveDrafts`. */
|
|
652
|
+
deleted: boolean;
|
|
653
|
+
};
|
|
654
|
+
|
|
538
655
|
/**
|
|
539
656
|
* The engine plus its store - `api.edit`.
|
|
540
657
|
*
|
|
541
|
-
* "One row, one form": `getForm` hands out the same
|
|
542
|
-
* editors write through, so a consumer can render it in
|
|
543
|
-
* 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.
|
|
544
663
|
*/
|
|
545
664
|
export type TMDataGridEditApi<
|
|
546
665
|
TData extends RowData = TMDataGridRowData,
|
|
@@ -549,8 +668,26 @@ export type TMDataGridEditApi<
|
|
|
549
668
|
store: Store<TMDataGridEditState>;
|
|
550
669
|
/** Current snapshot, for reads outside React. */
|
|
551
670
|
readonly state: TMDataGridEditState;
|
|
552
|
-
/**
|
|
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
|
+
*/
|
|
553
676
|
getForm: (rowId: string) => TMDataGridRowEditForm | undefined;
|
|
677
|
+
/**
|
|
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.
|
|
682
|
+
*/
|
|
683
|
+
getRowValues: (rowId: string) => TData | undefined;
|
|
684
|
+
/**
|
|
685
|
+
* Every row as shown, nothing filtered out: data rows overlaid with their
|
|
686
|
+
* drafts, entry rows appended, deletion-marked rows included and flagged.
|
|
687
|
+
* Built from the core row model, so it is unfiltered, unsorted and never
|
|
688
|
+
* contains group rows. Filter on `deleted` / `isNew` for the set you want.
|
|
689
|
+
*/
|
|
690
|
+
getRows: () => ReadonlyArray<TMDataGridEditRowSnapshot<TData>>;
|
|
554
691
|
/**
|
|
555
692
|
* Whether this cell may open an editor: the column maps to a field, nothing
|
|
556
693
|
* switched it off, and the row takes edits at all.
|
|
@@ -590,24 +727,33 @@ export type TMDataGridEditApi<
|
|
|
590
727
|
* Submits every open row, as if each had been OK'd: a row that validates
|
|
591
728
|
* commits (into the draft store with `editing.draft` on, straight to the
|
|
592
729
|
* consumer without it), a row that fails stays open with its errors.
|
|
593
|
-
*
|
|
594
|
-
*
|
|
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}.
|
|
595
736
|
*/
|
|
596
|
-
commitAll: () => Promise<
|
|
737
|
+
commitAll: () => Promise<TMDataGridCommitAllResult>;
|
|
597
738
|
/**
|
|
598
739
|
* Flushes the draft store: every committed edit, added row and deletion
|
|
599
740
|
* mark reaches the consumer, through `onSaveDrafts` in one call when it is
|
|
600
741
|
* set, or row by row through `onCommit` / `onRowAdd` / `onRowDelete`.
|
|
601
742
|
*
|
|
602
743
|
* Rows still open are left alone - they keep their form state and stay
|
|
603
|
-
* open
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
*
|
|
608
|
-
*
|
|
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}.
|
|
609
755
|
*/
|
|
610
|
-
|
|
756
|
+
saveDrafts: () => Promise<TMDataGridSaveDraftsResult>;
|
|
611
757
|
/** Writes the type's empty value into a cell and commits it - Delete. */
|
|
612
758
|
clearCell: (rowId: string, columnId: string) => Promise<boolean>;
|
|
613
759
|
/**
|
|
@@ -652,15 +798,19 @@ export type TMDataGridEditApi<
|
|
|
652
798
|
*/
|
|
653
799
|
addRow: (values?: Partial<TData>) => string;
|
|
654
800
|
/**
|
|
655
|
-
* 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
|
|
656
802
|
* batch, where a loop over `addRow` is one per row. Each row is seeded over
|
|
657
803
|
* `newRowDefaults` exactly as `addRow` does.
|
|
658
804
|
*
|
|
659
|
-
* `commit: true` submits
|
|
660
|
-
*
|
|
661
|
-
*
|
|
662
|
-
*
|
|
663
|
-
*
|
|
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.
|
|
664
814
|
*/
|
|
665
815
|
addRows: (
|
|
666
816
|
rows: ReadonlyArray<Partial<TData>>,
|
|
@@ -668,11 +818,26 @@ export type TMDataGridEditApi<
|
|
|
668
818
|
) => Promise<TMDataGridAddRowsResult>;
|
|
669
819
|
/**
|
|
670
820
|
* Deletes a row: `onRowDelete` straight away, or under `editing.draft` a
|
|
671
|
-
*
|
|
672
|
-
*
|
|
673
|
-
*
|
|
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.
|
|
674
826
|
*/
|
|
675
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;
|
|
676
841
|
/** Whether delete chrome makes sense - the lane's trash gate. */
|
|
677
842
|
canDeleteRows: () => boolean;
|
|
678
843
|
};
|
|
@@ -796,22 +961,36 @@ async function runFieldValidator(
|
|
|
796
961
|
* form keeps its values, meta and errors; scroll back and the editor
|
|
797
962
|
* re-mounts over the same form.
|
|
798
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
|
+
|
|
799
979
|
export function createEditEngine(
|
|
800
980
|
getContext: () => TMDataGridEditEngineContext,
|
|
801
|
-
):
|
|
981
|
+
): TMDataGridEditEngine {
|
|
802
982
|
const store = new Store<TMDataGridEditState>(EMPTY_EDIT_STATE);
|
|
803
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
|
+
*/
|
|
804
989
|
type FormEntry = {
|
|
805
990
|
form: TMDataGridRowEditForm;
|
|
806
991
|
original: TMDataGridRowData;
|
|
807
992
|
/** An entry-block row - a form with no backing row yet. */
|
|
808
993
|
isNew: boolean;
|
|
809
|
-
/**
|
|
810
|
-
* In the draft store: this row's form passed its submit and is parked,
|
|
811
|
-
* waiting for `saveDrafts`. Mirrored into the state's `committedRowIds`
|
|
812
|
-
* (existing rows) or `newRows[].committed` (entry rows).
|
|
813
|
-
*/
|
|
814
|
-
committed: boolean;
|
|
815
994
|
/** Set by the wrapped onSubmit when the consumer's commit resolved. */
|
|
816
995
|
lastSubmitOk: boolean;
|
|
817
996
|
/**
|
|
@@ -823,31 +1002,35 @@ export function createEditEngine(
|
|
|
823
1002
|
submitErrors: Array<{ field: string; value: unknown; message: string }>;
|
|
824
1003
|
/**
|
|
825
1004
|
* The park: this submit validates and puts the row in the draft store
|
|
826
|
-
* instead of calling the consumer. Set by `commit` per attempt
|
|
827
|
-
*
|
|
828
|
-
* what closes the per-row escape hatches (the lane's ✓,
|
|
829
|
-
* Delete-to-clear) at the engine.
|
|
1005
|
+
* instead of calling the consumer. Set by `commit` per attempt, from
|
|
1006
|
+
* `editing.draft`.
|
|
830
1007
|
*/
|
|
831
1008
|
parkOnly: boolean;
|
|
832
1009
|
/** A commit already running - Enter and blur race on the same edit. */
|
|
833
1010
|
pendingCommit: Promise<boolean> | null;
|
|
834
1011
|
unsubscribe: () => void;
|
|
835
|
-
unmount: () => void;
|
|
836
1012
|
};
|
|
837
1013
|
const forms = new Map<string, FormEntry>();
|
|
838
|
-
let newRowCounter = 0;
|
|
839
|
-
/** Lets `commit` tell a `saveDrafts` flush apart from a lone commit. */
|
|
840
|
-
let savingDrafts = false;
|
|
841
1014
|
|
|
842
1015
|
/**
|
|
843
|
-
*
|
|
844
|
-
*
|
|
845
|
-
*
|
|
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.
|
|
846
1021
|
*/
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
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__";
|
|
851
1034
|
|
|
852
1035
|
/**
|
|
853
1036
|
* The column's half of the rule, with no row in hand: it is a consumer
|
|
@@ -907,45 +1090,62 @@ export function createEditEngine(
|
|
|
907
1090
|
return Object.keys(fields).length > 0 ? fields : undefined;
|
|
908
1091
|
};
|
|
909
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
|
+
|
|
910
1105
|
/**
|
|
911
1106
|
* The collection as a table validator sees it: every data row overlaid
|
|
912
|
-
* with its draft where
|
|
913
|
-
* marked deleted removed. The committing row
|
|
914
|
-
* submitted
|
|
915
|
-
*
|
|
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.
|
|
916
1112
|
*/
|
|
917
|
-
const mergedRows = (
|
|
918
|
-
rowId: string
|
|
919
|
-
value: TMDataGridRowData
|
|
920
|
-
isNew: boolean
|
|
921
|
-
): Array<{ rowId: string; value: TMDataGridRowData }> => {
|
|
922
|
-
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;
|
|
923
1119
|
const rows: Array<{ rowId: string; value: TMDataGridRowData }> = [];
|
|
924
|
-
|
|
1120
|
+
const model = getContext().table.getCoreRowModel();
|
|
1121
|
+
for (const row of model.flatRows) {
|
|
925
1122
|
if (deleted.has(row.id)) continue;
|
|
926
|
-
if (row.id === rowId) {
|
|
927
|
-
rows.push({ rowId, value });
|
|
1123
|
+
if (row.id === committing?.rowId) {
|
|
1124
|
+
rows.push({ rowId: row.id, value: committing.value });
|
|
928
1125
|
continue;
|
|
929
1126
|
}
|
|
930
|
-
const held = forms.get(row.id);
|
|
931
1127
|
rows.push({
|
|
932
1128
|
rowId: row.id,
|
|
933
|
-
value:
|
|
934
|
-
held === undefined
|
|
935
|
-
? (row.original as TMDataGridRowData)
|
|
936
|
-
: (held.form.state.values as TMDataGridRowData),
|
|
1129
|
+
value: draftValues(row.id) ?? (row.original as TMDataGridRowData),
|
|
937
1130
|
});
|
|
938
1131
|
}
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
1132
|
+
// Entry rows the table does not hold: the open ones, and the committed
|
|
1133
|
+
// ones under `newRowsSticky`. A committed row in flow is in `data`
|
|
1134
|
+
// already, so it was listed above.
|
|
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 });
|
|
947
1148
|
}
|
|
948
|
-
if (isNew) rows.push({ rowId, value });
|
|
949
1149
|
return rows;
|
|
950
1150
|
};
|
|
951
1151
|
|
|
@@ -953,11 +1153,17 @@ export function createEditEngine(
|
|
|
953
1153
|
* Runs `editing.tableValidators` for one commit. `onSubmit` first; its
|
|
954
1154
|
* failure stands and `onSubmitAsync` is not consulted, mirroring how the
|
|
955
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.
|
|
956
1161
|
*/
|
|
957
1162
|
const runTableValidators = async (
|
|
958
1163
|
value: TMDataGridRowData,
|
|
959
1164
|
rowId: string,
|
|
960
1165
|
isNew: boolean,
|
|
1166
|
+
rows?: ReadonlyArray<{ rowId: string; value: TMDataGridRowData }>,
|
|
961
1167
|
): Promise<unknown> => {
|
|
962
1168
|
const validators = getContext().tableValidators;
|
|
963
1169
|
if (
|
|
@@ -970,7 +1176,7 @@ export function createEditEngine(
|
|
|
970
1176
|
value,
|
|
971
1177
|
rowId,
|
|
972
1178
|
isNew,
|
|
973
|
-
rows: mergedRows(rowId, value, isNew),
|
|
1179
|
+
rows: rows ?? mergedRows({ rowId, value, isNew }),
|
|
974
1180
|
};
|
|
975
1181
|
const sync = validators.onSubmit?.(args);
|
|
976
1182
|
if (isValidationError(sync)) return sync;
|
|
@@ -1051,15 +1257,16 @@ export function createEditEngine(
|
|
|
1051
1257
|
};
|
|
1052
1258
|
};
|
|
1053
1259
|
|
|
1260
|
+
/** What moved between a row as it was and a row as drafted. */
|
|
1054
1261
|
const diff = (
|
|
1055
|
-
|
|
1262
|
+
original: TMDataGridRowData,
|
|
1056
1263
|
values: TMDataGridRowData,
|
|
1057
1264
|
): Array<TMDataGridEditChange> => {
|
|
1058
1265
|
const changes: Array<TMDataGridEditChange> = [];
|
|
1059
1266
|
for (const column of editableColumns()) {
|
|
1060
1267
|
const field = getEditFieldName(column);
|
|
1061
1268
|
if (field === null) continue;
|
|
1062
|
-
const previous: unknown = getBy(
|
|
1269
|
+
const previous: unknown = getBy(original, field);
|
|
1063
1270
|
const next: unknown = getBy(values, field);
|
|
1064
1271
|
if (!sameValue(previous, next)) {
|
|
1065
1272
|
changes.push({ columnId: column.id, field, previous, next });
|
|
@@ -1091,7 +1298,7 @@ export function createEditEngine(
|
|
|
1091
1298
|
...kept.map(({ field, message }) => ({ field, message })),
|
|
1092
1299
|
];
|
|
1093
1300
|
return {
|
|
1094
|
-
dirtyFields: diff(entry, values).map((change) => change.field),
|
|
1301
|
+
dirtyFields: diff(entry.original, values).map((change) => change.field),
|
|
1095
1302
|
errorFields: errorMessages.map((error) => error.field),
|
|
1096
1303
|
errorMessages,
|
|
1097
1304
|
hasRowError: hasAnyError(state.errors),
|
|
@@ -1100,90 +1307,314 @@ export function createEditEngine(
|
|
|
1100
1307
|
};
|
|
1101
1308
|
};
|
|
1102
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
|
+
|
|
1103
1496
|
const publishRow = (rowId: string) => {
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
const rows = { ...prev.rows };
|
|
1107
|
-
if (entry === undefined) delete rows[rowId];
|
|
1108
|
-
else rows[rowId] = project(entry);
|
|
1109
|
-
return { ...prev, rows, openRowIds: [...forms.keys()] };
|
|
1110
|
-
});
|
|
1497
|
+
staleRows.add(rowId);
|
|
1498
|
+
publish();
|
|
1111
1499
|
};
|
|
1112
1500
|
|
|
1113
1501
|
const setActive = (active: TMDataGridEditState["active"]) => {
|
|
1114
|
-
|
|
1502
|
+
if (working.active === active) return;
|
|
1503
|
+
working.active = active;
|
|
1504
|
+
touch("active");
|
|
1115
1505
|
};
|
|
1116
1506
|
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1507
|
+
/**
|
|
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.
|
|
1514
|
+
*/
|
|
1515
|
+
const snapshotCommitted = (rowId: string) => {
|
|
1516
|
+
const values = committed.get(rowId)?.values;
|
|
1517
|
+
if (values === undefined || working.committedValues.get(rowId) === values) {
|
|
1518
|
+
return;
|
|
1519
|
+
}
|
|
1520
|
+
working.committedValues.set(rowId, values);
|
|
1521
|
+
dirty.add("committedValues");
|
|
1522
|
+
};
|
|
1523
|
+
|
|
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();
|
|
1128
1533
|
};
|
|
1129
1534
|
|
|
1130
1535
|
/**
|
|
1131
1536
|
* Moves an existing row across the line between form state and the draft
|
|
1132
|
-
* store. The values
|
|
1133
|
-
*
|
|
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.
|
|
1134
1541
|
*/
|
|
1135
|
-
const setCommitted = (rowId: string,
|
|
1136
|
-
|
|
1137
|
-
if (
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
committedRowIds: committed
|
|
1144
|
-
? [...prev.committedRowIds, rowId]
|
|
1145
|
-
: prev.committedRowIds.filter((id) => id !== rowId),
|
|
1146
|
-
};
|
|
1147
|
-
});
|
|
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();
|
|
1148
1550
|
};
|
|
1149
1551
|
|
|
1150
|
-
|
|
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) => {
|
|
1151
1559
|
const entry = forms.get(rowId);
|
|
1152
1560
|
if (entry === undefined) return;
|
|
1153
1561
|
entry.unsubscribe();
|
|
1154
|
-
entry.unmount();
|
|
1155
1562
|
forms.delete(rowId);
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
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
|
+
}
|
|
1169
1592
|
});
|
|
1170
1593
|
};
|
|
1171
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
|
+
*/
|
|
1172
1604
|
const createForm = (
|
|
1173
1605
|
rowId: string,
|
|
1174
1606
|
original: TMDataGridRowData,
|
|
1175
1607
|
isNew: boolean,
|
|
1608
|
+
values?: TMDataGridRowData,
|
|
1176
1609
|
): FormEntry => {
|
|
1177
1610
|
const entry: FormEntry = {
|
|
1178
1611
|
original,
|
|
1179
1612
|
isNew,
|
|
1180
|
-
committed: false,
|
|
1181
1613
|
lastSubmitOk: false,
|
|
1182
1614
|
submitErrors: [],
|
|
1183
1615
|
parkOnly: false,
|
|
1184
1616
|
pendingCommit: null,
|
|
1185
1617
|
unsubscribe: () => {},
|
|
1186
|
-
unmount: () => {},
|
|
1187
1618
|
form: new FormApi({
|
|
1188
1619
|
defaultValues: original,
|
|
1189
1620
|
// The consumer's vocabulary is Form's own; the cast erases the
|
|
@@ -1198,12 +1629,8 @@ export function createEditEngine(
|
|
|
1198
1629
|
tempId: rowId,
|
|
1199
1630
|
value: value as TMDataGridRowData,
|
|
1200
1631
|
};
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
entry.lastSubmitOk = true;
|
|
1204
|
-
return;
|
|
1205
|
-
}
|
|
1206
|
-
// 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.
|
|
1207
1634
|
if (entry.parkOnly) {
|
|
1208
1635
|
entry.lastSubmitOk = true;
|
|
1209
1636
|
return;
|
|
@@ -1219,7 +1646,7 @@ export function createEditEngine(
|
|
|
1219
1646
|
}
|
|
1220
1647
|
return;
|
|
1221
1648
|
}
|
|
1222
|
-
const changes = diff(entry, value as TMDataGridRowData);
|
|
1649
|
+
const changes = diff(entry.original, value as TMDataGridRowData);
|
|
1223
1650
|
const args: TMDataGridEditCommitArgs<TMDataGridRowData> = {
|
|
1224
1651
|
rowId,
|
|
1225
1652
|
value: value as TMDataGridRowData,
|
|
@@ -1227,12 +1654,8 @@ export function createEditEngine(
|
|
|
1227
1654
|
changes,
|
|
1228
1655
|
source: getContext().editMode,
|
|
1229
1656
|
};
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
entry.lastSubmitOk = true;
|
|
1233
|
-
return;
|
|
1234
|
-
}
|
|
1235
|
-
// 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.
|
|
1236
1659
|
if (entry.parkOnly) {
|
|
1237
1660
|
entry.lastSubmitOk = true;
|
|
1238
1661
|
return;
|
|
@@ -1250,22 +1673,118 @@ export function createEditEngine(
|
|
|
1250
1673
|
},
|
|
1251
1674
|
}) as TMDataGridRowEditForm,
|
|
1252
1675
|
};
|
|
1253
|
-
|
|
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
|
+
}
|
|
1254
1691
|
const subscription = entry.form.store.subscribe(() => publishRow(rowId));
|
|
1255
1692
|
entry.unsubscribe = () => subscription.unsubscribe();
|
|
1256
1693
|
forms.set(rowId, entry);
|
|
1257
|
-
|
|
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();
|
|
1701
|
+
return entry;
|
|
1702
|
+
};
|
|
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);
|
|
1258
1725
|
return entry;
|
|
1259
1726
|
};
|
|
1260
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
|
+
|
|
1261
1774
|
const canEditRow = (row: ErasedRow): boolean => {
|
|
1262
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;
|
|
1263
1781
|
return getContext().isRowEditable?.(row) !== false;
|
|
1264
1782
|
};
|
|
1265
1783
|
|
|
1266
1784
|
const canEditCell = (row: ErasedRow, column: ErasedColumn): boolean => {
|
|
1267
1785
|
const context = getContext();
|
|
1268
1786
|
if (row.getIsGrouped()) return false;
|
|
1787
|
+
if (working.deletedRowIds.has(row.id)) return false;
|
|
1269
1788
|
if (!isColumnEditable(column)) return false;
|
|
1270
1789
|
if (!isColumnEditableForRow(column, row)) return false;
|
|
1271
1790
|
if (context.isRowEditable !== undefined && !context.isRowEditable(row)) {
|
|
@@ -1305,6 +1824,8 @@ export function createEditEngine(
|
|
|
1305
1824
|
|
|
1306
1825
|
const commit = async (rowId: string): Promise<boolean> => {
|
|
1307
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.
|
|
1308
1829
|
if (entry === undefined) return true;
|
|
1309
1830
|
// Enter and blur race on the same edit; the second caller joins the
|
|
1310
1831
|
// first's commit instead of submitting the row twice.
|
|
@@ -1313,9 +1834,10 @@ export function createEditEngine(
|
|
|
1313
1834
|
// Not for a new row: adding an untouched entry is still an add.
|
|
1314
1835
|
if (
|
|
1315
1836
|
!entry.isNew &&
|
|
1316
|
-
diff(entry, entry.form.state.values as TMDataGridRowData)
|
|
1837
|
+
diff(entry.original, entry.form.state.values as TMDataGridRowData)
|
|
1838
|
+
.length === 0
|
|
1317
1839
|
) {
|
|
1318
|
-
|
|
1840
|
+
forget(rowId);
|
|
1319
1841
|
return true;
|
|
1320
1842
|
}
|
|
1321
1843
|
// Field errors the *form's* validator planted are cleared before the
|
|
@@ -1325,13 +1847,18 @@ export function createEditEngine(
|
|
|
1325
1847
|
clearFormSourcedFieldErrors(entry.form);
|
|
1326
1848
|
entry.lastSubmitOk = false;
|
|
1327
1849
|
entry.submitErrors = [];
|
|
1328
|
-
entry.parkOnly = getContext().draft
|
|
1850
|
+
entry.parkOnly = getContext().draft;
|
|
1329
1851
|
entry.pendingCommit = (async () => {
|
|
1330
1852
|
try {
|
|
1331
1853
|
await entry.form.handleSubmit();
|
|
1332
1854
|
} finally {
|
|
1333
1855
|
entry.pendingCommit = null;
|
|
1334
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;
|
|
1335
1862
|
if (!entry.lastSubmitOk) {
|
|
1336
1863
|
// Snapshot before the caller closes the editor: the field errors go
|
|
1337
1864
|
// with it, and the row is about to be left carrying them.
|
|
@@ -1340,49 +1867,67 @@ export function createEditEngine(
|
|
|
1340
1867
|
return false;
|
|
1341
1868
|
}
|
|
1342
1869
|
if (entry.parkOnly) {
|
|
1343
|
-
// Into the draft store: the
|
|
1344
|
-
//
|
|
1345
|
-
//
|
|
1346
|
-
//
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
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
|
+
});
|
|
1353
1889
|
return true;
|
|
1354
1890
|
}
|
|
1355
|
-
|
|
1891
|
+
forget(rowId);
|
|
1356
1892
|
return true;
|
|
1357
1893
|
})();
|
|
1358
1894
|
return entry.pendingCommit;
|
|
1359
1895
|
};
|
|
1360
1896
|
|
|
1361
1897
|
const beginOn = (rowId: string, columnId: string | null) => {
|
|
1362
|
-
//
|
|
1363
|
-
//
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
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
|
+
}
|
|
1369
1929
|
setActive({ rowId, columnId });
|
|
1370
|
-
|
|
1371
|
-
}
|
|
1372
|
-
const row = getRow(rowId);
|
|
1373
|
-
if (row === undefined) return;
|
|
1374
|
-
if (columnId !== null) {
|
|
1375
|
-
const column = getContext().table.getColumn(columnId);
|
|
1376
|
-
if (column === undefined || !canEditCell(row, column)) return;
|
|
1377
|
-
} else if (getContext().isRowEditable?.(row) === false) {
|
|
1378
|
-
return;
|
|
1379
|
-
}
|
|
1380
|
-
if (!forms.has(rowId)) createForm(rowId, row.original, false);
|
|
1381
|
-
// Reopening a committed row takes it back out of the draft store: what
|
|
1382
|
-
// the user is now editing is undecided again, and `saveDrafts` must not
|
|
1383
|
-
// send it until it is committed afresh.
|
|
1384
|
-
else if (forms.get(rowId)?.committed === true) setCommitted(rowId, false);
|
|
1385
|
-
setActive({ rowId, columnId });
|
|
1930
|
+
});
|
|
1386
1931
|
};
|
|
1387
1932
|
|
|
1388
1933
|
const begin: TMDataGridEditApi["begin"] = ({ rowId, columnId }) => {
|
|
@@ -1396,15 +1941,14 @@ export function createEditEngine(
|
|
|
1396
1941
|
// so a second row opening can neither discard the first nor be refused
|
|
1397
1942
|
// by it - nothing about one row's form bears on another's |
|
|
1398
1943
|
//
|
|
1399
|
-
// A
|
|
1400
|
-
// 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
|
|
1401
1946
|
// entry row is skipped too - it is row-shaped in every mode, and its ✓
|
|
1402
1947
|
// is the decision, so the sweep must not add a half-typed row.
|
|
1403
1948
|
if (editMode === "cell") {
|
|
1404
|
-
const openElsewhere = [...forms.keys()].find(
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
});
|
|
1949
|
+
const openElsewhere = [...forms.keys()].find(
|
|
1950
|
+
(id) => id !== rowId && forms.get(id)?.isNew !== true,
|
|
1951
|
+
);
|
|
1408
1952
|
if (openElsewhere !== undefined) {
|
|
1409
1953
|
void commit(openElsewhere).then((ok) => {
|
|
1410
1954
|
if (ok) beginOn(rowId, columnId);
|
|
@@ -1416,7 +1960,7 @@ export function createEditEngine(
|
|
|
1416
1960
|
};
|
|
1417
1961
|
|
|
1418
1962
|
const cancel = (rowId: string) => {
|
|
1419
|
-
|
|
1963
|
+
forget(rowId);
|
|
1420
1964
|
};
|
|
1421
1965
|
|
|
1422
1966
|
const deactivate = () => {
|
|
@@ -1424,26 +1968,34 @@ export function createEditEngine(
|
|
|
1424
1968
|
};
|
|
1425
1969
|
|
|
1426
1970
|
const cancelAll = () => {
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
...
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
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
|
+
});
|
|
1434
1984
|
};
|
|
1435
1985
|
|
|
1436
|
-
|
|
1986
|
+
/** Opens one entry row - the form and its `newRows` entry, in one write. */
|
|
1987
|
+
const openEntryRow = (values?: TMDataGridRowData): string => {
|
|
1437
1988
|
newRowCounter += 1;
|
|
1438
|
-
const tempId =
|
|
1989
|
+
const tempId = `${NEW_ROW_ID_PREFIX}${newRowCounter}`;
|
|
1439
1990
|
createForm(tempId, seedNewRow(values), true);
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
newRows: [...prev.newRows, { tempId, committed: false }],
|
|
1443
|
-
}));
|
|
1991
|
+
working.newRows.set(tempId, false);
|
|
1992
|
+
dirty.add("newRows");
|
|
1444
1993
|
return tempId;
|
|
1445
1994
|
};
|
|
1446
1995
|
|
|
1996
|
+
const addRow = (values?: TMDataGridRowData): string =>
|
|
1997
|
+
heldSync(() => openEntryRow(values));
|
|
1998
|
+
|
|
1447
1999
|
/** Seeds one entry row's values - `newRowDefaults` under `values`. */
|
|
1448
2000
|
const seedNewRow = (values?: TMDataGridRowData): TMDataGridRowData => {
|
|
1449
2001
|
const { newRowDefaults } = getContext();
|
|
@@ -1459,56 +2011,74 @@ export function createEditEngine(
|
|
|
1459
2011
|
const addRows = async (
|
|
1460
2012
|
rows: ReadonlyArray<TMDataGridRowData>,
|
|
1461
2013
|
options?: TMDataGridAddRowsOptions,
|
|
1462
|
-
): Promise<TMDataGridAddRowsResult> =>
|
|
1463
|
-
|
|
1464
|
-
//
|
|
1465
|
-
//
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
const tempId = `__new__${newRowCounter}`;
|
|
1471
|
-
createForm(tempId, seedNewRow(values), true);
|
|
1472
|
-
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 };
|
|
1473
2022
|
}
|
|
1474
|
-
store.setState((prev) => ({
|
|
1475
|
-
...prev,
|
|
1476
|
-
newRows: [
|
|
1477
|
-
...prev.newRows,
|
|
1478
|
-
...tempIds.map((tempId) => ({ tempId, committed: false })),
|
|
1479
|
-
],
|
|
1480
|
-
}));
|
|
1481
|
-
});
|
|
1482
|
-
|
|
1483
|
-
if (options?.commit !== true) return { committed: [], open: tempIds };
|
|
1484
2023
|
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
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
|
+
});
|
|
1495
2040
|
|
|
1496
2041
|
const deleteRow = (rowId: string) => {
|
|
1497
|
-
|
|
1498
|
-
//
|
|
1499
|
-
if (
|
|
1500
|
-
|
|
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);
|
|
1501
2049
|
return;
|
|
1502
2050
|
}
|
|
1503
2051
|
const context = getContext();
|
|
1504
2052
|
if (context.draft) {
|
|
1505
|
-
//
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
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
|
+
});
|
|
1512
2082
|
return;
|
|
1513
2083
|
}
|
|
1514
2084
|
const row = getRow(rowId);
|
|
@@ -1517,6 +2087,19 @@ export function createEditEngine(
|
|
|
1517
2087
|
void context.onRowDelete?.({ rowId, row });
|
|
1518
2088
|
};
|
|
1519
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
|
+
|
|
1520
2103
|
const canDeleteRows = (): boolean => {
|
|
1521
2104
|
const context = getContext();
|
|
1522
2105
|
if (context.draft) {
|
|
@@ -1528,151 +2111,275 @@ export function createEditEngine(
|
|
|
1528
2111
|
return context.onRowDelete !== undefined;
|
|
1529
2112
|
};
|
|
1530
2113
|
|
|
1531
|
-
/** The
|
|
1532
|
-
const
|
|
1533
|
-
|
|
1534
|
-
if (deleted.length > 0) {
|
|
1535
|
-
store.setState((prev) => ({ ...prev, deletedRowIds: [] }));
|
|
1536
|
-
}
|
|
1537
|
-
return deleted;
|
|
1538
|
-
};
|
|
1539
|
-
|
|
1540
|
-
/** The draft store's rows: committed forms, in the order they landed. */
|
|
1541
|
-
const committedFormIds = (): Array<string> =>
|
|
1542
|
-
[...forms.keys()].filter((rowId) => forms.get(rowId)?.committed === true);
|
|
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));
|
|
1543
2117
|
|
|
1544
|
-
const commitAll = async (): Promise<
|
|
1545
|
-
//
|
|
1546
|
-
//
|
|
1547
|
-
const openIds = [...forms.keys()]
|
|
1548
|
-
|
|
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))),
|
|
1549
2124
|
);
|
|
1550
|
-
|
|
1551
|
-
return results.every(Boolean);
|
|
2125
|
+
return splitCommitted(openIds, results);
|
|
1552
2126
|
};
|
|
1553
2127
|
|
|
1554
2128
|
/**
|
|
1555
2129
|
* The in-flight save. A second call while `onSaveDrafts` awaits would
|
|
1556
2130
|
* re-collect the same payload and send it again - a double-clicked Save
|
|
1557
2131
|
* would create every pending entry row twice - so concurrent calls join
|
|
1558
|
-
* this promise instead of starting a save of their own.
|
|
2132
|
+
* this promise, and its result, instead of starting a save of their own.
|
|
1559
2133
|
*/
|
|
1560
|
-
let saveInFlight: Promise<
|
|
2134
|
+
let saveInFlight: Promise<TMDataGridSaveDraftsResult> | null = null;
|
|
1561
2135
|
|
|
1562
|
-
const saveDrafts = (): Promise<
|
|
2136
|
+
const saveDrafts = (): Promise<TMDataGridSaveDraftsResult> => {
|
|
1563
2137
|
if (saveInFlight !== null) return saveInFlight;
|
|
1564
|
-
// While this runs, `commit` really commits - `editing.draft` parks
|
|
1565
|
-
// otherwise. Single-threaded flag, same idiom as the collectors.
|
|
1566
|
-
savingDrafts = true;
|
|
1567
2138
|
saveInFlight = saveDraftsInner().finally(() => {
|
|
1568
|
-
savingDrafts = false;
|
|
1569
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
|
+
}
|
|
1570
2146
|
});
|
|
1571
2147
|
return saveInFlight;
|
|
1572
2148
|
};
|
|
1573
2149
|
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
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
|
+
});
|
|
1577
2181
|
// Nothing decided: open rows are not this verb's business, so a grid
|
|
1578
2182
|
// mid-edit with an empty draft store saves cleanly and stays as it is.
|
|
1579
|
-
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();
|
|
1580
2200
|
|
|
1581
2201
|
if (getContext().onSaveDrafts === undefined) {
|
|
1582
2202
|
// The default: the per-row loop - edits through `onEditCommit`, entry
|
|
1583
|
-
// rows through `onRowAdd`, marked deletions through `onRowDelete`.
|
|
1584
|
-
|
|
1585
|
-
|
|
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
|
+
),
|
|
1586
2244
|
);
|
|
1587
|
-
|
|
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) {
|
|
1588
2256
|
const row = getRow(rowId);
|
|
1589
|
-
|
|
1590
|
-
|
|
2257
|
+
try {
|
|
2258
|
+
if (row !== undefined) {
|
|
2259
|
+
await getContext().onRowDelete?.({ rowId, row });
|
|
2260
|
+
}
|
|
2261
|
+
deletedOk.push(rowId);
|
|
2262
|
+
} catch {
|
|
2263
|
+
kept.push(rowId);
|
|
1591
2264
|
}
|
|
1592
2265
|
}
|
|
1593
|
-
|
|
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();
|
|
1594
2277
|
}
|
|
1595
2278
|
|
|
1596
|
-
// One consumer call for the lot.
|
|
1597
|
-
//
|
|
1598
|
-
//
|
|
1599
|
-
//
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
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
|
+
});
|
|
1617
2320
|
const deleted = deletedIds;
|
|
1618
2321
|
if (collected.length > 0 || added.length > 0 || deleted.length > 0) {
|
|
1619
|
-
let
|
|
2322
|
+
let response: void | TMDataGridSaveDraftsResponse;
|
|
1620
2323
|
try {
|
|
1621
|
-
|
|
2324
|
+
response = await getContext().onSaveDrafts?.({
|
|
1622
2325
|
updated: collected,
|
|
1623
2326
|
created: added,
|
|
1624
2327
|
deleted,
|
|
1625
|
-
// The pre-2.0 names, still filled - see TMDataGridSaveDraftsArgs.
|
|
1626
|
-
rows: collected,
|
|
1627
|
-
added,
|
|
1628
2328
|
});
|
|
1629
2329
|
} catch {
|
|
1630
|
-
|
|
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();
|
|
1631
2333
|
}
|
|
1632
2334
|
|
|
1633
|
-
// Nothing returned saves the lot. A
|
|
2335
|
+
// Nothing returned saves the lot. A response names what failed; those
|
|
1634
2336
|
// keep their drafts, committed, so the next save retries them.
|
|
1635
|
-
const outcomes =
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
)
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
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
|
+
});
|
|
1660
2365
|
}
|
|
1661
|
-
return
|
|
1662
|
-
};
|
|
1663
|
-
|
|
1664
|
-
/** @deprecated The old one-shot save - `commitAll` then `saveDrafts`. */
|
|
1665
|
-
const submitAll = async (): Promise<boolean> => {
|
|
1666
|
-
const committedOk = await commitAll();
|
|
1667
|
-
const savedOk = await saveDrafts();
|
|
1668
|
-
return committedOk && savedOk;
|
|
2366
|
+
return report();
|
|
1669
2367
|
};
|
|
1670
2368
|
|
|
1671
2369
|
/**
|
|
1672
2370
|
* The write path behind `clearCell`, `setCellValue` and `setRowValues`:
|
|
1673
2371
|
* fill the row's own form and commit it, exactly as a ✓ on an open editor
|
|
1674
2372
|
* would. Uses the form already open on the row when there is one, so a
|
|
1675
|
-
* programmatic write joins an edit in progress rather than discarding it
|
|
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.
|
|
1676
2383
|
*
|
|
1677
2384
|
* No editor is involved, so `meta.edit.mapValue` does not run - the caller
|
|
1678
2385
|
* writes the stored value itself. Validation is untouched: this is the same
|
|
@@ -1686,12 +2393,24 @@ export function createEditEngine(
|
|
|
1686
2393
|
): Promise<boolean> => {
|
|
1687
2394
|
const row = getRow(rowId);
|
|
1688
2395
|
if (row === undefined) return false;
|
|
2396
|
+
// Read-only while marked - see canEditRow.
|
|
2397
|
+
if (working.deletedRowIds.has(rowId)) return false;
|
|
1689
2398
|
if (writes.length === 0) return true;
|
|
1690
|
-
const
|
|
1691
|
-
|
|
1692
|
-
|
|
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));
|
|
1693
2409
|
}
|
|
1694
|
-
return
|
|
2410
|
+
return held(async () => {
|
|
2411
|
+
const entry = reopen(rowId);
|
|
2412
|
+
return entry === undefined ? false : write(entry);
|
|
2413
|
+
});
|
|
1695
2414
|
};
|
|
1696
2415
|
|
|
1697
2416
|
/** The field a cell writes to, or `null` when that cell takes no edit. */
|
|
@@ -1743,12 +2462,76 @@ export function createEditEngine(
|
|
|
1743
2462
|
return writeFields(rowId, writes);
|
|
1744
2463
|
};
|
|
1745
2464
|
|
|
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);
|
|
2470
|
+
|
|
2471
|
+
const getRows = (): ReadonlyArray<TMDataGridEditRowSnapshot> => {
|
|
2472
|
+
const deleted = working.deletedRowIds;
|
|
2473
|
+
const rows: Array<TMDataGridEditRowSnapshot> = [];
|
|
2474
|
+
const model = getContext().table.getCoreRowModel();
|
|
2475
|
+
for (const row of model.flatRows) {
|
|
2476
|
+
rows.push({
|
|
2477
|
+
rowId: row.id,
|
|
2478
|
+
value: draftValues(row.id) ?? (row.original as TMDataGridRowData),
|
|
2479
|
+
isNew:
|
|
2480
|
+
forms.get(row.id)?.isNew ?? committed.get(row.id)?.isNew ?? false,
|
|
2481
|
+
deleted: deleted.has(row.id),
|
|
2482
|
+
});
|
|
2483
|
+
}
|
|
2484
|
+
// Entry rows the table does not hold - see mergedRows.
|
|
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;
|
|
2489
|
+
rows.push({
|
|
2490
|
+
rowId: tempId,
|
|
2491
|
+
value: values,
|
|
2492
|
+
isNew: true,
|
|
2493
|
+
deleted: deleted.has(tempId),
|
|
2494
|
+
});
|
|
2495
|
+
}
|
|
2496
|
+
return rows;
|
|
2497
|
+
};
|
|
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
|
+
|
|
1746
2525
|
return {
|
|
1747
2526
|
store,
|
|
1748
2527
|
get state() {
|
|
1749
2528
|
return store.state;
|
|
1750
2529
|
},
|
|
2530
|
+
forgetMissingRows,
|
|
2531
|
+
isRowDeleted: (rowId) => working.deletedRowIds.has(rowId),
|
|
1751
2532
|
getForm: (rowId) => forms.get(rowId)?.form,
|
|
2533
|
+
getRowValues,
|
|
2534
|
+
getRows,
|
|
1752
2535
|
canEditCell,
|
|
1753
2536
|
canEditRow,
|
|
1754
2537
|
isColumnEditable,
|
|
@@ -1759,13 +2542,14 @@ export function createEditEngine(
|
|
|
1759
2542
|
cancelAll,
|
|
1760
2543
|
commitAll,
|
|
1761
2544
|
saveDrafts,
|
|
1762
|
-
submitAll,
|
|
1763
2545
|
clearCell,
|
|
1764
2546
|
setCellValue,
|
|
1765
2547
|
setRowValues,
|
|
1766
2548
|
addRow,
|
|
1767
2549
|
addRows,
|
|
1768
2550
|
deleteRow,
|
|
2551
|
+
deleteRows,
|
|
2552
|
+
restoreRow,
|
|
1769
2553
|
canDeleteRows,
|
|
1770
2554
|
};
|
|
1771
2555
|
}
|