@dxos/react-ui-form 0.8.4-main.67995b8 → 0.8.4-main.70d3990
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 +909 -675
- package/dist/lib/browser/index.mjs.map +4 -4
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/node-esm/index.mjs +909 -675
- 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 +81 -5
- 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 +76 -5
- 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 +10 -8
- package/dist/types/src/components/Form/Form.d.ts.map +1 -1
- package/dist/types/src/components/Form/Form.stories.d.ts +144 -33
- 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 +17 -11
- package/dist/types/src/components/Form/FormContent.d.ts.map +1 -1
- package/dist/types/src/components/Form/FormContext.d.ts +5 -5
- package/dist/types/src/components/Form/FormContext.d.ts.map +1 -1
- package/dist/types/src/components/Form/Input.d.ts +6 -3
- package/dist/types/src/components/Form/Input.d.ts.map +1 -1
- package/dist/types/src/components/Form/RefField.d.ts +15 -5
- 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 +76 -4
- 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 +2 -2
- package/dist/types/src/components/Form/factory.d.ts.map +1 -1
- package/dist/types/src/components/ObjectPicker/ObjectPicker.d.ts +41 -0
- package/dist/types/src/components/ObjectPicker/ObjectPicker.d.ts.map +1 -0
- package/dist/types/src/components/ObjectPicker/ObjectPicker.stories.d.ts +82 -0
- package/dist/types/src/components/ObjectPicker/ObjectPicker.stories.d.ts.map +1 -0
- package/dist/types/src/components/ObjectPicker/index.d.ts +2 -0
- package/dist/types/src/components/ObjectPicker/index.d.ts.map +1 -0
- package/dist/types/src/components/ViewEditor/ViewEditor.d.ts +20 -6
- package/dist/types/src/components/ViewEditor/ViewEditor.d.ts.map +1 -1
- package/dist/types/src/components/ViewEditor/ViewEditor.stories.d.ts +80 -8
- package/dist/types/src/components/ViewEditor/ViewEditor.stories.d.ts.map +1 -1
- package/dist/types/src/components/index.d.ts +1 -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 +6 -6
- package/dist/types/src/hooks/useForm.d.ts.map +1 -1
- package/dist/types/src/hooks/useQueryRefOptions.d.ts +9 -3
- package/dist/types/src/hooks/useQueryRefOptions.d.ts.map +1 -1
- package/dist/types/src/hooks/useRefQueryLookupHandler.d.ts.map +1 -1
- package/dist/types/src/translations.d.ts +14 -8
- package/dist/types/src/translations.d.ts.map +1 -1
- package/dist/types/src/util/field.d.ts +3 -3
- package/dist/types/src/util/field.d.ts.map +1 -1
- package/dist/types/src/util/formatting.d.ts +2 -2
- package/dist/types/src/util/formatting.d.ts.map +1 -1
- package/dist/types/src/util/refs.d.ts +1 -1
- package/dist/types/src/util/refs.d.ts.map +1 -1
- package/dist/types/src/util/schema.d.ts +1 -1
- package/dist/types/src/util/schema.d.ts.map +1 -1
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/package.json +43 -38
- package/src/components/ControlSection/ControlSection.stories.tsx +11 -14
- package/src/components/ControlSection/ControlSection.tsx +26 -12
- package/src/components/FieldEditor/FieldEditor.stories.tsx +16 -16
- package/src/components/FieldEditor/FieldEditor.test.tsx +3 -2
- package/src/components/FieldEditor/FieldEditor.tsx +28 -20
- package/src/components/Form/ArrayField.tsx +15 -12
- package/src/components/Form/Defaults.tsx +23 -21
- package/src/components/Form/Form.stories.tsx +144 -108
- package/src/components/Form/Form.tsx +33 -19
- package/src/components/Form/FormActions.tsx +9 -3
- package/src/components/Form/FormContent.tsx +172 -50
- package/src/components/Form/FormContext.tsx +11 -11
- package/src/components/Form/Input.tsx +6 -3
- package/src/components/Form/RefField.tsx +111 -41
- package/src/components/Form/custom/GeoPointInput.tsx +29 -21
- package/src/components/Form/custom/SelectOptionsInput.stories.tsx +11 -11
- package/src/components/Form/custom/SelectOptionsInput.tsx +31 -32
- package/src/components/Form/custom/TupleInput.tsx +2 -2
- package/src/components/Form/factory.tsx +8 -5
- package/src/components/ObjectPicker/ObjectPicker.stories.tsx +207 -0
- package/src/components/ObjectPicker/ObjectPicker.tsx +161 -0
- package/src/components/ObjectPicker/index.ts +5 -0
- package/src/components/ViewEditor/ViewEditor.stories.tsx +123 -51
- package/src/components/ViewEditor/ViewEditor.test.tsx +13 -15
- package/src/components/ViewEditor/ViewEditor.tsx +271 -219
- package/src/components/index.ts +1 -2
- package/src/components/testing.tsx +15 -9
- package/src/hooks/useForm.ts +15 -10
- package/src/hooks/useQueryRefOptions.ts +15 -6
- package/src/hooks/useRefQueryLookupHandler.ts +5 -5
- package/src/translations.ts +16 -9
- package/src/util/field.ts +33 -43
- package/src/util/formatting.ts +22 -22
- package/src/util/refs.ts +7 -7
- package/src/util/schema.ts +2 -4
- 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
|
@@ -2,24 +2,69 @@
|
|
|
2
2
|
// Copyright 2024 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import '@dxos-theme';
|
|
6
|
-
|
|
7
5
|
import { type Meta, type StoryObj } from '@storybook/react-vite';
|
|
8
|
-
import
|
|
6
|
+
import * as Schema from 'effect/Schema';
|
|
9
7
|
import React, { useCallback, useState } from 'react';
|
|
10
8
|
|
|
11
|
-
import {
|
|
12
|
-
import { type
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
16
|
-
import { withLayout, withTheme } from '@dxos/storybook-utils';
|
|
9
|
+
import { Format, Obj, Ref, Type } from '@dxos/echo';
|
|
10
|
+
import { type AnyProperties, type TypeAnnotation } from '@dxos/echo/internal';
|
|
11
|
+
import { Tooltip } from '@dxos/react-ui';
|
|
12
|
+
import { withLayoutVariants, withTheme } from '@dxos/react-ui/testing';
|
|
13
|
+
import { TestSchema } from '@dxos/schema/testing';
|
|
17
14
|
|
|
18
|
-
import { SelectInput } from './Defaults';
|
|
19
|
-
import { Form, type FormProps } from './Form';
|
|
20
15
|
import { translations } from '../../translations';
|
|
21
16
|
import { TestLayout, TestPanel } from '../testing';
|
|
22
17
|
|
|
18
|
+
import { SelectInput } from './Defaults';
|
|
19
|
+
import { Form, type FormProps } from './Form';
|
|
20
|
+
|
|
21
|
+
type StoryProps<T extends AnyProperties> = {
|
|
22
|
+
debug?: boolean;
|
|
23
|
+
schema: Schema.Schema.AnyNoContext;
|
|
24
|
+
} & FormProps<T>;
|
|
25
|
+
|
|
26
|
+
const DefaultStory = <T extends AnyProperties = any>({
|
|
27
|
+
debug,
|
|
28
|
+
schema,
|
|
29
|
+
values: initialValues,
|
|
30
|
+
...props
|
|
31
|
+
}: StoryProps<T>) => {
|
|
32
|
+
const [values, setValues] = useState(initialValues);
|
|
33
|
+
const handleSave = useCallback<NonNullable<FormProps<T>['onSave']>>((values) => {
|
|
34
|
+
setValues(values);
|
|
35
|
+
}, []);
|
|
36
|
+
|
|
37
|
+
if (debug) {
|
|
38
|
+
return (
|
|
39
|
+
<Tooltip.Provider>
|
|
40
|
+
<TestLayout json={{ values, schema: schema.ast }}>
|
|
41
|
+
<TestPanel>
|
|
42
|
+
<Form<T> schema={schema} values={values} onSave={handleSave} {...props} />
|
|
43
|
+
</TestPanel>
|
|
44
|
+
</TestLayout>
|
|
45
|
+
</Tooltip.Provider>
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return <Form<T> schema={schema} values={values} onSave={handleSave} {...props} />;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const RefStory = <T extends AnyProperties = any>(props: StoryProps<T>) => {
|
|
53
|
+
const onQueryRefOptions = useCallback((typeInfo: TypeAnnotation) => {
|
|
54
|
+
switch (typeInfo.typename) {
|
|
55
|
+
case TestSchema.Person.typename:
|
|
56
|
+
return [
|
|
57
|
+
{ dxn: Obj.getDXN(contact1), label: 'Alice' },
|
|
58
|
+
{ dxn: Obj.getDXN(contact2), label: 'Bob' },
|
|
59
|
+
];
|
|
60
|
+
default:
|
|
61
|
+
return [];
|
|
62
|
+
}
|
|
63
|
+
}, []);
|
|
64
|
+
|
|
65
|
+
return <DefaultStory<T> onQueryRefOptions={onQueryRefOptions} {...props} />;
|
|
66
|
+
};
|
|
67
|
+
|
|
23
68
|
const AddressSchema = Schema.Struct({
|
|
24
69
|
street: Schema.optional(Schema.String.annotations({ title: 'Street' })),
|
|
25
70
|
city: Schema.optional(Schema.String.annotations({ title: 'City' })),
|
|
@@ -27,8 +72,7 @@ const AddressSchema = Schema.Struct({
|
|
|
27
72
|
location: Schema.optional(Format.GeoPoint.annotations({ title: 'Location' })),
|
|
28
73
|
}).annotations({ title: 'Address' });
|
|
29
74
|
|
|
30
|
-
|
|
31
|
-
const TestSchema = Schema.Struct({
|
|
75
|
+
const ContactSchema = Schema.Struct({
|
|
32
76
|
name: Schema.optional(Schema.String.annotations({ title: 'Name' })),
|
|
33
77
|
active: Schema.optional(Schema.Boolean.annotations({ title: 'Active' })),
|
|
34
78
|
rank: Schema.optional(Schema.Number.annotations({ title: 'Rank' })),
|
|
@@ -36,40 +80,15 @@ const TestSchema = Schema.Struct({
|
|
|
36
80
|
address: Schema.optional(AddressSchema),
|
|
37
81
|
}).pipe(Schema.mutable);
|
|
38
82
|
|
|
39
|
-
type
|
|
40
|
-
|
|
41
|
-
type StoryProps<T extends BaseObject> = { schema: Schema.Schema<T> } & FormProps<T>;
|
|
42
|
-
|
|
43
|
-
const DefaultStory = <T extends BaseObject>({ schema, values: initialValues, ...props }: StoryProps<T>) => {
|
|
44
|
-
const [values, setValues] = useState(initialValues);
|
|
45
|
-
const handleSave = useCallback<NonNullable<FormProps<T>['onSave']>>((values) => {
|
|
46
|
-
setValues(values);
|
|
47
|
-
}, []);
|
|
48
|
-
|
|
49
|
-
return <Form<T> schema={schema} values={values} onSave={handleSave} {...props} />;
|
|
50
|
-
};
|
|
83
|
+
type ContactSchema = Schema.Schema.Type<typeof ContactSchema>;
|
|
51
84
|
|
|
52
|
-
const
|
|
53
|
-
const [values, setValues] = useState(initialValues);
|
|
54
|
-
const handleSave = useCallback<NonNullable<FormProps<T>['onSave']>>((values) => {
|
|
55
|
-
setValues(values);
|
|
56
|
-
}, []);
|
|
57
|
-
|
|
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
|
-
);
|
|
65
|
-
};
|
|
66
|
-
|
|
67
|
-
const meta: Meta<StoryProps<any>> = {
|
|
85
|
+
const meta = {
|
|
68
86
|
title: 'ui/react-ui-form/Form',
|
|
69
|
-
component: Form
|
|
70
|
-
render:
|
|
71
|
-
decorators: [
|
|
87
|
+
component: Form as any,
|
|
88
|
+
render: DefaultStory,
|
|
89
|
+
decorators: [withTheme, withLayoutVariants()],
|
|
72
90
|
parameters: {
|
|
91
|
+
layout: 'fullscreen',
|
|
73
92
|
translations,
|
|
74
93
|
},
|
|
75
94
|
argTypes: {
|
|
@@ -78,17 +97,15 @@ const meta: Meta<StoryProps<any>> = {
|
|
|
78
97
|
description: 'Readonly',
|
|
79
98
|
},
|
|
80
99
|
},
|
|
81
|
-
}
|
|
100
|
+
} satisfies Meta<StoryProps<any>>;
|
|
82
101
|
|
|
83
102
|
export default meta;
|
|
84
103
|
|
|
85
|
-
type Story
|
|
104
|
+
type Story = StoryObj<typeof meta>;
|
|
86
105
|
|
|
87
|
-
export const Default: Story
|
|
88
|
-
render: DefaultStory,
|
|
89
|
-
decorators: [withSurfaceVariantsLayout(), withTheme],
|
|
106
|
+
export const Default: Story = {
|
|
90
107
|
args: {
|
|
91
|
-
schema:
|
|
108
|
+
schema: ContactSchema,
|
|
92
109
|
values: {
|
|
93
110
|
name: 'DXOS',
|
|
94
111
|
active: true,
|
|
@@ -100,9 +117,10 @@ export const Default: Story<TestType> = {
|
|
|
100
117
|
},
|
|
101
118
|
};
|
|
102
119
|
|
|
103
|
-
export const Organization: Story
|
|
120
|
+
export const Organization: Story = {
|
|
104
121
|
args: {
|
|
105
|
-
|
|
122
|
+
debug: true,
|
|
123
|
+
schema: TestSchema.OrganizationSchema,
|
|
106
124
|
values: {
|
|
107
125
|
name: 'DXOS',
|
|
108
126
|
website: 'https://dxos.org',
|
|
@@ -111,9 +129,10 @@ export const Organization: Story<Schema.Schema.Type<typeof Testing.OrganizationS
|
|
|
111
129
|
},
|
|
112
130
|
};
|
|
113
131
|
|
|
114
|
-
export const OrganizationAutoSave: Story
|
|
132
|
+
export const OrganizationAutoSave: Story = {
|
|
115
133
|
args: {
|
|
116
|
-
|
|
134
|
+
debug: true,
|
|
135
|
+
schema: TestSchema.OrganizationSchema,
|
|
117
136
|
values: {
|
|
118
137
|
name: 'DXOS',
|
|
119
138
|
website: 'https://dxos.org',
|
|
@@ -125,15 +144,20 @@ export const OrganizationAutoSave: Story<Schema.Schema.Type<typeof Testing.Organ
|
|
|
125
144
|
|
|
126
145
|
// TODO(burdon): Type issue with employer reference.
|
|
127
146
|
// TODO(burdon): Test table/form with compound values (e.g., address).
|
|
128
|
-
//
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
147
|
+
// Property name is missing in type EncodedReference but required in type
|
|
148
|
+
export const Person: Story = {
|
|
149
|
+
args: {
|
|
150
|
+
debug: true,
|
|
151
|
+
schema: TestSchema.Person,
|
|
152
|
+
values: {
|
|
153
|
+
name: 'Bot',
|
|
154
|
+
},
|
|
155
|
+
},
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
//
|
|
159
|
+
// Union
|
|
160
|
+
//
|
|
137
161
|
|
|
138
162
|
const ShapeSchema = Schema.Struct({
|
|
139
163
|
shape: Schema.optional(
|
|
@@ -150,10 +174,11 @@ const ShapeSchema = Schema.Struct({
|
|
|
150
174
|
),
|
|
151
175
|
}).pipe(Schema.mutable);
|
|
152
176
|
|
|
153
|
-
type
|
|
177
|
+
type ShapeSchema = Schema.Schema.Type<typeof ShapeSchema>;
|
|
154
178
|
|
|
155
|
-
export const DiscriminatedShape:
|
|
179
|
+
export const DiscriminatedShape: StoryObj<StoryProps<ShapeSchema>> = {
|
|
156
180
|
args: {
|
|
181
|
+
debug: true,
|
|
157
182
|
schema: ShapeSchema,
|
|
158
183
|
readonly: false,
|
|
159
184
|
values: {
|
|
@@ -176,15 +201,20 @@ export const DiscriminatedShape: Story<ShapeType> = {
|
|
|
176
201
|
},
|
|
177
202
|
};
|
|
178
203
|
|
|
204
|
+
//
|
|
205
|
+
// Arrays
|
|
206
|
+
//
|
|
207
|
+
|
|
179
208
|
const ArraysSchema = Schema.Struct({
|
|
180
209
|
names: Schema.Array(Schema.String.pipe(Schema.nonEmptyString())),
|
|
181
210
|
addresses: Schema.Array(AddressSchema),
|
|
182
211
|
}).pipe(Schema.mutable);
|
|
183
212
|
|
|
184
|
-
type
|
|
213
|
+
type ArraysSchema = Schema.Schema.Type<typeof ArraysSchema>;
|
|
185
214
|
|
|
186
|
-
export const Arrays: StoryObj<
|
|
215
|
+
export const Arrays: StoryObj<StoryProps<ArraysSchema>> = {
|
|
187
216
|
args: {
|
|
217
|
+
debug: true,
|
|
188
218
|
schema: ArraysSchema,
|
|
189
219
|
readonly: false,
|
|
190
220
|
values: {
|
|
@@ -194,6 +224,34 @@ export const Arrays: StoryObj<FormProps<ArraysType>> = {
|
|
|
194
224
|
},
|
|
195
225
|
};
|
|
196
226
|
|
|
227
|
+
//
|
|
228
|
+
// Tuple
|
|
229
|
+
//
|
|
230
|
+
|
|
231
|
+
// TODO(wittjosiah): Only GeoPoint is works currently.
|
|
232
|
+
const TupleSchema = Schema.Struct({
|
|
233
|
+
tuple: Schema.Tuple(Schema.Number, Schema.String, Schema.Boolean),
|
|
234
|
+
geopoint: Format.GeoPoint,
|
|
235
|
+
}).pipe(Schema.mutable);
|
|
236
|
+
|
|
237
|
+
type TupleSchema = Schema.Schema.Type<typeof TupleSchema>;
|
|
238
|
+
|
|
239
|
+
export const Tuple: StoryObj<StoryProps<TupleSchema>> = {
|
|
240
|
+
args: {
|
|
241
|
+
debug: true,
|
|
242
|
+
schema: TupleSchema,
|
|
243
|
+
readonly: false,
|
|
244
|
+
values: {
|
|
245
|
+
tuple: [1, 'a', true],
|
|
246
|
+
geopoint: [-122.41941, 37.77492],
|
|
247
|
+
},
|
|
248
|
+
},
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
//
|
|
252
|
+
// Enum
|
|
253
|
+
//
|
|
254
|
+
|
|
197
255
|
const ColorSchema = Schema.Struct({
|
|
198
256
|
color: Schema.Union(Schema.Literal('red'), Schema.Literal('green'), Schema.Literal('blue')).annotations({
|
|
199
257
|
title: 'Color',
|
|
@@ -202,8 +260,9 @@ const ColorSchema = Schema.Struct({
|
|
|
202
260
|
|
|
203
261
|
type ColorType = Schema.Schema.Type<typeof ColorSchema>;
|
|
204
262
|
|
|
205
|
-
export const Enum: StoryObj<
|
|
263
|
+
export const Enum: StoryObj<StoryProps<ColorType>> = {
|
|
206
264
|
args: {
|
|
265
|
+
debug: true,
|
|
207
266
|
schema: ColorSchema,
|
|
208
267
|
readonly: false,
|
|
209
268
|
values: {
|
|
@@ -212,55 +271,32 @@ export const Enum: StoryObj<FormProps<ColorType>> = {
|
|
|
212
271
|
},
|
|
213
272
|
};
|
|
214
273
|
|
|
274
|
+
//
|
|
275
|
+
// Refs
|
|
276
|
+
//
|
|
277
|
+
|
|
215
278
|
const RefSchema = Schema.Struct({
|
|
216
|
-
contact: Ref(
|
|
217
|
-
optionalContact: Schema.optional(Ref(
|
|
218
|
-
refArray: Schema.optional(Schema.Array(Ref(
|
|
219
|
-
unknownExpando: Schema.optional(
|
|
279
|
+
contact: Type.Ref(TestSchema.Person).annotations({ title: 'Contact Reference' }),
|
|
280
|
+
optionalContact: Schema.optional(Type.Ref(TestSchema.Person).annotations({ title: 'Optional Contact Reference' })),
|
|
281
|
+
refArray: Schema.optional(Schema.Array(Type.Ref(TestSchema.Person))),
|
|
282
|
+
unknownExpando: Schema.optional(
|
|
283
|
+
Type.Ref(Type.Expando).annotations({ title: 'Optional Ref to an Expando (DXN Input)' }),
|
|
284
|
+
),
|
|
220
285
|
});
|
|
221
286
|
|
|
222
|
-
|
|
223
|
-
const contact2 = live(ContactType, { identifiers: [] });
|
|
287
|
+
type RefSchema = Schema.Schema.Type<typeof RefSchema>;
|
|
224
288
|
|
|
225
|
-
const
|
|
226
|
-
|
|
227
|
-
const handleSave = useCallback<NonNullable<FormProps<any>['onSave']>>((values) => {
|
|
228
|
-
setValues(values);
|
|
229
|
-
}, []);
|
|
289
|
+
const contact1 = Obj.make(TestSchema.Person, { name: 'Alice' });
|
|
290
|
+
const contact2 = Obj.make(TestSchema.Person, { name: 'Bob' });
|
|
230
291
|
|
|
231
|
-
|
|
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,21 +2,23 @@
|
|
|
2
2
|
// Copyright 2024 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import {
|
|
6
|
-
import
|
|
5
|
+
import { useFocusFinders } from '@fluentui/react-tabster';
|
|
6
|
+
import type * as Schema from 'effect/Schema';
|
|
7
|
+
import React, { type ReactElement, useCallback, useEffect, useRef } from 'react';
|
|
7
8
|
|
|
8
|
-
import { type
|
|
9
|
+
import { type AnyProperties, type PropertyKey } from '@dxos/echo/internal';
|
|
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 FormHandler, 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
|
-
export type PropsFilter<T extends
|
|
21
|
+
export type PropsFilter<T extends AnyProperties> = (props: SchemaProperty<T>[]) => SchemaProperty<T>[];
|
|
20
22
|
|
|
21
23
|
export type ComponentLookup = (args: {
|
|
22
24
|
prop: string;
|
|
@@ -26,30 +28,34 @@ export type ComponentLookup = (args: {
|
|
|
26
28
|
|
|
27
29
|
export type CustomInputMap = Partial<Record<string, InputComponent>>;
|
|
28
30
|
|
|
29
|
-
export type FormProps<T extends
|
|
31
|
+
export type FormProps<T extends AnyProperties> = ThemedClassName<{
|
|
32
|
+
id?: string;
|
|
30
33
|
values: Partial<T>;
|
|
31
34
|
// TODO(burdon): Autofocus first input.
|
|
32
35
|
autoFocus?: boolean;
|
|
33
36
|
// TODO(burdon): Change to JsonPath includes/excludes.
|
|
34
37
|
filter?: PropsFilter<T>;
|
|
35
38
|
sort?: PropertyKey<T>[];
|
|
39
|
+
projection?: ProjectionModel;
|
|
36
40
|
autoSave?: boolean;
|
|
37
41
|
outerSpacing?: FormOuterSpacing;
|
|
38
42
|
onCancel?: () => void;
|
|
39
43
|
}> &
|
|
40
44
|
Pick<FormOptions<T>, 'schema' | 'onValuesChanged' | 'onValidate' | 'onSave'> &
|
|
45
|
+
// TODO(wittjosiah): This needs to support different ref field options per field.
|
|
41
46
|
FormFieldsProps;
|
|
42
47
|
|
|
43
|
-
export const Form = <T extends
|
|
48
|
+
export const Form = <T extends AnyProperties>({
|
|
44
49
|
classNames,
|
|
50
|
+
id,
|
|
45
51
|
testId,
|
|
46
52
|
values: initialValues,
|
|
47
53
|
autoFocus,
|
|
48
54
|
readonly,
|
|
49
55
|
autoSave,
|
|
50
56
|
outerSpacing = true,
|
|
51
|
-
onCancel,
|
|
52
57
|
schema,
|
|
58
|
+
onCancel,
|
|
53
59
|
onValuesChanged,
|
|
54
60
|
onValidate,
|
|
55
61
|
onSave,
|
|
@@ -57,19 +63,27 @@ export const Form = <T extends BaseObject>({
|
|
|
57
63
|
}: FormProps<T>) => {
|
|
58
64
|
const formRef = useRef<HTMLDivElement>(null);
|
|
59
65
|
|
|
60
|
-
//
|
|
61
|
-
const
|
|
62
|
-
|
|
63
|
-
// Focus the first input element within this form.
|
|
66
|
+
// Focus the first focusable element within this form.
|
|
67
|
+
const { findFirstFocusable } = useFocusFinders();
|
|
64
68
|
useEffect(() => {
|
|
65
69
|
if (autoFocus && formRef.current) {
|
|
66
|
-
const
|
|
67
|
-
if (
|
|
68
|
-
|
|
70
|
+
const firstFocusable = findFirstFocusable(formRef.current);
|
|
71
|
+
if (firstFocusable) {
|
|
72
|
+
firstFocusable.focus();
|
|
69
73
|
}
|
|
70
74
|
}
|
|
71
75
|
}, [autoFocus]);
|
|
72
76
|
|
|
77
|
+
// TODO(burdon): Name?
|
|
78
|
+
const handleValid = useCallback(
|
|
79
|
+
async (values: any, meta: { changed: FormHandler<T>['changed'] }) => {
|
|
80
|
+
if (autoSave) {
|
|
81
|
+
await onSave?.(values, meta);
|
|
82
|
+
}
|
|
83
|
+
},
|
|
84
|
+
[autoSave, onSave],
|
|
85
|
+
);
|
|
86
|
+
|
|
73
87
|
return (
|
|
74
88
|
<FormProvider
|
|
75
89
|
formRef={formRef}
|
|
@@ -81,7 +95,7 @@ export const Form = <T extends BaseObject>({
|
|
|
81
95
|
onValid={handleValid}
|
|
82
96
|
onSave={onSave}
|
|
83
97
|
>
|
|
84
|
-
<div role='none' className='contents' data-testid={testId}>
|
|
98
|
+
<div role='none' className='contents' {...(id && { 'data-object-id': id })} data-testid={testId}>
|
|
85
99
|
<FormFields
|
|
86
100
|
{...props}
|
|
87
101
|
ref={formRef}
|
|
@@ -99,7 +113,7 @@ export const Form = <T extends BaseObject>({
|
|
|
99
113
|
readonly={readonly}
|
|
100
114
|
schema={schema}
|
|
101
115
|
/>
|
|
102
|
-
{(onCancel || onSave) && !autoSave && (
|
|
116
|
+
{(onCancel || onSave) && !autoSave && !readonly && (
|
|
103
117
|
<FormActions readonly={readonly} onCancel={onCancel} outerSpacing={outerSpacing} />
|
|
104
118
|
)}
|
|
105
119
|
</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 = {
|
|
@@ -36,7 +37,12 @@ export const FormActions = ({ readonly, onCancel, outerSpacing = true }: FormAct
|
|
|
36
37
|
)}
|
|
37
38
|
>
|
|
38
39
|
{onCancel && !readonly && (
|
|
39
|
-
<IconButton
|
|
40
|
+
<IconButton
|
|
41
|
+
data-testid='cancel-button'
|
|
42
|
+
icon='ph--x--regular'
|
|
43
|
+
label={t('cancel button label')}
|
|
44
|
+
onClick={onCancel}
|
|
45
|
+
/>
|
|
40
46
|
)}
|
|
41
47
|
{handleSave && (
|
|
42
48
|
<IconButton
|
|
@@ -44,7 +50,7 @@ export const FormActions = ({ readonly, onCancel, outerSpacing = true }: FormAct
|
|
|
44
50
|
data-testid='save-button'
|
|
45
51
|
disabled={!canSave}
|
|
46
52
|
icon='ph--check--regular'
|
|
47
|
-
label={t('button
|
|
53
|
+
label={t('save button label')}
|
|
48
54
|
onClick={handleSave}
|
|
49
55
|
/>
|
|
50
56
|
)}
|