@jielga/tmdatagrid 0.5.0 → 1.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 +25 -2
- package/dist/index.d.ts +856 -14
- package/dist/index.js +3370 -1546
- package/dist/index.js.map +1 -1
- package/dist/styles.css +1 -1
- package/dist/styles.layer.css +1 -0
- package/package.json +12 -2
- package/skills/columns/SKILL.md +1 -1
- package/skills/features/SKILL.md +1 -1
- package/skills/getting-started/SKILL.md +1 -1
- package/skills/options/SKILL.md +1 -1
- package/skills/server-side/SKILL.md +1 -1
- package/skills/testing/SKILL.md +198 -0
- package/src/tmdatagrid/components/TMDataGrid.module.css +11 -0
- package/src/tmdatagrid/components/TMDataGrid.tsx +34 -0
- package/src/tmdatagrid/components/TMDataGridCellEditor.tsx +256 -0
- package/src/tmdatagrid/components/TMDataGridColumnsPanel.tsx +35 -12
- package/src/tmdatagrid/components/TMDataGridDetailsColumn.tsx +3 -0
- package/src/tmdatagrid/components/TMDataGridEditActions.tsx +62 -0
- package/src/tmdatagrid/components/TMDataGridEditColumn.tsx +237 -0
- package/src/tmdatagrid/components/TMDataGridEntryRows.tsx +164 -0
- package/src/tmdatagrid/components/TMDataGridFilterPanel.tsx +77 -62
- package/src/tmdatagrid/components/TMDataGridFilterPills.tsx +6 -0
- package/src/tmdatagrid/components/TMDataGridFooter.tsx +10 -3
- package/src/tmdatagrid/components/TMDataGridGroupColumn.tsx +2 -0
- package/src/tmdatagrid/components/TMDataGridHeaderCell.module.css +15 -2
- package/src/tmdatagrid/components/TMDataGridHeaderCell.tsx +21 -4
- package/src/tmdatagrid/components/TMDataGridRowNumberColumn.tsx +44 -0
- package/src/tmdatagrid/components/TMDataGridSearch.tsx +2 -0
- package/src/tmdatagrid/components/TMDataGridSelectColumn.tsx +3 -0
- package/src/tmdatagrid/components/TMDataGridTable.module.css +206 -0
- package/src/tmdatagrid/components/TMDataGridTable.tsx +841 -57
- package/src/tmdatagrid/components/TMDataGridToolbar.tsx +12 -7
- package/src/tmdatagrid/components/editors/TMDataGridBooleanEditor.tsx +29 -0
- package/src/tmdatagrid/components/editors/TMDataGridDateEditor.tsx +57 -0
- package/src/tmdatagrid/components/editors/TMDataGridMultiSelectEditor.tsx +45 -0
- package/src/tmdatagrid/components/editors/TMDataGridNumberEditor.tsx +38 -0
- package/src/tmdatagrid/components/editors/TMDataGridSelectEditor.tsx +53 -0
- package/src/tmdatagrid/components/editors/TMDataGridStringEditor.tsx +31 -0
- package/src/tmdatagrid/components/editors/editorShared.ts +48 -0
- package/src/tmdatagrid/components/filters/DgAutocompleteFilter.tsx +46 -0
- package/src/tmdatagrid/components/filters/DgDateRangeFilter.tsx +43 -0
- package/src/tmdatagrid/components/filters/DgRangeSliderFilter.tsx +52 -0
- package/src/tmdatagrid/components/filters/DgTriStateFilter.tsx +38 -0
- package/src/tmdatagrid/components/filters/TMDataGridFilterValueInput.tsx +122 -0
- package/src/tmdatagrid/components/icons.ts +4 -0
- package/src/tmdatagrid/core/autosize.ts +1 -1
- package/src/tmdatagrid/core/capabilities.ts +37 -1
- package/src/tmdatagrid/core/cellNavigation.ts +41 -0
- package/src/tmdatagrid/core/columnUtils.ts +31 -6
- package/src/tmdatagrid/core/editEngine.ts +857 -0
- package/src/tmdatagrid/core/filterControls.ts +45 -0
- package/src/tmdatagrid/core/filterOperators.ts +72 -4
- package/src/tmdatagrid/core/labels.ts +68 -1
- package/src/tmdatagrid/core/labelsSv.ts +24 -1
- package/src/tmdatagrid/core/matchHighlight.ts +96 -0
- package/src/tmdatagrid/core/persistence.ts +97 -3
- package/src/tmdatagrid/core/quickSearch.ts +144 -0
- package/src/tmdatagrid/core/rowPinning.ts +37 -0
- package/src/tmdatagrid/core/rowSelection.ts +10 -1
- package/src/tmdatagrid/index.ts +51 -0
- package/src/tmdatagrid/useTMDataGrid.tsx +456 -15
|
@@ -11,7 +11,11 @@ import { openColumnFilter } from "../useTMDataGrid";
|
|
|
11
11
|
|
|
12
12
|
/** Row above the grid. Compose it from the pieces below, or anything else. */
|
|
13
13
|
export function TMDataGridToolbar({ children }: { children?: ReactNode }) {
|
|
14
|
-
return
|
|
14
|
+
return (
|
|
15
|
+
<div data-dg-part="toolbar" className={classes.toolbar}>
|
|
16
|
+
{children}
|
|
17
|
+
</div>
|
|
18
|
+
);
|
|
15
19
|
}
|
|
16
20
|
|
|
17
21
|
/** Pushes the following toolbar items to the right. */
|
|
@@ -39,7 +43,7 @@ export function TMDataGridToolbarSpacer() {
|
|
|
39
43
|
export function TMDataGridLoadingIndicator() {
|
|
40
44
|
const { table, labels } = useTMDataGridContext();
|
|
41
45
|
if (table.options.meta?.loading !== true) return null;
|
|
42
|
-
return <Loader size="xs" aria-label={labels.loading} />;
|
|
46
|
+
return <Loader size="xs" data-dg-part="loading" aria-label={labels.loading} />;
|
|
43
47
|
}
|
|
44
48
|
|
|
45
49
|
/**
|
|
@@ -52,7 +56,7 @@ export function TMDataGridSummaryCount({ children }: { children?: ReactNode }) {
|
|
|
52
56
|
|
|
53
57
|
if (children !== undefined) {
|
|
54
58
|
return (
|
|
55
|
-
<Text size={controlSize} c="dimmed">
|
|
59
|
+
<Text size={controlSize} c="dimmed" data-dg-part="summary-count">
|
|
56
60
|
{children}
|
|
57
61
|
</Text>
|
|
58
62
|
);
|
|
@@ -64,7 +68,7 @@ export function TMDataGridSummaryCount({ children }: { children?: ReactNode }) {
|
|
|
64
68
|
table.getPreFilteredRowModel().rows.length;
|
|
65
69
|
|
|
66
70
|
return (
|
|
67
|
-
<Text size={controlSize} c="dimmed">
|
|
71
|
+
<Text size={controlSize} c="dimmed" data-dg-part="summary-count">
|
|
68
72
|
{shown} / {total}
|
|
69
73
|
</Text>
|
|
70
74
|
);
|
|
@@ -97,6 +101,7 @@ export function TMDataGridColumnsButton() {
|
|
|
97
101
|
color="gray"
|
|
98
102
|
size={controlSize}
|
|
99
103
|
aria-label={labels.manageColumns}
|
|
104
|
+
data-dg-part="columns-button"
|
|
100
105
|
onClick={ui.actions.toggleColumnsPanel}
|
|
101
106
|
>
|
|
102
107
|
<BurgerIcon size={18} stroke={1.6} />
|
|
@@ -135,9 +140,9 @@ export function TMDataGridFilterButton() {
|
|
|
135
140
|
color={activeCount > 0 ? undefined : "gray"}
|
|
136
141
|
size={controlSize}
|
|
137
142
|
aria-label={labels.filters}
|
|
138
|
-
//
|
|
139
|
-
//
|
|
140
|
-
data-dg-filter-
|
|
143
|
+
// Also what the panel's click-away handler tests for, so that a click
|
|
144
|
+
// here reads as a toggle rather than as a click outside.
|
|
145
|
+
data-dg-part="filter-button"
|
|
141
146
|
onClick={() => {
|
|
142
147
|
if (opened) {
|
|
143
148
|
ui.actions.closeFilterPanel();
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { Checkbox } from "@mantine/core";
|
|
2
|
+
import type { TMDataGridEditorArgs } from "../../core/editEngine";
|
|
3
|
+
import { getColumnLabel } from "../../core/columnUtils";
|
|
4
|
+
import { useTMDataGridContext } from "../../TMDataGridContext";
|
|
5
|
+
import { useFieldError, useFieldValue } from "./editorShared";
|
|
6
|
+
|
|
7
|
+
/** The built-in editor for `meta.type: "boolean"` — a focused checkbox. */
|
|
8
|
+
export function TMDataGridBooleanEditor({
|
|
9
|
+
field,
|
|
10
|
+
autoFocus,
|
|
11
|
+
column,
|
|
12
|
+
size,
|
|
13
|
+
}: TMDataGridEditorArgs) {
|
|
14
|
+
const { labels } = useTMDataGridContext();
|
|
15
|
+
const value = useFieldValue(field);
|
|
16
|
+
const error = useFieldError(field);
|
|
17
|
+
return (
|
|
18
|
+
<Checkbox
|
|
19
|
+
size={size}
|
|
20
|
+
autoFocus={autoFocus}
|
|
21
|
+
aria-label={labels.editCell(getColumnLabel(column))}
|
|
22
|
+
data-dg-part="editor-input"
|
|
23
|
+
checked={value === true}
|
|
24
|
+
onChange={(event) => field.handleChange(event.currentTarget.checked)}
|
|
25
|
+
onBlur={field.handleBlur}
|
|
26
|
+
error={error}
|
|
27
|
+
/>
|
|
28
|
+
);
|
|
29
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { TextInput } from "@mantine/core";
|
|
2
|
+
import { useRef } from "react";
|
|
3
|
+
import type { TMDataGridEditorArgs } from "../../core/editEngine";
|
|
4
|
+
import { getColumnLabel } from "../../core/columnUtils";
|
|
5
|
+
import { useTMDataGridContext } from "../../TMDataGridContext";
|
|
6
|
+
import { useFieldError, useFieldValue } from "./editorShared";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* The built-in editor for `meta.type: "date"` — the native
|
|
10
|
+
* `<input type="date">` styled by Mantine, no `@mantine/dates` involved. A
|
|
11
|
+
* consumer wanting a real picker plugs one in through `meta.editor`.
|
|
12
|
+
*/
|
|
13
|
+
export function TMDataGridDateEditor({
|
|
14
|
+
field,
|
|
15
|
+
autoFocus,
|
|
16
|
+
column,
|
|
17
|
+
size,
|
|
18
|
+
}: TMDataGridEditorArgs) {
|
|
19
|
+
const { labels } = useTMDataGridContext();
|
|
20
|
+
const value = useFieldValue(field);
|
|
21
|
+
const error = useFieldError(field);
|
|
22
|
+
// The cell's original shape decides what the editor writes back: a `Date`
|
|
23
|
+
// cell keeps getting Dates, an ISO-string cell keeps getting strings —
|
|
24
|
+
// decided once at open, so clearing and retyping cannot flip the type.
|
|
25
|
+
const writesDates = useRef(value instanceof Date);
|
|
26
|
+
|
|
27
|
+
const text =
|
|
28
|
+
value instanceof Date
|
|
29
|
+
? value.toLocaleDateString("sv-SE")
|
|
30
|
+
: typeof value === "string"
|
|
31
|
+
? value.slice(0, 10)
|
|
32
|
+
: "";
|
|
33
|
+
|
|
34
|
+
return (
|
|
35
|
+
<TextInput
|
|
36
|
+
size={size}
|
|
37
|
+
w="100%"
|
|
38
|
+
autoFocus={autoFocus}
|
|
39
|
+
type="date"
|
|
40
|
+
aria-label={labels.editCell(getColumnLabel(column))}
|
|
41
|
+
data-dg-part="editor-input"
|
|
42
|
+
value={text}
|
|
43
|
+
onChange={(event) => {
|
|
44
|
+
const next = event.currentTarget.value;
|
|
45
|
+
if (next === "") {
|
|
46
|
+
field.handleChange(null);
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
field.handleChange(
|
|
50
|
+
writesDates.current ? new Date(`${next}T00:00:00`) : next,
|
|
51
|
+
);
|
|
52
|
+
}}
|
|
53
|
+
onBlur={field.handleBlur}
|
|
54
|
+
error={error}
|
|
55
|
+
/>
|
|
56
|
+
);
|
|
57
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { MultiSelect } from "@mantine/core";
|
|
2
|
+
import type { TMDataGridEditorArgs } from "../../core/editEngine";
|
|
3
|
+
import {
|
|
4
|
+
optionsToComboboxData,
|
|
5
|
+
resolveColumnOptions,
|
|
6
|
+
} from "../../core/columnOptions";
|
|
7
|
+
import { getColumnLabel } from "../../core/columnUtils";
|
|
8
|
+
import { useTMDataGridContext } from "../../TMDataGridContext";
|
|
9
|
+
import { useFieldError, useFieldValue } from "./editorShared";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* The built-in editor for `meta.type: "multiSelect"` — cells holding string
|
|
13
|
+
* arrays. Never commits on pick: building a set takes several, so the commit
|
|
14
|
+
* stays on Enter/blur like a text edit.
|
|
15
|
+
*/
|
|
16
|
+
export function TMDataGridMultiSelectEditor({
|
|
17
|
+
field,
|
|
18
|
+
autoFocus,
|
|
19
|
+
column,
|
|
20
|
+
row,
|
|
21
|
+
table,
|
|
22
|
+
size,
|
|
23
|
+
}: TMDataGridEditorArgs) {
|
|
24
|
+
const { labels } = useTMDataGridContext();
|
|
25
|
+
const value = useFieldValue(field);
|
|
26
|
+
const error = useFieldError(field);
|
|
27
|
+
return (
|
|
28
|
+
<MultiSelect
|
|
29
|
+
size={size}
|
|
30
|
+
w="100%"
|
|
31
|
+
autoFocus={autoFocus}
|
|
32
|
+
searchable
|
|
33
|
+
comboboxProps={{ withinPortal: false }}
|
|
34
|
+
aria-label={labels.editCell(getColumnLabel(column))}
|
|
35
|
+
data-dg-part="editor-input"
|
|
36
|
+
data={optionsToComboboxData(
|
|
37
|
+
resolveColumnOptions({ table, column, row, fallback: "faceted" }),
|
|
38
|
+
)}
|
|
39
|
+
value={Array.isArray(value) ? value.map(String) : []}
|
|
40
|
+
onChange={(next) => field.handleChange(next)}
|
|
41
|
+
onBlur={field.handleBlur}
|
|
42
|
+
error={error}
|
|
43
|
+
/>
|
|
44
|
+
);
|
|
45
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { NumberInput } from "@mantine/core";
|
|
2
|
+
import type { TMDataGridEditorArgs } from "../../core/editEngine";
|
|
3
|
+
import { getColumnLabel } from "../../core/columnUtils";
|
|
4
|
+
import { useTMDataGridContext } from "../../TMDataGridContext";
|
|
5
|
+
import { selectAllOnFocus, useFieldError, useFieldValue } from "./editorShared";
|
|
6
|
+
|
|
7
|
+
/** The built-in editor for `meta.type: "number"`. */
|
|
8
|
+
export function TMDataGridNumberEditor({
|
|
9
|
+
field,
|
|
10
|
+
autoFocus,
|
|
11
|
+
column,
|
|
12
|
+
size,
|
|
13
|
+
}: TMDataGridEditorArgs) {
|
|
14
|
+
const { labels } = useTMDataGridContext();
|
|
15
|
+
const value = useFieldValue(field);
|
|
16
|
+
const error = useFieldError(field);
|
|
17
|
+
return (
|
|
18
|
+
<NumberInput
|
|
19
|
+
size={size}
|
|
20
|
+
w="100%"
|
|
21
|
+
autoFocus={autoFocus}
|
|
22
|
+
onFocus={selectAllOnFocus}
|
|
23
|
+
hideControls
|
|
24
|
+
aria-label={labels.editCell(getColumnLabel(column))}
|
|
25
|
+
data-dg-part="editor-input"
|
|
26
|
+
value={typeof value === "number" ? value : ""}
|
|
27
|
+
onChange={(next) =>
|
|
28
|
+
// Mantine reports a number once one can be parsed, and a string while
|
|
29
|
+
// it cannot. An empty cell is `null` — the honest "no value".
|
|
30
|
+
field.handleChange(
|
|
31
|
+
typeof next === "number" ? next : next === "" ? null : Number(next),
|
|
32
|
+
)
|
|
33
|
+
}
|
|
34
|
+
onBlur={field.handleBlur}
|
|
35
|
+
error={error}
|
|
36
|
+
/>
|
|
37
|
+
);
|
|
38
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { Select } from "@mantine/core";
|
|
2
|
+
import type { TMDataGridEditorArgs } from "../../core/editEngine";
|
|
3
|
+
import {
|
|
4
|
+
optionsToComboboxData,
|
|
5
|
+
resolveColumnOptions,
|
|
6
|
+
} from "../../core/columnOptions";
|
|
7
|
+
import { getColumnLabel } from "../../core/columnUtils";
|
|
8
|
+
import { useTMDataGridContext } from "../../TMDataGridContext";
|
|
9
|
+
import { useFieldError, useFieldValue } from "./editorShared";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* The built-in editor for `meta.type: "select"`. Options come from the same
|
|
13
|
+
* `meta.options` the filter panel reads — with the row in hand, so a
|
|
14
|
+
* function source can depend on the record being edited.
|
|
15
|
+
*/
|
|
16
|
+
export function TMDataGridSelectEditor({
|
|
17
|
+
field,
|
|
18
|
+
autoFocus,
|
|
19
|
+
column,
|
|
20
|
+
row,
|
|
21
|
+
table,
|
|
22
|
+
size,
|
|
23
|
+
commit,
|
|
24
|
+
}: TMDataGridEditorArgs) {
|
|
25
|
+
const { labels, features } = useTMDataGridContext();
|
|
26
|
+
const value = useFieldValue(field);
|
|
27
|
+
const error = useFieldError(field);
|
|
28
|
+
return (
|
|
29
|
+
<Select
|
|
30
|
+
size={size}
|
|
31
|
+
w="100%"
|
|
32
|
+
autoFocus={autoFocus}
|
|
33
|
+
searchable
|
|
34
|
+
// Inside the cell, so picking an option is not a click-away — the same
|
|
35
|
+
// reason the filter panel keeps its dropdowns unportalled.
|
|
36
|
+
comboboxProps={{ withinPortal: false }}
|
|
37
|
+
aria-label={labels.editCell(getColumnLabel(column))}
|
|
38
|
+
data-dg-part="editor-input"
|
|
39
|
+
data={optionsToComboboxData(
|
|
40
|
+
resolveColumnOptions({ table, column, row, fallback: "faceted" }),
|
|
41
|
+
)}
|
|
42
|
+
value={typeof value === "string" ? value : null}
|
|
43
|
+
onChange={(next) => {
|
|
44
|
+
field.handleChange(next);
|
|
45
|
+
// Sheets semantics under `"cell"`: a pick is the edit, so it commits.
|
|
46
|
+
// The confirming modes leave the draft for their explicit commit.
|
|
47
|
+
if (next !== null && features.editMode === "cell") void commit();
|
|
48
|
+
}}
|
|
49
|
+
onBlur={field.handleBlur}
|
|
50
|
+
error={error}
|
|
51
|
+
/>
|
|
52
|
+
);
|
|
53
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { TextInput } from "@mantine/core";
|
|
2
|
+
import type { TMDataGridEditorArgs } from "../../core/editEngine";
|
|
3
|
+
import { getColumnLabel } from "../../core/columnUtils";
|
|
4
|
+
import { useTMDataGridContext } from "../../TMDataGridContext";
|
|
5
|
+
import { selectAllOnFocus, useFieldError, useFieldValue } from "./editorShared";
|
|
6
|
+
|
|
7
|
+
/** The built-in editor for `meta.type: "string"` (and the default). */
|
|
8
|
+
export function TMDataGridStringEditor({
|
|
9
|
+
field,
|
|
10
|
+
autoFocus,
|
|
11
|
+
column,
|
|
12
|
+
size,
|
|
13
|
+
}: TMDataGridEditorArgs) {
|
|
14
|
+
const { labels } = useTMDataGridContext();
|
|
15
|
+
const value = useFieldValue(field);
|
|
16
|
+
const error = useFieldError(field);
|
|
17
|
+
return (
|
|
18
|
+
<TextInput
|
|
19
|
+
size={size}
|
|
20
|
+
w="100%"
|
|
21
|
+
autoFocus={autoFocus}
|
|
22
|
+
onFocus={selectAllOnFocus}
|
|
23
|
+
aria-label={labels.editCell(getColumnLabel(column))}
|
|
24
|
+
data-dg-part="editor-input"
|
|
25
|
+
value={typeof value === "string" ? value : ""}
|
|
26
|
+
onChange={(event) => field.handleChange(event.currentTarget.value)}
|
|
27
|
+
onBlur={field.handleBlur}
|
|
28
|
+
error={error}
|
|
29
|
+
/>
|
|
30
|
+
);
|
|
31
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { useStore } from "@tanstack/react-form";
|
|
2
|
+
import type { TMDataGridEditField } from "../../core/editEngine";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The first error as text, whatever shape the validator produced — TanStack
|
|
6
|
+
* Form keeps errors verbatim, so a Zod issue arrives as `{ message }`, a
|
|
7
|
+
* plain function's return arrives as it was, and a form-level schema's
|
|
8
|
+
* pathless issues arrive keyed under an empty path (`{ "": [issues] }`).
|
|
9
|
+
* This digs until it finds a message.
|
|
10
|
+
*/
|
|
11
|
+
export function firstErrorText(error: unknown): string | undefined {
|
|
12
|
+
if (error === null || error === undefined) return undefined;
|
|
13
|
+
if (typeof error === "string") return error;
|
|
14
|
+
if (Array.isArray(error)) {
|
|
15
|
+
for (const entry of error) {
|
|
16
|
+
const text = firstErrorText(entry);
|
|
17
|
+
if (text !== undefined) return text;
|
|
18
|
+
}
|
|
19
|
+
return undefined;
|
|
20
|
+
}
|
|
21
|
+
if (typeof error === "object") {
|
|
22
|
+
const message = (error as { message?: unknown }).message;
|
|
23
|
+
if (typeof message === "string") return message;
|
|
24
|
+
for (const value of Object.values(error)) {
|
|
25
|
+
const text = firstErrorText(value);
|
|
26
|
+
if (text !== undefined) return text;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return undefined;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** The field's live value — one subscription, on the field's own store. */
|
|
33
|
+
export function useFieldValue(field: TMDataGridEditField): unknown {
|
|
34
|
+
return useStore(field.store, (state) => state.value as unknown);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** The field's first error, for the Mantine input's `error` prop. */
|
|
38
|
+
export function useFieldError(field: TMDataGridEditField): string | undefined {
|
|
39
|
+
return useStore(field.store, (state) =>
|
|
40
|
+
firstErrorText(state.meta.errors as ReadonlyArray<unknown>),
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Select-all on focus — F2/Enter open with the value ready to replace. */
|
|
45
|
+
export function selectAllOnFocus(event: { currentTarget: unknown }): void {
|
|
46
|
+
const target = event.currentTarget;
|
|
47
|
+
if (target instanceof HTMLInputElement) target.select();
|
|
48
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { Autocomplete } from "@mantine/core";
|
|
2
|
+
import { resolveColumnOptions } from "../../core/columnOptions";
|
|
3
|
+
import type { TMDataGridFilterControlArgs } from "../../core/filterControls";
|
|
4
|
+
import {
|
|
5
|
+
operatorNeedsValue,
|
|
6
|
+
operatorTakesArrayValue,
|
|
7
|
+
operatorTakesRangeValue,
|
|
8
|
+
} from "../../core/filterOperators";
|
|
9
|
+
import { TMDataGridFilterValueInput } from "./TMDataGridFilterValueInput";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* A built-in `meta.filterControl` for text-shaped columns: a free-text input
|
|
13
|
+
* that also offers the values present in the data (or the column's declared
|
|
14
|
+
* `meta.options`) as suggestions. Set or range operators fall back to the
|
|
15
|
+
* standard input.
|
|
16
|
+
*/
|
|
17
|
+
export function DgAutocompleteFilter(args: TMDataGridFilterControlArgs) {
|
|
18
|
+
const { column, table, operator, value, onChange, options, size, labels } =
|
|
19
|
+
args;
|
|
20
|
+
if (operatorTakesArrayValue(operator) || operatorTakesRangeValue(operator)) {
|
|
21
|
+
return <TMDataGridFilterValueInput {...args} />;
|
|
22
|
+
}
|
|
23
|
+
const needsValue = operatorNeedsValue(operator);
|
|
24
|
+
// `options` arrives pre-resolved only for declared or select-shaped
|
|
25
|
+
// columns; on a plain string column the suggestions come from the faceted
|
|
26
|
+
// values, resolved here.
|
|
27
|
+
const suggestions =
|
|
28
|
+
options.length > 0
|
|
29
|
+
? options
|
|
30
|
+
: resolveColumnOptions({ table, column, fallback: "faceted" });
|
|
31
|
+
|
|
32
|
+
return (
|
|
33
|
+
<Autocomplete
|
|
34
|
+
label={labels.filterValue}
|
|
35
|
+
size={size}
|
|
36
|
+
w={180}
|
|
37
|
+
comboboxProps={{ withinPortal: false }}
|
|
38
|
+
disabled={!needsValue}
|
|
39
|
+
placeholder={needsValue ? labels.filterValuePlaceholder : ""}
|
|
40
|
+
data={suggestions.map((option) => option.value)}
|
|
41
|
+
limit={20}
|
|
42
|
+
value={needsValue && typeof value === "string" ? value : ""}
|
|
43
|
+
onChange={onChange}
|
|
44
|
+
/>
|
|
45
|
+
);
|
|
46
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { Group, TextInput } from "@mantine/core";
|
|
2
|
+
import type { TMDataGridFilterControlArgs } from "../../core/filterControls";
|
|
3
|
+
import { operatorTakesRangeValue } from "../../core/filterOperators";
|
|
4
|
+
import { TMDataGridFilterValueInput } from "./TMDataGridFilterValueInput";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* A built-in `meta.filterControl` for `date` columns: a From/To pair of
|
|
8
|
+
* native date inputs writing the `between` pair — no `@mantine/dates`
|
|
9
|
+
* involved, like the built-in date editor. Pair it with
|
|
10
|
+
* `meta.defaultFilterOperator: "between"`; any non-range operator falls back
|
|
11
|
+
* to the standard input.
|
|
12
|
+
*/
|
|
13
|
+
export function DgDateRangeFilter(args: TMDataGridFilterControlArgs) {
|
|
14
|
+
const { operator, value, onChange, size, labels } = args;
|
|
15
|
+
if (!operatorTakesRangeValue(operator)) {
|
|
16
|
+
return <TMDataGridFilterValueInput {...args} />;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const pair: [string, string] = Array.isArray(value)
|
|
20
|
+
? [String(value[0] ?? ""), String(value[1] ?? "")]
|
|
21
|
+
: ["", ""];
|
|
22
|
+
|
|
23
|
+
return (
|
|
24
|
+
<Group gap={4} wrap="nowrap" align="flex-start">
|
|
25
|
+
<TextInput
|
|
26
|
+
label={labels.filterFrom}
|
|
27
|
+
size={size}
|
|
28
|
+
w={88}
|
|
29
|
+
type="date"
|
|
30
|
+
value={pair[0]}
|
|
31
|
+
onChange={(event) => onChange([event.currentTarget.value, pair[1]])}
|
|
32
|
+
/>
|
|
33
|
+
<TextInput
|
|
34
|
+
label={labels.filterTo}
|
|
35
|
+
size={size}
|
|
36
|
+
w={88}
|
|
37
|
+
type="date"
|
|
38
|
+
value={pair[1]}
|
|
39
|
+
onChange={(event) => onChange([pair[0], event.currentTarget.value])}
|
|
40
|
+
/>
|
|
41
|
+
</Group>
|
|
42
|
+
);
|
|
43
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { Input, RangeSlider } from "@mantine/core";
|
|
2
|
+
import type { TMDataGridFilterControlArgs } from "../../core/filterControls";
|
|
3
|
+
import { operatorTakesRangeValue } from "../../core/filterOperators";
|
|
4
|
+
import { TMDataGridFilterValueInput } from "./TMDataGridFilterValueInput";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* A built-in `meta.filterControl` for `number` columns: a range slider whose
|
|
8
|
+
* bounds are seeded from the values present in the data, writing the
|
|
9
|
+
* `between` pair. Pair it with `meta.defaultFilterOperator: "between"` so
|
|
10
|
+
* the filter opens on it; any non-range operator falls back to the standard
|
|
11
|
+
* input.
|
|
12
|
+
*/
|
|
13
|
+
export function DgRangeSliderFilter(args: TMDataGridFilterControlArgs) {
|
|
14
|
+
const { column, operator, value, onChange, size, labels } = args;
|
|
15
|
+
if (!operatorTakesRangeValue(operator)) {
|
|
16
|
+
return <TMDataGridFilterValueInput {...args} />;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Bounds from the faceted index — the min and max actually in the data.
|
|
20
|
+
const numbers: number[] = [];
|
|
21
|
+
for (const key of column.getFacetedUniqueValues().keys()) {
|
|
22
|
+
const numeric = Number(key);
|
|
23
|
+
if (Number.isFinite(numeric)) numbers.push(numeric);
|
|
24
|
+
}
|
|
25
|
+
const min = numbers.length > 0 ? Math.min(...numbers) : 0;
|
|
26
|
+
const max = numbers.length > 0 ? Math.max(...numbers) : 100;
|
|
27
|
+
|
|
28
|
+
const pair = Array.isArray(value) ? value : ["", ""];
|
|
29
|
+
const lower = pair[0] === undefined || pair[0] === "" ? min : Number(pair[0]);
|
|
30
|
+
const upper = pair[1] === undefined || pair[1] === "" ? max : Number(pair[1]);
|
|
31
|
+
|
|
32
|
+
return (
|
|
33
|
+
<Input.Wrapper label={labels.filterValue} size={size} w={180}>
|
|
34
|
+
<RangeSlider
|
|
35
|
+
size={size}
|
|
36
|
+
mt={6}
|
|
37
|
+
min={min}
|
|
38
|
+
max={max}
|
|
39
|
+
minRange={0}
|
|
40
|
+
value={[
|
|
41
|
+
Math.min(Math.max(lower, min), max),
|
|
42
|
+
Math.min(Math.max(upper, min), max),
|
|
43
|
+
]}
|
|
44
|
+
thumbFromLabel={labels.filterFrom}
|
|
45
|
+
thumbToLabel={labels.filterTo}
|
|
46
|
+
onChange={([nextLower, nextUpper]) =>
|
|
47
|
+
onChange([String(nextLower), String(nextUpper)])
|
|
48
|
+
}
|
|
49
|
+
/>
|
|
50
|
+
</Input.Wrapper>
|
|
51
|
+
);
|
|
52
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { Input, SegmentedControl } from "@mantine/core";
|
|
2
|
+
import type { TMDataGridFilterControlArgs } from "../../core/filterControls";
|
|
3
|
+
import {
|
|
4
|
+
operatorNeedsValue,
|
|
5
|
+
operatorTakesArrayValue,
|
|
6
|
+
operatorTakesRangeValue,
|
|
7
|
+
} from "../../core/filterOperators";
|
|
8
|
+
import { TMDataGridFilterValueInput } from "./TMDataGridFilterValueInput";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* A built-in `meta.filterControl` for `boolean` columns: an All / Yes / No
|
|
12
|
+
* segmented control — All clears the value, which deactivates the filter.
|
|
13
|
+
* Set or range operators fall back to the standard input.
|
|
14
|
+
*/
|
|
15
|
+
export function DgTriStateFilter(args: TMDataGridFilterControlArgs) {
|
|
16
|
+
const { operator, value, onChange, size, labels } = args;
|
|
17
|
+
if (operatorTakesArrayValue(operator) || operatorTakesRangeValue(operator)) {
|
|
18
|
+
return <TMDataGridFilterValueInput {...args} />;
|
|
19
|
+
}
|
|
20
|
+
const needsValue = operatorNeedsValue(operator);
|
|
21
|
+
const scalar = typeof value === "string" ? value : "";
|
|
22
|
+
|
|
23
|
+
return (
|
|
24
|
+
<Input.Wrapper label={labels.filterValue} size={size}>
|
|
25
|
+
<SegmentedControl
|
|
26
|
+
size={size}
|
|
27
|
+
disabled={!needsValue}
|
|
28
|
+
data={[
|
|
29
|
+
{ value: "all", label: labels.filterAll },
|
|
30
|
+
{ value: "true", label: labels.booleanTrue },
|
|
31
|
+
{ value: "false", label: labels.booleanFalse },
|
|
32
|
+
]}
|
|
33
|
+
value={scalar === "true" || scalar === "false" ? scalar : "all"}
|
|
34
|
+
onChange={(next) => onChange(next === "all" ? "" : next)}
|
|
35
|
+
/>
|
|
36
|
+
</Input.Wrapper>
|
|
37
|
+
);
|
|
38
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { Group, MultiSelect, Select, TextInput } from "@mantine/core";
|
|
2
|
+
import { optionsToComboboxData } from "../../core/columnOptions";
|
|
3
|
+
import { getColumnType } from "../../core/columnUtils";
|
|
4
|
+
import type { TMDataGridFilterControlArgs } from "../../core/filterControls";
|
|
5
|
+
import {
|
|
6
|
+
operatorNeedsValue,
|
|
7
|
+
operatorTakesArrayValue,
|
|
8
|
+
operatorTakesRangeValue,
|
|
9
|
+
} from "../../core/filterOperators";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* The built-in value control of a filter-panel row — what renders when a
|
|
13
|
+
* column declares no `meta.filterControl`. Shaped by the operator: a
|
|
14
|
+
* multi-select for the set operators, a From/To pair for `between`, a Yes/No
|
|
15
|
+
* dropdown for booleans, a typed input otherwise.
|
|
16
|
+
*
|
|
17
|
+
* Exported so a custom control can fall back to it for the operators it does
|
|
18
|
+
* not care about, instead of rebuilding them.
|
|
19
|
+
*/
|
|
20
|
+
export function TMDataGridFilterValueInput({
|
|
21
|
+
column,
|
|
22
|
+
operator,
|
|
23
|
+
value,
|
|
24
|
+
onChange,
|
|
25
|
+
options,
|
|
26
|
+
size,
|
|
27
|
+
labels,
|
|
28
|
+
}: TMDataGridFilterControlArgs) {
|
|
29
|
+
const type = getColumnType(column);
|
|
30
|
+
const needsValue = operatorNeedsValue(operator);
|
|
31
|
+
const scalarValue = typeof value === "string" ? value : "";
|
|
32
|
+
const inputType =
|
|
33
|
+
type === "number" ? "number" : type === "date" ? "date" : "text";
|
|
34
|
+
|
|
35
|
+
if (operatorTakesRangeValue(operator)) {
|
|
36
|
+
const rangeValue: [string, string] = Array.isArray(value)
|
|
37
|
+
? [String(value[0] ?? ""), String(value[1] ?? "")]
|
|
38
|
+
: ["", ""];
|
|
39
|
+
return (
|
|
40
|
+
// The interval's two ends. Either may stay empty — an open end — and
|
|
41
|
+
// each writes its slot of the `[min, max]` pair.
|
|
42
|
+
<Group gap={4} wrap="nowrap" align="flex-start">
|
|
43
|
+
<TextInput
|
|
44
|
+
label={labels.filterFrom}
|
|
45
|
+
size={size}
|
|
46
|
+
w={88}
|
|
47
|
+
type={inputType}
|
|
48
|
+
data-dg-part="filter-value-from"
|
|
49
|
+
value={rangeValue[0]}
|
|
50
|
+
onChange={(event) =>
|
|
51
|
+
onChange([event.currentTarget.value, rangeValue[1]])
|
|
52
|
+
}
|
|
53
|
+
/>
|
|
54
|
+
<TextInput
|
|
55
|
+
label={labels.filterTo}
|
|
56
|
+
size={size}
|
|
57
|
+
w={88}
|
|
58
|
+
type={inputType}
|
|
59
|
+
data-dg-part="filter-value-to"
|
|
60
|
+
value={rangeValue[1]}
|
|
61
|
+
onChange={(event) =>
|
|
62
|
+
onChange([rangeValue[0], event.currentTarget.value])
|
|
63
|
+
}
|
|
64
|
+
/>
|
|
65
|
+
</Group>
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (operatorTakesArrayValue(operator)) {
|
|
70
|
+
// The set the cell is tested against. Options come from `meta.options`;
|
|
71
|
+
// a select column that declares none still gets the values present in
|
|
72
|
+
// the data, via the faceted index.
|
|
73
|
+
return (
|
|
74
|
+
<MultiSelect
|
|
75
|
+
label={labels.filterValue}
|
|
76
|
+
size={size}
|
|
77
|
+
w={180}
|
|
78
|
+
comboboxProps={{ withinPortal: false }}
|
|
79
|
+
searchable
|
|
80
|
+
data-dg-part="filter-value"
|
|
81
|
+
data={optionsToComboboxData(options)}
|
|
82
|
+
value={Array.isArray(value) ? [...value] : []}
|
|
83
|
+
onChange={onChange}
|
|
84
|
+
/>
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (type === "boolean") {
|
|
89
|
+
return (
|
|
90
|
+
<Select
|
|
91
|
+
label={labels.filterValue}
|
|
92
|
+
size={size}
|
|
93
|
+
w={180}
|
|
94
|
+
comboboxProps={{ withinPortal: false }}
|
|
95
|
+
data-dg-part="filter-value"
|
|
96
|
+
disabled={!needsValue}
|
|
97
|
+
clearable
|
|
98
|
+
placeholder={needsValue ? labels.filterValuePlaceholder : ""}
|
|
99
|
+
data={[
|
|
100
|
+
{ value: "true", label: labels.booleanTrue },
|
|
101
|
+
{ value: "false", label: labels.booleanFalse },
|
|
102
|
+
]}
|
|
103
|
+
value={needsValue && scalarValue !== "" ? scalarValue : null}
|
|
104
|
+
onChange={(next) => onChange(next ?? "")}
|
|
105
|
+
/>
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return (
|
|
110
|
+
<TextInput
|
|
111
|
+
label={labels.filterValue}
|
|
112
|
+
size={size}
|
|
113
|
+
w={180}
|
|
114
|
+
type={needsValue ? inputType : "text"}
|
|
115
|
+
data-dg-part="filter-value"
|
|
116
|
+
disabled={!needsValue}
|
|
117
|
+
placeholder={needsValue ? labels.filterValuePlaceholder : ""}
|
|
118
|
+
value={needsValue ? scalarValue : ""}
|
|
119
|
+
onChange={(event) => onChange(event.currentTarget.value)}
|
|
120
|
+
/>
|
|
121
|
+
);
|
|
122
|
+
}
|