@jielga/tmdatagrid 2.4.0 → 2.6.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 +96 -9
- package/dist/index.js +417 -348
- package/dist/index.js.map +1 -1
- package/docs/columns.md +1 -0
- package/docs/components.md +3 -1
- package/docs/editors.md +127 -6
- package/docs/testing.md +1 -0
- package/package.json +1 -1
- package/skills/appearance/SKILL.md +1 -1
- package/skills/cell-selection/SKILL.md +1 -1
- package/skills/columns/SKILL.md +2 -2
- package/skills/columns/references/columns-api.md +1 -0
- package/skills/data/SKILL.md +1 -1
- package/skills/editing/SKILL.md +28 -17
- package/skills/editing/references/editing-api.md +4 -2
- package/skills/editing/references/editors-and-validation.md +103 -4
- package/skills/filtering/SKILL.md +1 -1
- package/skills/getting-started/SKILL.md +1 -1
- package/skills/grouping/SKILL.md +1 -1
- package/skills/migrating-to-2/SKILL.md +1 -1
- package/skills/options/SKILL.md +1 -1
- 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 +4 -2
- package/src/components/editors/TMDataGridBooleanEditor.tsx +26 -4
- package/src/components/editors/TMDataGridDateEditor.tsx +33 -5
- package/src/components/editors/TMDataGridMultiSelectEditor.tsx +39 -6
- package/src/components/editors/TMDataGridNumberEditor.tsx +33 -6
- package/src/components/editors/TMDataGridSelectEditor.tsx +39 -6
- package/src/components/editors/TMDataGridStringEditor.tsx +33 -6
- package/src/components/editors/editorShared.ts +27 -1
- package/src/core/editEngine.ts +101 -5
- package/src/index.ts +26 -6
- package/src/useTMDataGrid.tsx +1 -1
|
@@ -1,9 +1,32 @@
|
|
|
1
|
-
import { TextInput } from "@mantine/core";
|
|
1
|
+
import { TextInput, type TextInputProps } from "@mantine/core";
|
|
2
2
|
import { useRef } from "react";
|
|
3
3
|
import type { TMDataGridEditorArgs } from "../../core/editEngine";
|
|
4
4
|
import { getColumnLabel } from "../../core/columnUtils";
|
|
5
5
|
import { useTMDataGridContext } from "../../TMDataGridContext";
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
useFieldInvalid,
|
|
8
|
+
useFieldValue,
|
|
9
|
+
type EditorDataAttributes,
|
|
10
|
+
} from "./editorShared";
|
|
11
|
+
|
|
12
|
+
export type TMDataGridDateEditorProps = TMDataGridEditorArgs & {
|
|
13
|
+
/**
|
|
14
|
+
* Props for the Mantine `TextInput`. The grid keeps `type`, the value,
|
|
15
|
+
* `onChange`, `error`, `size` and the accessible name; `onBlur` runs after
|
|
16
|
+
* the grid's own.
|
|
17
|
+
*/
|
|
18
|
+
inputProps?: Omit<
|
|
19
|
+
TextInputProps,
|
|
20
|
+
| "type"
|
|
21
|
+
| "value"
|
|
22
|
+
| "defaultValue"
|
|
23
|
+
| "onChange"
|
|
24
|
+
| "error"
|
|
25
|
+
| "size"
|
|
26
|
+
| "aria-label"
|
|
27
|
+
> &
|
|
28
|
+
EditorDataAttributes;
|
|
29
|
+
};
|
|
7
30
|
|
|
8
31
|
/**
|
|
9
32
|
* The built-in editor for `meta.type: "date"` - the native
|
|
@@ -14,7 +37,8 @@ export function TMDataGridDateEditor({
|
|
|
14
37
|
field,
|
|
15
38
|
column,
|
|
16
39
|
size,
|
|
17
|
-
|
|
40
|
+
inputProps,
|
|
41
|
+
}: TMDataGridDateEditorProps) {
|
|
18
42
|
const { labels } = useTMDataGridContext();
|
|
19
43
|
const value = useFieldValue(field);
|
|
20
44
|
const invalid = useFieldInvalid(field);
|
|
@@ -32,8 +56,9 @@ export function TMDataGridDateEditor({
|
|
|
32
56
|
|
|
33
57
|
return (
|
|
34
58
|
<TextInput
|
|
35
|
-
size={size}
|
|
36
59
|
w="100%"
|
|
60
|
+
{...inputProps}
|
|
61
|
+
size={size}
|
|
37
62
|
type="date"
|
|
38
63
|
aria-label={labels.editCell(getColumnLabel(column))}
|
|
39
64
|
data-dg-part="editor-input"
|
|
@@ -48,7 +73,10 @@ export function TMDataGridDateEditor({
|
|
|
48
73
|
writesDates.current ? new Date(`${next}T00:00:00`) : next,
|
|
49
74
|
);
|
|
50
75
|
}}
|
|
51
|
-
onBlur={
|
|
76
|
+
onBlur={(event) => {
|
|
77
|
+
field.handleBlur();
|
|
78
|
+
inputProps?.onBlur?.(event);
|
|
79
|
+
}}
|
|
52
80
|
error={invalid}
|
|
53
81
|
/>
|
|
54
82
|
);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { MultiSelect } from "@mantine/core";
|
|
1
|
+
import { MultiSelect, type MultiSelectProps } from "@mantine/core";
|
|
2
2
|
import type { TMDataGridEditorArgs } from "../../core/editEngine";
|
|
3
3
|
import {
|
|
4
4
|
optionsToComboboxData,
|
|
@@ -6,7 +6,35 @@ import {
|
|
|
6
6
|
} from "../../core/columnOptions";
|
|
7
7
|
import { getColumnLabel } from "../../core/columnUtils";
|
|
8
8
|
import { useTMDataGridContext } from "../../TMDataGridContext";
|
|
9
|
-
import {
|
|
9
|
+
import {
|
|
10
|
+
useFieldInvalid,
|
|
11
|
+
useFieldValue,
|
|
12
|
+
type EditorDataAttributes,
|
|
13
|
+
} from "./editorShared";
|
|
14
|
+
|
|
15
|
+
export type TMDataGridMultiSelectEditorProps = TMDataGridEditorArgs & {
|
|
16
|
+
/**
|
|
17
|
+
* Props for the Mantine `MultiSelect`. The grid keeps `data`, the value,
|
|
18
|
+
* `onChange`, `error`, `size`, the accessible name and
|
|
19
|
+
* `comboboxProps.withinPortal`; `onBlur` runs after the grid's own.
|
|
20
|
+
*/
|
|
21
|
+
inputProps?: Omit<
|
|
22
|
+
MultiSelectProps,
|
|
23
|
+
| "data"
|
|
24
|
+
| "value"
|
|
25
|
+
| "defaultValue"
|
|
26
|
+
| "onChange"
|
|
27
|
+
| "error"
|
|
28
|
+
| "size"
|
|
29
|
+
| "aria-label"
|
|
30
|
+
| "comboboxProps"
|
|
31
|
+
> & {
|
|
32
|
+
comboboxProps?: Omit<
|
|
33
|
+
NonNullable<MultiSelectProps["comboboxProps"]>,
|
|
34
|
+
"withinPortal"
|
|
35
|
+
>;
|
|
36
|
+
} & EditorDataAttributes;
|
|
37
|
+
};
|
|
10
38
|
|
|
11
39
|
/**
|
|
12
40
|
* The built-in editor for `meta.type: "multiSelect"` - cells holding string
|
|
@@ -19,16 +47,18 @@ export function TMDataGridMultiSelectEditor({
|
|
|
19
47
|
row,
|
|
20
48
|
table,
|
|
21
49
|
size,
|
|
22
|
-
|
|
50
|
+
inputProps,
|
|
51
|
+
}: TMDataGridMultiSelectEditorProps) {
|
|
23
52
|
const { labels } = useTMDataGridContext();
|
|
24
53
|
const value = useFieldValue(field);
|
|
25
54
|
const invalid = useFieldInvalid(field);
|
|
26
55
|
return (
|
|
27
56
|
<MultiSelect
|
|
28
|
-
size={size}
|
|
29
57
|
w="100%"
|
|
30
58
|
searchable
|
|
31
|
-
|
|
59
|
+
{...inputProps}
|
|
60
|
+
size={size}
|
|
61
|
+
comboboxProps={{ ...inputProps?.comboboxProps, withinPortal: false }}
|
|
32
62
|
aria-label={labels.editCell(getColumnLabel(column))}
|
|
33
63
|
data-dg-part="editor-input"
|
|
34
64
|
data={optionsToComboboxData(
|
|
@@ -36,7 +66,10 @@ export function TMDataGridMultiSelectEditor({
|
|
|
36
66
|
)}
|
|
37
67
|
value={Array.isArray(value) ? value.map(String) : []}
|
|
38
68
|
onChange={(next) => field.handleChange(next)}
|
|
39
|
-
onBlur={
|
|
69
|
+
onBlur={(event) => {
|
|
70
|
+
field.handleBlur();
|
|
71
|
+
inputProps?.onBlur?.(event);
|
|
72
|
+
}}
|
|
40
73
|
error={invalid}
|
|
41
74
|
/>
|
|
42
75
|
);
|
|
@@ -1,17 +1,37 @@
|
|
|
1
|
-
import { NumberInput } from "@mantine/core";
|
|
1
|
+
import { NumberInput, type NumberInputProps } from "@mantine/core";
|
|
2
2
|
import { useState } from "react";
|
|
3
3
|
import type { TMDataGridEditorArgs } from "../../core/editEngine";
|
|
4
4
|
import { getColumnLabel } from "../../core/columnUtils";
|
|
5
5
|
import { useTMDataGridContext } from "../../TMDataGridContext";
|
|
6
6
|
import {
|
|
7
|
+
mergeRefs,
|
|
7
8
|
selectAllOnFocus,
|
|
8
9
|
useCaretKeeper,
|
|
9
10
|
useFieldInvalid,
|
|
10
11
|
useFieldValue,
|
|
12
|
+
type EditorDataAttributes,
|
|
11
13
|
} from "./editorShared";
|
|
12
14
|
|
|
15
|
+
export type TMDataGridNumberEditorProps = TMDataGridEditorArgs & {
|
|
16
|
+
/**
|
|
17
|
+
* Props for the Mantine `NumberInput`. The grid keeps the value, `onChange`,
|
|
18
|
+
* `error`, `size` and the accessible name; `onFocus` and `onBlur` run after
|
|
19
|
+
* the grid's own, and `ref` receives the input beside the grid's ref.
|
|
20
|
+
*/
|
|
21
|
+
inputProps?: Omit<
|
|
22
|
+
NumberInputProps,
|
|
23
|
+
"value" | "defaultValue" | "onChange" | "error" | "size" | "aria-label"
|
|
24
|
+
> &
|
|
25
|
+
EditorDataAttributes;
|
|
26
|
+
};
|
|
27
|
+
|
|
13
28
|
/** The built-in editor for `meta.type: "number"`. */
|
|
14
|
-
export function TMDataGridNumberEditor({
|
|
29
|
+
export function TMDataGridNumberEditor({
|
|
30
|
+
field,
|
|
31
|
+
column,
|
|
32
|
+
size,
|
|
33
|
+
inputProps,
|
|
34
|
+
}: TMDataGridNumberEditorProps) {
|
|
15
35
|
const { labels } = useTMDataGridContext();
|
|
16
36
|
const value = useFieldValue(field);
|
|
17
37
|
const invalid = useFieldInvalid(field);
|
|
@@ -31,11 +51,15 @@ export function TMDataGridNumberEditor({ field, column, size }: TMDataGridEditor
|
|
|
31
51
|
: value === null;
|
|
32
52
|
return (
|
|
33
53
|
<NumberInput
|
|
34
|
-
ref={inputRef}
|
|
35
|
-
size={size}
|
|
36
54
|
w="100%"
|
|
37
|
-
onFocus={selectAllOnFocus}
|
|
38
55
|
hideControls
|
|
56
|
+
{...inputProps}
|
|
57
|
+
ref={mergeRefs(inputRef, inputProps?.ref)}
|
|
58
|
+
size={size}
|
|
59
|
+
onFocus={(event) => {
|
|
60
|
+
selectAllOnFocus(event);
|
|
61
|
+
inputProps?.onFocus?.(event);
|
|
62
|
+
}}
|
|
39
63
|
aria-label={labels.editCell(getColumnLabel(column))}
|
|
40
64
|
data-dg-part="editor-input"
|
|
41
65
|
value={
|
|
@@ -63,7 +87,10 @@ export function TMDataGridNumberEditor({ field, column, size }: TMDataGridEditor
|
|
|
63
87
|
next !== "" && Number.isFinite(parsed) ? parsed : null,
|
|
64
88
|
);
|
|
65
89
|
}}
|
|
66
|
-
onBlur={
|
|
90
|
+
onBlur={(event) => {
|
|
91
|
+
field.handleBlur();
|
|
92
|
+
inputProps?.onBlur?.(event);
|
|
93
|
+
}}
|
|
67
94
|
error={invalid}
|
|
68
95
|
/>
|
|
69
96
|
);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Select } from "@mantine/core";
|
|
1
|
+
import { Select, type SelectProps } from "@mantine/core";
|
|
2
2
|
import type { TMDataGridEditorArgs } from "../../core/editEngine";
|
|
3
3
|
import {
|
|
4
4
|
optionsToComboboxData,
|
|
@@ -6,7 +6,35 @@ import {
|
|
|
6
6
|
} from "../../core/columnOptions";
|
|
7
7
|
import { getColumnLabel } from "../../core/columnUtils";
|
|
8
8
|
import { useTMDataGridContext } from "../../TMDataGridContext";
|
|
9
|
-
import {
|
|
9
|
+
import {
|
|
10
|
+
useFieldInvalid,
|
|
11
|
+
useFieldValue,
|
|
12
|
+
type EditorDataAttributes,
|
|
13
|
+
} from "./editorShared";
|
|
14
|
+
|
|
15
|
+
export type TMDataGridSelectEditorProps = TMDataGridEditorArgs & {
|
|
16
|
+
/**
|
|
17
|
+
* Props for the Mantine `Select`. The grid keeps `data`, the value,
|
|
18
|
+
* `onChange`, `error`, `size`, the accessible name and
|
|
19
|
+
* `comboboxProps.withinPortal`; `onBlur` runs after the grid's own.
|
|
20
|
+
*/
|
|
21
|
+
inputProps?: Omit<
|
|
22
|
+
SelectProps,
|
|
23
|
+
| "data"
|
|
24
|
+
| "value"
|
|
25
|
+
| "defaultValue"
|
|
26
|
+
| "onChange"
|
|
27
|
+
| "error"
|
|
28
|
+
| "size"
|
|
29
|
+
| "aria-label"
|
|
30
|
+
| "comboboxProps"
|
|
31
|
+
> & {
|
|
32
|
+
comboboxProps?: Omit<
|
|
33
|
+
NonNullable<SelectProps["comboboxProps"]>,
|
|
34
|
+
"withinPortal"
|
|
35
|
+
>;
|
|
36
|
+
} & EditorDataAttributes;
|
|
37
|
+
};
|
|
10
38
|
|
|
11
39
|
/**
|
|
12
40
|
* The built-in editor for `meta.type: "select"`. Options come from the same
|
|
@@ -20,18 +48,20 @@ export function TMDataGridSelectEditor({
|
|
|
20
48
|
table,
|
|
21
49
|
size,
|
|
22
50
|
commit,
|
|
23
|
-
|
|
51
|
+
inputProps,
|
|
52
|
+
}: TMDataGridSelectEditorProps) {
|
|
24
53
|
const { labels, features } = useTMDataGridContext();
|
|
25
54
|
const value = useFieldValue(field);
|
|
26
55
|
const invalid = useFieldInvalid(field);
|
|
27
56
|
return (
|
|
28
57
|
<Select
|
|
29
|
-
size={size}
|
|
30
58
|
w="100%"
|
|
31
59
|
searchable
|
|
60
|
+
{...inputProps}
|
|
61
|
+
size={size}
|
|
32
62
|
// Inside the cell, so picking an option is not a click-away - the same
|
|
33
63
|
// reason the filter panel keeps its dropdowns unportalled.
|
|
34
|
-
comboboxProps={{ withinPortal: false }}
|
|
64
|
+
comboboxProps={{ ...inputProps?.comboboxProps, withinPortal: false }}
|
|
35
65
|
aria-label={labels.editCell(getColumnLabel(column))}
|
|
36
66
|
data-dg-part="editor-input"
|
|
37
67
|
data={optionsToComboboxData(
|
|
@@ -44,7 +74,10 @@ export function TMDataGridSelectEditor({
|
|
|
44
74
|
// The confirming modes leave the draft for their explicit commit.
|
|
45
75
|
if (next !== null && features.editMode === "cell") void commit();
|
|
46
76
|
}}
|
|
47
|
-
onBlur={
|
|
77
|
+
onBlur={(event) => {
|
|
78
|
+
field.handleBlur();
|
|
79
|
+
inputProps?.onBlur?.(event);
|
|
80
|
+
}}
|
|
48
81
|
error={invalid}
|
|
49
82
|
/>
|
|
50
83
|
);
|
|
@@ -1,26 +1,50 @@
|
|
|
1
|
-
import { TextInput } from "@mantine/core";
|
|
1
|
+
import { TextInput, type TextInputProps } from "@mantine/core";
|
|
2
2
|
import type { TMDataGridEditorArgs } from "../../core/editEngine";
|
|
3
3
|
import { getColumnLabel } from "../../core/columnUtils";
|
|
4
4
|
import { useTMDataGridContext } from "../../TMDataGridContext";
|
|
5
5
|
import {
|
|
6
|
+
mergeRefs,
|
|
6
7
|
selectAllOnFocus,
|
|
7
8
|
useCaretKeeper,
|
|
8
9
|
useFieldInvalid,
|
|
9
10
|
useFieldValue,
|
|
11
|
+
type EditorDataAttributes,
|
|
10
12
|
} from "./editorShared";
|
|
11
13
|
|
|
14
|
+
export type TMDataGridStringEditorProps = TMDataGridEditorArgs & {
|
|
15
|
+
/**
|
|
16
|
+
* Props for the Mantine `TextInput`. The grid keeps the value, `onChange`,
|
|
17
|
+
* `error`, `size` and the accessible name; `onFocus` and `onBlur` run after
|
|
18
|
+
* the grid's own, and `ref` receives the input beside the grid's ref.
|
|
19
|
+
*/
|
|
20
|
+
inputProps?: Omit<
|
|
21
|
+
TextInputProps,
|
|
22
|
+
"value" | "defaultValue" | "onChange" | "error" | "size" | "aria-label"
|
|
23
|
+
> &
|
|
24
|
+
EditorDataAttributes;
|
|
25
|
+
};
|
|
26
|
+
|
|
12
27
|
/** The built-in editor for `meta.type: "string"` (and the default). */
|
|
13
|
-
export function TMDataGridStringEditor({
|
|
28
|
+
export function TMDataGridStringEditor({
|
|
29
|
+
field,
|
|
30
|
+
column,
|
|
31
|
+
size,
|
|
32
|
+
inputProps,
|
|
33
|
+
}: TMDataGridStringEditorProps) {
|
|
14
34
|
const { labels } = useTMDataGridContext();
|
|
15
35
|
const value = useFieldValue(field);
|
|
16
36
|
const invalid = useFieldInvalid(field);
|
|
17
37
|
const { inputRef, remember } = useCaretKeeper();
|
|
18
38
|
return (
|
|
19
39
|
<TextInput
|
|
20
|
-
ref={inputRef}
|
|
21
|
-
size={size}
|
|
22
40
|
w="100%"
|
|
23
|
-
|
|
41
|
+
{...inputProps}
|
|
42
|
+
ref={mergeRefs(inputRef, inputProps?.ref)}
|
|
43
|
+
size={size}
|
|
44
|
+
onFocus={(event) => {
|
|
45
|
+
selectAllOnFocus(event);
|
|
46
|
+
inputProps?.onFocus?.(event);
|
|
47
|
+
}}
|
|
24
48
|
aria-label={labels.editCell(getColumnLabel(column))}
|
|
25
49
|
data-dg-part="editor-input"
|
|
26
50
|
value={typeof value === "string" ? value : ""}
|
|
@@ -28,7 +52,10 @@ export function TMDataGridStringEditor({ field, column, size }: TMDataGridEditor
|
|
|
28
52
|
remember(event.currentTarget);
|
|
29
53
|
field.handleChange(event.currentTarget.value);
|
|
30
54
|
}}
|
|
31
|
-
onBlur={
|
|
55
|
+
onBlur={(event) => {
|
|
56
|
+
field.handleBlur();
|
|
57
|
+
inputProps?.onBlur?.(event);
|
|
58
|
+
}}
|
|
32
59
|
error={invalid}
|
|
33
60
|
/>
|
|
34
61
|
);
|
|
@@ -1,8 +1,34 @@
|
|
|
1
1
|
import { useStore } from "@tanstack/react-form";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
useLayoutEffect,
|
|
4
|
+
useRef,
|
|
5
|
+
type Ref,
|
|
6
|
+
type RefCallback,
|
|
7
|
+
type RefObject,
|
|
8
|
+
} from "react";
|
|
3
9
|
import { isInputElement } from "../../core/dom";
|
|
4
10
|
import { firstErrorText, type TMDataGridEditField } from "../../core/editEngine";
|
|
5
11
|
|
|
12
|
+
/**
|
|
13
|
+
* `data-*` attributes for a built-in editor's `inputProps`. JSX accepts them
|
|
14
|
+
* on any element, but a props object literal only takes declared keys.
|
|
15
|
+
*/
|
|
16
|
+
export type EditorDataAttributes = {
|
|
17
|
+
[attribute: `data-${string}`]: string | number | boolean | undefined;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
/** One ref callback feeding the grid's own ref and the consumer's. */
|
|
21
|
+
export function mergeRefs<T>(
|
|
22
|
+
...refs: Array<Ref<T> | undefined>
|
|
23
|
+
): RefCallback<T> {
|
|
24
|
+
return (node) => {
|
|
25
|
+
for (const ref of refs) {
|
|
26
|
+
if (typeof ref === "function") ref(node);
|
|
27
|
+
else if (ref !== null && ref !== undefined) ref.current = node;
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
6
32
|
/** The field's live value - one subscription, on the field's own store. */
|
|
7
33
|
export function useFieldValue(field: TMDataGridEditField): unknown {
|
|
8
34
|
return useStore(field.store, (state) => state.value as unknown);
|
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);
|
package/src/index.ts
CHANGED
|
@@ -110,6 +110,8 @@ export {
|
|
|
110
110
|
type TMDataGridEditRowProjection,
|
|
111
111
|
type TMDataGridEditRowSnapshot,
|
|
112
112
|
type TMDataGridEditState,
|
|
113
|
+
type TMDataGridEditCommitMap,
|
|
114
|
+
type TMDataGridEditCommitMapArgs,
|
|
113
115
|
type TMDataGridEditValueMap,
|
|
114
116
|
type TMDataGridEditValueMapArgs,
|
|
115
117
|
type TMDataGridFieldValidate,
|
|
@@ -141,12 +143,30 @@ export { DgRangeSliderFilter } from "./components/filters/DgRangeSliderFilter";
|
|
|
141
143
|
export { DgDateRangeFilter } from "./components/filters/DgDateRangeFilter";
|
|
142
144
|
export { DgAutocompleteFilter } from "./components/filters/DgAutocompleteFilter";
|
|
143
145
|
export { DgTriStateFilter } from "./components/filters/DgTriStateFilter";
|
|
144
|
-
export {
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
export {
|
|
149
|
-
|
|
146
|
+
export {
|
|
147
|
+
TMDataGridStringEditor,
|
|
148
|
+
type TMDataGridStringEditorProps,
|
|
149
|
+
} from "./components/editors/TMDataGridStringEditor";
|
|
150
|
+
export {
|
|
151
|
+
TMDataGridNumberEditor,
|
|
152
|
+
type TMDataGridNumberEditorProps,
|
|
153
|
+
} from "./components/editors/TMDataGridNumberEditor";
|
|
154
|
+
export {
|
|
155
|
+
TMDataGridBooleanEditor,
|
|
156
|
+
type TMDataGridBooleanEditorProps,
|
|
157
|
+
} from "./components/editors/TMDataGridBooleanEditor";
|
|
158
|
+
export {
|
|
159
|
+
TMDataGridDateEditor,
|
|
160
|
+
type TMDataGridDateEditorProps,
|
|
161
|
+
} from "./components/editors/TMDataGridDateEditor";
|
|
162
|
+
export {
|
|
163
|
+
TMDataGridSelectEditor,
|
|
164
|
+
type TMDataGridSelectEditorProps,
|
|
165
|
+
} from "./components/editors/TMDataGridSelectEditor";
|
|
166
|
+
export {
|
|
167
|
+
TMDataGridMultiSelectEditor,
|
|
168
|
+
type TMDataGridMultiSelectEditorProps,
|
|
169
|
+
} from "./components/editors/TMDataGridMultiSelectEditor";
|
|
150
170
|
export {
|
|
151
171
|
mergeLabels,
|
|
152
172
|
TMDATAGRID_LABELS_EN,
|
package/src/useTMDataGrid.tsx
CHANGED
|
@@ -777,7 +777,7 @@ type TMDataGridEditingCallbacks<TData extends RowData> = {
|
|
|
777
777
|
* One TanStack Form per editing row; drafts survive scrolling because the
|
|
778
778
|
* forms live outside the DOM, keyed by row id. Which columns edit, and with
|
|
779
779
|
* what, is declared per column under `meta.edit`: `meta.type` picks the
|
|
780
|
-
* built-in editor, and `enabled`, `field`, `editor`, `validate` and `
|
|
780
|
+
* built-in editor, and `enabled`, `field`, `editor`, `validate`, `mapValue` and `mapCommitValue`
|
|
781
781
|
* override the rest.
|
|
782
782
|
*/
|
|
783
783
|
export type TMDataGridEditingOptions<TData extends RowData> =
|