@jielga/tmdatagrid 2.0.0-beta.2 → 2.0.0-beta.21
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 +1664 -632
- package/dist/index.js +5226 -3223
- package/dist/index.js.map +1 -1
- package/dist/styles.css +1 -1
- package/docs/anatomy.md +102 -0
- package/docs/cell-selection.md +154 -0
- package/docs/column-layout.md +204 -0
- package/docs/columns.md +262 -0
- package/docs/components.md +304 -0
- package/docs/editing.md +603 -0
- package/docs/editors.md +250 -0
- package/docs/export.md +326 -0
- package/docs/filtering.md +358 -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/pagination.md +144 -0
- package/docs/persistence.md +111 -0
- package/docs/portfolio-rebalancer.md +94 -0
- package/docs/query-builder.md +175 -0
- package/docs/quick-search.md +83 -0
- package/docs/row-details.md +113 -0
- package/docs/row-interaction.md +148 -0
- package/docs/row-pinning.md +132 -0
- package/docs/row-selection.md +134 -0
- package/docs/row-styling.md +133 -0
- package/docs/scrolling.md +111 -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 +309 -0
- package/docs/toolbar.md +161 -0
- package/docs/use-tm-data-grid.md +361 -0
- package/package.json +21 -45
- package/skills/appearance/SKILL.md +70 -17
- package/skills/cell-selection/SKILL.md +70 -76
- package/skills/columns/SKILL.md +131 -32
- package/skills/data/SKILL.md +100 -23
- package/skills/editing/SKILL.md +217 -96
- package/skills/editing/references/common-mistakes.md +111 -24
- package/skills/editing/references/editing-api.md +63 -39
- package/skills/editing/references/editors-and-validation.md +77 -19
- package/skills/filtering/SKILL.md +148 -40
- package/skills/getting-started/SKILL.md +18 -16
- package/skills/grouping/SKILL.md +32 -15
- package/skills/options/SKILL.md +39 -9
- package/skills/rows/SKILL.md +22 -18
- package/skills/server-side/SKILL.md +170 -17
- package/skills/testing/SKILL.md +10 -7
- 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 +39 -23
- package/src/{tmdatagrid/components → components}/TMDataGridCellEditor.tsx +106 -38
- 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 +4 -4
- package/src/components/TMDataGridDraftActions.tsx +307 -0
- package/src/{tmdatagrid/components → components}/TMDataGridEditColumn.tsx +58 -50
- package/src/{tmdatagrid/components → components}/TMDataGridEntryRows.tsx +150 -115
- 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 +7 -5
- package/src/components/TMDataGridFilterSurface.module.css +54 -0
- package/src/components/TMDataGridFilterSurface.tsx +167 -0
- package/src/{tmdatagrid/components → components}/TMDataGridFooter.tsx +53 -13
- package/src/{tmdatagrid/components → components}/TMDataGridGroupColumn.tsx +4 -3
- package/src/{tmdatagrid/components → components}/TMDataGridHeaderCell.module.css +10 -0
- package/src/{tmdatagrid/components → components}/TMDataGridHeaderCell.tsx +100 -28
- package/src/components/TMDataGridHeaderFilterRow.module.css +51 -0
- package/src/components/TMDataGridHeaderFilterRow.tsx +301 -0
- package/src/components/TMDataGridMenu.tsx +354 -0
- package/src/{tmdatagrid/components → components}/TMDataGridSelectColumn.tsx +12 -7
- package/src/{tmdatagrid/components → components}/TMDataGridTable.module.css +90 -67
- package/src/{tmdatagrid/components → components}/TMDataGridTable.tsx +678 -156
- package/src/components/TMDataGridToolbar.module.css +21 -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/components/editors/TMDataGridNumberEditor.tsx +70 -0
- 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 +17 -31
- 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/{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 +14 -6
- package/src/{tmdatagrid/core → core}/columnOptions.ts +46 -0
- package/src/{tmdatagrid/core → core}/columnOrdering.ts +33 -14
- package/src/{tmdatagrid/core → core}/columnUtils.ts +44 -5
- package/src/core/controlledState.ts +179 -0
- package/src/core/controlledStateSync.ts +108 -0
- package/src/core/deletedRows.ts +34 -0
- package/src/core/dom.ts +74 -0
- package/src/core/editEngine.ts +2476 -0
- package/src/{tmdatagrid/core → core}/editorFocus.ts +8 -4
- package/src/core/export.ts +843 -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}/labels.ts +66 -8
- package/src/{tmdatagrid/core → core}/labelsSv.ts +26 -3
- package/src/core/pageReset.ts +120 -0
- package/src/{tmdatagrid/core → core}/persistence.ts +22 -5
- package/src/core/resizePreview.ts +141 -0
- package/src/core/summary.ts +59 -0
- package/src/core/useSettledTableState.ts +36 -0
- package/src/{tmdatagrid/index.ts → index.ts} +75 -12
- package/src/{tmdatagrid/useTMDataGrid.tsx → useTMDataGrid.tsx} +734 -135
- package/src/useTMDataGridExport.ts +78 -0
- package/src/tmdatagrid/components/TMDataGridEditActions.tsx +0 -162
- package/src/tmdatagrid/components/TMDataGridFilterPanel.module.css +0 -35
- package/src/tmdatagrid/components/TMDataGridFilterPanel.tsx +0 -354
- package/src/tmdatagrid/components/TMDataGridToolbar.module.css +0 -12
- package/src/tmdatagrid/components/TMDataGridToolbar.tsx +0 -162
- package/src/tmdatagrid/components/editors/TMDataGridNumberEditor.tsx +0 -40
- package/src/tmdatagrid/core/cellExport.ts +0 -320
- package/src/tmdatagrid/core/editEngine.ts +0 -1006
- package/src/tmdatagrid/core/summary.ts +0 -35
- /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}/draftCellContext.ts +0 -0
- /package/src/{tmdatagrid/core → core}/expanding.ts +0 -0
- /package/src/{tmdatagrid/core → core}/grouping.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}/rowPinning.ts +0 -0
- /package/src/{tmdatagrid/core → core}/rowSelection.ts +0 -0
- /package/src/{tmdatagrid/core → core}/sizes.ts +0 -0
|
@@ -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,7 @@ import {
|
|
|
14
16
|
isColumnEditableForRow,
|
|
15
17
|
isControlColumn,
|
|
16
18
|
} from "../core/columnUtils";
|
|
17
|
-
import { getEditFieldName } from "../core/editEngine";
|
|
19
|
+
import { getEditFieldName, type TMDataGridEditApi } from "../core/editEngine";
|
|
18
20
|
import { focusEditorContent } from "../core/editorFocus";
|
|
19
21
|
import {
|
|
20
22
|
tmDataGridFeatures,
|
|
@@ -25,6 +27,8 @@ import { EDIT_COLUMN_ID } from "./TMDataGridEditColumn";
|
|
|
25
27
|
import type { TMDataGridColumnLayout } from "./TMDataGridTable";
|
|
26
28
|
|
|
27
29
|
type ErasedColumn = Column<TMDataGridFeatures, TMDataGridRowData, unknown>;
|
|
30
|
+
type EntryRow = Row<TMDataGridFeatures, TMDataGridRowData>;
|
|
31
|
+
type EntryCellApi = Cell<TMDataGridFeatures, TMDataGridRowData, unknown>;
|
|
28
32
|
|
|
29
33
|
/**
|
|
30
34
|
* Whether an entry cell edits. `edit.canEditCell` asks the main table for
|
|
@@ -34,12 +38,98 @@ type ErasedColumn = Column<TMDataGridFeatures, TMDataGridRowData, unknown>;
|
|
|
34
38
|
function isEntryCellEditable(
|
|
35
39
|
row: Row<TMDataGridFeatures, TMDataGridRowData>,
|
|
36
40
|
column: ErasedColumn,
|
|
41
|
+
edit: TMDataGridEditApi,
|
|
37
42
|
): boolean {
|
|
38
|
-
if (
|
|
39
|
-
if (getEditFieldName(column) === null) return false;
|
|
43
|
+
if (!edit.isColumnEditable(column)) return false;
|
|
40
44
|
return isColumnEditableForRow(column, row);
|
|
41
45
|
}
|
|
42
46
|
|
|
47
|
+
/**
|
|
48
|
+
* One cell of an entry row: the lane's controls, a value once the row is
|
|
49
|
+
* committed, or an editor. The invalid marker subscribes here, per cell, as
|
|
50
|
+
* a body cell's does - a refused ✓ paints the cells its issues name and
|
|
51
|
+
* nothing else. No dirty marker: everything in an entry row is new.
|
|
52
|
+
*/
|
|
53
|
+
function EntryCell({
|
|
54
|
+
entryRow,
|
|
55
|
+
column,
|
|
56
|
+
cell,
|
|
57
|
+
committed,
|
|
58
|
+
layout,
|
|
59
|
+
rowHeight,
|
|
60
|
+
pinnedZ,
|
|
61
|
+
}: {
|
|
62
|
+
entryRow: EntryRow;
|
|
63
|
+
column: ErasedColumn;
|
|
64
|
+
cell: EntryCellApi | undefined;
|
|
65
|
+
committed: boolean;
|
|
66
|
+
layout: TMDataGridColumnLayout;
|
|
67
|
+
rowHeight: number;
|
|
68
|
+
pinnedZ: string;
|
|
69
|
+
}) {
|
|
70
|
+
const { edit } = useTMDataGridContext();
|
|
71
|
+
const fieldName = getEditFieldName(column);
|
|
72
|
+
const isInvalid = useSelector(edit.store, (state) => {
|
|
73
|
+
if (fieldName === null) return false;
|
|
74
|
+
const projection = state.rows[entryRow.id];
|
|
75
|
+
return projection !== undefined && projection.errorFields.includes(fieldName);
|
|
76
|
+
});
|
|
77
|
+
const editable =
|
|
78
|
+
cell !== undefined && isEntryCellEditable(entryRow, column, edit);
|
|
79
|
+
return (
|
|
80
|
+
<div
|
|
81
|
+
role="cell"
|
|
82
|
+
data-column-id={column.id}
|
|
83
|
+
data-align={getColumnAlign(column)}
|
|
84
|
+
data-control-column={isControlColumn(column.id)}
|
|
85
|
+
data-invalid={isInvalid || undefined}
|
|
86
|
+
// A committed row re-opens where it is double-clicked, the same
|
|
87
|
+
// gesture a body cell answers - and, like a body cell, a cell that
|
|
88
|
+
// takes no edit answers nothing.
|
|
89
|
+
onDoubleClick={
|
|
90
|
+
committed && editable
|
|
91
|
+
? () => edit.begin({ rowId: entryRow.id, columnId: column.id })
|
|
92
|
+
: undefined
|
|
93
|
+
}
|
|
94
|
+
className={[
|
|
95
|
+
classes.entryCell,
|
|
96
|
+
layout.isBoundary && layout.pinnedAt === "left" ? sticky.stickyLeft : "",
|
|
97
|
+
layout.isBoundary && layout.pinnedAt === "right"
|
|
98
|
+
? sticky.stickyRight
|
|
99
|
+
: "",
|
|
100
|
+
]
|
|
101
|
+
.filter(Boolean)
|
|
102
|
+
.join(" ")}
|
|
103
|
+
style={{
|
|
104
|
+
minHeight: rowHeight,
|
|
105
|
+
left: layout.pinnedAt === "left" ? layout.offset : undefined,
|
|
106
|
+
right: layout.pinnedAt === "right" ? layout.offset : undefined,
|
|
107
|
+
position: layout.pinnedAt ? "sticky" : undefined,
|
|
108
|
+
zIndex: layout.pinnedAt ? pinnedZ : undefined,
|
|
109
|
+
}}
|
|
110
|
+
>
|
|
111
|
+
{cell !== undefined && column.id === EDIT_COLUMN_ID ? (
|
|
112
|
+
// The lane's cell - the entry row's controls.
|
|
113
|
+
flexRender(cell.column.columnDef.cell, cell.getContext())
|
|
114
|
+
) : committed && cell !== undefined ? (
|
|
115
|
+
// Entered, awaiting Save all: a value row through the columns' own
|
|
116
|
+
// renderers, over the draft the entry table was fed.
|
|
117
|
+
<span className={classes.cellContent}>
|
|
118
|
+
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
|
119
|
+
</span>
|
|
120
|
+
) : editable && cell !== undefined ? (
|
|
121
|
+
<TMDataGridCellEditor
|
|
122
|
+
cell={cell}
|
|
123
|
+
row={entryRow}
|
|
124
|
+
takeSeedText={() => undefined}
|
|
125
|
+
onClose={() => {}}
|
|
126
|
+
inEntryBlock
|
|
127
|
+
/>
|
|
128
|
+
) : null}
|
|
129
|
+
</div>
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
|
|
43
133
|
/**
|
|
44
134
|
* The sticky entry block - one row of open editors per `edit.addRow()`,
|
|
45
135
|
* pinned under the header. The one place stickiness is genuinely required:
|
|
@@ -52,9 +142,9 @@ function isEntryCellEditable(
|
|
|
52
142
|
* validators all apply unchanged. The forms live in the same engine map as
|
|
53
143
|
* every other draft, keyed by their temporary ids.
|
|
54
144
|
*
|
|
55
|
-
* Enter commits the entry
|
|
56
|
-
*
|
|
57
|
-
*
|
|
145
|
+
* Enter commits the entry - `onRowAdd`, or a park under `editing.draft`,
|
|
146
|
+
* where `saveDrafts` reports it in `created`; Escape discards it. The edit
|
|
147
|
+
* lane, when present, offers the same pair as buttons.
|
|
58
148
|
*/
|
|
59
149
|
export function TMDataGridEntryRows({
|
|
60
150
|
orderedColumns,
|
|
@@ -66,13 +156,34 @@ export function TMDataGridEntryRows({
|
|
|
66
156
|
rowHeight: number;
|
|
67
157
|
}) {
|
|
68
158
|
const { table, edit, features } = useTMDataGridContext();
|
|
69
|
-
const
|
|
70
|
-
//
|
|
159
|
+
const sticky = features.editNewRowsSticky;
|
|
160
|
+
// A row being *typed* into is always here - it exists nowhere else to
|
|
161
|
+
// scroll back to. A committed row is a body row, sorted and filtered with
|
|
162
|
+
// the rest (the hook feeds it to the table as `data`), unless
|
|
163
|
+
// `newRowsSticky` keeps it pinned here until the save. Only the rows the
|
|
164
|
+
// block renders go into its table: after an import `newRows` runs to
|
|
165
|
+
// thousands, every one of them committed and in the body.
|
|
166
|
+
const newRows = useSelector(
|
|
167
|
+
edit.store,
|
|
168
|
+
(state) =>
|
|
169
|
+
sticky
|
|
170
|
+
? state.newRows
|
|
171
|
+
: state.newRows.filter((newRow) => !newRow.committed),
|
|
172
|
+
{ compare: shallow },
|
|
173
|
+
);
|
|
174
|
+
// What a committed row renders from. It has no form - its values are the
|
|
175
|
+
// draft store's - and this is the same snapshot the table feeds a
|
|
176
|
+
// committed row in the body.
|
|
177
|
+
const committedValues = useSelector(
|
|
178
|
+
edit.store,
|
|
179
|
+
(state) => state.committedValues,
|
|
180
|
+
);
|
|
181
|
+
// The reopen gesture: `begin` on a committed entry row flips it back to
|
|
71
182
|
// editors and names the cell double-clicked - where the caret goes.
|
|
72
183
|
const activeEntry = useSelector(edit.store, (state) =>
|
|
73
184
|
state.active !== null &&
|
|
74
185
|
state.newRows.some(
|
|
75
|
-
(newRow) => newRow.tempId === state.active?.rowId && !newRow.
|
|
186
|
+
(newRow) => newRow.tempId === state.active?.rowId && !newRow.committed,
|
|
76
187
|
)
|
|
77
188
|
? state.active
|
|
78
189
|
: null,
|
|
@@ -109,7 +220,7 @@ export function TMDataGridEntryRows({
|
|
|
109
220
|
});
|
|
110
221
|
|
|
111
222
|
/**
|
|
112
|
-
* The caret for a reopen: `begin` on a
|
|
223
|
+
* The caret for a reopen: `begin` on a committed row re-arms its editors
|
|
113
224
|
* and the double-clicked cell's editor should hold the caret, not the
|
|
114
225
|
* row's first. Keyed separately from the add effect above - the row was
|
|
115
226
|
* already focused once when it was added.
|
|
@@ -134,21 +245,21 @@ export function TMDataGridEntryRows({
|
|
|
134
245
|
focusEditorContent(editor);
|
|
135
246
|
});
|
|
136
247
|
|
|
137
|
-
//
|
|
248
|
+
// Open (uncommitted) rows keep the seed values frozen at addRow - the live values
|
|
138
249
|
// belong to the forms, which the editors read directly, and this table only
|
|
139
|
-
// provides row and cell identity. A
|
|
140
|
-
//
|
|
141
|
-
//
|
|
250
|
+
// provides row and cell identity. A committed row's cells render values, and
|
|
251
|
+
// a committed row is data: its values come from the draft store, which is
|
|
252
|
+
// also what the row keeps while a reopen is undecided.
|
|
142
253
|
const data = useMemo(
|
|
143
254
|
() =>
|
|
144
255
|
newRows.map(
|
|
145
|
-
({ tempId,
|
|
146
|
-
((
|
|
147
|
-
?
|
|
256
|
+
({ tempId, committed }) =>
|
|
257
|
+
((committed
|
|
258
|
+
? committedValues[tempId]
|
|
148
259
|
: edit.getForm(tempId)?.options.defaultValues) ??
|
|
149
260
|
{}) as TMDataGridRowData,
|
|
150
261
|
),
|
|
151
|
-
[newRows, edit],
|
|
262
|
+
[newRows, committedValues, edit],
|
|
152
263
|
);
|
|
153
264
|
|
|
154
265
|
const entryTable = useTable({
|
|
@@ -160,30 +271,16 @@ export function TMDataGridEntryRows({
|
|
|
160
271
|
|
|
161
272
|
if (newRows.length === 0) return null;
|
|
162
273
|
|
|
163
|
-
const
|
|
164
|
-
const
|
|
165
|
-
newRows.map((newRow) => [newRow.tempId, newRow.
|
|
274
|
+
const stickyRows = entryTable.getCoreRowModel().rows;
|
|
275
|
+
const committedById = new Map(
|
|
276
|
+
newRows.map((newRow) => [newRow.tempId, newRow.committed]),
|
|
166
277
|
);
|
|
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
278
|
|
|
179
|
-
const renderEntryRow = (
|
|
180
|
-
entryRow: (typeof entryRows)[number],
|
|
181
|
-
pinnedZ: string,
|
|
182
|
-
) => {
|
|
279
|
+
const renderEntryRow = (entryRow: EntryRow, pinnedZ: string) => {
|
|
183
280
|
const cellsById = new Map(
|
|
184
281
|
entryRow.getAllCells().map((cell) => [cell.column.id, cell]),
|
|
185
282
|
);
|
|
186
|
-
const
|
|
283
|
+
const committed = committedById.get(entryRow.id) === true;
|
|
187
284
|
return (
|
|
188
285
|
<div
|
|
189
286
|
key={entryRow.id}
|
|
@@ -191,73 +288,24 @@ export function TMDataGridEntryRows({
|
|
|
191
288
|
data-dg-part="entry-row"
|
|
192
289
|
data-row-id={entryRow.id}
|
|
193
290
|
data-new
|
|
194
|
-
data-
|
|
291
|
+
data-committed={committed}
|
|
292
|
+
// The same marker body rows carry once committed, so one selector
|
|
293
|
+
// reaches everything parked in the draft store.
|
|
294
|
+
data-draft={committed}
|
|
195
295
|
className={classes.entryRow}
|
|
196
296
|
>
|
|
197
|
-
{orderedColumns.map((column) =>
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
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
|
-
})}
|
|
297
|
+
{orderedColumns.map((column) => (
|
|
298
|
+
<EntryCell
|
|
299
|
+
key={column.id}
|
|
300
|
+
entryRow={entryRow}
|
|
301
|
+
column={column}
|
|
302
|
+
cell={cellsById.get(column.id)}
|
|
303
|
+
committed={committed}
|
|
304
|
+
layout={layoutFor(column.id)}
|
|
305
|
+
rowHeight={rowHeight}
|
|
306
|
+
pinnedZ={pinnedZ}
|
|
307
|
+
/>
|
|
308
|
+
))}
|
|
261
309
|
</div>
|
|
262
310
|
);
|
|
263
311
|
};
|
|
@@ -276,19 +324,6 @@ export function TMDataGridEntryRows({
|
|
|
276
324
|
)}
|
|
277
325
|
</div>
|
|
278
326
|
)}
|
|
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
327
|
</>
|
|
293
328
|
);
|
|
294
329
|
}
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Button,
|
|
3
|
+
Checkbox,
|
|
4
|
+
Group,
|
|
5
|
+
type MantineSize,
|
|
6
|
+
Modal,
|
|
7
|
+
ScrollArea,
|
|
8
|
+
Stack,
|
|
9
|
+
Text,
|
|
10
|
+
TextInput,
|
|
11
|
+
} from "@mantine/core";
|
|
12
|
+
import { useSelector } from "@tanstack/react-store";
|
|
13
|
+
import { useState } from "react";
|
|
14
|
+
import classes from "./TMDataGridExportPicker.module.css";
|
|
15
|
+
import { useTMDataGridContext } from "../TMDataGridContext";
|
|
16
|
+
import { getColumnLabel, showColumnSearch } from "../core/columnUtils";
|
|
17
|
+
import {
|
|
18
|
+
countSelectedExportRows,
|
|
19
|
+
getExportableColumns,
|
|
20
|
+
resolveExportOptions,
|
|
21
|
+
type TMDataGridExportPickerRequest,
|
|
22
|
+
} from "../core/export";
|
|
23
|
+
import type { TMDataGridSize } from "../core/sizes";
|
|
24
|
+
import { useTMDataGridExport } from "../useTMDataGridExport";
|
|
25
|
+
import { SearchIcon } from "./icons";
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* The column picker behind `columns="custom"` on the export menu items: a
|
|
29
|
+
* list of every exportable column, the visible ones ticked and the hidden
|
|
30
|
+
* ones marked, a select-all row over the list, a search box once the list is
|
|
31
|
+
* long, and an Export button that downloads the ticked ones.
|
|
32
|
+
*
|
|
33
|
+
* Mounted once by the root and driven by `ui.state.exportPicker`, because the
|
|
34
|
+
* menu item that opens it unmounts with the dropdown on the click. The form
|
|
35
|
+
* inside is keyed on the request, so each opening starts from the visible
|
|
36
|
+
* columns again rather than from the last choice.
|
|
37
|
+
*/
|
|
38
|
+
export function TMDataGridExportPicker() {
|
|
39
|
+
const { ui, labels, size, controlSize, exportOptions } =
|
|
40
|
+
useTMDataGridContext();
|
|
41
|
+
const request = useSelector(ui, (state) => state.exportPicker);
|
|
42
|
+
const format = request
|
|
43
|
+
? resolveExportOptions(exportOptions, request.options).format
|
|
44
|
+
: null;
|
|
45
|
+
|
|
46
|
+
return (
|
|
47
|
+
<Modal
|
|
48
|
+
opened={request !== null}
|
|
49
|
+
onClose={() => ui.actions.closeExportPicker()}
|
|
50
|
+
title={
|
|
51
|
+
format && labels.exportPickerTitle(format.extension.toUpperCase())
|
|
52
|
+
}
|
|
53
|
+
size={MODAL_WIDTH[size]}
|
|
54
|
+
closeButtonProps={{ size: CLOSE_BUTTON_SIZE[controlSize] }}
|
|
55
|
+
classNames={{ title: classes.title }}
|
|
56
|
+
styles={{ title: { fontSize: `var(--mantine-font-size-${controlSize})` } }}
|
|
57
|
+
>
|
|
58
|
+
{request && <PickerForm request={request} />}
|
|
59
|
+
</Modal>
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Dialog width per grid size: the same three steps as the controls. */
|
|
64
|
+
const MODAL_WIDTH: Record<TMDataGridSize, MantineSize> = {
|
|
65
|
+
xs: "xs",
|
|
66
|
+
sm: "xs",
|
|
67
|
+
md: "sm",
|
|
68
|
+
lg: "sm",
|
|
69
|
+
xl: "md",
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
/** The close button one step above the controls, which is Mantine's own ratio. */
|
|
73
|
+
const CLOSE_BUTTON_SIZE: Record<string, MantineSize> = {
|
|
74
|
+
xs: "sm",
|
|
75
|
+
sm: "md",
|
|
76
|
+
md: "lg",
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
function PickerForm({ request }: { request: TMDataGridExportPickerRequest }) {
|
|
80
|
+
const { table, ui, labels, controlSize } = useTMDataGridContext();
|
|
81
|
+
const { exportAll, exportSelected } = useTMDataGridExport(request.options);
|
|
82
|
+
const columns = getExportableColumns(table);
|
|
83
|
+
const [search, setSearch] = useState("");
|
|
84
|
+
const [checked, setChecked] = useState<ReadonlySet<string>>(
|
|
85
|
+
() =>
|
|
86
|
+
new Set(
|
|
87
|
+
columns
|
|
88
|
+
.filter((column) => column.getIsVisible())
|
|
89
|
+
.map((column) => column.id),
|
|
90
|
+
),
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
const searchable = showColumnSearch("auto", columns.length);
|
|
94
|
+
const needle = search.trim().toLowerCase();
|
|
95
|
+
const listed = needle
|
|
96
|
+
? columns.filter((column) =>
|
|
97
|
+
getColumnLabel(column).toLowerCase().includes(needle),
|
|
98
|
+
)
|
|
99
|
+
: columns;
|
|
100
|
+
|
|
101
|
+
// Select all works on the listed columns: with a search active it ticks the
|
|
102
|
+
// matches and leaves the rest as they are, the way a filtered list reads.
|
|
103
|
+
const listedChecked = listed.filter((column) => checked.has(column.id));
|
|
104
|
+
const allListed = listed.length > 0 && listedChecked.length === listed.length;
|
|
105
|
+
const someListed = listedChecked.length > 0 && !allListed;
|
|
106
|
+
|
|
107
|
+
const toggle = (columnId: string, on: boolean) =>
|
|
108
|
+
setChecked((previous) => {
|
|
109
|
+
const next = new Set(previous);
|
|
110
|
+
if (on) next.add(columnId);
|
|
111
|
+
else next.delete(columnId);
|
|
112
|
+
return next;
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
const toggleListed = (on: boolean) =>
|
|
116
|
+
setChecked((previous) => {
|
|
117
|
+
const next = new Set(previous);
|
|
118
|
+
for (const column of listed) {
|
|
119
|
+
if (on) next.add(column.id);
|
|
120
|
+
else next.delete(column.id);
|
|
121
|
+
}
|
|
122
|
+
return next;
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
const confirm = async () => {
|
|
126
|
+
// Render order, whatever order the boxes were ticked in.
|
|
127
|
+
const ids = columns
|
|
128
|
+
.filter((column) => checked.has(column.id))
|
|
129
|
+
.map((column) => column.id);
|
|
130
|
+
const run = request.rows === "selected" ? exportSelected : exportAll;
|
|
131
|
+
await run({ columns: ids });
|
|
132
|
+
ui.actions.closeExportPicker();
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
const hint = labels.exportPickerHint(
|
|
136
|
+
request.rows === "selected" ? countSelectedExportRows(table) : null,
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
return (
|
|
140
|
+
<Stack gap="sm" data-dg-part="export-picker">
|
|
141
|
+
<Text size={controlSize} c="dimmed" data-dg-part="export-picker-hint">
|
|
142
|
+
{hint}
|
|
143
|
+
</Text>
|
|
144
|
+
|
|
145
|
+
{searchable && (
|
|
146
|
+
<TextInput
|
|
147
|
+
value={search}
|
|
148
|
+
onChange={(event) => setSearch(event.currentTarget.value)}
|
|
149
|
+
placeholder={labels.columnsSearchPlaceholder}
|
|
150
|
+
leftSection={<SearchIcon size={16} stroke={1.6} />}
|
|
151
|
+
size={controlSize}
|
|
152
|
+
data-dg-part="export-picker-search"
|
|
153
|
+
data-autofocus
|
|
154
|
+
/>
|
|
155
|
+
)}
|
|
156
|
+
|
|
157
|
+
<div className={classes.list}>
|
|
158
|
+
<div className={classes.listHeader}>
|
|
159
|
+
<Checkbox
|
|
160
|
+
size={controlSize}
|
|
161
|
+
label={labels.exportPickerSelectAll}
|
|
162
|
+
checked={allListed}
|
|
163
|
+
indeterminate={someListed}
|
|
164
|
+
onChange={(event) => toggleListed(event.currentTarget.checked)}
|
|
165
|
+
classNames={{ label: classes.headerLabel }}
|
|
166
|
+
data-dg-part="export-column-all"
|
|
167
|
+
/>
|
|
168
|
+
<Text size="xs" c="dimmed" data-dg-part="export-picker-count">
|
|
169
|
+
{labels.exportPickerCount(checked.size, columns.length)}
|
|
170
|
+
</Text>
|
|
171
|
+
</div>
|
|
172
|
+
|
|
173
|
+
<ScrollArea.Autosize mah="50vh" type="auto">
|
|
174
|
+
<div className={classes.rows}>
|
|
175
|
+
{listed.map((column) => (
|
|
176
|
+
<Checkbox
|
|
177
|
+
key={column.id}
|
|
178
|
+
size={controlSize}
|
|
179
|
+
label={
|
|
180
|
+
<>
|
|
181
|
+
<span className={classes.rowText}>
|
|
182
|
+
{getColumnLabel(column)}
|
|
183
|
+
</span>
|
|
184
|
+
{!column.getIsVisible() && (
|
|
185
|
+
<span className={classes.rowHint}>
|
|
186
|
+
{labels.exportPickerHidden}
|
|
187
|
+
</span>
|
|
188
|
+
)}
|
|
189
|
+
</>
|
|
190
|
+
}
|
|
191
|
+
checked={checked.has(column.id)}
|
|
192
|
+
onChange={(event) =>
|
|
193
|
+
toggle(column.id, event.currentTarget.checked)
|
|
194
|
+
}
|
|
195
|
+
classNames={{
|
|
196
|
+
root: classes.row,
|
|
197
|
+
body: classes.rowBody,
|
|
198
|
+
labelWrapper: classes.rowLabelWrapper,
|
|
199
|
+
label: classes.rowLabel,
|
|
200
|
+
}}
|
|
201
|
+
data-dg-part="export-column"
|
|
202
|
+
data-column-id={column.id}
|
|
203
|
+
/>
|
|
204
|
+
))}
|
|
205
|
+
{listed.length === 0 && (
|
|
206
|
+
<Text size={controlSize} c="dimmed" className={classes.empty}>
|
|
207
|
+
{labels.columnsNoMatch(search)}
|
|
208
|
+
</Text>
|
|
209
|
+
)}
|
|
210
|
+
</div>
|
|
211
|
+
</ScrollArea.Autosize>
|
|
212
|
+
</div>
|
|
213
|
+
|
|
214
|
+
<Group justify="flex-end" gap="xs">
|
|
215
|
+
<Button
|
|
216
|
+
variant="default"
|
|
217
|
+
size={controlSize}
|
|
218
|
+
onClick={() => ui.actions.closeExportPicker()}
|
|
219
|
+
data-dg-part="export-picker-cancel"
|
|
220
|
+
>
|
|
221
|
+
{labels.exportPickerCancel}
|
|
222
|
+
</Button>
|
|
223
|
+
<Button
|
|
224
|
+
size={controlSize}
|
|
225
|
+
disabled={checked.size === 0}
|
|
226
|
+
onClick={() => void confirm()}
|
|
227
|
+
data-dg-part="export-picker-confirm"
|
|
228
|
+
>
|
|
229
|
+
{labels.exportPickerConfirm}
|
|
230
|
+
</Button>
|
|
231
|
+
</Group>
|
|
232
|
+
</Stack>
|
|
233
|
+
);
|
|
234
|
+
}
|