@dxos/react-ui-form 0.8.2-main.f11618f → 0.8.2-staging.42af850
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 +1732 -1374
- package/dist/lib/browser/index.mjs.map +4 -4
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/node/index.cjs +1715 -1365
- package/dist/lib/node/index.cjs.map +4 -4
- package/dist/lib/node/meta.json +1 -1
- package/dist/lib/node-esm/index.mjs +1732 -1374
- package/dist/lib/node-esm/index.mjs.map +4 -4
- package/dist/lib/node-esm/meta.json +1 -1
- package/dist/types/src/components/ControlSection/ControlSection.d.ts +1 -1
- package/dist/types/src/components/ControlSection/ControlSection.d.ts.map +1 -1
- package/dist/types/src/components/FieldEditor/FieldEditor.d.ts.map +1 -1
- package/dist/types/src/components/FieldEditor/FieldEditor.test.d.ts +2 -0
- package/dist/types/src/components/FieldEditor/FieldEditor.test.d.ts.map +1 -0
- package/dist/types/src/components/Form/ArrayField.d.ts.map +1 -1
- package/dist/types/src/components/Form/Defaults.d.ts +4 -4
- package/dist/types/src/components/Form/Defaults.d.ts.map +1 -1
- package/dist/types/src/components/Form/Form.d.ts +4 -4
- package/dist/types/src/components/Form/Form.d.ts.map +1 -1
- package/dist/types/src/components/Form/Form.stories.d.ts +34 -34
- package/dist/types/src/components/Form/Form.stories.d.ts.map +1 -1
- package/dist/types/src/components/Form/FormActions.d.ts +1 -1
- package/dist/types/src/components/Form/FormActions.d.ts.map +1 -1
- package/dist/types/src/components/Form/FormContent.d.ts +9 -8
- package/dist/types/src/components/Form/FormContent.d.ts.map +1 -1
- package/dist/types/src/components/Form/FormContext.d.ts.map +1 -1
- package/dist/types/src/components/Form/Input.d.ts.map +1 -1
- package/dist/types/src/components/Form/RefField.d.ts +7 -18
- package/dist/types/src/components/Form/RefField.d.ts.map +1 -1
- package/dist/types/src/components/Form/custom/GeoPointInput.d.ts.map +1 -1
- package/dist/types/src/components/Form/custom/SelectOptionsInput.d.ts.map +1 -1
- package/dist/types/src/components/Form/custom/SelectOptionsInput.stories.d.ts.map +1 -1
- package/dist/types/src/components/Form/custom/TupleInput.d.ts.map +1 -1
- package/dist/types/src/components/Form/factory.d.ts +1 -1
- package/dist/types/src/components/Form/factory.d.ts.map +1 -1
- package/dist/types/src/components/Form/index.d.ts.map +1 -1
- package/dist/types/src/components/ViewEditor/Test.stories.d.ts.map +1 -1
- package/dist/types/src/components/ViewEditor/ViewEditor.d.ts +2 -2
- package/dist/types/src/components/ViewEditor/ViewEditor.d.ts.map +1 -1
- package/dist/types/src/components/ViewEditor/ViewEditor.stories.d.ts +5 -3
- package/dist/types/src/components/ViewEditor/ViewEditor.stories.d.ts.map +1 -1
- package/dist/types/src/components/deprecated/Form.d.ts +2 -2
- package/dist/types/src/components/deprecated/Form.d.ts.map +1 -1
- package/dist/types/src/components/deprecated/Form.stories.d.ts.map +1 -1
- package/dist/types/src/components/deprecated/FormInput.d.ts.map +1 -1
- package/dist/types/src/components/testing.d.ts.map +1 -1
- package/dist/types/src/hooks/index.d.ts +2 -0
- package/dist/types/src/hooks/index.d.ts.map +1 -1
- package/dist/types/src/hooks/useForm.d.ts +2 -2
- package/dist/types/src/hooks/useForm.d.ts.map +1 -1
- package/dist/types/src/hooks/useQueryRefOptions.d.ts +23 -0
- package/dist/types/src/hooks/useQueryRefOptions.d.ts.map +1 -0
- package/dist/types/src/hooks/useRefQueryLookupHandler.d.ts +8 -0
- package/dist/types/src/hooks/useRefQueryLookupHandler.d.ts.map +1 -0
- package/dist/types/src/translations.d.ts +5 -0
- package/dist/types/src/translations.d.ts.map +1 -1
- package/dist/types/src/util/field.d.ts.map +1 -1
- package/dist/types/src/util/formatting.d.ts.map +1 -1
- package/dist/types/src/util/index.d.ts +2 -0
- package/dist/types/src/util/index.d.ts.map +1 -1
- package/dist/types/src/util/refs.d.ts +9 -0
- package/dist/types/src/util/refs.d.ts.map +1 -0
- package/dist/types/src/util/schema.d.ts +3 -0
- package/dist/types/src/util/schema.d.ts.map +1 -0
- package/dist/types/src/vitest-setup.d.ts +2 -0
- package/dist/types/src/vitest-setup.d.ts.map +1 -0
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/package.json +28 -25
- package/src/components/ControlSection/ControlSection.stories.tsx +1 -1
- package/src/components/ControlSection/ControlSection.tsx +2 -2
- package/src/components/FieldEditor/FieldEditor.stories.tsx +1 -1
- package/src/components/FieldEditor/FieldEditor.test.tsx +49 -0
- package/src/components/FieldEditor/FieldEditor.tsx +3 -2
- package/src/components/Form/ArrayField.tsx +60 -49
- package/src/components/Form/Defaults.tsx +77 -48
- package/src/components/Form/Form.stories.tsx +104 -122
- package/src/components/Form/Form.tsx +5 -6
- package/src/components/Form/FormActions.tsx +15 -18
- package/src/components/Form/FormContent.tsx +80 -54
- package/src/components/Form/FormContext.tsx +14 -8
- package/src/components/Form/Input.tsx +4 -10
- package/src/components/Form/RefField.tsx +167 -101
- package/src/components/Form/custom/SelectOptionsInput.stories.tsx +6 -5
- package/src/components/Form/factory.tsx +1 -1
- package/src/components/Form/index.ts +0 -1
- package/src/components/ViewEditor/ViewEditor.stories.tsx +19 -9
- package/src/components/ViewEditor/ViewEditor.tsx +65 -35
- package/src/components/deprecated/Form.tsx +6 -6
- package/src/components/testing.tsx +5 -9
- package/src/hooks/index.ts +3 -1
- package/src/hooks/useForm.ts +4 -3
- package/src/hooks/useQueryRefOptions.ts +53 -0
- package/src/hooks/useRefQueryLookupHandler.ts +39 -0
- package/src/translations.ts +9 -2
- package/src/util/index.ts +3 -1
- package/src/util/refs.ts +38 -0
- package/src/util/schema.ts +14 -0
- package/src/vitest-setup.ts +11 -0
|
@@ -5,38 +5,40 @@
|
|
|
5
5
|
import '@dxos-theme';
|
|
6
6
|
|
|
7
7
|
import { type Meta, type StoryObj } from '@storybook/react';
|
|
8
|
+
import { Schema } from 'effect';
|
|
8
9
|
import React, { useCallback, useState } from 'react';
|
|
9
10
|
|
|
10
11
|
import { ContactType } from '@dxos/client/testing';
|
|
11
|
-
import {
|
|
12
|
+
import { type BaseObject, Expando, Format, getDXN, Ref, type TypeAnnotation } from '@dxos/echo-schema';
|
|
12
13
|
import { live } from '@dxos/live-object';
|
|
14
|
+
import { withSurfaceVariantsLayout } from '@dxos/react-ui/testing';
|
|
13
15
|
import { Testing } from '@dxos/schema/testing';
|
|
14
|
-
import {
|
|
16
|
+
import { withTheme } from '@dxos/storybook-utils';
|
|
15
17
|
|
|
16
18
|
import { SelectInput } from './Defaults';
|
|
17
19
|
import { Form, type FormProps } from './Form';
|
|
18
20
|
import translations from '../../translations';
|
|
19
21
|
import { TestLayout, TestPanel } from '../testing';
|
|
20
22
|
|
|
21
|
-
const AddressSchema =
|
|
22
|
-
street:
|
|
23
|
-
city:
|
|
24
|
-
zip:
|
|
25
|
-
location:
|
|
26
|
-
}).annotations({
|
|
23
|
+
const AddressSchema = Schema.Struct({
|
|
24
|
+
street: Schema.optional(Schema.String.annotations({ title: 'Street' })),
|
|
25
|
+
city: Schema.optional(Schema.String.annotations({ title: 'City' })),
|
|
26
|
+
zip: Schema.optional(Schema.String.pipe(Schema.pattern(/^\d{5}(-\d{4})?$/)).annotations({ title: 'ZIP' })),
|
|
27
|
+
location: Schema.optional(Format.GeoPoint.annotations({ title: 'Location' })),
|
|
28
|
+
}).annotations({ title: 'Address' });
|
|
27
29
|
|
|
28
30
|
// TODO(burdon): Translations?
|
|
29
|
-
const TestSchema =
|
|
30
|
-
name:
|
|
31
|
-
active:
|
|
32
|
-
rank:
|
|
33
|
-
website:
|
|
34
|
-
address:
|
|
35
|
-
}).pipe(
|
|
31
|
+
const TestSchema = Schema.Struct({
|
|
32
|
+
name: Schema.optional(Schema.String.annotations({ title: 'Name' })),
|
|
33
|
+
active: Schema.optional(Schema.Boolean.annotations({ title: 'Active' })),
|
|
34
|
+
rank: Schema.optional(Schema.Number.annotations({ title: 'Rank' })),
|
|
35
|
+
website: Schema.optional(Format.URL.annotations({ title: 'Website' })),
|
|
36
|
+
address: Schema.optional(AddressSchema),
|
|
37
|
+
}).pipe(Schema.mutable);
|
|
36
38
|
|
|
37
|
-
type TestType =
|
|
39
|
+
type TestType = Schema.Schema.Type<typeof TestSchema>;
|
|
38
40
|
|
|
39
|
-
type StoryProps<T extends BaseObject> = { schema:
|
|
41
|
+
type StoryProps<T extends BaseObject> = { schema: Schema.Schema<T> } & FormProps<T>;
|
|
40
42
|
|
|
41
43
|
const DefaultStory = <T extends BaseObject>({ schema, values: initialValues, ...props }: StoryProps<T>) => {
|
|
42
44
|
const [values, setValues] = useState(initialValues);
|
|
@@ -44,6 +46,15 @@ const DefaultStory = <T extends BaseObject>({ schema, values: initialValues, ...
|
|
|
44
46
|
setValues(values);
|
|
45
47
|
}, []);
|
|
46
48
|
|
|
49
|
+
return <Form<T> schema={schema} values={values} onSave={handleSave} {...props} />;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const DebugStory = <T extends BaseObject>({ schema, values: initialValues, ...props }: StoryProps<T>) => {
|
|
53
|
+
const [values, setValues] = useState(initialValues);
|
|
54
|
+
const handleSave = useCallback<NonNullable<FormProps<T>['onSave']>>((values) => {
|
|
55
|
+
setValues(values);
|
|
56
|
+
}, []);
|
|
57
|
+
|
|
47
58
|
return (
|
|
48
59
|
<TestLayout json={{ values, schema: schema.ast.toJSON() }}>
|
|
49
60
|
<TestPanel>
|
|
@@ -56,11 +67,17 @@ const DefaultStory = <T extends BaseObject>({ schema, values: initialValues, ...
|
|
|
56
67
|
const meta: Meta<StoryProps<any>> = {
|
|
57
68
|
title: 'ui/react-ui-form/Form',
|
|
58
69
|
component: Form,
|
|
59
|
-
render:
|
|
60
|
-
decorators: [
|
|
70
|
+
render: DebugStory,
|
|
71
|
+
decorators: [withTheme],
|
|
61
72
|
parameters: {
|
|
62
73
|
translations,
|
|
63
74
|
},
|
|
75
|
+
argTypes: {
|
|
76
|
+
readonly: {
|
|
77
|
+
control: 'boolean',
|
|
78
|
+
description: 'Readonly',
|
|
79
|
+
},
|
|
80
|
+
},
|
|
64
81
|
};
|
|
65
82
|
|
|
66
83
|
export default meta;
|
|
@@ -68,6 +85,8 @@ export default meta;
|
|
|
68
85
|
type Story<T extends BaseObject> = StoryObj<StoryProps<T>>;
|
|
69
86
|
|
|
70
87
|
export const Default: Story<TestType> = {
|
|
88
|
+
render: DefaultStory,
|
|
89
|
+
decorators: [withSurfaceVariantsLayout(), withTheme],
|
|
71
90
|
args: {
|
|
72
91
|
schema: TestSchema,
|
|
73
92
|
values: {
|
|
@@ -77,6 +96,7 @@ export const Default: Story<TestType> = {
|
|
|
77
96
|
zip: '11205',
|
|
78
97
|
},
|
|
79
98
|
},
|
|
99
|
+
readonly: false,
|
|
80
100
|
},
|
|
81
101
|
};
|
|
82
102
|
|
|
@@ -85,8 +105,9 @@ export const Organization: Story<Testing.Organization> = {
|
|
|
85
105
|
schema: Testing.OrganizationSchema,
|
|
86
106
|
values: {
|
|
87
107
|
name: 'DXOS',
|
|
88
|
-
|
|
108
|
+
website: 'https://dxos.org',
|
|
89
109
|
},
|
|
110
|
+
readonly: false,
|
|
90
111
|
},
|
|
91
112
|
};
|
|
92
113
|
|
|
@@ -95,9 +116,10 @@ export const OrganizationAutoSave: Story<Testing.Organization> = {
|
|
|
95
116
|
schema: Testing.OrganizationSchema,
|
|
96
117
|
values: {
|
|
97
118
|
name: 'DXOS',
|
|
98
|
-
|
|
119
|
+
website: 'https://dxos.org',
|
|
99
120
|
},
|
|
100
121
|
autoSave: true,
|
|
122
|
+
readonly: false,
|
|
101
123
|
},
|
|
102
124
|
};
|
|
103
125
|
|
|
@@ -113,96 +135,58 @@ export const OrganizationAutoSave: Story<Testing.Organization> = {
|
|
|
113
135
|
// },
|
|
114
136
|
// };
|
|
115
137
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
S.Union(
|
|
123
|
-
S.Struct({
|
|
124
|
-
type: S.Literal('circle').annotations({ [AST.TitleAnnotationId]: 'Type' }),
|
|
125
|
-
radius: S.optional(S.Number.annotations({ [AST.TitleAnnotationId]: 'Radius' })),
|
|
138
|
+
const ShapeSchema = Schema.Struct({
|
|
139
|
+
shape: Schema.optional(
|
|
140
|
+
Schema.Union(
|
|
141
|
+
Schema.Struct({
|
|
142
|
+
type: Schema.Literal('circle').annotations({ title: 'Type' }),
|
|
143
|
+
radius: Schema.optional(Schema.Number.annotations({ title: 'Radius' })),
|
|
126
144
|
}),
|
|
127
|
-
|
|
128
|
-
type:
|
|
129
|
-
size:
|
|
145
|
+
Schema.Struct({
|
|
146
|
+
type: Schema.Literal('square').annotations({ title: 'Type' }),
|
|
147
|
+
size: Schema.optional(Schema.Number.pipe(Schema.nonNegative()).annotations({ title: 'Size' })),
|
|
130
148
|
}),
|
|
131
|
-
).annotations({
|
|
149
|
+
).annotations({ title: 'Shape' }),
|
|
132
150
|
),
|
|
133
|
-
}).pipe(
|
|
151
|
+
}).pipe(Schema.mutable);
|
|
134
152
|
|
|
135
|
-
type ShapeType =
|
|
153
|
+
type ShapeType = Schema.Schema.Type<typeof ShapeSchema>;
|
|
136
154
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
const DiscriminatedUnionStory = ({ values: initialValues }: DiscriminatedUnionStoryProps) => {
|
|
140
|
-
const [values, setValues] = useState(initialValues);
|
|
141
|
-
const handleSave = useCallback<NonNullable<FormProps<ShapeType>['onSave']>>((values) => {
|
|
142
|
-
setValues(values);
|
|
143
|
-
}, []);
|
|
144
|
-
|
|
145
|
-
return (
|
|
146
|
-
<TestLayout json={{ values, schema: ShapeSchema.ast.toJSON() }}>
|
|
147
|
-
<TestPanel>
|
|
148
|
-
<Form<ShapeType>
|
|
149
|
-
schema={ShapeSchema}
|
|
150
|
-
values={values}
|
|
151
|
-
onSave={handleSave}
|
|
152
|
-
Custom={{
|
|
153
|
-
['shape.type' as const]: (props) => (
|
|
154
|
-
<SelectInput
|
|
155
|
-
{...props}
|
|
156
|
-
options={['circle', 'square'].map((value) => ({
|
|
157
|
-
value,
|
|
158
|
-
label: value,
|
|
159
|
-
}))}
|
|
160
|
-
/>
|
|
161
|
-
),
|
|
162
|
-
}}
|
|
163
|
-
/>
|
|
164
|
-
</TestPanel>
|
|
165
|
-
</TestLayout>
|
|
166
|
-
);
|
|
167
|
-
};
|
|
168
|
-
|
|
169
|
-
export const DiscriminatedShape: StoryObj<DiscriminatedUnionStoryProps> = {
|
|
170
|
-
render: DiscriminatedUnionStory,
|
|
155
|
+
export const DiscriminatedShape: Story<ShapeType> = {
|
|
171
156
|
args: {
|
|
157
|
+
schema: ShapeSchema,
|
|
158
|
+
readonly: false,
|
|
172
159
|
values: {
|
|
173
160
|
shape: {
|
|
174
161
|
type: 'circle',
|
|
175
162
|
radius: 5,
|
|
176
163
|
},
|
|
177
164
|
},
|
|
165
|
+
Custom: {
|
|
166
|
+
['shape.type' as const]: (props) => (
|
|
167
|
+
<SelectInput
|
|
168
|
+
{...props}
|
|
169
|
+
options={['circle', 'square'].map((value) => ({
|
|
170
|
+
value,
|
|
171
|
+
label: value,
|
|
172
|
+
}))}
|
|
173
|
+
/>
|
|
174
|
+
),
|
|
175
|
+
},
|
|
178
176
|
},
|
|
179
177
|
};
|
|
180
178
|
|
|
181
|
-
const ArraysSchema =
|
|
182
|
-
names:
|
|
183
|
-
addresses:
|
|
184
|
-
}).pipe(
|
|
179
|
+
const ArraysSchema = Schema.Struct({
|
|
180
|
+
names: Schema.Array(Schema.String.pipe(Schema.nonEmptyString())),
|
|
181
|
+
addresses: Schema.Array(AddressSchema),
|
|
182
|
+
}).pipe(Schema.mutable);
|
|
185
183
|
|
|
186
|
-
type ArraysType =
|
|
187
|
-
|
|
188
|
-
const ArraysStory = ({ values: initialValues }: FormProps<ArraysType>) => {
|
|
189
|
-
const [values, setValues] = useState(initialValues);
|
|
190
|
-
const handleSave = useCallback<NonNullable<FormProps<ArraysType>['onSave']>>((values) => {
|
|
191
|
-
setValues(values);
|
|
192
|
-
}, []);
|
|
193
|
-
|
|
194
|
-
return (
|
|
195
|
-
<TestLayout json={{ values, schema: ArraysSchema.ast.toJSON() }}>
|
|
196
|
-
<TestPanel>
|
|
197
|
-
<Form<ArraysType> schema={ArraysSchema} values={values} onSave={handleSave} />
|
|
198
|
-
</TestPanel>
|
|
199
|
-
</TestLayout>
|
|
200
|
-
);
|
|
201
|
-
};
|
|
184
|
+
type ArraysType = Schema.Schema.Type<typeof ArraysSchema>;
|
|
202
185
|
|
|
203
186
|
export const Arrays: StoryObj<FormProps<ArraysType>> = {
|
|
204
|
-
render: ArraysStory,
|
|
205
187
|
args: {
|
|
188
|
+
schema: ArraysSchema,
|
|
189
|
+
readonly: false,
|
|
206
190
|
values: {
|
|
207
191
|
names: ['Alice', 'Bob'],
|
|
208
192
|
addresses: [],
|
|
@@ -210,53 +194,40 @@ export const Arrays: StoryObj<FormProps<ArraysType>> = {
|
|
|
210
194
|
},
|
|
211
195
|
};
|
|
212
196
|
|
|
213
|
-
const ColorSchema =
|
|
214
|
-
color:
|
|
215
|
-
|
|
197
|
+
const ColorSchema = Schema.Struct({
|
|
198
|
+
color: Schema.Union(Schema.Literal('red'), Schema.Literal('green'), Schema.Literal('blue')).annotations({
|
|
199
|
+
title: 'Color',
|
|
216
200
|
}),
|
|
217
|
-
}).pipe(
|
|
218
|
-
|
|
219
|
-
type ColorType = S.Schema.Type<typeof ColorSchema>;
|
|
220
|
-
|
|
221
|
-
const EnumStory = ({ values: initialValues }: FormProps<ColorType>) => {
|
|
222
|
-
const [values, setValues] = useState(initialValues);
|
|
223
|
-
const handleSave = useCallback<NonNullable<FormProps<ColorType>['onSave']>>((values) => {
|
|
224
|
-
setValues(values);
|
|
225
|
-
}, []);
|
|
201
|
+
}).pipe(Schema.mutable);
|
|
226
202
|
|
|
227
|
-
|
|
228
|
-
<TestLayout json={{ values, schema: ColorSchema.ast.toJSON() }}>
|
|
229
|
-
<TestPanel>
|
|
230
|
-
<Form<ColorType> schema={ColorSchema} values={values} onSave={handleSave} />
|
|
231
|
-
</TestPanel>
|
|
232
|
-
</TestLayout>
|
|
233
|
-
);
|
|
234
|
-
};
|
|
203
|
+
type ColorType = Schema.Schema.Type<typeof ColorSchema>;
|
|
235
204
|
|
|
236
205
|
export const Enum: StoryObj<FormProps<ColorType>> = {
|
|
237
|
-
render: EnumStory,
|
|
238
206
|
args: {
|
|
207
|
+
schema: ColorSchema,
|
|
208
|
+
readonly: false,
|
|
239
209
|
values: {
|
|
240
210
|
color: 'red',
|
|
241
211
|
},
|
|
242
212
|
},
|
|
243
213
|
};
|
|
244
214
|
|
|
245
|
-
const RefSchema =
|
|
215
|
+
const RefSchema = Schema.Struct({
|
|
246
216
|
contact: Ref(ContactType).annotations({ title: 'Contact Reference' }),
|
|
247
|
-
optionalContact:
|
|
248
|
-
|
|
217
|
+
optionalContact: Schema.optional(Ref(ContactType).annotations({ title: 'Optional Contact Reference' })),
|
|
218
|
+
refArray: Schema.optional(Schema.Array(Ref(ContactType))),
|
|
219
|
+
unknownExpando: Schema.optional(Ref(Expando).annotations({ title: 'Optional Ref to an Expando (DXN Input)' })),
|
|
249
220
|
});
|
|
250
221
|
|
|
251
|
-
const
|
|
222
|
+
const contact1 = live(ContactType, { identifiers: [] });
|
|
223
|
+
const contact2 = live(ContactType, { identifiers: [] });
|
|
224
|
+
|
|
225
|
+
const RefStory = ({ values: initialValues, readonly }: FormProps<any>) => {
|
|
252
226
|
const [values, setValues] = useState(initialValues);
|
|
253
227
|
const handleSave = useCallback<NonNullable<FormProps<any>['onSave']>>((values) => {
|
|
254
228
|
setValues(values);
|
|
255
229
|
}, []);
|
|
256
230
|
|
|
257
|
-
const contact1 = live(ContactType, { identifiers: [] });
|
|
258
|
-
const contact2 = live(ContactType, { identifiers: [] });
|
|
259
|
-
|
|
260
231
|
const onQueryRefOptions = useCallback((typeInfo: TypeAnnotation) => {
|
|
261
232
|
switch (typeInfo.typename) {
|
|
262
233
|
case ContactType.typename:
|
|
@@ -272,7 +243,13 @@ const RefStory = ({ values: initialValues }: FormProps<any>) => {
|
|
|
272
243
|
return (
|
|
273
244
|
<TestLayout json={{ values, schema: RefSchema.ast.toJSON() }}>
|
|
274
245
|
<TestPanel>
|
|
275
|
-
<Form
|
|
246
|
+
<Form
|
|
247
|
+
schema={RefSchema}
|
|
248
|
+
values={values}
|
|
249
|
+
readonly={readonly}
|
|
250
|
+
onSave={handleSave}
|
|
251
|
+
onQueryRefOptions={onQueryRefOptions}
|
|
252
|
+
/>
|
|
276
253
|
</TestPanel>
|
|
277
254
|
</TestLayout>
|
|
278
255
|
);
|
|
@@ -280,5 +257,10 @@ const RefStory = ({ values: initialValues }: FormProps<any>) => {
|
|
|
280
257
|
|
|
281
258
|
export const Refs: StoryObj<FormProps<ContactType>> = {
|
|
282
259
|
render: RefStory,
|
|
283
|
-
args: {
|
|
260
|
+
args: {
|
|
261
|
+
readonly: false,
|
|
262
|
+
values: {
|
|
263
|
+
refArray: [Ref.make(contact1), Ref.make(contact2)],
|
|
264
|
+
} as any,
|
|
265
|
+
},
|
|
284
266
|
};
|
|
@@ -2,9 +2,10 @@
|
|
|
2
2
|
// Copyright 2024 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
+
import { type Schema } from 'effect';
|
|
5
6
|
import React, { type ReactElement, useEffect, useMemo, useRef } from 'react';
|
|
6
7
|
|
|
7
|
-
import { type BaseObject, type
|
|
8
|
+
import { type BaseObject, type PropertyKey } from '@dxos/echo-schema';
|
|
8
9
|
import { type ThemedClassName } from '@dxos/react-ui';
|
|
9
10
|
import { mx } from '@dxos/react-ui-theme';
|
|
10
11
|
import { type SchemaProperty } from '@dxos/schema';
|
|
@@ -13,14 +14,13 @@ import { FormActions } from './FormActions';
|
|
|
13
14
|
import { FormFields } from './FormContent';
|
|
14
15
|
import { FormProvider } from './FormContext';
|
|
15
16
|
import { type InputProps, type InputComponent } from './Input';
|
|
16
|
-
import { type QueryRefOptions } from '
|
|
17
|
-
import { type FormOptions } from '../../hooks';
|
|
17
|
+
import { type FormOptions, type QueryRefOptions } from '../../hooks';
|
|
18
18
|
|
|
19
19
|
export type PropsFilter<T extends BaseObject> = (props: SchemaProperty<T>[]) => SchemaProperty<T>[];
|
|
20
20
|
|
|
21
21
|
export type ComponentLookup = (args: {
|
|
22
22
|
prop: string;
|
|
23
|
-
schema:
|
|
23
|
+
schema: Schema.Schema<any>;
|
|
24
24
|
inputProps: InputProps;
|
|
25
25
|
}) => ReactElement | undefined;
|
|
26
26
|
|
|
@@ -95,8 +95,7 @@ export const Form = <T extends BaseObject>({
|
|
|
95
95
|
onValid={onValid}
|
|
96
96
|
onSave={onSave}
|
|
97
97
|
>
|
|
98
|
-
{
|
|
99
|
-
<div ref={formRef} role='none' className={mx('p-2', classNames)} data-testid={testId}>
|
|
98
|
+
<div ref={formRef} role='none' className={mx('flex flex-col gap-4', classNames)} data-testid={testId}>
|
|
100
99
|
<FormFields
|
|
101
100
|
schema={schema}
|
|
102
101
|
path={path}
|
|
@@ -5,7 +5,6 @@
|
|
|
5
5
|
import React from 'react';
|
|
6
6
|
|
|
7
7
|
import { IconButton, useTranslation } from '@dxos/react-ui';
|
|
8
|
-
import { mx } from '@dxos/react-ui-theme';
|
|
9
8
|
|
|
10
9
|
import { useFormContext } from './FormContext';
|
|
11
10
|
import { translationKey } from '../../translations';
|
|
@@ -15,27 +14,25 @@ export type FormActionsProps = {
|
|
|
15
14
|
onCancel?: () => void;
|
|
16
15
|
};
|
|
17
16
|
|
|
18
|
-
export const FormActions = ({
|
|
17
|
+
export const FormActions = ({ readonly, onCancel }: FormActionsProps) => {
|
|
19
18
|
const { t } = useTranslation(translationKey);
|
|
20
19
|
const { canSave, handleSave } = useFormContext();
|
|
21
20
|
|
|
22
21
|
return (
|
|
23
|
-
<div role='none' className='
|
|
24
|
-
|
|
25
|
-
{
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
)}
|
|
38
|
-
</div>
|
|
22
|
+
<div role='none' className='flex [&_button]:grow gap-1'>
|
|
23
|
+
{onCancel && !readonly && (
|
|
24
|
+
<IconButton data-testid='cancel-button' icon='ph--x--regular' label={t('button cancel')} onClick={onCancel} />
|
|
25
|
+
)}
|
|
26
|
+
{handleSave && (
|
|
27
|
+
<IconButton
|
|
28
|
+
type='submit'
|
|
29
|
+
data-testid='save-button'
|
|
30
|
+
disabled={!canSave}
|
|
31
|
+
icon='ph--check--regular'
|
|
32
|
+
label={t('button save')}
|
|
33
|
+
onClick={handleSave}
|
|
34
|
+
/>
|
|
35
|
+
)}
|
|
39
36
|
</div>
|
|
40
37
|
);
|
|
41
38
|
};
|
|
@@ -2,12 +2,12 @@
|
|
|
2
2
|
// Copyright 2025 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import { pipe } from 'effect';
|
|
5
|
+
import { Schema, SchemaAST, pipe } from 'effect';
|
|
6
6
|
import { capitalize } from 'effect/String';
|
|
7
7
|
import React, { useMemo } from 'react';
|
|
8
8
|
|
|
9
|
-
import { AST, S } from '@dxos/echo-schema';
|
|
10
9
|
import { createJsonPath, findNode, getDiscriminatedType, isDiscriminatedUnion } from '@dxos/effect';
|
|
10
|
+
import { type ThemedClassName } from '@dxos/react-ui';
|
|
11
11
|
import { mx } from '@dxos/react-ui-theme';
|
|
12
12
|
import { getSchemaProperties, type SchemaProperty } from '@dxos/schema';
|
|
13
13
|
import { isNotFalsy } from '@dxos/util';
|
|
@@ -17,8 +17,10 @@ import { SelectInput } from './Defaults';
|
|
|
17
17
|
import { type ComponentLookup } from './Form';
|
|
18
18
|
import { useInputProps, useFormValues } from './FormContext';
|
|
19
19
|
import { type InputComponent } from './Input';
|
|
20
|
-
import { RefField
|
|
20
|
+
import { RefField } from './RefField';
|
|
21
21
|
import { getInputComponent } from './factory';
|
|
22
|
+
import { type QueryRefOptions } from '../../hooks';
|
|
23
|
+
import { getRefProps } from '../../util';
|
|
22
24
|
|
|
23
25
|
export type FormFieldProps = {
|
|
24
26
|
property: SchemaProperty<any>;
|
|
@@ -49,9 +51,13 @@ export const FormField = ({
|
|
|
49
51
|
[examples, description],
|
|
50
52
|
);
|
|
51
53
|
|
|
54
|
+
//
|
|
55
|
+
// Registry and Custom.
|
|
56
|
+
//
|
|
57
|
+
|
|
52
58
|
const FoundComponent = lookupComponent?.({
|
|
53
59
|
prop: name,
|
|
54
|
-
schema:
|
|
60
|
+
schema: Schema.make(ast),
|
|
55
61
|
inputProps: {
|
|
56
62
|
type,
|
|
57
63
|
format,
|
|
@@ -63,7 +69,7 @@ export const FormField = ({
|
|
|
63
69
|
});
|
|
64
70
|
|
|
65
71
|
if (FoundComponent) {
|
|
66
|
-
return
|
|
72
|
+
return FoundComponent;
|
|
67
73
|
}
|
|
68
74
|
|
|
69
75
|
const jsonPath = createJsonPath(path ?? []);
|
|
@@ -82,79 +88,97 @@ export const FormField = ({
|
|
|
82
88
|
);
|
|
83
89
|
}
|
|
84
90
|
|
|
91
|
+
//
|
|
92
|
+
// Refs.
|
|
93
|
+
//
|
|
94
|
+
|
|
95
|
+
const refProps = getRefProps(property);
|
|
96
|
+
if (refProps) {
|
|
97
|
+
return (
|
|
98
|
+
<RefField
|
|
99
|
+
ast={refProps.ast}
|
|
100
|
+
array={refProps.isArray}
|
|
101
|
+
type={type}
|
|
102
|
+
format={format}
|
|
103
|
+
label={label}
|
|
104
|
+
placeholder={placeholder}
|
|
105
|
+
disabled={readonly}
|
|
106
|
+
inputOnly={inline}
|
|
107
|
+
onQueryRefOptions={onQueryRefOptions}
|
|
108
|
+
{...inputProps}
|
|
109
|
+
/>
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
//
|
|
114
|
+
// Arrays.
|
|
115
|
+
//
|
|
116
|
+
|
|
85
117
|
if (array) {
|
|
86
118
|
return <ArrayField property={property} path={path} inputProps={inputProps} readonly={readonly} Custom={Custom} />;
|
|
87
119
|
}
|
|
88
120
|
|
|
121
|
+
//
|
|
122
|
+
// Standard Inputs.
|
|
123
|
+
//
|
|
124
|
+
|
|
89
125
|
const InputComponent = getInputComponent(type, format);
|
|
90
126
|
if (InputComponent) {
|
|
91
127
|
return (
|
|
92
|
-
<
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
/>
|
|
102
|
-
</div>
|
|
128
|
+
<InputComponent
|
|
129
|
+
type={type}
|
|
130
|
+
format={format}
|
|
131
|
+
label={label}
|
|
132
|
+
inputOnly={inline}
|
|
133
|
+
placeholder={placeholder}
|
|
134
|
+
disabled={readonly}
|
|
135
|
+
{...inputProps}
|
|
136
|
+
/>
|
|
103
137
|
);
|
|
104
138
|
}
|
|
105
139
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
<SelectInput
|
|
110
|
-
type={type}
|
|
111
|
-
format={format}
|
|
112
|
-
disabled={readonly}
|
|
113
|
-
inputOnly={inline}
|
|
114
|
-
label={label}
|
|
115
|
-
options={options.map((option) => ({ value: option, label: String(option) }))}
|
|
116
|
-
placeholder={placeholder}
|
|
117
|
-
{...inputProps}
|
|
118
|
-
/>
|
|
119
|
-
</div>
|
|
120
|
-
);
|
|
121
|
-
}
|
|
140
|
+
//
|
|
141
|
+
// Select.
|
|
142
|
+
//
|
|
122
143
|
|
|
123
|
-
|
|
124
|
-
if (format === 'ref') {
|
|
144
|
+
if (options) {
|
|
125
145
|
return (
|
|
126
|
-
<
|
|
127
|
-
ast={ast}
|
|
146
|
+
<SelectInput
|
|
128
147
|
type={type}
|
|
148
|
+
format={format}
|
|
149
|
+
disabled={readonly}
|
|
150
|
+
inputOnly={inline}
|
|
129
151
|
label={label}
|
|
130
|
-
|
|
152
|
+
options={options.map((option) => ({ value: option, label: String(option) }))}
|
|
131
153
|
placeholder={placeholder}
|
|
132
|
-
|
|
133
|
-
onQueryRefOptions={onQueryRefOptions}
|
|
134
|
-
inputProps={inputProps}
|
|
154
|
+
{...inputProps}
|
|
135
155
|
/>
|
|
136
156
|
);
|
|
137
157
|
}
|
|
138
158
|
|
|
159
|
+
//
|
|
160
|
+
// Nested Objects.
|
|
161
|
+
//
|
|
162
|
+
|
|
139
163
|
if (type === 'object') {
|
|
140
164
|
const baseNode = findNode(ast, isDiscriminatedUnion);
|
|
141
165
|
const typeLiteral = baseNode
|
|
142
166
|
? getDiscriminatedType(baseNode, inputProps.getValue() as any)
|
|
143
|
-
: findNode(ast,
|
|
167
|
+
: findNode(ast, SchemaAST.isTypeLiteral);
|
|
144
168
|
|
|
145
169
|
if (typeLiteral) {
|
|
146
170
|
return (
|
|
147
|
-
|
|
148
|
-
{!inline && <h3 className='text-lg mbs-2 mbe-1'>{label}</h3>}
|
|
171
|
+
<>
|
|
172
|
+
{!inline && <h3 className='text-lg mbs-2 mbe-1 first:mbs-0'>{label}</h3>}
|
|
149
173
|
<FormFields
|
|
150
|
-
schema={
|
|
174
|
+
schema={Schema.make(typeLiteral)}
|
|
151
175
|
path={path}
|
|
152
176
|
readonly={readonly}
|
|
153
177
|
onQueryRefOptions={onQueryRefOptions}
|
|
154
178
|
Custom={Custom}
|
|
155
179
|
lookupComponent={lookupComponent}
|
|
156
180
|
/>
|
|
157
|
-
|
|
181
|
+
</>
|
|
158
182
|
);
|
|
159
183
|
}
|
|
160
184
|
}
|
|
@@ -162,27 +186,28 @@ export const FormField = ({
|
|
|
162
186
|
return null;
|
|
163
187
|
};
|
|
164
188
|
|
|
165
|
-
export type
|
|
166
|
-
|
|
189
|
+
export type FormFieldsProps = ThemedClassName<{
|
|
190
|
+
Custom?: Partial<Record<string, InputComponent>>;
|
|
191
|
+
schema: Schema.Schema.All;
|
|
167
192
|
path?: (string | number)[];
|
|
168
193
|
filter?: (props: SchemaProperty<any>[]) => SchemaProperty<any>[];
|
|
169
194
|
sort?: string[];
|
|
170
195
|
readonly?: boolean;
|
|
171
|
-
onQueryRefOptions?: QueryRefOptions;
|
|
172
196
|
lookupComponent?: ComponentLookup;
|
|
173
|
-
|
|
174
|
-
}
|
|
197
|
+
onQueryRefOptions?: QueryRefOptions;
|
|
198
|
+
}>;
|
|
175
199
|
|
|
176
200
|
export const FormFields = ({
|
|
201
|
+
classNames,
|
|
202
|
+
Custom,
|
|
177
203
|
schema,
|
|
178
204
|
path,
|
|
179
205
|
filter,
|
|
180
206
|
sort,
|
|
181
207
|
readonly,
|
|
182
|
-
onQueryRefOptions,
|
|
183
208
|
lookupComponent,
|
|
184
|
-
|
|
185
|
-
}:
|
|
209
|
+
onQueryRefOptions,
|
|
210
|
+
}: FormFieldsProps) => {
|
|
186
211
|
const values = useFormValues(path);
|
|
187
212
|
|
|
188
213
|
const properties = useMemo(() => {
|
|
@@ -193,7 +218,8 @@ export const FormFields = ({
|
|
|
193
218
|
}, [schema, values, filter, sort]);
|
|
194
219
|
|
|
195
220
|
return (
|
|
196
|
-
|
|
221
|
+
// TODO(burdon): Consistent padding (with composite forms).
|
|
222
|
+
<div role='form' className={mx('p-2 is-full', classNames)}>
|
|
197
223
|
{properties
|
|
198
224
|
.map((property) => {
|
|
199
225
|
return (
|