@jielga/tmdatagrid 2.0.0-beta.9 → 2.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +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 +269 -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 +49 -48
- package/skills/columns/SKILL.md +125 -70
- package/skills/columns/references/columns-api.md +59 -0
- package/skills/data/SKILL.md +112 -18
- package/skills/editing/SKILL.md +76 -42
- package/skills/editing/references/common-mistakes.md +77 -69
- package/skills/editing/references/editing-api.md +25 -20
- package/skills/editing/references/editors-and-validation.md +80 -18
- package/skills/filtering/SKILL.md +155 -41
- package/skills/getting-started/SKILL.md +116 -16
- package/skills/grouping/SKILL.md +31 -16
- package/skills/migrating-to-2/SKILL.md +244 -0
- package/skills/options/SKILL.md +24 -12
- package/skills/rows/SKILL.md +22 -18
- package/skills/rows/references/rows-api.md +10 -6
- 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
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { getColumnLabel } from "../../core/columnUtils";
|
|
2
|
+
import type { TMDataGridFilterControlArgs } from "../../core/filterControls";
|
|
3
|
+
|
|
4
|
+
type LayoutArgs = Pick<
|
|
5
|
+
TMDataGridFilterControlArgs,
|
|
6
|
+
"column" | "layout" | "labels"
|
|
7
|
+
>;
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* The label and width props a filter control's field takes, by layout.
|
|
11
|
+
*
|
|
12
|
+
* Side by side in a panel row a field is a labelled control of a fixed
|
|
13
|
+
* comfortable width. Stacked in a narrow host it keeps the label and fills the
|
|
14
|
+
* host instead. In a header cell there is no room for a label above it at all,
|
|
15
|
+
* so the field names itself to assistive tech and fills the column.
|
|
16
|
+
*
|
|
17
|
+
* `qualifier` distinguishes the two ends of a pair, which would otherwise both
|
|
18
|
+
* read as the column's name.
|
|
19
|
+
*/
|
|
20
|
+
export function filterFieldProps(
|
|
21
|
+
args: LayoutArgs,
|
|
22
|
+
panel: { label: string; width?: number | string },
|
|
23
|
+
qualifier?: string,
|
|
24
|
+
): { label?: string; "aria-label"?: string; w?: number | string } {
|
|
25
|
+
if (args.layout === "row") return { label: panel.label, w: panel.width };
|
|
26
|
+
if (args.layout === "stacked") return { label: panel.label, w: "100%" };
|
|
27
|
+
const name = args.labels.filterOn(getColumnLabel(args.column));
|
|
28
|
+
return {
|
|
29
|
+
"aria-label": qualifier === undefined ? name : `${name} ${qualifier}`,
|
|
30
|
+
w: "100%",
|
|
31
|
+
};
|
|
32
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import type { Column } from "@tanstack/react-table";
|
|
2
|
+
import { resolveColumnOptions, type TMDataGridOption } from "../../core/columnOptions";
|
|
3
|
+
import { getColumnFilterControl, getColumnType } from "../../core/columnUtils";
|
|
4
|
+
import type { TMDataGridFilterControlComponent } from "../../core/filterControls";
|
|
5
|
+
import type { TMDataGridRowData } from "../../TMDataGridContext";
|
|
6
|
+
import type { TMDataGridFeatures, TMDataGridTable } from "../../useTMDataGrid";
|
|
7
|
+
import { TMDataGridFilterValueInput } from "./TMDataGridFilterValueInput";
|
|
8
|
+
|
|
9
|
+
type FilterColumn = Column<TMDataGridFeatures, TMDataGridRowData, unknown>;
|
|
10
|
+
|
|
11
|
+
/** One array, so "this column has no options" never changes identity. */
|
|
12
|
+
const NO_OPTIONS: ReadonlyArray<TMDataGridOption> = [];
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Whether resolving this column's options would read the faceted index -
|
|
16
|
+
* which is what makes the resolution worth memoizing, and what it goes stale
|
|
17
|
+
* against.
|
|
18
|
+
*/
|
|
19
|
+
export function filterOptionsUseFacets(column: FilterColumn): boolean {
|
|
20
|
+
const declared = column.columnDef.meta?.options;
|
|
21
|
+
return (
|
|
22
|
+
columnNeedsFilterOptions(column) &&
|
|
23
|
+
(declared === undefined || declared === "faceted")
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Whether a column's filter control is offered a list of options at all.
|
|
29
|
+
*
|
|
30
|
+
* Only where options mean something out of the box - a declared set, or a
|
|
31
|
+
* select-shaped column's faceted values. A custom control wanting faceted
|
|
32
|
+
* values on some other column resolves them itself; resolving here would build
|
|
33
|
+
* the faceted index for every filtered column.
|
|
34
|
+
*/
|
|
35
|
+
function columnNeedsFilterOptions(column: FilterColumn): boolean {
|
|
36
|
+
const type = getColumnType(column);
|
|
37
|
+
return (
|
|
38
|
+
column.columnDef.meta?.options !== undefined ||
|
|
39
|
+
type === "select" ||
|
|
40
|
+
type === "multiSelect"
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* What a column's filter control is, and what options it is handed - the one
|
|
46
|
+
* decision the panel row and the header cell make identically.
|
|
47
|
+
*
|
|
48
|
+
* Not a hook: the panel resolves this inside a `map` over its rows, where a
|
|
49
|
+
* hook cannot go. The header row memoizes the call itself, because it
|
|
50
|
+
* re-renders with the table on every scroll frame.
|
|
51
|
+
*/
|
|
52
|
+
export function filterControlFor(
|
|
53
|
+
table: TMDataGridTable<TMDataGridRowData>,
|
|
54
|
+
column: FilterColumn,
|
|
55
|
+
): {
|
|
56
|
+
options: ReadonlyArray<TMDataGridOption>;
|
|
57
|
+
ValueControl: TMDataGridFilterControlComponent;
|
|
58
|
+
} {
|
|
59
|
+
return {
|
|
60
|
+
options: columnNeedsFilterOptions(column)
|
|
61
|
+
? resolveColumnOptions({ table, column, fallback: "faceted" })
|
|
62
|
+
: NO_OPTIONS,
|
|
63
|
+
ValueControl: getColumnFilterControl(column) ?? TMDataGridFilterValueInput,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import type { ColumnDef, RowData } from "@tanstack/react-table";
|
|
2
|
+
import type { TMDataGridFeatures } from "../useTMDataGrid";
|
|
3
|
+
import {
|
|
4
|
+
DETAILS_COLUMN_ID,
|
|
5
|
+
DetailsCell,
|
|
6
|
+
DetailsHeader,
|
|
7
|
+
} from "./TMDataGridDetailsColumn";
|
|
8
|
+
import { EDIT_COLUMN_ID, EditLaneCell } from "./TMDataGridEditColumn";
|
|
9
|
+
import { GROUP_COLUMN_ID, GroupCell, GroupHeader } from "./TMDataGridGroupColumn";
|
|
10
|
+
import {
|
|
11
|
+
SELECT_COLUMN_ID,
|
|
12
|
+
SelectAllHeader,
|
|
13
|
+
SelectRowCheckbox,
|
|
14
|
+
} from "./TMDataGridSelectColumn";
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* The generated checkbox column, prepended under
|
|
18
|
+
* `selectionMode: "checkbox"` (the default) or `"checkboxAndHighlight"`.
|
|
19
|
+
*/
|
|
20
|
+
export function createSelectColumn<TData extends RowData>(
|
|
21
|
+
label = "Checkbox selection",
|
|
22
|
+
): ColumnDef<TMDataGridFeatures, TData, unknown> {
|
|
23
|
+
return {
|
|
24
|
+
id: SELECT_COLUMN_ID,
|
|
25
|
+
meta: {
|
|
26
|
+
label,
|
|
27
|
+
align: "center",
|
|
28
|
+
// Structurally the first column; it also anchors the left pinned lane, so
|
|
29
|
+
// no other column can be moved in front of it.
|
|
30
|
+
enableOrdering: false,
|
|
31
|
+
},
|
|
32
|
+
// A system lane: as wide as the control it holds and no wider. Fixed at
|
|
33
|
+
// every scale - the control does not grow with the font size, so neither
|
|
34
|
+
// should its track.
|
|
35
|
+
size: 36,
|
|
36
|
+
minSize: 36,
|
|
37
|
+
maxSize: 36,
|
|
38
|
+
enableResizing: false,
|
|
39
|
+
enableSorting: false,
|
|
40
|
+
enableColumnFilter: false,
|
|
41
|
+
enableGlobalFilter: false,
|
|
42
|
+
// Not a column the user chose, so not one they can switch off: hiding the
|
|
43
|
+
// lane would take the grid's only way to select a row with it, with the
|
|
44
|
+
// row-selection state left behind and no way back to it. Keeping it out of
|
|
45
|
+
// "Manage columns" follows from this - the panel lists what can be hidden.
|
|
46
|
+
enableHiding: false,
|
|
47
|
+
// Structurally pinned to the left; users shouldn't be able to move it.
|
|
48
|
+
enablePinning: false,
|
|
49
|
+
header: ({ table }) => <SelectAllHeader table={table} />,
|
|
50
|
+
cell: ({ row }) => <SelectRowCheckbox row={row} />,
|
|
51
|
+
// Every cell on a group row that is not the grouped column counts as
|
|
52
|
+
// aggregated, this lane included, and an aggregated cell with nothing
|
|
53
|
+
// declared renders blank. Without this the checkbox would disappear from
|
|
54
|
+
// exactly the rows that select a whole group. See renderCellContent.
|
|
55
|
+
aggregatedCell: ({ row }) => <SelectRowCheckbox row={row} />,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* The generated tree column, prepended whenever grouping is enabled and hidden
|
|
61
|
+
* again while `grouping` is empty - see the visibility effect in
|
|
62
|
+
* `useTMDataGrid`.
|
|
63
|
+
*
|
|
64
|
+
* It exists because TanStack ships no auto group column: `groupedColumnMode:
|
|
65
|
+
* "remove"` takes the grouped column out of the grid, so something has to hold
|
|
66
|
+
* the tree. Modelled on the checkbox column, which is generated the same way.
|
|
67
|
+
*
|
|
68
|
+
* Not groupable itself, and nothing had to be written to make that true -
|
|
69
|
+
* `column.getCanGroup()` requires an `accessorFn`, which a display column has
|
|
70
|
+
* no reason to have.
|
|
71
|
+
*/
|
|
72
|
+
export function createGroupColumn<TData extends RowData>(
|
|
73
|
+
label = "Group",
|
|
74
|
+
): ColumnDef<TMDataGridFeatures, TData, unknown> {
|
|
75
|
+
return {
|
|
76
|
+
id: GROUP_COLUMN_ID,
|
|
77
|
+
meta: {
|
|
78
|
+
label,
|
|
79
|
+
// Structurally the first column after the checkbox lane.
|
|
80
|
+
enableOrdering: false,
|
|
81
|
+
},
|
|
82
|
+
size: 260,
|
|
83
|
+
minSize: 180,
|
|
84
|
+
enableSorting: false,
|
|
85
|
+
enableColumnFilter: false,
|
|
86
|
+
enableGlobalFilter: false,
|
|
87
|
+
// Keeps it out of the columns panel and out of the header menu: its
|
|
88
|
+
// visibility is not the user's to set, it follows the grouping state.
|
|
89
|
+
enableHiding: false,
|
|
90
|
+
// Structurally pinned to the left; users shouldn't be able to move it.
|
|
91
|
+
enablePinning: false,
|
|
92
|
+
cell: ({ row }) => <GroupCell row={row} />,
|
|
93
|
+
// A group row has subRows, so every cell on it that is not the grouped
|
|
94
|
+
// column reports `getIsAggregated()` - this lane included. Without an
|
|
95
|
+
// `aggregatedCell` the body would take that as "nothing to summarise" and
|
|
96
|
+
// render the tree lane blank on exactly the rows it exists for.
|
|
97
|
+
aggregatedCell: ({ row }) => <GroupCell row={row} />,
|
|
98
|
+
header: ({ table }) => <GroupHeader table={table} />,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* The generated details lane, prepended whenever `renderDetails` is set.
|
|
104
|
+
*
|
|
105
|
+
* Structural, like the checkbox and tree columns: fixed width, pinned to the
|
|
106
|
+
* left after both of them, not hideable, not movable and not resizable. Moving
|
|
107
|
+
* or hiding the toggle would leave rows with panels that cannot be opened.
|
|
108
|
+
*
|
|
109
|
+
* Last of the three because it acts on a single record.
|
|
110
|
+
*
|
|
111
|
+
* A second toggle elsewhere is supported: `row.toggleExpanded()` is the entire
|
|
112
|
+
* interface, and this lane is only the control the grid ships.
|
|
113
|
+
*/
|
|
114
|
+
export function createDetailsColumn<TData extends RowData>(
|
|
115
|
+
label = "Details",
|
|
116
|
+
): ColumnDef<TMDataGridFeatures, TData, unknown> {
|
|
117
|
+
return {
|
|
118
|
+
id: DETAILS_COLUMN_ID,
|
|
119
|
+
meta: {
|
|
120
|
+
label,
|
|
121
|
+
align: "center",
|
|
122
|
+
// Structurally the last of the generated lanes.
|
|
123
|
+
enableOrdering: false,
|
|
124
|
+
},
|
|
125
|
+
// A system lane: as wide as the control it holds and no wider. Fixed at
|
|
126
|
+
// every scale - the control does not grow with the font size, so neither
|
|
127
|
+
// should its track.
|
|
128
|
+
size: 36,
|
|
129
|
+
minSize: 36,
|
|
130
|
+
maxSize: 36,
|
|
131
|
+
enableResizing: false,
|
|
132
|
+
enableSorting: false,
|
|
133
|
+
enableColumnFilter: false,
|
|
134
|
+
enableGlobalFilter: false,
|
|
135
|
+
// Its visibility is not the user's to set: hiding it would strand every
|
|
136
|
+
// panel behind a control that is no longer there.
|
|
137
|
+
enableHiding: false,
|
|
138
|
+
// Structurally pinned to the left; users shouldn't be able to move it.
|
|
139
|
+
enablePinning: false,
|
|
140
|
+
header: ({ table }) => <DetailsHeader table={table} />,
|
|
141
|
+
cell: ({ row }) => <DetailsCell row={row} />,
|
|
142
|
+
// Deliberately no `aggregatedCell`: on a group row every cell outside the
|
|
143
|
+
// grouped column counts as aggregated, and blank is the right answer here -
|
|
144
|
+
// groups expand into their rows, not into a panel.
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* The generated edit lane, appended and pinned right - the row's Save at the
|
|
150
|
+
* end of the row under `mode: "row"`, the state marker and revert under
|
|
151
|
+
* `editing.draft`, mirroring the checkbox lane's build on the left.
|
|
152
|
+
*/
|
|
153
|
+
export function createEditColumn<TData extends RowData>(
|
|
154
|
+
label = "Edit",
|
|
155
|
+
/** A draft lane holds three controls where the rest hold two. */
|
|
156
|
+
wide = false,
|
|
157
|
+
): ColumnDef<TMDataGridFeatures, TData, unknown> {
|
|
158
|
+
const width = wide ? 88 : 64;
|
|
159
|
+
return {
|
|
160
|
+
id: EDIT_COLUMN_ID,
|
|
161
|
+
meta: {
|
|
162
|
+
label,
|
|
163
|
+
align: "center",
|
|
164
|
+
enableOrdering: false,
|
|
165
|
+
},
|
|
166
|
+
// Wide enough for the pair (or draft's trio) it holds while editing.
|
|
167
|
+
size: width,
|
|
168
|
+
minSize: width,
|
|
169
|
+
maxSize: width,
|
|
170
|
+
enableResizing: false,
|
|
171
|
+
enableSorting: false,
|
|
172
|
+
enableColumnFilter: false,
|
|
173
|
+
enableGlobalFilter: false,
|
|
174
|
+
// The row's Save, Cancel and Delete live here, so hiding the lane would
|
|
175
|
+
// strand an open row with no way to commit or discard it. Same rule as the
|
|
176
|
+
// checkbox lane: chrome the grid generates is not a user setting.
|
|
177
|
+
enableHiding: false,
|
|
178
|
+
// Structurally pinned to the right; not movable.
|
|
179
|
+
enablePinning: false,
|
|
180
|
+
header: () => null,
|
|
181
|
+
cell: ({ row }) => <EditLaneCell row={row} />,
|
|
182
|
+
// Group rows: same reasoning as the checkbox lane - without this the
|
|
183
|
+
// cell renders blank on aggregated rows, but here blank is also correct,
|
|
184
|
+
// so the aggregated cell renders the same (null for groups).
|
|
185
|
+
aggregatedCell: ({ row }) => <EditLaneCell row={row} />,
|
|
186
|
+
};
|
|
187
|
+
}
|
|
@@ -104,3 +104,47 @@
|
|
|
104
104
|
opacity: 0;
|
|
105
105
|
}
|
|
106
106
|
}
|
|
107
|
+
|
|
108
|
+
/* The scrolled-under shadow: a soft band under the header, only while body
|
|
109
|
+
rows are actually beneath it. Worn by the last header row - the header/body
|
|
110
|
+
boundary - which is the filter row under `filters.inHeader` and the last
|
|
111
|
+
group row otherwise. Lives here rather than in either row's own module
|
|
112
|
+
because the two are in different modules and the boundary moves between
|
|
113
|
+
them.
|
|
114
|
+
|
|
115
|
+
A scroll-driven animation, like the pinned-lane gradients: the shadow tracks
|
|
116
|
+
the scroll on the compositor with no listener and no render, and an inactive
|
|
117
|
+
timeline (nothing to scroll) leaves `opacity: 0` standing, so a grid that
|
|
118
|
+
fits shows nothing. Where unsupported there is simply no shadow - the
|
|
119
|
+
header's border already draws the boundary. */
|
|
120
|
+
.headerBoundary::after {
|
|
121
|
+
content: "";
|
|
122
|
+
position: absolute;
|
|
123
|
+
inset: 100% 0 auto 0;
|
|
124
|
+
height: 6px;
|
|
125
|
+
pointer-events: none;
|
|
126
|
+
background: linear-gradient(
|
|
127
|
+
to bottom,
|
|
128
|
+
var(--dg-header-shadow-color, rgba(0, 0, 0, 0.14)),
|
|
129
|
+
transparent
|
|
130
|
+
);
|
|
131
|
+
opacity: 0;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
@supports (animation-timeline: scroll()) {
|
|
135
|
+
.headerBoundary::after {
|
|
136
|
+
animation: dgHeaderShadow linear both;
|
|
137
|
+
animation-timeline: scroll(nearest block);
|
|
138
|
+
/* Arrives over the first rows leaving, reads as depth not as a fade. */
|
|
139
|
+
animation-range: 0px 24px;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
@keyframes dgHeaderShadow {
|
|
144
|
+
from {
|
|
145
|
+
opacity: 0;
|
|
146
|
+
}
|
|
147
|
+
to {
|
|
148
|
+
opacity: 1;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { useSelector } from "@tanstack/react-store";
|
|
2
|
+
import { useTMDataGridContext } from "../TMDataGridContext";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The columns a column chooser may list, and the two writes it makes.
|
|
6
|
+
*
|
|
7
|
+
* Shared by `TMDataGridColumnsPanel` (plain controls) and
|
|
8
|
+
* `TMDataGrid.Menu.Columns` (menu items), so the two always list the same
|
|
9
|
+
* columns and hide them the same way.
|
|
10
|
+
*/
|
|
11
|
+
export function useHideableColumns() {
|
|
12
|
+
const { table } = useTMDataGridContext();
|
|
13
|
+
|
|
14
|
+
const columnVisibility = useSelector(
|
|
15
|
+
table.store,
|
|
16
|
+
(state) => state.columnVisibility,
|
|
17
|
+
);
|
|
18
|
+
|
|
19
|
+
// Only what can actually be hidden. A column with `enableHiding: false` is
|
|
20
|
+
// left out rather than listed and disabled: a box that cannot be ticked only
|
|
21
|
+
// invites the question, and every generated lane is one - the checkbox and
|
|
22
|
+
// edit lanes hold the controls the grid needs, the tree column follows the
|
|
23
|
+
// grouping state, the row-number gutter follows `enableRowNumbers`. None of
|
|
24
|
+
// them is a setting.
|
|
25
|
+
const columns = table
|
|
26
|
+
.getAllLeafColumns()
|
|
27
|
+
.filter((column) => column.getCanHide());
|
|
28
|
+
|
|
29
|
+
const shownCount = columns.filter(
|
|
30
|
+
(column) => columnVisibility[column.id] !== false,
|
|
31
|
+
).length;
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Show or hide every listed column.
|
|
35
|
+
*
|
|
36
|
+
* Not `table.toggleAllColumnsVisible`, which writes a visibility entry for
|
|
37
|
+
* *every* leaf column: showing all would publish the tree column - hidden
|
|
38
|
+
* because nothing is grouped, not because the user hid it - and hiding all
|
|
39
|
+
* would force the same column visible, since it writes `!getCanHide()` for
|
|
40
|
+
* the columns it will not touch. Either way a lane the panel never listed
|
|
41
|
+
* changes state, and persistence then keeps it that way.
|
|
42
|
+
*/
|
|
43
|
+
const setAllVisible = (visible: boolean) => {
|
|
44
|
+
table.setColumnVisibility((previous) => {
|
|
45
|
+
const next = { ...previous };
|
|
46
|
+
for (const column of columns) next[column.id] = visible;
|
|
47
|
+
return next;
|
|
48
|
+
});
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
return { columns, columnVisibility, shownCount, setAllVisible };
|
|
52
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { RowData } from "@tanstack/react-table";
|
|
2
2
|
import type { TMDataGridRowData } from "../TMDataGridContext";
|
|
3
3
|
import type { TMDataGridTable } from "../useTMDataGrid";
|
|
4
|
+
import { isHTMLElement } from "./dom";
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* Room the header keeps for its hover-revealed actions (sort arrow, menu) and
|
|
@@ -26,7 +27,9 @@ const CONTENT_ALLOWANCE = 2;
|
|
|
26
27
|
* measure (jsdom reports 0 there).
|
|
27
28
|
*/
|
|
28
29
|
function contentSpanWidth(content: HTMLElement): number {
|
|
29
|
-
|
|
30
|
+
// The cell's own document, not the global one: a Range from the opener's
|
|
31
|
+
// document cannot select nodes in a window opened with `window.open`.
|
|
32
|
+
const range = content.ownerDocument.createRange();
|
|
30
33
|
range.selectNodeContents(content);
|
|
31
34
|
// jsdom's Range has no getBoundingClientRect at all, hence the guard
|
|
32
35
|
// rather than a zero-check alone.
|
|
@@ -84,7 +87,7 @@ export function measureColumnContentWidth({
|
|
|
84
87
|
|
|
85
88
|
for (const cell of mountedCells(container, columnId)) {
|
|
86
89
|
const content = cell.firstElementChild;
|
|
87
|
-
if (!(content
|
|
90
|
+
if (!isHTMLElement(content)) continue;
|
|
88
91
|
const styles = getComputedStyle(cell);
|
|
89
92
|
const padding =
|
|
90
93
|
(Number.parseFloat(styles.paddingLeft) || 0) +
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Column, Row } from "@tanstack/react-table";
|
|
1
|
+
import type { Column, Row, RowData } from "@tanstack/react-table";
|
|
2
2
|
import type { TMDataGridRowData } from "../TMDataGridContext";
|
|
3
3
|
import type { TMDataGridFeatures, TMDataGridTable } from "../useTMDataGrid";
|
|
4
4
|
|
|
@@ -17,17 +17,21 @@ export type TMDataGridOption = {
|
|
|
17
17
|
group?: string;
|
|
18
18
|
};
|
|
19
19
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
20
|
+
/**
|
|
21
|
+
* What an options function is called with. `TData` is the row type of the
|
|
22
|
+
* column helper the column was declared with.
|
|
23
|
+
*/
|
|
24
|
+
export type TMDataGridOptionsArgs<TData extends RowData = TMDataGridRowData> = {
|
|
25
|
+
table: TMDataGridTable<TData>;
|
|
26
|
+
column: Column<TMDataGridFeatures, TData, unknown>;
|
|
23
27
|
/** The row an editor is standing on. Absent when the filter panel asks. */
|
|
24
|
-
row?: Row<TMDataGridFeatures,
|
|
28
|
+
row?: Row<TMDataGridFeatures, TData>;
|
|
25
29
|
/**
|
|
26
30
|
* Source to use when the column declares none. The filter panel passes
|
|
27
31
|
* `"faceted"` so a select column with no `meta.options` still offers the
|
|
28
32
|
* values that exist, rather than an empty dropdown.
|
|
29
33
|
*/
|
|
30
|
-
fallback?: TMDataGridOptionsSource
|
|
34
|
+
fallback?: TMDataGridOptionsSource<TData>;
|
|
31
35
|
};
|
|
32
36
|
|
|
33
37
|
/**
|
|
@@ -40,10 +44,57 @@ export type TMDataGridOptionsArgs = {
|
|
|
40
44
|
* | `"faceted"` | low-cardinality data columns: the distinct values present, via `getFacetedUniqueValues` |
|
|
41
45
|
* | function | large or contextual sets - `row` is set when an editor asks, so options can depend on the record (city given country) |
|
|
42
46
|
*/
|
|
43
|
-
export type TMDataGridOptionsSource =
|
|
47
|
+
export type TMDataGridOptionsSource<TData extends RowData = TMDataGridRowData> =
|
|
44
48
|
| ReadonlyArray<TMDataGridOption | string>
|
|
45
49
|
| "faceted"
|
|
46
|
-
| ((
|
|
50
|
+
| ((
|
|
51
|
+
args: TMDataGridOptionsArgs<TData>,
|
|
52
|
+
) => ReadonlyArray<TMDataGridOption | string>);
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Columns already warned about, per grid - so a warning fires once and a
|
|
56
|
+
* test's grid is not silenced by another test's. Keyed on `table.store`, not
|
|
57
|
+
* on the table: `useTable` returns a fresh table object every render, while
|
|
58
|
+
* the store is created once and shared by every render's copy.
|
|
59
|
+
*/
|
|
60
|
+
const warnedFaceted = new WeakMap<object, Set<string>>();
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Faceted options read the distinct values in `data`, which under
|
|
64
|
+
* `manualFiltering` or `manualPagination` is whatever the server sent for the
|
|
65
|
+
* current page. The dropdown then offers the values that happen to be on the
|
|
66
|
+
* page the user is looking at, and looks correct while being wrong - so it is
|
|
67
|
+
* said out loud, once per column.
|
|
68
|
+
*
|
|
69
|
+
* Fires from render, unlike the library's other warnings: the fallback form
|
|
70
|
+
* of `"faceted"` only exists at resolve time, which is render. The guard
|
|
71
|
+
* makes it once per grid regardless - a StrictMode double render or a
|
|
72
|
+
* discarded concurrent render marks the set the same way a committed one
|
|
73
|
+
* does. H3 in the backlog folds it into the diagnostics mechanism with the
|
|
74
|
+
* rest.
|
|
75
|
+
*/
|
|
76
|
+
function warnFacetedUnderManualMode(
|
|
77
|
+
table: TMDataGridTable<TMDataGridRowData>,
|
|
78
|
+
columnId: string,
|
|
79
|
+
): void {
|
|
80
|
+
if (
|
|
81
|
+
table.options.manualFiltering !== true &&
|
|
82
|
+
table.options.manualPagination !== true
|
|
83
|
+
) {
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
const store = table.store as object;
|
|
87
|
+
let warned = warnedFaceted.get(store);
|
|
88
|
+
if (warned === undefined) {
|
|
89
|
+
warned = new Set();
|
|
90
|
+
warnedFaceted.set(store, warned);
|
|
91
|
+
}
|
|
92
|
+
if (warned.has(columnId)) return;
|
|
93
|
+
warned.add(columnId);
|
|
94
|
+
console.warn(
|
|
95
|
+
`TMDataGrid: column "${columnId}" resolves faceted options while the server owns the rows - the distinct values of one page are not the distinct values of the result set. Pass meta.options as a list or a function instead.`,
|
|
96
|
+
);
|
|
97
|
+
}
|
|
47
98
|
|
|
48
99
|
function addFacetValue(target: Set<string>, value: unknown): void {
|
|
49
100
|
if (value === null || value === undefined || value === "") return;
|
|
@@ -68,6 +119,7 @@ export function resolveColumnOptions({
|
|
|
68
119
|
if (!source) return [];
|
|
69
120
|
|
|
70
121
|
if (source === "faceted") {
|
|
122
|
+
warnFacetedUnderManualMode(table, column.id);
|
|
71
123
|
const values = new Set<string>();
|
|
72
124
|
for (const key of column.getFacetedUniqueValues().keys()) {
|
|
73
125
|
if (Array.isArray(key)) {
|
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type {
|
|
2
|
+
Column,
|
|
3
|
+
ColumnPinningPosition,
|
|
4
|
+
ColumnPinningState,
|
|
5
|
+
} from "@tanstack/react-table";
|
|
2
6
|
import type { TMDataGridRowData } from "../TMDataGridContext";
|
|
3
7
|
import { isColumnReorderable, isGeneratedColumn } from "./columnUtils";
|
|
4
8
|
import type { TMDataGridFeatures, TMDataGridTable } from "../useTMDataGrid";
|
|
@@ -10,11 +14,11 @@ type GridTable = TMDataGridTable<TMDataGridRowData>;
|
|
|
10
14
|
* The lane a column renders in.
|
|
11
15
|
*
|
|
12
16
|
* Pinning splits the grid into three, and TanStack sequences them from two
|
|
13
|
-
* different state slices: `columnPinning.
|
|
17
|
+
* different state slices: `columnPinning.start` / `.end` order the pinned
|
|
14
18
|
* lanes, `columnOrder` orders the centre. A move therefore always stays inside
|
|
15
19
|
* one lane - moving a column into another one would be a pin, not a reorder.
|
|
16
20
|
*/
|
|
17
|
-
export type TMDataGridColumnRegion = "
|
|
21
|
+
export type TMDataGridColumnRegion = "start" | "center" | "end";
|
|
18
22
|
|
|
19
23
|
/** Which edge of the column under the pointer a dragged column lands on. */
|
|
20
24
|
export type TMDataGridDropSide = "before" | "after";
|
|
@@ -23,11 +27,26 @@ export function getColumnRegion(
|
|
|
23
27
|
columnPinning: ColumnPinningState,
|
|
24
28
|
columnId: string,
|
|
25
29
|
): TMDataGridColumnRegion {
|
|
26
|
-
if (columnPinning.
|
|
27
|
-
if (columnPinning.
|
|
30
|
+
if (columnPinning.start.includes(columnId)) return "start";
|
|
31
|
+
if (columnPinning.end.includes(columnId)) return "end";
|
|
28
32
|
return "center";
|
|
29
33
|
}
|
|
30
34
|
|
|
35
|
+
/**
|
|
36
|
+
* TanStack's pinned regions are logical: `start` and `end` follow the writing
|
|
37
|
+
* direction. The grid's own vocabulary - its sticky CSS, the `data-pinned`
|
|
38
|
+
* attribute, the column menu's "Pin left" and "Pin right" - is physical, and
|
|
39
|
+
* the grid renders left to right only. This translates between the pair:
|
|
40
|
+
* `left` maps to `start` and `right` to `end`.
|
|
41
|
+
*/
|
|
42
|
+
export function pinningPositionOfSide(
|
|
43
|
+
side: "left" | "right" | false,
|
|
44
|
+
): ColumnPinningPosition {
|
|
45
|
+
if (side === "left") return "start";
|
|
46
|
+
if (side === "right") return "end";
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
|
|
31
50
|
/** Moves one id next to another, leaving every other id in place. */
|
|
32
51
|
function moveInList(
|
|
33
52
|
ids: ReadonlyArray<string>,
|
|
@@ -87,7 +106,7 @@ export function moveColumn({
|
|
|
87
106
|
* Puts the generated lanes back on the outside of both pinned lanes: the ones
|
|
88
107
|
* on the left before every consumer column, the edit lane after all of them.
|
|
89
108
|
*
|
|
90
|
-
* `column.pin("
|
|
109
|
+
* `column.pin("end")` appends, so pinning a column right would otherwise drop
|
|
91
110
|
* it outside the edit lane, so the row's Save and Delete would no longer be
|
|
92
111
|
* last in the row. Pinning left appends too, which is already correct there,
|
|
93
112
|
* but the same pass keeps both lanes in place whatever a consumer writes into
|
|
@@ -101,13 +120,13 @@ export function keepGeneratedColumnsOutermost(
|
|
|
101
120
|
): ColumnPinningState {
|
|
102
121
|
const generated = (id: string) => isGeneratedColumn(id);
|
|
103
122
|
return {
|
|
104
|
-
|
|
105
|
-
...pinning.
|
|
106
|
-
...pinning.
|
|
123
|
+
start: [
|
|
124
|
+
...pinning.start.filter(generated),
|
|
125
|
+
...pinning.start.filter((id) => !generated(id)),
|
|
107
126
|
],
|
|
108
|
-
|
|
109
|
-
...pinning.
|
|
110
|
-
...pinning.
|
|
127
|
+
end: [
|
|
128
|
+
...pinning.end.filter((id) => !generated(id)),
|
|
129
|
+
...pinning.end.filter(generated),
|
|
111
130
|
],
|
|
112
131
|
};
|
|
113
132
|
}
|
|
@@ -117,8 +136,8 @@ function getRegionColumns(
|
|
|
117
136
|
table: GridTable,
|
|
118
137
|
region: TMDataGridColumnRegion,
|
|
119
138
|
): Array<GridColumn> {
|
|
120
|
-
if (region === "
|
|
121
|
-
if (region === "
|
|
139
|
+
if (region === "start") return table.getStartVisibleLeafColumns();
|
|
140
|
+
if (region === "end") return table.getEndVisibleLeafColumns();
|
|
122
141
|
return table.getCenterVisibleLeafColumns();
|
|
123
142
|
}
|
|
124
143
|
|