@jielga/tmdatagrid 2.0.0-beta.2 → 2.0.0-beta.21
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 +1664 -632
- package/dist/index.js +5226 -3223
- package/dist/index.js.map +1 -1
- package/dist/styles.css +1 -1
- package/docs/anatomy.md +102 -0
- package/docs/cell-selection.md +154 -0
- package/docs/column-layout.md +204 -0
- package/docs/columns.md +262 -0
- package/docs/components.md +304 -0
- package/docs/editing.md +603 -0
- package/docs/editors.md +250 -0
- package/docs/export.md +326 -0
- package/docs/filtering.md +358 -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/pagination.md +144 -0
- package/docs/persistence.md +111 -0
- package/docs/portfolio-rebalancer.md +94 -0
- package/docs/query-builder.md +175 -0
- package/docs/quick-search.md +83 -0
- package/docs/row-details.md +113 -0
- package/docs/row-interaction.md +148 -0
- package/docs/row-pinning.md +132 -0
- package/docs/row-selection.md +134 -0
- package/docs/row-styling.md +133 -0
- package/docs/scrolling.md +111 -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 +309 -0
- package/docs/toolbar.md +161 -0
- package/docs/use-tm-data-grid.md +361 -0
- package/package.json +21 -45
- package/skills/appearance/SKILL.md +70 -17
- package/skills/cell-selection/SKILL.md +70 -76
- package/skills/columns/SKILL.md +131 -32
- package/skills/data/SKILL.md +100 -23
- package/skills/editing/SKILL.md +217 -96
- package/skills/editing/references/common-mistakes.md +111 -24
- package/skills/editing/references/editing-api.md +63 -39
- package/skills/editing/references/editors-and-validation.md +77 -19
- package/skills/filtering/SKILL.md +148 -40
- package/skills/getting-started/SKILL.md +18 -16
- package/skills/grouping/SKILL.md +32 -15
- package/skills/options/SKILL.md +39 -9
- package/skills/rows/SKILL.md +22 -18
- package/skills/server-side/SKILL.md +170 -17
- package/skills/testing/SKILL.md +10 -7
- 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 +39 -23
- package/src/{tmdatagrid/components → components}/TMDataGridCellEditor.tsx +106 -38
- 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 +4 -4
- package/src/components/TMDataGridDraftActions.tsx +307 -0
- package/src/{tmdatagrid/components → components}/TMDataGridEditColumn.tsx +58 -50
- package/src/{tmdatagrid/components → components}/TMDataGridEntryRows.tsx +150 -115
- 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 +7 -5
- package/src/components/TMDataGridFilterSurface.module.css +54 -0
- package/src/components/TMDataGridFilterSurface.tsx +167 -0
- package/src/{tmdatagrid/components → components}/TMDataGridFooter.tsx +53 -13
- package/src/{tmdatagrid/components → components}/TMDataGridGroupColumn.tsx +4 -3
- package/src/{tmdatagrid/components → components}/TMDataGridHeaderCell.module.css +10 -0
- package/src/{tmdatagrid/components → components}/TMDataGridHeaderCell.tsx +100 -28
- package/src/components/TMDataGridHeaderFilterRow.module.css +51 -0
- package/src/components/TMDataGridHeaderFilterRow.tsx +301 -0
- package/src/components/TMDataGridMenu.tsx +354 -0
- package/src/{tmdatagrid/components → components}/TMDataGridSelectColumn.tsx +12 -7
- package/src/{tmdatagrid/components → components}/TMDataGridTable.module.css +90 -67
- package/src/{tmdatagrid/components → components}/TMDataGridTable.tsx +678 -156
- package/src/components/TMDataGridToolbar.module.css +21 -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/components/editors/TMDataGridNumberEditor.tsx +70 -0
- 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 +17 -31
- 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/{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 +14 -6
- package/src/{tmdatagrid/core → core}/columnOptions.ts +46 -0
- package/src/{tmdatagrid/core → core}/columnOrdering.ts +33 -14
- package/src/{tmdatagrid/core → core}/columnUtils.ts +44 -5
- package/src/core/controlledState.ts +179 -0
- package/src/core/controlledStateSync.ts +108 -0
- package/src/core/deletedRows.ts +34 -0
- package/src/core/dom.ts +74 -0
- package/src/core/editEngine.ts +2476 -0
- package/src/{tmdatagrid/core → core}/editorFocus.ts +8 -4
- package/src/core/export.ts +843 -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}/labels.ts +66 -8
- package/src/{tmdatagrid/core → core}/labelsSv.ts +26 -3
- package/src/core/pageReset.ts +120 -0
- package/src/{tmdatagrid/core → core}/persistence.ts +22 -5
- package/src/core/resizePreview.ts +141 -0
- package/src/core/summary.ts +59 -0
- package/src/core/useSettledTableState.ts +36 -0
- package/src/{tmdatagrid/index.ts → index.ts} +75 -12
- package/src/{tmdatagrid/useTMDataGrid.tsx → useTMDataGrid.tsx} +734 -135
- package/src/useTMDataGridExport.ts +78 -0
- package/src/tmdatagrid/components/TMDataGridEditActions.tsx +0 -162
- package/src/tmdatagrid/components/TMDataGridFilterPanel.module.css +0 -35
- package/src/tmdatagrid/components/TMDataGridFilterPanel.tsx +0 -354
- package/src/tmdatagrid/components/TMDataGridToolbar.module.css +0 -12
- package/src/tmdatagrid/components/TMDataGridToolbar.tsx +0 -162
- package/src/tmdatagrid/components/editors/TMDataGridNumberEditor.tsx +0 -40
- package/src/tmdatagrid/core/cellExport.ts +0 -320
- package/src/tmdatagrid/core/editEngine.ts +0 -1006
- package/src/tmdatagrid/core/summary.ts +0 -35
- /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}/draftCellContext.ts +0 -0
- /package/src/{tmdatagrid/core → core}/expanding.ts +0 -0
- /package/src/{tmdatagrid/core → core}/grouping.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}/rowPinning.ts +0 -0
- /package/src/{tmdatagrid/core → core}/rowSelection.ts +0 -0
- /package/src/{tmdatagrid/core → core}/sizes.ts +0 -0
|
@@ -0,0 +1,307 @@
|
|
|
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(edit.store, (state) => {
|
|
21
|
+
const deleted = new Set(state.deletedRowIds);
|
|
22
|
+
// An edited row that was then marked counts once: Save sends the
|
|
23
|
+
// deletion and leaves the edit behind.
|
|
24
|
+
const edited =
|
|
25
|
+
deleted.size === 0
|
|
26
|
+
? state.committedRowIds.length
|
|
27
|
+
: state.committedRowIds.filter((rowId) => !deleted.has(rowId)).length;
|
|
28
|
+
return (
|
|
29
|
+
edited +
|
|
30
|
+
state.newRows.filter((newRow) => newRow.committed).length +
|
|
31
|
+
deleted.size
|
|
32
|
+
);
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* How many rows are still open - edited or entered but not parked, so not
|
|
38
|
+
* part of the save. Surfaced beside Save so an undecided row is visible
|
|
39
|
+
* rather than silently left behind.
|
|
40
|
+
*/
|
|
41
|
+
function useOpenCount(): number {
|
|
42
|
+
const { edit } = useTMDataGridContext();
|
|
43
|
+
return useSelector(edit.store, (state) => getOpenRowIds(state).length);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* The ids behind {@link useOpenCount}. Shallow-compared: the array would
|
|
48
|
+
* otherwise be a new one on every edit-store publish, and it is handed to
|
|
49
|
+
* `renderActions`, where it may well end up in a dependency array.
|
|
50
|
+
*/
|
|
51
|
+
function useOpenRowIds(): ReadonlyArray<string> {
|
|
52
|
+
const { edit } = useTMDataGridContext();
|
|
53
|
+
return useSelector(edit.store, getOpenRowIds, { compare: shallow });
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/*
|
|
57
|
+
* Module scope, like the pagination controls and for the same reason: a
|
|
58
|
+
* component defined inside the render body is a new type every render, so
|
|
59
|
+
* React would remount the buttons underneath an interaction.
|
|
60
|
+
*/
|
|
61
|
+
|
|
62
|
+
function EditSaveButton() {
|
|
63
|
+
const { edit, labels } = useTMDataGridContext();
|
|
64
|
+
const draftCount = useDraftCount();
|
|
65
|
+
// Save sends the draft store, so its loading state is the save in flight,
|
|
66
|
+
// not an open row's submit.
|
|
67
|
+
const isSaving = useSelector(edit.store, (state) => state.isSaving);
|
|
68
|
+
|
|
69
|
+
return (
|
|
70
|
+
<Button
|
|
71
|
+
size="compact-sm"
|
|
72
|
+
disabled={draftCount === 0}
|
|
73
|
+
loading={isSaving}
|
|
74
|
+
data-dg-part="save-all"
|
|
75
|
+
data-draft-count={draftCount}
|
|
76
|
+
onClick={() => void edit.saveDrafts()}
|
|
77
|
+
>
|
|
78
|
+
{labels.saveAllEdits(draftCount)}
|
|
79
|
+
</Button>
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** The count of rows left open, or nothing while every row is decided. */
|
|
84
|
+
function EditOpenRowsNote() {
|
|
85
|
+
const { labels } = useTMDataGridContext();
|
|
86
|
+
const openCount = useOpenCount();
|
|
87
|
+
if (openCount === 0) return null;
|
|
88
|
+
return (
|
|
89
|
+
<Text
|
|
90
|
+
size="xs"
|
|
91
|
+
c="dimmed"
|
|
92
|
+
data-dg-part="open-rows-note"
|
|
93
|
+
data-open-count={openCount}
|
|
94
|
+
>
|
|
95
|
+
{labels.editRowsStillOpen(openCount)}
|
|
96
|
+
</Text>
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function EditDiscardButton() {
|
|
101
|
+
const { edit, labels } = useTMDataGridContext();
|
|
102
|
+
// Discard drops everything the grid is holding, open rows included, so it
|
|
103
|
+
// stays live while anything is uncommitted.
|
|
104
|
+
const pendingCount = useDraftCount() + useOpenCount();
|
|
105
|
+
|
|
106
|
+
return (
|
|
107
|
+
<Button
|
|
108
|
+
variant="subtle"
|
|
109
|
+
color="gray"
|
|
110
|
+
size="compact-sm"
|
|
111
|
+
disabled={pendingCount === 0}
|
|
112
|
+
data-dg-part="discard-all"
|
|
113
|
+
onClick={() => edit.cancelAll()}
|
|
114
|
+
>
|
|
115
|
+
{labels.discardAllEdits}
|
|
116
|
+
</Button>
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const EDIT_ACTIONS_CONTROLS: TMDataGridDraftActionsControls = {
|
|
121
|
+
Save: EditSaveButton,
|
|
122
|
+
Discard: EditDiscardButton,
|
|
123
|
+
OpenRowsNote: EditOpenRowsNote,
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
/** What the edit chrome is showing. */
|
|
127
|
+
export type TMDataGridDraftActionsState = {
|
|
128
|
+
/**
|
|
129
|
+
* Rows in the draft store, which is what Save sends: committed edits,
|
|
130
|
+
* committed entry rows and deletion marks.
|
|
131
|
+
*/
|
|
132
|
+
draftCount: number;
|
|
133
|
+
/**
|
|
134
|
+
* Rows still open - edited or entered but not committed, so not part of
|
|
135
|
+
* the save. They stay open across a save.
|
|
136
|
+
*/
|
|
137
|
+
openCount: number;
|
|
138
|
+
/**
|
|
139
|
+
* The ids behind {@link openCount}, in the order the grid opened them - a
|
|
140
|
+
* sort, a filter or a page never moves this list. An entered row appears as
|
|
141
|
+
* its `tempId`.
|
|
142
|
+
*
|
|
143
|
+
* Narrower than `edit.state.openRowIds`, which is every row holding a form,
|
|
144
|
+
* the parked ones included. And note the ordering:
|
|
145
|
+
* {@link TMDataGridDraftActionsActions.scrollToFirstOpenRow} takes "first"
|
|
146
|
+
* in display order, so it need not be `openRowIds[0]`.
|
|
147
|
+
*/
|
|
148
|
+
openRowIds: ReadonlyArray<string>;
|
|
149
|
+
/**
|
|
150
|
+
* @deprecated Was "everything uncommitted", which Save no longer sends.
|
|
151
|
+
* Reads as `draftCount + openCount`; use whichever you meant.
|
|
152
|
+
*/
|
|
153
|
+
pendingCount: number;
|
|
154
|
+
/** Whether a submit is in flight - any open row is submitting. */
|
|
155
|
+
isSubmitting: boolean;
|
|
156
|
+
/** `true` while `saveDrafts` is in flight. */
|
|
157
|
+
isSaving: boolean;
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
/** What the edit chrome can do. */
|
|
161
|
+
export type TMDataGridDraftActionsActions = {
|
|
162
|
+
/** Saves the draft store. Open rows are left alone. */
|
|
163
|
+
save: () => Promise<boolean>;
|
|
164
|
+
/** Submits every open row, committing the ones that validate. */
|
|
165
|
+
commitAll: () => Promise<boolean>;
|
|
166
|
+
/** Drops everything - open form state and the draft store alike. */
|
|
167
|
+
discard: () => void;
|
|
168
|
+
/**
|
|
169
|
+
* `grid.scrollToRow`, so a control in here can take the user to a row
|
|
170
|
+
* without the grid being threaded down to it.
|
|
171
|
+
*/
|
|
172
|
+
scrollToRow: (args: TMDataGridScrollToRowArgs) => boolean;
|
|
173
|
+
/**
|
|
174
|
+
* Scrolls to the first row still open, taking "first" in display order: the
|
|
175
|
+
* topmost open row under the current sort, filter and page. That need not
|
|
176
|
+
* be {@link TMDataGridDraftActionsState.openRowIds}`[0]`, which is the
|
|
177
|
+
* order the grid opened them in.
|
|
178
|
+
*
|
|
179
|
+
* Answers whether an open row could be reached. `false` when nothing is
|
|
180
|
+
* open, and when every open row is filtered out, on another page or
|
|
181
|
+
* collapsed inside a group. An open entry row answers `true` without
|
|
182
|
+
* scrolling - it is sticky under the header, so it is on screen already -
|
|
183
|
+
* and so does an open row pinned to an edge. The scroll goes through
|
|
184
|
+
* `scrollToRow`, so before `TMDataGrid.Table` has mounted there is nothing
|
|
185
|
+
* to scroll and the answer is `false`.
|
|
186
|
+
*/
|
|
187
|
+
scrollToFirstOpenRow: (align?: TMDataGridScrollAlign) => boolean;
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
/** The pre-bound pieces of the built-in edit chrome. */
|
|
191
|
+
export type TMDataGridDraftActionsControls = {
|
|
192
|
+
/** Save, with the draft count, disabled while the draft store is empty. */
|
|
193
|
+
Save: () => ReactNode;
|
|
194
|
+
/** Discard, disabled while nothing is pending. */
|
|
195
|
+
Discard: () => ReactNode;
|
|
196
|
+
/** The "N rows still being edited" note, or nothing while there are none. */
|
|
197
|
+
OpenRowsNote: () => ReactNode;
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
/** What {@link TMDataGridDraftActionsProps.renderActions} is handed. */
|
|
201
|
+
export type TMDataGridDraftActionsSlotArgs = {
|
|
202
|
+
state: TMDataGridDraftActionsState;
|
|
203
|
+
actions: TMDataGridDraftActionsActions;
|
|
204
|
+
Controls: TMDataGridDraftActionsControls;
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
export type TMDataGridDraftActionsProps = {
|
|
208
|
+
/**
|
|
209
|
+
* Replaces the built-in Save/Discard pair, and is handed the pieces of it.
|
|
210
|
+
*
|
|
211
|
+
* ```tsx
|
|
212
|
+
* <TMDataGrid.DraftActions
|
|
213
|
+
* renderActions={({ state, Controls }) => (
|
|
214
|
+
* <Group>
|
|
215
|
+
* {state.draftCount > 0 && <Badge>{state.draftCount}</Badge>}
|
|
216
|
+
* <Controls.OpenRowsNote />
|
|
217
|
+
* <Controls.Save />
|
|
218
|
+
* <Controls.Discard />
|
|
219
|
+
* </Group>
|
|
220
|
+
* )}
|
|
221
|
+
* />
|
|
222
|
+
* ```
|
|
223
|
+
*/
|
|
224
|
+
renderActions?: (args: TMDataGridDraftActionsSlotArgs) => ReactNode;
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* The draft store's toolbar chrome: Save with the store's count, Discard, and
|
|
229
|
+
* a note counting the rows still open. Save sends the store and leaves open
|
|
230
|
+
* rows alone, so it greys out while nothing is parked however much is being
|
|
231
|
+
* typed - the note is what makes those rows visible.
|
|
232
|
+
*
|
|
233
|
+
* Works under any `editing.mode`. The toolbar is declarative: the grid does
|
|
234
|
+
* not decide for you, so include this only when the grid runs a draft store -
|
|
235
|
+
* without `editing.draft` there is nothing to save and Save stays disabled.
|
|
236
|
+
*
|
|
237
|
+
* ```tsx
|
|
238
|
+
* <TMDataGrid.Toolbar>
|
|
239
|
+
* <TMDataGrid.SummaryCount />
|
|
240
|
+
* <TMDataGrid.Spacer />
|
|
241
|
+
* <TMDataGrid.DraftActions />
|
|
242
|
+
* </TMDataGrid.Toolbar>
|
|
243
|
+
* ```
|
|
244
|
+
*/
|
|
245
|
+
export function TMDataGridDraftActions({
|
|
246
|
+
renderActions,
|
|
247
|
+
}: TMDataGridDraftActionsProps = {}) {
|
|
248
|
+
const { edit, features, table, scrollToRow } = useTMDataGridContext();
|
|
249
|
+
const draftCount = useDraftCount();
|
|
250
|
+
const openRowIds = useOpenRowIds();
|
|
251
|
+
const isSubmitting = useSelector(edit.store, (state) =>
|
|
252
|
+
state.openRowIds.some((rowId) => state.rows[rowId]?.isSubmitting === true),
|
|
253
|
+
);
|
|
254
|
+
const isSaving = useSelector(edit.store, (state) => state.isSaving);
|
|
255
|
+
|
|
256
|
+
if (!features.editing) return null;
|
|
257
|
+
|
|
258
|
+
if (renderActions) {
|
|
259
|
+
return renderActions({
|
|
260
|
+
state: {
|
|
261
|
+
draftCount,
|
|
262
|
+
openCount: openRowIds.length,
|
|
263
|
+
openRowIds,
|
|
264
|
+
pendingCount: draftCount + openRowIds.length,
|
|
265
|
+
isSubmitting,
|
|
266
|
+
isSaving,
|
|
267
|
+
},
|
|
268
|
+
actions: {
|
|
269
|
+
save: () => edit.saveDrafts(),
|
|
270
|
+
commitAll: () => edit.commitAll(),
|
|
271
|
+
discard: () => edit.cancelAll(),
|
|
272
|
+
scrollToRow,
|
|
273
|
+
scrollToFirstOpenRow: (align) => {
|
|
274
|
+
// Read through the store rather than closing over this render's
|
|
275
|
+
// arrays: display order is only decided here, at the click.
|
|
276
|
+
const state = edit.state;
|
|
277
|
+
const openIds = getOpenRowIds(state);
|
|
278
|
+
if (openIds.length === 0) return false;
|
|
279
|
+
const open = new Set(openIds);
|
|
280
|
+
const first = getDisplayedRows(table, features).find((row) =>
|
|
281
|
+
open.has(row.id),
|
|
282
|
+
);
|
|
283
|
+
if (first !== undefined) {
|
|
284
|
+
return scrollToRow({ rowId: first.id, align });
|
|
285
|
+
}
|
|
286
|
+
// Nothing left in the scrolling order, which does not mean nothing
|
|
287
|
+
// is visible: an entry row is sticky under the header, and a pinned
|
|
288
|
+
// row is parked at an edge - `scrollToRow` answers for that one.
|
|
289
|
+
return openIds.some(
|
|
290
|
+
(rowId) =>
|
|
291
|
+
state.newRows.some((newRow) => newRow.tempId === rowId) ||
|
|
292
|
+
scrollToRow({ rowId, align }),
|
|
293
|
+
);
|
|
294
|
+
},
|
|
295
|
+
},
|
|
296
|
+
Controls: EDIT_ACTIONS_CONTROLS,
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
return (
|
|
301
|
+
<Group gap="xs" wrap="nowrap">
|
|
302
|
+
<EditOpenRowsNote />
|
|
303
|
+
<EditSaveButton />
|
|
304
|
+
<EditDiscardButton />
|
|
305
|
+
</Group>
|
|
306
|
+
);
|
|
307
|
+
}
|
|
@@ -3,11 +3,11 @@ import { useSelector } from "@tanstack/react-store";
|
|
|
3
3
|
import type { ColumnDef, Row, RowData } from "@tanstack/react-table";
|
|
4
4
|
import classes from "./TMDataGridTable.module.css";
|
|
5
5
|
import {
|
|
6
|
-
|
|
6
|
+
useBodyControlTabIndex,
|
|
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,23 @@ 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 - or an entry row's ✓ - red with the message in
|
|
69
|
+
* its tooltip: a pathless `.refine()` has nowhere else to land, and a field
|
|
70
|
+
* error outlives the editor that found it. A parked row never offers a save:
|
|
71
|
+
* it has had its submit, and `TMDataGrid.DraftActions` is what sends it.
|
|
72
|
+
*
|
|
73
|
+
* The lane's controls take the tab index every body control does - see
|
|
74
|
+
* useBodyControlTabIndex. The open row's save and cancel are the exception,
|
|
75
|
+
* see where they are built.
|
|
66
76
|
*/
|
|
67
77
|
function EditLaneCell<TData extends RowData>({
|
|
68
78
|
row,
|
|
@@ -70,7 +80,7 @@ function EditLaneCell<TData extends RowData>({
|
|
|
70
80
|
row: Row<TMDataGridFeatures, TData>;
|
|
71
81
|
}) {
|
|
72
82
|
const { edit, features, labels } = useTMDataGridContext();
|
|
73
|
-
const tabIndex =
|
|
83
|
+
const tabIndex = useBodyControlTabIndex();
|
|
74
84
|
const rowId = row.id;
|
|
75
85
|
const isOpen = useSelector(edit.store, (state) =>
|
|
76
86
|
state.openRowIds.includes(rowId),
|
|
@@ -82,21 +92,28 @@ function EditLaneCell<TData extends RowData>({
|
|
|
82
92
|
const isConfirmedNew = useSelector(
|
|
83
93
|
edit.store,
|
|
84
94
|
(state) =>
|
|
85
|
-
state.newRows.find((newRow) => newRow.tempId === rowId)?.
|
|
95
|
+
state.newRows.find((newRow) => newRow.tempId === rowId)?.committed ===
|
|
86
96
|
true,
|
|
87
97
|
);
|
|
98
|
+
// Parked: the row passed its submit and is waiting for the save. A subset
|
|
99
|
+
// of the open rows, so it is asked about first - the two overlap.
|
|
100
|
+
const isCommitted = useSelector(edit.store, (state) =>
|
|
101
|
+
state.committedRowIds.includes(rowId),
|
|
102
|
+
);
|
|
88
103
|
const isMarkedDeleted = useSelector(edit.store, (state) =>
|
|
89
104
|
state.deletedRowIds.includes(rowId),
|
|
90
105
|
);
|
|
91
106
|
|
|
92
107
|
if (row.getIsGrouped()) return null;
|
|
93
108
|
|
|
94
|
-
const isDraftMode = features.editMode === "draft";
|
|
95
109
|
const hasErrors =
|
|
96
110
|
projection !== undefined &&
|
|
97
111
|
(projection.hasRowError || projection.errorFields.length > 0);
|
|
112
|
+
// The row's own message first - a pathless issue is about the row - then
|
|
113
|
+
// the first field's, which is what a failed commit left behind.
|
|
98
114
|
const rowError = hasErrors
|
|
99
115
|
? (firstErrorText(edit.getForm(rowId)?.state.errors ?? []) ??
|
|
116
|
+
projection.errorMessages[0]?.message ??
|
|
100
117
|
labels.editRowErrors)
|
|
101
118
|
: null;
|
|
102
119
|
|
|
@@ -120,9 +137,9 @@ function EditLaneCell<TData extends RowData>({
|
|
|
120
137
|
</Tooltip>
|
|
121
138
|
) : null;
|
|
122
139
|
|
|
123
|
-
// An entry row: ✓ enters it - an add
|
|
124
|
-
//
|
|
125
|
-
//
|
|
140
|
+
// An entry row: ✓ enters it - an add, or a park under `editing.draft` -
|
|
141
|
+
// and ✕ discards the entry. A parked entry waits for the save as a value
|
|
142
|
+
// row: marked new, re-openable, still discardable.
|
|
126
143
|
if (isNew) {
|
|
127
144
|
if (isConfirmedNew) {
|
|
128
145
|
return (
|
|
@@ -169,10 +186,10 @@ function EditLaneCell<TData extends RowData>({
|
|
|
169
186
|
}
|
|
170
187
|
return (
|
|
171
188
|
<Group gap={0} wrap="nowrap">
|
|
172
|
-
<Tooltip label={labels.confirmNewRow} withArrow>
|
|
189
|
+
<Tooltip label={rowError ?? labels.confirmNewRow} withArrow>
|
|
173
190
|
<ActionIcon
|
|
174
191
|
variant="subtle"
|
|
175
|
-
color="green"
|
|
192
|
+
color={hasErrors ? "red" : "green"}
|
|
176
193
|
size="sm"
|
|
177
194
|
tabIndex={tabIndex}
|
|
178
195
|
aria-label={labels.confirmNewRow}
|
|
@@ -201,7 +218,7 @@ function EditLaneCell<TData extends RowData>({
|
|
|
201
218
|
);
|
|
202
219
|
}
|
|
203
220
|
|
|
204
|
-
// A
|
|
221
|
+
// A deletion mark is a draft like any other - restorable in place.
|
|
205
222
|
if (isMarkedDeleted) {
|
|
206
223
|
return (
|
|
207
224
|
<Group gap={0} wrap="nowrap">
|
|
@@ -222,7 +239,7 @@ function EditLaneCell<TData extends RowData>({
|
|
|
222
239
|
data-row-id={rowId}
|
|
223
240
|
onClick={(event) => {
|
|
224
241
|
event.stopPropagation();
|
|
225
|
-
edit.
|
|
242
|
+
edit.restoreRow(rowId);
|
|
226
243
|
}}
|
|
227
244
|
>
|
|
228
245
|
<RestoreIcon size={16} stroke={1.6} />
|
|
@@ -235,16 +252,14 @@ function EditLaneCell<TData extends RowData>({
|
|
|
235
252
|
// The pencil means "this row edits"; a row that does not gets nothing.
|
|
236
253
|
if (!edit.canEditRow(row as never)) return null;
|
|
237
254
|
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
const isDirty = (projection?.dirtyFields.length ?? 0) > 0;
|
|
247
|
-
if (!isDirty) return trash;
|
|
255
|
+
if (projection?.isSubmitting) {
|
|
256
|
+
return <Loader size="xs" aria-label={labels.loading} />;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Parked in the draft store: the mark, and the way back out. No trash
|
|
260
|
+
// beside the revert - the two undo-shaped icons were too easy to mistake
|
|
261
|
+
// for one another, so a drafted row reverts first, then deletes.
|
|
262
|
+
if (isCommitted) {
|
|
248
263
|
return (
|
|
249
264
|
<Group gap={0} wrap="nowrap">
|
|
250
265
|
<RowStateIndicator
|
|
@@ -301,16 +316,15 @@ function EditLaneCell<TData extends RowData>({
|
|
|
301
316
|
);
|
|
302
317
|
}
|
|
303
318
|
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
319
|
+
// An open row is a form, and its save and cancel are the form's last two
|
|
320
|
+
// fields: Tab walks into them from the last editor. The closed lane's
|
|
321
|
+
// controls stay out of the tab order like every other control in a cell.
|
|
308
322
|
const save = (
|
|
309
323
|
<ActionIcon
|
|
310
324
|
variant="subtle"
|
|
311
325
|
color={hasErrors ? "red" : "green"}
|
|
312
326
|
size="sm"
|
|
313
|
-
tabIndex={
|
|
327
|
+
tabIndex={0}
|
|
314
328
|
aria-label={labels.saveRow}
|
|
315
329
|
data-dg-part="save-row"
|
|
316
330
|
data-row-id={rowId}
|
|
@@ -325,21 +339,15 @@ function EditLaneCell<TData extends RowData>({
|
|
|
325
339
|
|
|
326
340
|
return (
|
|
327
341
|
<Group gap={0} wrap="nowrap">
|
|
328
|
-
{rowError
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
</Tooltip>
|
|
332
|
-
) : (
|
|
333
|
-
<Tooltip label={rowError} withArrow>
|
|
334
|
-
{save}
|
|
335
|
-
</Tooltip>
|
|
336
|
-
)}
|
|
342
|
+
<Tooltip label={rowError ?? labels.saveRow} withArrow>
|
|
343
|
+
{save}
|
|
344
|
+
</Tooltip>
|
|
337
345
|
<Tooltip label={labels.cancelRowEdit} withArrow>
|
|
338
346
|
<ActionIcon
|
|
339
347
|
variant="subtle"
|
|
340
348
|
color="gray"
|
|
341
349
|
size="sm"
|
|
342
|
-
tabIndex={
|
|
350
|
+
tabIndex={0}
|
|
343
351
|
aria-label={labels.cancelRowEdit}
|
|
344
352
|
data-dg-part="cancel-row"
|
|
345
353
|
data-row-id={rowId}
|
|
@@ -358,11 +366,11 @@ function EditLaneCell<TData extends RowData>({
|
|
|
358
366
|
/**
|
|
359
367
|
* The generated edit lane, appended and pinned right - the row's Save at the
|
|
360
368
|
* end of the row under `mode: "row"`, the state marker and revert under
|
|
361
|
-
* `
|
|
369
|
+
* `editing.draft`, mirroring the checkbox lane's build on the left.
|
|
362
370
|
*/
|
|
363
371
|
export function createEditColumn<TData extends RowData>(
|
|
364
372
|
label = "Edit",
|
|
365
|
-
/**
|
|
373
|
+
/** A draft lane holds three controls where the rest hold two. */
|
|
366
374
|
wide = false,
|
|
367
375
|
): ColumnDef<TMDataGridFeatures, TData, unknown> {
|
|
368
376
|
const width = wide ? 88 : 64;
|