@jielga/tmdatagrid 2.0.0-beta.2 → 2.0.0-beta.3
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 +155 -46
- package/dist/index.js +992 -848
- 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 +44 -22
- package/skills/editing/references/common-mistakes.md +10 -8
- package/skills/editing/references/editing-api.md +20 -13
- 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 +2 -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/TMDataGridCellEditor.tsx +10 -7
- package/src/tmdatagrid/components/TMDataGridEditActions.tsx +96 -25
- package/src/tmdatagrid/components/TMDataGridEditColumn.tsx +1 -1
- package/src/tmdatagrid/components/TMDataGridEntryRows.tsx +18 -18
- package/src/tmdatagrid/core/editEngine.ts +393 -67
- package/src/tmdatagrid/core/labels.ts +8 -1
- package/src/tmdatagrid/core/labelsSv.ts +2 -0
- package/src/tmdatagrid/index.ts +3 -0
- package/src/tmdatagrid/useTMDataGrid.tsx +42 -21
|
@@ -1,22 +1,43 @@
|
|
|
1
|
-
import { Button, Group } from "@mantine/core";
|
|
1
|
+
import { Button, Group, Text } from "@mantine/core";
|
|
2
2
|
import { useSelector } from "@tanstack/react-store";
|
|
3
3
|
import type { ReactNode } from "react";
|
|
4
4
|
import { useTMDataGridContext } from "../TMDataGridContext";
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
|
-
* How many rows
|
|
8
|
-
*
|
|
7
|
+
* How many rows the draft store holds - committed edits, committed entry
|
|
8
|
+
* rows and deletion marks. This is what Save sends, so it is what Save
|
|
9
|
+
* counts: a row the user is still typing into is not in here.
|
|
9
10
|
*/
|
|
10
|
-
function
|
|
11
|
+
function useDraftCount(): number {
|
|
12
|
+
const { edit } = useTMDataGridContext();
|
|
13
|
+
return useSelector(
|
|
14
|
+
edit.store,
|
|
15
|
+
(state) =>
|
|
16
|
+
state.committedRowIds.length +
|
|
17
|
+
state.newRows.filter((newRow) => newRow.committed).length +
|
|
18
|
+
state.deletedRowIds.length,
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* How many rows are still open - edited or entered but not committed, so not
|
|
24
|
+
* part of the save. Surfaced beside Save so an uncommitted row is visible
|
|
25
|
+
* rather than silently left behind.
|
|
26
|
+
*/
|
|
27
|
+
function useOpenCount(): number {
|
|
11
28
|
const { edit } = useTMDataGridContext();
|
|
12
29
|
return useSelector(
|
|
13
30
|
edit.store,
|
|
14
31
|
(state) =>
|
|
15
32
|
state.openRowIds.filter(
|
|
16
33
|
(rowId) =>
|
|
17
|
-
state.
|
|
18
|
-
|
|
19
|
-
|
|
34
|
+
!state.committedRowIds.includes(rowId) &&
|
|
35
|
+
!state.newRows.some(
|
|
36
|
+
(newRow) => newRow.tempId === rowId && newRow.committed,
|
|
37
|
+
) &&
|
|
38
|
+
(state.newRows.some((newRow) => newRow.tempId === rowId) ||
|
|
39
|
+
(state.rows[rowId]?.dirtyFields.length ?? 0) > 0),
|
|
40
|
+
).length,
|
|
20
41
|
);
|
|
21
42
|
}
|
|
22
43
|
|
|
@@ -28,7 +49,7 @@ function usePendingCount(): number {
|
|
|
28
49
|
|
|
29
50
|
function EditSaveButton() {
|
|
30
51
|
const { edit, labels } = useTMDataGridContext();
|
|
31
|
-
const
|
|
52
|
+
const draftCount = useDraftCount();
|
|
32
53
|
const isSubmitting = useSelector(edit.store, (state) =>
|
|
33
54
|
state.openRowIds.some((rowId) => state.rows[rowId]?.isSubmitting === true),
|
|
34
55
|
);
|
|
@@ -36,19 +57,39 @@ function EditSaveButton() {
|
|
|
36
57
|
return (
|
|
37
58
|
<Button
|
|
38
59
|
size="compact-sm"
|
|
39
|
-
disabled={
|
|
60
|
+
disabled={draftCount === 0}
|
|
40
61
|
loading={isSubmitting}
|
|
41
62
|
data-dg-part="save-all"
|
|
42
|
-
|
|
63
|
+
data-draft-count={draftCount}
|
|
64
|
+
onClick={() => void edit.saveDrafts()}
|
|
43
65
|
>
|
|
44
|
-
{labels.saveAllEdits(
|
|
66
|
+
{labels.saveAllEdits(draftCount)}
|
|
45
67
|
</Button>
|
|
46
68
|
);
|
|
47
69
|
}
|
|
48
70
|
|
|
71
|
+
/** The count of rows left open, or nothing while every row is decided. */
|
|
72
|
+
function EditOpenRowsNote() {
|
|
73
|
+
const { labels } = useTMDataGridContext();
|
|
74
|
+
const openCount = useOpenCount();
|
|
75
|
+
if (openCount === 0) return null;
|
|
76
|
+
return (
|
|
77
|
+
<Text
|
|
78
|
+
size="xs"
|
|
79
|
+
c="dimmed"
|
|
80
|
+
data-dg-part="open-rows-note"
|
|
81
|
+
data-open-count={openCount}
|
|
82
|
+
>
|
|
83
|
+
{labels.editRowsStillOpen(openCount)}
|
|
84
|
+
</Text>
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
49
88
|
function EditDiscardButton() {
|
|
50
89
|
const { edit, labels } = useTMDataGridContext();
|
|
51
|
-
|
|
90
|
+
// Discard drops everything the grid is holding, open rows included, so it
|
|
91
|
+
// stays live while anything is uncommitted.
|
|
92
|
+
const pendingCount = useDraftCount() + useOpenCount();
|
|
52
93
|
|
|
53
94
|
return (
|
|
54
95
|
<Button
|
|
@@ -67,11 +108,25 @@ function EditDiscardButton() {
|
|
|
67
108
|
const EDIT_ACTIONS_CONTROLS: TMDataGridEditActionsControls = {
|
|
68
109
|
Save: EditSaveButton,
|
|
69
110
|
Discard: EditDiscardButton,
|
|
111
|
+
OpenRowsNote: EditOpenRowsNote,
|
|
70
112
|
};
|
|
71
113
|
|
|
72
114
|
/** What the edit chrome is showing. */
|
|
73
115
|
export type TMDataGridEditActionsState = {
|
|
74
|
-
/**
|
|
116
|
+
/**
|
|
117
|
+
* Rows in the draft store, which is what Save sends: committed edits,
|
|
118
|
+
* committed entry rows and deletion marks.
|
|
119
|
+
*/
|
|
120
|
+
draftCount: number;
|
|
121
|
+
/**
|
|
122
|
+
* Rows still open - edited or entered but not committed, so not part of
|
|
123
|
+
* the save. They stay open across a save.
|
|
124
|
+
*/
|
|
125
|
+
openCount: number;
|
|
126
|
+
/**
|
|
127
|
+
* @deprecated Was "everything uncommitted", which Save no longer sends.
|
|
128
|
+
* Reads as `draftCount + openCount`; use whichever you meant.
|
|
129
|
+
*/
|
|
75
130
|
pendingCount: number;
|
|
76
131
|
/** Whether a submit is in flight. */
|
|
77
132
|
isSubmitting: boolean;
|
|
@@ -79,18 +134,22 @@ export type TMDataGridEditActionsState = {
|
|
|
79
134
|
|
|
80
135
|
/** What the edit chrome can do. */
|
|
81
136
|
export type TMDataGridEditActionsActions = {
|
|
82
|
-
/**
|
|
137
|
+
/** Saves the draft store. Open rows are left alone. */
|
|
83
138
|
save: () => Promise<boolean>;
|
|
84
|
-
/**
|
|
139
|
+
/** Submits every open row, committing the ones that validate. */
|
|
140
|
+
commitAll: () => Promise<boolean>;
|
|
141
|
+
/** Drops everything - open form state and the draft store alike. */
|
|
85
142
|
discard: () => void;
|
|
86
143
|
};
|
|
87
144
|
|
|
88
145
|
/** The pre-bound pieces of the built-in edit chrome. */
|
|
89
146
|
export type TMDataGridEditActionsControls = {
|
|
90
|
-
/** Save, with the
|
|
147
|
+
/** Save, with the draft count, disabled while the draft store is empty. */
|
|
91
148
|
Save: () => ReactNode;
|
|
92
149
|
/** Discard, disabled while nothing is pending. */
|
|
93
150
|
Discard: () => ReactNode;
|
|
151
|
+
/** The "N rows still being edited" note, or nothing while there are none. */
|
|
152
|
+
OpenRowsNote: () => ReactNode;
|
|
94
153
|
};
|
|
95
154
|
|
|
96
155
|
/** What {@link TMDataGridEditActionsProps.renderActions} is handed. */
|
|
@@ -108,7 +167,8 @@ export type TMDataGridEditActionsProps = {
|
|
|
108
167
|
* <TMDataGrid.EditActions
|
|
109
168
|
* renderActions={({ state, Controls }) => (
|
|
110
169
|
* <Group>
|
|
111
|
-
* {state.
|
|
170
|
+
* {state.draftCount > 0 && <Badge>{state.draftCount}</Badge>}
|
|
171
|
+
* <Controls.OpenRowsNote />
|
|
112
172
|
* <Controls.Save />
|
|
113
173
|
* <Controls.Discard />
|
|
114
174
|
* </Group>
|
|
@@ -120,11 +180,11 @@ export type TMDataGridEditActionsProps = {
|
|
|
120
180
|
};
|
|
121
181
|
|
|
122
182
|
/**
|
|
123
|
-
* Draft mode's toolbar chrome: Save with the
|
|
124
|
-
*
|
|
125
|
-
*
|
|
126
|
-
*
|
|
127
|
-
* nothing while editing is off.
|
|
183
|
+
* Draft mode's toolbar chrome: Save with the draft-store count, Discard, and
|
|
184
|
+
* a note counting rows still open. Save sends the draft store and leaves open
|
|
185
|
+
* rows alone, so it greys out while nothing is committed however much is
|
|
186
|
+
* being typed - the note is what makes those rows visible. Works under any
|
|
187
|
+
* `editing.mode` and renders nothing while editing is off.
|
|
128
188
|
*
|
|
129
189
|
* ```tsx
|
|
130
190
|
* <TMDataGrid.Toolbar>
|
|
@@ -138,7 +198,8 @@ export function TMDataGridEditActions({
|
|
|
138
198
|
renderActions,
|
|
139
199
|
}: TMDataGridEditActionsProps = {}) {
|
|
140
200
|
const { edit, features } = useTMDataGridContext();
|
|
141
|
-
const
|
|
201
|
+
const draftCount = useDraftCount();
|
|
202
|
+
const openCount = useOpenCount();
|
|
142
203
|
const isSubmitting = useSelector(edit.store, (state) =>
|
|
143
204
|
state.openRowIds.some((rowId) => state.rows[rowId]?.isSubmitting === true),
|
|
144
205
|
);
|
|
@@ -147,14 +208,24 @@ export function TMDataGridEditActions({
|
|
|
147
208
|
|
|
148
209
|
if (renderActions) {
|
|
149
210
|
return renderActions({
|
|
150
|
-
state: {
|
|
151
|
-
|
|
211
|
+
state: {
|
|
212
|
+
draftCount,
|
|
213
|
+
openCount,
|
|
214
|
+
pendingCount: draftCount + openCount,
|
|
215
|
+
isSubmitting,
|
|
216
|
+
},
|
|
217
|
+
actions: {
|
|
218
|
+
save: () => edit.saveDrafts(),
|
|
219
|
+
commitAll: () => edit.commitAll(),
|
|
220
|
+
discard: () => edit.cancelAll(),
|
|
221
|
+
},
|
|
152
222
|
Controls: EDIT_ACTIONS_CONTROLS,
|
|
153
223
|
});
|
|
154
224
|
}
|
|
155
225
|
|
|
156
226
|
return (
|
|
157
227
|
<Group gap="xs" wrap="nowrap">
|
|
228
|
+
<EditOpenRowsNote />
|
|
158
229
|
<EditSaveButton />
|
|
159
230
|
<EditDiscardButton />
|
|
160
231
|
</Group>
|
|
@@ -82,7 +82,7 @@ function EditLaneCell<TData extends RowData>({
|
|
|
82
82
|
const isConfirmedNew = useSelector(
|
|
83
83
|
edit.store,
|
|
84
84
|
(state) =>
|
|
85
|
-
state.newRows.find((newRow) => newRow.tempId === rowId)?.
|
|
85
|
+
state.newRows.find((newRow) => newRow.tempId === rowId)?.committed ===
|
|
86
86
|
true,
|
|
87
87
|
);
|
|
88
88
|
const isMarkedDeleted = useSelector(edit.store, (state) =>
|
|
@@ -67,12 +67,12 @@ export function TMDataGridEntryRows({
|
|
|
67
67
|
}) {
|
|
68
68
|
const { table, edit, features } = useTMDataGridContext();
|
|
69
69
|
const newRows = useSelector(edit.store, (state) => state.newRows);
|
|
70
|
-
// The reopen gesture: `begin` on a
|
|
70
|
+
// The reopen gesture: `begin` on a committed entry row flips it back to
|
|
71
71
|
// editors and names the cell double-clicked - where the caret goes.
|
|
72
72
|
const activeEntry = useSelector(edit.store, (state) =>
|
|
73
73
|
state.active !== null &&
|
|
74
74
|
state.newRows.some(
|
|
75
|
-
(newRow) => newRow.tempId === state.active?.rowId && !newRow.
|
|
75
|
+
(newRow) => newRow.tempId === state.active?.rowId && !newRow.committed,
|
|
76
76
|
)
|
|
77
77
|
? state.active
|
|
78
78
|
: null,
|
|
@@ -109,7 +109,7 @@ export function TMDataGridEntryRows({
|
|
|
109
109
|
});
|
|
110
110
|
|
|
111
111
|
/**
|
|
112
|
-
* The caret for a reopen: `begin` on a
|
|
112
|
+
* The caret for a reopen: `begin` on a committed row re-arms its editors
|
|
113
113
|
* and the double-clicked cell's editor should hold the caret, not the
|
|
114
114
|
* row's first. Keyed separately from the add effect above - the row was
|
|
115
115
|
* already focused once when it was added.
|
|
@@ -134,16 +134,16 @@ export function TMDataGridEntryRows({
|
|
|
134
134
|
focusEditorContent(editor);
|
|
135
135
|
});
|
|
136
136
|
|
|
137
|
-
//
|
|
137
|
+
// Open (uncommitted) rows keep the seed values frozen at addRow - the live values
|
|
138
138
|
// belong to the forms, which the editors read directly, and this table only
|
|
139
|
-
// provides row and cell identity. A
|
|
139
|
+
// provides row and cell identity. A committed row's cells render values, so
|
|
140
140
|
// there the draft itself is the row; the memo recomputes on every
|
|
141
|
-
//
|
|
141
|
+
// commit/reopen because either flips `newRows`' identity.
|
|
142
142
|
const data = useMemo(
|
|
143
143
|
() =>
|
|
144
144
|
newRows.map(
|
|
145
|
-
({ tempId,
|
|
146
|
-
((
|
|
145
|
+
({ tempId, committed }) =>
|
|
146
|
+
((committed
|
|
147
147
|
? edit.getForm(tempId)?.state.values
|
|
148
148
|
: edit.getForm(tempId)?.options.defaultValues) ??
|
|
149
149
|
{}) as TMDataGridRowData,
|
|
@@ -161,20 +161,20 @@ export function TMDataGridEntryRows({
|
|
|
161
161
|
if (newRows.length === 0) return null;
|
|
162
162
|
|
|
163
163
|
const entryRows = entryTable.getCoreRowModel().rows;
|
|
164
|
-
const
|
|
165
|
-
newRows.map((newRow) => [newRow.tempId, newRow.
|
|
164
|
+
const committedById = new Map(
|
|
165
|
+
newRows.map((newRow) => [newRow.tempId, newRow.committed]),
|
|
166
166
|
);
|
|
167
167
|
// A row being *typed* into is always sticky - it exists nowhere else to
|
|
168
|
-
// scroll back to. A
|
|
168
|
+
// scroll back to. A committed row joins the scrolling flow unless
|
|
169
169
|
// `newRowsSticky` keeps it pinned, so entering many rows cannot fill the
|
|
170
170
|
// viewport with sticky chrome.
|
|
171
171
|
const stickyRows = entryRows.filter(
|
|
172
172
|
(entryRow) =>
|
|
173
|
-
features.editNewRowsSticky ||
|
|
173
|
+
features.editNewRowsSticky || committedById.get(entryRow.id) !== true,
|
|
174
174
|
);
|
|
175
175
|
const flowRows = features.editNewRowsSticky
|
|
176
176
|
? []
|
|
177
|
-
: entryRows.filter((entryRow) =>
|
|
177
|
+
: entryRows.filter((entryRow) => committedById.get(entryRow.id) === true);
|
|
178
178
|
|
|
179
179
|
const renderEntryRow = (
|
|
180
180
|
entryRow: (typeof entryRows)[number],
|
|
@@ -183,7 +183,7 @@ export function TMDataGridEntryRows({
|
|
|
183
183
|
const cellsById = new Map(
|
|
184
184
|
entryRow.getAllCells().map((cell) => [cell.column.id, cell]),
|
|
185
185
|
);
|
|
186
|
-
const
|
|
186
|
+
const committed = committedById.get(entryRow.id) === true;
|
|
187
187
|
return (
|
|
188
188
|
<div
|
|
189
189
|
key={entryRow.id}
|
|
@@ -191,7 +191,7 @@ export function TMDataGridEntryRows({
|
|
|
191
191
|
data-dg-part="entry-row"
|
|
192
192
|
data-row-id={entryRow.id}
|
|
193
193
|
data-new
|
|
194
|
-
data-
|
|
194
|
+
data-committed={committed}
|
|
195
195
|
className={classes.entryRow}
|
|
196
196
|
>
|
|
197
197
|
{orderedColumns.map((column) => {
|
|
@@ -206,10 +206,10 @@ export function TMDataGridEntryRows({
|
|
|
206
206
|
data-column-id={column.id}
|
|
207
207
|
data-align={getColumnAlign(column)}
|
|
208
208
|
data-control-column={isControlColumn(column.id)}
|
|
209
|
-
// A
|
|
209
|
+
// A committed row re-opens where it is double-clicked, the
|
|
210
210
|
// same gesture a body cell answers.
|
|
211
211
|
onDoubleClick={
|
|
212
|
-
|
|
212
|
+
committed && !isControlColumn(column.id)
|
|
213
213
|
? () =>
|
|
214
214
|
edit.begin({
|
|
215
215
|
rowId: entryRow.id,
|
|
@@ -239,7 +239,7 @@ export function TMDataGridEntryRows({
|
|
|
239
239
|
{cell !== undefined && column.id === EDIT_COLUMN_ID ? (
|
|
240
240
|
// The lane's cell - the entry row's controls.
|
|
241
241
|
flexRender(cell.column.columnDef.cell, cell.getContext())
|
|
242
|
-
) :
|
|
242
|
+
) : committed && cell !== undefined ? (
|
|
243
243
|
// Entered, awaiting Save all: a value row through the
|
|
244
244
|
// columns' own renderers, over the draft the memo above
|
|
245
245
|
// fed this table.
|