@dxos/react-ui-form 0.7.2 → 0.7.3-staging.0905f03
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 +228 -58
- package/dist/lib/browser/index.mjs.map +3 -3
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/node/index.cjs +228 -57
- package/dist/lib/node/index.cjs.map +3 -3
- package/dist/lib/node/meta.json +1 -1
- package/dist/lib/node-esm/index.mjs +228 -58
- 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/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 +11 -2
- package/dist/types/src/components/Form/Form.d.ts.map +1 -1
- package/dist/types/src/components/Form/Form.stories.d.ts +20 -5
- package/dist/types/src/components/Form/Form.stories.d.ts.map +1 -1
- package/dist/types/src/components/Form/Input.d.ts +5 -4
- package/dist/types/src/components/Form/Input.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 +19 -11
- 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/Defaults.tsx +28 -13
- package/src/components/Form/Form.stories.tsx +81 -19
- package/src/components/Form/Form.tsx +216 -29
- package/src/components/Form/Input.tsx +5 -4
- package/src/components/ViewEditor/ViewEditor.tsx +8 -7
- package/src/components/deprecated/Form.stories.tsx +4 -4
- package/src/hooks/useForm.ts +65 -32
|
@@ -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';
|
|
@@ -15,42 +16,42 @@ import { Form, type FormProps } from './Form';
|
|
|
15
16
|
import translations from '../../translations';
|
|
16
17
|
import { TestLayout, TestPanel } from '../testing';
|
|
17
18
|
|
|
19
|
+
const AddressSchema = S.Struct({
|
|
20
|
+
street: S.optional(S.String.annotations({ [AST.TitleAnnotationId]: 'Street' })),
|
|
21
|
+
city: S.optional(S.String.annotations({ [AST.TitleAnnotationId]: 'City' })),
|
|
22
|
+
zip: S.optional(S.String.pipe(S.pattern(/^\d{5}(-\d{4})?$/)).annotations({ [AST.TitleAnnotationId]: 'ZIP' })),
|
|
23
|
+
location: S.optional(Format.GeoPoint.annotations({ [AST.TitleAnnotationId]: 'Location' })),
|
|
24
|
+
}).annotations({ [AST.TitleAnnotationId]: 'Address' });
|
|
25
|
+
|
|
18
26
|
// TODO(burdon): Translations?
|
|
19
27
|
const TestSchema = S.Struct({
|
|
20
28
|
name: S.optional(S.String.annotations({ [AST.TitleAnnotationId]: 'Name' })),
|
|
21
29
|
active: S.optional(S.Boolean.annotations({ [AST.TitleAnnotationId]: 'Active' })),
|
|
22
30
|
rank: S.optional(S.Number.annotations({ [AST.TitleAnnotationId]: 'Rank' })),
|
|
23
31
|
website: S.optional(Format.URL.annotations({ [AST.TitleAnnotationId]: 'Website' })),
|
|
24
|
-
address: S.optional(
|
|
25
|
-
S.Struct({
|
|
26
|
-
street: S.optional(S.String.annotations({ [AST.TitleAnnotationId]: 'Street' })),
|
|
27
|
-
city: S.optional(S.String.annotations({ [AST.TitleAnnotationId]: 'City' })),
|
|
28
|
-
zip: S.optional(S.String.pipe(S.pattern(/^\d{5}(-\d{4})?$/)).annotations({ [AST.TitleAnnotationId]: 'ZIP' })),
|
|
29
|
-
location: S.optional(Format.GeoPoint.annotations({ [AST.TitleAnnotationId]: 'Location' })),
|
|
30
|
-
}).annotations({ [AST.TitleAnnotationId]: 'Address' }),
|
|
31
|
-
),
|
|
32
|
+
address: S.optional(AddressSchema),
|
|
32
33
|
}).pipe(S.mutable);
|
|
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>
|
|
@@ -135,3 +165,35 @@ export const DiscriminatedShape: StoryObj<DiscriminatedUnionStoryProps> = {
|
|
|
135
165
|
},
|
|
136
166
|
},
|
|
137
167
|
};
|
|
168
|
+
|
|
169
|
+
const ArraysSchema = S.Struct({
|
|
170
|
+
names: S.Array(S.String.pipe(S.nonEmptyString())),
|
|
171
|
+
addresses: S.Array(AddressSchema),
|
|
172
|
+
}).pipe(S.mutable);
|
|
173
|
+
|
|
174
|
+
type ArraysType = S.Schema.Type<typeof ArraysSchema>;
|
|
175
|
+
|
|
176
|
+
const ArraysStory = ({ values: initialValues }: FormProps<ArraysType>) => {
|
|
177
|
+
const [values, setValues] = useState(initialValues);
|
|
178
|
+
const handleSave = useCallback<NonNullable<FormProps<ArraysType>['onSave']>>((values) => {
|
|
179
|
+
setValues(values);
|
|
180
|
+
}, []);
|
|
181
|
+
|
|
182
|
+
return (
|
|
183
|
+
<TestLayout json={{ values, schema: ArraysSchema.ast.toJSON() }}>
|
|
184
|
+
<TestPanel>
|
|
185
|
+
<Form<ArraysType> schema={ArraysSchema} values={values} onSave={handleSave} />
|
|
186
|
+
</TestPanel>
|
|
187
|
+
</TestLayout>
|
|
188
|
+
);
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
export const Arrays: StoryObj<FormProps<ArraysType>> = {
|
|
192
|
+
render: ArraysStory,
|
|
193
|
+
args: {
|
|
194
|
+
values: {
|
|
195
|
+
names: ['Alice', 'Bob'],
|
|
196
|
+
addresses: [],
|
|
197
|
+
},
|
|
198
|
+
},
|
|
199
|
+
};
|
|
@@ -6,8 +6,8 @@ import { pipe } from 'effect';
|
|
|
6
6
|
import { capitalize } from 'effect/String';
|
|
7
7
|
import React, { useEffect, useMemo } from 'react';
|
|
8
8
|
|
|
9
|
-
import { AST, type BaseObject, S, type PropertyKey } from '@dxos/echo-schema';
|
|
10
|
-
import { findNode, getDiscriminatedType, isDiscriminatedUnion } from '@dxos/effect';
|
|
9
|
+
import { AST, type BaseObject, S, type PropertyKey, type FormatEnum } from '@dxos/echo-schema';
|
|
10
|
+
import { findNode, getDiscriminatedType, isDiscriminatedUnion, SimpleType } from '@dxos/effect';
|
|
11
11
|
import { log } from '@dxos/log';
|
|
12
12
|
import { IconButton, type ThemedClassName, useTranslation } from '@dxos/react-ui';
|
|
13
13
|
import { mx } from '@dxos/react-ui-theme';
|
|
@@ -15,9 +15,9 @@ import { getSchemaProperties, type SchemaProperty } from '@dxos/schema';
|
|
|
15
15
|
import { isNotFalsy } from '@dxos/util';
|
|
16
16
|
|
|
17
17
|
import { SelectInput } from './Defaults';
|
|
18
|
-
import { type InputComponent } from './Input';
|
|
18
|
+
import { InputHeader, type InputComponent } from './Input';
|
|
19
19
|
import { getInputComponent } from './factory';
|
|
20
|
-
import { type FormOptions, useForm } from '../../hooks';
|
|
20
|
+
import { type FormHandler, type FormOptions, useForm } from '../../hooks';
|
|
21
21
|
import { translationKey } from '../../translations';
|
|
22
22
|
|
|
23
23
|
// TODO(burdon): Rename package react-ui-form; delete react-ui-card.
|
|
@@ -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,21 @@ 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
|
-
const
|
|
70
|
-
const
|
|
71
|
-
const { canSubmit, values, errors, handleSubmit, ...inputProps } = useForm<T>({
|
|
74
|
+
const onValid = useMemo(() => (autoSave ? onSave : undefined), [autoSave, onSave]);
|
|
75
|
+
const { canSave, values, errors, handleSave, ...inputProps } = useForm<T>({
|
|
72
76
|
schema,
|
|
73
77
|
initialValues,
|
|
74
78
|
onValuesChanged,
|
|
75
79
|
onValidate,
|
|
76
80
|
onValid,
|
|
77
|
-
|
|
81
|
+
onSave,
|
|
78
82
|
});
|
|
79
83
|
|
|
80
84
|
// Filter and sort props.
|
|
@@ -98,15 +102,16 @@ export const Form = <T extends BaseObject>({
|
|
|
98
102
|
}, [errors]);
|
|
99
103
|
|
|
100
104
|
return (
|
|
101
|
-
<div role='form' className={mx('flex flex-col w-full gap-2 py-2', classNames)}>
|
|
105
|
+
<div role='form' className={mx('flex flex-col w-full gap-2 py-2', classNames)} data-testid={testId}>
|
|
102
106
|
{properties
|
|
103
107
|
.map((property) => {
|
|
104
|
-
const { ast, name, type, format, title, description, examples, options } = property;
|
|
108
|
+
const { ast, name, type, format, title, description, examples, options, array } = property;
|
|
105
109
|
const key = [...path, name];
|
|
106
|
-
const label =
|
|
110
|
+
const label = title ?? pipe(name, capitalize);
|
|
107
111
|
const placeholder = examples?.length ? `Example: "${examples[0]}"` : description;
|
|
108
112
|
|
|
109
113
|
// Get generic input.
|
|
114
|
+
// TODO(ZaymonFC): We might need to switch this to using globs since we're now indexing into arrays.
|
|
110
115
|
let InputComponent = Custom?.[key.join('.')];
|
|
111
116
|
if (!InputComponent) {
|
|
112
117
|
// Select.
|
|
@@ -128,6 +133,26 @@ export const Form = <T extends BaseObject>({
|
|
|
128
133
|
}
|
|
129
134
|
|
|
130
135
|
InputComponent = getInputComponent<T>(type, format);
|
|
136
|
+
|
|
137
|
+
// TODO(ZaymonFC): Array handling is inelegant. We need to push this complexity down into SDK/Schema.
|
|
138
|
+
if (array) {
|
|
139
|
+
return (
|
|
140
|
+
<ArrayField
|
|
141
|
+
key={name}
|
|
142
|
+
name={name}
|
|
143
|
+
type={type}
|
|
144
|
+
array={true}
|
|
145
|
+
label={label}
|
|
146
|
+
ast={ast}
|
|
147
|
+
values={values}
|
|
148
|
+
format={format}
|
|
149
|
+
readonly={readonly}
|
|
150
|
+
placeholder={placeholder}
|
|
151
|
+
inputProps={inputProps}
|
|
152
|
+
Custom={Custom}
|
|
153
|
+
/>
|
|
154
|
+
);
|
|
155
|
+
}
|
|
131
156
|
}
|
|
132
157
|
|
|
133
158
|
if (!InputComponent) {
|
|
@@ -176,25 +201,187 @@ export const Form = <T extends BaseObject>({
|
|
|
176
201
|
);
|
|
177
202
|
})
|
|
178
203
|
.filter(isNotFalsy)}
|
|
204
|
+
{(onCancel || onSave) && !autoSave && (
|
|
205
|
+
<FormActions onCancel={onCancel} onSubmit={handleSave} canSubmit={canSave} readonly={readonly} />
|
|
206
|
+
)}
|
|
207
|
+
</div>
|
|
208
|
+
);
|
|
209
|
+
};
|
|
179
210
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
211
|
+
type ArrayFieldProps<T extends BaseObject> = {
|
|
212
|
+
name: PropertyKey<T>;
|
|
213
|
+
type: SimpleType;
|
|
214
|
+
array: true;
|
|
215
|
+
label: string;
|
|
216
|
+
ast: AST.AST;
|
|
217
|
+
values: Record<string, any>;
|
|
218
|
+
format?: FormatEnum;
|
|
219
|
+
readonly?: boolean;
|
|
220
|
+
placeholder?: string;
|
|
221
|
+
inputProps: Pick<FormHandler<T>, 'getStatus' | 'getValue' | 'onValueChange' | 'onBlur'>;
|
|
222
|
+
Custom?: Partial<Record<string, InputComponent<T>>>;
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
const ArrayField = <T extends BaseObject>({
|
|
226
|
+
name,
|
|
227
|
+
type,
|
|
228
|
+
label,
|
|
229
|
+
ast,
|
|
230
|
+
values,
|
|
231
|
+
format,
|
|
232
|
+
readonly,
|
|
233
|
+
placeholder,
|
|
234
|
+
inputProps,
|
|
235
|
+
Custom,
|
|
236
|
+
}: ArrayFieldProps<T>) => {
|
|
237
|
+
const { t } = useTranslation(translationKey);
|
|
238
|
+
const arrayValues = (values[name] ?? []) as any[];
|
|
239
|
+
const key = [name];
|
|
240
|
+
|
|
241
|
+
// TODO(ZaymonFC): Should this unwrapping happen at a lower level?
|
|
242
|
+
const tupleType = findNode(ast, AST.isTupleType);
|
|
243
|
+
const elementType = (tupleType as AST.TupleType | undefined)?.rest[0]?.type;
|
|
244
|
+
|
|
245
|
+
if (type === 'object' && elementType) {
|
|
246
|
+
const baseNode = findNode(elementType, isDiscriminatedUnion);
|
|
247
|
+
const typeLiteral = baseNode
|
|
248
|
+
? getDiscriminatedType(baseNode, values[name] as any)
|
|
249
|
+
: findNode(elementType, AST.isTypeLiteral);
|
|
250
|
+
|
|
251
|
+
if (typeLiteral) {
|
|
252
|
+
const schema = S.make(typeLiteral);
|
|
253
|
+
|
|
254
|
+
return (
|
|
255
|
+
<div key={name} role='none' className={mx(padding)}>
|
|
256
|
+
<InputHeader>{label}</InputHeader>
|
|
257
|
+
<div role='none' className='flex flex-col gap-1'>
|
|
258
|
+
{arrayValues.map((value, index) => {
|
|
259
|
+
return (
|
|
260
|
+
<div key={index} role='none' className='flex items-center gap-1'>
|
|
261
|
+
<div role='none' className='flex-1'>
|
|
262
|
+
<Form<any>
|
|
263
|
+
schema={schema}
|
|
264
|
+
path={[...key, index.toString()]}
|
|
265
|
+
values={value}
|
|
266
|
+
onValuesChanged={(childValues) => {
|
|
267
|
+
const newValues = [...arrayValues];
|
|
268
|
+
newValues[index] = childValues;
|
|
269
|
+
inputProps.onValueChange(name, 'object', newValues);
|
|
270
|
+
}}
|
|
271
|
+
Custom={Custom as any}
|
|
272
|
+
/>
|
|
273
|
+
</div>
|
|
274
|
+
<IconButton
|
|
275
|
+
icon='ph--trash--regular'
|
|
276
|
+
iconOnly
|
|
277
|
+
label={t('button remove')}
|
|
278
|
+
onClick={() => {
|
|
279
|
+
const newValues = arrayValues.filter((_, i) => i !== index);
|
|
280
|
+
inputProps.onValueChange(name, type, newValues);
|
|
281
|
+
}}
|
|
282
|
+
/>
|
|
283
|
+
</div>
|
|
284
|
+
);
|
|
285
|
+
})}
|
|
286
|
+
</div>
|
|
287
|
+
<div role='none' className='flex justify-between items-center plb-1'>
|
|
288
|
+
<IconButton
|
|
289
|
+
icon='ph--plus--regular'
|
|
290
|
+
iconOnly
|
|
291
|
+
label={t('button add')}
|
|
292
|
+
onClick={() => {
|
|
293
|
+
const newValues = [...arrayValues, {}];
|
|
294
|
+
inputProps.onValueChange(name, type, newValues);
|
|
295
|
+
}}
|
|
296
|
+
/>
|
|
195
297
|
</div>
|
|
196
298
|
</div>
|
|
197
|
-
)
|
|
299
|
+
);
|
|
300
|
+
}
|
|
301
|
+
return (
|
|
302
|
+
<div key={name} role='none'>
|
|
303
|
+
Nested form not supported in arrays, yet.
|
|
304
|
+
</div>
|
|
305
|
+
);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
const InputComponent = getInputComponent<T>(type, format);
|
|
309
|
+
if (!InputComponent) {
|
|
310
|
+
return null;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
return (
|
|
314
|
+
<div role='none' key='meta-input' className={mx(padding)}>
|
|
315
|
+
<InputHeader>{label}</InputHeader>
|
|
316
|
+
<div role='none' className='flex flex-col gap-1'>
|
|
317
|
+
{arrayValues.map((_, index) => (
|
|
318
|
+
<div key={index} role='none' className='flex items-start gap-1'>
|
|
319
|
+
<div role='none' className='flex-1'>
|
|
320
|
+
<InputComponent
|
|
321
|
+
type={type}
|
|
322
|
+
format={format}
|
|
323
|
+
label={label}
|
|
324
|
+
inputOnly
|
|
325
|
+
property={`${name}.${index}`}
|
|
326
|
+
disabled={readonly}
|
|
327
|
+
placeholder={placeholder}
|
|
328
|
+
{...inputProps}
|
|
329
|
+
/>
|
|
330
|
+
</div>
|
|
331
|
+
<IconButton
|
|
332
|
+
icon='ph--trash--regular'
|
|
333
|
+
iconOnly
|
|
334
|
+
label={t('button remove')}
|
|
335
|
+
onClick={() => {
|
|
336
|
+
const newValues = arrayValues.filter((_, i) => i !== index);
|
|
337
|
+
inputProps.onValueChange(name, type, newValues);
|
|
338
|
+
}}
|
|
339
|
+
/>
|
|
340
|
+
</div>
|
|
341
|
+
))}
|
|
342
|
+
</div>
|
|
343
|
+
<div role='none' className='flex justify-between items-center plb-1'>
|
|
344
|
+
<IconButton
|
|
345
|
+
icon='ph--plus--regular'
|
|
346
|
+
iconOnly
|
|
347
|
+
label={t('button add')}
|
|
348
|
+
onClick={() => {
|
|
349
|
+
const newValues = [...arrayValues, SimpleType.getDefaultValue(type)];
|
|
350
|
+
inputProps.onValueChange(name, type, newValues);
|
|
351
|
+
}}
|
|
352
|
+
/>
|
|
353
|
+
</div>
|
|
354
|
+
</div>
|
|
355
|
+
);
|
|
356
|
+
};
|
|
357
|
+
|
|
358
|
+
type FormActionsProps = {
|
|
359
|
+
readonly?: boolean;
|
|
360
|
+
canSubmit: boolean;
|
|
361
|
+
onCancel?: () => void;
|
|
362
|
+
onSubmit: () => void;
|
|
363
|
+
};
|
|
364
|
+
|
|
365
|
+
export const FormActions = ({ onCancel, onSubmit, canSubmit, readonly }: FormActionsProps) => {
|
|
366
|
+
const { t } = useTranslation(translationKey);
|
|
367
|
+
|
|
368
|
+
return (
|
|
369
|
+
<div role='none' className='flex justify-center'>
|
|
370
|
+
<div role='none' className={mx(onCancel && !readonly && 'grid grid-cols-2 gap-2')}>
|
|
371
|
+
{onCancel && !readonly && (
|
|
372
|
+
<IconButton data-testid='cancel-button' icon='ph--x--regular' label={t('button cancel')} onClick={onCancel} />
|
|
373
|
+
)}
|
|
374
|
+
{onSubmit && (
|
|
375
|
+
<IconButton
|
|
376
|
+
type='submit'
|
|
377
|
+
data-testid='save-button'
|
|
378
|
+
disabled={!canSubmit}
|
|
379
|
+
icon='ph--check--regular'
|
|
380
|
+
label={t('button save')}
|
|
381
|
+
onClick={onSubmit}
|
|
382
|
+
/>
|
|
383
|
+
)}
|
|
384
|
+
</div>
|
|
198
385
|
</div>
|
|
199
386
|
);
|
|
200
387
|
};
|
|
@@ -4,23 +4,24 @@
|
|
|
4
4
|
|
|
5
5
|
import React, { type FC, type PropsWithChildren } from 'react';
|
|
6
6
|
|
|
7
|
-
import { type BaseObject, type FormatEnum
|
|
7
|
+
import { type BaseObject, type FormatEnum } from '@dxos/echo-schema';
|
|
8
8
|
import { type SimpleType } from '@dxos/effect';
|
|
9
9
|
import { Icon, Tooltip } from '@dxos/react-ui';
|
|
10
10
|
import { errorText } from '@dxos/react-ui-theme';
|
|
11
11
|
|
|
12
|
-
import { type FormHandler } from '../../hooks';
|
|
12
|
+
import { type FormPath, type FormHandler } from '../../hooks';
|
|
13
13
|
|
|
14
14
|
/**
|
|
15
15
|
* Props passed to input components.
|
|
16
16
|
*/
|
|
17
17
|
export type InputProps<T extends BaseObject> = {
|
|
18
|
-
property:
|
|
18
|
+
property: FormPath<T>;
|
|
19
19
|
type: SimpleType;
|
|
20
20
|
format?: FormatEnum;
|
|
21
|
-
label
|
|
21
|
+
label?: string;
|
|
22
22
|
disabled?: boolean;
|
|
23
23
|
placeholder?: string;
|
|
24
|
+
inputOnly?: boolean;
|
|
24
25
|
} & Pick<FormHandler<T>, 'getStatus' | 'getValue' | 'onValueChange' | 'onBlur'>;
|
|
25
26
|
|
|
26
27
|
/**
|
|
@@ -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.type,
|
|
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.type =
|
|
72
|
-
schema.updateTypename(
|
|
71
|
+
view.query.type = 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
|
|