@jielga/tmdatagrid 2.0.0-beta.8 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -212
- package/dist/index.d.ts +1323 -796
- package/dist/index.js +4719 -3193
- package/dist/index.js.map +1 -1
- package/dist/styles.css +1 -1
- package/docs/adding-rows.md +132 -0
- package/docs/anatomy.md +119 -0
- package/docs/card-view.md +108 -0
- package/docs/cell-selection.md +194 -0
- package/docs/column-layout.md +182 -0
- package/docs/column-menu.md +66 -0
- package/docs/columns.md +268 -0
- package/docs/components.md +311 -0
- package/docs/draft-store.md +242 -0
- package/docs/editing.md +303 -0
- package/docs/editors.md +250 -0
- package/docs/export.md +319 -0
- package/docs/filtering.md +362 -0
- package/docs/getting-started.md +123 -0
- package/docs/grouping.md +165 -0
- package/docs/loading-and-empty.md +92 -0
- package/docs/localization.md +79 -0
- package/docs/menu.md +143 -0
- package/docs/migrating-to-2.md +163 -0
- package/docs/pagination.md +144 -0
- package/docs/persistence.md +114 -0
- package/docs/portfolio-rebalancer.md +94 -0
- package/docs/query-builder.md +179 -0
- package/docs/quick-search.md +84 -0
- package/docs/row-details.md +115 -0
- package/docs/row-interaction.md +149 -0
- package/docs/row-pinning.md +132 -0
- package/docs/row-selection.md +136 -0
- package/docs/row-styling.md +133 -0
- package/docs/scrolling.md +112 -0
- package/docs/server-query.md +246 -0
- package/docs/server-side.md +206 -0
- package/docs/sorting.md +101 -0
- package/docs/styling.md +126 -0
- package/docs/summary-row.md +76 -0
- package/docs/testing.md +744 -0
- package/docs/toolbar.md +161 -0
- package/docs/use-tm-data-grid.md +361 -0
- package/package.json +22 -46
- package/skills/appearance/SKILL.md +72 -19
- package/skills/cell-selection/SKILL.md +69 -78
- package/skills/columns/SKILL.md +90 -34
- package/skills/data/SKILL.md +86 -16
- package/skills/editing/SKILL.md +83 -50
- package/skills/editing/references/common-mistakes.md +77 -69
- package/skills/editing/references/editing-api.md +31 -23
- package/skills/editing/references/editors-and-validation.md +24 -17
- package/skills/filtering/SKILL.md +148 -40
- package/skills/getting-started/SKILL.md +17 -15
- package/skills/grouping/SKILL.md +31 -16
- package/skills/options/SKILL.md +8 -8
- package/skills/rows/SKILL.md +22 -18
- package/skills/server-side/SKILL.md +170 -17
- package/skills/testing/SKILL.md +150 -32
- package/skills/testing-components/SKILL.md +230 -0
- package/skills/testing-editing/SKILL.md +240 -0
- package/src/{tmdatagrid/TMDataGridContext.ts → TMDataGridContext.ts} +7 -19
- package/src/{tmdatagrid/components → components}/TMDataGrid.module.css +7 -1
- package/src/{tmdatagrid/components → components}/TMDataGrid.tsx +38 -21
- package/src/{tmdatagrid/components → components}/TMDataGridCellEditor.tsx +73 -7
- package/src/{tmdatagrid/components → components}/TMDataGridColumnsPanel.module.css +5 -1
- package/src/{tmdatagrid/components → components}/TMDataGridColumnsPanel.tsx +47 -55
- package/src/{tmdatagrid/components → components}/TMDataGridDetailsColumn.tsx +9 -55
- package/src/{tmdatagrid/components → components}/TMDataGridDraftActions.tsx +41 -24
- package/src/{tmdatagrid/components → components}/TMDataGridEditColumn.tsx +23 -63
- package/src/components/TMDataGridEntryRows.tsx +354 -0
- package/src/components/TMDataGridExportPicker.module.css +77 -0
- package/src/components/TMDataGridExportPicker.tsx +234 -0
- package/src/components/TMDataGridFilterPanel.module.css +54 -0
- package/src/components/TMDataGridFilterPanel.tsx +348 -0
- package/src/{tmdatagrid/components → components}/TMDataGridFilterPills.tsx +15 -8
- package/src/components/TMDataGridFilterSurface.module.css +54 -0
- package/src/components/TMDataGridFilterSurface.tsx +167 -0
- package/src/{tmdatagrid/components → components}/TMDataGridFooter.tsx +53 -90
- package/src/{tmdatagrid/components → components}/TMDataGridGroupColumn.tsx +9 -72
- package/src/{tmdatagrid/components → components}/TMDataGridHeaderCell.module.css +12 -2
- package/src/{tmdatagrid/components → components}/TMDataGridHeaderCell.tsx +58 -21
- package/src/components/TMDataGridHeaderFilterRow.module.css +51 -0
- package/src/components/TMDataGridHeaderFilterRow.tsx +301 -0
- package/src/components/TMDataGridMenu.tsx +357 -0
- package/src/{tmdatagrid/components → components}/TMDataGridSelectColumn.tsx +15 -53
- package/src/{tmdatagrid/components → components}/TMDataGridTable.module.css +88 -65
- package/src/{tmdatagrid/components → components}/TMDataGridTable.tsx +579 -165
- package/src/{tmdatagrid/components → components}/TMDataGridToolbar.module.css +5 -0
- package/src/components/TMDataGridToolbar.tsx +181 -0
- package/src/{tmdatagrid/components → components}/editors/TMDataGridBooleanEditor.tsx +3 -3
- package/src/{tmdatagrid/components → components}/editors/TMDataGridDateEditor.tsx +3 -3
- package/src/{tmdatagrid/components → components}/editors/TMDataGridMultiSelectEditor.tsx +3 -3
- package/src/{tmdatagrid/components → components}/editors/TMDataGridNumberEditor.tsx +3 -3
- package/src/{tmdatagrid/components → components}/editors/TMDataGridSelectEditor.tsx +3 -3
- package/src/{tmdatagrid/components → components}/editors/TMDataGridStringEditor.tsx +3 -3
- package/src/{tmdatagrid/components → components}/editors/editorShared.ts +16 -2
- package/src/{tmdatagrid/components → components}/filters/DgAutocompleteFilter.tsx +5 -4
- package/src/{tmdatagrid/components → components}/filters/DgDateRangeFilter.tsx +22 -5
- package/src/{tmdatagrid/components → components}/filters/DgRangeSliderFilter.tsx +5 -1
- package/src/{tmdatagrid/components → components}/filters/DgTriStateFilter.tsx +5 -1
- package/src/{tmdatagrid/components → components}/filters/TMDataGridFilterValueInput.tsx +44 -28
- package/src/components/filters/controlLayout.ts +32 -0
- package/src/components/filters/filterControlFor.ts +65 -0
- package/src/components/generatedColumns.tsx +187 -0
- package/src/{tmdatagrid/components → components}/icons.ts +1 -0
- package/src/{tmdatagrid/components → components}/sticky.module.css +44 -0
- package/src/components/useHideableColumns.ts +52 -0
- package/src/{tmdatagrid/core → core}/autosize.ts +5 -2
- package/src/{tmdatagrid/core → core}/capabilities.ts +5 -5
- package/src/{tmdatagrid/core → core}/columnOptions.ts +60 -8
- package/src/{tmdatagrid/core → core}/columnOrdering.ts +33 -14
- package/src/{tmdatagrid/core → core}/columnUtils.ts +44 -5
- package/src/core/controlledStateSync.ts +108 -0
- package/src/core/deletedRows.ts +34 -0
- package/src/core/dom.ts +74 -0
- package/src/{tmdatagrid/core → core}/editEngine.ts +1172 -388
- package/src/{tmdatagrid/core → core}/editorFocus.ts +8 -4
- package/src/core/export.ts +704 -0
- package/src/{tmdatagrid/core → core}/filterControls.ts +38 -1
- package/src/{tmdatagrid/core → core}/filterOperators.ts +64 -1
- package/src/core/filterSurface.ts +99 -0
- package/src/{tmdatagrid/core → core}/grouping.ts +21 -0
- package/src/{tmdatagrid/core → core}/labels.ts +51 -6
- package/src/{tmdatagrid/core → core}/labelsSv.ts +27 -6
- package/src/core/pageReset.ts +120 -0
- package/src/core/pagination.ts +81 -0
- package/src/{tmdatagrid/core → core}/persistence.ts +22 -5
- package/src/{tmdatagrid/core → core}/summary.ts +20 -4
- package/src/{tmdatagrid/index.ts → index.ts} +70 -36
- package/src/{tmdatagrid/useTMDataGrid.tsx → useTMDataGrid.tsx} +534 -123
- package/src/useTMDataGridExport.ts +78 -0
- package/src/tmdatagrid/components/TMDataGridEntryRows.tsx +0 -298
- package/src/tmdatagrid/components/TMDataGridFilterPanel.module.css +0 -35
- package/src/tmdatagrid/components/TMDataGridFilterPanel.tsx +0 -354
- package/src/tmdatagrid/components/TMDataGridToolbar.tsx +0 -161
- package/src/tmdatagrid/core/cellExport.ts +0 -320
- /package/src/{tmdatagrid/components → components}/TMDataGridDetailsColumn.module.css +0 -0
- /package/src/{tmdatagrid/components → components}/TMDataGridFilterPills.module.css +0 -0
- /package/src/{tmdatagrid/components → components}/TMDataGridFooter.module.css +0 -0
- /package/src/{tmdatagrid/components → components}/TMDataGridGroupColumn.module.css +0 -0
- /package/src/{tmdatagrid/components → components}/TMDataGridRowNumberColumn.tsx +0 -0
- /package/src/{tmdatagrid/components → components}/TMDataGridSearch.tsx +0 -0
- /package/src/{tmdatagrid/core → core}/cellNavigation.ts +0 -0
- /package/src/{tmdatagrid/core → core}/cellRange.ts +0 -0
- /package/src/{tmdatagrid/core → core}/controlledState.ts +0 -0
- /package/src/{tmdatagrid/core → core}/draftCellContext.ts +0 -0
- /package/src/{tmdatagrid/core → core}/expanding.ts +0 -0
- /package/src/{tmdatagrid/core → core}/matchHighlight.ts +0 -0
- /package/src/{tmdatagrid/core → core}/quickSearch.ts +0 -0
- /package/src/{tmdatagrid/core → core}/resizePreview.ts +0 -0
- /package/src/{tmdatagrid/core → core}/rowPinning.ts +0 -0
- /package/src/{tmdatagrid/core → core}/rowSelection.ts +0 -0
- /package/src/{tmdatagrid/core → core}/sizes.ts +0 -0
- /package/src/{tmdatagrid/core → core}/useSettledTableState.ts +0 -0
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ActionIcon,
|
|
3
|
+
Box,
|
|
4
|
+
type BoxProps,
|
|
5
|
+
Loader,
|
|
6
|
+
Text,
|
|
7
|
+
Tooltip,
|
|
8
|
+
} from "@mantine/core";
|
|
9
|
+
import { useSelector } from "@tanstack/react-store";
|
|
10
|
+
import type { ReactNode } from "react";
|
|
11
|
+
import classes from "./TMDataGridToolbar.module.css";
|
|
12
|
+
import { useTMDataGridContext } from "../TMDataGridContext";
|
|
13
|
+
import { getGridCapabilities } from "../core/capabilities";
|
|
14
|
+
import { useSettledTableState } from "../core/useSettledTableState";
|
|
15
|
+
import { isFilterActive } from "../core/filterOperators";
|
|
16
|
+
import { FilterIcon } from "./icons";
|
|
17
|
+
import { seedColumnFilter } from "../useTMDataGrid";
|
|
18
|
+
|
|
19
|
+
export type TMDataGridToolbarProps = BoxProps & {
|
|
20
|
+
children?: ReactNode;
|
|
21
|
+
/**
|
|
22
|
+
* Draws a 1px line under the toolbar in the theme's default border colour -
|
|
23
|
+
* the same line the header draws under itself. Defaults to `false`.
|
|
24
|
+
*/
|
|
25
|
+
withBottomBorder?: boolean;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Row above the grid. Compose it from the pieces below, or anything else.
|
|
30
|
+
* Takes Mantine's style props (`mb="sm"`, `px="md"`, `hiddenFrom="sm"`) and
|
|
31
|
+
* sets them on the row.
|
|
32
|
+
*/
|
|
33
|
+
export function TMDataGridToolbar({
|
|
34
|
+
children,
|
|
35
|
+
withBottomBorder = false,
|
|
36
|
+
className,
|
|
37
|
+
mod,
|
|
38
|
+
...others
|
|
39
|
+
}: TMDataGridToolbarProps) {
|
|
40
|
+
return (
|
|
41
|
+
<Box
|
|
42
|
+
data-dg-part="toolbar"
|
|
43
|
+
className={[classes.toolbar, className].filter(Boolean).join(" ")}
|
|
44
|
+
mod={[{ "with-bottom-border": withBottomBorder }, mod]}
|
|
45
|
+
{...others}
|
|
46
|
+
>
|
|
47
|
+
{children}
|
|
48
|
+
</Box>
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Pushes the following toolbar items to the right. Takes Mantine's style props. */
|
|
53
|
+
export function TMDataGridToolbarSpacer({ className, ...others }: BoxProps) {
|
|
54
|
+
return (
|
|
55
|
+
<Box
|
|
56
|
+
className={[classes.toolbarSpacer, className].filter(Boolean).join(" ")}
|
|
57
|
+
{...others}
|
|
58
|
+
/>
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* A small spinner shown while `meta.loading` is true, and nothing otherwise.
|
|
64
|
+
*
|
|
65
|
+
* The body only shows its loading state while the grid is *empty* - a
|
|
66
|
+
* server-driven grid refetching with rows on screen keeps showing them, which
|
|
67
|
+
* is right, but leaves nothing saying a fetch is running. This is that signal,
|
|
68
|
+
* and the consumer decides where it sits by placing it in the toolbar:
|
|
69
|
+
*
|
|
70
|
+
* ```tsx
|
|
71
|
+
* <TMDataGrid.Toolbar>
|
|
72
|
+
* <TMDataGrid.SummaryCount />
|
|
73
|
+
* <TMDataGrid.Spacer />
|
|
74
|
+
* <TMDataGrid.LoadingIndicator />
|
|
75
|
+
* <TMDataGrid.FilterButton />
|
|
76
|
+
* </TMDataGrid.Toolbar>
|
|
77
|
+
* ```
|
|
78
|
+
*/
|
|
79
|
+
export function TMDataGridLoadingIndicator() {
|
|
80
|
+
const { table, labels } = useTMDataGridContext();
|
|
81
|
+
if (table.options.meta?.loading !== true) return null;
|
|
82
|
+
return <Loader size="xs" data-dg-part="loading" aria-label={labels.loading} />;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Visible rows over total rows. On a server-driven grid, set
|
|
87
|
+
* `meta.totalRowCount` for the denominator - the client never sees every row.
|
|
88
|
+
* Without it the count is rendered alone, since the fallback denominator would
|
|
89
|
+
* be the rows the server sent for the current page: a plausible-looking wrong
|
|
90
|
+
* total, and on an unfiltered first page the same number twice.
|
|
91
|
+
*/
|
|
92
|
+
export function TMDataGridSummaryCount({ children }: { children?: ReactNode }) {
|
|
93
|
+
const { table, controlSize } = useTMDataGridContext();
|
|
94
|
+
useSettledTableState(table.store);
|
|
95
|
+
|
|
96
|
+
const serverDriven =
|
|
97
|
+
table.options.manualFiltering === true ||
|
|
98
|
+
table.options.manualPagination === true;
|
|
99
|
+
const shown = table.getRowCount();
|
|
100
|
+
const total =
|
|
101
|
+
table.options.meta?.totalRowCount ??
|
|
102
|
+
(serverDriven ? undefined : table.getPreFilteredRowModel().rows.length);
|
|
103
|
+
|
|
104
|
+
return (
|
|
105
|
+
<Text
|
|
106
|
+
span
|
|
107
|
+
size={controlSize}
|
|
108
|
+
c="dimmed"
|
|
109
|
+
className={classes.summaryCount}
|
|
110
|
+
data-dg-part="summary-count"
|
|
111
|
+
>
|
|
112
|
+
{children !== undefined
|
|
113
|
+
? children
|
|
114
|
+
: total === undefined
|
|
115
|
+
? `${shown}`
|
|
116
|
+
: `${shown} / ${total}`}
|
|
117
|
+
</Text>
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Toggles the grid's filter surface - the popup or the sidebar, whichever
|
|
123
|
+
* `filters.surface` names - seeding a filter row on the first filterable
|
|
124
|
+
* column. The count of active filters tints it.
|
|
125
|
+
*
|
|
126
|
+
* Renders nothing when no column can be filtered (`enableColumnFilters:
|
|
127
|
+
* false`), and nothing under `filters.surface: "none"`, where there is no
|
|
128
|
+
* automatic surface for it to toggle. Read `ui.state.filterPanelOpen` and
|
|
129
|
+
* render your own control if a hand-placed panel wants one.
|
|
130
|
+
*/
|
|
131
|
+
export function TMDataGridFilterButton() {
|
|
132
|
+
const api = useTMDataGridContext();
|
|
133
|
+
const { table, ui, features, filters, labels, controlSize } = api;
|
|
134
|
+
const opened = useSelector(ui, (state) => state.filterPanelOpen);
|
|
135
|
+
const columnFilters = useSelector(
|
|
136
|
+
table.store,
|
|
137
|
+
(state) => state.columnFilters,
|
|
138
|
+
);
|
|
139
|
+
const activeCount = columnFilters.filter((filter) =>
|
|
140
|
+
isFilterActive(filter.value),
|
|
141
|
+
).length;
|
|
142
|
+
|
|
143
|
+
if (!getGridCapabilities(table, features).canFilterAny) return null;
|
|
144
|
+
if (filters.surface === "none") return null;
|
|
145
|
+
|
|
146
|
+
return (
|
|
147
|
+
<Tooltip label={labels.filters} openDelay={400}>
|
|
148
|
+
<ActionIcon
|
|
149
|
+
variant={opened || activeCount > 0 ? "light" : "subtle"}
|
|
150
|
+
color={activeCount > 0 ? undefined : "gray"}
|
|
151
|
+
size={controlSize}
|
|
152
|
+
aria-label={labels.filters}
|
|
153
|
+
// Also what the panel's click-away handler tests for, so that a click
|
|
154
|
+
// here reads as a toggle rather than as a click outside.
|
|
155
|
+
data-dg-part="filter-button"
|
|
156
|
+
onClick={() => {
|
|
157
|
+
if (opened) {
|
|
158
|
+
ui.actions.closeFilterPanel();
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
// Not `openColumnFilter`: that one honours header filters and
|
|
162
|
+
// focuses the header control instead of opening anything. This
|
|
163
|
+
// button's whole job is to open the surface it belongs to, which a
|
|
164
|
+
// grid may well have alongside header filters.
|
|
165
|
+
//
|
|
166
|
+
// Seeded only when the panel would otherwise be empty: with filters
|
|
167
|
+
// already in state it has rows to show, and seeding would stack a
|
|
168
|
+
// blank row on the first filterable column on top of them.
|
|
169
|
+
const seedColumn =
|
|
170
|
+
columnFilters.length === 0
|
|
171
|
+
? table.getAllLeafColumns().find((column) => column.getCanFilter())
|
|
172
|
+
: undefined;
|
|
173
|
+
if (seedColumn) seedColumnFilter(api, seedColumn.id);
|
|
174
|
+
ui.actions.openFilterPanel(seedColumn?.id ?? null);
|
|
175
|
+
}}
|
|
176
|
+
>
|
|
177
|
+
<FilterIcon size={16} stroke={1.6} />
|
|
178
|
+
</ActionIcon>
|
|
179
|
+
</Tooltip>
|
|
180
|
+
);
|
|
181
|
+
}
|
|
@@ -2,7 +2,7 @@ import { Checkbox } from "@mantine/core";
|
|
|
2
2
|
import type { TMDataGridEditorArgs } from "../../core/editEngine";
|
|
3
3
|
import { getColumnLabel } from "../../core/columnUtils";
|
|
4
4
|
import { useTMDataGridContext } from "../../TMDataGridContext";
|
|
5
|
-
import {
|
|
5
|
+
import { useFieldInvalid, useFieldValue } from "./editorShared";
|
|
6
6
|
|
|
7
7
|
/** The built-in editor for `meta.type: "boolean"` - a focused checkbox. */
|
|
8
8
|
export function TMDataGridBooleanEditor({
|
|
@@ -12,7 +12,7 @@ export function TMDataGridBooleanEditor({
|
|
|
12
12
|
}: TMDataGridEditorArgs) {
|
|
13
13
|
const { labels } = useTMDataGridContext();
|
|
14
14
|
const value = useFieldValue(field);
|
|
15
|
-
const
|
|
15
|
+
const invalid = useFieldInvalid(field);
|
|
16
16
|
return (
|
|
17
17
|
<Checkbox
|
|
18
18
|
size={size}
|
|
@@ -21,7 +21,7 @@ export function TMDataGridBooleanEditor({
|
|
|
21
21
|
checked={value === true}
|
|
22
22
|
onChange={(event) => field.handleChange(event.currentTarget.checked)}
|
|
23
23
|
onBlur={field.handleBlur}
|
|
24
|
-
error={
|
|
24
|
+
error={invalid}
|
|
25
25
|
/>
|
|
26
26
|
);
|
|
27
27
|
}
|
|
@@ -3,7 +3,7 @@ import { useRef } from "react";
|
|
|
3
3
|
import type { TMDataGridEditorArgs } from "../../core/editEngine";
|
|
4
4
|
import { getColumnLabel } from "../../core/columnUtils";
|
|
5
5
|
import { useTMDataGridContext } from "../../TMDataGridContext";
|
|
6
|
-
import {
|
|
6
|
+
import { useFieldInvalid, useFieldValue } from "./editorShared";
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
9
|
* The built-in editor for `meta.type: "date"` - the native
|
|
@@ -17,7 +17,7 @@ export function TMDataGridDateEditor({
|
|
|
17
17
|
}: TMDataGridEditorArgs) {
|
|
18
18
|
const { labels } = useTMDataGridContext();
|
|
19
19
|
const value = useFieldValue(field);
|
|
20
|
-
const
|
|
20
|
+
const invalid = useFieldInvalid(field);
|
|
21
21
|
// The cell's original shape decides what the editor writes back: a `Date`
|
|
22
22
|
// cell keeps getting Dates, an ISO-string cell keeps getting strings -
|
|
23
23
|
// decided once at open, so clearing and retyping cannot flip the type.
|
|
@@ -49,7 +49,7 @@ export function TMDataGridDateEditor({
|
|
|
49
49
|
);
|
|
50
50
|
}}
|
|
51
51
|
onBlur={field.handleBlur}
|
|
52
|
-
error={
|
|
52
|
+
error={invalid}
|
|
53
53
|
/>
|
|
54
54
|
);
|
|
55
55
|
}
|
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
} from "../../core/columnOptions";
|
|
7
7
|
import { getColumnLabel } from "../../core/columnUtils";
|
|
8
8
|
import { useTMDataGridContext } from "../../TMDataGridContext";
|
|
9
|
-
import {
|
|
9
|
+
import { useFieldInvalid, useFieldValue } from "./editorShared";
|
|
10
10
|
|
|
11
11
|
/**
|
|
12
12
|
* The built-in editor for `meta.type: "multiSelect"` - cells holding string
|
|
@@ -22,7 +22,7 @@ export function TMDataGridMultiSelectEditor({
|
|
|
22
22
|
}: TMDataGridEditorArgs) {
|
|
23
23
|
const { labels } = useTMDataGridContext();
|
|
24
24
|
const value = useFieldValue(field);
|
|
25
|
-
const
|
|
25
|
+
const invalid = useFieldInvalid(field);
|
|
26
26
|
return (
|
|
27
27
|
<MultiSelect
|
|
28
28
|
size={size}
|
|
@@ -37,7 +37,7 @@ export function TMDataGridMultiSelectEditor({
|
|
|
37
37
|
value={Array.isArray(value) ? value.map(String) : []}
|
|
38
38
|
onChange={(next) => field.handleChange(next)}
|
|
39
39
|
onBlur={field.handleBlur}
|
|
40
|
-
error={
|
|
40
|
+
error={invalid}
|
|
41
41
|
/>
|
|
42
42
|
);
|
|
43
43
|
}
|
|
@@ -6,7 +6,7 @@ import { useTMDataGridContext } from "../../TMDataGridContext";
|
|
|
6
6
|
import {
|
|
7
7
|
selectAllOnFocus,
|
|
8
8
|
useCaretKeeper,
|
|
9
|
-
|
|
9
|
+
useFieldInvalid,
|
|
10
10
|
useFieldValue,
|
|
11
11
|
} from "./editorShared";
|
|
12
12
|
|
|
@@ -14,7 +14,7 @@ import {
|
|
|
14
14
|
export function TMDataGridNumberEditor({ field, column, size }: TMDataGridEditorArgs) {
|
|
15
15
|
const { labels } = useTMDataGridContext();
|
|
16
16
|
const value = useFieldValue(field);
|
|
17
|
-
const
|
|
17
|
+
const invalid = useFieldInvalid(field);
|
|
18
18
|
const { inputRef, remember } = useCaretKeeper();
|
|
19
19
|
/**
|
|
20
20
|
* The last text Mantine reported instead of a number - "-" or "1e" on the
|
|
@@ -64,7 +64,7 @@ export function TMDataGridNumberEditor({ field, column, size }: TMDataGridEditor
|
|
|
64
64
|
);
|
|
65
65
|
}}
|
|
66
66
|
onBlur={field.handleBlur}
|
|
67
|
-
error={
|
|
67
|
+
error={invalid}
|
|
68
68
|
/>
|
|
69
69
|
);
|
|
70
70
|
}
|
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
} from "../../core/columnOptions";
|
|
7
7
|
import { getColumnLabel } from "../../core/columnUtils";
|
|
8
8
|
import { useTMDataGridContext } from "../../TMDataGridContext";
|
|
9
|
-
import {
|
|
9
|
+
import { useFieldInvalid, useFieldValue } from "./editorShared";
|
|
10
10
|
|
|
11
11
|
/**
|
|
12
12
|
* The built-in editor for `meta.type: "select"`. Options come from the same
|
|
@@ -23,7 +23,7 @@ export function TMDataGridSelectEditor({
|
|
|
23
23
|
}: TMDataGridEditorArgs) {
|
|
24
24
|
const { labels, features } = useTMDataGridContext();
|
|
25
25
|
const value = useFieldValue(field);
|
|
26
|
-
const
|
|
26
|
+
const invalid = useFieldInvalid(field);
|
|
27
27
|
return (
|
|
28
28
|
<Select
|
|
29
29
|
size={size}
|
|
@@ -45,7 +45,7 @@ export function TMDataGridSelectEditor({
|
|
|
45
45
|
if (next !== null && features.editMode === "cell") void commit();
|
|
46
46
|
}}
|
|
47
47
|
onBlur={field.handleBlur}
|
|
48
|
-
error={
|
|
48
|
+
error={invalid}
|
|
49
49
|
/>
|
|
50
50
|
);
|
|
51
51
|
}
|
|
@@ -5,7 +5,7 @@ import { useTMDataGridContext } from "../../TMDataGridContext";
|
|
|
5
5
|
import {
|
|
6
6
|
selectAllOnFocus,
|
|
7
7
|
useCaretKeeper,
|
|
8
|
-
|
|
8
|
+
useFieldInvalid,
|
|
9
9
|
useFieldValue,
|
|
10
10
|
} from "./editorShared";
|
|
11
11
|
|
|
@@ -13,7 +13,7 @@ import {
|
|
|
13
13
|
export function TMDataGridStringEditor({ field, column, size }: TMDataGridEditorArgs) {
|
|
14
14
|
const { labels } = useTMDataGridContext();
|
|
15
15
|
const value = useFieldValue(field);
|
|
16
|
-
const
|
|
16
|
+
const invalid = useFieldInvalid(field);
|
|
17
17
|
const { inputRef, remember } = useCaretKeeper();
|
|
18
18
|
return (
|
|
19
19
|
<TextInput
|
|
@@ -29,7 +29,7 @@ export function TMDataGridStringEditor({ field, column, size }: TMDataGridEditor
|
|
|
29
29
|
field.handleChange(event.currentTarget.value);
|
|
30
30
|
}}
|
|
31
31
|
onBlur={field.handleBlur}
|
|
32
|
-
error={
|
|
32
|
+
error={invalid}
|
|
33
33
|
/>
|
|
34
34
|
);
|
|
35
35
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { useStore } from "@tanstack/react-form";
|
|
2
2
|
import { useLayoutEffect, useRef, type RefObject } from "react";
|
|
3
|
+
import { isInputElement } from "../../core/dom";
|
|
3
4
|
import { firstErrorText, type TMDataGridEditField } from "../../core/editEngine";
|
|
4
5
|
|
|
5
6
|
/** The field's live value - one subscription, on the field's own store. */
|
|
@@ -7,17 +8,30 @@ export function useFieldValue(field: TMDataGridEditField): unknown {
|
|
|
7
8
|
return useStore(field.store, (state) => state.value as unknown);
|
|
8
9
|
}
|
|
9
10
|
|
|
10
|
-
/** The field's first error
|
|
11
|
+
/** The field's first error - the message the editor's host puts in a tooltip. */
|
|
11
12
|
export function useFieldError(field: TMDataGridEditField): string | undefined {
|
|
12
13
|
return useStore(field.store, (state) =>
|
|
13
14
|
firstErrorText(state.meta.errors as ReadonlyArray<unknown>),
|
|
14
15
|
);
|
|
15
16
|
}
|
|
16
17
|
|
|
18
|
+
/**
|
|
19
|
+
* Whether the field has an error, for the Mantine input's `error` prop - the
|
|
20
|
+
* invalid border and nothing else. The message itself would wrap inside a
|
|
21
|
+
* narrow cell and be clipped, so the host shows it in a tooltip instead.
|
|
22
|
+
*/
|
|
23
|
+
export function useFieldInvalid(field: TMDataGridEditField): boolean {
|
|
24
|
+
return useStore(
|
|
25
|
+
field.store,
|
|
26
|
+
(state) =>
|
|
27
|
+
firstErrorText(state.meta.errors as ReadonlyArray<unknown>) !== undefined,
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
|
|
17
31
|
/** Select-all on focus - F2/Enter open with the value ready to replace. */
|
|
18
32
|
export function selectAllOnFocus(event: { currentTarget: unknown }): void {
|
|
19
33
|
const target = event.currentTarget;
|
|
20
|
-
if (target
|
|
34
|
+
if (isInputElement(target)) target.select();
|
|
21
35
|
}
|
|
22
36
|
|
|
23
37
|
/**
|
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
operatorTakesArrayValue,
|
|
7
7
|
operatorTakesRangeValue,
|
|
8
8
|
} from "../../core/filterOperators";
|
|
9
|
+
import { filterFieldProps } from "./controlLayout";
|
|
9
10
|
import { TMDataGridFilterValueInput } from "./TMDataGridFilterValueInput";
|
|
10
11
|
|
|
11
12
|
/**
|
|
@@ -31,12 +32,12 @@ export function DgAutocompleteFilter(args: TMDataGridFilterControlArgs) {
|
|
|
31
32
|
|
|
32
33
|
return (
|
|
33
34
|
<Autocomplete
|
|
34
|
-
label
|
|
35
|
+
{...filterFieldProps(args, { label: labels.filterValue, width: 180 })}
|
|
35
36
|
size={size}
|
|
36
|
-
w={180}
|
|
37
|
-
comboboxProps={{ withinPortal: false }}
|
|
38
37
|
disabled={!needsValue}
|
|
39
|
-
placeholder={
|
|
38
|
+
placeholder={
|
|
39
|
+
needsValue && args.layout !== "header" ? labels.filterValuePlaceholder : ""
|
|
40
|
+
}
|
|
40
41
|
data={suggestions.map((option) => option.value)}
|
|
41
42
|
limit={20}
|
|
42
43
|
value={needsValue && typeof value === "string" ? value : ""}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Group, TextInput } from "@mantine/core";
|
|
2
2
|
import type { TMDataGridFilterControlArgs } from "../../core/filterControls";
|
|
3
3
|
import { operatorTakesRangeValue } from "../../core/filterOperators";
|
|
4
|
+
import { filterFieldProps } from "./controlLayout";
|
|
4
5
|
import { TMDataGridFilterValueInput } from "./TMDataGridFilterValueInput";
|
|
5
6
|
|
|
6
7
|
/**
|
|
@@ -16,25 +17,41 @@ export function DgDateRangeFilter(args: TMDataGridFilterControlArgs) {
|
|
|
16
17
|
return <TMDataGridFilterValueInput {...args} />;
|
|
17
18
|
}
|
|
18
19
|
|
|
20
|
+
const fills = args.layout !== "row";
|
|
19
21
|
const pair: [string, string] = Array.isArray(value)
|
|
20
22
|
? [String(value[0] ?? ""), String(value[1] ?? "")]
|
|
21
23
|
: ["", ""];
|
|
22
24
|
|
|
23
25
|
return (
|
|
24
|
-
<Group
|
|
26
|
+
<Group
|
|
27
|
+
gap={4}
|
|
28
|
+
wrap="nowrap"
|
|
29
|
+
align="flex-start"
|
|
30
|
+
w={fills ? "100%" : undefined}
|
|
31
|
+
>
|
|
25
32
|
<TextInput
|
|
26
|
-
|
|
33
|
+
{...filterFieldProps(
|
|
34
|
+
args,
|
|
35
|
+
{ label: labels.filterFrom, width: 88 },
|
|
36
|
+
labels.filterFrom,
|
|
37
|
+
)}
|
|
27
38
|
size={size}
|
|
28
|
-
w={88}
|
|
29
39
|
type="date"
|
|
40
|
+
flex={fills ? 1 : undefined}
|
|
41
|
+
miw={0}
|
|
30
42
|
value={pair[0]}
|
|
31
43
|
onChange={(event) => onChange([event.currentTarget.value, pair[1]])}
|
|
32
44
|
/>
|
|
33
45
|
<TextInput
|
|
34
|
-
|
|
46
|
+
{...filterFieldProps(
|
|
47
|
+
args,
|
|
48
|
+
{ label: labels.filterTo, width: 88 },
|
|
49
|
+
labels.filterTo,
|
|
50
|
+
)}
|
|
35
51
|
size={size}
|
|
36
|
-
w={88}
|
|
37
52
|
type="date"
|
|
53
|
+
flex={fills ? 1 : undefined}
|
|
54
|
+
miw={0}
|
|
38
55
|
value={pair[1]}
|
|
39
56
|
onChange={(event) => onChange([pair[0], event.currentTarget.value])}
|
|
40
57
|
/>
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Input, RangeSlider } from "@mantine/core";
|
|
2
2
|
import type { TMDataGridFilterControlArgs } from "../../core/filterControls";
|
|
3
3
|
import { operatorTakesRangeValue } from "../../core/filterOperators";
|
|
4
|
+
import { filterFieldProps } from "./controlLayout";
|
|
4
5
|
import { TMDataGridFilterValueInput } from "./TMDataGridFilterValueInput";
|
|
5
6
|
|
|
6
7
|
/**
|
|
@@ -30,7 +31,10 @@ export function DgRangeSliderFilter(args: TMDataGridFilterControlArgs) {
|
|
|
30
31
|
const upper = pair[1] === undefined || pair[1] === "" ? max : Number(pair[1]);
|
|
31
32
|
|
|
32
33
|
return (
|
|
33
|
-
<Input.Wrapper
|
|
34
|
+
<Input.Wrapper
|
|
35
|
+
{...filterFieldProps(args, { label: labels.filterValue, width: 180 })}
|
|
36
|
+
size={size}
|
|
37
|
+
>
|
|
34
38
|
<RangeSlider
|
|
35
39
|
size={size}
|
|
36
40
|
mt={6}
|
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
operatorTakesArrayValue,
|
|
6
6
|
operatorTakesRangeValue,
|
|
7
7
|
} from "../../core/filterOperators";
|
|
8
|
+
import { filterFieldProps } from "./controlLayout";
|
|
8
9
|
import { TMDataGridFilterValueInput } from "./TMDataGridFilterValueInput";
|
|
9
10
|
|
|
10
11
|
/**
|
|
@@ -21,7 +22,10 @@ export function DgTriStateFilter(args: TMDataGridFilterControlArgs) {
|
|
|
21
22
|
const scalar = typeof value === "string" ? value : "";
|
|
22
23
|
|
|
23
24
|
return (
|
|
24
|
-
<Input.Wrapper
|
|
25
|
+
<Input.Wrapper
|
|
26
|
+
{...filterFieldProps(args, { label: labels.filterValue })}
|
|
27
|
+
size={size}
|
|
28
|
+
>
|
|
25
29
|
<SegmentedControl
|
|
26
30
|
size={size}
|
|
27
31
|
disabled={!needsValue}
|
|
@@ -7,30 +7,39 @@ import {
|
|
|
7
7
|
operatorTakesArrayValue,
|
|
8
8
|
operatorTakesRangeValue,
|
|
9
9
|
} from "../../core/filterOperators";
|
|
10
|
+
import { filterFieldProps } from "./controlLayout";
|
|
10
11
|
|
|
11
12
|
/**
|
|
12
|
-
* The built-in value control of a filter
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
13
|
+
* The built-in value control of a filter row - what renders when a column
|
|
14
|
+
* declares no `meta.filter.control`. Shaped by the operator: a multi-select
|
|
15
|
+
* for the set operators, a From/To pair for `between`, a Yes/No dropdown for
|
|
16
|
+
* booleans, a typed input otherwise.
|
|
17
|
+
*
|
|
18
|
+
* Shaped by `layout` as well: in a header cell the fields drop their labels,
|
|
19
|
+
* fill the column's width and name themselves through `aria-label`.
|
|
16
20
|
*
|
|
17
21
|
* Exported so a custom control can fall back to it for the operators it does
|
|
18
22
|
* not care about, instead of rebuilding them.
|
|
19
23
|
*/
|
|
20
|
-
export function TMDataGridFilterValueInput({
|
|
21
|
-
column,
|
|
22
|
-
|
|
23
|
-
value,
|
|
24
|
-
onChange,
|
|
25
|
-
options,
|
|
26
|
-
size,
|
|
27
|
-
labels,
|
|
28
|
-
}: TMDataGridFilterControlArgs) {
|
|
24
|
+
export function TMDataGridFilterValueInput(args: TMDataGridFilterControlArgs) {
|
|
25
|
+
const { column, operator, value, onChange, options, size, labels, layout } =
|
|
26
|
+
args;
|
|
29
27
|
const type = getColumnType(column);
|
|
30
28
|
const needsValue = operatorNeedsValue(operator);
|
|
31
29
|
const scalarValue = typeof value === "string" ? value : "";
|
|
32
30
|
const inputType =
|
|
33
31
|
type === "number" ? "number" : type === "date" ? "date" : "text";
|
|
32
|
+
const inHeader = layout === "header";
|
|
33
|
+
const fills = layout !== "row";
|
|
34
|
+
// Portalled everywhere: drawn inline, the dropdown is clipped - by the
|
|
35
|
+
// header cell, by the grid frame's `overflow: hidden` (its corner radius)
|
|
36
|
+
// under the popup, or by the sidebar's scroller. The popup's click-away
|
|
37
|
+
// exempts portal nodes, so picking an option does not read as a click away.
|
|
38
|
+
const comboboxProps = { withinPortal: true };
|
|
39
|
+
// No placeholder in a header cell: the column name is directly above the
|
|
40
|
+
// field, and "Filter value" only ever fits a wide column anyway.
|
|
41
|
+
const placeholder =
|
|
42
|
+
needsValue && !inHeader ? labels.filterValuePlaceholder : "";
|
|
34
43
|
|
|
35
44
|
if (operatorTakesRangeValue(operator)) {
|
|
36
45
|
const rangeValue: [string, string] = Array.isArray(value)
|
|
@@ -39,12 +48,17 @@ export function TMDataGridFilterValueInput({
|
|
|
39
48
|
return (
|
|
40
49
|
// The interval's two ends. Either may stay empty (an open end), and
|
|
41
50
|
// each writes its slot of the `[min, max]` pair.
|
|
42
|
-
<Group gap={4} wrap="nowrap" align="flex-start">
|
|
51
|
+
<Group gap={4} wrap="nowrap" align="flex-start" w={fills ? "100%" : undefined}>
|
|
43
52
|
<TextInput
|
|
44
|
-
|
|
53
|
+
{...filterFieldProps(
|
|
54
|
+
args,
|
|
55
|
+
{ label: labels.filterFrom, width: 88 },
|
|
56
|
+
labels.filterFrom,
|
|
57
|
+
)}
|
|
45
58
|
size={size}
|
|
46
|
-
w={88}
|
|
47
59
|
type={inputType}
|
|
60
|
+
flex={fills ? 1 : undefined}
|
|
61
|
+
miw={0}
|
|
48
62
|
data-dg-part="filter-value-from"
|
|
49
63
|
value={rangeValue[0]}
|
|
50
64
|
onChange={(event) =>
|
|
@@ -52,10 +66,15 @@ export function TMDataGridFilterValueInput({
|
|
|
52
66
|
}
|
|
53
67
|
/>
|
|
54
68
|
<TextInput
|
|
55
|
-
|
|
69
|
+
{...filterFieldProps(
|
|
70
|
+
args,
|
|
71
|
+
{ label: labels.filterTo, width: 88 },
|
|
72
|
+
labels.filterTo,
|
|
73
|
+
)}
|
|
56
74
|
size={size}
|
|
57
|
-
w={88}
|
|
58
75
|
type={inputType}
|
|
76
|
+
flex={fills ? 1 : undefined}
|
|
77
|
+
miw={0}
|
|
59
78
|
data-dg-part="filter-value-to"
|
|
60
79
|
value={rangeValue[1]}
|
|
61
80
|
onChange={(event) =>
|
|
@@ -72,10 +91,9 @@ export function TMDataGridFilterValueInput({
|
|
|
72
91
|
// the data, via the faceted index.
|
|
73
92
|
return (
|
|
74
93
|
<MultiSelect
|
|
75
|
-
label
|
|
94
|
+
{...filterFieldProps(args, { label: labels.filterValue, width: 180 })}
|
|
76
95
|
size={size}
|
|
77
|
-
|
|
78
|
-
comboboxProps={{ withinPortal: false }}
|
|
96
|
+
comboboxProps={comboboxProps}
|
|
79
97
|
searchable
|
|
80
98
|
data-dg-part="filter-value"
|
|
81
99
|
data={optionsToComboboxData(options)}
|
|
@@ -88,14 +106,13 @@ export function TMDataGridFilterValueInput({
|
|
|
88
106
|
if (type === "boolean") {
|
|
89
107
|
return (
|
|
90
108
|
<Select
|
|
91
|
-
label
|
|
109
|
+
{...filterFieldProps(args, { label: labels.filterValue, width: 180 })}
|
|
92
110
|
size={size}
|
|
93
|
-
|
|
94
|
-
comboboxProps={{ withinPortal: false }}
|
|
111
|
+
comboboxProps={comboboxProps}
|
|
95
112
|
data-dg-part="filter-value"
|
|
96
113
|
disabled={!needsValue}
|
|
97
114
|
clearable
|
|
98
|
-
placeholder={
|
|
115
|
+
placeholder={placeholder}
|
|
99
116
|
data={[
|
|
100
117
|
{ value: "true", label: labels.booleanTrue },
|
|
101
118
|
{ value: "false", label: labels.booleanFalse },
|
|
@@ -108,13 +125,12 @@ export function TMDataGridFilterValueInput({
|
|
|
108
125
|
|
|
109
126
|
return (
|
|
110
127
|
<TextInput
|
|
111
|
-
label
|
|
128
|
+
{...filterFieldProps(args, { label: labels.filterValue, width: 180 })}
|
|
112
129
|
size={size}
|
|
113
|
-
w={180}
|
|
114
130
|
type={needsValue ? inputType : "text"}
|
|
115
131
|
data-dg-part="filter-value"
|
|
116
132
|
disabled={!needsValue}
|
|
117
|
-
placeholder={
|
|
133
|
+
placeholder={placeholder}
|
|
118
134
|
value={needsValue ? scalarValue : ""}
|
|
119
135
|
onChange={(event) => onChange(event.currentTarget.value)}
|
|
120
136
|
/>
|