@jielga/tmdatagrid 2.3.0 → 2.5.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 +289 -9
- package/dist/index.js +1418 -1280
- package/dist/index.js.map +1 -1
- package/docs/columns.md +8 -0
- package/docs/editors.md +31 -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 +5 -2
- package/skills/columns/references/columns-api.md +1 -0
- package/skills/data/SKILL.md +1 -1
- package/skills/editing/SKILL.md +15 -2
- package/skills/editing/references/editing-api.md +2 -1
- package/skills/editing/references/editors-and-validation.md +24 -0
- 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/editEngine.ts +101 -5
- 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 +15 -0
- package/src/useTMDataGrid.tsx +51 -8
|
@@ -15,7 +15,7 @@ description: >
|
|
|
15
15
|
metadata:
|
|
16
16
|
type: lifecycle
|
|
17
17
|
library: '@jielga/tmdatagrid'
|
|
18
|
-
library_version: '2.
|
|
18
|
+
library_version: '2.5.0'
|
|
19
19
|
sources:
|
|
20
20
|
- 'Jielga/react:packages/tmdatagrid/docs/migrating-to-2.md'
|
|
21
21
|
- 'Jielga/react:packages/tmdatagrid/src/index.ts'
|
package/skills/options/SKILL.md
CHANGED
|
@@ -10,20 +10,22 @@ description: >
|
|
|
10
10
|
enablePagination, resetPageOnQueryChange, enableRowNumbers, selectionMode,
|
|
11
11
|
showSelectedBackground, cellSelection, renderDetails, overscan,
|
|
12
12
|
quickSearchMode), persist with dataKey/settingsKey slices, storageMode and
|
|
13
|
-
urlKey deep links, the returned api (table, ui,
|
|
14
|
-
resetSettings, scrollToRow), ui actions, and reading
|
|
15
|
-
useSelector over table.store, since table.getState() does not
|
|
16
|
-
Load when configuring the hook, looking up an option's default,
|
|
17
|
-
layout or filters, or reacting to grid state from a parent.
|
|
13
|
+
urlKey and createTMDataGridUrlState deep links, the returned api (table, ui,
|
|
14
|
+
edit, features, labels, resetSettings, scrollToRow), ui actions, and reading
|
|
15
|
+
grid state with useSelector over table.store, since table.getState() does not
|
|
16
|
+
exist in v9. Load when configuring the hook, looking up an option's default,
|
|
17
|
+
persisting layout or filters, or reacting to grid state from a parent.
|
|
18
18
|
metadata:
|
|
19
19
|
type: core
|
|
20
20
|
library: '@jielga/tmdatagrid'
|
|
21
|
-
library_version: '2.
|
|
21
|
+
library_version: '2.5.0'
|
|
22
22
|
sources:
|
|
23
23
|
- 'Jielga/react:packages/tmdatagrid/docs/use-tm-data-grid.md'
|
|
24
24
|
- 'Jielga/react:packages/tmdatagrid/docs/persistence.md'
|
|
25
|
+
- 'Jielga/react:packages/tmdatagrid/docs/typed-deep-links.md'
|
|
25
26
|
- 'Jielga/react:packages/tmdatagrid/src/useTMDataGrid.tsx'
|
|
26
27
|
- 'Jielga/react:packages/tmdatagrid/src/core/persistence.ts'
|
|
28
|
+
- 'Jielga/react:packages/tmdatagrid/src/core/urlState.ts'
|
|
27
29
|
---
|
|
28
30
|
|
|
29
31
|
# TMDataGrid - useTMDataGrid
|
|
@@ -298,6 +300,28 @@ const persist = useMemo(
|
|
|
298
300
|
|
|
299
301
|
Source: `packages/tmdatagrid/docs/persistence.md` (Deep links).
|
|
300
302
|
|
|
303
|
+
### Typed links
|
|
304
|
+
|
|
305
|
+
`createTMDataGridUrlState({ key, columns, slices? })` defines a grid's deep
|
|
306
|
+
links from the columns the grid renders, built with
|
|
307
|
+
`createTMDataGridColumnHelper`. A link can name only the filters, operators,
|
|
308
|
+
values and sorts those columns offer; anything else is a type error. The
|
|
309
|
+
definition gives `urlKey` for `persist`, the link as `searchParams`, `search`
|
|
310
|
+
or `payload`, a route `validateSearch`, and `persist(storage?)`, which keeps
|
|
311
|
+
the param in a router's search such as TanStack Router's.
|
|
312
|
+
|
|
313
|
+
```tsx
|
|
314
|
+
export const employeesUrl = createTMDataGridUrlState({ key: "employees", columns });
|
|
315
|
+
|
|
316
|
+
const persist = { urlKey: employeesUrl.urlKey } satisfies TMDataGridPersistence;
|
|
317
|
+
const href = `/employees?${employeesUrl.searchParams({
|
|
318
|
+
filters: { department: { operator: "isAnyOf", value: ["Design"] } },
|
|
319
|
+
})}`;
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
Every rule, every member and the TanStack Router setup are in
|
|
323
|
+
[references/typed-links.md](references/typed-links.md).
|
|
324
|
+
|
|
301
325
|
## Return value
|
|
302
326
|
|
|
303
327
|
| Field | Type | Description |
|
|
@@ -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.5.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.5.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.5.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}
|
package/src/core/editEngine.ts
CHANGED
|
@@ -367,6 +367,28 @@ export type TMDataGridEditValueMap = (
|
|
|
367
367
|
args: TMDataGridEditValueMapArgs,
|
|
368
368
|
) => unknown;
|
|
369
369
|
|
|
370
|
+
/** What `meta.edit.mapCommitValue` is handed for one field of a row being committed. */
|
|
371
|
+
export type TMDataGridEditCommitMapArgs = {
|
|
372
|
+
/** The value the field holds as the row is committed. */
|
|
373
|
+
value: unknown;
|
|
374
|
+
/** What the field held when editing began. */
|
|
375
|
+
previous: unknown;
|
|
376
|
+
/** The row being committed. `undefined` for a row that is being added. */
|
|
377
|
+
row: ErasedRow | undefined;
|
|
378
|
+
column: ErasedColumn;
|
|
379
|
+
table: TMDataGridTable<TMDataGridRowData>;
|
|
380
|
+
};
|
|
381
|
+
|
|
382
|
+
/**
|
|
383
|
+
* `meta.edit.mapCommitValue` - the value as the row commits, mapped.
|
|
384
|
+
*
|
|
385
|
+
* Runs once per field, not per keystroke, so the user can type what the
|
|
386
|
+
* mapped value would not allow, like a trailing space.
|
|
387
|
+
*/
|
|
388
|
+
export type TMDataGridEditCommitMap = (
|
|
389
|
+
args: TMDataGridEditCommitMapArgs,
|
|
390
|
+
) => unknown;
|
|
391
|
+
|
|
370
392
|
/**
|
|
371
393
|
* `meta.edit` - everything about how this column is edited, in one place,
|
|
372
394
|
* mirroring the `edit.*` engine the grid exposes at runtime.
|
|
@@ -419,6 +441,14 @@ export type TMDataGridColumnEditOptions<
|
|
|
419
441
|
* {@link TMDataGridEditValueMap}.
|
|
420
442
|
*/
|
|
421
443
|
mapValue?: TMDataGridEditValueMap;
|
|
444
|
+
/**
|
|
445
|
+
* Maps the value of each field the user changed, once, as its row commits -
|
|
446
|
+
* trim a string the user was free to type spaces into. It runs before the
|
|
447
|
+
* validators of the submit, and its result is written back into the form,
|
|
448
|
+
* so the draft, `changes` and `onCommit` hold the mapped value. See
|
|
449
|
+
* {@link TMDataGridEditCommitMap}.
|
|
450
|
+
*/
|
|
451
|
+
mapCommitValue?: TMDataGridEditCommitMap;
|
|
422
452
|
};
|
|
423
453
|
|
|
424
454
|
/** A new row being committed - `onRowAdd`, and `saveDrafts`'s `created`. */
|
|
@@ -1822,7 +1852,64 @@ export function createEditEngine(
|
|
|
1822
1852
|
);
|
|
1823
1853
|
};
|
|
1824
1854
|
|
|
1825
|
-
|
|
1855
|
+
/**
|
|
1856
|
+
* Runs every `meta.edit.mapCommitValue` over the row's form and writes the
|
|
1857
|
+
* results back. An open row maps the fields the user changed - a value that
|
|
1858
|
+
* was already stored is not the user's to have rewritten; a row being added
|
|
1859
|
+
* has no stored value, so every editable field of it is mapped. A field in
|
|
1860
|
+
* `callerFields` holds a value the caller wrote, not the user, and is left
|
|
1861
|
+
* as written. Written without validation: the submit that follows judges
|
|
1862
|
+
* the mapped values.
|
|
1863
|
+
*/
|
|
1864
|
+
const applyCommitMaps = (
|
|
1865
|
+
entry: FormEntry,
|
|
1866
|
+
rowId: string,
|
|
1867
|
+
callerFields: ReadonlySet<string>,
|
|
1868
|
+
) => {
|
|
1869
|
+
const { table } = getContext();
|
|
1870
|
+
const values = entry.form.state.values as TMDataGridRowData;
|
|
1871
|
+
const changed = entry.isNew
|
|
1872
|
+
? null
|
|
1873
|
+
: new Set(diff(entry.original, values).map((change) => change.columnId));
|
|
1874
|
+
const row = entry.isNew ? undefined : getRow(rowId);
|
|
1875
|
+
for (const column of editableColumns()) {
|
|
1876
|
+
const map = column.columnDef.meta?.edit?.mapCommitValue;
|
|
1877
|
+
if (map === undefined) continue;
|
|
1878
|
+
if (changed !== null && !changed.has(column.id)) continue;
|
|
1879
|
+
const field = getEditFieldName(column);
|
|
1880
|
+
if (field === null || callerFields.has(field)) continue;
|
|
1881
|
+
const value: unknown = getBy(
|
|
1882
|
+
entry.form.state.values as TMDataGridRowData,
|
|
1883
|
+
field,
|
|
1884
|
+
);
|
|
1885
|
+
const mapped = map({
|
|
1886
|
+
value,
|
|
1887
|
+
previous: getBy(entry.original, field),
|
|
1888
|
+
row,
|
|
1889
|
+
column,
|
|
1890
|
+
table,
|
|
1891
|
+
});
|
|
1892
|
+
if (sameValue(value, mapped)) continue;
|
|
1893
|
+
entry.form.setFieldValue(field as never, mapped as never, {
|
|
1894
|
+
dontValidate: true,
|
|
1895
|
+
});
|
|
1896
|
+
}
|
|
1897
|
+
};
|
|
1898
|
+
|
|
1899
|
+
const commit = (rowId: string): Promise<boolean> =>
|
|
1900
|
+
commitRow(rowId, new Set());
|
|
1901
|
+
|
|
1902
|
+
/**
|
|
1903
|
+
* `callerFields` names the fields a caller wrote itself, which no editor
|
|
1904
|
+
* touched - `writeFields` passes the ones it writes, the rows of
|
|
1905
|
+
* `addRows({ commit: true })` pass `"all"`. They hold stored values already,
|
|
1906
|
+
* as `meta.edit.mapValue` has always assumed, so `mapCommitValue` leaves
|
|
1907
|
+
* them alone; every other field the user changed is still mapped.
|
|
1908
|
+
*/
|
|
1909
|
+
const commitRow = async (
|
|
1910
|
+
rowId: string,
|
|
1911
|
+
callerFields: ReadonlySet<string> | "all",
|
|
1912
|
+
): Promise<boolean> => {
|
|
1826
1913
|
const entry = forms.get(rowId);
|
|
1827
1914
|
// No form: the row is committed already, or the grid never held it.
|
|
1828
1915
|
// Either way there is nothing left to decide.
|
|
@@ -1830,6 +1917,9 @@ export function createEditEngine(
|
|
|
1830
1917
|
// Enter and blur race on the same edit; the second caller joins the
|
|
1831
1918
|
// first's commit instead of submitting the row twice.
|
|
1832
1919
|
if (entry.pendingCommit !== null) return entry.pendingCommit;
|
|
1920
|
+
// Before the pristine check: a value mapped back to the original leaves
|
|
1921
|
+
// nothing to commit.
|
|
1922
|
+
if (callerFields !== "all") applyCommitMaps(entry, rowId, callerFields);
|
|
1833
1923
|
// A pristine form has nothing to say - drop it without a consumer call.
|
|
1834
1924
|
// Not for a new row: adding an untouched entry is still an add.
|
|
1835
1925
|
if (
|
|
@@ -2030,10 +2120,14 @@ export function createEditEngine(
|
|
|
2030
2120
|
// validate side by side. Out to `onRowAdd`, one at a time and in
|
|
2031
2121
|
// order, so the consumer sees the rows as the file had them.
|
|
2032
2122
|
const results = getContext().draft
|
|
2033
|
-
? await Promise.all(
|
|
2123
|
+
? await Promise.all(
|
|
2124
|
+
tempIds.map((tempId) => commitRow(tempId, "all")),
|
|
2125
|
+
)
|
|
2034
2126
|
: [];
|
|
2035
2127
|
if (!getContext().draft) {
|
|
2036
|
-
for (const tempId of tempIds)
|
|
2128
|
+
for (const tempId of tempIds) {
|
|
2129
|
+
results.push(await commitRow(tempId, "all"));
|
|
2130
|
+
}
|
|
2037
2131
|
}
|
|
2038
2132
|
return splitCommitted(tempIds, results);
|
|
2039
2133
|
});
|
|
@@ -2382,7 +2476,9 @@ export function createEditEngine(
|
|
|
2382
2476
|
* round trip would stall every other row's markers and the caret.
|
|
2383
2477
|
*
|
|
2384
2478
|
* No editor is involved, so `meta.edit.mapValue` does not run - the caller
|
|
2385
|
-
* writes the stored value itself
|
|
2479
|
+
* writes the stored value itself - and `meta.edit.mapCommitValue` skips the
|
|
2480
|
+
* fields written here, while still mapping what the user typed elsewhere in
|
|
2481
|
+
* an open row. Validation is untouched: this is the same
|
|
2386
2482
|
* `commit`, so `meta.edit.validate` and `editing.rowValidators` still decide
|
|
2387
2483
|
* whether the row lands, and a failure leaves the row open carrying its
|
|
2388
2484
|
* errors.
|
|
@@ -2400,7 +2496,7 @@ export function createEditEngine(
|
|
|
2400
2496
|
for (const { field, value } of writes) {
|
|
2401
2497
|
entry.form.setFieldValue(field as never, value as never);
|
|
2402
2498
|
}
|
|
2403
|
-
return
|
|
2499
|
+
return commitRow(rowId, new Set(writes.map(({ field }) => field)));
|
|
2404
2500
|
};
|
|
2405
2501
|
const open = forms.get(rowId);
|
|
2406
2502
|
if (open !== undefined) return write(open);
|