@jielga/tmdatagrid 2.0.0-beta.0 → 2.0.0-beta.2
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 +32 -34
- package/dist/index.d.ts +164 -102
- package/dist/index.js +2060 -1705
- package/dist/index.js.map +1 -1
- package/dist/styles.css +1 -1
- package/package.json +1 -1
- package/skills/appearance/SKILL.md +37 -34
- package/skills/cell-selection/SKILL.md +19 -19
- package/skills/columns/SKILL.md +9 -9
- package/skills/data/SKILL.md +53 -53
- package/skills/editing/SKILL.md +152 -311
- package/skills/editing/references/common-mistakes.md +177 -0
- package/skills/editing/references/editing-api.md +56 -28
- package/skills/editing/references/editors-and-validation.md +26 -19
- package/skills/filtering/SKILL.md +51 -50
- package/skills/getting-started/SKILL.md +10 -10
- package/skills/grouping/SKILL.md +41 -43
- package/skills/options/SKILL.md +4 -4
- package/skills/rows/SKILL.md +59 -59
- package/skills/rows/references/rows-api.md +7 -7
- package/skills/server-side/SKILL.md +1 -1
- package/skills/testing/SKILL.md +11 -10
- package/src/tmdatagrid/components/TMDataGrid.module.css +13 -1
- package/src/tmdatagrid/components/TMDataGridCellEditor.tsx +20 -8
- package/src/tmdatagrid/components/TMDataGridColumnsPanel.tsx +29 -18
- package/src/tmdatagrid/components/TMDataGridDetailsColumn.tsx +5 -8
- package/src/tmdatagrid/components/TMDataGridEditActions.tsx +2 -2
- package/src/tmdatagrid/components/TMDataGridEditColumn.tsx +246 -86
- package/src/tmdatagrid/components/TMDataGridEntryRows.tsx +176 -77
- package/src/tmdatagrid/components/TMDataGridHeaderCell.tsx +11 -6
- package/src/tmdatagrid/components/TMDataGridSelectColumn.tsx +9 -5
- package/src/tmdatagrid/components/TMDataGridTable.module.css +47 -3
- package/src/tmdatagrid/components/TMDataGridTable.tsx +101 -40
- package/src/tmdatagrid/components/editors/TMDataGridNumberEditor.tsx +1 -1
- package/src/tmdatagrid/components/icons.ts +1 -0
- package/src/tmdatagrid/core/autosize.ts +30 -6
- package/src/tmdatagrid/core/capabilities.ts +15 -5
- package/src/tmdatagrid/core/cellExport.ts +6 -7
- package/src/tmdatagrid/core/cellNavigation.ts +2 -2
- package/src/tmdatagrid/core/cellRange.ts +6 -6
- package/src/tmdatagrid/core/columnOrdering.ts +30 -1
- package/src/tmdatagrid/core/columnUtils.ts +14 -0
- package/src/tmdatagrid/core/draftCellContext.ts +68 -0
- package/src/tmdatagrid/core/editEngine.ts +111 -37
- package/src/tmdatagrid/core/filterOperators.ts +6 -6
- package/src/tmdatagrid/core/labels.ts +13 -1
- package/src/tmdatagrid/core/labelsSv.ts +4 -0
- package/src/tmdatagrid/core/matchHighlight.ts +3 -3
- package/src/tmdatagrid/core/persistence.ts +3 -3
- package/src/tmdatagrid/core/rowSelection.ts +3 -3
- package/src/tmdatagrid/index.ts +3 -1
- package/src/tmdatagrid/useTMDataGrid.tsx +141 -99
|
@@ -41,6 +41,11 @@
|
|
|
41
41
|
}
|
|
42
42
|
|
|
43
43
|
.root {
|
|
44
|
+
/* The frame's corner radius. Any length or Mantine radius token; `0` squares
|
|
45
|
+
the grid off against a card or a full-bleed page. Override it on the grid:
|
|
46
|
+
<TMDataGrid style={{ "--dg-radius": 0 }} />
|
|
47
|
+
The root clips its overflow, so the header and the last row follow it. */
|
|
48
|
+
--dg-radius: var(--mantine-radius-md);
|
|
44
49
|
/* Fallbacks, so the parts still work if rendered outside a sized root. */
|
|
45
50
|
--dg-row-height: 52px;
|
|
46
51
|
--dg-header-height: 44px;
|
|
@@ -66,6 +71,13 @@
|
|
|
66
71
|
var(--mantine-color-text) 3.5%,
|
|
67
72
|
var(--mantine-color-body)
|
|
68
73
|
);
|
|
74
|
+
/* A confirmed entry row under draft mode - entered, awaiting Save all. A
|
|
75
|
+
whisper of green over the body, one declaration for both themes. */
|
|
76
|
+
--dg-row-new-bg: color-mix(
|
|
77
|
+
in srgb,
|
|
78
|
+
var(--mantine-color-green-6) 8%,
|
|
79
|
+
var(--mantine-color-body)
|
|
80
|
+
);
|
|
69
81
|
|
|
70
82
|
/* The stacking ladder, stated once. Body cells sit at auto (0); everything
|
|
71
83
|
sticky stacks over them in this order, and a sticky row crossing a sticky
|
|
@@ -89,6 +101,6 @@
|
|
|
89
101
|
from the host page would centre headers away from their cell content. */
|
|
90
102
|
text-align: left;
|
|
91
103
|
border: 1px solid var(--mantine-color-default-border);
|
|
92
|
-
border-radius: var(--
|
|
104
|
+
border-radius: var(--dg-radius);
|
|
93
105
|
background-color: var(--mantine-color-body);
|
|
94
106
|
}
|
|
@@ -43,7 +43,7 @@ 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
|
-
/**
|
|
46
|
+
/** Draft: the editor closed, the draft stays - Enter parks, Tab moves on. */
|
|
47
47
|
| { committed: false; via: "defer" | "defer-tab" | "defer-shift-tab" };
|
|
48
48
|
|
|
49
49
|
/**
|
|
@@ -59,6 +59,7 @@ export function TMDataGridCellEditor({
|
|
|
59
59
|
row,
|
|
60
60
|
takeSeedText,
|
|
61
61
|
onClose,
|
|
62
|
+
inEntryBlock = false,
|
|
62
63
|
}: {
|
|
63
64
|
cell: Cell<TMDataGridFeatures, TMDataGridRowData, unknown>;
|
|
64
65
|
row: Row<TMDataGridFeatures, TMDataGridRowData>;
|
|
@@ -66,6 +67,13 @@ export function TMDataGridCellEditor({
|
|
|
66
67
|
takeSeedText: () => string | undefined;
|
|
67
68
|
/** After the editor closed itself - the table moves the focus. */
|
|
68
69
|
onClose: (args: TMDataGridCellEditorClose) => void;
|
|
70
|
+
/**
|
|
71
|
+
* Hosted by the entry block rather than a body cell. Under draft the keys
|
|
72
|
+
* change meaning there: Enter confirms the whole entry (the engine parks
|
|
73
|
+
* it), and Tab stays the browser's, walking the entry row's own inputs the
|
|
74
|
+
* way row mode's do.
|
|
75
|
+
*/
|
|
76
|
+
inEntryBlock?: boolean;
|
|
69
77
|
}) {
|
|
70
78
|
const { table, edit, features, labels, controlSize } = useTMDataGridContext();
|
|
71
79
|
// Row mode mounts one host per editable cell of the row; every key that
|
|
@@ -164,7 +172,7 @@ export function TMDataGridCellEditor({
|
|
|
164
172
|
onClose({ committed: false, via: "escape" });
|
|
165
173
|
};
|
|
166
174
|
|
|
167
|
-
/**
|
|
175
|
+
/** Draft: the key closes the editor and leaves the draft for submitAll. */
|
|
168
176
|
const deferAndClose = (via: "defer" | "defer-tab" | "defer-shift-tab") => {
|
|
169
177
|
if (closingRef.current) return;
|
|
170
178
|
closingRef.current = true;
|
|
@@ -173,23 +181,27 @@ export function TMDataGridCellEditor({
|
|
|
173
181
|
};
|
|
174
182
|
|
|
175
183
|
const handleKeyDown = (event: KeyboardEvent<HTMLDivElement>) => {
|
|
176
|
-
const
|
|
184
|
+
const isDraft = features.editMode === "draft";
|
|
177
185
|
if (event.key === "Enter") {
|
|
178
186
|
event.preventDefault();
|
|
179
187
|
event.stopPropagation();
|
|
180
188
|
// In row mode Enter (and Ctrl+Enter, the documented pair) saves the
|
|
181
|
-
// row; in
|
|
182
|
-
// the draft
|
|
183
|
-
|
|
189
|
+
// row; in draft nothing reaches a callback until submitAll, so Enter
|
|
190
|
+
// parks the draft - and in the entry block it confirms the entry, which
|
|
191
|
+
// the engine parks the same way.
|
|
192
|
+
if (isDraft && !inEntryBlock) deferAndClose("defer");
|
|
184
193
|
else void commitAndClose("enter");
|
|
185
194
|
} else if (event.key === "Escape") {
|
|
186
195
|
event.preventDefault();
|
|
187
196
|
event.stopPropagation();
|
|
188
197
|
cancelAndClose();
|
|
189
|
-
} else if (event.key === "Tab" && !isRowMode) {
|
|
198
|
+
} else if (event.key === "Tab" && !isRowMode && !(isDraft && inEntryBlock)) {
|
|
199
|
+
// In the draft entry block Tab stays the browser's: every editable
|
|
200
|
+
// cell's editor is mounted, so it walks the row's inputs as row mode's
|
|
201
|
+
// does.
|
|
190
202
|
event.preventDefault();
|
|
191
203
|
event.stopPropagation();
|
|
192
|
-
if (
|
|
204
|
+
if (isDraft) {
|
|
193
205
|
deferAndClose(event.shiftKey ? "defer-shift-tab" : "defer-tab");
|
|
194
206
|
} else {
|
|
195
207
|
void commitAndClose(event.shiftKey ? "shift-tab" : "tab");
|
|
@@ -11,8 +11,6 @@ import { useState } from "react";
|
|
|
11
11
|
import classes from "./TMDataGridColumnsPanel.module.css";
|
|
12
12
|
import { useTMDataGridContext } from "../TMDataGridContext";
|
|
13
13
|
import { getColumnLabel } from "../core/columnUtils";
|
|
14
|
-
import { GROUP_COLUMN_ID } from "./TMDataGridGroupColumn";
|
|
15
|
-
import { ROW_NUMBER_COLUMN_ID } from "./TMDataGridRowNumberColumn";
|
|
16
14
|
import { SearchIcon } from "./icons";
|
|
17
15
|
|
|
18
16
|
/**
|
|
@@ -29,16 +27,15 @@ export function TMDataGridColumnsPanel() {
|
|
|
29
27
|
(state) => state.columnVisibility,
|
|
30
28
|
);
|
|
31
29
|
|
|
32
|
-
//
|
|
33
|
-
//
|
|
34
|
-
//
|
|
35
|
-
//
|
|
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
36
|
const columns = table
|
|
37
37
|
.getAllLeafColumns()
|
|
38
|
-
.filter(
|
|
39
|
-
(column) =>
|
|
40
|
-
column.id !== GROUP_COLUMN_ID && column.id !== ROW_NUMBER_COLUMN_ID,
|
|
41
|
-
);
|
|
38
|
+
.filter((column) => column.getCanHide());
|
|
42
39
|
const needle = search.trim().toLowerCase();
|
|
43
40
|
const visibleInPanel = needle
|
|
44
41
|
? columns.filter((column) =>
|
|
@@ -46,11 +43,28 @@ export function TMDataGridColumnsPanel() {
|
|
|
46
43
|
)
|
|
47
44
|
: columns;
|
|
48
45
|
|
|
49
|
-
const
|
|
50
|
-
const shownCount = hideableColumns.filter(
|
|
46
|
+
const shownCount = columns.filter(
|
|
51
47
|
(column) => columnVisibility[column.id] !== false,
|
|
52
48
|
).length;
|
|
53
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
|
+
|
|
54
68
|
return (
|
|
55
69
|
<div data-dg-part="columns-panel" className={classes.columnsPanel}>
|
|
56
70
|
<div className={classes.columnsPanelSearch}>
|
|
@@ -74,7 +88,6 @@ export function TMDataGridColumnsPanel() {
|
|
|
74
88
|
label={getColumnLabel(column)}
|
|
75
89
|
data-dg-part="columns-toggle"
|
|
76
90
|
data-column-id={column.id}
|
|
77
|
-
disabled={!column.getCanHide()}
|
|
78
91
|
checked={columnVisibility[column.id] !== false}
|
|
79
92
|
onChange={(event) =>
|
|
80
93
|
column.toggleVisibility(event.currentTarget.checked)
|
|
@@ -94,11 +107,9 @@ export function TMDataGridColumnsPanel() {
|
|
|
94
107
|
size={controlSize}
|
|
95
108
|
label={labels.columnsShowHideAll}
|
|
96
109
|
data-dg-part="columns-toggle-all"
|
|
97
|
-
checked={shownCount ===
|
|
98
|
-
indeterminate={shownCount > 0 && shownCount <
|
|
99
|
-
onChange={(event) =>
|
|
100
|
-
table.toggleAllColumnsVisible(event.currentTarget.checked)
|
|
101
|
-
}
|
|
110
|
+
checked={shownCount === columns.length}
|
|
111
|
+
indeterminate={shownCount > 0 && shownCount < columns.length}
|
|
112
|
+
onChange={(event) => setAllVisible(event.currentTarget.checked)}
|
|
102
113
|
/>
|
|
103
114
|
{/* The whole layout, not only visibility: one reset with an honest
|
|
104
115
|
scope, stated in the tooltip. `table.resetColumnVisibility()` would
|
|
@@ -117,16 +117,13 @@ function DetailsHeader<TData extends RowData>({
|
|
|
117
117
|
* The generated details lane, prepended whenever `renderDetails` is set.
|
|
118
118
|
*
|
|
119
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.
|
|
121
|
-
*
|
|
122
|
-
* leave rows with panels no one can open.
|
|
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.
|
|
123
122
|
*
|
|
124
|
-
* Last of the three because it acts on
|
|
125
|
-
* and the tree says which group they are in, and only then is there a row to
|
|
126
|
-
* open.
|
|
123
|
+
* Last of the three because it acts on a single record.
|
|
127
124
|
*
|
|
128
|
-
*
|
|
129
|
-
* interface, and this lane is only the
|
|
125
|
+
* A second toggle elsewhere is supported: `row.toggleExpanded()` is the entire
|
|
126
|
+
* interface, and this lane is only the control the grid ships.
|
|
130
127
|
*/
|
|
131
128
|
export function createDetailsColumn<TData extends RowData>(
|
|
132
129
|
label = "Details",
|
|
@@ -120,9 +120,9 @@ export type TMDataGridEditActionsProps = {
|
|
|
120
120
|
};
|
|
121
121
|
|
|
122
122
|
/**
|
|
123
|
-
*
|
|
123
|
+
* Draft mode's toolbar chrome: Save with the dirty-row count, and Discard.
|
|
124
124
|
* Both read the edit store, so they grey out while nothing is dirty and the
|
|
125
|
-
* Save spins while a submit is in flight. Works under any `
|
|
125
|
+
* Save spins while a submit is in flight. Works under any `editing.mode` - a
|
|
126
126
|
* cellConfirm grid accumulating drafts can offer the same pair - and renders
|
|
127
127
|
* nothing while editing is off.
|
|
128
128
|
*
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { ActionIcon, Group, Loader, Tooltip } from "@mantine/core";
|
|
2
2
|
import { useSelector } from "@tanstack/react-store";
|
|
3
3
|
import type { ColumnDef, Row, RowData } from "@tanstack/react-table";
|
|
4
|
+
import classes from "./TMDataGridTable.module.css";
|
|
4
5
|
import {
|
|
5
6
|
useCellControlTabIndex,
|
|
6
7
|
useTMDataGridContext,
|
|
@@ -11,6 +12,7 @@ import {
|
|
|
11
12
|
CheckIcon,
|
|
12
13
|
CloseIcon,
|
|
13
14
|
PencilIcon,
|
|
15
|
+
PlusIcon,
|
|
14
16
|
RestoreIcon,
|
|
15
17
|
TrashIcon,
|
|
16
18
|
} from "./icons";
|
|
@@ -18,10 +20,49 @@ import {
|
|
|
18
20
|
export const EDIT_COLUMN_ID = "__edit__";
|
|
19
21
|
|
|
20
22
|
/**
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
23
|
+
* Draft mode's change marker in the lane - what kind of uncommitted change
|
|
24
|
+
* the row carries. Non-interactive; the buttons beside it act. `data-state`
|
|
25
|
+
* is the styling hook, `data-error` flips it red and puts the message in the
|
|
26
|
+
* tooltip in place of the state's name.
|
|
27
|
+
*/
|
|
28
|
+
function RowStateIndicator({
|
|
29
|
+
state,
|
|
30
|
+
rowId,
|
|
31
|
+
label,
|
|
32
|
+
error,
|
|
33
|
+
}: {
|
|
34
|
+
state: "new" | "edited" | "deleted";
|
|
35
|
+
rowId: string;
|
|
36
|
+
label: string;
|
|
37
|
+
error: string | null;
|
|
38
|
+
}) {
|
|
39
|
+
const Icon =
|
|
40
|
+
state === "new" ? PlusIcon : state === "edited" ? PencilIcon : TrashIcon;
|
|
41
|
+
return (
|
|
42
|
+
<Tooltip label={error ?? label} withArrow>
|
|
43
|
+
<span
|
|
44
|
+
role="img"
|
|
45
|
+
aria-label={label}
|
|
46
|
+
data-dg-part="row-state"
|
|
47
|
+
data-state={state}
|
|
48
|
+
data-row-id={rowId}
|
|
49
|
+
data-error={error !== null || undefined}
|
|
50
|
+
className={classes.rowStateIndicator}
|
|
51
|
+
>
|
|
52
|
+
<Icon size={16} stroke={1.6} />
|
|
53
|
+
</span>
|
|
54
|
+
</Tooltip>
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* One row's slot in the edit lane, driven off the edit store. Outside draft
|
|
60
|
+
* mode: a pencil while idle, Save/Cancel while the row's form is open, a
|
|
61
|
+
* loader while its commit is in flight; a row error turns the Save red with
|
|
62
|
+
* the message in its tooltip - the pathless `.refine()` has nowhere else to
|
|
63
|
+
* land. Under draft mode the lane is the change marker and the per-row
|
|
64
|
+
* revert instead: a state icon (new/edited/deleted) beside undo, discard or
|
|
65
|
+
* restore - never a per-row save, which the engine would only park anyway.
|
|
25
66
|
*/
|
|
26
67
|
function EditLaneCell<TData extends RowData>({
|
|
27
68
|
row,
|
|
@@ -38,18 +79,97 @@ function EditLaneCell<TData extends RowData>({
|
|
|
38
79
|
const isNew = useSelector(edit.store, (state) =>
|
|
39
80
|
state.newRows.some((newRow) => newRow.tempId === rowId),
|
|
40
81
|
);
|
|
82
|
+
const isConfirmedNew = useSelector(
|
|
83
|
+
edit.store,
|
|
84
|
+
(state) =>
|
|
85
|
+
state.newRows.find((newRow) => newRow.tempId === rowId)?.confirmed ===
|
|
86
|
+
true,
|
|
87
|
+
);
|
|
41
88
|
const isMarkedDeleted = useSelector(edit.store, (state) =>
|
|
42
89
|
state.deletedRowIds.includes(rowId),
|
|
43
90
|
);
|
|
44
91
|
|
|
45
92
|
if (row.getIsGrouped()) return null;
|
|
46
93
|
|
|
47
|
-
|
|
48
|
-
|
|
94
|
+
const isDraftMode = features.editMode === "draft";
|
|
95
|
+
const hasErrors =
|
|
96
|
+
projection !== undefined &&
|
|
97
|
+
(projection.hasRowError || projection.errorFields.length > 0);
|
|
98
|
+
const rowError = hasErrors
|
|
99
|
+
? (firstErrorText(edit.getForm(rowId)?.state.errors ?? []) ??
|
|
100
|
+
labels.editRowErrors)
|
|
101
|
+
: null;
|
|
102
|
+
|
|
103
|
+
const trash = edit.canDeleteRows() ? (
|
|
104
|
+
<Tooltip label={labels.deleteRow} withArrow>
|
|
105
|
+
<ActionIcon
|
|
106
|
+
variant="subtle"
|
|
107
|
+
color="gray"
|
|
108
|
+
size="sm"
|
|
109
|
+
tabIndex={tabIndex}
|
|
110
|
+
aria-label={labels.deleteRow}
|
|
111
|
+
data-dg-part="delete-row"
|
|
112
|
+
data-row-id={rowId}
|
|
113
|
+
onClick={(event) => {
|
|
114
|
+
event.stopPropagation();
|
|
115
|
+
edit.deleteRow(rowId);
|
|
116
|
+
}}
|
|
117
|
+
>
|
|
118
|
+
<TrashIcon size={16} stroke={1.6} />
|
|
119
|
+
</ActionIcon>
|
|
120
|
+
</Tooltip>
|
|
121
|
+
) : null;
|
|
122
|
+
|
|
123
|
+
// An entry row: ✓ enters it - an add under the immediate modes, a parked
|
|
124
|
+
// confirm under draft - and ✕ discards the entry. A confirmed entry waits
|
|
125
|
+
// for Save all as a value row: marked new, re-openable, still discardable.
|
|
49
126
|
if (isNew) {
|
|
127
|
+
if (isConfirmedNew) {
|
|
128
|
+
return (
|
|
129
|
+
<Group gap={0} wrap="nowrap">
|
|
130
|
+
<RowStateIndicator
|
|
131
|
+
state="new"
|
|
132
|
+
rowId={rowId}
|
|
133
|
+
label={labels.rowStateNew}
|
|
134
|
+
error={rowError}
|
|
135
|
+
/>
|
|
136
|
+
<Tooltip label={labels.editRow} withArrow>
|
|
137
|
+
<ActionIcon
|
|
138
|
+
variant="subtle"
|
|
139
|
+
color="gray"
|
|
140
|
+
size="sm"
|
|
141
|
+
tabIndex={tabIndex}
|
|
142
|
+
aria-label={labels.editRow}
|
|
143
|
+
data-dg-part="edit-row"
|
|
144
|
+
data-row-id={rowId}
|
|
145
|
+
onClick={(event) => {
|
|
146
|
+
event.stopPropagation();
|
|
147
|
+
edit.begin({ rowId, columnId: null });
|
|
148
|
+
}}
|
|
149
|
+
>
|
|
150
|
+
<PencilIcon size={16} stroke={1.6} />
|
|
151
|
+
</ActionIcon>
|
|
152
|
+
</Tooltip>
|
|
153
|
+
<Tooltip label={labels.discardNewRow} withArrow>
|
|
154
|
+
<ActionIcon
|
|
155
|
+
variant="subtle"
|
|
156
|
+
color="gray"
|
|
157
|
+
size="sm"
|
|
158
|
+
tabIndex={tabIndex}
|
|
159
|
+
aria-label={labels.discardNewRow}
|
|
160
|
+
data-dg-part="discard-new-row"
|
|
161
|
+
data-row-id={rowId}
|
|
162
|
+
onClick={() => edit.cancel(rowId)}
|
|
163
|
+
>
|
|
164
|
+
<CloseIcon size={16} stroke={1.6} />
|
|
165
|
+
</ActionIcon>
|
|
166
|
+
</Tooltip>
|
|
167
|
+
</Group>
|
|
168
|
+
);
|
|
169
|
+
}
|
|
50
170
|
return (
|
|
51
171
|
<Group gap={0} wrap="nowrap">
|
|
52
|
-
{
|
|
172
|
+
<Tooltip label={labels.confirmNewRow} withArrow>
|
|
53
173
|
<ActionIcon
|
|
54
174
|
variant="subtle"
|
|
55
175
|
color="green"
|
|
@@ -62,84 +182,121 @@ function EditLaneCell<TData extends RowData>({
|
|
|
62
182
|
>
|
|
63
183
|
<CheckIcon size={16} stroke={2} />
|
|
64
184
|
</ActionIcon>
|
|
65
|
-
|
|
66
|
-
<
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
185
|
+
</Tooltip>
|
|
186
|
+
<Tooltip label={labels.discardNewRow} withArrow>
|
|
187
|
+
<ActionIcon
|
|
188
|
+
variant="subtle"
|
|
189
|
+
color="gray"
|
|
190
|
+
size="sm"
|
|
191
|
+
tabIndex={tabIndex}
|
|
192
|
+
aria-label={labels.discardNewRow}
|
|
193
|
+
data-dg-part="discard-new-row"
|
|
194
|
+
data-row-id={rowId}
|
|
195
|
+
onClick={() => edit.cancel(rowId)}
|
|
196
|
+
>
|
|
197
|
+
<CloseIcon size={16} stroke={1.6} />
|
|
198
|
+
</ActionIcon>
|
|
199
|
+
</Tooltip>
|
|
78
200
|
</Group>
|
|
79
201
|
);
|
|
80
202
|
}
|
|
81
203
|
|
|
82
|
-
// A
|
|
204
|
+
// A draft-mode deletion mark is a draft like any other - restorable in place.
|
|
83
205
|
if (isMarkedDeleted) {
|
|
84
|
-
return (
|
|
85
|
-
<ActionIcon
|
|
86
|
-
variant="subtle"
|
|
87
|
-
color="gray"
|
|
88
|
-
size="sm"
|
|
89
|
-
tabIndex={tabIndex}
|
|
90
|
-
aria-label={labels.restoreRow}
|
|
91
|
-
data-dg-part="restore-row"
|
|
92
|
-
data-row-id={rowId}
|
|
93
|
-
onClick={(event) => {
|
|
94
|
-
event.stopPropagation();
|
|
95
|
-
edit.deleteRow(rowId);
|
|
96
|
-
}}
|
|
97
|
-
>
|
|
98
|
-
<RestoreIcon size={16} stroke={1.6} />
|
|
99
|
-
</ActionIcon>
|
|
100
|
-
);
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
// The pencil means "this row edits"; a row that does not gets nothing.
|
|
104
|
-
if (!edit.canEditRow(row as never)) return null;
|
|
105
|
-
|
|
106
|
-
if (!isOpen) {
|
|
107
206
|
return (
|
|
108
207
|
<Group gap={0} wrap="nowrap">
|
|
109
|
-
|
|
208
|
+
<RowStateIndicator
|
|
209
|
+
state="deleted"
|
|
210
|
+
rowId={rowId}
|
|
211
|
+
label={labels.rowStateDeleted}
|
|
212
|
+
error={null}
|
|
213
|
+
/>
|
|
214
|
+
<Tooltip label={labels.restoreRow} withArrow>
|
|
110
215
|
<ActionIcon
|
|
111
216
|
variant="subtle"
|
|
112
217
|
color="gray"
|
|
113
218
|
size="sm"
|
|
114
219
|
tabIndex={tabIndex}
|
|
115
|
-
aria-label={labels.
|
|
116
|
-
data-dg-part="
|
|
220
|
+
aria-label={labels.restoreRow}
|
|
221
|
+
data-dg-part="restore-row"
|
|
117
222
|
data-row-id={rowId}
|
|
118
223
|
onClick={(event) => {
|
|
119
224
|
event.stopPropagation();
|
|
120
|
-
edit.
|
|
225
|
+
edit.deleteRow(rowId);
|
|
121
226
|
}}
|
|
122
227
|
>
|
|
123
|
-
<
|
|
228
|
+
<RestoreIcon size={16} stroke={1.6} />
|
|
124
229
|
</ActionIcon>
|
|
125
|
-
|
|
126
|
-
|
|
230
|
+
</Tooltip>
|
|
231
|
+
</Group>
|
|
232
|
+
);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// The pencil means "this row edits"; a row that does not gets nothing.
|
|
236
|
+
if (!edit.canEditRow(row as never)) return null;
|
|
237
|
+
|
|
238
|
+
// Draft mode never offers a per-row save - the engine would only park it,
|
|
239
|
+
// and Save all is the commit. The lane is the state marker and the revert.
|
|
240
|
+
// No trash beside the revert: the two undo-shaped icons were too easy to
|
|
241
|
+
// mistake for one another, so a dirty row reverts first, then deletes.
|
|
242
|
+
if (isDraftMode) {
|
|
243
|
+
if (projection?.isSubmitting) {
|
|
244
|
+
return <Loader size="xs" aria-label={labels.loading} />;
|
|
245
|
+
}
|
|
246
|
+
const isDirty = (projection?.dirtyFields.length ?? 0) > 0;
|
|
247
|
+
if (!isDirty) return trash;
|
|
248
|
+
return (
|
|
249
|
+
<Group gap={0} wrap="nowrap">
|
|
250
|
+
<RowStateIndicator
|
|
251
|
+
state="edited"
|
|
252
|
+
rowId={rowId}
|
|
253
|
+
label={labels.rowStateEdited}
|
|
254
|
+
error={rowError}
|
|
255
|
+
/>
|
|
256
|
+
<Tooltip label={labels.revertRow} withArrow>
|
|
127
257
|
<ActionIcon
|
|
128
258
|
variant="subtle"
|
|
129
259
|
color="gray"
|
|
130
260
|
size="sm"
|
|
131
261
|
tabIndex={tabIndex}
|
|
132
|
-
aria-label={labels.
|
|
133
|
-
data-dg-part="
|
|
262
|
+
aria-label={labels.revertRow}
|
|
263
|
+
data-dg-part="revert-row"
|
|
134
264
|
data-row-id={rowId}
|
|
135
265
|
onClick={(event) => {
|
|
136
266
|
event.stopPropagation();
|
|
137
|
-
edit.
|
|
267
|
+
edit.cancel(rowId);
|
|
138
268
|
}}
|
|
139
269
|
>
|
|
140
|
-
<
|
|
270
|
+
<RestoreIcon size={16} stroke={1.6} />
|
|
141
271
|
</ActionIcon>
|
|
272
|
+
</Tooltip>
|
|
273
|
+
</Group>
|
|
274
|
+
);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
if (!isOpen) {
|
|
278
|
+
return (
|
|
279
|
+
<Group gap={0} wrap="nowrap">
|
|
280
|
+
{features.editMode === "row" && (
|
|
281
|
+
<Tooltip label={labels.editRow} withArrow>
|
|
282
|
+
<ActionIcon
|
|
283
|
+
variant="subtle"
|
|
284
|
+
color="gray"
|
|
285
|
+
size="sm"
|
|
286
|
+
tabIndex={tabIndex}
|
|
287
|
+
aria-label={labels.editRow}
|
|
288
|
+
data-dg-part="edit-row"
|
|
289
|
+
data-row-id={rowId}
|
|
290
|
+
onClick={(event) => {
|
|
291
|
+
event.stopPropagation();
|
|
292
|
+
edit.begin({ rowId, columnId: null });
|
|
293
|
+
}}
|
|
294
|
+
>
|
|
295
|
+
<PencilIcon size={16} stroke={1.6} />
|
|
296
|
+
</ActionIcon>
|
|
297
|
+
</Tooltip>
|
|
142
298
|
)}
|
|
299
|
+
{trash}
|
|
143
300
|
</Group>
|
|
144
301
|
);
|
|
145
302
|
}
|
|
@@ -148,14 +305,6 @@ function EditLaneCell<TData extends RowData>({
|
|
|
148
305
|
return <Loader size="xs" aria-label={labels.loading} />;
|
|
149
306
|
}
|
|
150
307
|
|
|
151
|
-
const hasErrors =
|
|
152
|
-
projection !== undefined &&
|
|
153
|
-
(projection.hasRowError || projection.errorFields.length > 0);
|
|
154
|
-
const rowError = hasErrors
|
|
155
|
-
? (firstErrorText(edit.getForm(rowId)?.state.errors ?? []) ??
|
|
156
|
-
labels.editRowErrors)
|
|
157
|
-
: null;
|
|
158
|
-
|
|
159
308
|
const save = (
|
|
160
309
|
<ActionIcon
|
|
161
310
|
variant="subtle"
|
|
@@ -177,39 +326,46 @@ function EditLaneCell<TData extends RowData>({
|
|
|
177
326
|
return (
|
|
178
327
|
<Group gap={0} wrap="nowrap">
|
|
179
328
|
{rowError === null ? (
|
|
180
|
-
|
|
329
|
+
<Tooltip label={labels.saveRow} withArrow>
|
|
330
|
+
{save}
|
|
331
|
+
</Tooltip>
|
|
181
332
|
) : (
|
|
182
333
|
<Tooltip label={rowError} withArrow>
|
|
183
334
|
{save}
|
|
184
335
|
</Tooltip>
|
|
185
336
|
)}
|
|
186
|
-
<
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
event
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
337
|
+
<Tooltip label={labels.cancelRowEdit} withArrow>
|
|
338
|
+
<ActionIcon
|
|
339
|
+
variant="subtle"
|
|
340
|
+
color="gray"
|
|
341
|
+
size="sm"
|
|
342
|
+
tabIndex={tabIndex}
|
|
343
|
+
aria-label={labels.cancelRowEdit}
|
|
344
|
+
data-dg-part="cancel-row"
|
|
345
|
+
data-row-id={rowId}
|
|
346
|
+
onClick={(event) => {
|
|
347
|
+
event.stopPropagation();
|
|
348
|
+
edit.cancel(rowId);
|
|
349
|
+
}}
|
|
350
|
+
>
|
|
351
|
+
<CloseIcon size={16} stroke={1.6} />
|
|
352
|
+
</ActionIcon>
|
|
353
|
+
</Tooltip>
|
|
201
354
|
</Group>
|
|
202
355
|
);
|
|
203
356
|
}
|
|
204
357
|
|
|
205
358
|
/**
|
|
206
|
-
* The generated edit lane, appended and pinned right
|
|
207
|
-
*
|
|
208
|
-
* checkbox lane's build on the left.
|
|
359
|
+
* The generated edit lane, appended and pinned right - the row's Save at the
|
|
360
|
+
* end of the row under `mode: "row"`, the state marker and revert under
|
|
361
|
+
* `mode: "draft"`, mirroring the checkbox lane's build on the left.
|
|
209
362
|
*/
|
|
210
363
|
export function createEditColumn<TData extends RowData>(
|
|
211
364
|
label = "Edit",
|
|
365
|
+
/** Draft mode holds three controls where the others hold two. */
|
|
366
|
+
wide = false,
|
|
212
367
|
): ColumnDef<TMDataGridFeatures, TData, unknown> {
|
|
368
|
+
const width = wide ? 88 : 64;
|
|
213
369
|
return {
|
|
214
370
|
id: EDIT_COLUMN_ID,
|
|
215
371
|
meta: {
|
|
@@ -217,14 +373,18 @@ export function createEditColumn<TData extends RowData>(
|
|
|
217
373
|
align: "center",
|
|
218
374
|
enableOrdering: false,
|
|
219
375
|
},
|
|
220
|
-
// Wide enough for the
|
|
221
|
-
size:
|
|
222
|
-
minSize:
|
|
223
|
-
maxSize:
|
|
376
|
+
// Wide enough for the pair (or draft's trio) it holds while editing.
|
|
377
|
+
size: width,
|
|
378
|
+
minSize: width,
|
|
379
|
+
maxSize: width,
|
|
224
380
|
enableResizing: false,
|
|
225
381
|
enableSorting: false,
|
|
226
382
|
enableColumnFilter: false,
|
|
227
383
|
enableGlobalFilter: false,
|
|
384
|
+
// The row's Save, Cancel and Delete live here, so hiding the lane would
|
|
385
|
+
// strand an open row with no way to commit or discard it. Same rule as the
|
|
386
|
+
// checkbox lane: chrome the grid generates is not a user setting.
|
|
387
|
+
enableHiding: false,
|
|
228
388
|
// Structurally pinned to the right; not movable.
|
|
229
389
|
enablePinning: false,
|
|
230
390
|
header: () => null,
|