@dxos/react-ui-form 0.8.4-main.b97322e → 0.8.4-main.dedc0f3
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 +601 -528
- package/dist/lib/browser/index.mjs.map +4 -4
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/node-esm/index.mjs +601 -528
- 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 +2 -2
- package/dist/types/src/components/ControlSection/ControlSection.d.ts.map +1 -1
- package/dist/types/src/components/ControlSection/ControlSection.stories.d.ts +79 -4
- package/dist/types/src/components/ControlSection/ControlSection.stories.d.ts.map +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/FieldEditor/FieldEditor.stories.d.ts +74 -4
- package/dist/types/src/components/FieldEditor/FieldEditor.stories.d.ts.map +1 -1
- package/dist/types/src/components/Form/ArrayField.d.ts +2 -2
- package/dist/types/src/components/Form/ArrayField.d.ts.map +1 -1
- package/dist/types/src/components/Form/Defaults.d.ts +5 -5
- package/dist/types/src/components/Form/Defaults.d.ts.map +1 -1
- package/dist/types/src/components/Form/Form.d.ts +5 -4
- package/dist/types/src/components/Form/Form.d.ts.map +1 -1
- package/dist/types/src/components/Form/Form.stories.d.ts +126 -30
- package/dist/types/src/components/Form/Form.stories.d.ts.map +1 -1
- package/dist/types/src/components/Form/FormActions.d.ts.map +1 -1
- package/dist/types/src/components/Form/FormContent.d.ts +16 -10
- package/dist/types/src/components/Form/FormContent.d.ts.map +1 -1
- package/dist/types/src/components/Form/FormContext.d.ts +1 -1
- package/dist/types/src/components/Form/FormContext.d.ts.map +1 -1
- package/dist/types/src/components/Form/Input.d.ts +4 -1
- package/dist/types/src/components/Form/Input.d.ts.map +1 -1
- package/dist/types/src/components/Form/RefField.d.ts +10 -4
- package/dist/types/src/components/Form/RefField.d.ts.map +1 -1
- package/dist/types/src/components/Form/custom/GeoPointInput.d.ts +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 +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 +74 -3
- package/dist/types/src/components/Form/custom/SelectOptionsInput.stories.d.ts.map +1 -1
- package/dist/types/src/components/Form/custom/TupleInput.d.ts +1 -1
- package/dist/types/src/components/Form/factory.d.ts.map +1 -1
- package/dist/types/src/components/ViewEditor/ViewEditor.d.ts +3 -2
- package/dist/types/src/components/ViewEditor/ViewEditor.d.ts.map +1 -1
- package/dist/types/src/components/ViewEditor/ViewEditor.stories.d.ts +76 -5
- package/dist/types/src/components/ViewEditor/ViewEditor.stories.d.ts.map +1 -1
- package/dist/types/src/components/index.d.ts +0 -1
- package/dist/types/src/components/index.d.ts.map +1 -1
- package/dist/types/src/components/testing.d.ts +6 -3
- package/dist/types/src/components/testing.d.ts.map +1 -1
- package/dist/types/src/hooks/useForm.d.ts +1 -1
- package/dist/types/src/hooks/useForm.d.ts.map +1 -1
- package/dist/types/src/translations.d.ts +9 -4
- package/dist/types/src/translations.d.ts.map +1 -1
- package/dist/types/src/util/field.d.ts.map +1 -1
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/package.json +35 -30
- package/src/components/ControlSection/ControlSection.stories.tsx +8 -10
- package/src/components/ControlSection/ControlSection.tsx +26 -12
- package/src/components/FieldEditor/FieldEditor.stories.tsx +8 -9
- package/src/components/FieldEditor/FieldEditor.test.tsx +3 -2
- package/src/components/FieldEditor/FieldEditor.tsx +22 -17
- package/src/components/Form/ArrayField.tsx +8 -8
- package/src/components/Form/Defaults.tsx +22 -20
- package/src/components/Form/Form.stories.tsx +129 -93
- package/src/components/Form/Form.tsx +16 -12
- package/src/components/Form/FormActions.tsx +2 -1
- package/src/components/Form/FormContent.tsx +96 -34
- package/src/components/Form/FormContext.tsx +5 -5
- package/src/components/Form/Input.tsx +4 -1
- package/src/components/Form/RefField.tsx +126 -40
- package/src/components/Form/custom/GeoPointInput.tsx +29 -21
- package/src/components/Form/custom/SelectOptionsInput.stories.tsx +7 -6
- package/src/components/Form/custom/SelectOptionsInput.tsx +29 -30
- package/src/components/Form/custom/TupleInput.tsx +2 -2
- package/src/components/Form/factory.tsx +1 -1
- package/src/components/ViewEditor/ViewEditor.stories.tsx +50 -23
- package/src/components/ViewEditor/ViewEditor.test.tsx +7 -10
- package/src/components/ViewEditor/ViewEditor.tsx +151 -132
- package/src/components/index.ts +0 -2
- package/src/components/testing.tsx +15 -9
- package/src/hooks/useForm.ts +5 -3
- package/src/hooks/useRefQueryLookupHandler.ts +1 -1
- package/src/translations.ts +11 -5
- package/src/util/field.ts +8 -18
- package/dist/types/src/components/deprecated/Form.d.ts +0 -15
- package/dist/types/src/components/deprecated/Form.d.ts.map +0 -1
- package/dist/types/src/components/deprecated/Form.stories.d.ts +0 -13
- package/dist/types/src/components/deprecated/Form.stories.d.ts.map +0 -1
- package/dist/types/src/components/deprecated/FormInput.d.ts +0 -16
- package/dist/types/src/components/deprecated/FormInput.d.ts.map +0 -1
- package/dist/types/src/components/deprecated/index.d.ts +0 -2
- package/dist/types/src/components/deprecated/index.d.ts.map +0 -1
- package/src/components/deprecated/Form.stories.tsx +0 -62
- package/src/components/deprecated/Form.tsx +0 -119
- package/src/components/deprecated/FormInput.tsx +0 -71
- package/src/components/deprecated/index.ts +0 -5
|
@@ -9,65 +9,68 @@ import { Schema } from 'effect';
|
|
|
9
9
|
import React, { useCallback, useState } from 'react';
|
|
10
10
|
|
|
11
11
|
import { ContactType } from '@dxos/client/testing';
|
|
12
|
-
import { type
|
|
12
|
+
import { type Type } from '@dxos/echo';
|
|
13
|
+
import { type BaseObject, Expando, Format, Ref, type TypeAnnotation, getObjectDXN } from '@dxos/echo-schema';
|
|
13
14
|
import { live } from '@dxos/live-object';
|
|
14
15
|
import { withSurfaceVariantsLayout } from '@dxos/react-ui/testing';
|
|
15
16
|
import { Testing } from '@dxos/schema/testing';
|
|
16
17
|
import { withLayout, withTheme } from '@dxos/storybook-utils';
|
|
17
18
|
|
|
18
|
-
import { SelectInput } from './Defaults';
|
|
19
|
-
import { Form, type FormProps } from './Form';
|
|
20
19
|
import { translations } from '../../translations';
|
|
21
20
|
import { TestLayout, TestPanel } from '../testing';
|
|
22
21
|
|
|
23
|
-
|
|
24
|
-
|
|
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' });
|
|
29
|
-
|
|
30
|
-
// TODO(burdon): Translations?
|
|
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);
|
|
38
|
-
|
|
39
|
-
type TestType = Schema.Schema.Type<typeof TestSchema>;
|
|
40
|
-
|
|
41
|
-
type StoryProps<T extends BaseObject> = { schema: Schema.Schema<T> } & FormProps<T>;
|
|
22
|
+
import { SelectInput } from './Defaults';
|
|
23
|
+
import { Form, type FormProps } from './Form';
|
|
42
24
|
|
|
43
|
-
|
|
25
|
+
type StoryProps<T extends BaseObject> = {
|
|
26
|
+
debug?: boolean;
|
|
27
|
+
schema: Type.Obj.Any;
|
|
28
|
+
} & FormProps<T>;
|
|
29
|
+
|
|
30
|
+
const DefaultStory = <T extends BaseObject = any>({
|
|
31
|
+
debug,
|
|
32
|
+
schema,
|
|
33
|
+
values: initialValues,
|
|
34
|
+
...props
|
|
35
|
+
}: StoryProps<T>) => {
|
|
44
36
|
const [values, setValues] = useState(initialValues);
|
|
45
37
|
const handleSave = useCallback<NonNullable<FormProps<T>['onSave']>>((values) => {
|
|
46
38
|
setValues(values);
|
|
47
39
|
}, []);
|
|
48
40
|
|
|
41
|
+
if (debug) {
|
|
42
|
+
return (
|
|
43
|
+
<TestLayout json={{ values, schema: schema.ast }}>
|
|
44
|
+
<TestPanel>
|
|
45
|
+
<Form<T> schema={schema} values={values} onSave={handleSave} {...props} />
|
|
46
|
+
</TestPanel>
|
|
47
|
+
</TestLayout>
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
49
51
|
return <Form<T> schema={schema} values={values} onSave={handleSave} {...props} />;
|
|
50
52
|
};
|
|
51
53
|
|
|
52
|
-
const
|
|
53
|
-
const
|
|
54
|
-
|
|
55
|
-
|
|
54
|
+
const RefStory = <T extends BaseObject = any>(props: StoryProps<T>) => {
|
|
55
|
+
const onQueryRefOptions = useCallback((typeInfo: TypeAnnotation) => {
|
|
56
|
+
switch (typeInfo.typename) {
|
|
57
|
+
case ContactType.typename:
|
|
58
|
+
return [
|
|
59
|
+
{ dxn: getObjectDXN(contact1)!, label: 'John Coltraine' },
|
|
60
|
+
{ dxn: getObjectDXN(contact2)!, label: 'Erykah Badu' },
|
|
61
|
+
];
|
|
62
|
+
default:
|
|
63
|
+
return [];
|
|
64
|
+
}
|
|
56
65
|
}, []);
|
|
57
66
|
|
|
58
|
-
return
|
|
59
|
-
<TestLayout json={{ values, schema: schema.ast.toJSON() }}>
|
|
60
|
-
<TestPanel>
|
|
61
|
-
<Form<T> schema={schema} values={values} onSave={handleSave} {...props} />
|
|
62
|
-
</TestPanel>
|
|
63
|
-
</TestLayout>
|
|
64
|
-
);
|
|
67
|
+
return <DefaultStory<T> onQueryRefOptions={onQueryRefOptions} {...props} />;
|
|
65
68
|
};
|
|
66
69
|
|
|
67
|
-
const meta
|
|
70
|
+
const meta = {
|
|
68
71
|
title: 'ui/react-ui-form/Form',
|
|
69
|
-
component: Form
|
|
70
|
-
render:
|
|
72
|
+
component: Form as any,
|
|
73
|
+
render: DefaultStory,
|
|
71
74
|
decorators: [withLayout({ fullscreen: true }), withTheme],
|
|
72
75
|
parameters: {
|
|
73
76
|
translations,
|
|
@@ -78,17 +81,33 @@ const meta: Meta<StoryProps<any>> = {
|
|
|
78
81
|
description: 'Readonly',
|
|
79
82
|
},
|
|
80
83
|
},
|
|
81
|
-
}
|
|
84
|
+
} satisfies Meta<StoryProps<any>>;
|
|
82
85
|
|
|
83
86
|
export default meta;
|
|
84
87
|
|
|
85
|
-
|
|
88
|
+
const AddressSchema = Schema.Struct({
|
|
89
|
+
street: Schema.optional(Schema.String.annotations({ title: 'Street' })),
|
|
90
|
+
city: Schema.optional(Schema.String.annotations({ title: 'City' })),
|
|
91
|
+
zip: Schema.optional(Schema.String.pipe(Schema.pattern(/^\d{5}(-\d{4})?$/)).annotations({ title: 'ZIP' })),
|
|
92
|
+
location: Schema.optional(Format.GeoPoint.annotations({ title: 'Location' })),
|
|
93
|
+
}).annotations({ title: 'Address' });
|
|
86
94
|
|
|
87
|
-
|
|
88
|
-
|
|
95
|
+
const ContactSchema = Schema.Struct({
|
|
96
|
+
name: Schema.optional(Schema.String.annotations({ title: 'Name' })),
|
|
97
|
+
active: Schema.optional(Schema.Boolean.annotations({ title: 'Active' })),
|
|
98
|
+
rank: Schema.optional(Schema.Number.annotations({ title: 'Rank' })),
|
|
99
|
+
website: Schema.optional(Format.URL.annotations({ title: 'Website' })),
|
|
100
|
+
address: Schema.optional(AddressSchema),
|
|
101
|
+
}).pipe(Schema.mutable);
|
|
102
|
+
|
|
103
|
+
type ContactSchema = Schema.Schema.Type<typeof ContactSchema>;
|
|
104
|
+
|
|
105
|
+
type Story = StoryObj<StoryProps<any>>;
|
|
106
|
+
|
|
107
|
+
export const Default: Story = {
|
|
89
108
|
decorators: [withSurfaceVariantsLayout(), withTheme],
|
|
90
109
|
args: {
|
|
91
|
-
schema:
|
|
110
|
+
schema: ContactSchema,
|
|
92
111
|
values: {
|
|
93
112
|
name: 'DXOS',
|
|
94
113
|
active: true,
|
|
@@ -100,8 +119,9 @@ export const Default: Story<TestType> = {
|
|
|
100
119
|
},
|
|
101
120
|
};
|
|
102
121
|
|
|
103
|
-
export const Organization: Story
|
|
122
|
+
export const Organization: Story = {
|
|
104
123
|
args: {
|
|
124
|
+
debug: true,
|
|
105
125
|
schema: Testing.OrganizationSchema,
|
|
106
126
|
values: {
|
|
107
127
|
name: 'DXOS',
|
|
@@ -111,8 +131,9 @@ export const Organization: Story<Schema.Schema.Type<typeof Testing.OrganizationS
|
|
|
111
131
|
},
|
|
112
132
|
};
|
|
113
133
|
|
|
114
|
-
export const OrganizationAutoSave: Story
|
|
134
|
+
export const OrganizationAutoSave: Story = {
|
|
115
135
|
args: {
|
|
136
|
+
debug: true,
|
|
116
137
|
schema: Testing.OrganizationSchema,
|
|
117
138
|
values: {
|
|
118
139
|
name: 'DXOS',
|
|
@@ -125,15 +146,20 @@ export const OrganizationAutoSave: Story<Schema.Schema.Type<typeof Testing.Organ
|
|
|
125
146
|
|
|
126
147
|
// TODO(burdon): Type issue with employer reference.
|
|
127
148
|
// TODO(burdon): Test table/form with compound values (e.g., address).
|
|
128
|
-
//
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
149
|
+
// Property name is missing in type EncodedReference but required in type
|
|
150
|
+
export const Contact: Story = {
|
|
151
|
+
args: {
|
|
152
|
+
debug: true,
|
|
153
|
+
schema: Testing.ContactSchema,
|
|
154
|
+
values: {
|
|
155
|
+
name: 'Bot',
|
|
156
|
+
},
|
|
157
|
+
},
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
//
|
|
161
|
+
// Union
|
|
162
|
+
//
|
|
137
163
|
|
|
138
164
|
const ShapeSchema = Schema.Struct({
|
|
139
165
|
shape: Schema.optional(
|
|
@@ -150,10 +176,11 @@ const ShapeSchema = Schema.Struct({
|
|
|
150
176
|
),
|
|
151
177
|
}).pipe(Schema.mutable);
|
|
152
178
|
|
|
153
|
-
type
|
|
179
|
+
type ShapeSchema = Schema.Schema.Type<typeof ShapeSchema>;
|
|
154
180
|
|
|
155
|
-
export const DiscriminatedShape:
|
|
181
|
+
export const DiscriminatedShape: StoryObj<StoryProps<ShapeSchema>> = {
|
|
156
182
|
args: {
|
|
183
|
+
debug: true,
|
|
157
184
|
schema: ShapeSchema,
|
|
158
185
|
readonly: false,
|
|
159
186
|
values: {
|
|
@@ -176,15 +203,20 @@ export const DiscriminatedShape: Story<ShapeType> = {
|
|
|
176
203
|
},
|
|
177
204
|
};
|
|
178
205
|
|
|
206
|
+
//
|
|
207
|
+
// Arrays
|
|
208
|
+
//
|
|
209
|
+
|
|
179
210
|
const ArraysSchema = Schema.Struct({
|
|
180
211
|
names: Schema.Array(Schema.String.pipe(Schema.nonEmptyString())),
|
|
181
212
|
addresses: Schema.Array(AddressSchema),
|
|
182
213
|
}).pipe(Schema.mutable);
|
|
183
214
|
|
|
184
|
-
type
|
|
215
|
+
type ArraysSchema = Schema.Schema.Type<typeof ArraysSchema>;
|
|
185
216
|
|
|
186
|
-
export const Arrays: StoryObj<
|
|
217
|
+
export const Arrays: StoryObj<StoryProps<ArraysSchema>> = {
|
|
187
218
|
args: {
|
|
219
|
+
debug: true,
|
|
188
220
|
schema: ArraysSchema,
|
|
189
221
|
readonly: false,
|
|
190
222
|
values: {
|
|
@@ -194,6 +226,34 @@ export const Arrays: StoryObj<FormProps<ArraysType>> = {
|
|
|
194
226
|
},
|
|
195
227
|
};
|
|
196
228
|
|
|
229
|
+
//
|
|
230
|
+
// Tuple
|
|
231
|
+
//
|
|
232
|
+
|
|
233
|
+
// TODO(wittjosiah): Only GeoPoint is works currently.
|
|
234
|
+
const TupleSchema = Schema.Struct({
|
|
235
|
+
tuple: Schema.Tuple(Schema.Number, Schema.String, Schema.Boolean),
|
|
236
|
+
geopoint: Format.GeoPoint,
|
|
237
|
+
}).pipe(Schema.mutable);
|
|
238
|
+
|
|
239
|
+
type TupleSchema = Schema.Schema.Type<typeof TupleSchema>;
|
|
240
|
+
|
|
241
|
+
export const Tuple: StoryObj<StoryProps<TupleSchema>> = {
|
|
242
|
+
args: {
|
|
243
|
+
debug: true,
|
|
244
|
+
schema: TupleSchema,
|
|
245
|
+
readonly: false,
|
|
246
|
+
values: {
|
|
247
|
+
tuple: [1, 'a', true],
|
|
248
|
+
geopoint: [-122.41941, 37.77492],
|
|
249
|
+
},
|
|
250
|
+
},
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
//
|
|
254
|
+
// Enum
|
|
255
|
+
//
|
|
256
|
+
|
|
197
257
|
const ColorSchema = Schema.Struct({
|
|
198
258
|
color: Schema.Union(Schema.Literal('red'), Schema.Literal('green'), Schema.Literal('blue')).annotations({
|
|
199
259
|
title: 'Color',
|
|
@@ -202,8 +262,9 @@ const ColorSchema = Schema.Struct({
|
|
|
202
262
|
|
|
203
263
|
type ColorType = Schema.Schema.Type<typeof ColorSchema>;
|
|
204
264
|
|
|
205
|
-
export const Enum: StoryObj<
|
|
265
|
+
export const Enum: StoryObj<StoryProps<ColorType>> = {
|
|
206
266
|
args: {
|
|
267
|
+
debug: true,
|
|
207
268
|
schema: ColorSchema,
|
|
208
269
|
readonly: false,
|
|
209
270
|
values: {
|
|
@@ -212,6 +273,10 @@ export const Enum: StoryObj<FormProps<ColorType>> = {
|
|
|
212
273
|
},
|
|
213
274
|
};
|
|
214
275
|
|
|
276
|
+
//
|
|
277
|
+
// Refs
|
|
278
|
+
//
|
|
279
|
+
|
|
215
280
|
const RefSchema = Schema.Struct({
|
|
216
281
|
contact: Ref(ContactType).annotations({ title: 'Contact Reference' }),
|
|
217
282
|
optionalContact: Schema.optional(Ref(ContactType).annotations({ title: 'Optional Contact Reference' })),
|
|
@@ -219,48 +284,19 @@ const RefSchema = Schema.Struct({
|
|
|
219
284
|
unknownExpando: Schema.optional(Ref(Expando).annotations({ title: 'Optional Ref to an Expando (DXN Input)' })),
|
|
220
285
|
});
|
|
221
286
|
|
|
287
|
+
type RefSchema = Schema.Schema.Type<typeof RefSchema>;
|
|
288
|
+
|
|
222
289
|
const contact1 = live(ContactType, { identifiers: [] });
|
|
223
290
|
const contact2 = live(ContactType, { identifiers: [] });
|
|
224
291
|
|
|
225
|
-
const
|
|
226
|
-
const [values, setValues] = useState(initialValues);
|
|
227
|
-
const handleSave = useCallback<NonNullable<FormProps<any>['onSave']>>((values) => {
|
|
228
|
-
setValues(values);
|
|
229
|
-
}, []);
|
|
230
|
-
|
|
231
|
-
const onQueryRefOptions = useCallback((typeInfo: TypeAnnotation) => {
|
|
232
|
-
switch (typeInfo.typename) {
|
|
233
|
-
case ContactType.typename:
|
|
234
|
-
return [
|
|
235
|
-
{ dxn: getObjectDXN(contact1)!, label: 'John Coltraine' },
|
|
236
|
-
{ dxn: getObjectDXN(contact2)!, label: 'Erykah Badu' },
|
|
237
|
-
];
|
|
238
|
-
default:
|
|
239
|
-
return [];
|
|
240
|
-
}
|
|
241
|
-
}, []);
|
|
242
|
-
|
|
243
|
-
return (
|
|
244
|
-
<TestLayout json={{ values, schema: RefSchema.ast.toJSON() }}>
|
|
245
|
-
<TestPanel>
|
|
246
|
-
<Form
|
|
247
|
-
schema={RefSchema}
|
|
248
|
-
values={values}
|
|
249
|
-
readonly={readonly}
|
|
250
|
-
onSave={handleSave}
|
|
251
|
-
onQueryRefOptions={onQueryRefOptions}
|
|
252
|
-
/>
|
|
253
|
-
</TestPanel>
|
|
254
|
-
</TestLayout>
|
|
255
|
-
);
|
|
256
|
-
};
|
|
257
|
-
|
|
258
|
-
export const Refs: StoryObj<FormProps<ContactType>> = {
|
|
292
|
+
export const Refs: StoryObj<StoryProps<RefSchema>> = {
|
|
259
293
|
render: RefStory,
|
|
260
294
|
args: {
|
|
295
|
+
debug: true,
|
|
296
|
+
schema: RefSchema,
|
|
261
297
|
readonly: false,
|
|
262
298
|
values: {
|
|
263
299
|
refArray: [Ref.make(contact1), Ref.make(contact2)],
|
|
264
|
-
}
|
|
300
|
+
},
|
|
265
301
|
},
|
|
266
302
|
};
|
|
@@ -2,19 +2,21 @@
|
|
|
2
2
|
// Copyright 2024 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
+
import { useFocusFinders } from '@fluentui/react-tabster';
|
|
5
6
|
import { type Schema } from 'effect';
|
|
6
7
|
import React, { type ReactElement, useEffect, useMemo, useRef } from 'react';
|
|
7
8
|
|
|
8
9
|
import { type BaseObject, type PropertyKey } from '@dxos/echo-schema';
|
|
9
10
|
import { type ThemedClassName } from '@dxos/react-ui';
|
|
10
11
|
import { cardDialogOverflow, cardSpacing } from '@dxos/react-ui-stack';
|
|
11
|
-
import { type SchemaProperty } from '@dxos/schema';
|
|
12
|
+
import { type ProjectionModel, type SchemaProperty } from '@dxos/schema';
|
|
13
|
+
|
|
14
|
+
import { type FormOptions } from '../../hooks';
|
|
12
15
|
|
|
13
16
|
import { FormActions, type FormOuterSpacing } from './FormActions';
|
|
14
17
|
import { FormFields, type FormFieldsProps } from './FormContent';
|
|
15
18
|
import { FormProvider } from './FormContext';
|
|
16
|
-
import { type
|
|
17
|
-
import { type FormOptions } from '../../hooks';
|
|
19
|
+
import { type InputComponent, type InputProps } from './Input';
|
|
18
20
|
|
|
19
21
|
export type PropsFilter<T extends BaseObject> = (props: SchemaProperty<T>[]) => SchemaProperty<T>[];
|
|
20
22
|
|
|
@@ -33,6 +35,7 @@ export type FormProps<T extends BaseObject> = ThemedClassName<{
|
|
|
33
35
|
// TODO(burdon): Change to JsonPath includes/excludes.
|
|
34
36
|
filter?: PropsFilter<T>;
|
|
35
37
|
sort?: PropertyKey<T>[];
|
|
38
|
+
projection?: ProjectionModel;
|
|
36
39
|
autoSave?: boolean;
|
|
37
40
|
outerSpacing?: FormOuterSpacing;
|
|
38
41
|
onCancel?: () => void;
|
|
@@ -48,8 +51,8 @@ export const Form = <T extends BaseObject>({
|
|
|
48
51
|
readonly,
|
|
49
52
|
autoSave,
|
|
50
53
|
outerSpacing = true,
|
|
51
|
-
onCancel,
|
|
52
54
|
schema,
|
|
55
|
+
onCancel,
|
|
53
56
|
onValuesChanged,
|
|
54
57
|
onValidate,
|
|
55
58
|
onSave,
|
|
@@ -57,19 +60,20 @@ export const Form = <T extends BaseObject>({
|
|
|
57
60
|
}: FormProps<T>) => {
|
|
58
61
|
const formRef = useRef<HTMLDivElement>(null);
|
|
59
62
|
|
|
60
|
-
//
|
|
61
|
-
const
|
|
62
|
-
|
|
63
|
-
// Focus the first input element within this form.
|
|
63
|
+
// Focus the first focusable element within this form.
|
|
64
|
+
const { findFirstFocusable } = useFocusFinders();
|
|
64
65
|
useEffect(() => {
|
|
65
66
|
if (autoFocus && formRef.current) {
|
|
66
|
-
const
|
|
67
|
-
if (
|
|
68
|
-
|
|
67
|
+
const firstFocusable = findFirstFocusable(formRef.current);
|
|
68
|
+
if (firstFocusable) {
|
|
69
|
+
firstFocusable.focus();
|
|
69
70
|
}
|
|
70
71
|
}
|
|
71
72
|
}, [autoFocus]);
|
|
72
73
|
|
|
74
|
+
// TODO(burdon): Name?
|
|
75
|
+
const handleValid = useMemo(() => (autoSave ? onSave : undefined), [autoSave, onSave]);
|
|
76
|
+
|
|
73
77
|
return (
|
|
74
78
|
<FormProvider
|
|
75
79
|
formRef={formRef}
|
|
@@ -99,7 +103,7 @@ export const Form = <T extends BaseObject>({
|
|
|
99
103
|
readonly={readonly}
|
|
100
104
|
schema={schema}
|
|
101
105
|
/>
|
|
102
|
-
{(onCancel || onSave) && !autoSave && (
|
|
106
|
+
{(onCancel || onSave) && !autoSave && !readonly && (
|
|
103
107
|
<FormActions readonly={readonly} onCancel={onCancel} outerSpacing={outerSpacing} />
|
|
104
108
|
)}
|
|
105
109
|
</div>
|
|
@@ -8,9 +8,10 @@ import { IconButton, useTranslation } from '@dxos/react-ui';
|
|
|
8
8
|
import { cardSpacing } from '@dxos/react-ui-stack';
|
|
9
9
|
import { mx } from '@dxos/react-ui-theme';
|
|
10
10
|
|
|
11
|
-
import { useFormContext } from './FormContext';
|
|
12
11
|
import { translationKey } from '../../translations';
|
|
13
12
|
|
|
13
|
+
import { useFormContext } from './FormContext';
|
|
14
|
+
|
|
14
15
|
export type FormOuterSpacing = boolean | 'blockStart-0' | 'scroll-fields';
|
|
15
16
|
|
|
16
17
|
export type FormActionsProps = {
|
|
@@ -9,36 +9,43 @@ import React, { forwardRef, useMemo } from 'react';
|
|
|
9
9
|
import { createJsonPath, findNode, getDiscriminatedType, isDiscriminatedUnion } from '@dxos/effect';
|
|
10
10
|
import { type ThemedClassName } from '@dxos/react-ui';
|
|
11
11
|
import { mx } from '@dxos/react-ui-theme';
|
|
12
|
-
import {
|
|
12
|
+
import { type ProjectionModel, type SchemaProperty, getSchemaProperties } from '@dxos/schema';
|
|
13
13
|
import { isNotFalsy } from '@dxos/util';
|
|
14
14
|
|
|
15
|
+
import { type QueryRefOptions } from '../../hooks';
|
|
16
|
+
import { getRefProps } from '../../util';
|
|
17
|
+
|
|
15
18
|
import { ArrayField } from './ArrayField';
|
|
16
19
|
import { SelectInput } from './Defaults';
|
|
20
|
+
import { getInputComponent } from './factory';
|
|
17
21
|
import { type ComponentLookup } from './Form';
|
|
18
|
-
import {
|
|
22
|
+
import { useFormValues, useInputProps } from './FormContext';
|
|
19
23
|
import { type InputComponent } from './Input';
|
|
20
|
-
import { RefField } from './RefField';
|
|
21
|
-
import { getInputComponent } from './factory';
|
|
22
|
-
import { type QueryRefOptions } from '../../hooks';
|
|
23
|
-
import { getRefProps } from '../../util';
|
|
24
|
+
import { RefField, type RefFieldProps } from './RefField';
|
|
24
25
|
|
|
25
26
|
export type FormFieldProps = {
|
|
26
27
|
property: SchemaProperty<any>;
|
|
27
28
|
path?: (string | number)[];
|
|
28
|
-
readonly?: boolean;
|
|
29
29
|
/** Used to indicate if input should be presented inline (e.g. for array items). */
|
|
30
30
|
inline?: boolean;
|
|
31
|
-
|
|
31
|
+
projection?: ProjectionModel;
|
|
32
32
|
lookupComponent?: ComponentLookup;
|
|
33
33
|
Custom?: Partial<Record<string, InputComponent>>;
|
|
34
|
-
}
|
|
34
|
+
} & Pick<
|
|
35
|
+
RefFieldProps,
|
|
36
|
+
'readonly' | 'onQueryRefOptions' | 'createOptionLabel' | 'createOptionIcon' | 'onCreateFromQuery'
|
|
37
|
+
>;
|
|
35
38
|
|
|
36
39
|
export const FormField = ({
|
|
37
40
|
property,
|
|
38
41
|
path,
|
|
39
42
|
readonly,
|
|
40
43
|
inline,
|
|
44
|
+
projection,
|
|
41
45
|
onQueryRefOptions,
|
|
46
|
+
createOptionLabel,
|
|
47
|
+
createOptionIcon,
|
|
48
|
+
onCreateFromQuery,
|
|
42
49
|
lookupComponent,
|
|
43
50
|
Custom,
|
|
44
51
|
}: FormFieldProps) => {
|
|
@@ -62,7 +69,7 @@ export const FormField = ({
|
|
|
62
69
|
type,
|
|
63
70
|
format,
|
|
64
71
|
label,
|
|
65
|
-
|
|
72
|
+
readonly,
|
|
66
73
|
placeholder,
|
|
67
74
|
...inputProps,
|
|
68
75
|
},
|
|
@@ -82,7 +89,7 @@ export const FormField = ({
|
|
|
82
89
|
label={label}
|
|
83
90
|
inputOnly={inline}
|
|
84
91
|
placeholder={placeholder}
|
|
85
|
-
|
|
92
|
+
readonly={readonly}
|
|
86
93
|
{...inputProps}
|
|
87
94
|
/>
|
|
88
95
|
);
|
|
@@ -102,9 +109,12 @@ export const FormField = ({
|
|
|
102
109
|
format={format}
|
|
103
110
|
label={label}
|
|
104
111
|
placeholder={placeholder}
|
|
105
|
-
|
|
112
|
+
readonly={readonly}
|
|
106
113
|
inputOnly={inline}
|
|
107
114
|
onQueryRefOptions={onQueryRefOptions}
|
|
115
|
+
createOptionLabel={createOptionLabel}
|
|
116
|
+
createOptionIcon={createOptionIcon}
|
|
117
|
+
onCreateFromQuery={onCreateFromQuery}
|
|
108
118
|
{...inputProps}
|
|
109
119
|
/>
|
|
110
120
|
);
|
|
@@ -131,7 +141,7 @@ export const FormField = ({
|
|
|
131
141
|
label={label}
|
|
132
142
|
inputOnly={inline}
|
|
133
143
|
placeholder={placeholder}
|
|
134
|
-
|
|
144
|
+
readonly={readonly}
|
|
135
145
|
{...inputProps}
|
|
136
146
|
/>
|
|
137
147
|
);
|
|
@@ -146,7 +156,7 @@ export const FormField = ({
|
|
|
146
156
|
<SelectInput
|
|
147
157
|
type={type}
|
|
148
158
|
format={format}
|
|
149
|
-
|
|
159
|
+
readonly={readonly}
|
|
150
160
|
inputOnly={inline}
|
|
151
161
|
label={label}
|
|
152
162
|
options={options.map((option) => ({ value: option, label: String(option) }))}
|
|
@@ -174,7 +184,11 @@ export const FormField = ({
|
|
|
174
184
|
schema={Schema.make(typeLiteral)}
|
|
175
185
|
path={path}
|
|
176
186
|
readonly={readonly}
|
|
187
|
+
projection={projection}
|
|
177
188
|
onQueryRefOptions={onQueryRefOptions}
|
|
189
|
+
createOptionLabel={createOptionLabel}
|
|
190
|
+
createOptionIcon={createOptionIcon}
|
|
191
|
+
onCreateFromQuery={onCreateFromQuery}
|
|
178
192
|
Custom={Custom}
|
|
179
193
|
lookupComponent={lookupComponent}
|
|
180
194
|
/>
|
|
@@ -186,36 +200,82 @@ export const FormField = ({
|
|
|
186
200
|
return null;
|
|
187
201
|
};
|
|
188
202
|
|
|
189
|
-
export type FormFieldsProps = ThemedClassName<
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
203
|
+
export type FormFieldsProps = ThemedClassName<
|
|
204
|
+
{
|
|
205
|
+
testId?: string;
|
|
206
|
+
schema: Schema.Schema.All;
|
|
207
|
+
/**
|
|
208
|
+
* Path to the current object from the root. Used with nested forms.
|
|
209
|
+
*/
|
|
210
|
+
path?: (string | number)[];
|
|
211
|
+
filter?: (props: SchemaProperty<any>[]) => SchemaProperty<any>[];
|
|
212
|
+
sort?: string[];
|
|
213
|
+
/**
|
|
214
|
+
* Optional projection for projection-based field management
|
|
215
|
+
*/
|
|
216
|
+
projection?: ProjectionModel;
|
|
217
|
+
lookupComponent?: ComponentLookup;
|
|
218
|
+
/**
|
|
219
|
+
* Map of custom renderers for specific properties.
|
|
220
|
+
* Prefer lookupComponent for plugin specific input surfaces.
|
|
221
|
+
*/
|
|
222
|
+
Custom?: Partial<Record<string, InputComponent>>;
|
|
223
|
+
onQueryRefOptions?: QueryRefOptions;
|
|
224
|
+
} & Pick<FormFieldProps, 'readonly'> &
|
|
225
|
+
Pick<RefFieldProps, 'onQueryRefOptions' | 'createOptionLabel' | 'createOptionIcon' | 'onCreateFromQuery'>
|
|
226
|
+
>;
|
|
207
227
|
|
|
208
228
|
export const FormFields = forwardRef<HTMLDivElement, FormFieldsProps>(
|
|
209
229
|
(
|
|
210
|
-
{
|
|
230
|
+
{
|
|
231
|
+
classNames,
|
|
232
|
+
schema,
|
|
233
|
+
path,
|
|
234
|
+
filter,
|
|
235
|
+
sort,
|
|
236
|
+
projection,
|
|
237
|
+
readonly,
|
|
238
|
+
lookupComponent,
|
|
239
|
+
Custom,
|
|
240
|
+
onQueryRefOptions,
|
|
241
|
+
...props
|
|
242
|
+
},
|
|
211
243
|
forwardRef,
|
|
212
244
|
) => {
|
|
213
245
|
const values = useFormValues(path);
|
|
214
246
|
const properties = useMemo(() => {
|
|
215
247
|
const props = getSchemaProperties(schema.ast, values);
|
|
248
|
+
|
|
249
|
+
// Use projection-based field management when view and projection are available
|
|
250
|
+
if (projection) {
|
|
251
|
+
const fieldProjections = projection.getFieldProjections();
|
|
252
|
+
const hiddenProperties = new Set(projection.getHiddenProperties());
|
|
253
|
+
|
|
254
|
+
// Filter properties to only include visible ones and order by projection
|
|
255
|
+
const visibleProps = props.filter((prop) => !hiddenProperties.has(prop.name));
|
|
256
|
+
const orderedProps: SchemaProperty<any>[] = [];
|
|
257
|
+
|
|
258
|
+
// Add properties in projection field order
|
|
259
|
+
for (const fieldProjection of fieldProjections) {
|
|
260
|
+
const fieldPath = String(fieldProjection.field.path);
|
|
261
|
+
const prop = visibleProps.find((p) => p.name === fieldPath);
|
|
262
|
+
if (prop) {
|
|
263
|
+
orderedProps.push(prop);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// Add any remaining properties not in projection
|
|
268
|
+
const projectionPaths = new Set(fieldProjections.map((fp) => String(fp.field.path)));
|
|
269
|
+
const remainingProps = visibleProps.filter((prop) => !projectionPaths.has(prop.name));
|
|
270
|
+
orderedProps.push(...remainingProps);
|
|
271
|
+
|
|
272
|
+
return orderedProps;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// Fallback to legacy filter/sort behavior
|
|
216
276
|
const filtered = filter ? filter(props) : props;
|
|
217
277
|
return sort ? filtered.sort((a, b) => sort.indexOf(a.name) - sort.indexOf(b.name)) : filtered;
|
|
218
|
-
}, [schema, values, filter, sort]);
|
|
278
|
+
}, [schema, values, filter, sort, projection?.fields]);
|
|
219
279
|
|
|
220
280
|
return (
|
|
221
281
|
<div role='form' className={mx('is-full', classNames)} ref={forwardRef}>
|
|
@@ -227,9 +287,11 @@ export const FormFields = forwardRef<HTMLDivElement, FormFieldsProps>(
|
|
|
227
287
|
property={property}
|
|
228
288
|
path={[...(path ?? []), property.name]}
|
|
229
289
|
readonly={readonly}
|
|
290
|
+
projection={projection}
|
|
230
291
|
onQueryRefOptions={onQueryRefOptions}
|
|
231
292
|
lookupComponent={lookupComponent}
|
|
232
293
|
Custom={Custom}
|
|
294
|
+
{...props}
|
|
233
295
|
/>
|
|
234
296
|
);
|
|
235
297
|
})
|
|
@@ -3,19 +3,19 @@
|
|
|
3
3
|
//
|
|
4
4
|
|
|
5
5
|
import React, {
|
|
6
|
-
|
|
6
|
+
type FocusEvent,
|
|
7
|
+
type PropsWithChildren,
|
|
7
8
|
type RefObject,
|
|
9
|
+
createContext,
|
|
10
|
+
useCallback,
|
|
8
11
|
useContext,
|
|
9
12
|
useEffect,
|
|
10
13
|
useMemo,
|
|
11
|
-
type FocusEvent,
|
|
12
|
-
type PropsWithChildren,
|
|
13
|
-
useCallback,
|
|
14
14
|
} from 'react';
|
|
15
15
|
|
|
16
16
|
import { raise } from '@dxos/debug';
|
|
17
17
|
import { type BaseObject, getValue } from '@dxos/echo-schema';
|
|
18
|
-
import {
|
|
18
|
+
import { type SimpleType, createJsonPath } from '@dxos/effect';
|
|
19
19
|
|
|
20
20
|
import { type FormHandler, type FormOptions, useForm } from '../../hooks';
|
|
21
21
|
|