@jielga/tmdatagrid 2.0.0-beta.1 → 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/dist/index.d.ts +60 -23
- package/dist/index.js +2020 -1704
- package/dist/index.js.map +1 -1
- package/dist/styles.css +1 -1
- package/package.json +1 -1
- package/skills/appearance/SKILL.md +1 -1
- package/skills/cell-selection/SKILL.md +1 -1
- package/skills/columns/SKILL.md +2 -2
- package/skills/data/SKILL.md +1 -1
- package/skills/editing/SKILL.md +68 -213
- package/skills/editing/references/common-mistakes.md +177 -0
- package/skills/editing/references/editing-api.md +39 -13
- package/skills/editing/references/editors-and-validation.md +5 -2
- package/skills/filtering/SKILL.md +1 -1
- package/skills/getting-started/SKILL.md +1 -1
- package/skills/grouping/SKILL.md +1 -1
- package/skills/options/SKILL.md +2 -2
- package/skills/rows/SKILL.md +1 -1
- package/skills/rows/references/rows-api.md +1 -1
- package/skills/server-side/SKILL.md +1 -1
- package/skills/testing/SKILL.md +3 -2
- package/src/tmdatagrid/components/TMDataGrid.module.css +7 -0
- package/src/tmdatagrid/components/TMDataGridCellEditor.tsx +20 -8
- package/src/tmdatagrid/components/TMDataGridEditActions.tsx +1 -1
- package/src/tmdatagrid/components/TMDataGridEditColumn.tsx +242 -86
- package/src/tmdatagrid/components/TMDataGridEntryRows.tsx +176 -77
- package/src/tmdatagrid/components/TMDataGridTable.module.css +47 -3
- package/src/tmdatagrid/components/TMDataGridTable.tsx +44 -4
- package/src/tmdatagrid/components/icons.ts +1 -0
- package/src/tmdatagrid/core/capabilities.ts +10 -0
- package/src/tmdatagrid/core/draftCellContext.ts +68 -0
- package/src/tmdatagrid/core/editEngine.ts +106 -32
- package/src/tmdatagrid/core/labels.ts +13 -1
- package/src/tmdatagrid/core/labelsSv.ts +4 -0
- package/src/tmdatagrid/index.ts +1 -1
- package/src/tmdatagrid/useTMDataGrid.tsx +39 -20
|
@@ -53,7 +53,7 @@ function isEntryCellEditable(
|
|
|
53
53
|
* every other draft, keyed by their temporary ids.
|
|
54
54
|
*
|
|
55
55
|
* Enter commits the entry (`onRowAdd`) under the immediate modes and parks
|
|
56
|
-
* it under
|
|
56
|
+
* it under draft, where `submitAll` reports it in `added`; Escape discards
|
|
57
57
|
* it. The edit lane, when present, offers the same pair as buttons.
|
|
58
58
|
*/
|
|
59
59
|
export function TMDataGridEntryRows({
|
|
@@ -65,11 +65,23 @@ export function TMDataGridEntryRows({
|
|
|
65
65
|
layoutFor: (columnId: string) => TMDataGridColumnLayout;
|
|
66
66
|
rowHeight: number;
|
|
67
67
|
}) {
|
|
68
|
-
const { table, edit } = useTMDataGridContext();
|
|
68
|
+
const { table, edit, features } = useTMDataGridContext();
|
|
69
69
|
const newRows = useSelector(edit.store, (state) => state.newRows);
|
|
70
|
+
// The reopen gesture: `begin` on a confirmed entry row flips it back to
|
|
71
|
+
// editors and names the cell double-clicked - where the caret goes.
|
|
72
|
+
const activeEntry = useSelector(edit.store, (state) =>
|
|
73
|
+
state.active !== null &&
|
|
74
|
+
state.newRows.some(
|
|
75
|
+
(newRow) => newRow.tempId === state.active?.rowId && !newRow.confirmed,
|
|
76
|
+
)
|
|
77
|
+
? state.active
|
|
78
|
+
: null,
|
|
79
|
+
);
|
|
70
80
|
const blockRef = useRef<HTMLDivElement>(null);
|
|
71
81
|
/** The entry row the caret has already been placed in. */
|
|
72
82
|
const focusedTempIdRef = useRef<string | null>(null);
|
|
83
|
+
/** The reopen target (`rowId:columnId`) the caret has already landed in. */
|
|
84
|
+
const focusedActiveRef = useRef<string | null>(null);
|
|
73
85
|
|
|
74
86
|
/**
|
|
75
87
|
* The caret goes into a row the moment `edit.addRow()` opens it, landing in
|
|
@@ -96,14 +108,44 @@ export function TMDataGridEntryRows({
|
|
|
96
108
|
focusEditorContent(editor);
|
|
97
109
|
});
|
|
98
110
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
111
|
+
/**
|
|
112
|
+
* The caret for a reopen: `begin` on a confirmed row re-arms its editors
|
|
113
|
+
* and the double-clicked cell's editor should hold the caret, not the
|
|
114
|
+
* row's first. Keyed separately from the add effect above - the row was
|
|
115
|
+
* already focused once when it was added.
|
|
116
|
+
*/
|
|
117
|
+
useLayoutEffect(() => {
|
|
118
|
+
if (activeEntry === null) {
|
|
119
|
+
focusedActiveRef.current = null;
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
const key = `${activeEntry.rowId}:${activeEntry.columnId ?? ""}`;
|
|
123
|
+
if (focusedActiveRef.current === key) return;
|
|
124
|
+
const block = blockRef.current;
|
|
125
|
+
if (block === null) return;
|
|
126
|
+
const editor = block.querySelector<HTMLElement>(
|
|
127
|
+
activeEntry.columnId !== null
|
|
128
|
+
? `[data-dg-part="editor"][data-row-id="${CSS.escape(activeEntry.rowId)}"][data-column-id="${CSS.escape(activeEntry.columnId)}"]`
|
|
129
|
+
: `[data-dg-part="editor"][data-row-id="${CSS.escape(activeEntry.rowId)}"]`,
|
|
130
|
+
);
|
|
131
|
+
// Not mounted yet; the next render tries again, as the add effect does.
|
|
132
|
+
if (editor === null) return;
|
|
133
|
+
focusedActiveRef.current = key;
|
|
134
|
+
focusEditorContent(editor);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
// Unconfirmed rows keep the seed values frozen at addRow - the live values
|
|
138
|
+
// belong to the forms, which the editors read directly, and this table only
|
|
139
|
+
// provides row and cell identity. A confirmed row's cells render values, so
|
|
140
|
+
// there the draft itself is the row; the memo recomputes on every
|
|
141
|
+
// confirm/reopen because either flips `newRows`' identity.
|
|
102
142
|
const data = useMemo(
|
|
103
143
|
() =>
|
|
104
144
|
newRows.map(
|
|
105
|
-
({ tempId }) =>
|
|
106
|
-
(
|
|
145
|
+
({ tempId, confirmed }) =>
|
|
146
|
+
((confirmed
|
|
147
|
+
? edit.getForm(tempId)?.state.values
|
|
148
|
+
: edit.getForm(tempId)?.options.defaultValues) ??
|
|
107
149
|
{}) as TMDataGridRowData,
|
|
108
150
|
),
|
|
109
151
|
[newRows, edit],
|
|
@@ -119,77 +161,134 @@ export function TMDataGridEntryRows({
|
|
|
119
161
|
if (newRows.length === 0) return null;
|
|
120
162
|
|
|
121
163
|
const entryRows = entryTable.getCoreRowModel().rows;
|
|
164
|
+
const confirmedById = new Map(
|
|
165
|
+
newRows.map((newRow) => [newRow.tempId, newRow.confirmed]),
|
|
166
|
+
);
|
|
167
|
+
// A row being *typed* into is always sticky - it exists nowhere else to
|
|
168
|
+
// scroll back to. A confirmed row joins the scrolling flow unless
|
|
169
|
+
// `newRowsSticky` keeps it pinned, so entering many rows cannot fill the
|
|
170
|
+
// viewport with sticky chrome.
|
|
171
|
+
const stickyRows = entryRows.filter(
|
|
172
|
+
(entryRow) =>
|
|
173
|
+
features.editNewRowsSticky || confirmedById.get(entryRow.id) !== true,
|
|
174
|
+
);
|
|
175
|
+
const flowRows = features.editNewRowsSticky
|
|
176
|
+
? []
|
|
177
|
+
: entryRows.filter((entryRow) => confirmedById.get(entryRow.id) === true);
|
|
178
|
+
|
|
179
|
+
const renderEntryRow = (
|
|
180
|
+
entryRow: (typeof entryRows)[number],
|
|
181
|
+
pinnedZ: string,
|
|
182
|
+
) => {
|
|
183
|
+
const cellsById = new Map(
|
|
184
|
+
entryRow.getAllCells().map((cell) => [cell.column.id, cell]),
|
|
185
|
+
);
|
|
186
|
+
const confirmed = confirmedById.get(entryRow.id) === true;
|
|
187
|
+
return (
|
|
188
|
+
<div
|
|
189
|
+
key={entryRow.id}
|
|
190
|
+
role="row"
|
|
191
|
+
data-dg-part="entry-row"
|
|
192
|
+
data-row-id={entryRow.id}
|
|
193
|
+
data-new
|
|
194
|
+
data-confirmed={confirmed}
|
|
195
|
+
className={classes.entryRow}
|
|
196
|
+
>
|
|
197
|
+
{orderedColumns.map((column) => {
|
|
198
|
+
const cell = cellsById.get(column.id);
|
|
199
|
+
const layout = layoutFor(column.id);
|
|
200
|
+
const editable =
|
|
201
|
+
cell !== undefined && isEntryCellEditable(entryRow, column);
|
|
202
|
+
return (
|
|
203
|
+
<div
|
|
204
|
+
key={column.id}
|
|
205
|
+
role="cell"
|
|
206
|
+
data-column-id={column.id}
|
|
207
|
+
data-align={getColumnAlign(column)}
|
|
208
|
+
data-control-column={isControlColumn(column.id)}
|
|
209
|
+
// A confirmed row re-opens where it is double-clicked, the
|
|
210
|
+
// same gesture a body cell answers.
|
|
211
|
+
onDoubleClick={
|
|
212
|
+
confirmed && !isControlColumn(column.id)
|
|
213
|
+
? () =>
|
|
214
|
+
edit.begin({
|
|
215
|
+
rowId: entryRow.id,
|
|
216
|
+
columnId: editable ? column.id : null,
|
|
217
|
+
})
|
|
218
|
+
: undefined
|
|
219
|
+
}
|
|
220
|
+
className={[
|
|
221
|
+
classes.entryCell,
|
|
222
|
+
layout.isBoundary && layout.pinnedAt === "left"
|
|
223
|
+
? sticky.stickyLeft
|
|
224
|
+
: "",
|
|
225
|
+
layout.isBoundary && layout.pinnedAt === "right"
|
|
226
|
+
? sticky.stickyRight
|
|
227
|
+
: "",
|
|
228
|
+
]
|
|
229
|
+
.filter(Boolean)
|
|
230
|
+
.join(" ")}
|
|
231
|
+
style={{
|
|
232
|
+
minHeight: rowHeight,
|
|
233
|
+
left: layout.pinnedAt === "left" ? layout.offset : undefined,
|
|
234
|
+
right: layout.pinnedAt === "right" ? layout.offset : undefined,
|
|
235
|
+
position: layout.pinnedAt ? "sticky" : undefined,
|
|
236
|
+
zIndex: layout.pinnedAt ? pinnedZ : undefined,
|
|
237
|
+
}}
|
|
238
|
+
>
|
|
239
|
+
{cell !== undefined && column.id === EDIT_COLUMN_ID ? (
|
|
240
|
+
// The lane's cell - the entry row's controls.
|
|
241
|
+
flexRender(cell.column.columnDef.cell, cell.getContext())
|
|
242
|
+
) : confirmed && cell !== undefined ? (
|
|
243
|
+
// Entered, awaiting Save all: a value row through the
|
|
244
|
+
// columns' own renderers, over the draft the memo above
|
|
245
|
+
// fed this table.
|
|
246
|
+
<span className={classes.cellContent}>
|
|
247
|
+
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
|
248
|
+
</span>
|
|
249
|
+
) : editable && cell !== undefined ? (
|
|
250
|
+
<TMDataGridCellEditor
|
|
251
|
+
cell={cell}
|
|
252
|
+
row={entryRow}
|
|
253
|
+
takeSeedText={() => undefined}
|
|
254
|
+
onClose={() => {}}
|
|
255
|
+
inEntryBlock
|
|
256
|
+
/>
|
|
257
|
+
) : null}
|
|
258
|
+
</div>
|
|
259
|
+
);
|
|
260
|
+
})}
|
|
261
|
+
</div>
|
|
262
|
+
);
|
|
263
|
+
};
|
|
122
264
|
|
|
123
265
|
return (
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
data-column-id={column.id}
|
|
152
|
-
data-align={getColumnAlign(column)}
|
|
153
|
-
data-control-column={isControlColumn(column.id)}
|
|
154
|
-
className={[
|
|
155
|
-
classes.entryCell,
|
|
156
|
-
layout.isBoundary && layout.pinnedAt === "left"
|
|
157
|
-
? sticky.stickyLeft
|
|
158
|
-
: "",
|
|
159
|
-
layout.isBoundary && layout.pinnedAt === "right"
|
|
160
|
-
? sticky.stickyRight
|
|
161
|
-
: "",
|
|
162
|
-
]
|
|
163
|
-
.filter(Boolean)
|
|
164
|
-
.join(" ")}
|
|
165
|
-
style={{
|
|
166
|
-
minHeight: rowHeight,
|
|
167
|
-
left: layout.pinnedAt === "left" ? layout.offset : undefined,
|
|
168
|
-
right:
|
|
169
|
-
layout.pinnedAt === "right" ? layout.offset : undefined,
|
|
170
|
-
position: layout.pinnedAt ? "sticky" : undefined,
|
|
171
|
-
zIndex: layout.pinnedAt
|
|
172
|
-
? "var(--dg-z-pinned-row-pinned-cell, 5)"
|
|
173
|
-
: undefined,
|
|
174
|
-
}}
|
|
175
|
-
>
|
|
176
|
-
{editable && cell !== undefined ? (
|
|
177
|
-
<TMDataGridCellEditor
|
|
178
|
-
cell={cell}
|
|
179
|
-
row={entryRow}
|
|
180
|
-
takeSeedText={() => undefined}
|
|
181
|
-
onClose={() => {}}
|
|
182
|
-
/>
|
|
183
|
-
) : cell !== undefined && column.id === EDIT_COLUMN_ID ? (
|
|
184
|
-
// The lane's cell - the entry row's ✓/✕ pair.
|
|
185
|
-
flexRender(cell.column.columnDef.cell, cell.getContext())
|
|
186
|
-
) : null}
|
|
187
|
-
</div>
|
|
188
|
-
);
|
|
189
|
-
})}
|
|
190
|
-
</div>
|
|
191
|
-
);
|
|
192
|
-
})}
|
|
193
|
-
</div>
|
|
266
|
+
<>
|
|
267
|
+
{stickyRows.length > 0 && (
|
|
268
|
+
<div
|
|
269
|
+
ref={blockRef}
|
|
270
|
+
role="rowgroup"
|
|
271
|
+
data-dg-entry-block
|
|
272
|
+
className={classes.entryBlock}
|
|
273
|
+
>
|
|
274
|
+
{stickyRows.map((entryRow) =>
|
|
275
|
+
renderEntryRow(entryRow, "var(--dg-z-pinned-row-pinned-cell, 5)"),
|
|
276
|
+
)}
|
|
277
|
+
</div>
|
|
278
|
+
)}
|
|
279
|
+
{flowRows.length > 0 && (
|
|
280
|
+
<div
|
|
281
|
+
role="rowgroup"
|
|
282
|
+
data-dg-entry-flow-block
|
|
283
|
+
className={classes.entryFlowBlock}
|
|
284
|
+
>
|
|
285
|
+
{/* In flow, so a pinned cell stacks like a body row's, under the
|
|
286
|
+
sticky blocks it scrolls past. */}
|
|
287
|
+
{flowRows.map((entryRow) =>
|
|
288
|
+
renderEntryRow(entryRow, "var(--dg-z-pinned-cell, 2)"),
|
|
289
|
+
)}
|
|
290
|
+
</div>
|
|
291
|
+
)}
|
|
292
|
+
</>
|
|
194
293
|
);
|
|
195
294
|
}
|
|
@@ -329,12 +329,18 @@
|
|
|
329
329
|
padding: var(--mantine-spacing-xl);
|
|
330
330
|
}
|
|
331
331
|
|
|
332
|
-
/* Marked deleted under
|
|
333
|
-
reports it
|
|
334
|
-
|
|
332
|
+
/* Marked deleted under draft: struck through and inert until submitAll
|
|
333
|
+
reports it - a row about to go away must not keep taking edits. Interaction
|
|
334
|
+
comes off the data cells, not the row: `pointer-events: none` on the row
|
|
335
|
+
would also kill the edit lane's Restore, which is the one control the row
|
|
336
|
+
still needs (jsdom does not enforce pointer-events, so only a real browser
|
|
337
|
+
ever saw that bug). */
|
|
335
338
|
.bodyRow[data-deleted="true"] {
|
|
336
339
|
text-decoration: line-through;
|
|
337
340
|
opacity: 0.55;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
.bodyRow[data-deleted="true"] .bodyCell:not([data-control-column="true"]) {
|
|
338
344
|
pointer-events: none;
|
|
339
345
|
}
|
|
340
346
|
|
|
@@ -359,6 +365,15 @@
|
|
|
359
365
|
grid-template-columns: subgrid;
|
|
360
366
|
}
|
|
361
367
|
|
|
368
|
+
/* Confirmed entry rows in the scrolling flow (the default; `newRowsSticky`
|
|
369
|
+
keeps them in the sticky block instead). Same tracks, no stickiness - they
|
|
370
|
+
sit above the body rows and scroll with them. */
|
|
371
|
+
.entryFlowBlock {
|
|
372
|
+
display: grid;
|
|
373
|
+
grid-column: 1 / -1;
|
|
374
|
+
grid-template-columns: subgrid;
|
|
375
|
+
}
|
|
376
|
+
|
|
362
377
|
/* The pinned edge blocks - user-pinned rows held at the top or bottom of the
|
|
363
378
|
body, on the entry block's sticky mechanics: the block is the sticky
|
|
364
379
|
element, its rows flow normally inside it. The top block stacks under the
|
|
@@ -407,6 +422,35 @@
|
|
|
407
422
|
justify-content: center;
|
|
408
423
|
}
|
|
409
424
|
|
|
425
|
+
/* A confirmed entry row - entered, awaiting Save all. It reads as a value
|
|
426
|
+
row: text padding, the body's border weight, and the "new" tint instead of
|
|
427
|
+
the editing underline. */
|
|
428
|
+
.entryRow[data-confirmed="true"] .entryCell {
|
|
429
|
+
padding: 0 var(--dg-padding);
|
|
430
|
+
border-bottom: 1px solid var(--mantine-color-default-border);
|
|
431
|
+
background-color: var(--dg-row-new-bg, var(--mantine-color-body));
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
.entryRow[data-confirmed="true"] .entryCell[data-control-column="true"] {
|
|
435
|
+
padding: 0;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
/* Draft mode's change marker in the edit lane - dimmed beside the buttons
|
|
439
|
+
that act, red when validation blocks the row. */
|
|
440
|
+
.rowStateIndicator {
|
|
441
|
+
display: inline-flex;
|
|
442
|
+
align-items: center;
|
|
443
|
+
justify-content: center;
|
|
444
|
+
width: 22px;
|
|
445
|
+
height: 22px;
|
|
446
|
+
flex: 0 0 auto;
|
|
447
|
+
color: var(--mantine-color-dimmed);
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
.rowStateIndicator[data-error] {
|
|
451
|
+
color: var(--mantine-color-error);
|
|
452
|
+
}
|
|
453
|
+
|
|
410
454
|
.entryCell[data-align="right"] {
|
|
411
455
|
justify-content: flex-end;
|
|
412
456
|
}
|
|
@@ -35,6 +35,7 @@ import {
|
|
|
35
35
|
} from "./TMDataGridCellEditor";
|
|
36
36
|
import { TMDataGridEntryRows } from "./TMDataGridEntryRows";
|
|
37
37
|
import { getColumnAlign, isControlColumn } from "../core/columnUtils";
|
|
38
|
+
import { draftCellContext } from "../core/draftCellContext";
|
|
38
39
|
import {
|
|
39
40
|
getEditFieldName,
|
|
40
41
|
type TMDataGridEditState,
|
|
@@ -266,6 +267,7 @@ function TMDataGridBodyCell({
|
|
|
266
267
|
const { table, edit } = useTMDataGridContext();
|
|
267
268
|
const fieldName = getEditFieldName(cell.column);
|
|
268
269
|
const cellRowId = cell.row.id;
|
|
270
|
+
const isControl = isControlColumn(cell.column.id);
|
|
269
271
|
const isDirty = useSelector(edit.store, (state) => {
|
|
270
272
|
if (fieldName === null) return false;
|
|
271
273
|
const projection = state.rows[cellRowId];
|
|
@@ -276,6 +278,13 @@ function TMDataGridBodyCell({
|
|
|
276
278
|
const projection = state.rows[cellRowId];
|
|
277
279
|
return projection !== undefined && projection.errorFields.includes(fieldName);
|
|
278
280
|
});
|
|
281
|
+
// The whole draft, not one field: a computed or custom renderer may read any
|
|
282
|
+
// field off the row, so every data cell of a drafted row repaints together
|
|
283
|
+
// when its draft moves - and only then, `values` being reference-stable
|
|
284
|
+
// across meta-only form events. `undefined` everywhere else.
|
|
285
|
+
const draftValues = useSelector(edit.store, (state) =>
|
|
286
|
+
isControl ? undefined : state.rows[cellRowId]?.values,
|
|
287
|
+
);
|
|
279
288
|
return (
|
|
280
289
|
<div
|
|
281
290
|
// `gridcell` rather than `cell` is what tells a screen reader the arrow
|
|
@@ -319,7 +328,7 @@ function TMDataGridBodyCell({
|
|
|
319
328
|
data-align={getColumnAlign(cell.column)}
|
|
320
329
|
// A control lane is a fixed track, so it cannot take the cell padding the
|
|
321
330
|
// scale grows for text. See isControlColumn.
|
|
322
|
-
data-control-column={
|
|
331
|
+
data-control-column={isControl}
|
|
323
332
|
onContextMenu={onContextMenu}
|
|
324
333
|
className={[
|
|
325
334
|
classes.bodyCell,
|
|
@@ -354,6 +363,17 @@ function TMDataGridBodyCell({
|
|
|
354
363
|
{editor ?? (
|
|
355
364
|
<span className={classes.cellContent}>
|
|
356
365
|
{(() => {
|
|
366
|
+
// A row with a live form shows its draft, not `data`: the column's
|
|
367
|
+
// own renderer over a context reading the form's values. Parked
|
|
368
|
+
// drafts - draft mode, cellConfirm's kept-on-blur - are the point;
|
|
369
|
+
// group cells are excluded because no group row ever has a form.
|
|
370
|
+
const draftContext =
|
|
371
|
+
draftValues !== undefined &&
|
|
372
|
+
contentOverride === undefined &&
|
|
373
|
+
!cell.getIsPlaceholder() &&
|
|
374
|
+
!cell.getIsAggregated()
|
|
375
|
+
? draftCellContext(cell, draftValues)
|
|
376
|
+
: undefined;
|
|
357
377
|
// Highlighting reproduces the *default* renderer, value then
|
|
358
378
|
// `toString`, with the matched parts marked. A column with its own
|
|
359
379
|
// `cell` is excluded: its renderer is not the default one, and the
|
|
@@ -365,11 +385,17 @@ function TMDataGridBodyCell({
|
|
|
365
385
|
!cell.getIsAggregated() &&
|
|
366
386
|
cell.column.columnDef.cell === table.getDefaultColumnDef().cell
|
|
367
387
|
) {
|
|
368
|
-
const value =
|
|
388
|
+
const value =
|
|
389
|
+
draftContext !== undefined
|
|
390
|
+
? draftContext.renderValue()
|
|
391
|
+
: cell.renderValue();
|
|
369
392
|
if (typeof value === "string" || typeof value === "number") {
|
|
370
393
|
return withMatchHighlights(String(value), highlightNeedles);
|
|
371
394
|
}
|
|
372
395
|
}
|
|
396
|
+
if (draftContext !== undefined) {
|
|
397
|
+
return flexRender(cell.column.columnDef.cell, draftContext);
|
|
398
|
+
}
|
|
373
399
|
return contentOverride ?? renderCellContent(cell);
|
|
374
400
|
})()}
|
|
375
401
|
</span>
|
|
@@ -716,6 +742,14 @@ export function TMDataGridTable<TData extends RowData = TMDataGridRowData>({
|
|
|
716
742
|
const editOpenRowIds = useSelector(edit.store, (state) => state.openRowIds);
|
|
717
743
|
const newRowCount = useSelector(edit.store, (state) => state.newRows.length);
|
|
718
744
|
const deletedRowIds = useSelector(edit.store, (state) => state.deletedRowIds);
|
|
745
|
+
// Rows carrying a dirty draft, for the row-level marker. Derived from the
|
|
746
|
+
// same projections the cells subscribe to; the array's identity churns with
|
|
747
|
+
// `openRowIds`' own per-publish churn, so this adds no new render class.
|
|
748
|
+
const editDirtyRowIds = useSelector(edit.store, (state) =>
|
|
749
|
+
state.openRowIds.filter(
|
|
750
|
+
(id) => (state.rows[id]?.dirtyFields.length ?? 0) > 0,
|
|
751
|
+
),
|
|
752
|
+
);
|
|
719
753
|
|
|
720
754
|
const { loading, noResultsLabel = labels.noResults } =
|
|
721
755
|
table.options.meta ?? {};
|
|
@@ -1497,7 +1531,7 @@ export function TMDataGridTable<TData extends RowData = TMDataGridRowData>({
|
|
|
1497
1531
|
close: TMDataGridCellEditorClose,
|
|
1498
1532
|
) => {
|
|
1499
1533
|
// Where the keyboard goes: Enter moves down, Tab to the next editable
|
|
1500
|
-
// cell (the deferring
|
|
1534
|
+
// cell (the deferring draft variants move the same way, draft in tow) -
|
|
1501
1535
|
// everything else, and a saved row, goes back to where the edit was.
|
|
1502
1536
|
const move =
|
|
1503
1537
|
close.via === "enter"
|
|
@@ -2188,11 +2222,17 @@ export function TMDataGridTable<TData extends RowData = TMDataGridRowData>({
|
|
|
2188
2222
|
data-highlighted={
|
|
2189
2223
|
features.highlightRow && row.id === highlightedRowId
|
|
2190
2224
|
}
|
|
2191
|
-
// Marked deleted under
|
|
2225
|
+
// Marked deleted under draft: struck through and inert
|
|
2192
2226
|
// until submitAll reports it, or the mark is toggled back.
|
|
2193
2227
|
data-deleted={
|
|
2194
2228
|
deletedRowIds.length > 0 && deletedRowIds.includes(row.id)
|
|
2195
2229
|
}
|
|
2230
|
+
// Carrying a dirty draft - the row-level face of the cells'
|
|
2231
|
+
// own data-dirty markers, for row-scoped styling.
|
|
2232
|
+
data-dirty={
|
|
2233
|
+
editDirtyRowIds.length > 0 &&
|
|
2234
|
+
editDirtyRowIds.includes(row.id)
|
|
2235
|
+
}
|
|
2196
2236
|
// The menu is anchored to the rowgroup, so Mantine's own
|
|
2197
2237
|
// `data-expanded` lands there rather than on a row. This is
|
|
2198
2238
|
// what says which row the open menu is about.
|
|
@@ -122,6 +122,13 @@ export type TMDataGridFeatureFlags = {
|
|
|
122
122
|
editing: boolean;
|
|
123
123
|
/** The commit policy, or `null` while editing is off. */
|
|
124
124
|
editMode: TMDataGridEditMode | null;
|
|
125
|
+
/**
|
|
126
|
+
* Whether a confirmed entry row stays pinned in the sticky entry block
|
|
127
|
+
* until Save all - `editing.newRowsSticky`, draft mode only. Off by
|
|
128
|
+
* default: a confirmed row joins the scrolling flow instead, so entering
|
|
129
|
+
* many rows cannot fill the viewport with sticky chrome.
|
|
130
|
+
*/
|
|
131
|
+
editNewRowsSticky: boolean;
|
|
125
132
|
/** The generated row-number gutter - `enableRowNumbers`. Off by default. */
|
|
126
133
|
rowNumbers: boolean;
|
|
127
134
|
/**
|
|
@@ -204,6 +211,9 @@ export function readFeatureFlags<TData extends RowData>(
|
|
|
204
211
|
grouping: options.enableGrouping ?? options.manualPagination !== true,
|
|
205
212
|
editing: options.editing !== undefined,
|
|
206
213
|
editMode: options.editing?.mode ?? null,
|
|
214
|
+
editNewRowsSticky:
|
|
215
|
+
options.editing?.mode === "draft" &&
|
|
216
|
+
options.editing.newRowsSticky === true,
|
|
207
217
|
rowNumbers: options.enableRowNumbers === true,
|
|
208
218
|
// A predicate counts as on - some rows may still pin.
|
|
209
219
|
rowPinning:
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import type { Cell, CellContext, Row } from "@tanstack/react-table";
|
|
2
|
+
import type { TMDataGridRowData } from "../TMDataGridContext";
|
|
3
|
+
import type { TMDataGridFeatures } from "../useTMDataGrid";
|
|
4
|
+
|
|
5
|
+
type ErasedCell = Cell<TMDataGridFeatures, TMDataGridRowData, unknown>;
|
|
6
|
+
type ErasedRow = Row<TMDataGridFeatures, TMDataGridRowData>;
|
|
7
|
+
type ErasedCellContext = CellContext<
|
|
8
|
+
TMDataGridFeatures,
|
|
9
|
+
TMDataGridRowData,
|
|
10
|
+
unknown
|
|
11
|
+
>;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* The row internals the draft wrapper overrides. Private to table-core, but
|
|
15
|
+
* load-bearing for it: `row.getValue` reads `_valuesCache` before running
|
|
16
|
+
* `column.accessorFn(row.original, row.index)`, so owning fresh caches and a
|
|
17
|
+
* draft `original` is what makes every accessor - key, function, dot path -
|
|
18
|
+
* re-derive from the draft. Pinned by draftCellContext.test.ts, which is the
|
|
19
|
+
* tripwire for a beta bump moving these.
|
|
20
|
+
*/
|
|
21
|
+
type RowInternals = {
|
|
22
|
+
original: TMDataGridRowData;
|
|
23
|
+
_valuesCache: Record<string, unknown>;
|
|
24
|
+
_uniqueValuesCache: Record<string, unknown>;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* A cell render context whose values come from a row's edit draft instead of
|
|
29
|
+
* `data` - how a parked draft is shown through the column's own `cell`
|
|
30
|
+
* renderer.
|
|
31
|
+
*
|
|
32
|
+
* v9 constructs rows and cells as `Object.create(prototype)` instances whose
|
|
33
|
+
* API methods dispatch on `this`, so a wrapper inheriting the real object but
|
|
34
|
+
* owning `original` and empty value caches serves draft values from the same
|
|
35
|
+
* code paths the real row uses. The context is hand-built rather than taken
|
|
36
|
+
* from the wrapper's `getContext()`, which is memoized per instance and would
|
|
37
|
+
* be inherited from the real cell.
|
|
38
|
+
*
|
|
39
|
+
* Limitation, accepted: instance-memoized traversals (`row.getAllCells()` and
|
|
40
|
+
* friends) are inherited already-bound to the real row, so a renderer walking
|
|
41
|
+
* `ctx.row` beyond `original` / `getValue()` / `renderValue()` sees committed
|
|
42
|
+
* values. The documented renderer surface sees the draft.
|
|
43
|
+
*/
|
|
44
|
+
export function draftCellContext(
|
|
45
|
+
cell: ErasedCell,
|
|
46
|
+
draftValues: TMDataGridRowData,
|
|
47
|
+
): ErasedCellContext {
|
|
48
|
+
const draftRow = Object.create(cell.row) as ErasedRow;
|
|
49
|
+
const internals = draftRow as unknown as RowInternals;
|
|
50
|
+
internals.original = draftValues;
|
|
51
|
+
internals._valuesCache = Object.create(null) as Record<string, unknown>;
|
|
52
|
+
internals._uniqueValuesCache = Object.create(null) as Record<string, unknown>;
|
|
53
|
+
|
|
54
|
+
const draftCell = Object.create(cell) as ErasedCell;
|
|
55
|
+
(draftCell as { row: ErasedRow }).row = draftRow;
|
|
56
|
+
|
|
57
|
+
return {
|
|
58
|
+
table: cell.table,
|
|
59
|
+
column: cell.column,
|
|
60
|
+
row: draftRow,
|
|
61
|
+
cell: draftCell,
|
|
62
|
+
getValue: () => draftRow.getValue(cell.column.id),
|
|
63
|
+
renderValue: () =>
|
|
64
|
+
draftRow.getValue(cell.column.id) ??
|
|
65
|
+
cell.table.options.renderFallbackValue ??
|
|
66
|
+
null,
|
|
67
|
+
};
|
|
68
|
+
}
|