@jielga/tmdatagrid 1.0.2 → 1.1.1
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 +17 -14
- package/dist/index.d.ts +193 -21
- package/dist/index.js +1914 -1802
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/skills/appearance/SKILL.md +1 -1
- package/skills/cell-selection/SKILL.md +4 -4
- package/skills/columns/SKILL.md +1 -1
- package/skills/data/SKILL.md +33 -20
- package/skills/editing/SKILL.md +100 -103
- package/skills/editing/references/editing-api.md +5 -2
- package/skills/editing/references/editors-and-validation.md +7 -1
- package/skills/filtering/SKILL.md +1 -1
- package/skills/getting-started/SKILL.md +23 -17
- package/skills/grouping/SKILL.md +1 -1
- package/skills/options/SKILL.md +1 -1
- package/skills/rows/SKILL.md +50 -15
- package/skills/rows/references/rows-api.md +3 -2
- package/skills/server-side/SKILL.md +1 -1
- package/skills/testing/SKILL.md +1 -1
- package/src/tmdatagrid/components/TMDataGridCellEditor.tsx +1 -1
- package/src/tmdatagrid/components/TMDataGridEditActions.tsx +132 -32
- package/src/tmdatagrid/components/TMDataGridFooter.tsx +223 -95
- package/src/tmdatagrid/components/TMDataGridHeaderCell.tsx +41 -4
- package/src/tmdatagrid/components/TMDataGridTable.tsx +218 -67
- package/src/tmdatagrid/core/editEngine.ts +28 -20
- package/src/tmdatagrid/index.ts +17 -2
package/skills/rows/SKILL.md
CHANGED
|
@@ -6,17 +6,18 @@ description: >
|
|
|
6
6
|
highlight as state of its own with defaultHighlightedRowId and
|
|
7
7
|
onHighlightedRowChange, showSelectedBackground and --dg-row-selected-bg,
|
|
8
8
|
reading a selection through the table store, onRowClick / onCellClick /
|
|
9
|
-
onCellDoubleClick / onCellContextMenu, the
|
|
10
|
-
rowContextMenuProps, per-row
|
|
11
|
-
the --row-bg rule, the row
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
to an edge, or
|
|
9
|
+
onCellDoubleClick / onCellContextMenu, the renderRowContextMenu slot with its
|
|
10
|
+
internalItems handback, renderColumnMenuItems and rowContextMenuProps, per-row
|
|
11
|
+
styling with rowStyle, rowClassName, striped and the --row-bg rule, the row
|
|
12
|
+
details panel through renderDetails and DETAILS_COLUMN_ID, row pinning with
|
|
13
|
+
enableRowPinning and row.pin, and the row-number gutter through
|
|
14
|
+
enableRowNumbers. Load when selecting rows, reacting to a click, opening a
|
|
15
|
+
detail panel, colouring rows by their data, pinning rows to an edge, or
|
|
16
|
+
numbering them.
|
|
16
17
|
metadata:
|
|
17
18
|
type: core
|
|
18
19
|
library: '@jielga/tmdatagrid'
|
|
19
|
-
library_version: '1.
|
|
20
|
+
library_version: '1.1.1'
|
|
20
21
|
sources:
|
|
21
22
|
- 'Jielga/TMDataGrid:src/docs/row-selection.md'
|
|
22
23
|
- 'Jielga/TMDataGrid:src/docs/row-interaction.md'
|
|
@@ -117,7 +118,7 @@ not replace selection or the highlight; yours runs in addition.
|
|
|
117
118
|
| `onRowClick` | `row` | Rows show a pointer cursor when set |
|
|
118
119
|
| `onCellClick` | `{ cell, row, column, event }` | The cell cursor still moves |
|
|
119
120
|
| `onCellDoubleClick` | same | A double-click that opens an editor still does |
|
|
120
|
-
| `onCellContextMenu` | same | `
|
|
121
|
+
| `onCellContextMenu` | same | `renderRowContextMenu` still opens |
|
|
121
122
|
|
|
122
123
|
```tsx
|
|
123
124
|
<TMDataGrid.Table<Employee> onRowClick={(row) => open(row.original.id)} />
|
|
@@ -127,13 +128,13 @@ Pass the row type, as above, and `row.original` is typed. Group rows sit out all
|
|
|
127
128
|
four: TanStack builds a group row on top of its first child's record, so a
|
|
128
129
|
handler would receive a real-looking row that is the wrong one.
|
|
129
130
|
|
|
130
|
-
`
|
|
131
|
+
`renderRowContextMenu` is a slot saying what goes inside the menu. The grid
|
|
131
132
|
owns the Mantine `Menu`, opening it at the pointer and closing it on Escape, an
|
|
132
133
|
outside click, a body scroll and after an item is picked.
|
|
133
134
|
|
|
134
135
|
```tsx
|
|
135
136
|
<TMDataGrid.Table<Employee>
|
|
136
|
-
|
|
137
|
+
renderRowContextMenu={({ table, row, cell }) => (
|
|
137
138
|
<>
|
|
138
139
|
<Menu.Label>{row.original.firstName}</Menu.Label>
|
|
139
140
|
<Menu.Item onClick={() => open(row.original.id)}>Open</Menu.Item>
|
|
@@ -156,6 +157,35 @@ a pure function of its arguments and do the work in the item handlers. Return
|
|
|
156
157
|
the row with `data-context-menu` while its menu is open, so an action meant for
|
|
157
158
|
a multi-selection reads `table` and falls back to the clicked row.
|
|
158
159
|
|
|
160
|
+
Under `cellSelection: "range"` the grid has its own items for this menu - copy,
|
|
161
|
+
export, include headers. `internalItems` is those items, and **reading it hands
|
|
162
|
+
the composition over**: place it and the menu is exactly what you returned;
|
|
163
|
+
never mention it and the grid keeps its half above a divider and yours below.
|
|
164
|
+
|
|
165
|
+
```tsx
|
|
166
|
+
renderRowContextMenu={({ row, internalItems }) => (
|
|
167
|
+
<>
|
|
168
|
+
<Menu.Item onClick={() => open(row.id)}>Open</Menu.Item>
|
|
169
|
+
<Menu.Divider />
|
|
170
|
+
{internalItems}
|
|
171
|
+
</>
|
|
172
|
+
)}
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
`renderColumnMenuItems` is the same idea for a column's menu, and returns an
|
|
176
|
+
array rather than a node:
|
|
177
|
+
|
|
178
|
+
```tsx
|
|
179
|
+
<TMDataGrid.Table
|
|
180
|
+
renderColumnMenuItems={({ column, internalItems }) => [
|
|
181
|
+
...internalItems,
|
|
182
|
+
<Menu.Item key="stats" onClick={() => showStats(column.id)}>Stats</Menu.Item>,
|
|
183
|
+
]}
|
|
184
|
+
/>
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
Returning an empty list leaves the column with no menu button at all.
|
|
188
|
+
|
|
159
189
|
## Row styling
|
|
160
190
|
|
|
161
191
|
```tsx
|
|
@@ -219,10 +249,15 @@ const grid = useTMDataGrid({
|
|
|
219
249
|
});
|
|
220
250
|
```
|
|
221
251
|
|
|
222
|
-
There is no built-in pin gesture
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
252
|
+
There is no built-in pin gesture and no pin icon - build one from TanStack's own
|
|
253
|
+
`row.pin("top" | "bottom" | false)`, `row.getIsPinned()` and `row.getCanPin()`,
|
|
254
|
+
either as a display column whose cell is a pin button or in the row context
|
|
255
|
+
menu. Read the pinned state through a subscription
|
|
256
|
+
(`useSelector(row.table.store, () => row.getIsPinned())`) rather than calling it
|
|
257
|
+
in a component body: the `row` identity survives a pin, so the React Compiler
|
|
258
|
+
would cache the call and the icon would never change. Whichever gesture you
|
|
259
|
+
build, put unpin in it too. Pinned rows leave the scrolling order and render in
|
|
260
|
+
sticky blocks: top under the header, bottom above the summary row.
|
|
226
261
|
|
|
227
262
|
Pinned rows are still body rows - selection, editing, details, the context menu
|
|
228
263
|
and per-row styling all behave normally. What they sit out are the statements
|
|
@@ -31,11 +31,12 @@ All are props of `TMDataGrid.Table`, not hook options.
|
|
|
31
31
|
| `onCellClick` | `(args) => void` | Cell click. `args` is `TMDataGridCellEventArgs`. |
|
|
32
32
|
| `onCellDoubleClick` | `(args) => void` | Cell double-click. |
|
|
33
33
|
| `onCellContextMenu` | `(args) => void` | Cell right-click. |
|
|
34
|
-
| `
|
|
34
|
+
| `renderRowContextMenu` | `({ table, row, cell, close, internalItems }) => ReactNode` | Contents of the row's context menu. `null` for no menu. Reading `internalItems` hands the composition over. |
|
|
35
|
+
| `renderColumnMenuItems` | `({ column, table, internalItems }) => ReactNode[]` | Contents of a column's menu. An empty list removes the button. |
|
|
35
36
|
| `rowContextMenuProps` | `MenuProps` | Passed to the Mantine `Menu` untouched apart from its open state. |
|
|
36
37
|
|
|
37
38
|
`TMDataGridCellEventArgs` is `{ cell, row, column, event }`. The context-menu
|
|
38
|
-
|
|
39
|
+
slot's `cell` is `null` only when a custom cell renderer stopped the
|
|
39
40
|
event. One `Menu` serves the whole body: a closed Mantine `Popover` still runs
|
|
40
41
|
its hooks on every render, and the virtualized body re-renders every scroll
|
|
41
42
|
frame.
|
package/skills/testing/SKILL.md
CHANGED
|
@@ -64,7 +64,7 @@ export function TMDataGridCellEditor({
|
|
|
64
64
|
row: Row<TMDataGridFeatures, TMDataGridRowData>;
|
|
65
65
|
/** Consumes the pending type-to-edit seed, when typing opened this editor. */
|
|
66
66
|
takeSeedText: () => string | undefined;
|
|
67
|
-
/**
|
|
67
|
+
/** @deprecated The grid places the caret; see `TMDataGridEditorArgs`. */
|
|
68
68
|
autoFocus?: boolean;
|
|
69
69
|
/** After the editor closed itself - the table moves the focus. */
|
|
70
70
|
onClose: (args: TMDataGridCellEditorClose) => void;
|
|
@@ -1,7 +1,124 @@
|
|
|
1
1
|
import { Button, Group } from "@mantine/core";
|
|
2
2
|
import { useSelector } from "@tanstack/react-store";
|
|
3
|
+
import type { ReactNode } from "react";
|
|
3
4
|
import { useTMDataGridContext } from "../TMDataGridContext";
|
|
4
5
|
|
|
6
|
+
/**
|
|
7
|
+
* How many rows are waiting to be saved: dirty edits, entry rows (an add is
|
|
8
|
+
* pending whether or not it was touched), and deletion marks.
|
|
9
|
+
*/
|
|
10
|
+
function usePendingCount(): number {
|
|
11
|
+
const { edit } = useTMDataGridContext();
|
|
12
|
+
return useSelector(
|
|
13
|
+
edit.store,
|
|
14
|
+
(state) =>
|
|
15
|
+
state.openRowIds.filter(
|
|
16
|
+
(rowId) =>
|
|
17
|
+
state.newRows.some((newRow) => newRow.tempId === rowId) ||
|
|
18
|
+
(state.rows[rowId]?.dirtyFields.length ?? 0) > 0,
|
|
19
|
+
).length + state.deletedRowIds.length,
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/*
|
|
24
|
+
* Module scope, like the pagination controls and for the same reason: a
|
|
25
|
+
* component defined inside the render body is a new type every render, so
|
|
26
|
+
* React would remount the buttons underneath an interaction.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
function EditSaveButton() {
|
|
30
|
+
const { edit, labels } = useTMDataGridContext();
|
|
31
|
+
const pendingCount = usePendingCount();
|
|
32
|
+
const isSubmitting = useSelector(edit.store, (state) =>
|
|
33
|
+
state.openRowIds.some((rowId) => state.rows[rowId]?.isSubmitting === true),
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
return (
|
|
37
|
+
<Button
|
|
38
|
+
size="compact-sm"
|
|
39
|
+
disabled={pendingCount === 0}
|
|
40
|
+
loading={isSubmitting}
|
|
41
|
+
data-dg-part="save-all"
|
|
42
|
+
onClick={() => void edit.submitAll()}
|
|
43
|
+
>
|
|
44
|
+
{labels.saveAllEdits(pendingCount)}
|
|
45
|
+
</Button>
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function EditDiscardButton() {
|
|
50
|
+
const { edit, labels } = useTMDataGridContext();
|
|
51
|
+
const pendingCount = usePendingCount();
|
|
52
|
+
|
|
53
|
+
return (
|
|
54
|
+
<Button
|
|
55
|
+
variant="subtle"
|
|
56
|
+
color="gray"
|
|
57
|
+
size="compact-sm"
|
|
58
|
+
disabled={pendingCount === 0}
|
|
59
|
+
data-dg-part="discard-all"
|
|
60
|
+
onClick={() => edit.cancelAll()}
|
|
61
|
+
>
|
|
62
|
+
{labels.discardAllEdits}
|
|
63
|
+
</Button>
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const EDIT_ACTIONS_CONTROLS: TMDataGridEditActionsControls = {
|
|
68
|
+
Save: EditSaveButton,
|
|
69
|
+
Discard: EditDiscardButton,
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
/** What the edit chrome is showing. */
|
|
73
|
+
export type TMDataGridEditActionsState = {
|
|
74
|
+
/** Rows with unsaved work: dirty edits, entry rows, and deletion marks. */
|
|
75
|
+
pendingCount: number;
|
|
76
|
+
/** Whether a submit is in flight. */
|
|
77
|
+
isSubmitting: boolean;
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
/** What the edit chrome can do. */
|
|
81
|
+
export type TMDataGridEditActionsActions = {
|
|
82
|
+
/** Commits every open row. Resolves `false` when a row stayed open. */
|
|
83
|
+
save: () => Promise<boolean>;
|
|
84
|
+
/** Drops every draft. */
|
|
85
|
+
discard: () => void;
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
/** The pre-bound pieces of the built-in edit chrome. */
|
|
89
|
+
export type TMDataGridEditActionsControls = {
|
|
90
|
+
/** Save, with the pending count, disabled while nothing is pending. */
|
|
91
|
+
Save: () => ReactNode;
|
|
92
|
+
/** Discard, disabled while nothing is pending. */
|
|
93
|
+
Discard: () => ReactNode;
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
/** What {@link TMDataGridEditActionsProps.renderActions} is handed. */
|
|
97
|
+
export type TMDataGridEditActionsSlotArgs = {
|
|
98
|
+
state: TMDataGridEditActionsState;
|
|
99
|
+
actions: TMDataGridEditActionsActions;
|
|
100
|
+
Controls: TMDataGridEditActionsControls;
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
export type TMDataGridEditActionsProps = {
|
|
104
|
+
/**
|
|
105
|
+
* Replaces the built-in Save/Discard pair, and is handed the pieces of it.
|
|
106
|
+
*
|
|
107
|
+
* ```tsx
|
|
108
|
+
* <TMDataGrid.EditActions
|
|
109
|
+
* renderActions={({ state, Controls }) => (
|
|
110
|
+
* <Group>
|
|
111
|
+
* {state.pendingCount > 0 && <Badge>{state.pendingCount}</Badge>}
|
|
112
|
+
* <Controls.Save />
|
|
113
|
+
* <Controls.Discard />
|
|
114
|
+
* </Group>
|
|
115
|
+
* )}
|
|
116
|
+
* />
|
|
117
|
+
* ```
|
|
118
|
+
*/
|
|
119
|
+
renderActions?: (args: TMDataGridEditActionsSlotArgs) => ReactNode;
|
|
120
|
+
};
|
|
121
|
+
|
|
5
122
|
/**
|
|
6
123
|
* Batch mode's toolbar chrome: Save with the dirty-row count, and Discard.
|
|
7
124
|
* Both read the edit store, so they grey out while nothing is dirty and the
|
|
@@ -17,46 +134,29 @@ import { useTMDataGridContext } from "../TMDataGridContext";
|
|
|
17
134
|
* </TMDataGrid.Toolbar>
|
|
18
135
|
* ```
|
|
19
136
|
*/
|
|
20
|
-
export function TMDataGridEditActions(
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
const
|
|
25
|
-
edit.store,
|
|
26
|
-
(state) =>
|
|
27
|
-
state.openRowIds.filter(
|
|
28
|
-
(rowId) =>
|
|
29
|
-
state.newRows.some((newRow) => newRow.tempId === rowId) ||
|
|
30
|
-
(state.rows[rowId]?.dirtyFields.length ?? 0) > 0,
|
|
31
|
-
).length + state.deletedRowIds.length,
|
|
32
|
-
);
|
|
137
|
+
export function TMDataGridEditActions({
|
|
138
|
+
renderActions,
|
|
139
|
+
}: TMDataGridEditActionsProps = {}) {
|
|
140
|
+
const { edit, features } = useTMDataGridContext();
|
|
141
|
+
const pendingCount = usePendingCount();
|
|
33
142
|
const isSubmitting = useSelector(edit.store, (state) =>
|
|
34
143
|
state.openRowIds.some((rowId) => state.rows[rowId]?.isSubmitting === true),
|
|
35
144
|
);
|
|
36
145
|
|
|
37
146
|
if (!features.editing) return null;
|
|
38
147
|
|
|
148
|
+
if (renderActions) {
|
|
149
|
+
return renderActions({
|
|
150
|
+
state: { pendingCount, isSubmitting },
|
|
151
|
+
actions: { save: () => edit.submitAll(), discard: () => edit.cancelAll() },
|
|
152
|
+
Controls: EDIT_ACTIONS_CONTROLS,
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
|
|
39
156
|
return (
|
|
40
157
|
<Group gap="xs" wrap="nowrap">
|
|
41
|
-
<
|
|
42
|
-
|
|
43
|
-
disabled={dirtyCount === 0}
|
|
44
|
-
loading={isSubmitting}
|
|
45
|
-
data-dg-part="save-all"
|
|
46
|
-
onClick={() => void edit.submitAll()}
|
|
47
|
-
>
|
|
48
|
-
{labels.saveAllEdits(dirtyCount)}
|
|
49
|
-
</Button>
|
|
50
|
-
<Button
|
|
51
|
-
variant="subtle"
|
|
52
|
-
color="gray"
|
|
53
|
-
size="compact-sm"
|
|
54
|
-
disabled={dirtyCount === 0}
|
|
55
|
-
data-dg-part="discard-all"
|
|
56
|
-
onClick={() => edit.cancelAll()}
|
|
57
|
-
>
|
|
58
|
-
{labels.discardAllEdits}
|
|
59
|
-
</Button>
|
|
158
|
+
<EditSaveButton />
|
|
159
|
+
<EditDiscardButton />
|
|
60
160
|
</Group>
|
|
61
161
|
);
|
|
62
162
|
}
|