@jielga/tmdatagrid 2.0.0-beta.4 → 2.0.0-beta.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +339 -152
- package/dist/index.js +2602 -2450
- package/dist/index.js.map +1 -1
- package/dist/styles.css +1 -1
- package/package.json +1 -1
- package/skills/appearance/SKILL.md +1 -1
- package/skills/cell-selection/SKILL.md +1 -1
- package/skills/columns/SKILL.md +44 -2
- package/skills/data/SKILL.md +14 -10
- package/skills/editing/SKILL.md +140 -68
- package/skills/editing/references/common-mistakes.md +98 -8
- package/skills/editing/references/editing-api.md +40 -30
- package/skills/editing/references/editors-and-validation.md +65 -14
- package/skills/filtering/SKILL.md +1 -1
- package/skills/getting-started/SKILL.md +3 -3
- package/skills/grouping/SKILL.md +5 -3
- package/skills/options/SKILL.md +2 -2
- package/skills/rows/SKILL.md +7 -3
- package/skills/server-side/SKILL.md +1 -1
- package/skills/testing/SKILL.md +1 -1
- package/src/tmdatagrid/components/TMDataGrid.tsx +2 -2
- package/src/tmdatagrid/components/TMDataGridCellEditor.tsx +33 -34
- package/src/tmdatagrid/components/TMDataGridDraftActions.tsx +296 -0
- package/src/tmdatagrid/components/TMDataGridEditColumn.tsx +40 -33
- package/src/tmdatagrid/components/TMDataGridEntryRows.tsx +7 -7
- package/src/tmdatagrid/components/TMDataGridTable.tsx +27 -5
- package/src/tmdatagrid/components/TMDataGridToolbar.module.css +4 -0
- package/src/tmdatagrid/components/TMDataGridToolbar.tsx +8 -10
- package/src/tmdatagrid/components/editors/TMDataGridNumberEditor.tsx +34 -4
- package/src/tmdatagrid/components/editors/editorShared.ts +1 -29
- package/src/tmdatagrid/core/capabilities.ts +14 -6
- package/src/tmdatagrid/core/editEngine.ts +472 -96
- package/src/tmdatagrid/core/labels.ts +3 -3
- package/src/tmdatagrid/core/summary.ts +10 -2
- package/src/tmdatagrid/index.ts +9 -7
- package/src/tmdatagrid/useTMDataGrid.tsx +107 -47
- package/src/tmdatagrid/components/TMDataGridEditActions.tsx +0 -233
|
@@ -43,8 +43,8 @@ const BUILT_IN_EDITORS: Record<
|
|
|
43
43
|
export type TMDataGridCellEditorClose =
|
|
44
44
|
| { committed: true; via: "enter" | "tab" | "shift-tab" | "pick" }
|
|
45
45
|
| { committed: false; via: "escape" }
|
|
46
|
-
/**
|
|
47
|
-
| { committed: false; via: "defer
|
|
46
|
+
/** `cellConfirm`: the editor closed, the draft stays, waiting for its ✓. */
|
|
47
|
+
| { committed: false; via: "defer-tab" | "defer-shift-tab" };
|
|
48
48
|
|
|
49
49
|
/**
|
|
50
50
|
* The host an editing cell mounts - it owns the field, the keys and the blur
|
|
@@ -68,10 +68,10 @@ export function TMDataGridCellEditor({
|
|
|
68
68
|
/** After the editor closed itself - the table moves the focus. */
|
|
69
69
|
onClose: (args: TMDataGridCellEditorClose) => void;
|
|
70
70
|
/**
|
|
71
|
-
* Hosted by the entry block rather than a body cell.
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
*
|
|
71
|
+
* Hosted by the entry block rather than a body cell. An entry row is
|
|
72
|
+
* row-shaped in every mode: Enter enters the whole row, Tab stays the
|
|
73
|
+
* browser's - walking the row's own inputs the way row mode's do - and
|
|
74
|
+
* focus leaving decides nothing, because the ✓ is the decision.
|
|
75
75
|
*/
|
|
76
76
|
inEntryBlock?: boolean;
|
|
77
77
|
}) {
|
|
@@ -79,8 +79,9 @@ export function TMDataGridCellEditor({
|
|
|
79
79
|
// Row mode mounts one host per editable cell of the row; every key that
|
|
80
80
|
// commits or cancels acts on the whole row's form either way, but Tab must
|
|
81
81
|
// stay the browser's (it walks the row's inputs) and blur must do nothing
|
|
82
|
-
// (focus moves between this row's own editors).
|
|
83
|
-
|
|
82
|
+
// (focus moves between this row's own editors). The entry block is built
|
|
83
|
+
// the same way, in every mode, so it follows the same two rules.
|
|
84
|
+
const isRowShaped = features.editMode === "row" || inEntryBlock;
|
|
84
85
|
const column = cell.column;
|
|
85
86
|
const form = edit.getForm(row.id);
|
|
86
87
|
const fieldName = getEditFieldName(column);
|
|
@@ -173,11 +174,11 @@ export function TMDataGridCellEditor({
|
|
|
173
174
|
};
|
|
174
175
|
|
|
175
176
|
/**
|
|
176
|
-
*
|
|
177
|
-
*
|
|
178
|
-
*
|
|
177
|
+
* `cellConfirm`: the key closes the editor and leaves the draft where it
|
|
178
|
+
* is, waiting for the ✓. Moving along the row is not a decision about it;
|
|
179
|
+
* Enter is, and commits instead.
|
|
179
180
|
*/
|
|
180
|
-
const deferAndClose = (via: "defer
|
|
181
|
+
const deferAndClose = (via: "defer-tab" | "defer-shift-tab") => {
|
|
181
182
|
if (closingRef.current) return;
|
|
182
183
|
closingRef.current = true;
|
|
183
184
|
edit.deactivate();
|
|
@@ -185,26 +186,23 @@ export function TMDataGridCellEditor({
|
|
|
185
186
|
};
|
|
186
187
|
|
|
187
188
|
const handleKeyDown = (event: KeyboardEvent<HTMLDivElement>) => {
|
|
188
|
-
const isDraft = features.editMode === "draft";
|
|
189
189
|
if (event.key === "Enter") {
|
|
190
190
|
event.preventDefault();
|
|
191
191
|
event.stopPropagation();
|
|
192
192
|
// Enter is OK, in every mode: the row's form submits and has to pass
|
|
193
|
-
// validation.
|
|
194
|
-
//
|
|
195
|
-
// draft store for `saveDrafts` - so this key does not branch.
|
|
193
|
+
// validation. Where a commit *goes* is `editing.draft`'s business, not
|
|
194
|
+
// this key's - out to the consumer, or into the draft store.
|
|
196
195
|
void commitAndClose("enter");
|
|
197
196
|
} else if (event.key === "Escape") {
|
|
198
197
|
event.preventDefault();
|
|
199
198
|
event.stopPropagation();
|
|
200
199
|
cancelAndClose();
|
|
201
|
-
} else if (event.key === "Tab" && !
|
|
202
|
-
// In the draft entry block Tab stays the browser's: every editable
|
|
203
|
-
// cell's editor is mounted, so it walks the row's inputs as row mode's
|
|
204
|
-
// does.
|
|
200
|
+
} else if (event.key === "Tab" && !isRowShaped) {
|
|
205
201
|
event.preventDefault();
|
|
206
202
|
event.stopPropagation();
|
|
207
|
-
|
|
203
|
+
// `cellConfirm` is the one mode where leaving a cell decides nothing:
|
|
204
|
+
// the caret moves on and the draft waits for its ✓.
|
|
205
|
+
if (features.editMode === "cellConfirm") {
|
|
208
206
|
deferAndClose(event.shiftKey ? "defer-shift-tab" : "defer-tab");
|
|
209
207
|
} else {
|
|
210
208
|
void commitAndClose(event.shiftKey ? "shift-tab" : "tab");
|
|
@@ -214,27 +212,28 @@ export function TMDataGridCellEditor({
|
|
|
214
212
|
|
|
215
213
|
/**
|
|
216
214
|
* Focus left the editor entirely - a click somewhere else. Under `"cell"`
|
|
217
|
-
* that commits (Sheets)
|
|
218
|
-
*
|
|
219
|
-
*
|
|
215
|
+
* that commits (Sheets): a row the user has walked away from is a row they
|
|
216
|
+
* are done typing into, and where the commit lands is `editing.draft`'s
|
|
217
|
+
* business. A commit blocked by validation keeps the form and its invalid
|
|
218
|
+
* marker, with the editor closed.
|
|
220
219
|
*/
|
|
221
220
|
const handleBlur = (event: FocusEvent<HTMLDivElement>) => {
|
|
222
221
|
if (closingRef.current) return;
|
|
223
|
-
// Row mode: the row's other editors are outside this
|
|
224
|
-
// moving between them is not leaving the edit. Blur means
|
|
225
|
-
// the lane's
|
|
226
|
-
if (
|
|
222
|
+
// Row mode and the entry block: the row's other editors are outside this
|
|
223
|
+
// host, and focus moving between them is not leaving the edit. Blur means
|
|
224
|
+
// nothing there; the lane's buttons and the keys end the edit.
|
|
225
|
+
if (isRowShaped) return;
|
|
227
226
|
const next = event.relatedTarget;
|
|
228
227
|
if (next instanceof Node && event.currentTarget.contains(next)) return;
|
|
229
228
|
closingRef.current = true;
|
|
230
|
-
if (features.editMode === "
|
|
231
|
-
|
|
232
|
-
if (!ok) edit.deactivate();
|
|
233
|
-
});
|
|
234
|
-
} else {
|
|
235
|
-
// cellConfirm: the draft stays, dirty-marked, waiting for its ✓.
|
|
229
|
+
if (features.editMode === "cellConfirm") {
|
|
230
|
+
// The draft stays, dirty-marked, waiting for its ✓.
|
|
236
231
|
edit.deactivate();
|
|
232
|
+
return;
|
|
237
233
|
}
|
|
234
|
+
void edit.commit(row.id).then((ok) => {
|
|
235
|
+
if (!ok) edit.deactivate();
|
|
236
|
+
});
|
|
238
237
|
};
|
|
239
238
|
|
|
240
239
|
if (form === undefined || fieldName === null) return null;
|
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
import { Button, Group, Text } from "@mantine/core";
|
|
2
|
+
import { useSelector } from "@tanstack/react-store";
|
|
3
|
+
import { shallow } from "@tanstack/store";
|
|
4
|
+
import type { ReactNode } from "react";
|
|
5
|
+
import { useTMDataGridContext } from "../TMDataGridContext";
|
|
6
|
+
import { getOpenRowIds } from "../core/editEngine";
|
|
7
|
+
import { getDisplayedRows } from "../core/rowSelection";
|
|
8
|
+
import type {
|
|
9
|
+
TMDataGridScrollAlign,
|
|
10
|
+
TMDataGridScrollToRowArgs,
|
|
11
|
+
} from "../useTMDataGrid";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* How many rows the draft store holds - parked edits, parked entry rows and
|
|
15
|
+
* deletion marks. This is what Save sends, so it is what Save counts: a row
|
|
16
|
+
* the user is still typing into is not in here.
|
|
17
|
+
*/
|
|
18
|
+
function useDraftCount(): number {
|
|
19
|
+
const { edit } = useTMDataGridContext();
|
|
20
|
+
return useSelector(
|
|
21
|
+
edit.store,
|
|
22
|
+
(state) =>
|
|
23
|
+
state.committedRowIds.length +
|
|
24
|
+
state.newRows.filter((newRow) => newRow.committed).length +
|
|
25
|
+
state.deletedRowIds.length,
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* How many rows are still open - edited or entered but not parked, so not
|
|
31
|
+
* part of the save. Surfaced beside Save so an undecided row is visible
|
|
32
|
+
* rather than silently left behind.
|
|
33
|
+
*/
|
|
34
|
+
function useOpenCount(): number {
|
|
35
|
+
const { edit } = useTMDataGridContext();
|
|
36
|
+
return useSelector(edit.store, (state) => getOpenRowIds(state).length);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* The ids behind {@link useOpenCount}. Shallow-compared: the array would
|
|
41
|
+
* otherwise be a new one on every edit-store publish, and it is handed to
|
|
42
|
+
* `renderActions`, where it may well end up in a dependency array.
|
|
43
|
+
*/
|
|
44
|
+
function useOpenRowIds(): ReadonlyArray<string> {
|
|
45
|
+
const { edit } = useTMDataGridContext();
|
|
46
|
+
return useSelector(edit.store, getOpenRowIds, { compare: shallow });
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/*
|
|
50
|
+
* Module scope, like the pagination controls and for the same reason: a
|
|
51
|
+
* component defined inside the render body is a new type every render, so
|
|
52
|
+
* React would remount the buttons underneath an interaction.
|
|
53
|
+
*/
|
|
54
|
+
|
|
55
|
+
function EditSaveButton() {
|
|
56
|
+
const { edit, labels } = useTMDataGridContext();
|
|
57
|
+
const draftCount = useDraftCount();
|
|
58
|
+
const isSubmitting = useSelector(edit.store, (state) =>
|
|
59
|
+
state.openRowIds.some((rowId) => state.rows[rowId]?.isSubmitting === true),
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
return (
|
|
63
|
+
<Button
|
|
64
|
+
size="compact-sm"
|
|
65
|
+
disabled={draftCount === 0}
|
|
66
|
+
loading={isSubmitting}
|
|
67
|
+
data-dg-part="save-all"
|
|
68
|
+
data-draft-count={draftCount}
|
|
69
|
+
onClick={() => void edit.saveDrafts()}
|
|
70
|
+
>
|
|
71
|
+
{labels.saveAllEdits(draftCount)}
|
|
72
|
+
</Button>
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** The count of rows left open, or nothing while every row is decided. */
|
|
77
|
+
function EditOpenRowsNote() {
|
|
78
|
+
const { labels } = useTMDataGridContext();
|
|
79
|
+
const openCount = useOpenCount();
|
|
80
|
+
if (openCount === 0) return null;
|
|
81
|
+
return (
|
|
82
|
+
<Text
|
|
83
|
+
size="xs"
|
|
84
|
+
c="dimmed"
|
|
85
|
+
data-dg-part="open-rows-note"
|
|
86
|
+
data-open-count={openCount}
|
|
87
|
+
>
|
|
88
|
+
{labels.editRowsStillOpen(openCount)}
|
|
89
|
+
</Text>
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function EditDiscardButton() {
|
|
94
|
+
const { edit, labels } = useTMDataGridContext();
|
|
95
|
+
// Discard drops everything the grid is holding, open rows included, so it
|
|
96
|
+
// stays live while anything is uncommitted.
|
|
97
|
+
const pendingCount = useDraftCount() + useOpenCount();
|
|
98
|
+
|
|
99
|
+
return (
|
|
100
|
+
<Button
|
|
101
|
+
variant="subtle"
|
|
102
|
+
color="gray"
|
|
103
|
+
size="compact-sm"
|
|
104
|
+
disabled={pendingCount === 0}
|
|
105
|
+
data-dg-part="discard-all"
|
|
106
|
+
onClick={() => edit.cancelAll()}
|
|
107
|
+
>
|
|
108
|
+
{labels.discardAllEdits}
|
|
109
|
+
</Button>
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const EDIT_ACTIONS_CONTROLS: TMDataGridDraftActionsControls = {
|
|
114
|
+
Save: EditSaveButton,
|
|
115
|
+
Discard: EditDiscardButton,
|
|
116
|
+
OpenRowsNote: EditOpenRowsNote,
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
/** What the edit chrome is showing. */
|
|
120
|
+
export type TMDataGridDraftActionsState = {
|
|
121
|
+
/**
|
|
122
|
+
* Rows in the draft store, which is what Save sends: committed edits,
|
|
123
|
+
* committed entry rows and deletion marks.
|
|
124
|
+
*/
|
|
125
|
+
draftCount: number;
|
|
126
|
+
/**
|
|
127
|
+
* Rows still open - edited or entered but not committed, so not part of
|
|
128
|
+
* the save. They stay open across a save.
|
|
129
|
+
*/
|
|
130
|
+
openCount: number;
|
|
131
|
+
/**
|
|
132
|
+
* The ids behind {@link openCount}, in the order the grid opened them - a
|
|
133
|
+
* sort, a filter or a page never moves this list. An entered row appears as
|
|
134
|
+
* its `tempId`.
|
|
135
|
+
*
|
|
136
|
+
* Narrower than `edit.state.openRowIds`, which is every row holding a form,
|
|
137
|
+
* the parked ones included. And note the ordering:
|
|
138
|
+
* {@link TMDataGridDraftActionsActions.scrollToFirstOpenRow} takes "first"
|
|
139
|
+
* in display order, so it need not be `openRowIds[0]`.
|
|
140
|
+
*/
|
|
141
|
+
openRowIds: ReadonlyArray<string>;
|
|
142
|
+
/**
|
|
143
|
+
* @deprecated Was "everything uncommitted", which Save no longer sends.
|
|
144
|
+
* Reads as `draftCount + openCount`; use whichever you meant.
|
|
145
|
+
*/
|
|
146
|
+
pendingCount: number;
|
|
147
|
+
/** Whether a submit is in flight. */
|
|
148
|
+
isSubmitting: boolean;
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
/** What the edit chrome can do. */
|
|
152
|
+
export type TMDataGridDraftActionsActions = {
|
|
153
|
+
/** Saves the draft store. Open rows are left alone. */
|
|
154
|
+
save: () => Promise<boolean>;
|
|
155
|
+
/** Submits every open row, committing the ones that validate. */
|
|
156
|
+
commitAll: () => Promise<boolean>;
|
|
157
|
+
/** Drops everything - open form state and the draft store alike. */
|
|
158
|
+
discard: () => void;
|
|
159
|
+
/**
|
|
160
|
+
* `grid.scrollToRow`, so a control in here can take the user to a row
|
|
161
|
+
* without the grid being threaded down to it.
|
|
162
|
+
*/
|
|
163
|
+
scrollToRow: (args: TMDataGridScrollToRowArgs) => boolean;
|
|
164
|
+
/**
|
|
165
|
+
* Scrolls to the first row still open, taking "first" in display order: the
|
|
166
|
+
* topmost open row under the current sort, filter and page. That need not
|
|
167
|
+
* be {@link TMDataGridDraftActionsState.openRowIds}`[0]`, which is the
|
|
168
|
+
* order the grid opened them in.
|
|
169
|
+
*
|
|
170
|
+
* Answers whether an open row could be reached. `false` when nothing is
|
|
171
|
+
* open, and when every open row is filtered out, on another page or
|
|
172
|
+
* collapsed inside a group. An open entry row answers `true` without
|
|
173
|
+
* scrolling - it is sticky under the header, so it is on screen already -
|
|
174
|
+
* and so does an open row pinned to an edge. The scroll goes through
|
|
175
|
+
* `scrollToRow`, so before `TMDataGrid.Table` has mounted there is nothing
|
|
176
|
+
* to scroll and the answer is `false`.
|
|
177
|
+
*/
|
|
178
|
+
scrollToFirstOpenRow: (align?: TMDataGridScrollAlign) => boolean;
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
/** The pre-bound pieces of the built-in edit chrome. */
|
|
182
|
+
export type TMDataGridDraftActionsControls = {
|
|
183
|
+
/** Save, with the draft count, disabled while the draft store is empty. */
|
|
184
|
+
Save: () => ReactNode;
|
|
185
|
+
/** Discard, disabled while nothing is pending. */
|
|
186
|
+
Discard: () => ReactNode;
|
|
187
|
+
/** The "N rows still being edited" note, or nothing while there are none. */
|
|
188
|
+
OpenRowsNote: () => ReactNode;
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
/** What {@link TMDataGridDraftActionsProps.renderActions} is handed. */
|
|
192
|
+
export type TMDataGridDraftActionsSlotArgs = {
|
|
193
|
+
state: TMDataGridDraftActionsState;
|
|
194
|
+
actions: TMDataGridDraftActionsActions;
|
|
195
|
+
Controls: TMDataGridDraftActionsControls;
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
export type TMDataGridDraftActionsProps = {
|
|
199
|
+
/**
|
|
200
|
+
* Replaces the built-in Save/Discard pair, and is handed the pieces of it.
|
|
201
|
+
*
|
|
202
|
+
* ```tsx
|
|
203
|
+
* <TMDataGrid.DraftActions
|
|
204
|
+
* renderActions={({ state, Controls }) => (
|
|
205
|
+
* <Group>
|
|
206
|
+
* {state.draftCount > 0 && <Badge>{state.draftCount}</Badge>}
|
|
207
|
+
* <Controls.OpenRowsNote />
|
|
208
|
+
* <Controls.Save />
|
|
209
|
+
* <Controls.Discard />
|
|
210
|
+
* </Group>
|
|
211
|
+
* )}
|
|
212
|
+
* />
|
|
213
|
+
* ```
|
|
214
|
+
*/
|
|
215
|
+
renderActions?: (args: TMDataGridDraftActionsSlotArgs) => ReactNode;
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* The draft store's toolbar chrome: Save with the store's count, Discard, and
|
|
220
|
+
* a note counting the rows still open. Save sends the store and leaves open
|
|
221
|
+
* rows alone, so it greys out while nothing is parked however much is being
|
|
222
|
+
* typed - the note is what makes those rows visible.
|
|
223
|
+
*
|
|
224
|
+
* Works under any `editing.mode`. The toolbar is declarative: the grid does
|
|
225
|
+
* not decide for you, so include this only when the grid runs a draft store -
|
|
226
|
+
* without `editing.draft` there is nothing to save and Save stays disabled.
|
|
227
|
+
*
|
|
228
|
+
* ```tsx
|
|
229
|
+
* <TMDataGrid.Toolbar>
|
|
230
|
+
* <TMDataGrid.SummaryCount />
|
|
231
|
+
* <TMDataGrid.Spacer />
|
|
232
|
+
* <TMDataGrid.DraftActions />
|
|
233
|
+
* </TMDataGrid.Toolbar>
|
|
234
|
+
* ```
|
|
235
|
+
*/
|
|
236
|
+
export function TMDataGridDraftActions({
|
|
237
|
+
renderActions,
|
|
238
|
+
}: TMDataGridDraftActionsProps = {}) {
|
|
239
|
+
const { edit, features, table, scrollToRow } = useTMDataGridContext();
|
|
240
|
+
const draftCount = useDraftCount();
|
|
241
|
+
const openRowIds = useOpenRowIds();
|
|
242
|
+
const isSubmitting = useSelector(edit.store, (state) =>
|
|
243
|
+
state.openRowIds.some((rowId) => state.rows[rowId]?.isSubmitting === true),
|
|
244
|
+
);
|
|
245
|
+
|
|
246
|
+
if (!features.editing) return null;
|
|
247
|
+
|
|
248
|
+
if (renderActions) {
|
|
249
|
+
return renderActions({
|
|
250
|
+
state: {
|
|
251
|
+
draftCount,
|
|
252
|
+
openCount: openRowIds.length,
|
|
253
|
+
openRowIds,
|
|
254
|
+
pendingCount: draftCount + openRowIds.length,
|
|
255
|
+
isSubmitting,
|
|
256
|
+
},
|
|
257
|
+
actions: {
|
|
258
|
+
save: () => edit.saveDrafts(),
|
|
259
|
+
commitAll: () => edit.commitAll(),
|
|
260
|
+
discard: () => edit.cancelAll(),
|
|
261
|
+
scrollToRow,
|
|
262
|
+
scrollToFirstOpenRow: (align) => {
|
|
263
|
+
// Read through the store rather than closing over this render's
|
|
264
|
+
// arrays: display order is only decided here, at the click.
|
|
265
|
+
const state = edit.state;
|
|
266
|
+
const openIds = getOpenRowIds(state);
|
|
267
|
+
if (openIds.length === 0) return false;
|
|
268
|
+
const open = new Set(openIds);
|
|
269
|
+
const first = getDisplayedRows(table, features).find((row) =>
|
|
270
|
+
open.has(row.id),
|
|
271
|
+
);
|
|
272
|
+
if (first !== undefined) {
|
|
273
|
+
return scrollToRow({ rowId: first.id, align });
|
|
274
|
+
}
|
|
275
|
+
// Nothing left in the scrolling order, which does not mean nothing
|
|
276
|
+
// is visible: an entry row is sticky under the header, and a pinned
|
|
277
|
+
// row is parked at an edge - `scrollToRow` answers for that one.
|
|
278
|
+
return openIds.some(
|
|
279
|
+
(rowId) =>
|
|
280
|
+
state.newRows.some((newRow) => newRow.tempId === rowId) ||
|
|
281
|
+
scrollToRow({ rowId, align }),
|
|
282
|
+
);
|
|
283
|
+
},
|
|
284
|
+
},
|
|
285
|
+
Controls: EDIT_ACTIONS_CONTROLS,
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
return (
|
|
290
|
+
<Group gap="xs" wrap="nowrap">
|
|
291
|
+
<EditOpenRowsNote />
|
|
292
|
+
<EditSaveButton />
|
|
293
|
+
<EditDiscardButton />
|
|
294
|
+
</Group>
|
|
295
|
+
);
|
|
296
|
+
}
|
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
useTMDataGridContext,
|
|
8
8
|
} from "../TMDataGridContext";
|
|
9
9
|
import type { TMDataGridFeatures } from "../useTMDataGrid";
|
|
10
|
-
import { firstErrorText } from "
|
|
10
|
+
import { firstErrorText } from "../core/editEngine";
|
|
11
11
|
import {
|
|
12
12
|
CheckIcon,
|
|
13
13
|
CloseIcon,
|
|
@@ -20,10 +20,10 @@ import {
|
|
|
20
20
|
export const EDIT_COLUMN_ID = "__edit__";
|
|
21
21
|
|
|
22
22
|
/**
|
|
23
|
-
*
|
|
24
|
-
* the
|
|
25
|
-
* is the styling hook, `data-error` flips it red and puts the
|
|
26
|
-
* tooltip in place of the state's name.
|
|
23
|
+
* The draft store's change marker in the lane - what kind of change the row
|
|
24
|
+
* is holding for the save. Non-interactive; the buttons beside it act.
|
|
25
|
+
* `data-state` is the styling hook, `data-error` flips it red and puts the
|
|
26
|
+
* message in the tooltip in place of the state's name.
|
|
27
27
|
*/
|
|
28
28
|
function RowStateIndicator({
|
|
29
29
|
state,
|
|
@@ -56,13 +56,19 @@ function RowStateIndicator({
|
|
|
56
56
|
}
|
|
57
57
|
|
|
58
58
|
/**
|
|
59
|
-
* One row's slot in the edit lane, driven off the edit store.
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
*
|
|
59
|
+
* One row's slot in the edit lane, driven off the edit store. Two things
|
|
60
|
+
* decide what it holds, and they are the two axes of `editing`:
|
|
61
|
+
*
|
|
62
|
+
* | Row is | Lane holds | From |
|
|
63
|
+
* | ------ | ---------- | ---- |
|
|
64
|
+
* | parked in the draft store | the change marker and the way back out - revert, discard or restore | `editing.draft` |
|
|
65
|
+
* | open | the mode's own controls: Save and Cancel | `editing.mode` |
|
|
66
|
+
* | neither | the pencil (row mode) and the trash | `editing.mode` |
|
|
67
|
+
*
|
|
68
|
+
* A row error turns the Save red with the message in its tooltip - a
|
|
69
|
+
* pathless `.refine()` has nowhere else to land, and a field error outlives
|
|
70
|
+
* the editor that found it. A parked row never offers a save: it has had its
|
|
71
|
+
* submit, and `TMDataGrid.DraftActions` is what sends it.
|
|
66
72
|
*/
|
|
67
73
|
function EditLaneCell<TData extends RowData>({
|
|
68
74
|
row,
|
|
@@ -85,18 +91,25 @@ function EditLaneCell<TData extends RowData>({
|
|
|
85
91
|
state.newRows.find((newRow) => newRow.tempId === rowId)?.committed ===
|
|
86
92
|
true,
|
|
87
93
|
);
|
|
94
|
+
// Parked: the row passed its submit and is waiting for the save. A subset
|
|
95
|
+
// of the open rows, so it is asked about first - the two overlap.
|
|
96
|
+
const isCommitted = useSelector(edit.store, (state) =>
|
|
97
|
+
state.committedRowIds.includes(rowId),
|
|
98
|
+
);
|
|
88
99
|
const isMarkedDeleted = useSelector(edit.store, (state) =>
|
|
89
100
|
state.deletedRowIds.includes(rowId),
|
|
90
101
|
);
|
|
91
102
|
|
|
92
103
|
if (row.getIsGrouped()) return null;
|
|
93
104
|
|
|
94
|
-
const isDraftMode = features.editMode === "draft";
|
|
95
105
|
const hasErrors =
|
|
96
106
|
projection !== undefined &&
|
|
97
107
|
(projection.hasRowError || projection.errorFields.length > 0);
|
|
108
|
+
// The row's own message first - a pathless issue is about the row - then
|
|
109
|
+
// the first field's, which is what a failed commit left behind.
|
|
98
110
|
const rowError = hasErrors
|
|
99
111
|
? (firstErrorText(edit.getForm(rowId)?.state.errors ?? []) ??
|
|
112
|
+
projection.errorMessages[0]?.message ??
|
|
100
113
|
labels.editRowErrors)
|
|
101
114
|
: null;
|
|
102
115
|
|
|
@@ -120,9 +133,9 @@ function EditLaneCell<TData extends RowData>({
|
|
|
120
133
|
</Tooltip>
|
|
121
134
|
) : null;
|
|
122
135
|
|
|
123
|
-
// An entry row: ✓ enters it - an add
|
|
124
|
-
//
|
|
125
|
-
//
|
|
136
|
+
// An entry row: ✓ enters it - an add, or a park under `editing.draft` -
|
|
137
|
+
// and ✕ discards the entry. A parked entry waits for the save as a value
|
|
138
|
+
// row: marked new, re-openable, still discardable.
|
|
126
139
|
if (isNew) {
|
|
127
140
|
if (isConfirmedNew) {
|
|
128
141
|
return (
|
|
@@ -201,7 +214,7 @@ function EditLaneCell<TData extends RowData>({
|
|
|
201
214
|
);
|
|
202
215
|
}
|
|
203
216
|
|
|
204
|
-
// A
|
|
217
|
+
// A deletion mark is a draft like any other - restorable in place.
|
|
205
218
|
if (isMarkedDeleted) {
|
|
206
219
|
return (
|
|
207
220
|
<Group gap={0} wrap="nowrap">
|
|
@@ -235,16 +248,14 @@ function EditLaneCell<TData extends RowData>({
|
|
|
235
248
|
// The pencil means "this row edits"; a row that does not gets nothing.
|
|
236
249
|
if (!edit.canEditRow(row as never)) return null;
|
|
237
250
|
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
const isDirty = (projection?.dirtyFields.length ?? 0) > 0;
|
|
247
|
-
if (!isDirty) return trash;
|
|
251
|
+
if (projection?.isSubmitting) {
|
|
252
|
+
return <Loader size="xs" aria-label={labels.loading} />;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Parked in the draft store: the mark, and the way back out. No trash
|
|
256
|
+
// beside the revert - the two undo-shaped icons were too easy to mistake
|
|
257
|
+
// for one another, so a drafted row reverts first, then deletes.
|
|
258
|
+
if (isCommitted) {
|
|
248
259
|
return (
|
|
249
260
|
<Group gap={0} wrap="nowrap">
|
|
250
261
|
<RowStateIndicator
|
|
@@ -301,10 +312,6 @@ function EditLaneCell<TData extends RowData>({
|
|
|
301
312
|
);
|
|
302
313
|
}
|
|
303
314
|
|
|
304
|
-
if (projection?.isSubmitting) {
|
|
305
|
-
return <Loader size="xs" aria-label={labels.loading} />;
|
|
306
|
-
}
|
|
307
|
-
|
|
308
315
|
const save = (
|
|
309
316
|
<ActionIcon
|
|
310
317
|
variant="subtle"
|
|
@@ -358,11 +365,11 @@ function EditLaneCell<TData extends RowData>({
|
|
|
358
365
|
/**
|
|
359
366
|
* The generated edit lane, appended and pinned right - the row's Save at the
|
|
360
367
|
* end of the row under `mode: "row"`, the state marker and revert under
|
|
361
|
-
* `
|
|
368
|
+
* `editing.draft`, mirroring the checkbox lane's build on the left.
|
|
362
369
|
*/
|
|
363
370
|
export function createEditColumn<TData extends RowData>(
|
|
364
371
|
label = "Edit",
|
|
365
|
-
/**
|
|
372
|
+
/** A draft lane holds three controls where the rest hold two. */
|
|
366
373
|
wide = false,
|
|
367
374
|
): ColumnDef<TMDataGridFeatures, TData, unknown> {
|
|
368
375
|
const width = wide ? 88 : 64;
|
|
@@ -14,7 +14,7 @@ import {
|
|
|
14
14
|
isColumnEditableForRow,
|
|
15
15
|
isControlColumn,
|
|
16
16
|
} from "../core/columnUtils";
|
|
17
|
-
import {
|
|
17
|
+
import type { TMDataGridEditApi } from "../core/editEngine";
|
|
18
18
|
import { focusEditorContent } from "../core/editorFocus";
|
|
19
19
|
import {
|
|
20
20
|
tmDataGridFeatures,
|
|
@@ -34,9 +34,9 @@ type ErasedColumn = Column<TMDataGridFeatures, TMDataGridRowData, unknown>;
|
|
|
34
34
|
function isEntryCellEditable(
|
|
35
35
|
row: Row<TMDataGridFeatures, TMDataGridRowData>,
|
|
36
36
|
column: ErasedColumn,
|
|
37
|
+
edit: TMDataGridEditApi,
|
|
37
38
|
): boolean {
|
|
38
|
-
if (
|
|
39
|
-
if (getEditFieldName(column) === null) return false;
|
|
39
|
+
if (!edit.isColumnEditable(column)) return false;
|
|
40
40
|
return isColumnEditableForRow(column, row);
|
|
41
41
|
}
|
|
42
42
|
|
|
@@ -52,9 +52,9 @@ function isEntryCellEditable(
|
|
|
52
52
|
* validators all apply unchanged. The forms live in the same engine map as
|
|
53
53
|
* every other draft, keyed by their temporary ids.
|
|
54
54
|
*
|
|
55
|
-
* Enter commits the entry
|
|
56
|
-
*
|
|
57
|
-
*
|
|
55
|
+
* Enter commits the entry - `onRowAdd`, or a park under `editing.draft`,
|
|
56
|
+
* where `saveDrafts` reports it in `created`; Escape discards it. The edit
|
|
57
|
+
* lane, when present, offers the same pair as buttons.
|
|
58
58
|
*/
|
|
59
59
|
export function TMDataGridEntryRows({
|
|
60
60
|
orderedColumns,
|
|
@@ -201,7 +201,7 @@ export function TMDataGridEntryRows({
|
|
|
201
201
|
const cell = cellsById.get(column.id);
|
|
202
202
|
const layout = layoutFor(column.id);
|
|
203
203
|
const editable =
|
|
204
|
-
cell !== undefined && isEntryCellEditable(entryRow, column);
|
|
204
|
+
cell !== undefined && isEntryCellEditable(entryRow, column, edit);
|
|
205
205
|
return (
|
|
206
206
|
<div
|
|
207
207
|
key={column.id}
|