@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,9 +1,9 @@
|
|
|
1
1
|
import { ActionIcon, Group, Loader, Tooltip } from "@mantine/core";
|
|
2
2
|
import { useSelector } from "@tanstack/react-store";
|
|
3
|
-
import type {
|
|
3
|
+
import type { Row, RowData } from "@tanstack/react-table";
|
|
4
4
|
import classes from "./TMDataGridTable.module.css";
|
|
5
5
|
import {
|
|
6
|
-
|
|
6
|
+
useBodyControlTabIndex,
|
|
7
7
|
useTMDataGridContext,
|
|
8
8
|
} from "../TMDataGridContext";
|
|
9
9
|
import type { TMDataGridFeatures } from "../useTMDataGrid";
|
|
@@ -65,18 +65,22 @@ function RowStateIndicator({
|
|
|
65
65
|
* | open | the mode's own controls: Save and Cancel | `editing.mode` |
|
|
66
66
|
* | neither | the pencil (row mode) and the trash | `editing.mode` |
|
|
67
67
|
*
|
|
68
|
-
* A row error turns the Save red with the message in
|
|
69
|
-
* pathless `.refine()` has nowhere else to land, and a field
|
|
70
|
-
* the editor that found it. A parked row never offers a save:
|
|
71
|
-
* submit, and `TMDataGrid.DraftActions` is what sends it.
|
|
68
|
+
* A row error turns the Save - or an entry row's ✓ - red with the message in
|
|
69
|
+
* its tooltip: a pathless `.refine()` has nowhere else to land, and a field
|
|
70
|
+
* error outlives the editor that found it. A parked row never offers a save:
|
|
71
|
+
* it has had its submit, and `TMDataGrid.DraftActions` is what sends it.
|
|
72
|
+
*
|
|
73
|
+
* The lane's controls take the tab index every body control does - see
|
|
74
|
+
* useBodyControlTabIndex. The open row's save and cancel are the exception,
|
|
75
|
+
* see where they are built.
|
|
72
76
|
*/
|
|
73
|
-
function EditLaneCell<TData extends RowData>({
|
|
77
|
+
export function EditLaneCell<TData extends RowData>({
|
|
74
78
|
row,
|
|
75
79
|
}: {
|
|
76
80
|
row: Row<TMDataGridFeatures, TData>;
|
|
77
81
|
}) {
|
|
78
82
|
const { edit, features, labels } = useTMDataGridContext();
|
|
79
|
-
const tabIndex =
|
|
83
|
+
const tabIndex = useBodyControlTabIndex();
|
|
80
84
|
const rowId = row.id;
|
|
81
85
|
const isOpen = useSelector(edit.store, (state) =>
|
|
82
86
|
state.openRowIds.includes(rowId),
|
|
@@ -182,10 +186,10 @@ function EditLaneCell<TData extends RowData>({
|
|
|
182
186
|
}
|
|
183
187
|
return (
|
|
184
188
|
<Group gap={0} wrap="nowrap">
|
|
185
|
-
<Tooltip label={labels.confirmNewRow} withArrow>
|
|
189
|
+
<Tooltip label={rowError ?? labels.confirmNewRow} withArrow>
|
|
186
190
|
<ActionIcon
|
|
187
191
|
variant="subtle"
|
|
188
|
-
color="green"
|
|
192
|
+
color={hasErrors ? "red" : "green"}
|
|
189
193
|
size="sm"
|
|
190
194
|
tabIndex={tabIndex}
|
|
191
195
|
aria-label={labels.confirmNewRow}
|
|
@@ -235,7 +239,7 @@ function EditLaneCell<TData extends RowData>({
|
|
|
235
239
|
data-row-id={rowId}
|
|
236
240
|
onClick={(event) => {
|
|
237
241
|
event.stopPropagation();
|
|
238
|
-
edit.
|
|
242
|
+
edit.restoreRow(rowId);
|
|
239
243
|
}}
|
|
240
244
|
>
|
|
241
245
|
<RestoreIcon size={16} stroke={1.6} />
|
|
@@ -312,12 +316,15 @@ function EditLaneCell<TData extends RowData>({
|
|
|
312
316
|
);
|
|
313
317
|
}
|
|
314
318
|
|
|
319
|
+
// An open row is a form, and its save and cancel are the form's last two
|
|
320
|
+
// fields: Tab walks into them from the last editor. The closed lane's
|
|
321
|
+
// controls stay out of the tab order like every other control in a cell.
|
|
315
322
|
const save = (
|
|
316
323
|
<ActionIcon
|
|
317
324
|
variant="subtle"
|
|
318
325
|
color={hasErrors ? "red" : "green"}
|
|
319
326
|
size="sm"
|
|
320
|
-
tabIndex={
|
|
327
|
+
tabIndex={0}
|
|
321
328
|
aria-label={labels.saveRow}
|
|
322
329
|
data-dg-part="save-row"
|
|
323
330
|
data-row-id={rowId}
|
|
@@ -332,21 +339,15 @@ function EditLaneCell<TData extends RowData>({
|
|
|
332
339
|
|
|
333
340
|
return (
|
|
334
341
|
<Group gap={0} wrap="nowrap">
|
|
335
|
-
{rowError
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
</Tooltip>
|
|
339
|
-
) : (
|
|
340
|
-
<Tooltip label={rowError} withArrow>
|
|
341
|
-
{save}
|
|
342
|
-
</Tooltip>
|
|
343
|
-
)}
|
|
342
|
+
<Tooltip label={rowError ?? labels.saveRow} withArrow>
|
|
343
|
+
{save}
|
|
344
|
+
</Tooltip>
|
|
344
345
|
<Tooltip label={labels.cancelRowEdit} withArrow>
|
|
345
346
|
<ActionIcon
|
|
346
347
|
variant="subtle"
|
|
347
348
|
color="gray"
|
|
348
349
|
size="sm"
|
|
349
|
-
tabIndex={
|
|
350
|
+
tabIndex={0}
|
|
350
351
|
aria-label={labels.cancelRowEdit}
|
|
351
352
|
data-dg-part="cancel-row"
|
|
352
353
|
data-row-id={rowId}
|
|
@@ -361,44 +362,3 @@ function EditLaneCell<TData extends RowData>({
|
|
|
361
362
|
</Group>
|
|
362
363
|
);
|
|
363
364
|
}
|
|
364
|
-
|
|
365
|
-
/**
|
|
366
|
-
* The generated edit lane, appended and pinned right - the row's Save at the
|
|
367
|
-
* end of the row under `mode: "row"`, the state marker and revert under
|
|
368
|
-
* `editing.draft`, mirroring the checkbox lane's build on the left.
|
|
369
|
-
*/
|
|
370
|
-
export function createEditColumn<TData extends RowData>(
|
|
371
|
-
label = "Edit",
|
|
372
|
-
/** A draft lane holds three controls where the rest hold two. */
|
|
373
|
-
wide = false,
|
|
374
|
-
): ColumnDef<TMDataGridFeatures, TData, unknown> {
|
|
375
|
-
const width = wide ? 88 : 64;
|
|
376
|
-
return {
|
|
377
|
-
id: EDIT_COLUMN_ID,
|
|
378
|
-
meta: {
|
|
379
|
-
label,
|
|
380
|
-
align: "center",
|
|
381
|
-
enableOrdering: false,
|
|
382
|
-
},
|
|
383
|
-
// Wide enough for the pair (or draft's trio) it holds while editing.
|
|
384
|
-
size: width,
|
|
385
|
-
minSize: width,
|
|
386
|
-
maxSize: width,
|
|
387
|
-
enableResizing: false,
|
|
388
|
-
enableSorting: false,
|
|
389
|
-
enableColumnFilter: false,
|
|
390
|
-
enableGlobalFilter: false,
|
|
391
|
-
// The row's Save, Cancel and Delete live here, so hiding the lane would
|
|
392
|
-
// strand an open row with no way to commit or discard it. Same rule as the
|
|
393
|
-
// checkbox lane: chrome the grid generates is not a user setting.
|
|
394
|
-
enableHiding: false,
|
|
395
|
-
// Structurally pinned to the right; not movable.
|
|
396
|
-
enablePinning: false,
|
|
397
|
-
header: () => null,
|
|
398
|
-
cell: ({ row }) => <EditLaneCell row={row} />,
|
|
399
|
-
// Group rows: same reasoning as the checkbox lane - without this the
|
|
400
|
-
// cell renders blank on aggregated rows, but here blank is also correct,
|
|
401
|
-
// so the aggregated cell renders the same (null for groups).
|
|
402
|
-
aggregatedCell: ({ row }) => <EditLaneCell row={row} />,
|
|
403
|
-
};
|
|
404
|
-
}
|
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
import {
|
|
2
|
+
flexRender,
|
|
3
|
+
useTable,
|
|
4
|
+
type Cell,
|
|
5
|
+
type Column,
|
|
6
|
+
type Row,
|
|
7
|
+
} from "@tanstack/react-table";
|
|
8
|
+
import { useSelector } from "@tanstack/react-store";
|
|
9
|
+
import { shallow } from "@tanstack/store";
|
|
10
|
+
import { useLayoutEffect, useMemo, useRef } from "react";
|
|
11
|
+
import classes from "./TMDataGridTable.module.css";
|
|
12
|
+
import sticky from "./sticky.module.css";
|
|
13
|
+
import { type TMDataGridRowData, useTMDataGridContext } from "../TMDataGridContext";
|
|
14
|
+
import {
|
|
15
|
+
getColumnAlign,
|
|
16
|
+
isColumnEditableForRow,
|
|
17
|
+
isControlColumn,
|
|
18
|
+
} from "../core/columnUtils";
|
|
19
|
+
import { draftCellContext } from "../core/draftCellContext";
|
|
20
|
+
import { getEditFieldName, type TMDataGridEditApi } from "../core/editEngine";
|
|
21
|
+
import { focusEditorContent } from "../core/editorFocus";
|
|
22
|
+
import {
|
|
23
|
+
tmDataGridFeatures,
|
|
24
|
+
type TMDataGridFeatures,
|
|
25
|
+
} from "../useTMDataGrid";
|
|
26
|
+
import { TMDataGridCellEditor } from "./TMDataGridCellEditor";
|
|
27
|
+
import { EDIT_COLUMN_ID } from "./TMDataGridEditColumn";
|
|
28
|
+
import type { TMDataGridColumnLayout } from "./TMDataGridTable";
|
|
29
|
+
|
|
30
|
+
type ErasedColumn = Column<TMDataGridFeatures, TMDataGridRowData, unknown>;
|
|
31
|
+
type EntryRow = Row<TMDataGridFeatures, TMDataGridRowData>;
|
|
32
|
+
type EntryCellApi = Cell<TMDataGridFeatures, TMDataGridRowData, unknown>;
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Whether an entry cell edits. `edit.canEditCell` asks the main table for
|
|
36
|
+
* the row, and an entry row is precisely not there yet - so the structural
|
|
37
|
+
* half of the same rule is applied to the entry table's own row.
|
|
38
|
+
*/
|
|
39
|
+
function isEntryCellEditable(
|
|
40
|
+
row: Row<TMDataGridFeatures, TMDataGridRowData>,
|
|
41
|
+
column: ErasedColumn,
|
|
42
|
+
edit: TMDataGridEditApi,
|
|
43
|
+
): boolean {
|
|
44
|
+
if (!edit.isColumnEditable(column)) return false;
|
|
45
|
+
return isColumnEditableForRow(column, row);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* One cell of an entry row: the lane's controls, an editor for a cell the
|
|
50
|
+
* open row edits, or a value through the column's own renderer. The invalid
|
|
51
|
+
* marker subscribes here, per cell, as a body cell's does - a refused ✓
|
|
52
|
+
* paints the cells its issues name and nothing else. No dirty marker:
|
|
53
|
+
* everything in an entry row is new.
|
|
54
|
+
*/
|
|
55
|
+
function EntryCell({
|
|
56
|
+
entryRow,
|
|
57
|
+
column,
|
|
58
|
+
cell,
|
|
59
|
+
committed,
|
|
60
|
+
layout,
|
|
61
|
+
rowHeight,
|
|
62
|
+
pinnedZ,
|
|
63
|
+
}: {
|
|
64
|
+
entryRow: EntryRow;
|
|
65
|
+
column: ErasedColumn;
|
|
66
|
+
cell: EntryCellApi | undefined;
|
|
67
|
+
committed: boolean;
|
|
68
|
+
layout: TMDataGridColumnLayout;
|
|
69
|
+
rowHeight: number;
|
|
70
|
+
pinnedZ: string;
|
|
71
|
+
}) {
|
|
72
|
+
const { edit } = useTMDataGridContext();
|
|
73
|
+
const fieldName = getEditFieldName(column);
|
|
74
|
+
const isInvalid = useSelector(edit.store, (state) => {
|
|
75
|
+
if (fieldName === null) return false;
|
|
76
|
+
const projection = state.rows[entryRow.id];
|
|
77
|
+
return projection !== undefined && projection.errorFields.includes(fieldName);
|
|
78
|
+
});
|
|
79
|
+
const editable =
|
|
80
|
+
cell !== undefined && isEntryCellEditable(entryRow, column, edit);
|
|
81
|
+
// An open row's cells outside the editors follow the form as it is typed,
|
|
82
|
+
// as a body row's do: the column's renderer over a context reading the
|
|
83
|
+
// form's values - the whole row, so a computed cell repaints with any
|
|
84
|
+
// field. `undefined` for a committed row, whose values the entry table
|
|
85
|
+
// already holds as data, and for the lanes.
|
|
86
|
+
const draftValues = useSelector(edit.store, (state) =>
|
|
87
|
+
committed || isControlColumn(column.id)
|
|
88
|
+
? undefined
|
|
89
|
+
: state.rows[entryRow.id]?.values,
|
|
90
|
+
);
|
|
91
|
+
return (
|
|
92
|
+
<div
|
|
93
|
+
role="cell"
|
|
94
|
+
data-column-id={column.id}
|
|
95
|
+
data-align={getColumnAlign(column)}
|
|
96
|
+
data-control-column={isControlColumn(column.id) || undefined}
|
|
97
|
+
data-invalid={isInvalid || undefined}
|
|
98
|
+
// A committed row re-opens where it is double-clicked, the same
|
|
99
|
+
// gesture a body cell answers - and, like a body cell, a cell that
|
|
100
|
+
// takes no edit answers nothing.
|
|
101
|
+
onDoubleClick={
|
|
102
|
+
committed && editable
|
|
103
|
+
? () => edit.begin({ rowId: entryRow.id, columnId: column.id })
|
|
104
|
+
: undefined
|
|
105
|
+
}
|
|
106
|
+
className={[
|
|
107
|
+
classes.entryCell,
|
|
108
|
+
layout.isBoundary && layout.pinnedAt === "left" ? sticky.stickyLeft : "",
|
|
109
|
+
layout.isBoundary && layout.pinnedAt === "right"
|
|
110
|
+
? sticky.stickyRight
|
|
111
|
+
: "",
|
|
112
|
+
]
|
|
113
|
+
.filter(Boolean)
|
|
114
|
+
.join(" ")}
|
|
115
|
+
style={{
|
|
116
|
+
minHeight: rowHeight,
|
|
117
|
+
left: layout.pinnedAt === "left" ? layout.offset : undefined,
|
|
118
|
+
right: layout.pinnedAt === "right" ? layout.offset : undefined,
|
|
119
|
+
position: layout.pinnedAt ? "sticky" : undefined,
|
|
120
|
+
zIndex: layout.pinnedAt ? pinnedZ : undefined,
|
|
121
|
+
}}
|
|
122
|
+
>
|
|
123
|
+
{cell === undefined ? null : column.id === EDIT_COLUMN_ID ? (
|
|
124
|
+
// The lane's cell - the entry row's controls.
|
|
125
|
+
flexRender(cell.column.columnDef.cell, cell.getContext())
|
|
126
|
+
) : !committed && editable ? (
|
|
127
|
+
<TMDataGridCellEditor
|
|
128
|
+
cell={cell}
|
|
129
|
+
row={entryRow}
|
|
130
|
+
takeSeedText={() => undefined}
|
|
131
|
+
onClose={() => {}}
|
|
132
|
+
inEntryBlock
|
|
133
|
+
/>
|
|
134
|
+
) : committed || !isControlColumn(column.id) ? (
|
|
135
|
+
// A value through the column's own renderer. Every cell of a row
|
|
136
|
+
// entered and awaiting Save all, over the draft the entry table was
|
|
137
|
+
// fed - and, while the row is open, every cell it does not open (a
|
|
138
|
+
// display column, `meta.edit.enabled` off), over the form as it is
|
|
139
|
+
// typed, as a body row in row mode shows them. Blank was how a
|
|
140
|
+
// calculated column or a button cell vanished the moment a
|
|
141
|
+
// committed row was reopened. The generated lanes stay blank while
|
|
142
|
+
// the row is open: a checkbox or a chevron there would act on the
|
|
143
|
+
// entry table's own state, which reaches nothing.
|
|
144
|
+
<span className={classes.cellContent}>
|
|
145
|
+
{flexRender(
|
|
146
|
+
cell.column.columnDef.cell,
|
|
147
|
+
draftValues !== undefined
|
|
148
|
+
? draftCellContext(cell, draftValues)
|
|
149
|
+
: cell.getContext(),
|
|
150
|
+
)}
|
|
151
|
+
</span>
|
|
152
|
+
) : null}
|
|
153
|
+
</div>
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* The sticky entry block - one row of open editors per `edit.addRow()`,
|
|
159
|
+
* pinned under the header. The one place stickiness is genuinely required:
|
|
160
|
+
* an existing row that scrolls away has a place to scroll back to, a row
|
|
161
|
+
* being typed into does not exist anywhere else.
|
|
162
|
+
*
|
|
163
|
+
* A second, tiny table instance over the new rows' seed values - same
|
|
164
|
+
* columns, its own `useTable`, no sorting or filtering ever exercised - so
|
|
165
|
+
* these are real `Cell`s and the editor host, the typed editors and the
|
|
166
|
+
* validators all apply unchanged. The forms live in the same engine map as
|
|
167
|
+
* every other draft, keyed by their temporary ids.
|
|
168
|
+
*
|
|
169
|
+
* Enter commits the entry - `onRowAdd`, or a park under `editing.draft`,
|
|
170
|
+
* where `saveDrafts` reports it in `created`; Escape discards it. The edit
|
|
171
|
+
* lane, when present, offers the same pair as buttons.
|
|
172
|
+
*/
|
|
173
|
+
export function TMDataGridEntryRows({
|
|
174
|
+
orderedColumns,
|
|
175
|
+
layoutFor,
|
|
176
|
+
rowHeight,
|
|
177
|
+
}: {
|
|
178
|
+
orderedColumns: ReadonlyArray<ErasedColumn>;
|
|
179
|
+
layoutFor: (columnId: string) => TMDataGridColumnLayout;
|
|
180
|
+
rowHeight: number;
|
|
181
|
+
}) {
|
|
182
|
+
const { table, edit, features } = useTMDataGridContext();
|
|
183
|
+
const sticky = features.editNewRowsSticky;
|
|
184
|
+
// A row being *typed* into is always here - it exists nowhere else to
|
|
185
|
+
// scroll back to. A committed row is a body row, sorted and filtered with
|
|
186
|
+
// the rest (the hook feeds it to the table as `data`), unless
|
|
187
|
+
// `newRowsSticky` keeps it pinned here until the save. Only the rows the
|
|
188
|
+
// block renders go into its table: after an import `newRows` runs to
|
|
189
|
+
// thousands, every one of them committed and in the body.
|
|
190
|
+
const newRows = useSelector(
|
|
191
|
+
edit.store,
|
|
192
|
+
(state) =>
|
|
193
|
+
sticky
|
|
194
|
+
? state.newRows
|
|
195
|
+
: state.newRows.filter((newRow) => !newRow.committed),
|
|
196
|
+
{ compare: shallow },
|
|
197
|
+
);
|
|
198
|
+
// What a committed row renders from. It has no form - its values are the
|
|
199
|
+
// draft store's - and this is the same snapshot the table feeds a
|
|
200
|
+
// committed row in the body.
|
|
201
|
+
const committedValues = useSelector(
|
|
202
|
+
edit.store,
|
|
203
|
+
(state) => state.committedValues,
|
|
204
|
+
);
|
|
205
|
+
// The reopen gesture: `begin` on a committed entry row flips it back to
|
|
206
|
+
// editors and names the cell double-clicked - where the caret goes.
|
|
207
|
+
const activeEntry = useSelector(edit.store, (state) =>
|
|
208
|
+
state.active !== null &&
|
|
209
|
+
state.newRows.some(
|
|
210
|
+
(newRow) => newRow.tempId === state.active?.rowId && !newRow.committed,
|
|
211
|
+
)
|
|
212
|
+
? state.active
|
|
213
|
+
: null,
|
|
214
|
+
);
|
|
215
|
+
const blockRef = useRef<HTMLDivElement>(null);
|
|
216
|
+
/** The entry row the caret has already been placed in. */
|
|
217
|
+
const focusedTempIdRef = useRef<string | null>(null);
|
|
218
|
+
/** The reopen target (`rowId:columnId`) the caret has already landed in. */
|
|
219
|
+
const focusedActiveRef = useRef<string | null>(null);
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* The caret goes into a row the moment `edit.addRow()` opens it, landing in
|
|
223
|
+
* its first editable cell - the same placement the main table makes for a
|
|
224
|
+
* row opened by a gesture, and made here for the same reason: an editor is
|
|
225
|
+
* free not to focus itself, and then a new row appeared with the caret
|
|
226
|
+
* still outside it.
|
|
227
|
+
*/
|
|
228
|
+
useLayoutEffect(() => {
|
|
229
|
+
const newest = newRows.at(-1)?.tempId;
|
|
230
|
+
if (newest === undefined) {
|
|
231
|
+
focusedTempIdRef.current = null;
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
if (focusedTempIdRef.current === newest) return;
|
|
235
|
+
const block = blockRef.current;
|
|
236
|
+
if (block === null) return;
|
|
237
|
+
const editor = block.querySelector<HTMLElement>(
|
|
238
|
+
`[data-dg-part="editor"][data-row-id="${CSS.escape(newest)}"]`,
|
|
239
|
+
);
|
|
240
|
+
// Not mounted yet; the next render tries again, as the table's does.
|
|
241
|
+
if (editor === null) return;
|
|
242
|
+
focusedTempIdRef.current = newest;
|
|
243
|
+
focusEditorContent(editor);
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* The caret for a reopen: `begin` on a committed row re-arms its editors
|
|
248
|
+
* and the double-clicked cell's editor should hold the caret, not the
|
|
249
|
+
* row's first. Keyed separately from the add effect above - the row was
|
|
250
|
+
* already focused once when it was added.
|
|
251
|
+
*/
|
|
252
|
+
useLayoutEffect(() => {
|
|
253
|
+
if (activeEntry === null) {
|
|
254
|
+
focusedActiveRef.current = null;
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
const key = `${activeEntry.rowId}:${activeEntry.columnId ?? ""}`;
|
|
258
|
+
if (focusedActiveRef.current === key) return;
|
|
259
|
+
const block = blockRef.current;
|
|
260
|
+
if (block === null) return;
|
|
261
|
+
const editor = block.querySelector<HTMLElement>(
|
|
262
|
+
activeEntry.columnId !== null
|
|
263
|
+
? `[data-dg-part="editor"][data-row-id="${CSS.escape(activeEntry.rowId)}"][data-column-id="${CSS.escape(activeEntry.columnId)}"]`
|
|
264
|
+
: `[data-dg-part="editor"][data-row-id="${CSS.escape(activeEntry.rowId)}"]`,
|
|
265
|
+
);
|
|
266
|
+
// Not mounted yet; the next render tries again, as the add effect does.
|
|
267
|
+
if (editor === null) return;
|
|
268
|
+
focusedActiveRef.current = key;
|
|
269
|
+
focusEditorContent(editor);
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
// What the rows render over. A committed row is data: its values are the
|
|
273
|
+
// draft store's, which stand through a reopen. An open row's cells read
|
|
274
|
+
// the form itself - the editors directly, the rest through EntryCell's
|
|
275
|
+
// draft overlay - so its entry here gives the row an identity and a
|
|
276
|
+
// fallback: the committed values it was reopened from, else the seed
|
|
277
|
+
// frozen at addRow (the form's `defaultValues`, which a reopen keeps at
|
|
278
|
+
// the seed and writes the committed values over).
|
|
279
|
+
const data = useMemo(
|
|
280
|
+
() =>
|
|
281
|
+
newRows.map(
|
|
282
|
+
({ tempId }) =>
|
|
283
|
+
(committedValues[tempId] ??
|
|
284
|
+
edit.getForm(tempId)?.options.defaultValues ??
|
|
285
|
+
{}) as TMDataGridRowData,
|
|
286
|
+
),
|
|
287
|
+
[newRows, committedValues, edit],
|
|
288
|
+
);
|
|
289
|
+
|
|
290
|
+
const entryTable = useTable({
|
|
291
|
+
features: tmDataGridFeatures,
|
|
292
|
+
columns: table.options.columns,
|
|
293
|
+
data,
|
|
294
|
+
getRowId: (_row, index) => newRows[index]?.tempId ?? String(index),
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
if (newRows.length === 0) return null;
|
|
298
|
+
|
|
299
|
+
const stickyRows = entryTable.getCoreRowModel().rows;
|
|
300
|
+
const committedById = new Map(
|
|
301
|
+
newRows.map((newRow) => [newRow.tempId, newRow.committed]),
|
|
302
|
+
);
|
|
303
|
+
|
|
304
|
+
const renderEntryRow = (entryRow: EntryRow, pinnedZ: string) => {
|
|
305
|
+
const cellsById = new Map(
|
|
306
|
+
entryRow.getAllCells().map((cell) => [cell.column.id, cell]),
|
|
307
|
+
);
|
|
308
|
+
const committed = committedById.get(entryRow.id) === true;
|
|
309
|
+
return (
|
|
310
|
+
<div
|
|
311
|
+
key={entryRow.id}
|
|
312
|
+
role="row"
|
|
313
|
+
data-dg-part="entry-row"
|
|
314
|
+
data-row-id={entryRow.id}
|
|
315
|
+
data-new
|
|
316
|
+
data-committed={committed || undefined}
|
|
317
|
+
// The same marker body rows carry once committed, so one selector
|
|
318
|
+
// reaches everything parked in the draft store.
|
|
319
|
+
data-draft={committed || undefined}
|
|
320
|
+
className={classes.entryRow}
|
|
321
|
+
>
|
|
322
|
+
{orderedColumns.map((column) => (
|
|
323
|
+
<EntryCell
|
|
324
|
+
key={column.id}
|
|
325
|
+
entryRow={entryRow}
|
|
326
|
+
column={column}
|
|
327
|
+
cell={cellsById.get(column.id)}
|
|
328
|
+
committed={committed}
|
|
329
|
+
layout={layoutFor(column.id)}
|
|
330
|
+
rowHeight={rowHeight}
|
|
331
|
+
pinnedZ={pinnedZ}
|
|
332
|
+
/>
|
|
333
|
+
))}
|
|
334
|
+
</div>
|
|
335
|
+
);
|
|
336
|
+
};
|
|
337
|
+
|
|
338
|
+
return (
|
|
339
|
+
<>
|
|
340
|
+
{stickyRows.length > 0 && (
|
|
341
|
+
<div
|
|
342
|
+
ref={blockRef}
|
|
343
|
+
role="rowgroup"
|
|
344
|
+
data-dg-entry-block
|
|
345
|
+
className={classes.entryBlock}
|
|
346
|
+
>
|
|
347
|
+
{stickyRows.map((entryRow) =>
|
|
348
|
+
renderEntryRow(entryRow, "var(--dg-z-pinned-row-pinned-cell, 5)"),
|
|
349
|
+
)}
|
|
350
|
+
</div>
|
|
351
|
+
)}
|
|
352
|
+
</>
|
|
353
|
+
);
|
|
354
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
.title {
|
|
2
|
+
font-weight: 500;
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
.list {
|
|
6
|
+
display: flex;
|
|
7
|
+
flex-direction: column;
|
|
8
|
+
border: 1px solid var(--mantine-color-default-border);
|
|
9
|
+
border-radius: var(--mantine-radius-default);
|
|
10
|
+
overflow: hidden;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
.listHeader {
|
|
14
|
+
display: flex;
|
|
15
|
+
align-items: center;
|
|
16
|
+
justify-content: space-between;
|
|
17
|
+
gap: var(--mantine-spacing-sm);
|
|
18
|
+
padding: var(--mantine-spacing-xs) var(--mantine-spacing-sm);
|
|
19
|
+
background-color: var(--mantine-color-default-hover);
|
|
20
|
+
border-bottom: 1px solid var(--mantine-color-default-border);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
.headerLabel {
|
|
24
|
+
font-weight: 500;
|
|
25
|
+
cursor: pointer;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
.rows {
|
|
29
|
+
display: flex;
|
|
30
|
+
flex-direction: column;
|
|
31
|
+
padding: calc(var(--mantine-spacing-xs) / 2) 0;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/* Each column is one full-width row: the label stretches over the free space
|
|
35
|
+
so the whole row toggles the box, and the row highlights on hover like a
|
|
36
|
+
menu item. */
|
|
37
|
+
.row {
|
|
38
|
+
padding: 0 var(--mantine-spacing-sm);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
.row:hover {
|
|
42
|
+
background-color: var(--mantine-color-default-hover);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
.rowBody {
|
|
46
|
+
align-items: center;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
.rowLabelWrapper {
|
|
50
|
+
flex: 1;
|
|
51
|
+
min-width: 0;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
.rowLabel {
|
|
55
|
+
display: flex;
|
|
56
|
+
align-items: center;
|
|
57
|
+
justify-content: space-between;
|
|
58
|
+
gap: var(--mantine-spacing-sm);
|
|
59
|
+
padding-block: calc(var(--mantine-spacing-xs) * 0.75);
|
|
60
|
+
cursor: pointer;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
.rowText {
|
|
64
|
+
overflow: hidden;
|
|
65
|
+
text-overflow: ellipsis;
|
|
66
|
+
white-space: nowrap;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
.rowHint {
|
|
70
|
+
flex: none;
|
|
71
|
+
font-size: var(--mantine-font-size-xs);
|
|
72
|
+
color: var(--mantine-color-dimmed);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
.empty {
|
|
76
|
+
padding: var(--mantine-spacing-xs) var(--mantine-spacing-sm);
|
|
77
|
+
}
|