@jielga/tmdatagrid 2.0.0-beta.9 → 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 +1281 -768
- package/dist/index.js +4607 -3250
- 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 +46 -47
- package/skills/columns/SKILL.md +90 -34
- package/skills/data/SKILL.md +86 -16
- package/skills/editing/SKILL.md +67 -40
- package/skills/editing/references/common-mistakes.md +77 -69
- package/skills/editing/references/editing-api.md +22 -19
- 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 +7 -7
- 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/components → components}/TMDataGrid.tsx +38 -21
- package/src/{tmdatagrid/components → components}/TMDataGridCellEditor.tsx +54 -8
- 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 +5 -51
- package/src/{tmdatagrid/components → components}/TMDataGridDraftActions.tsx +41 -24
- package/src/{tmdatagrid/components → components}/TMDataGridEditColumn.tsx +12 -59
- package/src/{tmdatagrid/components → components}/TMDataGridEntryRows.tsx +164 -92
- 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 +5 -69
- 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 +11 -48
- package/src/{tmdatagrid/components → components}/TMDataGridTable.module.css +69 -56
- package/src/{tmdatagrid/components → components}/TMDataGridTable.tsx +240 -138
- 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}/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 +1107 -460
- 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} +69 -35
- package/src/{tmdatagrid/useTMDataGrid.tsx → useTMDataGrid.tsx} +428 -109
- package/src/useTMDataGridExport.ts +78 -0
- 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/TMDataGridContext.ts → TMDataGridContext.ts} +0 -0
- /package/src/{tmdatagrid/components → components}/TMDataGrid.module.css +0 -0
- /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}/capabilities.ts +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}/editorFocus.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
|
@@ -3,7 +3,7 @@ import {
|
|
|
3
3
|
useBodyControlTabIndex,
|
|
4
4
|
useTMDataGridContext,
|
|
5
5
|
} from "../TMDataGridContext";
|
|
6
|
-
import type {
|
|
6
|
+
import type { Row, RowData } from "@tanstack/react-table";
|
|
7
7
|
import { useSelector } from "@tanstack/react-store";
|
|
8
8
|
import classes from "./TMDataGridDetailsColumn.module.css";
|
|
9
9
|
import { ChevronRightIcon } from "./icons";
|
|
@@ -20,7 +20,7 @@ export const DETAILS_COLUMN_ID = "__details__";
|
|
|
20
20
|
* expand, so the React Compiler would cache the call along with it and the
|
|
21
21
|
* chevron would never turn. Same reason the select checkbox subscribes.
|
|
22
22
|
*/
|
|
23
|
-
function DetailsCell<TData extends RowData>({
|
|
23
|
+
export function DetailsCell<TData extends RowData>({
|
|
24
24
|
row,
|
|
25
25
|
}: {
|
|
26
26
|
row: Row<TMDataGridFeatures, TData>;
|
|
@@ -51,7 +51,7 @@ function DetailsCell<TData extends RowData>({
|
|
|
51
51
|
row.toggleExpanded();
|
|
52
52
|
}}
|
|
53
53
|
>
|
|
54
|
-
<span className={classes.chevron} data-expanded={expanded}>
|
|
54
|
+
<span className={classes.chevron} data-expanded={expanded || undefined}>
|
|
55
55
|
<ChevronRightIcon size={16} stroke={1.6} />
|
|
56
56
|
</span>
|
|
57
57
|
</UnstyledButton>
|
|
@@ -67,7 +67,7 @@ function DetailsCell<TData extends RowData>({
|
|
|
67
67
|
* so it would unfold every group as well. Only the data rows are touched here;
|
|
68
68
|
* whatever the tree was showing, it goes on showing. See resolveExpandAll.
|
|
69
69
|
*/
|
|
70
|
-
function DetailsHeader<TData extends RowData>({
|
|
70
|
+
export function DetailsHeader<TData extends RowData>({
|
|
71
71
|
table,
|
|
72
72
|
}: {
|
|
73
73
|
table: TMDataGridTable<TData>;
|
|
@@ -106,55 +106,9 @@ function DetailsHeader<TData extends RowData>({
|
|
|
106
106
|
)
|
|
107
107
|
}
|
|
108
108
|
>
|
|
109
|
-
<span className={classes.chevron} data-expanded={allExpanded}>
|
|
109
|
+
<span className={classes.chevron} data-expanded={allExpanded || undefined}>
|
|
110
110
|
<ChevronRightIcon size={16} stroke={1.6} />
|
|
111
111
|
</span>
|
|
112
112
|
</UnstyledButton>
|
|
113
113
|
);
|
|
114
114
|
}
|
|
115
|
-
|
|
116
|
-
/**
|
|
117
|
-
* The generated details lane, prepended whenever `renderDetails` is set.
|
|
118
|
-
*
|
|
119
|
-
* Structural, like the checkbox and tree columns: fixed width, pinned to the
|
|
120
|
-
* left after both of them, not hideable, not movable and not resizable. Moving
|
|
121
|
-
* or hiding the toggle would leave rows with panels that cannot be opened.
|
|
122
|
-
*
|
|
123
|
-
* Last of the three because it acts on a single record.
|
|
124
|
-
*
|
|
125
|
-
* A second toggle elsewhere is supported: `row.toggleExpanded()` is the entire
|
|
126
|
-
* interface, and this lane is only the control the grid ships.
|
|
127
|
-
*/
|
|
128
|
-
export function createDetailsColumn<TData extends RowData>(
|
|
129
|
-
label = "Details",
|
|
130
|
-
): ColumnDef<TMDataGridFeatures, TData, unknown> {
|
|
131
|
-
return {
|
|
132
|
-
id: DETAILS_COLUMN_ID,
|
|
133
|
-
meta: {
|
|
134
|
-
label,
|
|
135
|
-
align: "center",
|
|
136
|
-
// Structurally the last of the generated lanes.
|
|
137
|
-
enableOrdering: false,
|
|
138
|
-
},
|
|
139
|
-
// A system lane: as wide as the control it holds and no wider. Fixed at
|
|
140
|
-
// every scale - the control does not grow with the font size, so neither
|
|
141
|
-
// should its track.
|
|
142
|
-
size: 36,
|
|
143
|
-
minSize: 36,
|
|
144
|
-
maxSize: 36,
|
|
145
|
-
enableResizing: false,
|
|
146
|
-
enableSorting: false,
|
|
147
|
-
enableColumnFilter: false,
|
|
148
|
-
enableGlobalFilter: false,
|
|
149
|
-
// Its visibility is not the user's to set: hiding it would strand every
|
|
150
|
-
// panel behind a control that is no longer there.
|
|
151
|
-
enableHiding: false,
|
|
152
|
-
// Structurally pinned to the left; users shouldn't be able to move it.
|
|
153
|
-
enablePinning: false,
|
|
154
|
-
header: ({ table }) => <DetailsHeader table={table} />,
|
|
155
|
-
cell: ({ row }) => <DetailsCell row={row} />,
|
|
156
|
-
// Deliberately no `aggregatedCell`: on a group row every cell outside the
|
|
157
|
-
// grouped column counts as aggregated, and blank is the right answer here -
|
|
158
|
-
// groups expand into their rows, not into a panel.
|
|
159
|
-
};
|
|
160
|
-
}
|
|
@@ -3,7 +3,11 @@ import { useSelector } from "@tanstack/react-store";
|
|
|
3
3
|
import { shallow } from "@tanstack/store";
|
|
4
4
|
import type { ReactNode } from "react";
|
|
5
5
|
import { useTMDataGridContext } from "../TMDataGridContext";
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
getOpenRowIds,
|
|
8
|
+
type TMDataGridCommitAllResult,
|
|
9
|
+
type TMDataGridSaveDraftsResult,
|
|
10
|
+
} from "../core/editEngine";
|
|
7
11
|
import { getDisplayedRows } from "../core/rowSelection";
|
|
8
12
|
import type {
|
|
9
13
|
TMDataGridScrollAlign,
|
|
@@ -17,13 +21,20 @@ import type {
|
|
|
17
21
|
*/
|
|
18
22
|
function useDraftCount(): number {
|
|
19
23
|
const { edit } = useTMDataGridContext();
|
|
20
|
-
return useSelector(
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
+
return useSelector(edit.store, (state) => {
|
|
25
|
+
const deleted = new Set(state.deletedRowIds);
|
|
26
|
+
// An edited row that was then marked counts once: Save sends the
|
|
27
|
+
// deletion and leaves the edit behind.
|
|
28
|
+
const edited =
|
|
29
|
+
deleted.size === 0
|
|
30
|
+
? state.committedRowIds.length
|
|
31
|
+
: state.committedRowIds.filter((rowId) => !deleted.has(rowId)).length;
|
|
32
|
+
return (
|
|
33
|
+
edited +
|
|
24
34
|
state.newRows.filter((newRow) => newRow.committed).length +
|
|
25
|
-
|
|
26
|
-
|
|
35
|
+
deleted.size
|
|
36
|
+
);
|
|
37
|
+
});
|
|
27
38
|
}
|
|
28
39
|
|
|
29
40
|
/**
|
|
@@ -55,15 +66,15 @@ function useOpenRowIds(): ReadonlyArray<string> {
|
|
|
55
66
|
function EditSaveButton() {
|
|
56
67
|
const { edit, labels } = useTMDataGridContext();
|
|
57
68
|
const draftCount = useDraftCount();
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
);
|
|
69
|
+
// Save sends the draft store, so its loading state is the save in flight,
|
|
70
|
+
// not an open row's submit.
|
|
71
|
+
const isSaving = useSelector(edit.store, (state) => state.isSaving);
|
|
61
72
|
|
|
62
73
|
return (
|
|
63
74
|
<Button
|
|
64
75
|
size="compact-sm"
|
|
65
76
|
disabled={draftCount === 0}
|
|
66
|
-
loading={
|
|
77
|
+
loading={isSaving}
|
|
67
78
|
data-dg-part="save-all"
|
|
68
79
|
data-draft-count={draftCount}
|
|
69
80
|
onClick={() => void edit.saveDrafts()}
|
|
@@ -94,14 +105,14 @@ function EditDiscardButton() {
|
|
|
94
105
|
const { edit, labels } = useTMDataGridContext();
|
|
95
106
|
// Discard drops everything the grid is holding, open rows included, so it
|
|
96
107
|
// stays live while anything is uncommitted.
|
|
97
|
-
const
|
|
108
|
+
const uncommittedCount = useDraftCount() + useOpenCount();
|
|
98
109
|
|
|
99
110
|
return (
|
|
100
111
|
<Button
|
|
101
112
|
variant="subtle"
|
|
102
113
|
color="gray"
|
|
103
114
|
size="compact-sm"
|
|
104
|
-
disabled={
|
|
115
|
+
disabled={uncommittedCount === 0}
|
|
105
116
|
data-dg-part="discard-all"
|
|
106
117
|
onClick={() => edit.cancelAll()}
|
|
107
118
|
>
|
|
@@ -139,21 +150,26 @@ export type TMDataGridDraftActionsState = {
|
|
|
139
150
|
* in display order, so it need not be `openRowIds[0]`.
|
|
140
151
|
*/
|
|
141
152
|
openRowIds: ReadonlyArray<string>;
|
|
142
|
-
/**
|
|
143
|
-
* @deprecated Was "everything uncommitted", which Save no longer sends.
|
|
144
|
-
* Reads as `draftCount + openCount`; use whichever you meant.
|
|
145
|
-
*/
|
|
146
|
-
pendingCount: number;
|
|
147
|
-
/** Whether a submit is in flight. */
|
|
153
|
+
/** Whether a submit is in flight - any open row is submitting. */
|
|
148
154
|
isSubmitting: boolean;
|
|
155
|
+
/** `true` while `saveDrafts` is in flight. */
|
|
156
|
+
isSaving: boolean;
|
|
149
157
|
};
|
|
150
158
|
|
|
151
159
|
/** What the edit chrome can do. */
|
|
152
160
|
export type TMDataGridDraftActionsActions = {
|
|
153
|
-
/**
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
161
|
+
/**
|
|
162
|
+
* Saves the draft store - `edit.saveDrafts()`. Open rows are left alone.
|
|
163
|
+
* Resolves which ids were saved, kept in the draft store for the next
|
|
164
|
+
* save, or reopened with an error; see {@link TMDataGridSaveDraftsResult}.
|
|
165
|
+
*/
|
|
166
|
+
save: () => Promise<TMDataGridSaveDraftsResult>;
|
|
167
|
+
/**
|
|
168
|
+
* Submits every open row, committing the ones that validate -
|
|
169
|
+
* `edit.commitAll()`. Resolves which rows committed and which are still
|
|
170
|
+
* open; see {@link TMDataGridCommitAllResult}.
|
|
171
|
+
*/
|
|
172
|
+
commitAll: () => Promise<TMDataGridCommitAllResult>;
|
|
157
173
|
/** Drops everything - open form state and the draft store alike. */
|
|
158
174
|
discard: () => void;
|
|
159
175
|
/**
|
|
@@ -242,6 +258,7 @@ export function TMDataGridDraftActions({
|
|
|
242
258
|
const isSubmitting = useSelector(edit.store, (state) =>
|
|
243
259
|
state.openRowIds.some((rowId) => state.rows[rowId]?.isSubmitting === true),
|
|
244
260
|
);
|
|
261
|
+
const isSaving = useSelector(edit.store, (state) => state.isSaving);
|
|
245
262
|
|
|
246
263
|
if (!features.editing) return null;
|
|
247
264
|
|
|
@@ -251,8 +268,8 @@ export function TMDataGridDraftActions({
|
|
|
251
268
|
draftCount,
|
|
252
269
|
openCount: openRowIds.length,
|
|
253
270
|
openRowIds,
|
|
254
|
-
pendingCount: draftCount + openRowIds.length,
|
|
255
271
|
isSubmitting,
|
|
272
|
+
isSaving,
|
|
256
273
|
},
|
|
257
274
|
actions: {
|
|
258
275
|
save: () => edit.saveDrafts(),
|
|
@@ -1,6 +1,6 @@
|
|
|
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,
|
|
@@ -65,16 +65,16 @@ 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
72
|
*
|
|
73
73
|
* The lane's controls take the tab index every body control does - see
|
|
74
74
|
* useBodyControlTabIndex. The open row's save and cancel are the exception,
|
|
75
75
|
* see where they are built.
|
|
76
76
|
*/
|
|
77
|
-
function EditLaneCell<TData extends RowData>({
|
|
77
|
+
export function EditLaneCell<TData extends RowData>({
|
|
78
78
|
row,
|
|
79
79
|
}: {
|
|
80
80
|
row: Row<TMDataGridFeatures, TData>;
|
|
@@ -186,10 +186,10 @@ function EditLaneCell<TData extends RowData>({
|
|
|
186
186
|
}
|
|
187
187
|
return (
|
|
188
188
|
<Group gap={0} wrap="nowrap">
|
|
189
|
-
<Tooltip label={labels.confirmNewRow} withArrow>
|
|
189
|
+
<Tooltip label={rowError ?? labels.confirmNewRow} withArrow>
|
|
190
190
|
<ActionIcon
|
|
191
191
|
variant="subtle"
|
|
192
|
-
color="green"
|
|
192
|
+
color={hasErrors ? "red" : "green"}
|
|
193
193
|
size="sm"
|
|
194
194
|
tabIndex={tabIndex}
|
|
195
195
|
aria-label={labels.confirmNewRow}
|
|
@@ -239,7 +239,7 @@ function EditLaneCell<TData extends RowData>({
|
|
|
239
239
|
data-row-id={rowId}
|
|
240
240
|
onClick={(event) => {
|
|
241
241
|
event.stopPropagation();
|
|
242
|
-
edit.
|
|
242
|
+
edit.restoreRow(rowId);
|
|
243
243
|
}}
|
|
244
244
|
>
|
|
245
245
|
<RestoreIcon size={16} stroke={1.6} />
|
|
@@ -339,15 +339,9 @@ function EditLaneCell<TData extends RowData>({
|
|
|
339
339
|
|
|
340
340
|
return (
|
|
341
341
|
<Group gap={0} wrap="nowrap">
|
|
342
|
-
{rowError
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
</Tooltip>
|
|
346
|
-
) : (
|
|
347
|
-
<Tooltip label={rowError} withArrow>
|
|
348
|
-
{save}
|
|
349
|
-
</Tooltip>
|
|
350
|
-
)}
|
|
342
|
+
<Tooltip label={rowError ?? labels.saveRow} withArrow>
|
|
343
|
+
{save}
|
|
344
|
+
</Tooltip>
|
|
351
345
|
<Tooltip label={labels.cancelRowEdit} withArrow>
|
|
352
346
|
<ActionIcon
|
|
353
347
|
variant="subtle"
|
|
@@ -368,44 +362,3 @@ function EditLaneCell<TData extends RowData>({
|
|
|
368
362
|
</Group>
|
|
369
363
|
);
|
|
370
364
|
}
|
|
371
|
-
|
|
372
|
-
/**
|
|
373
|
-
* The generated edit lane, appended and pinned right - the row's Save at the
|
|
374
|
-
* end of the row under `mode: "row"`, the state marker and revert under
|
|
375
|
-
* `editing.draft`, mirroring the checkbox lane's build on the left.
|
|
376
|
-
*/
|
|
377
|
-
export function createEditColumn<TData extends RowData>(
|
|
378
|
-
label = "Edit",
|
|
379
|
-
/** A draft lane holds three controls where the rest hold two. */
|
|
380
|
-
wide = false,
|
|
381
|
-
): ColumnDef<TMDataGridFeatures, TData, unknown> {
|
|
382
|
-
const width = wide ? 88 : 64;
|
|
383
|
-
return {
|
|
384
|
-
id: EDIT_COLUMN_ID,
|
|
385
|
-
meta: {
|
|
386
|
-
label,
|
|
387
|
-
align: "center",
|
|
388
|
-
enableOrdering: false,
|
|
389
|
-
},
|
|
390
|
-
// Wide enough for the pair (or draft's trio) it holds while editing.
|
|
391
|
-
size: width,
|
|
392
|
-
minSize: width,
|
|
393
|
-
maxSize: width,
|
|
394
|
-
enableResizing: false,
|
|
395
|
-
enableSorting: false,
|
|
396
|
-
enableColumnFilter: false,
|
|
397
|
-
enableGlobalFilter: false,
|
|
398
|
-
// The row's Save, Cancel and Delete live here, so hiding the lane would
|
|
399
|
-
// strand an open row with no way to commit or discard it. Same rule as the
|
|
400
|
-
// checkbox lane: chrome the grid generates is not a user setting.
|
|
401
|
-
enableHiding: false,
|
|
402
|
-
// Structurally pinned to the right; not movable.
|
|
403
|
-
enablePinning: false,
|
|
404
|
-
header: () => null,
|
|
405
|
-
cell: ({ row }) => <EditLaneCell row={row} />,
|
|
406
|
-
// Group rows: same reasoning as the checkbox lane - without this the
|
|
407
|
-
// cell renders blank on aggregated rows, but here blank is also correct,
|
|
408
|
-
// so the aggregated cell renders the same (null for groups).
|
|
409
|
-
aggregatedCell: ({ row }) => <EditLaneCell row={row} />,
|
|
410
|
-
};
|
|
411
|
-
}
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import {
|
|
2
2
|
flexRender,
|
|
3
3
|
useTable,
|
|
4
|
+
type Cell,
|
|
4
5
|
type Column,
|
|
5
6
|
type Row,
|
|
6
7
|
} from "@tanstack/react-table";
|
|
7
8
|
import { useSelector } from "@tanstack/react-store";
|
|
9
|
+
import { shallow } from "@tanstack/store";
|
|
8
10
|
import { useLayoutEffect, useMemo, useRef } from "react";
|
|
9
11
|
import classes from "./TMDataGridTable.module.css";
|
|
10
12
|
import sticky from "./sticky.module.css";
|
|
@@ -14,7 +16,8 @@ import {
|
|
|
14
16
|
isColumnEditableForRow,
|
|
15
17
|
isControlColumn,
|
|
16
18
|
} from "../core/columnUtils";
|
|
17
|
-
import
|
|
19
|
+
import { draftCellContext } from "../core/draftCellContext";
|
|
20
|
+
import { getEditFieldName, type TMDataGridEditApi } from "../core/editEngine";
|
|
18
21
|
import { focusEditorContent } from "../core/editorFocus";
|
|
19
22
|
import {
|
|
20
23
|
tmDataGridFeatures,
|
|
@@ -25,6 +28,8 @@ import { EDIT_COLUMN_ID } from "./TMDataGridEditColumn";
|
|
|
25
28
|
import type { TMDataGridColumnLayout } from "./TMDataGridTable";
|
|
26
29
|
|
|
27
30
|
type ErasedColumn = Column<TMDataGridFeatures, TMDataGridRowData, unknown>;
|
|
31
|
+
type EntryRow = Row<TMDataGridFeatures, TMDataGridRowData>;
|
|
32
|
+
type EntryCellApi = Cell<TMDataGridFeatures, TMDataGridRowData, unknown>;
|
|
28
33
|
|
|
29
34
|
/**
|
|
30
35
|
* Whether an entry cell edits. `edit.canEditCell` asks the main table for
|
|
@@ -40,6 +45,115 @@ function isEntryCellEditable(
|
|
|
40
45
|
return isColumnEditableForRow(column, row);
|
|
41
46
|
}
|
|
42
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
|
+
|
|
43
157
|
/**
|
|
44
158
|
* The sticky entry block - one row of open editors per `edit.addRow()`,
|
|
45
159
|
* pinned under the header. The one place stickiness is genuinely required:
|
|
@@ -66,7 +180,28 @@ export function TMDataGridEntryRows({
|
|
|
66
180
|
rowHeight: number;
|
|
67
181
|
}) {
|
|
68
182
|
const { table, edit, features } = useTMDataGridContext();
|
|
69
|
-
const
|
|
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
|
+
);
|
|
70
205
|
// The reopen gesture: `begin` on a committed entry row flips it back to
|
|
71
206
|
// editors and names the cell double-clicked - where the caret goes.
|
|
72
207
|
const activeEntry = useSelector(edit.store, (state) =>
|
|
@@ -134,21 +269,22 @@ export function TMDataGridEntryRows({
|
|
|
134
269
|
focusEditorContent(editor);
|
|
135
270
|
});
|
|
136
271
|
|
|
137
|
-
//
|
|
138
|
-
//
|
|
139
|
-
//
|
|
140
|
-
//
|
|
141
|
-
//
|
|
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).
|
|
142
279
|
const data = useMemo(
|
|
143
280
|
() =>
|
|
144
281
|
newRows.map(
|
|
145
|
-
({ tempId
|
|
146
|
-
(
|
|
147
|
-
|
|
148
|
-
: edit.getForm(tempId)?.options.defaultValues) ??
|
|
282
|
+
({ tempId }) =>
|
|
283
|
+
(committedValues[tempId] ??
|
|
284
|
+
edit.getForm(tempId)?.options.defaultValues ??
|
|
149
285
|
{}) as TMDataGridRowData,
|
|
150
286
|
),
|
|
151
|
-
[newRows, edit],
|
|
287
|
+
[newRows, committedValues, edit],
|
|
152
288
|
);
|
|
153
289
|
|
|
154
290
|
const entryTable = useTable({
|
|
@@ -160,23 +296,12 @@ export function TMDataGridEntryRows({
|
|
|
160
296
|
|
|
161
297
|
if (newRows.length === 0) return null;
|
|
162
298
|
|
|
163
|
-
const
|
|
299
|
+
const stickyRows = entryTable.getCoreRowModel().rows;
|
|
164
300
|
const committedById = new Map(
|
|
165
301
|
newRows.map((newRow) => [newRow.tempId, newRow.committed]),
|
|
166
302
|
);
|
|
167
|
-
// A row being *typed* into is always here - it exists nowhere else to
|
|
168
|
-
// scroll back to. A committed row is a body row, sorted and filtered with
|
|
169
|
-
// the rest (the hook feeds it to the table as `data`), unless
|
|
170
|
-
// `newRowsSticky` keeps it pinned here until the save.
|
|
171
|
-
const stickyRows = entryRows.filter(
|
|
172
|
-
(entryRow) =>
|
|
173
|
-
features.editNewRowsSticky || committedById.get(entryRow.id) !== true,
|
|
174
|
-
);
|
|
175
303
|
|
|
176
|
-
const renderEntryRow = (
|
|
177
|
-
entryRow: (typeof entryRows)[number],
|
|
178
|
-
pinnedZ: string,
|
|
179
|
-
) => {
|
|
304
|
+
const renderEntryRow = (entryRow: EntryRow, pinnedZ: string) => {
|
|
180
305
|
const cellsById = new Map(
|
|
181
306
|
entryRow.getAllCells().map((cell) => [cell.column.id, cell]),
|
|
182
307
|
);
|
|
@@ -188,77 +313,24 @@ export function TMDataGridEntryRows({
|
|
|
188
313
|
data-dg-part="entry-row"
|
|
189
314
|
data-row-id={entryRow.id}
|
|
190
315
|
data-new
|
|
191
|
-
data-committed={committed}
|
|
316
|
+
data-committed={committed || undefined}
|
|
192
317
|
// The same marker body rows carry once committed, so one selector
|
|
193
318
|
// reaches everything parked in the draft store.
|
|
194
|
-
data-draft={committed}
|
|
319
|
+
data-draft={committed || undefined}
|
|
195
320
|
className={classes.entryRow}
|
|
196
321
|
>
|
|
197
|
-
{orderedColumns.map((column) =>
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
// A committed row re-opens where it is double-clicked, the
|
|
210
|
-
// same gesture a body cell answers - and, like a body cell, a
|
|
211
|
-
// cell that takes no edit answers nothing.
|
|
212
|
-
onDoubleClick={
|
|
213
|
-
committed && editable
|
|
214
|
-
? () =>
|
|
215
|
-
edit.begin({
|
|
216
|
-
rowId: entryRow.id,
|
|
217
|
-
columnId: column.id,
|
|
218
|
-
})
|
|
219
|
-
: undefined
|
|
220
|
-
}
|
|
221
|
-
className={[
|
|
222
|
-
classes.entryCell,
|
|
223
|
-
layout.isBoundary && layout.pinnedAt === "left"
|
|
224
|
-
? sticky.stickyLeft
|
|
225
|
-
: "",
|
|
226
|
-
layout.isBoundary && layout.pinnedAt === "right"
|
|
227
|
-
? sticky.stickyRight
|
|
228
|
-
: "",
|
|
229
|
-
]
|
|
230
|
-
.filter(Boolean)
|
|
231
|
-
.join(" ")}
|
|
232
|
-
style={{
|
|
233
|
-
minHeight: rowHeight,
|
|
234
|
-
left: layout.pinnedAt === "left" ? layout.offset : undefined,
|
|
235
|
-
right: layout.pinnedAt === "right" ? layout.offset : undefined,
|
|
236
|
-
position: layout.pinnedAt ? "sticky" : undefined,
|
|
237
|
-
zIndex: layout.pinnedAt ? pinnedZ : undefined,
|
|
238
|
-
}}
|
|
239
|
-
>
|
|
240
|
-
{cell !== undefined && column.id === EDIT_COLUMN_ID ? (
|
|
241
|
-
// The lane's cell - the entry row's controls.
|
|
242
|
-
flexRender(cell.column.columnDef.cell, cell.getContext())
|
|
243
|
-
) : committed && cell !== undefined ? (
|
|
244
|
-
// Entered, awaiting Save all: a value row through the
|
|
245
|
-
// columns' own renderers, over the draft the memo above
|
|
246
|
-
// fed this table.
|
|
247
|
-
<span className={classes.cellContent}>
|
|
248
|
-
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
|
249
|
-
</span>
|
|
250
|
-
) : editable && cell !== undefined ? (
|
|
251
|
-
<TMDataGridCellEditor
|
|
252
|
-
cell={cell}
|
|
253
|
-
row={entryRow}
|
|
254
|
-
takeSeedText={() => undefined}
|
|
255
|
-
onClose={() => {}}
|
|
256
|
-
inEntryBlock
|
|
257
|
-
/>
|
|
258
|
-
) : null}
|
|
259
|
-
</div>
|
|
260
|
-
);
|
|
261
|
-
})}
|
|
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
|
+
))}
|
|
262
334
|
</div>
|
|
263
335
|
);
|
|
264
336
|
};
|