@jielga/tmdatagrid 2.0.0-beta.3 → 2.0.0-beta.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +37 -9
- package/dist/index.js +1021 -948
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/skills/appearance/SKILL.md +1 -1
- package/skills/cell-selection/SKILL.md +1 -1
- package/skills/columns/SKILL.md +1 -1
- package/skills/data/SKILL.md +1 -1
- package/skills/editing/SKILL.md +18 -6
- package/skills/filtering/SKILL.md +1 -1
- package/skills/getting-started/SKILL.md +1 -1
- package/skills/grouping/SKILL.md +1 -1
- package/skills/options/SKILL.md +32 -2
- package/skills/rows/SKILL.md +1 -1
- package/skills/server-side/SKILL.md +1 -1
- package/skills/testing/SKILL.md +1 -1
- package/src/tmdatagrid/components/TMDataGridEntryRows.tsx +3 -0
- package/src/tmdatagrid/components/TMDataGridTable.tsx +13 -0
- package/src/tmdatagrid/core/controlledState.ts +179 -0
- package/src/tmdatagrid/core/editEngine.ts +92 -15
- package/src/tmdatagrid/index.ts +2 -0
- package/src/tmdatagrid/useTMDataGrid.tsx +115 -4
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jielga/tmdatagrid",
|
|
3
|
-
"version": "2.0.0-beta.
|
|
3
|
+
"version": "2.0.0-beta.4",
|
|
4
4
|
"description": "A React data grid built on TanStack Table v9 and Mantine - always virtualized, with resizable, reorderable, sortable, filterable, hideable and pinnable columns.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -14,7 +14,7 @@ description: >
|
|
|
14
14
|
metadata:
|
|
15
15
|
type: core
|
|
16
16
|
library: '@jielga/tmdatagrid'
|
|
17
|
-
library_version: '2.0.0-beta.
|
|
17
|
+
library_version: '2.0.0-beta.4'
|
|
18
18
|
sources:
|
|
19
19
|
- 'Jielga/TMDataGrid:src/docs/cell-selection.md'
|
|
20
20
|
- 'Jielga/TMDataGrid:src/tmdatagrid/core/cellNavigation.ts'
|
package/skills/columns/SKILL.md
CHANGED
package/skills/data/SKILL.md
CHANGED
package/skills/editing/SKILL.md
CHANGED
|
@@ -18,7 +18,7 @@ description: >
|
|
|
18
18
|
metadata:
|
|
19
19
|
type: core
|
|
20
20
|
library: '@jielga/tmdatagrid'
|
|
21
|
-
library_version: '2.0.0-beta.
|
|
21
|
+
library_version: '2.0.0-beta.4'
|
|
22
22
|
sources:
|
|
23
23
|
- 'Jielga/TMDataGrid:src/docs/editing.md'
|
|
24
24
|
- 'Jielga/TMDataGrid:src/docs/query-builder.md'
|
|
@@ -123,9 +123,21 @@ tooltip.
|
|
|
123
123
|
|
|
124
124
|
`saveDrafts` then sends the draft store: through the per-row
|
|
125
125
|
`editing.onCommit` / `editing.onRowAdd` / `editing.onRowDelete` loop by default,
|
|
126
|
-
or through one `editing.onSaveDrafts({
|
|
127
|
-
is set.
|
|
128
|
-
|
|
126
|
+
or through one `editing.onSaveDrafts({ updated, created, deleted })` call when
|
|
127
|
+
that is set. `updated` entries carry a `rowId`, `created` entries a `tempId`, `deleted` is
|
|
128
|
+
a list of row ids. Rows failing validation stay open either way.
|
|
129
|
+
|
|
130
|
+
`onSaveDrafts` decides how much of the store is cleared: returning nothing
|
|
131
|
+
saves everything, throwing saves nothing, and returning
|
|
132
|
+
`{ updated, created, deleted }` saves everything except the ids reported
|
|
133
|
+
`false`. Each key takes `false` for the whole bucket or a map of id to result;
|
|
134
|
+
an unnamed id saved. A kept row stays committed, so the next `saveDrafts()`
|
|
135
|
+
retries it, and `saveDrafts()` resolves `false` when anything was kept.
|
|
136
|
+
|
|
137
|
+
Rows carry `data-dirty` (values typed in), `data-draft` (committed, waiting for
|
|
138
|
+
Save), `data-deleted` and, on entry rows, `data-new`. The grid paints none of
|
|
139
|
+
them; use `rowStyle` / `rowClassName` or the attributes to highlight what is
|
|
140
|
+
pending.
|
|
129
141
|
|
|
130
142
|
## What a commit receives
|
|
131
143
|
|
|
@@ -236,8 +248,8 @@ const grid = useTMDataGrid({
|
|
|
236
248
|
editing: {
|
|
237
249
|
mode: "draft",
|
|
238
250
|
newRowDefaults: () => ({ id: 0, firstName: "", salary: 30_000 }),
|
|
239
|
-
onSaveDrafts: async ({
|
|
240
|
-
await api.saveBatch({
|
|
251
|
+
onSaveDrafts: async ({ updated, created, deleted }) => {
|
|
252
|
+
await api.saveBatch({ updated, created, deleted });
|
|
241
253
|
},
|
|
242
254
|
},
|
|
243
255
|
});
|
package/skills/grouping/SKILL.md
CHANGED
package/skills/options/SKILL.md
CHANGED
|
@@ -14,7 +14,7 @@ description: >
|
|
|
14
14
|
metadata:
|
|
15
15
|
type: core
|
|
16
16
|
library: '@jielga/tmdatagrid'
|
|
17
|
-
library_version: '2.0.0-beta.
|
|
17
|
+
library_version: '2.0.0-beta.4'
|
|
18
18
|
sources:
|
|
19
19
|
- 'Jielga/TMDataGrid:src/docs/use-tm-data-grid.md'
|
|
20
20
|
- 'Jielga/TMDataGrid:src/tmdatagrid/useTMDataGrid.tsx'
|
|
@@ -62,7 +62,8 @@ rather than forwarded to TanStack.
|
|
|
62
62
|
| `onFocusedCellChange` | `(cell \| null) => void` | – | Fires whenever the focused cell moves. |
|
|
63
63
|
| `overscan` | `number` | `6` | Rows the virtualizer keeps mounted above and below the viewport. Defined by the grid. |
|
|
64
64
|
| `columnResizeMode` | `"onChange" \| "onEnd"` | `"onChange"` | Resize update strategy. |
|
|
65
|
-
| `initialState` | `Partial<TableState>` | See below | Merged over the grid defaults. |
|
|
65
|
+
| `initialState` | `Partial<TableState>` | See below | The state the grid starts from, read once on mount. Merged over the grid defaults. |
|
|
66
|
+
| `state` | `Partial<TableState>` | – | Controlled state. Each slice requires its `onXChange` - see below. |
|
|
66
67
|
| `meta` | `TMDataGridTableMeta` | `{}` | Grid configuration, see below. |
|
|
67
68
|
| `persist` | `TMDataGridPersistence` | – | State persistence, see below. |
|
|
68
69
|
| `editing` | `TMDataGridEditingOptions` | off | Turns editing on. `mode` (`"cell" \| "cellConfirm" \| "row" \| "draft"`) picks the commit policy; the object also holds `onCommit`, `onSaveDrafts`, `rowValidators`, `isRowEditable`, `newRowDefaults`, `newRowsSticky`, `onRowAdd` and `onRowDelete` - see the `editing` skill. |
|
|
@@ -76,6 +77,35 @@ rather than forwarded to TanStack.
|
|
|
76
77
|
| `columnPinning.left` | The checkbox column, followed by any columns you provide |
|
|
77
78
|
| `globalFilterFn` | `"includesString"` |
|
|
78
79
|
|
|
80
|
+
### Controlled state
|
|
81
|
+
|
|
82
|
+
`state` makes a slice controlled: the parent owns the value, the grid reads it
|
|
83
|
+
every render, and all writes go through the matching `onXChange`. Without the
|
|
84
|
+
callback the slice cannot change; the grid logs a console warning in
|
|
85
|
+
development. For a starting value only, use `initialState`.
|
|
86
|
+
|
|
87
|
+
```tsx
|
|
88
|
+
const [columnVisibility, setColumnVisibility] = useState({ play: false });
|
|
89
|
+
|
|
90
|
+
const grid = useTMDataGrid({
|
|
91
|
+
data,
|
|
92
|
+
columns,
|
|
93
|
+
state: { columnVisibility },
|
|
94
|
+
onColumnVisibilityChange: setColumnVisibility,
|
|
95
|
+
});
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
- A key set to `undefined` is ignored; the slice is uncontrolled.
|
|
99
|
+
- Slices compare structurally between renders, so the `state` object can be
|
|
100
|
+
built inline. `Date`s compare by time; `Map`s and class instances compare by
|
|
101
|
+
identity and belong in `useState` or `useMemo`.
|
|
102
|
+
- `atoms` also controls a slice, with no callback. Takes precedence over
|
|
103
|
+
`state` for the same slice.
|
|
104
|
+
- `columnVisibility` toggles user-defined columns only. Entries for the
|
|
105
|
+
generated columns are ignored; enable or disable those through their feature
|
|
106
|
+
options. The tree column's entry follows `grouping`.
|
|
107
|
+
- `persist` cannot restore a controlled slice; it still writes it to storage.
|
|
108
|
+
|
|
79
109
|
## meta
|
|
80
110
|
|
|
81
111
|
| Field | Type | Default | Description |
|
package/skills/rows/SKILL.md
CHANGED
|
@@ -17,7 +17,7 @@ description: >
|
|
|
17
17
|
metadata:
|
|
18
18
|
type: core
|
|
19
19
|
library: '@jielga/tmdatagrid'
|
|
20
|
-
library_version: '2.0.0-beta.
|
|
20
|
+
library_version: '2.0.0-beta.4'
|
|
21
21
|
sources:
|
|
22
22
|
- 'Jielga/TMDataGrid:src/docs/row-selection.md'
|
|
23
23
|
- 'Jielga/TMDataGrid:src/docs/row-interaction.md'
|
|
@@ -10,7 +10,7 @@ description: >
|
|
|
10
10
|
metadata:
|
|
11
11
|
type: core
|
|
12
12
|
library: '@jielga/tmdatagrid'
|
|
13
|
-
library_version: '2.0.0-beta.
|
|
13
|
+
library_version: '2.0.0-beta.4'
|
|
14
14
|
sources:
|
|
15
15
|
- 'Jielga/TMDataGrid:src/docs/server-side.md'
|
|
16
16
|
- 'Jielga/TMDataGrid:src/tmdatagrid/useTMDataGrid.tsx'
|
package/skills/testing/SKILL.md
CHANGED
|
@@ -11,7 +11,7 @@ description: >
|
|
|
11
11
|
metadata:
|
|
12
12
|
type: core
|
|
13
13
|
library: '@jielga/tmdatagrid'
|
|
14
|
-
library_version: '2.0.0-beta.
|
|
14
|
+
library_version: '2.0.0-beta.4'
|
|
15
15
|
sources:
|
|
16
16
|
- 'Jielga/TMDataGrid:src/docs/testing.md'
|
|
17
17
|
- 'Jielga/TMDataGrid:src/tmdatagrid/components/TMDataGrid.tsx'
|
|
@@ -192,6 +192,9 @@ export function TMDataGridEntryRows({
|
|
|
192
192
|
data-row-id={entryRow.id}
|
|
193
193
|
data-new
|
|
194
194
|
data-committed={committed}
|
|
195
|
+
// The same marker body rows carry once committed, so one selector
|
|
196
|
+
// reaches everything parked in the draft store.
|
|
197
|
+
data-draft={committed}
|
|
195
198
|
className={classes.entryRow}
|
|
196
199
|
>
|
|
197
200
|
{orderedColumns.map((column) => {
|
|
@@ -750,6 +750,14 @@ export function TMDataGridTable<TData extends RowData = TMDataGridRowData>({
|
|
|
750
750
|
(id) => (state.rows[id]?.dirtyFields.length ?? 0) > 0,
|
|
751
751
|
),
|
|
752
752
|
);
|
|
753
|
+
// Rows parked in the draft store, waiting for Save. A separate marker from
|
|
754
|
+
// `data-dirty`, which covers any row being typed into: after a partial save
|
|
755
|
+
// these are what is left, so one selector highlights everything still
|
|
756
|
+
// pending.
|
|
757
|
+
const editDraftRowIds = useSelector(
|
|
758
|
+
edit.store,
|
|
759
|
+
(state) => state.committedRowIds,
|
|
760
|
+
);
|
|
753
761
|
|
|
754
762
|
const { loading, noResultsLabel = labels.noResults } =
|
|
755
763
|
table.options.meta ?? {};
|
|
@@ -2233,6 +2241,11 @@ export function TMDataGridTable<TData extends RowData = TMDataGridRowData>({
|
|
|
2233
2241
|
editDirtyRowIds.length > 0 &&
|
|
2234
2242
|
editDirtyRowIds.includes(row.id)
|
|
2235
2243
|
}
|
|
2244
|
+
// Committed into the draft store, waiting for Save.
|
|
2245
|
+
data-draft={
|
|
2246
|
+
editDraftRowIds.length > 0 &&
|
|
2247
|
+
editDraftRowIds.includes(row.id)
|
|
2248
|
+
}
|
|
2236
2249
|
// The menu is anchored to the rowgroup, so Mantine's own
|
|
2237
2250
|
// `data-expanded` lands there rather than on a row. This is
|
|
2238
2251
|
// what says which row the open menu is about.
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import type { TableState } from "@tanstack/react-table";
|
|
2
|
+
import type { TMDataGridFeatures } from "../useTMDataGrid";
|
|
3
|
+
|
|
4
|
+
type GridState = TableState<TMDataGridFeatures>;
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Support for TanStack's `state` option (controlled state).
|
|
8
|
+
*
|
|
9
|
+
* table-core re-reads `options.state` on every render (`useTable` calls
|
|
10
|
+
* `setOptions` while rendering) and writes any slice whose value is not
|
|
11
|
+
* identical to the table's current value into the table's atom. The write
|
|
12
|
+
* publishes the store and re-renders the consumer, so a slice object built in
|
|
13
|
+
* the render body causes an infinite render loop.
|
|
14
|
+
*
|
|
15
|
+
* {@link stabilizeControlledState} prevents the loop by forwarding the
|
|
16
|
+
* previous render's value for a slice whose contents are unchanged.
|
|
17
|
+
* {@link findFrozenStateSlices} detects a controlled slice without its
|
|
18
|
+
* `onXChange` callback, which the grid cannot write to.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* The `onXChange` option that owns each controlled slice - every slice v9
|
|
23
|
+
* defines. `globalFilterFn` is not among them: in this beta it is a table
|
|
24
|
+
* option, not state.
|
|
25
|
+
*/
|
|
26
|
+
export const CONTROLLED_SLICE_HANDLERS = {
|
|
27
|
+
columnFilters: "onColumnFiltersChange",
|
|
28
|
+
columnOrder: "onColumnOrderChange",
|
|
29
|
+
columnPinning: "onColumnPinningChange",
|
|
30
|
+
columnResizing: "onColumnResizingChange",
|
|
31
|
+
columnSizing: "onColumnSizingChange",
|
|
32
|
+
columnVisibility: "onColumnVisibilityChange",
|
|
33
|
+
expanded: "onExpandedChange",
|
|
34
|
+
globalFilter: "onGlobalFilterChange",
|
|
35
|
+
grouping: "onGroupingChange",
|
|
36
|
+
pagination: "onPaginationChange",
|
|
37
|
+
rowPinning: "onRowPinningChange",
|
|
38
|
+
rowSelection: "onRowSelectionChange",
|
|
39
|
+
sorting: "onSortingChange",
|
|
40
|
+
} as const satisfies Partial<Record<keyof GridState, string>>;
|
|
41
|
+
|
|
42
|
+
export type TMDataGridControlledSlice = keyof typeof CONTROLLED_SLICE_HANDLERS;
|
|
43
|
+
|
|
44
|
+
/** The options {@link findFrozenStateSlices} reads - a subset of `TableOptions`. */
|
|
45
|
+
type ControlledOptions = {
|
|
46
|
+
state?: Partial<GridState>;
|
|
47
|
+
/**
|
|
48
|
+
* The v9 way of owning a slice: an atom the table writes through directly.
|
|
49
|
+
* It outranks `state`, so a slice backed by one is never frozen.
|
|
50
|
+
*/
|
|
51
|
+
atoms?: Record<string, unknown>;
|
|
52
|
+
} & Partial<
|
|
53
|
+
Record<(typeof CONTROLLED_SLICE_HANDLERS)[TMDataGridControlledSlice], unknown>
|
|
54
|
+
>;
|
|
55
|
+
|
|
56
|
+
const hasOwn = (value: object, key: string): boolean =>
|
|
57
|
+
Object.prototype.hasOwnProperty.call(value, key);
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* A plain data object: `Object.prototype` or null prototype. table-core builds
|
|
61
|
+
* its state maps with `Object.create(null)`, so null prototypes must compare
|
|
62
|
+
* as plain objects.
|
|
63
|
+
*/
|
|
64
|
+
function isDataObject(value: unknown): value is Record<string, unknown> {
|
|
65
|
+
if (typeof value !== "object" || value === null) return false;
|
|
66
|
+
const proto = Object.getPrototypeOf(value);
|
|
67
|
+
return proto === Object.prototype || proto === null;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Structural equality for state values.
|
|
72
|
+
*
|
|
73
|
+
* `Date`s compare by time; the built-in date filters hold them in filter
|
|
74
|
+
* values. `Map`s and class instances compare by identity, so a controlled
|
|
75
|
+
* slice containing one rebuilt each render reads as changed on every render
|
|
76
|
+
* and re-renders the table. Use primitives, plain objects, arrays or `Date`s
|
|
77
|
+
* in controlled state.
|
|
78
|
+
*/
|
|
79
|
+
export function sameStateValue(a: unknown, b: unknown): boolean {
|
|
80
|
+
if (Object.is(a, b)) return true;
|
|
81
|
+
if (a instanceof Date || b instanceof Date) {
|
|
82
|
+
return (
|
|
83
|
+
a instanceof Date && b instanceof Date && a.getTime() === b.getTime()
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
if (Array.isArray(a) || Array.isArray(b)) {
|
|
87
|
+
if (!Array.isArray(a) || !Array.isArray(b)) return false;
|
|
88
|
+
return (
|
|
89
|
+
a.length === b.length &&
|
|
90
|
+
a.every((item, index) => sameStateValue(item, b[index]))
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
if (!isDataObject(a) || !isDataObject(b)) return false;
|
|
94
|
+
const keys = Object.keys(a);
|
|
95
|
+
if (keys.length !== Object.keys(b).length) return false;
|
|
96
|
+
return keys.every((key) => hasOwn(b, key) && sameStateValue(a[key], b[key]));
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Removes keys whose value is `undefined`, as in
|
|
101
|
+
* `state: { sorting: cond ? sorting : undefined }`.
|
|
102
|
+
*
|
|
103
|
+
* TanStack's sync writes every present key into the slice's atom, `undefined`
|
|
104
|
+
* included, which breaks everything that reads the slice. A key set to
|
|
105
|
+
* `undefined` is treated as not controlled: the key is removed and the slice
|
|
106
|
+
* falls back to the table's own state. Identity is preserved when there is
|
|
107
|
+
* nothing to remove; the stabilizer compares by it.
|
|
108
|
+
*/
|
|
109
|
+
export function withoutUndefinedSlices(
|
|
110
|
+
state: Partial<GridState> | undefined,
|
|
111
|
+
): Partial<GridState> | undefined {
|
|
112
|
+
if (state === undefined) return state;
|
|
113
|
+
if (!Object.values(state).includes(undefined)) return state;
|
|
114
|
+
const compacted: Record<string, unknown> = {};
|
|
115
|
+
for (const [key, value] of Object.entries(state)) {
|
|
116
|
+
if (value !== undefined) compacted[key] = value;
|
|
117
|
+
}
|
|
118
|
+
return compacted as Partial<GridState>;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Returns `next` with every slice that structurally equals its counterpart in
|
|
123
|
+
* `previous` replaced by the previous object, so table-core's identity-based
|
|
124
|
+
* sync finds nothing to write. Returns `previous` when nothing changed.
|
|
125
|
+
*/
|
|
126
|
+
export function stabilizeControlledState(
|
|
127
|
+
next: Partial<GridState> | undefined,
|
|
128
|
+
previous: Partial<GridState> | undefined,
|
|
129
|
+
): Partial<GridState> | undefined {
|
|
130
|
+
if (next === undefined || previous === undefined || next === previous) {
|
|
131
|
+
return next;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const keys = Object.keys(next);
|
|
135
|
+
const stabilized: Record<string, unknown> = {};
|
|
136
|
+
let changed = keys.length !== Object.keys(previous).length;
|
|
137
|
+
|
|
138
|
+
for (const key of keys) {
|
|
139
|
+
const nextValue = (next as Record<string, unknown>)[key];
|
|
140
|
+
const previousValue = (previous as Record<string, unknown>)[key];
|
|
141
|
+
if (hasOwn(previous, key) && sameStateValue(nextValue, previousValue)) {
|
|
142
|
+
// Unchanged contents: keep the object the table already holds.
|
|
143
|
+
stabilized[key] = previousValue;
|
|
144
|
+
} else {
|
|
145
|
+
stabilized[key] = nextValue;
|
|
146
|
+
changed = true;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return changed ? (stabilized as Partial<GridState>) : previous;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Controlled slices without a write path: `state.x` is set but no `onXChange`
|
|
155
|
+
* was passed and no external atom owns the slice. TanStack routes every write
|
|
156
|
+
* through the callback, so such a slice cannot change. Reported so the hook
|
|
157
|
+
* can warn in development; `initialState` is the option for a starting value.
|
|
158
|
+
*/
|
|
159
|
+
export function findFrozenStateSlices(
|
|
160
|
+
options: ControlledOptions,
|
|
161
|
+
): Array<{ slice: TMDataGridControlledSlice; handler: string }> {
|
|
162
|
+
const state = options.state;
|
|
163
|
+
if (state === undefined) return [];
|
|
164
|
+
|
|
165
|
+
return (
|
|
166
|
+
Object.entries(CONTROLLED_SLICE_HANDLERS) as Array<
|
|
167
|
+
[TMDataGridControlledSlice, string]
|
|
168
|
+
>
|
|
169
|
+
)
|
|
170
|
+
.filter(
|
|
171
|
+
([slice, handler]) =>
|
|
172
|
+
// Undefined-valued keys are removed before the table sees them; the
|
|
173
|
+
// slice is not controlled.
|
|
174
|
+
(state as Record<string, unknown>)[slice] !== undefined &&
|
|
175
|
+
(options as Record<string, unknown>)[handler] === undefined &&
|
|
176
|
+
options.atoms?.[slice] === undefined,
|
|
177
|
+
)
|
|
178
|
+
.map(([slice, handler]) => ({ slice, handler }));
|
|
179
|
+
}
|
|
@@ -308,14 +308,53 @@ export type TMDataGridRowDeleteArgs<TData extends RowData> = {
|
|
|
308
308
|
* Rows still open (undecided form state) are not in here and stay open.
|
|
309
309
|
*/
|
|
310
310
|
export type TMDataGridSaveDraftsArgs<TData extends RowData> = {
|
|
311
|
-
/**
|
|
312
|
-
|
|
313
|
-
/**
|
|
314
|
-
|
|
311
|
+
/** Committed edits to existing rows; each entry carries its `rowId`. */
|
|
312
|
+
updated: Array<TMDataGridEditCommitArgs<TData>>;
|
|
313
|
+
/** Committed new rows from the entry block; each entry carries its `tempId`. */
|
|
314
|
+
created: Array<TMDataGridRowAddArgs<TData>>;
|
|
315
315
|
/** Ids marked deleted while the drafts accumulated. */
|
|
316
316
|
deleted: Array<string>;
|
|
317
|
+
/** @deprecated Renamed to {@link updated}. Removed in a later beta. */
|
|
318
|
+
rows: Array<TMDataGridEditCommitArgs<TData>>;
|
|
319
|
+
/** @deprecated Renamed to {@link created}. Removed in a later beta. */
|
|
320
|
+
added: Array<TMDataGridRowAddArgs<TData>>;
|
|
317
321
|
};
|
|
318
322
|
|
|
323
|
+
/**
|
|
324
|
+
* Which entries of one bucket saved. `true`, or an id the map does not name,
|
|
325
|
+
* saved and is dropped from the draft store; `false` failed and keeps its
|
|
326
|
+
* draft. A bare boolean answers for the whole bucket.
|
|
327
|
+
*/
|
|
328
|
+
export type TMDataGridSaveOutcomes = boolean | Record<string, boolean>;
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* What `onSaveDrafts` may return to save part of the store.
|
|
332
|
+
*
|
|
333
|
+
* Returning nothing saves everything, and throwing saves nothing. Between
|
|
334
|
+
* those, name the ids that failed: they keep their drafts, committed and
|
|
335
|
+
* ready for the next save, while the rest are dropped. The grid marks them
|
|
336
|
+
* with nothing beyond the state itself - a failed edit keeps `data-draft`,
|
|
337
|
+
* a failed deletion keeps `data-deleted` - so the display is the consumer's.
|
|
338
|
+
*/
|
|
339
|
+
export type TMDataGridSaveDraftsResult = {
|
|
340
|
+
/** Keyed by `rowId`. */
|
|
341
|
+
updated?: TMDataGridSaveOutcomes;
|
|
342
|
+
/** Keyed by `tempId`. */
|
|
343
|
+
created?: TMDataGridSaveOutcomes;
|
|
344
|
+
/** Keyed by `rowId`. */
|
|
345
|
+
deleted?: TMDataGridSaveOutcomes;
|
|
346
|
+
};
|
|
347
|
+
|
|
348
|
+
/** Whether one id of a bucket saved. Unnamed ids saved. */
|
|
349
|
+
function isSaved(
|
|
350
|
+
outcomes: TMDataGridSaveOutcomes | undefined,
|
|
351
|
+
id: string,
|
|
352
|
+
): boolean {
|
|
353
|
+
if (outcomes === undefined) return true;
|
|
354
|
+
if (typeof outcomes === "boolean") return outcomes;
|
|
355
|
+
return outcomes[id] !== false;
|
|
356
|
+
}
|
|
357
|
+
|
|
319
358
|
/**
|
|
320
359
|
* @deprecated Renamed to {@link TMDataGridSaveDraftsArgs} - the payload is
|
|
321
360
|
* the draft store being saved, not a commit. Removed in a later beta.
|
|
@@ -334,7 +373,10 @@ export type TMDataGridEditEngineContext = {
|
|
|
334
373
|
) => void | Promise<void>;
|
|
335
374
|
onSaveDrafts?: (
|
|
336
375
|
args: TMDataGridSaveDraftsArgs<TMDataGridRowData>,
|
|
337
|
-
) =>
|
|
376
|
+
) =>
|
|
377
|
+
| void
|
|
378
|
+
| TMDataGridSaveDraftsResult
|
|
379
|
+
| Promise<void | TMDataGridSaveDraftsResult>;
|
|
338
380
|
/**
|
|
339
381
|
* Seed values for `addRow`, under the values it is called with. A function
|
|
340
382
|
* is called per added row.
|
|
@@ -1192,15 +1234,24 @@ export function createEditEngine(
|
|
|
1192
1234
|
return results.every(Boolean);
|
|
1193
1235
|
};
|
|
1194
1236
|
|
|
1195
|
-
|
|
1237
|
+
/**
|
|
1238
|
+
* The in-flight save. A second call while `onSaveDrafts` awaits would
|
|
1239
|
+
* re-collect the same payload and send it again - a double-clicked Save
|
|
1240
|
+
* would create every pending entry row twice - so concurrent calls join
|
|
1241
|
+
* this promise instead of starting a save of their own.
|
|
1242
|
+
*/
|
|
1243
|
+
let saveInFlight: Promise<boolean> | null = null;
|
|
1244
|
+
|
|
1245
|
+
const saveDrafts = (): Promise<boolean> => {
|
|
1246
|
+
if (saveInFlight !== null) return saveInFlight;
|
|
1196
1247
|
// While this runs, `commit` really commits - under draft it parks
|
|
1197
1248
|
// otherwise. Single-threaded flag, same idiom as the collectors.
|
|
1198
1249
|
savingDrafts = true;
|
|
1199
|
-
|
|
1200
|
-
return await saveDraftsInner();
|
|
1201
|
-
} finally {
|
|
1250
|
+
saveInFlight = saveDraftsInner().finally(() => {
|
|
1202
1251
|
savingDrafts = false;
|
|
1203
|
-
|
|
1252
|
+
saveInFlight = null;
|
|
1253
|
+
});
|
|
1254
|
+
return saveInFlight;
|
|
1204
1255
|
};
|
|
1205
1256
|
|
|
1206
1257
|
const saveDraftsInner = async (): Promise<boolean> => {
|
|
@@ -1248,21 +1299,47 @@ export function createEditEngine(
|
|
|
1248
1299
|
}
|
|
1249
1300
|
const deleted = deletedIds;
|
|
1250
1301
|
if (collected.length > 0 || added.length > 0 || deleted.length > 0) {
|
|
1302
|
+
let result: void | TMDataGridSaveDraftsResult;
|
|
1251
1303
|
try {
|
|
1252
|
-
await getContext().onSaveDrafts?.({
|
|
1304
|
+
result = await getContext().onSaveDrafts?.({
|
|
1305
|
+
updated: collected,
|
|
1306
|
+
created: added,
|
|
1307
|
+
deleted,
|
|
1308
|
+
// The pre-2.0 names, still filled - see TMDataGridSaveDraftsArgs.
|
|
1309
|
+
rows: collected,
|
|
1310
|
+
added,
|
|
1311
|
+
});
|
|
1253
1312
|
} catch {
|
|
1254
1313
|
return false;
|
|
1255
1314
|
}
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1315
|
+
|
|
1316
|
+
// Nothing returned saves the lot. A result names what failed; those
|
|
1317
|
+
// keep their drafts, committed, so the next save retries them.
|
|
1318
|
+
const outcomes = result ?? {};
|
|
1319
|
+
let savedAll = true;
|
|
1320
|
+
|
|
1321
|
+
for (const args of collected) {
|
|
1322
|
+
if (isSaved(outcomes.updated, args.rowId)) drop(args.rowId);
|
|
1323
|
+
else savedAll = false;
|
|
1324
|
+
}
|
|
1325
|
+
for (const args of added) {
|
|
1326
|
+
if (isSaved(outcomes.created, args.tempId)) drop(args.tempId);
|
|
1327
|
+
else savedAll = false;
|
|
1328
|
+
}
|
|
1329
|
+
const savedDeletions = deleted.filter((id) =>
|
|
1330
|
+
isSaved(outcomes.deleted, id),
|
|
1331
|
+
);
|
|
1332
|
+
if (savedDeletions.length > 0) {
|
|
1259
1333
|
store.setState((prev) => ({
|
|
1260
1334
|
...prev,
|
|
1261
1335
|
deletedRowIds: prev.deletedRowIds.filter(
|
|
1262
|
-
(id) => !
|
|
1336
|
+
(id) => !savedDeletions.includes(id),
|
|
1263
1337
|
),
|
|
1264
1338
|
}));
|
|
1265
1339
|
}
|
|
1340
|
+
if (savedDeletions.length < deleted.length) savedAll = false;
|
|
1341
|
+
|
|
1342
|
+
if (!savedAll) return false;
|
|
1266
1343
|
}
|
|
1267
1344
|
return allValid;
|
|
1268
1345
|
};
|
package/src/tmdatagrid/index.ts
CHANGED
|
@@ -83,6 +83,8 @@ export {
|
|
|
83
83
|
type TMDataGridEditChange,
|
|
84
84
|
type TMDataGridEditCommitArgs,
|
|
85
85
|
type TMDataGridSaveDraftsArgs,
|
|
86
|
+
type TMDataGridSaveDraftsResult,
|
|
87
|
+
type TMDataGridSaveOutcomes,
|
|
86
88
|
type TMDataGridEditCommitDraftsArgs,
|
|
87
89
|
type TMDataGridAddRowsOptions,
|
|
88
90
|
type TMDataGridAddRowsResult,
|