@dxos/react-ui-form 0.7.2 → 0.7.3-staging.971cd8d
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 +49 -39
- package/dist/lib/browser/index.mjs.map +3 -3
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/node/index.cjs +49 -39
- package/dist/lib/node/index.cjs.map +3 -3
- package/dist/lib/node/meta.json +1 -1
- package/dist/lib/node-esm/index.mjs +49 -39
- package/dist/lib/node-esm/index.mjs.map +3 -3
- 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/Form.d.ts +3 -2
- package/dist/types/src/components/Form/Form.d.ts.map +1 -1
- package/dist/types/src/components/Form/Form.stories.d.ts +9 -5
- package/dist/types/src/components/Form/Form.stories.d.ts.map +1 -1
- package/dist/types/src/components/ViewEditor/ViewEditor.d.ts.map +1 -1
- package/dist/types/src/components/deprecated/Form.stories.d.ts.map +1 -1
- package/dist/types/src/hooks/useForm.d.ts +12 -5
- package/dist/types/src/hooks/useForm.d.ts.map +1 -1
- package/package.json +23 -22
- package/src/components/FieldEditor/FieldEditor.stories.tsx +3 -3
- package/src/components/FieldEditor/FieldEditor.tsx +8 -8
- package/src/components/Form/Form.stories.tsx +41 -11
- package/src/components/Form/Form.tsx +23 -12
- package/src/components/ViewEditor/ViewEditor.tsx +8 -7
- package/src/components/deprecated/Form.stories.tsx +4 -4
- package/src/hooks/useForm.ts +29 -14
|
@@ -2,6 +2,7 @@ import { type FocusEvent } from 'react';
|
|
|
2
2
|
import { type BaseObject, type PropertyKey } from '@dxos/echo-schema';
|
|
3
3
|
import { type SimpleType, type S } from '@dxos/effect';
|
|
4
4
|
import { type ValidationError } from '@dxos/schema';
|
|
5
|
+
import { type MaybePromise } from '@dxos/util';
|
|
5
6
|
/**
|
|
6
7
|
* Return type from `useForm` hook.
|
|
7
8
|
*/
|
|
@@ -10,8 +11,8 @@ export type FormHandler<T extends BaseObject> = {
|
|
|
10
11
|
errors: Record<PropertyKey<T>, string>;
|
|
11
12
|
touched: Record<PropertyKey<T>, boolean>;
|
|
12
13
|
changed: Record<PropertyKey<T>, boolean>;
|
|
13
|
-
|
|
14
|
-
|
|
14
|
+
canSave: boolean;
|
|
15
|
+
handleSave: () => void;
|
|
15
16
|
getStatus: (property: PropertyKey<T>) => {
|
|
16
17
|
status?: 'error';
|
|
17
18
|
error?: string;
|
|
@@ -24,7 +25,13 @@ export type FormHandler<T extends BaseObject> = {
|
|
|
24
25
|
* Hook options.
|
|
25
26
|
*/
|
|
26
27
|
export interface FormOptions<T extends BaseObject> {
|
|
28
|
+
/**
|
|
29
|
+
* Effect schema.
|
|
30
|
+
*/
|
|
27
31
|
schema: S.Schema<T>;
|
|
32
|
+
/**
|
|
33
|
+
* Initial values (which may not pass validation).
|
|
34
|
+
*/
|
|
28
35
|
initialValues: T;
|
|
29
36
|
/**
|
|
30
37
|
* Callback for value changes. Note: This is called even when values are invalid.
|
|
@@ -47,13 +54,13 @@ export interface FormOptions<T extends BaseObject> {
|
|
|
47
54
|
/**
|
|
48
55
|
* Called when the form is submitted and passes validation.
|
|
49
56
|
*/
|
|
50
|
-
|
|
57
|
+
onSave?: (values: T, meta: {
|
|
51
58
|
changed: FormHandler<T>['changed'];
|
|
52
|
-
}) => void
|
|
59
|
+
}) => MaybePromise<void>;
|
|
53
60
|
}
|
|
54
61
|
/**
|
|
55
62
|
* Creates a hook for managing form state, including values, validation, and submission.
|
|
56
63
|
* Deeply integrated with `@dxos/schema` for schema-based validation.
|
|
57
64
|
*/
|
|
58
|
-
export declare const useForm: <T extends BaseObject>({ schema, initialValues, onValuesChanged, onValidate, onValid,
|
|
65
|
+
export declare const useForm: <T extends BaseObject>({ schema, initialValues, onValuesChanged, onValidate, onValid, onSave, }: FormOptions<T>) => FormHandler<T>;
|
|
59
66
|
//# sourceMappingURL=useForm.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useForm.d.ts","sourceRoot":"","sources":["../../../../src/hooks/useForm.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,KAAK,UAAU,EAA6C,MAAM,OAAO,CAAC;AAEnF,OAAO,EAAE,KAAK,UAAU,EAAE,KAAK,WAAW,EAAE,MAAM,mBAAmB,CAAC;AACtE,OAAO,EAAE,KAAK,UAAU,EAAE,KAAK,CAAC,EAAE,MAAM,cAAc,CAAC;AAGvD,OAAO,EAAkB,KAAK,eAAe,EAAE,MAAM,cAAc,CAAC;
|
|
1
|
+
{"version":3,"file":"useForm.d.ts","sourceRoot":"","sources":["../../../../src/hooks/useForm.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,KAAK,UAAU,EAA6C,MAAM,OAAO,CAAC;AAEnF,OAAO,EAAE,KAAK,UAAU,EAAE,KAAK,WAAW,EAAE,MAAM,mBAAmB,CAAC;AACtE,OAAO,EAAE,KAAK,UAAU,EAAE,KAAK,CAAC,EAAE,MAAM,cAAc,CAAC;AAGvD,OAAO,EAAkB,KAAK,eAAe,EAAE,MAAM,cAAc,CAAC;AACpE,OAAO,EAAE,KAAK,YAAY,EAAE,MAAM,YAAY,CAAC;AAE/C;;GAEG;AACH,MAAM,MAAM,WAAW,CAAC,CAAC,SAAS,UAAU,IAAI;IAK9C,MAAM,EAAE,CAAC,CAAC;IACV,MAAM,EAAE,MAAM,CAAC,WAAW,CAAC,CAAC,CAAC,EAAE,MAAM,CAAC,CAAC;IACvC,OAAO,EAAE,MAAM,CAAC,WAAW,CAAC,CAAC,CAAC,EAAE,OAAO,CAAC,CAAC;IACzC,OAAO,EAAE,MAAM,CAAC,WAAW,CAAC,CAAC,CAAC,EAAE,OAAO,CAAC,CAAC;IACzC,OAAO,EAAE,OAAO,CAAC;IACjB,UAAU,EAAE,MAAM,IAAI,CAAC;IAMvB,SAAS,EAAE,CAAC,QAAQ,EAAE,WAAW,CAAC,CAAC,CAAC,KAAK;QAAE,MAAM,CAAC,EAAE,OAAO,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IAC9E,QAAQ,EAAE,CAAC,CAAC,EAAE,QAAQ,EAAE,WAAW,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC;IAC7C,aAAa,EAAE,CAAC,CAAC,EAAE,QAAQ,EAAE,WAAW,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,UAAU,EAAE,KAAK,EAAE,CAAC,KAAK,IAAI,CAAC;IACjF,MAAM,EAAE,CAAC,KAAK,EAAE,UAAU,CAAC,gBAAgB,GAAG,mBAAmB,CAAC,KAAK,IAAI,CAAC;CAC7E,CAAC;AAEF;;GAEG;AACH,MAAM,WAAW,WAAW,CAAC,CAAC,SAAS,UAAU;IAC/C;;OAEG;IAEH,MAAM,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC;IAEpB;;OAEG;IAEH,aAAa,EAAE,CAAC,CAAC;IAEjB;;;;OAIG;IACH,eAAe,CAAC,EAAE,CAAC,MAAM,EAAE,CAAC,KAAK,IAAI,CAAC;IAEtC;;;;OAIG;IAEH,UAAU,CAAC,EAAE,CAAC,MAAM,EAAE,CAAC,KAAK,eAAe,EAAE,GAAG,SAAS,CAAC;IAE1D;;OAEG;IACH,OAAO,CAAC,EAAE,CAAC,MAAM,EAAE,CAAC,EAAE,IAAI,EAAE;QAAE,OAAO,EAAE,WAAW,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAA;KAAE,KAAK,IAAI,CAAC;IAE5E;;OAEG;IACH,MAAM,CAAC,EAAE,CAAC,MAAM,EAAE,CAAC,EAAE,IAAI,EAAE;QAAE,OAAO,EAAE,WAAW,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAA;KAAE,KAAK,YAAY,CAAC,IAAI,CAAC,CAAC;CAC1F;AAED;;;GAGG;AACH,eAAO,MAAM,OAAO,GAAI,CAAC,SAAS,UAAU,4EAOzC,WAAW,CAAC,CAAC,CAAC,KAAG,WAAW,CAAC,CAAC,CAwIhC,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dxos/react-ui-form",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.3-staging.971cd8d",
|
|
4
4
|
"description": "Data components.",
|
|
5
5
|
"homepage": "https://dxos.org",
|
|
6
6
|
"bugs": "https://github.com/dxos/dxos/issues",
|
|
@@ -29,19 +29,20 @@
|
|
|
29
29
|
"date-fns": "^3.3.1",
|
|
30
30
|
"effect": "^3.9.2",
|
|
31
31
|
"jsonpath": "^1.1.1",
|
|
32
|
-
"@dxos/async": "0.7.
|
|
33
|
-
"@dxos/debug": "0.7.
|
|
34
|
-
"@dxos/echo-db": "0.7.
|
|
35
|
-
"@dxos/echo-schema": "0.7.
|
|
36
|
-
"@dxos/
|
|
37
|
-
"@dxos/
|
|
38
|
-
"@dxos/
|
|
39
|
-
"@dxos/log": "0.7.
|
|
40
|
-
"@dxos/
|
|
41
|
-
"@dxos/react-
|
|
42
|
-
"@dxos/react-ui-
|
|
43
|
-
"@dxos/
|
|
44
|
-
"@dxos/util": "0.7.
|
|
32
|
+
"@dxos/async": "0.7.3-staging.971cd8d",
|
|
33
|
+
"@dxos/debug": "0.7.3-staging.971cd8d",
|
|
34
|
+
"@dxos/echo-db": "0.7.3-staging.971cd8d",
|
|
35
|
+
"@dxos/echo-schema": "0.7.3-staging.971cd8d",
|
|
36
|
+
"@dxos/invariant": "0.7.3-staging.971cd8d",
|
|
37
|
+
"@dxos/effect": "0.7.3-staging.971cd8d",
|
|
38
|
+
"@dxos/keys": "0.7.3-staging.971cd8d",
|
|
39
|
+
"@dxos/log": "0.7.3-staging.971cd8d",
|
|
40
|
+
"@dxos/live-object": "0.7.3-staging.971cd8d",
|
|
41
|
+
"@dxos/react-client": "0.7.3-staging.971cd8d",
|
|
42
|
+
"@dxos/react-ui-list": "0.7.3-staging.971cd8d",
|
|
43
|
+
"@dxos/react-ui-types": "0.7.3-staging.971cd8d",
|
|
44
|
+
"@dxos/util": "0.7.3-staging.971cd8d",
|
|
45
|
+
"@dxos/schema": "0.7.3-staging.971cd8d"
|
|
45
46
|
},
|
|
46
47
|
"devDependencies": {
|
|
47
48
|
"@phosphor-icons/react": "^2.1.5",
|
|
@@ -52,12 +53,12 @@
|
|
|
52
53
|
"react": "~18.2.0",
|
|
53
54
|
"react-dom": "~18.2.0",
|
|
54
55
|
"vite": "5.4.7",
|
|
55
|
-
"@dxos/echo-signals": "0.7.
|
|
56
|
-
"@dxos/random": "0.7.
|
|
57
|
-
"@dxos/react-ui
|
|
58
|
-
"@dxos/react-ui": "0.7.
|
|
59
|
-
"@dxos/react-ui-theme": "0.7.
|
|
60
|
-
"@dxos/storybook-utils": "0.7.
|
|
56
|
+
"@dxos/echo-signals": "0.7.3-staging.971cd8d",
|
|
57
|
+
"@dxos/random": "0.7.3-staging.971cd8d",
|
|
58
|
+
"@dxos/react-ui": "0.7.3-staging.971cd8d",
|
|
59
|
+
"@dxos/react-ui-syntax-highlighter": "0.7.3-staging.971cd8d",
|
|
60
|
+
"@dxos/react-ui-theme": "0.7.3-staging.971cd8d",
|
|
61
|
+
"@dxos/storybook-utils": "0.7.3-staging.971cd8d"
|
|
61
62
|
},
|
|
62
63
|
"peerDependencies": {
|
|
63
64
|
"@effect/schema": "^0.75.5",
|
|
@@ -65,8 +66,8 @@
|
|
|
65
66
|
"effect": "^3.9.2",
|
|
66
67
|
"react": "~18.2.0",
|
|
67
68
|
"react-dom": "~18.2.0",
|
|
68
|
-
"@dxos/react-ui-theme": "0.7.
|
|
69
|
-
"@dxos/react-ui": "0.7.
|
|
69
|
+
"@dxos/react-ui-theme": "0.7.3-staging.971cd8d",
|
|
70
|
+
"@dxos/react-ui": "0.7.3-staging.971cd8d"
|
|
70
71
|
},
|
|
71
72
|
"publishConfig": {
|
|
72
73
|
"access": "public"
|
|
@@ -7,7 +7,7 @@ import '@dxos-theme';
|
|
|
7
7
|
import { type Meta, type StoryObj } from '@storybook/react';
|
|
8
8
|
import React from 'react';
|
|
9
9
|
|
|
10
|
-
import { createMutableSchema } from '@dxos/
|
|
10
|
+
import { createMutableSchema } from '@dxos/live-object/testing';
|
|
11
11
|
import { log } from '@dxos/log';
|
|
12
12
|
import { ViewProjection } from '@dxos/schema';
|
|
13
13
|
import { TestSchema, testView } from '@dxos/schema/testing';
|
|
@@ -20,14 +20,14 @@ import { TestLayout, TestPanel } from '../testing';
|
|
|
20
20
|
type StoryProps = FieldEditorProps;
|
|
21
21
|
|
|
22
22
|
const DefaultStory = (props: FieldEditorProps) => {
|
|
23
|
-
const handleComplete: FieldEditorProps['
|
|
23
|
+
const handleComplete: FieldEditorProps['onSave'] = () => {
|
|
24
24
|
log.info('onClose', { props });
|
|
25
25
|
};
|
|
26
26
|
|
|
27
27
|
return (
|
|
28
28
|
<TestLayout json={{ props }}>
|
|
29
29
|
<TestPanel>
|
|
30
|
-
<FieldEditor {...props}
|
|
30
|
+
<FieldEditor {...props} onSave={handleComplete} />
|
|
31
31
|
</TestPanel>
|
|
32
32
|
</TestLayout>
|
|
33
33
|
);
|
|
@@ -26,14 +26,14 @@ export type FieldEditorProps = {
|
|
|
26
26
|
projection: ViewProjection;
|
|
27
27
|
field: FieldType;
|
|
28
28
|
registry?: SchemaResolver;
|
|
29
|
-
|
|
29
|
+
onSave: () => void;
|
|
30
30
|
onCancel?: () => void;
|
|
31
31
|
};
|
|
32
32
|
|
|
33
33
|
/**
|
|
34
34
|
* Displays a Form representing the metadata for a given `Field` and `View`.
|
|
35
35
|
*/
|
|
36
|
-
export const FieldEditor = ({ view, projection, field, registry,
|
|
36
|
+
export const FieldEditor = ({ view, projection, field, registry, onSave, onCancel }: FieldEditorProps) => {
|
|
37
37
|
const { t } = useTranslation(translationKey);
|
|
38
38
|
const [props, setProps] = useState<PropertyType>(projection.getFieldProjection(field.id).props);
|
|
39
39
|
useEffect(() => setProps(projection.getFieldProjection(field.id).props), [field, projection]);
|
|
@@ -100,19 +100,19 @@ export const FieldEditor = ({ view, projection, field, registry, onClose, onCanc
|
|
|
100
100
|
[view.fields, field],
|
|
101
101
|
);
|
|
102
102
|
|
|
103
|
-
const
|
|
103
|
+
const handleSave = useCallback<NonNullable<FormProps<PropertyType>['onSave']>>(
|
|
104
104
|
(props) => {
|
|
105
105
|
projection.setFieldProjection({ field, props });
|
|
106
|
-
|
|
106
|
+
onSave();
|
|
107
107
|
},
|
|
108
|
-
[projection, field,
|
|
108
|
+
[projection, field, onSave],
|
|
109
109
|
);
|
|
110
110
|
|
|
111
111
|
const handleCancel = useCallback<NonNullable<FormProps<PropertyType>['onCancel']>>(() => {
|
|
112
112
|
// Need to defer to allow form to close.
|
|
113
113
|
requestAnimationFrame(() => onCancel?.());
|
|
114
|
-
|
|
115
|
-
}, [
|
|
114
|
+
onSave();
|
|
115
|
+
}, [onSave]);
|
|
116
116
|
|
|
117
117
|
if (!fieldSchema) {
|
|
118
118
|
log.warn('invalid format', { props });
|
|
@@ -129,7 +129,7 @@ export const FieldEditor = ({ view, projection, field, registry, onClose, onCanc
|
|
|
129
129
|
sort={['property', 'format']}
|
|
130
130
|
onValuesChanged={handleValuesChanged}
|
|
131
131
|
onValidate={handleValidate}
|
|
132
|
-
|
|
132
|
+
onSave={handleSave}
|
|
133
133
|
onCancel={handleCancel}
|
|
134
134
|
Custom={{
|
|
135
135
|
['format' satisfies keyof PropertyType]: (props) => (
|
|
@@ -7,7 +7,8 @@ import '@dxos-theme';
|
|
|
7
7
|
import { type Meta, type StoryObj } from '@storybook/react';
|
|
8
8
|
import React, { useCallback, useState } from 'react';
|
|
9
9
|
|
|
10
|
-
import { AST, Format, S } from '@dxos/echo-schema';
|
|
10
|
+
import { AST, type BaseObject, Format, S } from '@dxos/echo-schema';
|
|
11
|
+
import { Testing } from '@dxos/schema/testing';
|
|
11
12
|
import { withLayout, withTheme } from '@dxos/storybook-utils';
|
|
12
13
|
|
|
13
14
|
import { SelectInput } from './Defaults';
|
|
@@ -33,24 +34,24 @@ const TestSchema = S.Struct({
|
|
|
33
34
|
|
|
34
35
|
type TestType = S.Schema.Type<typeof TestSchema>;
|
|
35
36
|
|
|
36
|
-
type StoryProps = FormProps<
|
|
37
|
+
type StoryProps<T extends BaseObject> = { schema: S.Schema<T> } & FormProps<T>;
|
|
37
38
|
|
|
38
|
-
const DefaultStory = ({ values: initialValues }: StoryProps) => {
|
|
39
|
+
const DefaultStory = <T extends BaseObject>({ schema, values: initialValues }: StoryProps<T>) => {
|
|
39
40
|
const [values, setValues] = useState(initialValues);
|
|
40
|
-
const
|
|
41
|
+
const handleSave = useCallback<NonNullable<FormProps<T>['onSave']>>((values) => {
|
|
41
42
|
setValues(values);
|
|
42
43
|
}, []);
|
|
43
44
|
|
|
44
45
|
return (
|
|
45
|
-
<TestLayout json={{ values, schema:
|
|
46
|
+
<TestLayout json={{ values, schema: schema.ast.toJSON() }}>
|
|
46
47
|
<TestPanel>
|
|
47
|
-
<Form<
|
|
48
|
+
<Form<T> schema={schema} values={values} onSave={handleSave} />
|
|
48
49
|
</TestPanel>
|
|
49
50
|
</TestLayout>
|
|
50
51
|
);
|
|
51
52
|
};
|
|
52
53
|
|
|
53
|
-
const meta: Meta<StoryProps
|
|
54
|
+
const meta: Meta<StoryProps<any>> = {
|
|
54
55
|
title: 'ui/react-ui-form/Form',
|
|
55
56
|
component: Form,
|
|
56
57
|
render: DefaultStory,
|
|
@@ -62,10 +63,11 @@ const meta: Meta<StoryProps> = {
|
|
|
62
63
|
|
|
63
64
|
export default meta;
|
|
64
65
|
|
|
65
|
-
type Story = StoryObj<StoryProps
|
|
66
|
+
type Story<T extends BaseObject> = StoryObj<StoryProps<T>>;
|
|
66
67
|
|
|
67
|
-
export const Default: Story = {
|
|
68
|
+
export const Default: Story<TestType> = {
|
|
68
69
|
args: {
|
|
70
|
+
schema: TestSchema,
|
|
69
71
|
values: {
|
|
70
72
|
name: 'DXOS',
|
|
71
73
|
active: true,
|
|
@@ -76,6 +78,33 @@ export const Default: Story = {
|
|
|
76
78
|
},
|
|
77
79
|
};
|
|
78
80
|
|
|
81
|
+
// TODO(burdon): Should accept partial values.
|
|
82
|
+
export const Org: Story<Testing.OrgSchemaType> = {
|
|
83
|
+
args: {
|
|
84
|
+
schema: Testing.OrgSchema,
|
|
85
|
+
values: {
|
|
86
|
+
name: 'DXOS',
|
|
87
|
+
website: 'https://dxos.org',
|
|
88
|
+
},
|
|
89
|
+
},
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
// TODO(burdon): Type issue with employer reference.
|
|
93
|
+
// TODO(burdon): Test table/form with compound values (e.g., address).
|
|
94
|
+
// export const Contact: Story<Testing.ContactSchemaType> = {
|
|
95
|
+
// args: {
|
|
96
|
+
// // Property name is missing in type EncodedReference but required in type
|
|
97
|
+
// schema: Testing.ContactSchema,
|
|
98
|
+
// values: {
|
|
99
|
+
// name: 'Bot',
|
|
100
|
+
// },
|
|
101
|
+
// },
|
|
102
|
+
// };
|
|
103
|
+
|
|
104
|
+
//
|
|
105
|
+
// TODO(burdon): Move into separate storybook and use test types.
|
|
106
|
+
//
|
|
107
|
+
|
|
79
108
|
const ShapeSchema = S.Struct({
|
|
80
109
|
shape: S.optional(
|
|
81
110
|
S.Union(
|
|
@@ -92,11 +121,12 @@ const ShapeSchema = S.Struct({
|
|
|
92
121
|
}).pipe(S.mutable);
|
|
93
122
|
|
|
94
123
|
type ShapeType = S.Schema.Type<typeof ShapeSchema>;
|
|
124
|
+
|
|
95
125
|
type DiscriminatedUnionStoryProps = FormProps<ShapeType>;
|
|
96
126
|
|
|
97
127
|
const DiscriminatedUnionStory = ({ values: initialValues }: DiscriminatedUnionStoryProps) => {
|
|
98
128
|
const [values, setValues] = useState(initialValues);
|
|
99
|
-
const
|
|
129
|
+
const handleSave = useCallback<NonNullable<FormProps<ShapeType>['onSave']>>((values) => {
|
|
100
130
|
setValues(values);
|
|
101
131
|
}, []);
|
|
102
132
|
|
|
@@ -106,7 +136,7 @@ const DiscriminatedUnionStory = ({ values: initialValues }: DiscriminatedUnionSt
|
|
|
106
136
|
<Form<ShapeType>
|
|
107
137
|
schema={ShapeSchema}
|
|
108
138
|
values={values}
|
|
109
|
-
|
|
139
|
+
onSave={handleSave}
|
|
110
140
|
Custom={{
|
|
111
141
|
['shape.type' as const]: (props) => (
|
|
112
142
|
<SelectInput<ShapeType>
|
|
@@ -28,7 +28,9 @@ export type PropsFilter<T extends BaseObject> = (props: SchemaProperty<T>[]) =>
|
|
|
28
28
|
|
|
29
29
|
export type FormProps<T extends BaseObject> = ThemedClassName<
|
|
30
30
|
{
|
|
31
|
+
// TODO(burdon): Partial<T>?
|
|
31
32
|
values: T;
|
|
33
|
+
|
|
32
34
|
/** Path to the current object from the root. Used with nested forms. */
|
|
33
35
|
path?: string[];
|
|
34
36
|
|
|
@@ -39,18 +41,20 @@ export type FormProps<T extends BaseObject> = ThemedClassName<
|
|
|
39
41
|
filter?: PropsFilter<T>;
|
|
40
42
|
sort?: PropertyKey<T>[];
|
|
41
43
|
autoSave?: boolean;
|
|
44
|
+
testId?: string;
|
|
42
45
|
onCancel?: () => void;
|
|
43
46
|
|
|
44
47
|
/**
|
|
45
48
|
* Map of custom renderers for specific properties.
|
|
46
49
|
*/
|
|
47
50
|
Custom?: Partial<Record<string, InputComponent<T>>>;
|
|
48
|
-
} & Pick<FormOptions<T>, 'schema' | 'onValuesChanged' | 'onValidate' | '
|
|
51
|
+
} & Pick<FormOptions<T>, 'schema' | 'onValuesChanged' | 'onValidate' | 'onSave'>
|
|
49
52
|
>;
|
|
50
53
|
|
|
51
54
|
/**
|
|
52
55
|
* General purpose form component that displays field controls based on the given schema.
|
|
53
56
|
*/
|
|
57
|
+
// TODO(burdon): Area to show general validation errors.
|
|
54
58
|
export const Form = <T extends BaseObject>({
|
|
55
59
|
classNames,
|
|
56
60
|
schema,
|
|
@@ -60,21 +64,22 @@ export const Form = <T extends BaseObject>({
|
|
|
60
64
|
filter,
|
|
61
65
|
sort,
|
|
62
66
|
autoSave,
|
|
67
|
+
testId,
|
|
63
68
|
onValuesChanged,
|
|
64
69
|
onValidate,
|
|
65
|
-
|
|
70
|
+
onSave,
|
|
66
71
|
onCancel,
|
|
67
72
|
Custom,
|
|
68
73
|
}: FormProps<T>) => {
|
|
69
74
|
const { t } = useTranslation(translationKey);
|
|
70
|
-
const onValid = useMemo(() => (autoSave ?
|
|
71
|
-
const {
|
|
75
|
+
const onValid = useMemo(() => (autoSave ? onSave : undefined), [autoSave, onSave]);
|
|
76
|
+
const { canSave, values, errors, handleSave, ...inputProps } = useForm<T>({
|
|
72
77
|
schema,
|
|
73
78
|
initialValues,
|
|
74
79
|
onValuesChanged,
|
|
75
80
|
onValidate,
|
|
76
81
|
onValid,
|
|
77
|
-
|
|
82
|
+
onSave,
|
|
78
83
|
});
|
|
79
84
|
|
|
80
85
|
// Filter and sort props.
|
|
@@ -98,12 +103,12 @@ export const Form = <T extends BaseObject>({
|
|
|
98
103
|
}, [errors]);
|
|
99
104
|
|
|
100
105
|
return (
|
|
101
|
-
<div role='form' className={mx('flex flex-col w-full gap-2 py-2', classNames)}>
|
|
106
|
+
<div role='form' className={mx('flex flex-col w-full gap-2 py-2', classNames)} data-testid={testId}>
|
|
102
107
|
{properties
|
|
103
108
|
.map((property) => {
|
|
104
109
|
const { ast, name, type, format, title, description, examples, options } = property;
|
|
105
110
|
const key = [...path, name];
|
|
106
|
-
const label =
|
|
111
|
+
const label = title ?? pipe(name, capitalize);
|
|
107
112
|
const placeholder = examples?.length ? `Example: "${examples[0]}"` : description;
|
|
108
113
|
|
|
109
114
|
// Get generic input.
|
|
@@ -177,19 +182,25 @@ export const Form = <T extends BaseObject>({
|
|
|
177
182
|
})
|
|
178
183
|
.filter(isNotFalsy)}
|
|
179
184
|
|
|
180
|
-
{(onCancel ||
|
|
185
|
+
{(onCancel || onSave) && !autoSave && (
|
|
181
186
|
<div role='none' className='flex justify-center'>
|
|
182
187
|
<div role='none' className={mx(onCancel && !readonly && 'grid grid-cols-2 gap-2')}>
|
|
183
188
|
{onCancel && !readonly && (
|
|
184
|
-
<IconButton
|
|
189
|
+
<IconButton
|
|
190
|
+
data-testid='cancel-button'
|
|
191
|
+
icon='ph--x--regular'
|
|
192
|
+
label={t('button cancel')}
|
|
193
|
+
onClick={onCancel}
|
|
194
|
+
/>
|
|
185
195
|
)}
|
|
186
|
-
{
|
|
196
|
+
{onSave && (
|
|
187
197
|
<IconButton
|
|
188
198
|
type='submit'
|
|
189
|
-
|
|
199
|
+
data-testid='save-button'
|
|
200
|
+
disabled={!canSave}
|
|
190
201
|
icon='ph--check--regular'
|
|
191
202
|
label={t('button save')}
|
|
192
|
-
onClick={
|
|
203
|
+
onClick={handleSave}
|
|
193
204
|
/>
|
|
194
205
|
)}
|
|
195
206
|
</div>
|
|
@@ -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
|
|
@@ -7,11 +7,11 @@ import '@dxos-theme';
|
|
|
7
7
|
import { type Meta, type StoryObj } from '@storybook/react';
|
|
8
8
|
import React from 'react';
|
|
9
9
|
|
|
10
|
-
import { create } from '@dxos/
|
|
11
|
-
import { testData,
|
|
12
|
-
import {
|
|
10
|
+
import { create } from '@dxos/live-object';
|
|
11
|
+
import { testData, TestSchema, testView, type TestType } from '@dxos/schema/testing';
|
|
12
|
+
import { withLayout, withTheme } from '@dxos/storybook-utils';
|
|
13
13
|
|
|
14
|
-
import { type DeprecatedFormProps
|
|
14
|
+
import { DeprecatedForm, type DeprecatedFormProps } from './Form';
|
|
15
15
|
import translations from '../../translations';
|
|
16
16
|
import { TestLayout, TestPanel } from '../testing';
|
|
17
17
|
|
package/src/hooks/useForm.ts
CHANGED
|
@@ -9,6 +9,7 @@ import { type SimpleType, type S } from '@dxos/effect';
|
|
|
9
9
|
import { invariant } from '@dxos/invariant';
|
|
10
10
|
import { log } from '@dxos/log';
|
|
11
11
|
import { validateSchema, type ValidationError } from '@dxos/schema';
|
|
12
|
+
import { type MaybePromise } from '@dxos/util';
|
|
12
13
|
|
|
13
14
|
/**
|
|
14
15
|
* Return type from `useForm` hook.
|
|
@@ -22,8 +23,8 @@ export type FormHandler<T extends BaseObject> = {
|
|
|
22
23
|
errors: Record<PropertyKey<T>, string>;
|
|
23
24
|
touched: Record<PropertyKey<T>, boolean>;
|
|
24
25
|
changed: Record<PropertyKey<T>, boolean>;
|
|
25
|
-
|
|
26
|
-
|
|
26
|
+
canSave: boolean;
|
|
27
|
+
handleSave: () => void;
|
|
27
28
|
|
|
28
29
|
//
|
|
29
30
|
// Form input component helpers.
|
|
@@ -39,9 +40,16 @@ export type FormHandler<T extends BaseObject> = {
|
|
|
39
40
|
* Hook options.
|
|
40
41
|
*/
|
|
41
42
|
export interface FormOptions<T extends BaseObject> {
|
|
43
|
+
/**
|
|
44
|
+
* Effect schema.
|
|
45
|
+
*/
|
|
46
|
+
// TODO(burdon): Change to S.Struct<T>?
|
|
42
47
|
schema: S.Schema<T>;
|
|
43
48
|
|
|
44
|
-
|
|
49
|
+
/**
|
|
50
|
+
* Initial values (which may not pass validation).
|
|
51
|
+
*/
|
|
52
|
+
// TODO(burdon): Should be partial?
|
|
45
53
|
initialValues: T;
|
|
46
54
|
|
|
47
55
|
/**
|
|
@@ -56,7 +64,7 @@ export interface FormOptions<T extends BaseObject> {
|
|
|
56
64
|
* Use this for complex validation logic that can't be expressed in the schema.
|
|
57
65
|
* @returns Array of validation errors, or undefined if validation passes
|
|
58
66
|
*/
|
|
59
|
-
// TODO(burdon): Change to key x value
|
|
67
|
+
// TODO(burdon): Change to key x value?
|
|
60
68
|
onValidate?: (values: T) => ValidationError[] | undefined;
|
|
61
69
|
|
|
62
70
|
/**
|
|
@@ -67,7 +75,7 @@ export interface FormOptions<T extends BaseObject> {
|
|
|
67
75
|
/**
|
|
68
76
|
* Called when the form is submitted and passes validation.
|
|
69
77
|
*/
|
|
70
|
-
|
|
78
|
+
onSave?: (values: T, meta: { changed: FormHandler<T>['changed'] }) => MaybePromise<void>;
|
|
71
79
|
}
|
|
72
80
|
|
|
73
81
|
/**
|
|
@@ -80,7 +88,7 @@ export const useForm = <T extends BaseObject>({
|
|
|
80
88
|
onValuesChanged,
|
|
81
89
|
onValidate,
|
|
82
90
|
onValid,
|
|
83
|
-
|
|
91
|
+
onSave,
|
|
84
92
|
}: FormOptions<T>): FormHandler<T> => {
|
|
85
93
|
const [values, setValues] = useState<T>(initialValues);
|
|
86
94
|
useEffect(() => {
|
|
@@ -90,6 +98,7 @@ export const useForm = <T extends BaseObject>({
|
|
|
90
98
|
const [touched, setTouched] = useState<Record<PropertyKey<T>, boolean>>(createKeySet(initialValues, false));
|
|
91
99
|
const [changed, setChanged] = useState<Record<PropertyKey<T>, boolean>>(createKeySet(initialValues, false));
|
|
92
100
|
const [errors, setErrors] = useState<Record<PropertyKey<T>, string>>({} as Record<PropertyKey<T>, string>);
|
|
101
|
+
const [saving, setSaving] = useState(false);
|
|
93
102
|
|
|
94
103
|
//
|
|
95
104
|
// Validation.
|
|
@@ -123,19 +132,25 @@ export const useForm = <T extends BaseObject>({
|
|
|
123
132
|
* NOTE: We can submit if there is no touched field that has an error.
|
|
124
133
|
* Basically, if there's a validation message visible in the form, submit should be disabled.
|
|
125
134
|
*/
|
|
126
|
-
const
|
|
135
|
+
const canSave = useMemo(
|
|
127
136
|
() =>
|
|
137
|
+
!saving &&
|
|
128
138
|
Object.keys(values).every(
|
|
129
139
|
(property) => touched[property as PropertyKey<T>] === false || !errors[property as PropertyKey<T>],
|
|
130
140
|
),
|
|
131
|
-
[values, touched, errors],
|
|
141
|
+
[values, touched, errors, saving],
|
|
132
142
|
);
|
|
133
143
|
|
|
134
|
-
const
|
|
144
|
+
const handleSave = useCallback(async () => {
|
|
135
145
|
if (validate(values)) {
|
|
136
|
-
|
|
146
|
+
setSaving(true);
|
|
147
|
+
try {
|
|
148
|
+
await onSave?.(values, { changed });
|
|
149
|
+
} finally {
|
|
150
|
+
setSaving(false);
|
|
151
|
+
}
|
|
137
152
|
}
|
|
138
|
-
}, [values, validate,
|
|
153
|
+
}, [values, validate, onSave]);
|
|
139
154
|
|
|
140
155
|
//
|
|
141
156
|
// Fields.
|
|
@@ -185,7 +200,7 @@ export const useForm = <T extends BaseObject>({
|
|
|
185
200
|
|
|
186
201
|
// TODO(Zan): This should be configurable behavior.
|
|
187
202
|
if (event.relatedTarget?.getAttribute('type') === 'submit') {
|
|
188
|
-
// NOTE: We do this here instead of
|
|
203
|
+
// NOTE: We do this here instead of onSave because the blur event is triggered before the submit event
|
|
189
204
|
// and results in the submit button being disabled when the form is invalid.
|
|
190
205
|
setTouched(createKeySet(values, true));
|
|
191
206
|
}
|
|
@@ -201,8 +216,8 @@ export const useForm = <T extends BaseObject>({
|
|
|
201
216
|
errors,
|
|
202
217
|
touched,
|
|
203
218
|
changed,
|
|
204
|
-
|
|
205
|
-
|
|
219
|
+
canSave,
|
|
220
|
+
handleSave,
|
|
206
221
|
|
|
207
222
|
// Field utils.
|
|
208
223
|
getStatus,
|