@jielga/tmdatagrid 2.0.0-beta.8 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -212
- package/dist/index.d.ts +1323 -796
- package/dist/index.js +4719 -3193
- package/dist/index.js.map +1 -1
- package/dist/styles.css +1 -1
- package/docs/adding-rows.md +132 -0
- package/docs/anatomy.md +119 -0
- package/docs/card-view.md +108 -0
- package/docs/cell-selection.md +194 -0
- package/docs/column-layout.md +182 -0
- package/docs/column-menu.md +66 -0
- package/docs/columns.md +268 -0
- package/docs/components.md +311 -0
- package/docs/draft-store.md +242 -0
- package/docs/editing.md +303 -0
- package/docs/editors.md +250 -0
- package/docs/export.md +319 -0
- package/docs/filtering.md +362 -0
- package/docs/getting-started.md +123 -0
- package/docs/grouping.md +165 -0
- package/docs/loading-and-empty.md +92 -0
- package/docs/localization.md +79 -0
- package/docs/menu.md +143 -0
- package/docs/migrating-to-2.md +163 -0
- package/docs/pagination.md +144 -0
- package/docs/persistence.md +114 -0
- package/docs/portfolio-rebalancer.md +94 -0
- package/docs/query-builder.md +179 -0
- package/docs/quick-search.md +84 -0
- package/docs/row-details.md +115 -0
- package/docs/row-interaction.md +149 -0
- package/docs/row-pinning.md +132 -0
- package/docs/row-selection.md +136 -0
- package/docs/row-styling.md +133 -0
- package/docs/scrolling.md +112 -0
- package/docs/server-query.md +246 -0
- package/docs/server-side.md +206 -0
- package/docs/sorting.md +101 -0
- package/docs/styling.md +126 -0
- package/docs/summary-row.md +76 -0
- package/docs/testing.md +744 -0
- package/docs/toolbar.md +161 -0
- package/docs/use-tm-data-grid.md +361 -0
- package/package.json +22 -46
- package/skills/appearance/SKILL.md +72 -19
- package/skills/cell-selection/SKILL.md +69 -78
- package/skills/columns/SKILL.md +90 -34
- package/skills/data/SKILL.md +86 -16
- package/skills/editing/SKILL.md +83 -50
- package/skills/editing/references/common-mistakes.md +77 -69
- package/skills/editing/references/editing-api.md +31 -23
- package/skills/editing/references/editors-and-validation.md +24 -17
- package/skills/filtering/SKILL.md +148 -40
- package/skills/getting-started/SKILL.md +17 -15
- package/skills/grouping/SKILL.md +31 -16
- package/skills/options/SKILL.md +8 -8
- package/skills/rows/SKILL.md +22 -18
- package/skills/server-side/SKILL.md +170 -17
- package/skills/testing/SKILL.md +150 -32
- package/skills/testing-components/SKILL.md +230 -0
- package/skills/testing-editing/SKILL.md +240 -0
- package/src/{tmdatagrid/TMDataGridContext.ts → TMDataGridContext.ts} +7 -19
- package/src/{tmdatagrid/components → components}/TMDataGrid.module.css +7 -1
- package/src/{tmdatagrid/components → components}/TMDataGrid.tsx +38 -21
- package/src/{tmdatagrid/components → components}/TMDataGridCellEditor.tsx +73 -7
- package/src/{tmdatagrid/components → components}/TMDataGridColumnsPanel.module.css +5 -1
- package/src/{tmdatagrid/components → components}/TMDataGridColumnsPanel.tsx +47 -55
- package/src/{tmdatagrid/components → components}/TMDataGridDetailsColumn.tsx +9 -55
- package/src/{tmdatagrid/components → components}/TMDataGridDraftActions.tsx +41 -24
- package/src/{tmdatagrid/components → components}/TMDataGridEditColumn.tsx +23 -63
- package/src/components/TMDataGridEntryRows.tsx +354 -0
- package/src/components/TMDataGridExportPicker.module.css +77 -0
- package/src/components/TMDataGridExportPicker.tsx +234 -0
- package/src/components/TMDataGridFilterPanel.module.css +54 -0
- package/src/components/TMDataGridFilterPanel.tsx +348 -0
- package/src/{tmdatagrid/components → components}/TMDataGridFilterPills.tsx +15 -8
- package/src/components/TMDataGridFilterSurface.module.css +54 -0
- package/src/components/TMDataGridFilterSurface.tsx +167 -0
- package/src/{tmdatagrid/components → components}/TMDataGridFooter.tsx +53 -90
- package/src/{tmdatagrid/components → components}/TMDataGridGroupColumn.tsx +9 -72
- package/src/{tmdatagrid/components → components}/TMDataGridHeaderCell.module.css +12 -2
- package/src/{tmdatagrid/components → components}/TMDataGridHeaderCell.tsx +58 -21
- package/src/components/TMDataGridHeaderFilterRow.module.css +51 -0
- package/src/components/TMDataGridHeaderFilterRow.tsx +301 -0
- package/src/components/TMDataGridMenu.tsx +357 -0
- package/src/{tmdatagrid/components → components}/TMDataGridSelectColumn.tsx +15 -53
- package/src/{tmdatagrid/components → components}/TMDataGridTable.module.css +88 -65
- package/src/{tmdatagrid/components → components}/TMDataGridTable.tsx +579 -165
- package/src/{tmdatagrid/components → components}/TMDataGridToolbar.module.css +5 -0
- package/src/components/TMDataGridToolbar.tsx +181 -0
- package/src/{tmdatagrid/components → components}/editors/TMDataGridBooleanEditor.tsx +3 -3
- package/src/{tmdatagrid/components → components}/editors/TMDataGridDateEditor.tsx +3 -3
- package/src/{tmdatagrid/components → components}/editors/TMDataGridMultiSelectEditor.tsx +3 -3
- package/src/{tmdatagrid/components → components}/editors/TMDataGridNumberEditor.tsx +3 -3
- package/src/{tmdatagrid/components → components}/editors/TMDataGridSelectEditor.tsx +3 -3
- package/src/{tmdatagrid/components → components}/editors/TMDataGridStringEditor.tsx +3 -3
- package/src/{tmdatagrid/components → components}/editors/editorShared.ts +16 -2
- package/src/{tmdatagrid/components → components}/filters/DgAutocompleteFilter.tsx +5 -4
- package/src/{tmdatagrid/components → components}/filters/DgDateRangeFilter.tsx +22 -5
- package/src/{tmdatagrid/components → components}/filters/DgRangeSliderFilter.tsx +5 -1
- package/src/{tmdatagrid/components → components}/filters/DgTriStateFilter.tsx +5 -1
- package/src/{tmdatagrid/components → components}/filters/TMDataGridFilterValueInput.tsx +44 -28
- package/src/components/filters/controlLayout.ts +32 -0
- package/src/components/filters/filterControlFor.ts +65 -0
- package/src/components/generatedColumns.tsx +187 -0
- package/src/{tmdatagrid/components → components}/icons.ts +1 -0
- package/src/{tmdatagrid/components → components}/sticky.module.css +44 -0
- package/src/components/useHideableColumns.ts +52 -0
- package/src/{tmdatagrid/core → core}/autosize.ts +5 -2
- package/src/{tmdatagrid/core → core}/capabilities.ts +5 -5
- package/src/{tmdatagrid/core → core}/columnOptions.ts +60 -8
- package/src/{tmdatagrid/core → core}/columnOrdering.ts +33 -14
- package/src/{tmdatagrid/core → core}/columnUtils.ts +44 -5
- package/src/core/controlledStateSync.ts +108 -0
- package/src/core/deletedRows.ts +34 -0
- package/src/core/dom.ts +74 -0
- package/src/{tmdatagrid/core → core}/editEngine.ts +1172 -388
- package/src/{tmdatagrid/core → core}/editorFocus.ts +8 -4
- package/src/core/export.ts +704 -0
- package/src/{tmdatagrid/core → core}/filterControls.ts +38 -1
- package/src/{tmdatagrid/core → core}/filterOperators.ts +64 -1
- package/src/core/filterSurface.ts +99 -0
- package/src/{tmdatagrid/core → core}/grouping.ts +21 -0
- package/src/{tmdatagrid/core → core}/labels.ts +51 -6
- package/src/{tmdatagrid/core → core}/labelsSv.ts +27 -6
- package/src/core/pageReset.ts +120 -0
- package/src/core/pagination.ts +81 -0
- package/src/{tmdatagrid/core → core}/persistence.ts +22 -5
- package/src/{tmdatagrid/core → core}/summary.ts +20 -4
- package/src/{tmdatagrid/index.ts → index.ts} +70 -36
- package/src/{tmdatagrid/useTMDataGrid.tsx → useTMDataGrid.tsx} +534 -123
- package/src/useTMDataGridExport.ts +78 -0
- package/src/tmdatagrid/components/TMDataGridEntryRows.tsx +0 -298
- package/src/tmdatagrid/components/TMDataGridFilterPanel.module.css +0 -35
- package/src/tmdatagrid/components/TMDataGridFilterPanel.tsx +0 -354
- package/src/tmdatagrid/components/TMDataGridToolbar.tsx +0 -161
- package/src/tmdatagrid/core/cellExport.ts +0 -320
- /package/src/{tmdatagrid/components → components}/TMDataGridDetailsColumn.module.css +0 -0
- /package/src/{tmdatagrid/components → components}/TMDataGridFilterPills.module.css +0 -0
- /package/src/{tmdatagrid/components → components}/TMDataGridFooter.module.css +0 -0
- /package/src/{tmdatagrid/components → components}/TMDataGridGroupColumn.module.css +0 -0
- /package/src/{tmdatagrid/components → components}/TMDataGridRowNumberColumn.tsx +0 -0
- /package/src/{tmdatagrid/components → components}/TMDataGridSearch.tsx +0 -0
- /package/src/{tmdatagrid/core → core}/cellNavigation.ts +0 -0
- /package/src/{tmdatagrid/core → core}/cellRange.ts +0 -0
- /package/src/{tmdatagrid/core → core}/controlledState.ts +0 -0
- /package/src/{tmdatagrid/core → core}/draftCellContext.ts +0 -0
- /package/src/{tmdatagrid/core → core}/expanding.ts +0 -0
- /package/src/{tmdatagrid/core → core}/matchHighlight.ts +0 -0
- /package/src/{tmdatagrid/core → core}/quickSearch.ts +0 -0
- /package/src/{tmdatagrid/core → core}/resizePreview.ts +0 -0
- /package/src/{tmdatagrid/core → core}/rowPinning.ts +0 -0
- /package/src/{tmdatagrid/core → core}/rowSelection.ts +0 -0
- /package/src/{tmdatagrid/core → core}/sizes.ts +0 -0
- /package/src/{tmdatagrid/core → core}/useSettledTableState.ts +0 -0
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ActionIcon } from "@mantine/core";
|
|
1
|
+
import { ActionIcon, Tooltip } from "@mantine/core";
|
|
2
2
|
import { FieldApi } from "@tanstack/react-form";
|
|
3
3
|
import type { Cell, Row } from "@tanstack/react-table";
|
|
4
4
|
import {
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
} from "react";
|
|
12
12
|
import classes from "./TMDataGridTable.module.css";
|
|
13
13
|
import { type TMDataGridRowData, useTMDataGridContext } from "../TMDataGridContext";
|
|
14
|
+
import { isButtonElement, isInputElement, isNode } from "../core/dom";
|
|
14
15
|
import {
|
|
15
16
|
getEditFieldName,
|
|
16
17
|
normalizeFieldValidate,
|
|
@@ -19,6 +20,7 @@ import {
|
|
|
19
20
|
type TMDataGridEditValueMap,
|
|
20
21
|
} from "../core/editEngine";
|
|
21
22
|
import type { TMDataGridFeatures } from "../useTMDataGrid";
|
|
23
|
+
import { FOCUSABLE_IN_CELL } from "../core/editorFocus";
|
|
22
24
|
import { CheckIcon, CloseIcon } from "./icons";
|
|
23
25
|
import { TMDataGridBooleanEditor } from "./editors/TMDataGridBooleanEditor";
|
|
24
26
|
import { TMDataGridDateEditor } from "./editors/TMDataGridDateEditor";
|
|
@@ -26,6 +28,7 @@ import { TMDataGridMultiSelectEditor } from "./editors/TMDataGridMultiSelectEdit
|
|
|
26
28
|
import { TMDataGridNumberEditor } from "./editors/TMDataGridNumberEditor";
|
|
27
29
|
import { TMDataGridSelectEditor } from "./editors/TMDataGridSelectEditor";
|
|
28
30
|
import { TMDataGridStringEditor } from "./editors/TMDataGridStringEditor";
|
|
31
|
+
import { useFieldError } from "./editors/editorShared";
|
|
29
32
|
|
|
30
33
|
const BUILT_IN_EDITORS: Record<
|
|
31
34
|
string,
|
|
@@ -137,6 +140,17 @@ export function TMDataGridCellEditor({
|
|
|
137
140
|
});
|
|
138
141
|
useEffect(() => field.mount(), [field]);
|
|
139
142
|
|
|
143
|
+
// The field's message, read here rather than in each editor: the tooltip
|
|
144
|
+
// below is the host's, so a column's own `meta.edit.editor` gets it too.
|
|
145
|
+
const error = useFieldError(field);
|
|
146
|
+
// When that tooltip is up, held here rather than left to Tooltip's own
|
|
147
|
+
// `events`: those bring floating-ui's dismiss interaction with them, which
|
|
148
|
+
// puts an `onKeyDown` on the anchor and stops Escape inside the editor
|
|
149
|
+
// before the host's handler below can cancel on it. A controlled `opened`
|
|
150
|
+
// turns the interaction off, so hover and focus are tracked here instead.
|
|
151
|
+
const [messageHovered, setMessageHovered] = useState(false);
|
|
152
|
+
const [messageFocused, setMessageFocused] = useState(false);
|
|
153
|
+
|
|
140
154
|
// Type-to-edit: the seed replaces the value, as it does in a spreadsheet.
|
|
141
155
|
// Only for the types where keystrokes are the input.
|
|
142
156
|
useEffect(() => {
|
|
@@ -186,6 +200,13 @@ export function TMDataGridCellEditor({
|
|
|
186
200
|
};
|
|
187
201
|
|
|
188
202
|
const handleKeyDown = (event: KeyboardEvent<HTMLDivElement>) => {
|
|
203
|
+
// Enter on the ✓ or ✕ is the button's own press, and does what a click on
|
|
204
|
+
// it does: commit and stay, or cancel. Left to the branch below it would
|
|
205
|
+
// commit and move down, and the same button would mean two things.
|
|
206
|
+
if (event.key === "Enter" && isButtonElement(event.target)) {
|
|
207
|
+
event.stopPropagation();
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
189
210
|
if (event.key === "Enter") {
|
|
190
211
|
event.preventDefault();
|
|
191
212
|
event.stopPropagation();
|
|
@@ -201,8 +222,20 @@ export function TMDataGridCellEditor({
|
|
|
201
222
|
event.preventDefault();
|
|
202
223
|
event.stopPropagation();
|
|
203
224
|
// `cellConfirm` is the one mode where leaving a cell decides nothing:
|
|
204
|
-
// the caret moves on and the draft waits for its ✓.
|
|
225
|
+
// the caret moves on and the draft waits for its ✓. The ✓ and ✕ beside
|
|
226
|
+
// the input are the cell's own form, so Tab reaches them first - input,
|
|
227
|
+
// ✓, ✕ - and only past the last of them moves on.
|
|
205
228
|
if (features.editMode === "cellConfirm") {
|
|
229
|
+
const tabbables = Array.from(
|
|
230
|
+
event.currentTarget.querySelectorAll<HTMLElement>(FOCUSABLE_IN_CELL),
|
|
231
|
+
).filter((element) => element.tabIndex >= 0);
|
|
232
|
+
const index = tabbables.indexOf(event.target as HTMLElement);
|
|
233
|
+
const next = tabbables[index + (event.shiftKey ? -1 : 1)];
|
|
234
|
+
if (index >= 0 && next !== undefined) {
|
|
235
|
+
next.focus();
|
|
236
|
+
if (isInputElement(next)) next.select();
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
206
239
|
deferAndClose(event.shiftKey ? "defer-shift-tab" : "defer-tab");
|
|
207
240
|
} else {
|
|
208
241
|
void commitAndClose(event.shiftKey ? "shift-tab" : "tab");
|
|
@@ -214,8 +247,12 @@ export function TMDataGridCellEditor({
|
|
|
214
247
|
* Focus left the editor entirely - a click somewhere else. Under `"cell"`
|
|
215
248
|
* that commits (Sheets): a row the user has walked away from is a row they
|
|
216
249
|
* are done typing into, and where the commit lands is `editing.draft`'s
|
|
217
|
-
* business. A commit
|
|
218
|
-
*
|
|
250
|
+
* business. A refused commit - validation, or `onCommit` rejecting - leaves
|
|
251
|
+
* the editor where it is, invalid, with the value still in it: closed, the
|
|
252
|
+
* cell would render the refused value as if it had landed, and the message
|
|
253
|
+
* would go with the editor. The engine holds the same line when another
|
|
254
|
+
* row is opened over a refused one (see `begin`). The user fixes the value,
|
|
255
|
+
* or Escapes.
|
|
219
256
|
*/
|
|
220
257
|
const handleBlur = (event: FocusEvent<HTMLDivElement>) => {
|
|
221
258
|
if (closingRef.current) return;
|
|
@@ -224,7 +261,7 @@ export function TMDataGridCellEditor({
|
|
|
224
261
|
// nothing there; the lane's buttons and the keys end the edit.
|
|
225
262
|
if (isRowShaped) return;
|
|
226
263
|
const next = event.relatedTarget;
|
|
227
|
-
if (next
|
|
264
|
+
if (isNode(next) && event.currentTarget.contains(next)) return;
|
|
228
265
|
closingRef.current = true;
|
|
229
266
|
if (features.editMode === "cellConfirm") {
|
|
230
267
|
// The draft stays, dirty-marked, waiting for its ✓.
|
|
@@ -232,7 +269,8 @@ export function TMDataGridCellEditor({
|
|
|
232
269
|
return;
|
|
233
270
|
}
|
|
234
271
|
void edit.commit(row.id).then((ok) => {
|
|
235
|
-
|
|
272
|
+
// Open for another try: the next blur or key has to be able to commit.
|
|
273
|
+
if (!ok) closingRef.current = false;
|
|
236
274
|
});
|
|
237
275
|
};
|
|
238
276
|
|
|
@@ -269,7 +307,35 @@ export function TMDataGridCellEditor({
|
|
|
269
307
|
onMouseDown={(event) => event.stopPropagation()}
|
|
270
308
|
onDoubleClick={(event) => event.stopPropagation()}
|
|
271
309
|
>
|
|
272
|
-
|
|
310
|
+
{/* The validation message, on the editor rather than under it: inside a
|
|
311
|
+
narrow column Mantine's inline error wraps, grows the row and is
|
|
312
|
+
clipped, so the input keeps the invalid border and the text comes up
|
|
313
|
+
beside it. The editor gets a box of its own because Tooltip needs a
|
|
314
|
+
child that takes a ref, which a `meta.edit.editor` need not forward. */}
|
|
315
|
+
<Tooltip
|
|
316
|
+
label={error}
|
|
317
|
+
opened={error !== undefined && (messageHovered || messageFocused)}
|
|
318
|
+
withArrow
|
|
319
|
+
position="bottom-start"
|
|
320
|
+
>
|
|
321
|
+
<div
|
|
322
|
+
className={classes.cellEditorField}
|
|
323
|
+
onMouseEnter={() => setMessageHovered(true)}
|
|
324
|
+
onMouseLeave={() => setMessageHovered(false)}
|
|
325
|
+
onFocus={() => setMessageFocused(true)}
|
|
326
|
+
onBlur={(event) => {
|
|
327
|
+
// Focus moving inside the editor - into an open dropdown, say - is
|
|
328
|
+
// not focus leaving it.
|
|
329
|
+
const next = event.relatedTarget;
|
|
330
|
+
if (isNode(next) && event.currentTarget.contains(next)) {
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
setMessageFocused(false);
|
|
334
|
+
}}
|
|
335
|
+
>
|
|
336
|
+
<Editor {...args} />
|
|
337
|
+
</div>
|
|
338
|
+
</Tooltip>
|
|
273
339
|
{/* cellConfirm's chrome: the draft only commits through the ✓ (or
|
|
274
340
|
Enter), so the pair sits right beside the input. */}
|
|
275
341
|
{features.editMode === "cellConfirm" && (
|
|
@@ -12,7 +12,11 @@
|
|
|
12
12
|
display: flex;
|
|
13
13
|
flex-direction: column;
|
|
14
14
|
gap: var(--mantine-spacing-sm);
|
|
15
|
-
padding:
|
|
15
|
+
padding: var(--mantine-spacing-sm);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
.columnsPanelSearch + * .columnsPanelList {
|
|
19
|
+
padding-top: 0;
|
|
16
20
|
}
|
|
17
21
|
|
|
18
22
|
.columnsPanelFooter {
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
|
+
Box,
|
|
3
|
+
type BoxProps,
|
|
2
4
|
Button,
|
|
3
5
|
Checkbox,
|
|
4
6
|
ScrollArea,
|
|
@@ -6,36 +8,42 @@ import {
|
|
|
6
8
|
TextInput,
|
|
7
9
|
Tooltip,
|
|
8
10
|
} from "@mantine/core";
|
|
9
|
-
import { useSelector } from "@tanstack/react-store";
|
|
10
11
|
import { useState } from "react";
|
|
11
12
|
import classes from "./TMDataGridColumnsPanel.module.css";
|
|
12
13
|
import { useTMDataGridContext } from "../TMDataGridContext";
|
|
13
|
-
import {
|
|
14
|
+
import {
|
|
15
|
+
getColumnLabel,
|
|
16
|
+
showColumnSearch,
|
|
17
|
+
type TMDataGridColumnSearchable,
|
|
18
|
+
} from "../core/columnUtils";
|
|
19
|
+
import { useHideableColumns } from "./useHideableColumns";
|
|
14
20
|
import { SearchIcon } from "./icons";
|
|
15
21
|
|
|
16
22
|
/**
|
|
17
|
-
* The "Manage columns" surface
|
|
18
|
-
*
|
|
19
|
-
*
|
|
23
|
+
* The "Manage columns" surface as plain controls, for a host that is not a
|
|
24
|
+
* menu - a Popover, a Drawer, an inline layout. `TMDataGrid.Menu.Columns` is
|
|
25
|
+
* the same chooser as menu items.
|
|
20
26
|
*/
|
|
21
|
-
|
|
22
|
-
|
|
27
|
+
/** Mantine's style props (`w={320}`, `p="sm"`) are set on the panel block. */
|
|
28
|
+
export type TMDataGridColumnsPanelProps = BoxProps & {
|
|
29
|
+
/**
|
|
30
|
+
* The search box over the list: `"auto"` (the default) from six hideable
|
|
31
|
+
* columns, `true` always, `false` never.
|
|
32
|
+
*/
|
|
33
|
+
searchable?: TMDataGridColumnSearchable;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export function TMDataGridColumnsPanel({
|
|
37
|
+
className,
|
|
38
|
+
searchable = "auto",
|
|
39
|
+
...others
|
|
40
|
+
}: TMDataGridColumnsPanelProps = {}) {
|
|
41
|
+
const { labels, controlSize, resetSettings } = useTMDataGridContext();
|
|
23
42
|
const [search, setSearch] = useState("");
|
|
24
43
|
|
|
25
|
-
const columnVisibility =
|
|
26
|
-
|
|
27
|
-
(state) => state.columnVisibility,
|
|
28
|
-
);
|
|
44
|
+
const { columns, columnVisibility, shownCount, setAllVisible } =
|
|
45
|
+
useHideableColumns();
|
|
29
46
|
|
|
30
|
-
// Only what can actually be hidden. A column with `enableHiding: false` is
|
|
31
|
-
// left out rather than listed and disabled: a box that cannot be ticked only
|
|
32
|
-
// invites the question, and every generated lane is one - the checkbox and
|
|
33
|
-
// edit lanes hold the controls the grid needs, the tree column follows the
|
|
34
|
-
// grouping state, the row-number gutter follows `enableRowNumbers`. None of
|
|
35
|
-
// them is a setting.
|
|
36
|
-
const columns = table
|
|
37
|
-
.getAllLeafColumns()
|
|
38
|
-
.filter((column) => column.getCanHide());
|
|
39
47
|
const needle = search.trim().toLowerCase();
|
|
40
48
|
const visibleInPanel = needle
|
|
41
49
|
? columns.filter((column) =>
|
|
@@ -43,41 +51,25 @@ export function TMDataGridColumnsPanel() {
|
|
|
43
51
|
)
|
|
44
52
|
: columns;
|
|
45
53
|
|
|
46
|
-
const shownCount = columns.filter(
|
|
47
|
-
(column) => columnVisibility[column.id] !== false,
|
|
48
|
-
).length;
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* Show or hide every listed column.
|
|
52
|
-
*
|
|
53
|
-
* Not `table.toggleAllColumnsVisible`, which writes a visibility entry for
|
|
54
|
-
* *every* leaf column: showing all would publish the tree column - hidden
|
|
55
|
-
* because nothing is grouped, not because the user hid it - and hiding all
|
|
56
|
-
* would force the same column visible, since it writes `!getCanHide()` for
|
|
57
|
-
* the columns it will not touch. Either way a lane the panel never listed
|
|
58
|
-
* changes state, and persistence then keeps it that way.
|
|
59
|
-
*/
|
|
60
|
-
const setAllVisible = (visible: boolean) => {
|
|
61
|
-
table.setColumnVisibility((previous) => {
|
|
62
|
-
const next = { ...previous };
|
|
63
|
-
for (const column of columns) next[column.id] = visible;
|
|
64
|
-
return next;
|
|
65
|
-
});
|
|
66
|
-
};
|
|
67
|
-
|
|
68
54
|
return (
|
|
69
|
-
<
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
55
|
+
<Box
|
|
56
|
+
data-dg-part="columns-panel"
|
|
57
|
+
className={[classes.columnsPanel, className].filter(Boolean).join(" ")}
|
|
58
|
+
{...others}
|
|
59
|
+
>
|
|
60
|
+
{showColumnSearch(searchable, columns.length) && (
|
|
61
|
+
<div className={classes.columnsPanelSearch}>
|
|
62
|
+
<TextInput
|
|
63
|
+
value={search}
|
|
64
|
+
onChange={(event) => setSearch(event.currentTarget.value)}
|
|
65
|
+
placeholder={labels.columnsSearchPlaceholder}
|
|
66
|
+
leftSection={<SearchIcon size={16} stroke={1.6} />}
|
|
67
|
+
size={controlSize}
|
|
68
|
+
data-dg-part="columns-search"
|
|
69
|
+
data-autofocus
|
|
70
|
+
/>
|
|
71
|
+
</div>
|
|
72
|
+
)}
|
|
81
73
|
|
|
82
74
|
<ScrollArea.Autosize mah={260} type="auto">
|
|
83
75
|
<div className={classes.columnsPanelList}>
|
|
@@ -126,6 +118,6 @@ export function TMDataGridColumnsPanel() {
|
|
|
126
118
|
</Button>
|
|
127
119
|
</Tooltip>
|
|
128
120
|
</div>
|
|
129
|
-
</
|
|
121
|
+
</Box>
|
|
130
122
|
);
|
|
131
123
|
}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { UnstyledButton } from "@mantine/core";
|
|
2
2
|
import {
|
|
3
|
-
|
|
3
|
+
useBodyControlTabIndex,
|
|
4
4
|
useTMDataGridContext,
|
|
5
5
|
} from "../TMDataGridContext";
|
|
6
|
-
import type {
|
|
6
|
+
import type { Row, RowData } from "@tanstack/react-table";
|
|
7
7
|
import { useSelector } from "@tanstack/react-store";
|
|
8
8
|
import classes from "./TMDataGridDetailsColumn.module.css";
|
|
9
9
|
import { ChevronRightIcon } from "./icons";
|
|
@@ -20,16 +20,14 @@ export const DETAILS_COLUMN_ID = "__details__";
|
|
|
20
20
|
* expand, so the React Compiler would cache the call along with it and the
|
|
21
21
|
* chevron would never turn. Same reason the select checkbox subscribes.
|
|
22
22
|
*/
|
|
23
|
-
function DetailsCell<TData extends RowData>({
|
|
23
|
+
export function DetailsCell<TData extends RowData>({
|
|
24
24
|
row,
|
|
25
25
|
}: {
|
|
26
26
|
row: Row<TMDataGridFeatures, TData>;
|
|
27
27
|
}) {
|
|
28
28
|
const { labels } = useTMDataGridContext();
|
|
29
|
+
const tabIndex = useBodyControlTabIndex();
|
|
29
30
|
const expanded = useSelector(row.table.store, () => row.getIsExpanded());
|
|
30
|
-
// See useCellControlTabIndex: a body control is reached by stepping into its
|
|
31
|
-
// cell, not by Tab, or the grid would have one tab stop per mounted row.
|
|
32
|
-
const tabIndex = useCellControlTabIndex();
|
|
33
31
|
|
|
34
32
|
// Group rows open into their children, not into a panel - see the body's
|
|
35
33
|
// `showsDetails`. Their cells are aggregated, so this lane is already blank
|
|
@@ -39,6 +37,8 @@ function DetailsCell<TData extends RowData>({
|
|
|
39
37
|
return (
|
|
40
38
|
<UnstyledButton
|
|
41
39
|
className={classes.detailsToggle}
|
|
40
|
+
// See useBodyControlTabIndex: under cell selection, reached by stepping
|
|
41
|
+
// into the cell or by the Tab walk within the row, not by the page.
|
|
42
42
|
tabIndex={tabIndex}
|
|
43
43
|
aria-expanded={expanded}
|
|
44
44
|
aria-label={expanded ? labels.hideDetails : labels.showDetails}
|
|
@@ -51,7 +51,7 @@ function DetailsCell<TData extends RowData>({
|
|
|
51
51
|
row.toggleExpanded();
|
|
52
52
|
}}
|
|
53
53
|
>
|
|
54
|
-
<span className={classes.chevron} data-expanded={expanded}>
|
|
54
|
+
<span className={classes.chevron} data-expanded={expanded || undefined}>
|
|
55
55
|
<ChevronRightIcon size={16} stroke={1.6} />
|
|
56
56
|
</span>
|
|
57
57
|
</UnstyledButton>
|
|
@@ -67,7 +67,7 @@ function DetailsCell<TData extends RowData>({
|
|
|
67
67
|
* so it would unfold every group as well. Only the data rows are touched here;
|
|
68
68
|
* whatever the tree was showing, it goes on showing. See resolveExpandAll.
|
|
69
69
|
*/
|
|
70
|
-
function DetailsHeader<TData extends RowData>({
|
|
70
|
+
export function DetailsHeader<TData extends RowData>({
|
|
71
71
|
table,
|
|
72
72
|
}: {
|
|
73
73
|
table: TMDataGridTable<TData>;
|
|
@@ -106,55 +106,9 @@ function DetailsHeader<TData extends RowData>({
|
|
|
106
106
|
)
|
|
107
107
|
}
|
|
108
108
|
>
|
|
109
|
-
<span className={classes.chevron} data-expanded={allExpanded}>
|
|
109
|
+
<span className={classes.chevron} data-expanded={allExpanded || undefined}>
|
|
110
110
|
<ChevronRightIcon size={16} stroke={1.6} />
|
|
111
111
|
</span>
|
|
112
112
|
</UnstyledButton>
|
|
113
113
|
);
|
|
114
114
|
}
|
|
115
|
-
|
|
116
|
-
/**
|
|
117
|
-
* The generated details lane, prepended whenever `renderDetails` is set.
|
|
118
|
-
*
|
|
119
|
-
* Structural, like the checkbox and tree columns: fixed width, pinned to the
|
|
120
|
-
* left after both of them, not hideable, not movable and not resizable. Moving
|
|
121
|
-
* or hiding the toggle would leave rows with panels that cannot be opened.
|
|
122
|
-
*
|
|
123
|
-
* Last of the three because it acts on a single record.
|
|
124
|
-
*
|
|
125
|
-
* A second toggle elsewhere is supported: `row.toggleExpanded()` is the entire
|
|
126
|
-
* interface, and this lane is only the control the grid ships.
|
|
127
|
-
*/
|
|
128
|
-
export function createDetailsColumn<TData extends RowData>(
|
|
129
|
-
label = "Details",
|
|
130
|
-
): ColumnDef<TMDataGridFeatures, TData, unknown> {
|
|
131
|
-
return {
|
|
132
|
-
id: DETAILS_COLUMN_ID,
|
|
133
|
-
meta: {
|
|
134
|
-
label,
|
|
135
|
-
align: "center",
|
|
136
|
-
// Structurally the last of the generated lanes.
|
|
137
|
-
enableOrdering: false,
|
|
138
|
-
},
|
|
139
|
-
// A system lane: as wide as the control it holds and no wider. Fixed at
|
|
140
|
-
// every scale - the control does not grow with the font size, so neither
|
|
141
|
-
// should its track.
|
|
142
|
-
size: 36,
|
|
143
|
-
minSize: 36,
|
|
144
|
-
maxSize: 36,
|
|
145
|
-
enableResizing: false,
|
|
146
|
-
enableSorting: false,
|
|
147
|
-
enableColumnFilter: false,
|
|
148
|
-
enableGlobalFilter: false,
|
|
149
|
-
// Its visibility is not the user's to set: hiding it would strand every
|
|
150
|
-
// panel behind a control that is no longer there.
|
|
151
|
-
enableHiding: false,
|
|
152
|
-
// Structurally pinned to the left; users shouldn't be able to move it.
|
|
153
|
-
enablePinning: false,
|
|
154
|
-
header: ({ table }) => <DetailsHeader table={table} />,
|
|
155
|
-
cell: ({ row }) => <DetailsCell row={row} />,
|
|
156
|
-
// Deliberately no `aggregatedCell`: on a group row every cell outside the
|
|
157
|
-
// grouped column counts as aggregated, and blank is the right answer here -
|
|
158
|
-
// groups expand into their rows, not into a panel.
|
|
159
|
-
};
|
|
160
|
-
}
|
|
@@ -3,7 +3,11 @@ import { useSelector } from "@tanstack/react-store";
|
|
|
3
3
|
import { shallow } from "@tanstack/store";
|
|
4
4
|
import type { ReactNode } from "react";
|
|
5
5
|
import { useTMDataGridContext } from "../TMDataGridContext";
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
getOpenRowIds,
|
|
8
|
+
type TMDataGridCommitAllResult,
|
|
9
|
+
type TMDataGridSaveDraftsResult,
|
|
10
|
+
} from "../core/editEngine";
|
|
7
11
|
import { getDisplayedRows } from "../core/rowSelection";
|
|
8
12
|
import type {
|
|
9
13
|
TMDataGridScrollAlign,
|
|
@@ -17,13 +21,20 @@ import type {
|
|
|
17
21
|
*/
|
|
18
22
|
function useDraftCount(): number {
|
|
19
23
|
const { edit } = useTMDataGridContext();
|
|
20
|
-
return useSelector(
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
+
return useSelector(edit.store, (state) => {
|
|
25
|
+
const deleted = new Set(state.deletedRowIds);
|
|
26
|
+
// An edited row that was then marked counts once: Save sends the
|
|
27
|
+
// deletion and leaves the edit behind.
|
|
28
|
+
const edited =
|
|
29
|
+
deleted.size === 0
|
|
30
|
+
? state.committedRowIds.length
|
|
31
|
+
: state.committedRowIds.filter((rowId) => !deleted.has(rowId)).length;
|
|
32
|
+
return (
|
|
33
|
+
edited +
|
|
24
34
|
state.newRows.filter((newRow) => newRow.committed).length +
|
|
25
|
-
|
|
26
|
-
|
|
35
|
+
deleted.size
|
|
36
|
+
);
|
|
37
|
+
});
|
|
27
38
|
}
|
|
28
39
|
|
|
29
40
|
/**
|
|
@@ -55,15 +66,15 @@ function useOpenRowIds(): ReadonlyArray<string> {
|
|
|
55
66
|
function EditSaveButton() {
|
|
56
67
|
const { edit, labels } = useTMDataGridContext();
|
|
57
68
|
const draftCount = useDraftCount();
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
);
|
|
69
|
+
// Save sends the draft store, so its loading state is the save in flight,
|
|
70
|
+
// not an open row's submit.
|
|
71
|
+
const isSaving = useSelector(edit.store, (state) => state.isSaving);
|
|
61
72
|
|
|
62
73
|
return (
|
|
63
74
|
<Button
|
|
64
75
|
size="compact-sm"
|
|
65
76
|
disabled={draftCount === 0}
|
|
66
|
-
loading={
|
|
77
|
+
loading={isSaving}
|
|
67
78
|
data-dg-part="save-all"
|
|
68
79
|
data-draft-count={draftCount}
|
|
69
80
|
onClick={() => void edit.saveDrafts()}
|
|
@@ -94,14 +105,14 @@ function EditDiscardButton() {
|
|
|
94
105
|
const { edit, labels } = useTMDataGridContext();
|
|
95
106
|
// Discard drops everything the grid is holding, open rows included, so it
|
|
96
107
|
// stays live while anything is uncommitted.
|
|
97
|
-
const
|
|
108
|
+
const uncommittedCount = useDraftCount() + useOpenCount();
|
|
98
109
|
|
|
99
110
|
return (
|
|
100
111
|
<Button
|
|
101
112
|
variant="subtle"
|
|
102
113
|
color="gray"
|
|
103
114
|
size="compact-sm"
|
|
104
|
-
disabled={
|
|
115
|
+
disabled={uncommittedCount === 0}
|
|
105
116
|
data-dg-part="discard-all"
|
|
106
117
|
onClick={() => edit.cancelAll()}
|
|
107
118
|
>
|
|
@@ -139,21 +150,26 @@ export type TMDataGridDraftActionsState = {
|
|
|
139
150
|
* in display order, so it need not be `openRowIds[0]`.
|
|
140
151
|
*/
|
|
141
152
|
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. */
|
|
153
|
+
/** Whether a submit is in flight - any open row is submitting. */
|
|
148
154
|
isSubmitting: boolean;
|
|
155
|
+
/** `true` while `saveDrafts` is in flight. */
|
|
156
|
+
isSaving: boolean;
|
|
149
157
|
};
|
|
150
158
|
|
|
151
159
|
/** What the edit chrome can do. */
|
|
152
160
|
export type TMDataGridDraftActionsActions = {
|
|
153
|
-
/**
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
161
|
+
/**
|
|
162
|
+
* Saves the draft store - `edit.saveDrafts()`. Open rows are left alone.
|
|
163
|
+
* Resolves which ids were saved, kept in the draft store for the next
|
|
164
|
+
* save, or reopened with an error; see {@link TMDataGridSaveDraftsResult}.
|
|
165
|
+
*/
|
|
166
|
+
save: () => Promise<TMDataGridSaveDraftsResult>;
|
|
167
|
+
/**
|
|
168
|
+
* Submits every open row, committing the ones that validate -
|
|
169
|
+
* `edit.commitAll()`. Resolves which rows committed and which are still
|
|
170
|
+
* open; see {@link TMDataGridCommitAllResult}.
|
|
171
|
+
*/
|
|
172
|
+
commitAll: () => Promise<TMDataGridCommitAllResult>;
|
|
157
173
|
/** Drops everything - open form state and the draft store alike. */
|
|
158
174
|
discard: () => void;
|
|
159
175
|
/**
|
|
@@ -242,6 +258,7 @@ export function TMDataGridDraftActions({
|
|
|
242
258
|
const isSubmitting = useSelector(edit.store, (state) =>
|
|
243
259
|
state.openRowIds.some((rowId) => state.rows[rowId]?.isSubmitting === true),
|
|
244
260
|
);
|
|
261
|
+
const isSaving = useSelector(edit.store, (state) => state.isSaving);
|
|
245
262
|
|
|
246
263
|
if (!features.editing) return null;
|
|
247
264
|
|
|
@@ -251,8 +268,8 @@ export function TMDataGridDraftActions({
|
|
|
251
268
|
draftCount,
|
|
252
269
|
openCount: openRowIds.length,
|
|
253
270
|
openRowIds,
|
|
254
|
-
pendingCount: draftCount + openRowIds.length,
|
|
255
271
|
isSubmitting,
|
|
272
|
+
isSaving,
|
|
256
273
|
},
|
|
257
274
|
actions: {
|
|
258
275
|
save: () => edit.saveDrafts(),
|