@jielga/tmdatagrid 2.3.0 → 2.4.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/dist/index.d.ts +262 -8
- package/dist/index.js +1340 -1218
- package/dist/index.js.map +1 -1
- package/docs/columns.md +7 -0
- package/docs/filtering.md +2 -1
- package/docs/grouping.md +1 -1
- package/docs/localization.md +3 -3
- package/docs/persistence.md +88 -0
- package/docs/typed-deep-links.md +169 -0
- package/package.json +1 -1
- package/skills/appearance/SKILL.md +1 -1
- package/skills/appearance/references/labels.md +3 -3
- package/skills/cell-selection/SKILL.md +1 -1
- package/skills/columns/SKILL.md +4 -1
- package/skills/data/SKILL.md +1 -1
- package/skills/editing/SKILL.md +1 -1
- package/skills/filtering/SKILL.md +3 -2
- package/skills/getting-started/SKILL.md +2 -2
- package/skills/grouping/SKILL.md +2 -2
- package/skills/migrating-to-2/SKILL.md +1 -1
- package/skills/options/SKILL.md +30 -6
- package/skills/options/references/typed-links.md +134 -0
- package/skills/rows/SKILL.md +1 -1
- package/skills/server-side/SKILL.md +1 -1
- package/skills/testing/SKILL.md +1 -1
- package/skills/testing-components/SKILL.md +1 -1
- package/skills/testing-editing/SKILL.md +1 -1
- package/src/components/TMDataGridFilterPills.tsx +15 -1
- package/src/components/TMDataGridGroupColumn.tsx +19 -4
- package/src/components/TMDataGridHeaderFilterRow.tsx +7 -1
- package/src/core/filterOperators.ts +41 -18
- package/src/core/grouping.ts +11 -3
- package/src/core/labels.ts +4 -3
- package/src/core/persistence.ts +9 -1
- package/src/core/urlState.ts +439 -0
- package/src/core/valueText.ts +46 -0
- package/src/index.ts +13 -0
- package/src/useTMDataGrid.tsx +50 -7
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
# Typed links
|
|
2
|
+
|
|
3
|
+
Reference for the `options` skill: `createTMDataGridUrlState`, what a link
|
|
4
|
+
may name, the definition's members, and the TanStack Router setup.
|
|
5
|
+
|
|
6
|
+
`createTMDataGridUrlState({ key, columns, slices? })` defines a grid's deep
|
|
7
|
+
links from its columns. `columns` is the array the grid renders, built with
|
|
8
|
+
`createTMDataGridColumnHelper`; each helper column carries its id, `meta.type`,
|
|
9
|
+
value type, `meta.filter.operators` and its `enableColumnFilter`,
|
|
10
|
+
`enableSorting` and `meta.filter.url` switches in its type only
|
|
11
|
+
(`TMDataGridColumnInfo`, read with `TMDataGridColumnInfoOf`; the brand type is
|
|
12
|
+
`TMDataGridTypedColumn`).
|
|
13
|
+
|
|
14
|
+
```tsx
|
|
15
|
+
const columns = columnHelper.columns([
|
|
16
|
+
columnHelper.accessor("department", { header: "Department", meta: { type: "select" } }),
|
|
17
|
+
columnHelper.accessor("salary", { header: "Salary", meta: { type: "number" } }),
|
|
18
|
+
columnHelper.accessor("email", { header: "Email", enableColumnFilter: false }),
|
|
19
|
+
]);
|
|
20
|
+
|
|
21
|
+
export const employeesUrl = createTMDataGridUrlState({
|
|
22
|
+
key: "employees",
|
|
23
|
+
columns,
|
|
24
|
+
slices: ["columnFilters", "sorting"],
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
const persist = { urlKey: employeesUrl.urlKey } satisfies TMDataGridPersistence;
|
|
28
|
+
|
|
29
|
+
const href = `/employees?${employeesUrl.searchParams({
|
|
30
|
+
filters: { department: { operator: "isAnyOf", value: ["Design"] } },
|
|
31
|
+
sorting: [{ id: "salary", desc: true }],
|
|
32
|
+
})}`;
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
What a link may name (`TMDataGridUrlInput`); anything else is a type error:
|
|
36
|
+
|
|
37
|
+
- `filters` (`TMDataGridUrlFiltersInput`): only columns whose
|
|
38
|
+
`enableColumnFilter` is not `false` and `meta.filter.url` is not `false`.
|
|
39
|
+
With no such column, `filters` accepts no key at all.
|
|
40
|
+
- The operator: one the column's `meta.type` offers, narrowed by
|
|
41
|
+
`meta.filter.operators`.
|
|
42
|
+
- The value: a `number` for a number column (`between` takes
|
|
43
|
+
`[number | null, number | null]`, `null` leaving that end open); a `Date` or a
|
|
44
|
+
`YYYY-MM-DD` string for a date column (`TMDataGridDateInput`; a `Date` is
|
|
45
|
+
written as its local calendar day, a longer string is cut to 10 characters);
|
|
46
|
+
a `boolean` for a boolean column; a `string` for a string column; an array of
|
|
47
|
+
the column's own values (the element type for an array value) for `isAnyOf` /
|
|
48
|
+
`isNoneOf`; no `value` for `isEmpty` / `isNotEmpty`.
|
|
49
|
+
- `sorting` (`TMDataGridUrlSortableId`): only columns whose `enableSorting` is
|
|
50
|
+
not `false`. `desc` defaults to `false`.
|
|
51
|
+
- `globalFilter` and `pagination` (`{ pageIndex, pageSize }`): only when the
|
|
52
|
+
definition carries those slices. `slices` defaults to every data slice.
|
|
53
|
+
- A column with an accessor function is named by its `id`; without an `id` it
|
|
54
|
+
cannot be named. A dotted accessor key is named with underscores
|
|
55
|
+
(`address_city`).
|
|
56
|
+
- Only literal values count: `enableSorting: isAdmin` reads as not sortable.
|
|
57
|
+
Columns built without the helper offer nothing. A table-level
|
|
58
|
+
`enableColumnFilters` or `enableSorting` is not seen.
|
|
59
|
+
|
|
60
|
+
The definition (`TMDataGridUrlState`):
|
|
61
|
+
|
|
62
|
+
| Member | Type | Description |
|
|
63
|
+
| --- | --- | --- |
|
|
64
|
+
| `key` | `string` | The URL search param. |
|
|
65
|
+
| `slices` | `TMDataGridDataSlice[]` | The data slices the param carries. Default: every data slice. |
|
|
66
|
+
| `urlKey` | `TMDataGridPersistKey` | For `persist.urlKey`: the key, or `[key, slices]` when `slices` is set. |
|
|
67
|
+
| `searchParams(input, { serialize? })` | `URLSearchParams` | The link as search params, encoded with `serialize` (default `JSON.stringify`, as the grid's default). Empty when the input sets nothing. Pass the `serialize` given to `persist`, if any. |
|
|
68
|
+
| `search(input)` | `{ [key]: TMDataGridUrlPayload \| undefined }` | The link as a router search object. |
|
|
69
|
+
| `payload(input)` | `TMDataGridUrlPayload \| undefined` | The param's value in the grid's state shape (filters as `{ id, value: { operator, value } }` with string values), or `undefined` when the input sets nothing. |
|
|
70
|
+
| `validateSearch(search)` | `{ [key]?: TMDataGridUrlPayload }` | A route search validator: keeps the param when it is a plain object, drops anything else. The grid checks each slice itself on read. |
|
|
71
|
+
| `persist(storage?)` | `{ urlKey, urlStorage? }` | Spread into `persist`. Without `storage`, only `urlKey`, and the grid uses the browser's URL. |
|
|
72
|
+
|
|
73
|
+
`persist(storage)` takes a `TMDataGridUrlSearchStorage`,
|
|
74
|
+
`{ get(name): unknown; set(name, value: TMDataGridUrlPayload | undefined): void }`,
|
|
75
|
+
for a router that keeps objects in its search and encodes them itself, such as
|
|
76
|
+
TanStack Router. `get` returns the param's value or `undefined` (a string value
|
|
77
|
+
is passed to the grid unchanged); `set` receives the payload object, or
|
|
78
|
+
`undefined` to remove the param. The bridge converts with JSON, so leave
|
|
79
|
+
`serialize` and `deserialize` at their defaults with it.
|
|
80
|
+
|
|
81
|
+
```tsx
|
|
82
|
+
// TanStack Router: the route validates, the grid writes through the router.
|
|
83
|
+
const employeesRoute = createRoute({
|
|
84
|
+
getParentRoute: () => rootRoute,
|
|
85
|
+
path: "/employees",
|
|
86
|
+
validateSearch: employeesUrl.validateSearch,
|
|
87
|
+
component: EmployeesPage,
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
function EmployeesPage() {
|
|
91
|
+
const router = useRouter();
|
|
92
|
+
const persist = useMemo(
|
|
93
|
+
() =>
|
|
94
|
+
employeesUrl.persist({
|
|
95
|
+
get: (name) => (router.state.location.search as Record<string, unknown>)[name],
|
|
96
|
+
set: (name, value) =>
|
|
97
|
+
void router.navigate({
|
|
98
|
+
to: ".",
|
|
99
|
+
search: (prev: Record<string, unknown>) => ({ ...prev, [name]: value }),
|
|
100
|
+
replace: true,
|
|
101
|
+
}),
|
|
102
|
+
}),
|
|
103
|
+
[router],
|
|
104
|
+
);
|
|
105
|
+
const grid = useTMDataGrid({ data, columns, persist });
|
|
106
|
+
// ...
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
<Link
|
|
110
|
+
to="/employees"
|
|
111
|
+
search={employeesUrl.search({
|
|
112
|
+
filters: { salary: { operator: "between", value: [40_000, 60_000] } },
|
|
113
|
+
})}
|
|
114
|
+
/>;
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
- A route with its own search params spreads the validator:
|
|
118
|
+
`validateSearch: (s) => ({ ...employeesUrl.validateSearch(s), tab: ... })`.
|
|
119
|
+
- With `createHashHistory()` the param sits after the `#` with the rest of the
|
|
120
|
+
router's search.
|
|
121
|
+
- For URLs without percent escapes, give the router `parseSearch:
|
|
122
|
+
parseSearchWith(parse)` from `jsurl2`, and a `stringifySearch` that encodes
|
|
123
|
+
each object value with JSURL2's `stringify` and joins the pairs with
|
|
124
|
+
`encodeURIComponent`. TanStack Router's own `stringifySearchWith(stringify)`
|
|
125
|
+
joins them with `URLSearchParams`, which escapes JSURL2's `(`, `)`, `~` and
|
|
126
|
+
`!` again. JSURL2's `parse` also reads JSON, so older links still open. The
|
|
127
|
+
grid is unaffected. The full `stringifySearch` is on the
|
|
128
|
+
`typed-deep-links.md` docs page.
|
|
129
|
+
- The grid reads its param once, on mount. A link to the route the grid is
|
|
130
|
+
already on changes the URL but not the grid; link from another route, or
|
|
131
|
+
remount the grid on that navigation.
|
|
132
|
+
|
|
133
|
+
Source: `packages/tmdatagrid/docs/persistence.md` (Typed links) and
|
|
134
|
+
`packages/tmdatagrid/docs/typed-deep-links.md`.
|
package/skills/rows/SKILL.md
CHANGED
|
@@ -17,7 +17,7 @@ description: >
|
|
|
17
17
|
metadata:
|
|
18
18
|
type: core
|
|
19
19
|
library: '@jielga/tmdatagrid'
|
|
20
|
-
library_version: '2.
|
|
20
|
+
library_version: '2.4.0'
|
|
21
21
|
sources:
|
|
22
22
|
- 'Jielga/react:packages/tmdatagrid/docs/row-selection.md'
|
|
23
23
|
- 'Jielga/react:packages/tmdatagrid/docs/row-interaction.md'
|
|
@@ -15,7 +15,7 @@ description: >
|
|
|
15
15
|
metadata:
|
|
16
16
|
type: core
|
|
17
17
|
library: '@jielga/tmdatagrid'
|
|
18
|
-
library_version: '2.
|
|
18
|
+
library_version: '2.4.0'
|
|
19
19
|
sources:
|
|
20
20
|
- 'Jielga/react:packages/tmdatagrid/docs/server-side.md'
|
|
21
21
|
- 'Jielga/react:packages/tmdatagrid/docs/server-query.md'
|
package/skills/testing/SKILL.md
CHANGED
|
@@ -12,7 +12,7 @@ description: >
|
|
|
12
12
|
metadata:
|
|
13
13
|
type: core
|
|
14
14
|
library: '@jielga/tmdatagrid'
|
|
15
|
-
library_version: '2.
|
|
15
|
+
library_version: '2.4.0'
|
|
16
16
|
sources:
|
|
17
17
|
- 'Jielga/react:packages/tmdatagrid/docs/testing.md'
|
|
18
18
|
- 'Jielga/react:packages/tmdatagrid/test/gallery/main.tsx'
|
|
@@ -2,6 +2,7 @@ import { Box, type BoxProps, Button, Pill, UnstyledButton } from "@mantine/core"
|
|
|
2
2
|
import type { RowData } from "@tanstack/react-table";
|
|
3
3
|
import { useSelector } from "@tanstack/react-store";
|
|
4
4
|
import classes from "./TMDataGridFilterPills.module.css";
|
|
5
|
+
import { resolveColumnOptions } from "../core/columnOptions";
|
|
5
6
|
import { getColumnLabel, getColumnType } from "../core/columnUtils";
|
|
6
7
|
import {
|
|
7
8
|
formatFilterLabel,
|
|
@@ -9,7 +10,12 @@ import {
|
|
|
9
10
|
isTMDataGridFilterValue,
|
|
10
11
|
} from "../core/filterOperators";
|
|
11
12
|
import type { TMDataGridSize } from "../core/sizes";
|
|
12
|
-
import {
|
|
13
|
+
import type { TMDataGridRowData } from "../TMDataGridContext";
|
|
14
|
+
import {
|
|
15
|
+
openColumnFilter,
|
|
16
|
+
type TMDataGridApi,
|
|
17
|
+
type TMDataGridTable,
|
|
18
|
+
} from "../useTMDataGrid";
|
|
13
19
|
|
|
14
20
|
/** Mantine's style props (`mb="sm"`, `hiddenFrom="sm"`) are set on the wrapper. */
|
|
15
21
|
export type TMDataGridFilterPillsProps<TData extends RowData> = BoxProps & {
|
|
@@ -55,6 +61,9 @@ export function TMDataGridFilterPills<TData extends RowData>({
|
|
|
55
61
|
...others
|
|
56
62
|
}: TMDataGridFilterPillsProps<TData>) {
|
|
57
63
|
const { table, labels } = api;
|
|
64
|
+
// The options readers take the row-type-erased table, like the rest of the
|
|
65
|
+
// chrome does through context.
|
|
66
|
+
const erased = table as unknown as TMDataGridTable<TMDataGridRowData>;
|
|
58
67
|
const columnFilters = useSelector(
|
|
59
68
|
table.store,
|
|
60
69
|
(state) => state.columnFilters,
|
|
@@ -76,6 +85,7 @@ export function TMDataGridFilterPills<TData extends RowData>({
|
|
|
76
85
|
<Pill.Group size={size}>
|
|
77
86
|
{activeFilters.map((filter) => {
|
|
78
87
|
const column = table.getColumn(filter.id);
|
|
88
|
+
const erasedColumn = erased.getColumn(filter.id);
|
|
79
89
|
const label = column ? getColumnLabel(column) : filter.id;
|
|
80
90
|
const value = filter.value;
|
|
81
91
|
// isFilterActive already vouched for the shape; this narrows it.
|
|
@@ -125,6 +135,10 @@ export function TMDataGridFilterPills<TData extends RowData>({
|
|
|
125
135
|
type: column ? getColumnType(column) : "string",
|
|
126
136
|
filter: value,
|
|
127
137
|
operatorLabels: labels.operators,
|
|
138
|
+
options: erasedColumn
|
|
139
|
+
? resolveColumnOptions({ table: erased, column: erasedColumn })
|
|
140
|
+
: undefined,
|
|
141
|
+
labels,
|
|
128
142
|
})}
|
|
129
143
|
</UnstyledButton>
|
|
130
144
|
</Pill>
|
|
@@ -8,7 +8,8 @@ import { useSelector } from "@tanstack/react-store";
|
|
|
8
8
|
import { shallow } from "@tanstack/store";
|
|
9
9
|
import classes from "./TMDataGridGroupColumn.module.css";
|
|
10
10
|
import { ChevronRightIcon } from "./icons";
|
|
11
|
-
import {
|
|
11
|
+
import { resolveColumnOptions } from "../core/columnOptions";
|
|
12
|
+
import { getColumnLabel, getColumnType } from "../core/columnUtils";
|
|
12
13
|
import { formatGroupValue, getGroupDataRows } from "../core/grouping";
|
|
13
14
|
import type { TMDataGridFeatures, TMDataGridTable } from "../useTMDataGrid";
|
|
14
15
|
|
|
@@ -30,7 +31,7 @@ export function GroupCell<TData extends RowData>({
|
|
|
30
31
|
}: {
|
|
31
32
|
row: Row<TMDataGridFeatures, TData>;
|
|
32
33
|
}) {
|
|
33
|
-
const { labels } = useTMDataGridContext();
|
|
34
|
+
const { table, labels } = useTMDataGridContext();
|
|
34
35
|
const tabIndex = useBodyControlTabIndex();
|
|
35
36
|
const expanded = useSelector(row.table.store, () => row.getIsExpanded());
|
|
36
37
|
|
|
@@ -38,7 +39,21 @@ export function GroupCell<TData extends RowData>({
|
|
|
38
39
|
// the indent alone is what places them under their group.
|
|
39
40
|
if (!row.getIsGrouped()) return null;
|
|
40
41
|
|
|
41
|
-
|
|
42
|
+
// `groupingValue` is TanStack's map key, the value already turned into a
|
|
43
|
+
// string: `true` arrives as "true", a Date as its English timestamp, `null`
|
|
44
|
+
// as "null". The first record's grouping value is the value itself.
|
|
45
|
+
const columnId = row.groupingColumnId;
|
|
46
|
+
const column = columnId === undefined ? undefined : table.getColumn(columnId);
|
|
47
|
+
const dataRows = getGroupDataRows(row);
|
|
48
|
+
const value =
|
|
49
|
+
columnId === undefined || dataRows[0] === undefined
|
|
50
|
+
? row.groupingValue
|
|
51
|
+
: dataRows[0].getGroupingValue(columnId);
|
|
52
|
+
const label = formatGroupValue(value, labels.blankGroupValue, {
|
|
53
|
+
type: column ? getColumnType(column) : undefined,
|
|
54
|
+
options: column ? resolveColumnOptions({ table, column }) : undefined,
|
|
55
|
+
labels,
|
|
56
|
+
});
|
|
42
57
|
|
|
43
58
|
return (
|
|
44
59
|
<UnstyledButton
|
|
@@ -68,7 +83,7 @@ export function GroupCell<TData extends RowData>({
|
|
|
68
83
|
{/* Data rows, not direct children and not `getLeafRows()`: a nested group
|
|
69
84
|
counts the records under it, never the sub-groups in between. */}
|
|
70
85
|
<span className={classes.groupCount}>
|
|
71
|
-
({
|
|
86
|
+
({dataRows.length})
|
|
72
87
|
</span>
|
|
73
88
|
</UnstyledButton>
|
|
74
89
|
);
|
|
@@ -268,7 +268,13 @@ export function TMDataGridHeaderFilterRow({
|
|
|
268
268
|
<div
|
|
269
269
|
key={header.id}
|
|
270
270
|
role="columnheader"
|
|
271
|
-
|
|
271
|
+
// Named only where there is a filter: a generated lane or a column
|
|
272
|
+
// that does not filter holds no control to name.
|
|
273
|
+
aria-label={
|
|
274
|
+
column.getCanFilter()
|
|
275
|
+
? labels.filterOn(getColumnLabel(column))
|
|
276
|
+
: undefined
|
|
277
|
+
}
|
|
272
278
|
data-dg-part="header-filter-cell"
|
|
273
279
|
data-column-id={column.id}
|
|
274
280
|
data-control-column={isControlColumn(column.id) || undefined}
|
|
@@ -6,6 +6,9 @@ import type {
|
|
|
6
6
|
TableFeatures,
|
|
7
7
|
} from "@tanstack/react-table";
|
|
8
8
|
import type { TMDataGridFeatures, TMDataGridTable } from "../useTMDataGrid";
|
|
9
|
+
import type { TMDataGridOption } from "./columnOptions";
|
|
10
|
+
import type { TMDataGridLabels } from "./labels";
|
|
11
|
+
import { formatValueText } from "./valueText";
|
|
9
12
|
|
|
10
13
|
/**
|
|
11
14
|
* The value shape stored in `columnFilters` for every TMDataGrid column.
|
|
@@ -75,7 +78,7 @@ export const FILTER_OPERATOR_LABELS: Record<TMDataGridFilterOperator, string> =
|
|
|
75
78
|
isNotEmpty: "is not empty",
|
|
76
79
|
};
|
|
77
80
|
|
|
78
|
-
const STRING_OPERATORS
|
|
81
|
+
const STRING_OPERATORS = [
|
|
79
82
|
"contains",
|
|
80
83
|
"equals",
|
|
81
84
|
"notEquals",
|
|
@@ -83,9 +86,9 @@ const STRING_OPERATORS: readonly TMDataGridFilterOperator[] = [
|
|
|
83
86
|
"endsWith",
|
|
84
87
|
"isEmpty",
|
|
85
88
|
"isNotEmpty",
|
|
86
|
-
];
|
|
89
|
+
] as const satisfies readonly TMDataGridFilterOperator[];
|
|
87
90
|
|
|
88
|
-
const NUMBER_OPERATORS
|
|
91
|
+
const NUMBER_OPERATORS = [
|
|
89
92
|
"equals",
|
|
90
93
|
"notEquals",
|
|
91
94
|
"greaterThan",
|
|
@@ -95,12 +98,12 @@ const NUMBER_OPERATORS: readonly TMDataGridFilterOperator[] = [
|
|
|
95
98
|
"between",
|
|
96
99
|
"isEmpty",
|
|
97
100
|
"isNotEmpty",
|
|
98
|
-
];
|
|
101
|
+
] as const satisfies readonly TMDataGridFilterOperator[];
|
|
99
102
|
|
|
100
103
|
// `equals` doubles as a date's "is": it compares by calendar day whenever the
|
|
101
104
|
// cell holds a Date (see matchesFilter), so a separate `is` operator would be
|
|
102
105
|
// the same comparison under a second name.
|
|
103
|
-
const DATE_OPERATORS
|
|
106
|
+
const DATE_OPERATORS = [
|
|
104
107
|
"equals",
|
|
105
108
|
"notEquals",
|
|
106
109
|
"before",
|
|
@@ -110,23 +113,23 @@ const DATE_OPERATORS: readonly TMDataGridFilterOperator[] = [
|
|
|
110
113
|
"between",
|
|
111
114
|
"isEmpty",
|
|
112
115
|
"isNotEmpty",
|
|
113
|
-
];
|
|
116
|
+
] as const satisfies readonly TMDataGridFilterOperator[];
|
|
114
117
|
|
|
115
|
-
const BOOLEAN_OPERATORS
|
|
118
|
+
const BOOLEAN_OPERATORS = [
|
|
116
119
|
"equals",
|
|
117
120
|
"notEquals",
|
|
118
121
|
"isEmpty",
|
|
119
122
|
"isNotEmpty",
|
|
120
|
-
];
|
|
123
|
+
] as const satisfies readonly TMDataGridFilterOperator[];
|
|
121
124
|
|
|
122
125
|
// One set for both select types: the operators are about membership in the
|
|
123
126
|
// filter's set, which reads the same whether the cell holds one value or many.
|
|
124
|
-
const SELECT_OPERATORS
|
|
127
|
+
const SELECT_OPERATORS = [
|
|
125
128
|
"isAnyOf",
|
|
126
129
|
"isNoneOf",
|
|
127
130
|
"isEmpty",
|
|
128
131
|
"isNotEmpty",
|
|
129
|
-
];
|
|
132
|
+
] as const satisfies readonly TMDataGridFilterOperator[];
|
|
130
133
|
|
|
131
134
|
/** Operators that ignore the value input (the input is disabled for these). */
|
|
132
135
|
const VALUELESS_OPERATORS: readonly TMDataGridFilterOperator[] = [
|
|
@@ -143,16 +146,27 @@ const ARRAY_OPERATORS: readonly TMDataGridFilterOperator[] = [
|
|
|
143
146
|
/** Operators whose value is a `[min, max]` pair. */
|
|
144
147
|
const RANGE_OPERATORS: readonly TMDataGridFilterOperator[] = ["between"];
|
|
145
148
|
|
|
146
|
-
const OPERATORS_BY_TYPE
|
|
147
|
-
TMDataGridColumnType,
|
|
148
|
-
readonly TMDataGridFilterOperator[]
|
|
149
|
-
> = {
|
|
149
|
+
const OPERATORS_BY_TYPE = {
|
|
150
150
|
string: STRING_OPERATORS,
|
|
151
151
|
number: NUMBER_OPERATORS,
|
|
152
152
|
boolean: BOOLEAN_OPERATORS,
|
|
153
153
|
date: DATE_OPERATORS,
|
|
154
154
|
select: SELECT_OPERATORS,
|
|
155
155
|
multiSelect: SELECT_OPERATORS,
|
|
156
|
+
} as const satisfies Record<
|
|
157
|
+
TMDataGridColumnType,
|
|
158
|
+
readonly TMDataGridFilterOperator[]
|
|
159
|
+
>;
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* The operators each column type offers, as a type: the same lists the filter
|
|
163
|
+
* panel offers, so a typed link can never name an operator the panel lacks.
|
|
164
|
+
*
|
|
165
|
+
* @internal Feeds the typed links in urlState.ts; not exported from the
|
|
166
|
+
* package.
|
|
167
|
+
*/
|
|
168
|
+
export type OperatorsByColumnType = {
|
|
169
|
+
[TType in TMDataGridColumnType]: (typeof OPERATORS_BY_TYPE)[TType][number];
|
|
156
170
|
};
|
|
157
171
|
|
|
158
172
|
const DEFAULT_OPERATOR_BY_TYPE: Record<
|
|
@@ -307,30 +321,39 @@ export function activeColumnFilters<TData extends RowData>(
|
|
|
307
321
|
*
|
|
308
322
|
* The type's default operator is left implicit - "First name: Sofia" reads the
|
|
309
323
|
* way a person would say it - while any other operator is spelled out, since
|
|
310
|
-
* that is the part a reader cannot guess.
|
|
324
|
+
* that is the part a reader cannot guess. A value with an option reads as the
|
|
325
|
+
* option's label, and a boolean as `labels.booleanTrue` / `booleanFalse`.
|
|
311
326
|
*/
|
|
312
327
|
export function formatFilterLabel({
|
|
313
328
|
label,
|
|
314
329
|
type,
|
|
315
330
|
filter,
|
|
316
331
|
operatorLabels = FILTER_OPERATOR_LABELS,
|
|
332
|
+
options,
|
|
333
|
+
labels,
|
|
317
334
|
}: {
|
|
318
335
|
label: string;
|
|
319
336
|
type: TMDataGridColumnType;
|
|
320
337
|
filter: TMDataGridFilterValue;
|
|
321
338
|
/** Localized operator names - `labels.operators`. Defaults to English. */
|
|
322
339
|
operatorLabels?: Record<TMDataGridFilterOperator, string>;
|
|
340
|
+
/** The column's options, so a value reads as its option's label. */
|
|
341
|
+
options?: ReadonlyArray<TMDataGridOption>;
|
|
342
|
+
/** What a boolean value reads as. Defaults to English. */
|
|
343
|
+
labels?: Pick<TMDataGridLabels, "booleanTrue" | "booleanFalse">;
|
|
323
344
|
}): string {
|
|
324
345
|
const { operator, value } = filter;
|
|
325
346
|
if (!operatorNeedsValue(operator)) {
|
|
326
347
|
return `${label} ${operatorLabels[operator]}`;
|
|
327
348
|
}
|
|
349
|
+
const valueText = (entry: string) =>
|
|
350
|
+
formatValueText(String(entry).trim(), { type, options, labels });
|
|
328
351
|
const text =
|
|
329
352
|
operator === "between" && Array.isArray(value)
|
|
330
|
-
? value.map(
|
|
353
|
+
? value.map(valueText).join("–")
|
|
331
354
|
: Array.isArray(value)
|
|
332
|
-
? value.join(", ")
|
|
333
|
-
: String(value)
|
|
355
|
+
? value.map(valueText).join(", ")
|
|
356
|
+
: valueText(String(value));
|
|
334
357
|
if (operator === getDefaultOperator(type)) return `${label}: ${text}`;
|
|
335
358
|
return `${label} ${operatorLabels[operator]} ${text}`;
|
|
336
359
|
}
|
package/src/core/grouping.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { Row, RowData } from "@tanstack/react-table";
|
|
2
2
|
import type { TMDataGridFeatures } from "../useTMDataGrid";
|
|
3
|
+
import { formatValueText, type TMDataGridValueTextOptions } from "./valueText";
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* The data rows under a group row, at any depth.
|
|
@@ -29,14 +30,21 @@ const BLANK_GROUP_LABEL = "(Blank)";
|
|
|
29
30
|
* Deliberately not the grouped column's own `cell` renderer: that renderer is
|
|
30
31
|
* written for a data row and is free to reach into `row.original`, which on a
|
|
31
32
|
* group row is the first leaf's record rather than anything about the group.
|
|
33
|
+
* `options` carries what the chrome knows of the column instead - its type,
|
|
34
|
+
* its option labels and the boolean labels.
|
|
32
35
|
*/
|
|
33
36
|
export function formatGroupValue(
|
|
34
37
|
value: unknown,
|
|
35
38
|
blankLabel = BLANK_GROUP_LABEL,
|
|
39
|
+
options?: TMDataGridValueTextOptions,
|
|
36
40
|
): string {
|
|
37
|
-
if (
|
|
41
|
+
if (
|
|
42
|
+
value === null ||
|
|
43
|
+
value === undefined ||
|
|
44
|
+
value === "" ||
|
|
45
|
+
(Array.isArray(value) && value.length === 0)
|
|
46
|
+
) {
|
|
38
47
|
return blankLabel;
|
|
39
48
|
}
|
|
40
|
-
|
|
41
|
-
return String(value);
|
|
49
|
+
return formatValueText(value, options);
|
|
42
50
|
}
|
package/src/core/labels.ts
CHANGED
|
@@ -2,6 +2,7 @@ import {
|
|
|
2
2
|
FILTER_OPERATOR_LABELS,
|
|
3
3
|
type TMDataGridFilterOperator,
|
|
4
4
|
} from "./filterOperators";
|
|
5
|
+
import { BOOLEAN_LABELS_EN } from "./valueText";
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* Every user-facing string in the grid - menu items, panels, tooltips, the
|
|
@@ -55,7 +56,7 @@ export type TMDataGridLabels = {
|
|
|
55
56
|
filterTo: string;
|
|
56
57
|
/** `DgTriStateFilter`'s no-filter segment. */
|
|
57
58
|
filterAll: string;
|
|
58
|
-
/** What a boolean
|
|
59
|
+
/** What a boolean `true` reads as - its filter choice, filter pill and group row. */
|
|
59
60
|
booleanTrue: string;
|
|
60
61
|
booleanFalse: string;
|
|
61
62
|
addFilter: string;
|
|
@@ -225,8 +226,8 @@ export const TMDATAGRID_LABELS_EN: TMDataGridLabels = {
|
|
|
225
226
|
filterFrom: "From",
|
|
226
227
|
filterTo: "To",
|
|
227
228
|
filterAll: "All",
|
|
228
|
-
booleanTrue:
|
|
229
|
-
booleanFalse:
|
|
229
|
+
booleanTrue: BOOLEAN_LABELS_EN.booleanTrue,
|
|
230
|
+
booleanFalse: BOOLEAN_LABELS_EN.booleanFalse,
|
|
230
231
|
addFilter: "Add filter",
|
|
231
232
|
clearAllFilters: "Clear all",
|
|
232
233
|
closeFilters: "Close filters",
|
package/src/core/persistence.ts
CHANGED
|
@@ -515,7 +515,15 @@ export function collectUrlExcludedColumnIds(
|
|
|
515
515
|
return into;
|
|
516
516
|
}
|
|
517
517
|
|
|
518
|
-
|
|
518
|
+
/**
|
|
519
|
+
* Visits every leaf column definition under `defs` with the id TanStack gives
|
|
520
|
+
* it: `id`, else the `accessorKey` with dots replaced by underscores, else a
|
|
521
|
+
* string `header`.
|
|
522
|
+
*
|
|
523
|
+
* @internal Shared by persistence and the typed links in urlState.ts; not
|
|
524
|
+
* exported from the package.
|
|
525
|
+
*/
|
|
526
|
+
export function forEachLeafColumnDef(
|
|
519
527
|
defs: ReadonlyArray<unknown>,
|
|
520
528
|
visit: (id: string, record: Record<string, unknown>) => void,
|
|
521
529
|
): void {
|