@jielga/tmdatagrid 2.0.0-beta.11 → 2.0.0-beta.13
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/dist/index.d.ts +285 -22
- package/dist/index.js +2857 -2376
- package/dist/index.js.map +1 -1
- package/dist/styles.css +1 -1
- package/package.json +1 -2
- package/skills/appearance/SKILL.md +1 -1
- package/skills/cell-selection/SKILL.md +1 -1
- package/skills/columns/SKILL.md +2 -2
- package/skills/data/SKILL.md +5 -2
- package/skills/editing/SKILL.md +1 -1
- package/skills/filtering/SKILL.md +112 -26
- package/skills/getting-started/SKILL.md +4 -4
- package/skills/grouping/SKILL.md +1 -1
- package/skills/options/SKILL.md +3 -1
- package/skills/rows/SKILL.md +1 -1
- package/skills/server-side/SKILL.md +44 -15
- package/skills/testing/SKILL.md +6 -3
- package/src/tmdatagrid/components/TMDataGrid.tsx +3 -0
- package/src/tmdatagrid/components/TMDataGridEditColumn.tsx +1 -1
- package/src/tmdatagrid/components/TMDataGridFilterPanel.module.css +37 -18
- package/src/tmdatagrid/components/TMDataGridFilterPanel.tsx +138 -160
- package/src/tmdatagrid/components/TMDataGridFilterSurface.module.css +54 -0
- package/src/tmdatagrid/components/TMDataGridFilterSurface.tsx +162 -0
- package/src/tmdatagrid/components/TMDataGridFooter.tsx +28 -1
- package/src/tmdatagrid/components/TMDataGridHeaderCell.module.css +10 -0
- package/src/tmdatagrid/components/TMDataGridHeaderCell.tsx +12 -3
- package/src/tmdatagrid/components/TMDataGridHeaderFilterRow.module.css +51 -0
- package/src/tmdatagrid/components/TMDataGridHeaderFilterRow.tsx +303 -0
- package/src/tmdatagrid/components/TMDataGridTable.module.css +9 -41
- package/src/tmdatagrid/components/TMDataGridTable.tsx +84 -15
- package/src/tmdatagrid/components/TMDataGridToolbar.tsx +37 -11
- package/src/tmdatagrid/components/filters/DgAutocompleteFilter.tsx +5 -4
- package/src/tmdatagrid/components/filters/DgDateRangeFilter.tsx +22 -5
- package/src/tmdatagrid/components/filters/DgRangeSliderFilter.tsx +5 -1
- package/src/tmdatagrid/components/filters/DgTriStateFilter.tsx +5 -1
- package/src/tmdatagrid/components/filters/TMDataGridFilterValueInput.tsx +44 -28
- package/src/tmdatagrid/components/filters/controlLayout.ts +32 -0
- package/src/tmdatagrid/components/filters/filterControlFor.ts +65 -0
- package/src/tmdatagrid/components/sticky.module.css +44 -0
- package/src/tmdatagrid/core/columnOptions.ts +46 -0
- package/src/tmdatagrid/core/controlledStateSync.ts +108 -0
- package/src/tmdatagrid/core/editEngine.ts +68 -12
- package/src/tmdatagrid/core/filterControls.ts +28 -0
- package/src/tmdatagrid/core/filterOperators.ts +64 -1
- package/src/tmdatagrid/core/filterSurface.ts +99 -0
- package/src/tmdatagrid/core/labels.ts +7 -0
- package/src/tmdatagrid/core/labelsSv.ts +3 -0
- package/src/tmdatagrid/core/pageReset.ts +115 -0
- package/src/tmdatagrid/index.ts +13 -0
- package/src/tmdatagrid/useTMDataGrid.tsx +191 -11
|
@@ -1,30 +1,49 @@
|
|
|
1
|
-
/*
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
/* A plain block of controls. Nothing here positions it - the popup and the
|
|
2
|
+
sidebar own their own placement, and a consumer placing the panel by hand
|
|
3
|
+
places it the way any other block. */
|
|
4
4
|
.filterPanel {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
z-index: 5;
|
|
9
|
-
padding: var(--mantine-spacing-sm);
|
|
10
|
-
border-radius: var(--mantine-radius-md);
|
|
11
|
-
border: 1px solid var(--mantine-color-default-border);
|
|
12
|
-
background-color: var(--mantine-color-body);
|
|
13
|
-
box-shadow: var(--mantine-shadow-md);
|
|
5
|
+
display: flex;
|
|
6
|
+
flex-direction: column;
|
|
7
|
+
gap: var(--mantine-spacing-xs);
|
|
14
8
|
}
|
|
15
9
|
|
|
10
|
+
/* Column, operator and value side by side, bottom-aligned so their fields line
|
|
11
|
+
up under labels of differing heights. */
|
|
16
12
|
.filterRow {
|
|
17
13
|
display: flex;
|
|
18
14
|
align-items: flex-end;
|
|
19
15
|
gap: var(--mantine-spacing-xs);
|
|
20
16
|
}
|
|
21
17
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
18
|
+
/* The narrow-host layout: the same three fields one under the other, each
|
|
19
|
+
filling the width, with the ✕ beside the column it removes. A card rather
|
|
20
|
+
than a bare stack, because without the side-by-side alignment there is
|
|
21
|
+
nothing else saying where one filter ends and the next begins. */
|
|
22
|
+
.filterRow[data-layout="stacked"] {
|
|
23
|
+
display: grid;
|
|
24
|
+
grid-template-columns: 1fr auto;
|
|
25
|
+
align-items: end;
|
|
26
|
+
gap: 6px var(--mantine-spacing-xs);
|
|
27
|
+
padding: var(--mantine-spacing-xs);
|
|
28
|
+
border: 1px solid var(--mantine-color-default-border);
|
|
29
|
+
border-radius: var(--mantine-radius-sm);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
.filterRow[data-layout="stacked"] .filterRemove {
|
|
33
|
+
grid-column: 2;
|
|
34
|
+
grid-row: 1;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
.filterRow[data-layout="stacked"] .filterColumnSlot {
|
|
38
|
+
grid-column: 1;
|
|
39
|
+
grid-row: 1;
|
|
40
|
+
min-width: 0;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
.filterRow[data-layout="stacked"] .filterOperatorSlot,
|
|
44
|
+
.filterRow[data-layout="stacked"] .filterValueSlot {
|
|
45
|
+
grid-column: 1 / -1;
|
|
46
|
+
min-width: 0;
|
|
28
47
|
}
|
|
29
48
|
|
|
30
49
|
.filterPanelFooter {
|
|
@@ -1,19 +1,10 @@
|
|
|
1
|
-
import {
|
|
2
|
-
ActionIcon,
|
|
3
|
-
Button,
|
|
4
|
-
Select,
|
|
5
|
-
Stack,
|
|
6
|
-
Text,
|
|
7
|
-
TextInput,
|
|
8
|
-
} from "@mantine/core";
|
|
1
|
+
import { ActionIcon, Button, Select, Stack, TextInput } from "@mantine/core";
|
|
9
2
|
import { useSelector } from "@tanstack/react-store";
|
|
10
3
|
import { useEffect, useRef } from "react";
|
|
11
4
|
import classes from "./TMDataGridFilterPanel.module.css";
|
|
12
5
|
import { useTMDataGridContext } from "../TMDataGridContext";
|
|
13
|
-
import { resolveColumnOptions } from "../core/columnOptions";
|
|
14
6
|
import {
|
|
15
7
|
getColumnDefaultOperator,
|
|
16
|
-
getColumnFilterControl,
|
|
17
8
|
getColumnLabel,
|
|
18
9
|
getColumnType,
|
|
19
10
|
} from "../core/columnUtils";
|
|
@@ -21,14 +12,15 @@ import {
|
|
|
21
12
|
type TMDataGridFilterOperator,
|
|
22
13
|
type TMDataGridFilterValue,
|
|
23
14
|
emptyValueForOperator,
|
|
15
|
+
filterValueShape,
|
|
24
16
|
getDefaultOperator,
|
|
25
17
|
getOperatorsForType,
|
|
26
18
|
isTMDataGridFilterValue,
|
|
27
19
|
operatorNeedsValue,
|
|
28
|
-
operatorTakesArrayValue,
|
|
29
|
-
operatorTakesRangeValue,
|
|
30
20
|
} from "../core/filterOperators";
|
|
21
|
+
import type { TMDataGridFilterPanelLayout } from "../core/filterControls";
|
|
31
22
|
import { CloseIcon } from "./icons";
|
|
23
|
+
import { filterControlFor } from "./filters/filterControlFor";
|
|
32
24
|
import { TMDataGridFilterValueInput } from "./filters/TMDataGridFilterValueInput";
|
|
33
25
|
|
|
34
26
|
const FALLBACK_FILTER: TMDataGridFilterValue = { operator: "contains", value: "" };
|
|
@@ -37,55 +29,63 @@ function asFilterValue(value: unknown): TMDataGridFilterValue {
|
|
|
37
29
|
return isTMDataGridFilterValue(value) ? value : FALLBACK_FILTER;
|
|
38
30
|
}
|
|
39
31
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
32
|
+
export type TMDataGridFilterPanelProps = {
|
|
33
|
+
/**
|
|
34
|
+
* How one filter row is laid out.
|
|
35
|
+
*
|
|
36
|
+
* `"row"` - the default - puts column, operator and value side by side,
|
|
37
|
+
* which wants about 550px. `"stacked"` puts them one under the other, each
|
|
38
|
+
* filling the width, for a host too narrow for that: the sidebar surface
|
|
39
|
+
* uses it, and so should a panel you place in a drawer or a narrow column.
|
|
40
|
+
*
|
|
41
|
+
* Passed through to every value control as its `layout`, so a
|
|
42
|
+
* `meta.filter.control` can size itself to the same decision.
|
|
43
|
+
*/
|
|
44
|
+
layout?: TMDataGridFilterPanelLayout;
|
|
45
|
+
};
|
|
52
46
|
|
|
53
47
|
/**
|
|
54
48
|
* The MUI-style filter surface: one row per active column filter, each row a
|
|
55
|
-
* column / operator / value triple
|
|
56
|
-
*
|
|
57
|
-
*
|
|
49
|
+
* column / operator / value triple, over an "Add filter" / "Clear all" footer.
|
|
50
|
+
*
|
|
51
|
+
* A plain block of controls, the way `TMDataGrid.ColumnsPanel` is - it renders
|
|
52
|
+
* whenever it is mounted and knows nothing about floating. The chrome that
|
|
53
|
+
* makes a floating surface (a title, a close button, click-away, Escape)
|
|
54
|
+
* belongs to the popup and the sidebar, so this can equally be dropped into a
|
|
55
|
+
* drawer, a page column or a form. See the `filters.surface` option.
|
|
56
|
+
*
|
|
57
|
+
* It only ever reads and writes the table's `columnFilters` state, so a
|
|
58
|
+
* `manualFiltering` grid gets the same panel for free - the state is forwarded
|
|
59
|
+
* to the server instead of a row model.
|
|
58
60
|
*/
|
|
59
|
-
export function TMDataGridFilterPanel(
|
|
61
|
+
export function TMDataGridFilterPanel({
|
|
62
|
+
layout = "row",
|
|
63
|
+
}: TMDataGridFilterPanelProps = {}) {
|
|
60
64
|
const { table, ui, labels, controlSize } = useTMDataGridContext();
|
|
61
|
-
const opened = useSelector(ui, (state) => state.filterPanelOpen);
|
|
62
65
|
const columnFilters = useSelector(
|
|
63
66
|
table.store,
|
|
64
67
|
(state) => state.columnFilters,
|
|
65
68
|
);
|
|
69
|
+
const focusColumnId = useSelector(ui, (state) => state.filterPanelColumnId);
|
|
66
70
|
const panelRef = useRef<HTMLDivElement>(null);
|
|
71
|
+
const stacked = layout === "stacked";
|
|
67
72
|
|
|
68
|
-
//
|
|
69
|
-
//
|
|
70
|
-
//
|
|
73
|
+
// Whoever pointed at a column - the column menu, a pill, the toolbar button -
|
|
74
|
+
// meant "let me type here", so the value slot takes the focus. Cleared once
|
|
75
|
+
// taken, which is what lets the same column be pointed at twice.
|
|
71
76
|
useEffect(() => {
|
|
72
|
-
if (
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
document.addEventListener("pointerdown", handlePointerDown);
|
|
85
|
-
return () => document.removeEventListener("pointerdown", handlePointerDown);
|
|
86
|
-
}, [opened, ui]);
|
|
87
|
-
|
|
88
|
-
if (!opened) return null;
|
|
77
|
+
if (focusColumnId === null) return;
|
|
78
|
+
const row = panelRef.current?.querySelector<HTMLElement>(
|
|
79
|
+
`[data-dg-part="filter-row"][data-column-id="${CSS.escape(focusColumnId)}"]`,
|
|
80
|
+
);
|
|
81
|
+
const slot = row?.querySelector<HTMLElement>("[data-dg-filter-value-slot]");
|
|
82
|
+
slot
|
|
83
|
+
?.querySelector<HTMLElement>(
|
|
84
|
+
"input:not([type='hidden']), textarea, select, [tabindex]:not([tabindex='-1'])",
|
|
85
|
+
)
|
|
86
|
+
?.focus();
|
|
87
|
+
ui.actions.focusPanelFilter(null);
|
|
88
|
+
}, [focusColumnId, ui]);
|
|
89
89
|
|
|
90
90
|
const filterableColumns = table
|
|
91
91
|
.getAllLeafColumns()
|
|
@@ -97,9 +97,9 @@ export function TMDataGridFilterPanel() {
|
|
|
97
97
|
}));
|
|
98
98
|
|
|
99
99
|
function removeFilter(columnId: string) {
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
100
|
+
table.setColumnFilters(
|
|
101
|
+
columnFilters.filter((filter) => filter.id !== columnId),
|
|
102
|
+
);
|
|
103
103
|
}
|
|
104
104
|
|
|
105
105
|
function changeFilterColumn(fromColumnId: string, toColumnId: string) {
|
|
@@ -114,7 +114,7 @@ export function TMDataGridFilterPanel() {
|
|
|
114
114
|
// operator - a text needle has no meaning to an `isAnyOf` set, or a set
|
|
115
115
|
// to a text input.
|
|
116
116
|
const value =
|
|
117
|
-
|
|
117
|
+
filterValueShape(operator) === filterValueShape(current.operator)
|
|
118
118
|
? current.value
|
|
119
119
|
: emptyValueForOperator(operator);
|
|
120
120
|
table.setColumnFilters(
|
|
@@ -129,7 +129,9 @@ export function TMDataGridFilterPanel() {
|
|
|
129
129
|
: filter,
|
|
130
130
|
),
|
|
131
131
|
);
|
|
132
|
-
|
|
132
|
+
// The row now names a different column; put the caret back in its value
|
|
133
|
+
// slot rather than leaving it on a dropdown whose meaning just changed.
|
|
134
|
+
ui.actions.focusPanelFilter(toColumnId);
|
|
133
135
|
}
|
|
134
136
|
|
|
135
137
|
function patchFilter(columnId: string, patch: Partial<TMDataGridFilterValue>) {
|
|
@@ -149,20 +151,12 @@ export function TMDataGridFilterPanel() {
|
|
|
149
151
|
// Same arity rule as changing the column: keep the value across operators
|
|
150
152
|
// of the same shape, reset it across any shape boundary.
|
|
151
153
|
const value =
|
|
152
|
-
|
|
154
|
+
filterValueShape(operator) === filterValueShape(current.operator)
|
|
153
155
|
? current.value
|
|
154
156
|
: emptyValueForOperator(operator);
|
|
155
157
|
patchFilter(columnId, { operator, value });
|
|
156
158
|
}
|
|
157
159
|
|
|
158
|
-
/** Clears every filter, including half-typed ones the pills never showed. */
|
|
159
|
-
function clearAllFilters() {
|
|
160
|
-
table.setColumnFilters([]);
|
|
161
|
-
// Same exit as removing the last filter row by hand: an empty panel has
|
|
162
|
-
// nothing to show but its own buttons.
|
|
163
|
-
ui.actions.closeFilterPanel();
|
|
164
|
-
}
|
|
165
|
-
|
|
166
160
|
function addFilter() {
|
|
167
161
|
const nextColumn = filterableColumns.find(
|
|
168
162
|
(column) => !columnFilters.some((filter) => filter.id === column.id),
|
|
@@ -179,6 +173,7 @@ export function TMDataGridFilterPanel() {
|
|
|
179
173
|
} satisfies TMDataGridFilterValue,
|
|
180
174
|
},
|
|
181
175
|
]);
|
|
176
|
+
ui.actions.focusPanelFilter(nextColumn.id);
|
|
182
177
|
}
|
|
183
178
|
|
|
184
179
|
const canAddFilter = filterableColumns.some(
|
|
@@ -191,31 +186,9 @@ export function TMDataGridFilterPanel() {
|
|
|
191
186
|
role="group"
|
|
192
187
|
aria-label={labels.filters}
|
|
193
188
|
data-dg-part="filter-panel"
|
|
189
|
+
data-layout={layout}
|
|
194
190
|
className={classes.filterPanel}
|
|
195
|
-
// Escape is what closes a floating surface, and every control that can
|
|
196
|
-
// hold focus in here sits inside this element.
|
|
197
|
-
onKeyDown={(event) => {
|
|
198
|
-
if (event.key !== "Escape") return;
|
|
199
|
-
event.stopPropagation();
|
|
200
|
-
ui.actions.closeFilterPanel();
|
|
201
|
-
}}
|
|
202
191
|
>
|
|
203
|
-
<div className={classes.filterPanelHeader}>
|
|
204
|
-
<Text span size={controlSize} fw={600}>
|
|
205
|
-
{labels.filters}
|
|
206
|
-
</Text>
|
|
207
|
-
<ActionIcon
|
|
208
|
-
variant="subtle"
|
|
209
|
-
color="gray"
|
|
210
|
-
size="sm"
|
|
211
|
-
aria-label={labels.closeFilters}
|
|
212
|
-
data-dg-part="filter-panel-close"
|
|
213
|
-
onClick={ui.actions.closeFilterPanel}
|
|
214
|
-
>
|
|
215
|
-
<CloseIcon size={16} stroke={1.6} />
|
|
216
|
-
</ActionIcon>
|
|
217
|
-
</div>
|
|
218
|
-
|
|
219
192
|
<Stack gap="xs">
|
|
220
193
|
{columnFilters.map((filter) => {
|
|
221
194
|
const column = table.getColumn(filter.id);
|
|
@@ -223,21 +196,11 @@ export function TMDataGridFilterPanel() {
|
|
|
223
196
|
const type = column ? getColumnType(column) : "string";
|
|
224
197
|
const needsValue = operatorNeedsValue(value.operator);
|
|
225
198
|
const scalarValue = typeof value.value === "string" ? value.value : "";
|
|
226
|
-
//
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
const options =
|
|
232
|
-
column &&
|
|
233
|
-
(column.columnDef.meta?.options !== undefined ||
|
|
234
|
-
type === "select" ||
|
|
235
|
-
type === "multiSelect")
|
|
236
|
-
? resolveColumnOptions({ table, column, fallback: "faceted" })
|
|
237
|
-
: [];
|
|
238
|
-
const ValueControl =
|
|
239
|
-
(column === undefined ? undefined : getColumnFilterControl(column)) ??
|
|
240
|
-
TMDataGridFilterValueInput;
|
|
199
|
+
// The same options-and-control decision the header cells make.
|
|
200
|
+
const { options, ValueControl } =
|
|
201
|
+
column === undefined
|
|
202
|
+
? { options: [], ValueControl: TMDataGridFilterValueInput }
|
|
203
|
+
: filterControlFor(table, column);
|
|
241
204
|
|
|
242
205
|
return (
|
|
243
206
|
<div
|
|
@@ -247,12 +210,14 @@ export function TMDataGridFilterPanel() {
|
|
|
247
210
|
// count on is which row it is in.
|
|
248
211
|
data-dg-part="filter-row"
|
|
249
212
|
data-column-id={filter.id}
|
|
213
|
+
data-layout={layout}
|
|
250
214
|
className={classes.filterRow}
|
|
251
215
|
>
|
|
252
216
|
<ActionIcon
|
|
253
217
|
variant="subtle"
|
|
254
218
|
color="gray"
|
|
255
219
|
size="lg"
|
|
220
|
+
className={classes.filterRemove}
|
|
256
221
|
aria-label={labels.removeFilter}
|
|
257
222
|
data-dg-part="filter-remove"
|
|
258
223
|
onClick={() => removeFilter(filter.id)}
|
|
@@ -260,69 +225,82 @@ export function TMDataGridFilterPanel() {
|
|
|
260
225
|
<CloseIcon size={18} stroke={1.6} />
|
|
261
226
|
</ActionIcon>
|
|
262
227
|
|
|
263
|
-
<
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
w={160}
|
|
267
|
-
allowDeselect={false}
|
|
268
|
-
// Both dropdowns render inside the panel: a portalled one is
|
|
269
|
-
// outside it in the DOM, and picking an option would read as a
|
|
270
|
-
// click away and close the panel under the user.
|
|
271
|
-
comboboxProps={{ withinPortal: false }}
|
|
272
|
-
data-dg-part="filter-column"
|
|
273
|
-
data={columnOptions}
|
|
274
|
-
value={filter.id}
|
|
275
|
-
onChange={(next) => next && changeFilterColumn(filter.id, next)}
|
|
276
|
-
/>
|
|
277
|
-
|
|
278
|
-
<Select
|
|
279
|
-
label={labels.filterOperator}
|
|
280
|
-
size={controlSize}
|
|
281
|
-
w={170}
|
|
282
|
-
allowDeselect={false}
|
|
283
|
-
comboboxProps={{ withinPortal: false }}
|
|
284
|
-
data-dg-part="filter-operator"
|
|
285
|
-
data={getOperatorsForType(type).map((operator) => ({
|
|
286
|
-
value: operator,
|
|
287
|
-
label: labels.operators[operator],
|
|
288
|
-
}))}
|
|
289
|
-
value={value.operator}
|
|
290
|
-
onChange={(next) =>
|
|
291
|
-
next &&
|
|
292
|
-
changeOperator(filter.id, next as TMDataGridFilterOperator)
|
|
293
|
-
}
|
|
294
|
-
/>
|
|
295
|
-
|
|
296
|
-
{column ? (
|
|
297
|
-
// The value slot: `meta.filter.control` if the column declares
|
|
298
|
-
// one, the built-in shape-by-operator input otherwise - both
|
|
299
|
-
// through the same value-only contract.
|
|
300
|
-
<ValueControl
|
|
301
|
-
column={column}
|
|
302
|
-
table={table}
|
|
303
|
-
operator={value.operator}
|
|
304
|
-
value={value.value}
|
|
305
|
-
onChange={(next) => patchFilter(filter.id, { value: next })}
|
|
306
|
-
options={options}
|
|
228
|
+
<div className={classes.filterColumnSlot}>
|
|
229
|
+
<Select
|
|
230
|
+
label={labels.filterColumn}
|
|
307
231
|
size={controlSize}
|
|
308
|
-
|
|
232
|
+
w={stacked ? "100%" : 160}
|
|
233
|
+
allowDeselect={false}
|
|
234
|
+
// Both dropdowns portal, like every dropdown in the panel:
|
|
235
|
+
// drawn inline they are clipped by the grid frame's
|
|
236
|
+
// `overflow: hidden` under the popup, or by the sidebar's
|
|
237
|
+
// scroller. The popup's click-away exempts portal nodes.
|
|
238
|
+
data-dg-part="filter-column"
|
|
239
|
+
data={columnOptions}
|
|
240
|
+
value={filter.id}
|
|
241
|
+
onChange={(next) =>
|
|
242
|
+
next && changeFilterColumn(filter.id, next)
|
|
243
|
+
}
|
|
309
244
|
/>
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
<
|
|
314
|
-
label={labels.
|
|
245
|
+
</div>
|
|
246
|
+
|
|
247
|
+
<div className={classes.filterOperatorSlot}>
|
|
248
|
+
<Select
|
|
249
|
+
label={labels.filterOperator}
|
|
315
250
|
size={controlSize}
|
|
316
|
-
w={
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
251
|
+
w={stacked ? "100%" : 170}
|
|
252
|
+
allowDeselect={false}
|
|
253
|
+
data-dg-part="filter-operator"
|
|
254
|
+
data={getOperatorsForType(type).map((operator) => ({
|
|
255
|
+
value: operator,
|
|
256
|
+
label: labels.operators[operator],
|
|
257
|
+
}))}
|
|
258
|
+
value={value.operator}
|
|
259
|
+
onChange={(next) =>
|
|
260
|
+
next &&
|
|
261
|
+
changeOperator(filter.id, next as TMDataGridFilterOperator)
|
|
323
262
|
}
|
|
324
263
|
/>
|
|
325
|
-
|
|
264
|
+
</div>
|
|
265
|
+
|
|
266
|
+
{/* What the focus effect above aims at. A `meta.filter.control`
|
|
267
|
+
may render anything, so the slot is the only landmark the
|
|
268
|
+
panel can count on. */}
|
|
269
|
+
<div className={classes.filterValueSlot} data-dg-filter-value-slot>
|
|
270
|
+
{column ? (
|
|
271
|
+
// `meta.filter.control` if the column declares one, the
|
|
272
|
+
// built-in shape-by-operator input otherwise - both through
|
|
273
|
+
// the same value-only contract.
|
|
274
|
+
<ValueControl
|
|
275
|
+
column={column}
|
|
276
|
+
table={table}
|
|
277
|
+
operator={value.operator}
|
|
278
|
+
value={value.value}
|
|
279
|
+
onChange={(next) => patchFilter(filter.id, { value: next })}
|
|
280
|
+
options={options}
|
|
281
|
+
size={controlSize}
|
|
282
|
+
labels={labels}
|
|
283
|
+
layout={layout}
|
|
284
|
+
/>
|
|
285
|
+
) : (
|
|
286
|
+
// The column is gone from the definition; the row survives
|
|
287
|
+
// only to be re-pointed or removed.
|
|
288
|
+
<TextInput
|
|
289
|
+
label={labels.filterValue}
|
|
290
|
+
size={controlSize}
|
|
291
|
+
w={stacked ? "100%" : 180}
|
|
292
|
+
data-dg-part="filter-value"
|
|
293
|
+
disabled={!needsValue}
|
|
294
|
+
placeholder={needsValue ? labels.filterValuePlaceholder : ""}
|
|
295
|
+
value={needsValue ? scalarValue : ""}
|
|
296
|
+
onChange={(event) =>
|
|
297
|
+
patchFilter(filter.id, {
|
|
298
|
+
value: event.currentTarget.value,
|
|
299
|
+
})
|
|
300
|
+
}
|
|
301
|
+
/>
|
|
302
|
+
)}
|
|
303
|
+
</div>
|
|
326
304
|
</div>
|
|
327
305
|
);
|
|
328
306
|
})}
|
|
@@ -343,7 +321,7 @@ export function TMDataGridFilterPanel() {
|
|
|
343
321
|
size="compact-sm"
|
|
344
322
|
disabled={columnFilters.length === 0}
|
|
345
323
|
data-dg-part="filter-clear-all"
|
|
346
|
-
onClick={
|
|
324
|
+
onClick={() => table.setColumnFilters([])}
|
|
347
325
|
>
|
|
348
326
|
{labels.clearAllFilters}
|
|
349
327
|
</Button>
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/* Chrome shared by the two automatic surfaces. The panel inside them is a
|
|
2
|
+
plain block; everything that makes one float or sit beside the rows is
|
|
3
|
+
here. */
|
|
4
|
+
|
|
5
|
+
.surfaceHeader {
|
|
6
|
+
display: flex;
|
|
7
|
+
align-items: center;
|
|
8
|
+
justify-content: space-between;
|
|
9
|
+
gap: var(--mantine-spacing-md);
|
|
10
|
+
padding-bottom: var(--mantine-spacing-xs);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/* Floats over the first body rows, anchored to `.tableWrapper`. Sits below the
|
|
14
|
+
whole header - group rows and the filter row included - which the table
|
|
15
|
+
measures onto the wrapper as `--dg-header-stack-height`. The size scale's
|
|
16
|
+
single-row value is the fallback for the frame before the first
|
|
17
|
+
measurement. */
|
|
18
|
+
.filterPopup {
|
|
19
|
+
position: absolute;
|
|
20
|
+
top: calc(var(--dg-header-stack-height, var(--dg-header-height)) + 2px);
|
|
21
|
+
left: var(--mantine-spacing-md);
|
|
22
|
+
z-index: var(--dg-z-filter-popup, 20);
|
|
23
|
+
padding: var(--mantine-spacing-sm);
|
|
24
|
+
border-radius: var(--mantine-radius-md);
|
|
25
|
+
border: 1px solid var(--mantine-color-default-border);
|
|
26
|
+
background-color: var(--mantine-color-body);
|
|
27
|
+
box-shadow: var(--mantine-shadow-md);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/* A column of the table wrapper rather than a layer over it: the rows give up
|
|
31
|
+
the width instead of being covered by it. `flex: none` so the declared
|
|
32
|
+
width holds whatever the panel inside asks for. */
|
|
33
|
+
.filterSidebar {
|
|
34
|
+
display: flex;
|
|
35
|
+
flex-direction: column;
|
|
36
|
+
flex: none;
|
|
37
|
+
min-width: 0;
|
|
38
|
+
padding: var(--mantine-spacing-sm);
|
|
39
|
+
background-color: var(--mantine-color-body);
|
|
40
|
+
border-left: 1px solid var(--mantine-color-default-border);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/* On the left it is the same panel with its divider on the other edge, and
|
|
44
|
+
ahead of the scroll container in the flex order. */
|
|
45
|
+
.filterSidebar[data-side="left"] {
|
|
46
|
+
order: -1;
|
|
47
|
+
border-left: none;
|
|
48
|
+
border-right: 1px solid var(--mantine-color-default-border);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
.filterSidebarBody {
|
|
52
|
+
flex: 1;
|
|
53
|
+
min-height: 0;
|
|
54
|
+
}
|