@dxos/react-ui-form 0.7.1 → 0.7.2-main.f1adc9f
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/lib/browser/index.mjs +137 -111
- package/dist/lib/browser/index.mjs.map +4 -4
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/node/index.cjs +130 -104
- package/dist/lib/node/index.cjs.map +4 -4
- package/dist/lib/node/meta.json +1 -1
- package/dist/lib/node-esm/index.mjs +137 -111
- package/dist/lib/node-esm/index.mjs.map +4 -4
- package/dist/lib/node-esm/meta.json +1 -1
- package/dist/types/src/components/FieldEditor/FieldEditor.d.ts +2 -2
- package/dist/types/src/components/FieldEditor/FieldEditor.d.ts.map +1 -1
- package/dist/types/src/components/Form/Custom.d.ts +2 -1
- package/dist/types/src/components/Form/Custom.d.ts.map +1 -1
- package/dist/types/src/components/Form/Defaults.d.ts +11 -4
- package/dist/types/src/components/Form/Defaults.d.ts.map +1 -1
- package/dist/types/src/components/Form/Form.d.ts +6 -5
- package/dist/types/src/components/Form/Form.d.ts.map +1 -1
- package/dist/types/src/components/Form/Form.stories.d.ts +1 -4
- package/dist/types/src/components/Form/Form.stories.d.ts.map +1 -1
- package/dist/types/src/components/Form/Input.d.ts +3 -8
- package/dist/types/src/components/Form/Input.d.ts.map +1 -1
- package/dist/types/src/components/Form/factory.d.ts +2 -2
- package/dist/types/src/components/Form/factory.d.ts.map +1 -1
- package/dist/types/src/components/ViewEditor/ViewEditor.d.ts.map +1 -1
- package/dist/types/src/components/ViewEditor/ViewEditor.stories.d.ts.map +1 -1
- package/dist/types/src/hooks/useForm.d.ts +8 -7
- package/dist/types/src/hooks/useForm.d.ts.map +1 -1
- package/package.json +22 -24
- package/src/components/FieldEditor/FieldEditor.stories.tsx +2 -2
- package/src/components/FieldEditor/FieldEditor.tsx +8 -8
- package/src/components/Form/Custom.tsx +9 -9
- package/src/components/Form/Defaults.tsx +10 -5
- package/src/components/Form/Form.stories.tsx +6 -6
- package/src/components/Form/Form.tsx +52 -28
- package/src/components/Form/Input.tsx +3 -7
- package/src/components/Form/factory.tsx +4 -4
- package/src/components/ViewEditor/ViewEditor.stories.tsx +1 -2
- package/src/components/ViewEditor/ViewEditor.tsx +8 -7
- package/src/hooks/useForm.ts +18 -17
|
@@ -2,16 +2,19 @@
|
|
|
2
2
|
// Copyright 2024 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
+
import { pipe } from 'effect';
|
|
6
|
+
import { capitalize } from 'effect/String';
|
|
5
7
|
import React, { useEffect, useMemo } from 'react';
|
|
6
8
|
|
|
7
|
-
import { AST, S } from '@dxos/echo-schema';
|
|
9
|
+
import { AST, type BaseObject, S, type PropertyKey } from '@dxos/echo-schema';
|
|
8
10
|
import { findNode, getDiscriminatedType, isDiscriminatedUnion } from '@dxos/effect';
|
|
9
11
|
import { log } from '@dxos/log';
|
|
10
12
|
import { IconButton, type ThemedClassName, useTranslation } from '@dxos/react-ui';
|
|
11
13
|
import { mx } from '@dxos/react-ui-theme';
|
|
12
|
-
import { getSchemaProperties, type
|
|
14
|
+
import { getSchemaProperties, type SchemaProperty } from '@dxos/schema';
|
|
13
15
|
import { isNotFalsy } from '@dxos/util';
|
|
14
16
|
|
|
17
|
+
import { SelectInput } from './Defaults';
|
|
15
18
|
import { type InputComponent } from './Input';
|
|
16
19
|
import { getInputComponent } from './factory';
|
|
17
20
|
import { type FormOptions, useForm } from '../../hooks';
|
|
@@ -21,9 +24,9 @@ import { translationKey } from '../../translations';
|
|
|
21
24
|
|
|
22
25
|
const padding = 'px-2';
|
|
23
26
|
|
|
24
|
-
export type PropsFilter<T extends
|
|
27
|
+
export type PropsFilter<T extends BaseObject<T>> = (props: SchemaProperty<T>[]) => SchemaProperty<T>[];
|
|
25
28
|
|
|
26
|
-
export type FormProps<T extends
|
|
29
|
+
export type FormProps<T extends BaseObject<T>> = ThemedClassName<
|
|
27
30
|
{
|
|
28
31
|
values: T;
|
|
29
32
|
/** Path to the current object from the root. Used with nested forms. */
|
|
@@ -42,13 +45,13 @@ export type FormProps<T extends object> = ThemedClassName<
|
|
|
42
45
|
* Map of custom renderers for specific properties.
|
|
43
46
|
*/
|
|
44
47
|
Custom?: Partial<Record<string, InputComponent<T>>>;
|
|
45
|
-
} & Pick<FormOptions<T>, 'schema' | 'onValuesChanged' | 'onValidate' | '
|
|
48
|
+
} & Pick<FormOptions<T>, 'schema' | 'onValuesChanged' | 'onValidate' | 'onSave'>
|
|
46
49
|
>;
|
|
47
50
|
|
|
48
51
|
/**
|
|
49
52
|
* General purpose form component that displays field controls based on the given schema.
|
|
50
53
|
*/
|
|
51
|
-
export const Form = <T extends
|
|
54
|
+
export const Form = <T extends BaseObject<T>>({
|
|
52
55
|
classNames,
|
|
53
56
|
schema,
|
|
54
57
|
values: initialValues,
|
|
@@ -59,24 +62,24 @@ export const Form = <T extends object = {}>({
|
|
|
59
62
|
autoSave,
|
|
60
63
|
onValuesChanged,
|
|
61
64
|
onValidate,
|
|
62
|
-
|
|
65
|
+
onSave,
|
|
63
66
|
onCancel,
|
|
64
67
|
Custom,
|
|
65
68
|
}: FormProps<T>) => {
|
|
66
69
|
const { t } = useTranslation(translationKey);
|
|
67
|
-
const onValid = useMemo(() => (autoSave ?
|
|
68
|
-
const {
|
|
70
|
+
const onValid = useMemo(() => (autoSave ? onSave : undefined), [autoSave, onSave]);
|
|
71
|
+
const { canSave, values, errors, handleSave, ...inputProps } = useForm<T>({
|
|
69
72
|
schema,
|
|
70
73
|
initialValues,
|
|
71
74
|
onValuesChanged,
|
|
72
75
|
onValidate,
|
|
73
76
|
onValid,
|
|
74
|
-
|
|
77
|
+
onSave,
|
|
75
78
|
});
|
|
76
79
|
|
|
77
80
|
// Filter and sort props.
|
|
78
81
|
// TODO(burdon): Move into useForm?
|
|
79
|
-
const
|
|
82
|
+
const properties = useMemo(() => {
|
|
80
83
|
const props = getSchemaProperties<T>(schema.ast, values);
|
|
81
84
|
const filtered = filter ? filter(props) : props;
|
|
82
85
|
const findIndex = (props: PropertyKey<T>[], prop: PropertyKey<T>) => {
|
|
@@ -96,25 +99,50 @@ export const Form = <T extends object = {}>({
|
|
|
96
99
|
|
|
97
100
|
return (
|
|
98
101
|
<div role='form' className={mx('flex flex-col w-full gap-2 py-2', classNames)}>
|
|
99
|
-
{
|
|
100
|
-
.map((
|
|
101
|
-
|
|
102
|
-
// TODO(burdon): Use Select control if options are present in annotation?
|
|
102
|
+
{properties
|
|
103
|
+
.map((property) => {
|
|
104
|
+
const { ast, name, type, format, title, description, examples, options } = property;
|
|
103
105
|
const key = [...path, name];
|
|
104
|
-
const
|
|
106
|
+
const label = title ?? pipe(name, capitalize);
|
|
107
|
+
const placeholder = examples?.length ? `Example: "${examples[0]}"` : description;
|
|
108
|
+
|
|
109
|
+
// Get generic input.
|
|
110
|
+
let InputComponent = Custom?.[key.join('.')];
|
|
111
|
+
if (!InputComponent) {
|
|
112
|
+
// Select.
|
|
113
|
+
if (options) {
|
|
114
|
+
return (
|
|
115
|
+
<div key={name} role='none' className={padding}>
|
|
116
|
+
<SelectInput
|
|
117
|
+
type={type}
|
|
118
|
+
format={format}
|
|
119
|
+
property={name}
|
|
120
|
+
disabled={readonly}
|
|
121
|
+
label={label}
|
|
122
|
+
options={options.map((option) => ({ value: option, label: String(option) }))}
|
|
123
|
+
placeholder={placeholder}
|
|
124
|
+
{...inputProps}
|
|
125
|
+
/>
|
|
126
|
+
</div>
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
InputComponent = getInputComponent<T>(type, format);
|
|
131
|
+
}
|
|
132
|
+
|
|
105
133
|
if (!InputComponent) {
|
|
106
134
|
// Recursively render form.
|
|
107
135
|
if (type === 'object') {
|
|
108
|
-
const baseNode = findNode(
|
|
136
|
+
const baseNode = findNode(ast, isDiscriminatedUnion);
|
|
109
137
|
const typeLiteral = baseNode
|
|
110
138
|
? getDiscriminatedType(baseNode, values[name] as any)
|
|
111
|
-
: findNode(
|
|
139
|
+
: findNode(ast, AST.isTypeLiteral);
|
|
112
140
|
|
|
113
141
|
if (typeLiteral) {
|
|
114
142
|
const schema = S.make(typeLiteral);
|
|
115
143
|
return (
|
|
116
144
|
<div key={name} role='none'>
|
|
117
|
-
<div className={padding}>{
|
|
145
|
+
<div className={padding}>{label}</div>
|
|
118
146
|
<Form<any>
|
|
119
147
|
schema={schema}
|
|
120
148
|
path={key}
|
|
@@ -133,8 +161,6 @@ export const Form = <T extends object = {}>({
|
|
|
133
161
|
return null;
|
|
134
162
|
}
|
|
135
163
|
|
|
136
|
-
const placeholder = examples?.length ? `Example: ${examples[0]}` : description;
|
|
137
|
-
|
|
138
164
|
return (
|
|
139
165
|
<div key={name} role='none' className={padding}>
|
|
140
166
|
<InputComponent
|
|
@@ -142,7 +168,7 @@ export const Form = <T extends object = {}>({
|
|
|
142
168
|
format={format}
|
|
143
169
|
property={name}
|
|
144
170
|
disabled={readonly}
|
|
145
|
-
label={
|
|
171
|
+
label={label}
|
|
146
172
|
placeholder={placeholder}
|
|
147
173
|
{...inputProps}
|
|
148
174
|
/>
|
|
@@ -151,21 +177,19 @@ export const Form = <T extends object = {}>({
|
|
|
151
177
|
})
|
|
152
178
|
.filter(isNotFalsy)}
|
|
153
179
|
|
|
154
|
-
{
|
|
155
|
-
|
|
156
|
-
{(onCancel || onSubmit) && !autoSave && (
|
|
180
|
+
{(onCancel || onSave) && !autoSave && (
|
|
157
181
|
<div role='none' className='flex justify-center'>
|
|
158
182
|
<div role='none' className={mx(onCancel && !readonly && 'grid grid-cols-2 gap-2')}>
|
|
159
183
|
{onCancel && !readonly && (
|
|
160
184
|
<IconButton icon='ph--x--regular' label={t('button cancel')} onClick={onCancel} />
|
|
161
185
|
)}
|
|
162
|
-
{
|
|
186
|
+
{onSave && (
|
|
163
187
|
<IconButton
|
|
164
188
|
type='submit'
|
|
165
|
-
disabled={!
|
|
189
|
+
disabled={!canSave}
|
|
166
190
|
icon='ph--check--regular'
|
|
167
191
|
label={t('button save')}
|
|
168
|
-
onClick={
|
|
192
|
+
onClick={handleSave}
|
|
169
193
|
/>
|
|
170
194
|
)}
|
|
171
195
|
</div>
|
|
@@ -4,33 +4,29 @@
|
|
|
4
4
|
|
|
5
5
|
import React, { type FC, type PropsWithChildren } from 'react';
|
|
6
6
|
|
|
7
|
-
import { type FormatEnum } from '@dxos/echo-schema';
|
|
7
|
+
import { type BaseObject, type FormatEnum, type PropertyKey } from '@dxos/echo-schema';
|
|
8
8
|
import { type SimpleType } from '@dxos/effect';
|
|
9
9
|
import { Icon, Tooltip } from '@dxos/react-ui';
|
|
10
10
|
import { errorText } from '@dxos/react-ui-theme';
|
|
11
|
-
import { type PropertyKey } from '@dxos/schema';
|
|
12
11
|
|
|
13
12
|
import { type FormHandler } from '../../hooks';
|
|
14
13
|
|
|
15
14
|
/**
|
|
16
15
|
* Props passed to input components.
|
|
17
16
|
*/
|
|
18
|
-
export type InputProps<T extends
|
|
17
|
+
export type InputProps<T extends BaseObject<T>> = {
|
|
19
18
|
property: PropertyKey<T>;
|
|
20
19
|
type: SimpleType;
|
|
21
20
|
format?: FormatEnum;
|
|
22
21
|
label: string;
|
|
23
22
|
disabled?: boolean;
|
|
24
23
|
placeholder?: string;
|
|
25
|
-
// TODO(burdon): Provide from annotations? E.g., enum.
|
|
26
|
-
options?: Array<{ value: string | number; label?: string }>;
|
|
27
24
|
} & Pick<FormHandler<T>, 'getStatus' | 'getValue' | 'onValueChange' | 'onBlur'>;
|
|
28
25
|
|
|
29
26
|
/**
|
|
30
27
|
* Form input component.
|
|
31
28
|
*/
|
|
32
|
-
|
|
33
|
-
export type InputComponent<T extends object = {}> = FC<InputProps<T>>;
|
|
29
|
+
export type InputComponent<T extends BaseObject<T>> = FC<InputProps<T>>;
|
|
34
30
|
|
|
35
31
|
export type InputHeaderProps = PropsWithChildren<{
|
|
36
32
|
error?: string;
|
|
@@ -2,23 +2,23 @@
|
|
|
2
2
|
// Copyright 2024 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import { FormatEnum } from '@dxos/echo-schema';
|
|
5
|
+
import { type BaseObject, FormatEnum } from '@dxos/echo-schema';
|
|
6
6
|
import { type SimpleType } from '@dxos/effect';
|
|
7
7
|
|
|
8
|
-
import {
|
|
8
|
+
import { GeiPointInput } from './Custom';
|
|
9
9
|
import { BooleanInput, NumberInput, TextInput } from './Defaults';
|
|
10
10
|
import { type InputComponent } from './Input';
|
|
11
11
|
|
|
12
12
|
/**
|
|
13
13
|
* Get property input component.
|
|
14
14
|
*/
|
|
15
|
-
export const getInputComponent = <T extends
|
|
15
|
+
export const getInputComponent = <T extends BaseObject<T>>(
|
|
16
16
|
type: SimpleType,
|
|
17
17
|
format?: FormatEnum,
|
|
18
18
|
): InputComponent<T> | undefined => {
|
|
19
19
|
switch (format) {
|
|
20
20
|
case FormatEnum.LatLng:
|
|
21
|
-
return
|
|
21
|
+
return GeiPointInput;
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
switch (type) {
|
|
@@ -10,8 +10,7 @@ import React, { useCallback, useEffect, useState } from 'react';
|
|
|
10
10
|
import { Format, type MutableSchema, S, toJsonSchema, TypedObject } from '@dxos/echo-schema';
|
|
11
11
|
import { useSpace } from '@dxos/react-client/echo';
|
|
12
12
|
import { withClientProvider } from '@dxos/react-client/testing';
|
|
13
|
-
import { ViewProjection, createView } from '@dxos/schema';
|
|
14
|
-
import { type ViewType } from '@dxos/schema';
|
|
13
|
+
import { type ViewType, ViewProjection, createView } from '@dxos/schema';
|
|
15
14
|
import { withTheme, withLayout } from '@dxos/storybook-utils';
|
|
16
15
|
|
|
17
16
|
import { ViewEditor } from './ViewEditor';
|
|
@@ -22,7 +22,7 @@ const ViewMetaSchema = S.Struct({
|
|
|
22
22
|
name: S.String.annotations({
|
|
23
23
|
[AST.TitleAnnotationId]: 'View',
|
|
24
24
|
}),
|
|
25
|
-
|
|
25
|
+
typename: Format.DXN.annotations({
|
|
26
26
|
[AST.TitleAnnotationId]: 'Typename',
|
|
27
27
|
}),
|
|
28
28
|
}).pipe(S.mutable);
|
|
@@ -60,16 +60,16 @@ export const ViewEditor = ({
|
|
|
60
60
|
name: view.name,
|
|
61
61
|
// TODO(burdon): Need to warn user of possible consequences of editing.
|
|
62
62
|
// TODO(burdon): Settings should have domain name owned by user.
|
|
63
|
-
|
|
63
|
+
typename: view.query.typename,
|
|
64
64
|
};
|
|
65
65
|
}, [view]);
|
|
66
66
|
|
|
67
67
|
const handleViewUpdate = useCallback(
|
|
68
|
-
({ name,
|
|
68
|
+
({ name, typename }: ViewMetaType) => {
|
|
69
69
|
requestAnimationFrame(() => {
|
|
70
70
|
view.name = name;
|
|
71
|
-
view.query.
|
|
72
|
-
schema.updateTypename(
|
|
71
|
+
view.query.typename = typename;
|
|
72
|
+
schema.updateTypename(typename);
|
|
73
73
|
});
|
|
74
74
|
},
|
|
75
75
|
[view, schema],
|
|
@@ -95,7 +95,7 @@ export const ViewEditor = ({
|
|
|
95
95
|
|
|
96
96
|
return (
|
|
97
97
|
<div role='none' className={mx('flex flex-col w-full divide-y divide-separator', classNames)}>
|
|
98
|
-
<Form<ViewMetaType> autoFocus autoSave schema={ViewMetaSchema} values={viewValues}
|
|
98
|
+
<Form<ViewMetaType> autoFocus autoSave schema={ViewMetaSchema} values={viewValues} onSave={handleViewUpdate} />
|
|
99
99
|
|
|
100
100
|
<div>
|
|
101
101
|
{/* TODO(burdon): Clean up common form ux. */}
|
|
@@ -139,9 +139,10 @@ export const ViewEditor = ({
|
|
|
139
139
|
projection={projection}
|
|
140
140
|
field={field}
|
|
141
141
|
registry={registry}
|
|
142
|
-
|
|
142
|
+
onSave={handleClose}
|
|
143
143
|
/>
|
|
144
144
|
)}
|
|
145
|
+
|
|
145
146
|
{!readonly && !field && (
|
|
146
147
|
<div className='flex p-2 justify-center'>
|
|
147
148
|
<IconButton
|
package/src/hooks/useForm.ts
CHANGED
|
@@ -4,15 +4,16 @@
|
|
|
4
4
|
|
|
5
5
|
import { type FocusEvent, useCallback, useEffect, useMemo, useState } from 'react';
|
|
6
6
|
|
|
7
|
+
import { type BaseObject, type PropertyKey } from '@dxos/echo-schema';
|
|
7
8
|
import { type SimpleType, type S } from '@dxos/effect';
|
|
8
9
|
import { invariant } from '@dxos/invariant';
|
|
9
10
|
import { log } from '@dxos/log';
|
|
10
|
-
import { validateSchema, type
|
|
11
|
+
import { validateSchema, type ValidationError } from '@dxos/schema';
|
|
11
12
|
|
|
12
13
|
/**
|
|
13
14
|
* Return type from `useForm` hook.
|
|
14
15
|
*/
|
|
15
|
-
export type FormHandler<T extends
|
|
16
|
+
export type FormHandler<T extends BaseObject<T>> = {
|
|
16
17
|
//
|
|
17
18
|
// Form state management.
|
|
18
19
|
//
|
|
@@ -21,8 +22,8 @@ export type FormHandler<T extends object = {}> = {
|
|
|
21
22
|
errors: Record<PropertyKey<T>, string>;
|
|
22
23
|
touched: Record<PropertyKey<T>, boolean>;
|
|
23
24
|
changed: Record<PropertyKey<T>, boolean>;
|
|
24
|
-
|
|
25
|
-
|
|
25
|
+
canSave: boolean;
|
|
26
|
+
handleSave: () => void;
|
|
26
27
|
|
|
27
28
|
//
|
|
28
29
|
// Form input component helpers.
|
|
@@ -37,7 +38,7 @@ export type FormHandler<T extends object = {}> = {
|
|
|
37
38
|
/**
|
|
38
39
|
* Hook options.
|
|
39
40
|
*/
|
|
40
|
-
export interface FormOptions<T extends
|
|
41
|
+
export interface FormOptions<T extends BaseObject<T>> {
|
|
41
42
|
schema: S.Schema<T>;
|
|
42
43
|
|
|
43
44
|
// TODO(burdon): Are these reactive?
|
|
@@ -66,20 +67,20 @@ export interface FormOptions<T extends object> {
|
|
|
66
67
|
/**
|
|
67
68
|
* Called when the form is submitted and passes validation.
|
|
68
69
|
*/
|
|
69
|
-
|
|
70
|
+
onSave?: (values: T, meta: { changed: FormHandler<T>['changed'] }) => void;
|
|
70
71
|
}
|
|
71
72
|
|
|
72
73
|
/**
|
|
73
74
|
* Creates a hook for managing form state, including values, validation, and submission.
|
|
74
75
|
* Deeply integrated with `@dxos/schema` for schema-based validation.
|
|
75
76
|
*/
|
|
76
|
-
export const useForm = <T extends
|
|
77
|
+
export const useForm = <T extends BaseObject<T>>({
|
|
77
78
|
schema,
|
|
78
79
|
initialValues,
|
|
79
80
|
onValuesChanged,
|
|
80
81
|
onValidate,
|
|
81
82
|
onValid,
|
|
82
|
-
|
|
83
|
+
onSave,
|
|
83
84
|
}: FormOptions<T>): FormHandler<T> => {
|
|
84
85
|
const [values, setValues] = useState<T>(initialValues);
|
|
85
86
|
useEffect(() => {
|
|
@@ -122,7 +123,7 @@ export const useForm = <T extends object>({
|
|
|
122
123
|
* NOTE: We can submit if there is no touched field that has an error.
|
|
123
124
|
* Basically, if there's a validation message visible in the form, submit should be disabled.
|
|
124
125
|
*/
|
|
125
|
-
const
|
|
126
|
+
const canSave = useMemo(
|
|
126
127
|
() =>
|
|
127
128
|
Object.keys(values).every(
|
|
128
129
|
(property) => touched[property as PropertyKey<T>] === false || !errors[property as PropertyKey<T>],
|
|
@@ -130,11 +131,11 @@ export const useForm = <T extends object>({
|
|
|
130
131
|
[values, touched, errors],
|
|
131
132
|
);
|
|
132
133
|
|
|
133
|
-
const
|
|
134
|
+
const handleSave = useCallback(() => {
|
|
134
135
|
if (validate(values)) {
|
|
135
|
-
|
|
136
|
+
onSave?.(values, { changed });
|
|
136
137
|
}
|
|
137
|
-
}, [values, validate,
|
|
138
|
+
}, [values, validate, onSave]);
|
|
138
139
|
|
|
139
140
|
//
|
|
140
141
|
// Fields.
|
|
@@ -184,7 +185,7 @@ export const useForm = <T extends object>({
|
|
|
184
185
|
|
|
185
186
|
// TODO(Zan): This should be configurable behavior.
|
|
186
187
|
if (event.relatedTarget?.getAttribute('type') === 'submit') {
|
|
187
|
-
// NOTE: We do this here instead of
|
|
188
|
+
// NOTE: We do this here instead of onSave because the blur event is triggered before the submit event
|
|
188
189
|
// and results in the submit button being disabled when the form is invalid.
|
|
189
190
|
setTouched(createKeySet(values, true));
|
|
190
191
|
}
|
|
@@ -200,8 +201,8 @@ export const useForm = <T extends object>({
|
|
|
200
201
|
errors,
|
|
201
202
|
touched,
|
|
202
203
|
changed,
|
|
203
|
-
|
|
204
|
-
|
|
204
|
+
canSave,
|
|
205
|
+
handleSave,
|
|
205
206
|
|
|
206
207
|
// Field utils.
|
|
207
208
|
getStatus,
|
|
@@ -211,12 +212,12 @@ export const useForm = <T extends object>({
|
|
|
211
212
|
} satisfies FormHandler<T>;
|
|
212
213
|
};
|
|
213
214
|
|
|
214
|
-
const createKeySet = <T extends
|
|
215
|
+
const createKeySet = <T extends BaseObject<T>, V>(obj: T, value: V): Record<PropertyKey<T>, V> => {
|
|
215
216
|
invariant(obj);
|
|
216
217
|
return Object.keys(obj).reduce((acc, key) => ({ ...acc, [key]: value }), {} as Record<PropertyKey<T>, V>);
|
|
217
218
|
};
|
|
218
219
|
|
|
219
|
-
const flatMap = <T extends
|
|
220
|
+
const flatMap = <T extends BaseObject<T>>(errors: ValidationError[]) => {
|
|
220
221
|
return errors.reduce(
|
|
221
222
|
(result, { path, message }) => {
|
|
222
223
|
if (!(path in result)) {
|