@dxos/react-ui-form 0.8.2-main.fbd8ed0 → 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 +1445 -1245
- package/dist/lib/browser/index.mjs.map +3 -3
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/node/index.cjs +1431 -1231
- package/dist/lib/node/index.cjs.map +3 -3
- package/dist/lib/node/meta.json +1 -1
- package/dist/lib/node-esm/index.mjs +1445 -1245
- package/dist/lib/node-esm/index.mjs.map +3 -3
- 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.map +1 -1
- package/dist/types/src/components/Form/Form.stories.d.ts +1 -2
- 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 +6 -5
- 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/RefField.d.ts.map +1 -1
- 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/testing.d.ts.map +1 -1
- package/dist/types/src/hooks/useRefQueryLookupHandler.d.ts.map +1 -1
- package/dist/types/src/translations.d.ts +5 -0
- package/dist/types/src/translations.d.ts.map +1 -1
- package/dist/types/src/vitest-setup.d.ts +2 -0
- package/dist/types/src/vitest-setup.d.ts.map +1 -0
- package/package.json +27 -25
- package/src/components/ControlSection/ControlSection.tsx +2 -2
- package/src/components/FieldEditor/FieldEditor.test.tsx +49 -0
- package/src/components/FieldEditor/FieldEditor.tsx +3 -2
- package/src/components/Form/ArrayField.tsx +51 -34
- package/src/components/Form/Defaults.tsx +74 -48
- package/src/components/Form/Form.stories.tsx +77 -101
- package/src/components/Form/Form.tsx +1 -2
- package/src/components/Form/FormActions.tsx +15 -18
- package/src/components/Form/FormContent.tsx +34 -35
- package/src/components/Form/FormContext.tsx +11 -7
- package/src/components/Form/Input.tsx +1 -1
- package/src/components/Form/RefField.tsx +30 -10
- package/src/components/ViewEditor/ViewEditor.stories.tsx +14 -5
- package/src/components/ViewEditor/ViewEditor.tsx +51 -24
- package/src/components/testing.tsx +5 -9
- package/src/hooks/useRefQueryLookupHandler.ts +1 -3
- package/src/translations.ts +9 -2
- package/src/vitest-setup.ts +11 -0
|
@@ -5,14 +5,15 @@
|
|
|
5
5
|
import '@dxos-theme';
|
|
6
6
|
|
|
7
7
|
import { type Meta, type StoryObj } from '@storybook/react';
|
|
8
|
-
import { Schema
|
|
8
|
+
import { Schema } from 'effect';
|
|
9
9
|
import React, { useCallback, useState } from 'react';
|
|
10
10
|
|
|
11
11
|
import { ContactType } from '@dxos/client/testing';
|
|
12
12
|
import { type BaseObject, Expando, Format, getDXN, Ref, type TypeAnnotation } from '@dxos/echo-schema';
|
|
13
13
|
import { live } from '@dxos/live-object';
|
|
14
|
+
import { withSurfaceVariantsLayout } from '@dxos/react-ui/testing';
|
|
14
15
|
import { Testing } from '@dxos/schema/testing';
|
|
15
|
-
import {
|
|
16
|
+
import { withTheme } from '@dxos/storybook-utils';
|
|
16
17
|
|
|
17
18
|
import { SelectInput } from './Defaults';
|
|
18
19
|
import { Form, type FormProps } from './Form';
|
|
@@ -20,20 +21,18 @@ import translations from '../../translations';
|
|
|
20
21
|
import { TestLayout, TestPanel } from '../testing';
|
|
21
22
|
|
|
22
23
|
const AddressSchema = Schema.Struct({
|
|
23
|
-
street: Schema.optional(Schema.String.annotations({
|
|
24
|
-
city: Schema.optional(Schema.String.annotations({
|
|
25
|
-
zip: Schema.optional(
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
location: Schema.optional(Format.GeoPoint.annotations({ [SchemaAST.TitleAnnotationId]: 'Location' })),
|
|
29
|
-
}).annotations({ [SchemaAST.TitleAnnotationId]: 'Address' });
|
|
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' });
|
|
30
29
|
|
|
31
30
|
// TODO(burdon): Translations?
|
|
32
31
|
const TestSchema = Schema.Struct({
|
|
33
|
-
name: Schema.optional(Schema.String.annotations({
|
|
34
|
-
active: Schema.optional(Schema.Boolean.annotations({
|
|
35
|
-
rank: Schema.optional(Schema.Number.annotations({
|
|
36
|
-
website: Schema.optional(Format.URL.annotations({
|
|
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' })),
|
|
37
36
|
address: Schema.optional(AddressSchema),
|
|
38
37
|
}).pipe(Schema.mutable);
|
|
39
38
|
|
|
@@ -47,6 +46,15 @@ const DefaultStory = <T extends BaseObject>({ schema, values: initialValues, ...
|
|
|
47
46
|
setValues(values);
|
|
48
47
|
}, []);
|
|
49
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
|
+
|
|
50
58
|
return (
|
|
51
59
|
<TestLayout json={{ values, schema: schema.ast.toJSON() }}>
|
|
52
60
|
<TestPanel>
|
|
@@ -59,11 +67,17 @@ const DefaultStory = <T extends BaseObject>({ schema, values: initialValues, ...
|
|
|
59
67
|
const meta: Meta<StoryProps<any>> = {
|
|
60
68
|
title: 'ui/react-ui-form/Form',
|
|
61
69
|
component: Form,
|
|
62
|
-
render:
|
|
63
|
-
decorators: [
|
|
70
|
+
render: DebugStory,
|
|
71
|
+
decorators: [withTheme],
|
|
64
72
|
parameters: {
|
|
65
73
|
translations,
|
|
66
74
|
},
|
|
75
|
+
argTypes: {
|
|
76
|
+
readonly: {
|
|
77
|
+
control: 'boolean',
|
|
78
|
+
description: 'Readonly',
|
|
79
|
+
},
|
|
80
|
+
},
|
|
67
81
|
};
|
|
68
82
|
|
|
69
83
|
export default meta;
|
|
@@ -71,6 +85,8 @@ export default meta;
|
|
|
71
85
|
type Story<T extends BaseObject> = StoryObj<StoryProps<T>>;
|
|
72
86
|
|
|
73
87
|
export const Default: Story<TestType> = {
|
|
88
|
+
render: DefaultStory,
|
|
89
|
+
decorators: [withSurfaceVariantsLayout(), withTheme],
|
|
74
90
|
args: {
|
|
75
91
|
schema: TestSchema,
|
|
76
92
|
values: {
|
|
@@ -80,6 +96,7 @@ export const Default: Story<TestType> = {
|
|
|
80
96
|
zip: '11205',
|
|
81
97
|
},
|
|
82
98
|
},
|
|
99
|
+
readonly: false,
|
|
83
100
|
},
|
|
84
101
|
};
|
|
85
102
|
|
|
@@ -88,8 +105,9 @@ export const Organization: Story<Testing.Organization> = {
|
|
|
88
105
|
schema: Testing.OrganizationSchema,
|
|
89
106
|
values: {
|
|
90
107
|
name: 'DXOS',
|
|
91
|
-
|
|
108
|
+
website: 'https://dxos.org',
|
|
92
109
|
},
|
|
110
|
+
readonly: false,
|
|
93
111
|
},
|
|
94
112
|
};
|
|
95
113
|
|
|
@@ -98,9 +116,10 @@ export const OrganizationAutoSave: Story<Testing.Organization> = {
|
|
|
98
116
|
schema: Testing.OrganizationSchema,
|
|
99
117
|
values: {
|
|
100
118
|
name: 'DXOS',
|
|
101
|
-
|
|
119
|
+
website: 'https://dxos.org',
|
|
102
120
|
},
|
|
103
121
|
autoSave: true,
|
|
122
|
+
readonly: false,
|
|
104
123
|
},
|
|
105
124
|
};
|
|
106
125
|
|
|
@@ -116,70 +135,44 @@ export const OrganizationAutoSave: Story<Testing.Organization> = {
|
|
|
116
135
|
// },
|
|
117
136
|
// };
|
|
118
137
|
|
|
119
|
-
//
|
|
120
|
-
// TODO(burdon): Move into separate storybook and use test types.
|
|
121
|
-
//
|
|
122
|
-
|
|
123
138
|
const ShapeSchema = Schema.Struct({
|
|
124
139
|
shape: Schema.optional(
|
|
125
140
|
Schema.Union(
|
|
126
141
|
Schema.Struct({
|
|
127
|
-
type: Schema.Literal('circle').annotations({
|
|
128
|
-
radius: Schema.optional(Schema.Number.annotations({
|
|
142
|
+
type: Schema.Literal('circle').annotations({ title: 'Type' }),
|
|
143
|
+
radius: Schema.optional(Schema.Number.annotations({ title: 'Radius' })),
|
|
129
144
|
}),
|
|
130
145
|
Schema.Struct({
|
|
131
|
-
type: Schema.Literal('square').annotations({
|
|
132
|
-
size: Schema.optional(
|
|
133
|
-
Schema.Number.pipe(Schema.nonNegative()).annotations({ [SchemaAST.TitleAnnotationId]: 'Size' }),
|
|
134
|
-
),
|
|
146
|
+
type: Schema.Literal('square').annotations({ title: 'Type' }),
|
|
147
|
+
size: Schema.optional(Schema.Number.pipe(Schema.nonNegative()).annotations({ title: 'Size' })),
|
|
135
148
|
}),
|
|
136
|
-
).annotations({
|
|
149
|
+
).annotations({ title: 'Shape' }),
|
|
137
150
|
),
|
|
138
151
|
}).pipe(Schema.mutable);
|
|
139
152
|
|
|
140
153
|
type ShapeType = Schema.Schema.Type<typeof ShapeSchema>;
|
|
141
154
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
const DiscriminatedUnionStory = ({ values: initialValues }: DiscriminatedUnionStoryProps) => {
|
|
145
|
-
const [values, setValues] = useState(initialValues);
|
|
146
|
-
const handleSave = useCallback<NonNullable<FormProps<ShapeType>['onSave']>>((values) => {
|
|
147
|
-
setValues(values);
|
|
148
|
-
}, []);
|
|
149
|
-
|
|
150
|
-
return (
|
|
151
|
-
<TestLayout json={{ values, schema: ShapeSchema.ast.toJSON() }}>
|
|
152
|
-
<TestPanel>
|
|
153
|
-
<Form<ShapeType>
|
|
154
|
-
schema={ShapeSchema}
|
|
155
|
-
values={values}
|
|
156
|
-
onSave={handleSave}
|
|
157
|
-
Custom={{
|
|
158
|
-
['shape.type' as const]: (props) => (
|
|
159
|
-
<SelectInput
|
|
160
|
-
{...props}
|
|
161
|
-
options={['circle', 'square'].map((value) => ({
|
|
162
|
-
value,
|
|
163
|
-
label: value,
|
|
164
|
-
}))}
|
|
165
|
-
/>
|
|
166
|
-
),
|
|
167
|
-
}}
|
|
168
|
-
/>
|
|
169
|
-
</TestPanel>
|
|
170
|
-
</TestLayout>
|
|
171
|
-
);
|
|
172
|
-
};
|
|
173
|
-
|
|
174
|
-
export const DiscriminatedShape: StoryObj<DiscriminatedUnionStoryProps> = {
|
|
175
|
-
render: DiscriminatedUnionStory,
|
|
155
|
+
export const DiscriminatedShape: Story<ShapeType> = {
|
|
176
156
|
args: {
|
|
157
|
+
schema: ShapeSchema,
|
|
158
|
+
readonly: false,
|
|
177
159
|
values: {
|
|
178
160
|
shape: {
|
|
179
161
|
type: 'circle',
|
|
180
162
|
radius: 5,
|
|
181
163
|
},
|
|
182
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
|
+
},
|
|
183
176
|
},
|
|
184
177
|
};
|
|
185
178
|
|
|
@@ -190,24 +183,10 @@ const ArraysSchema = Schema.Struct({
|
|
|
190
183
|
|
|
191
184
|
type ArraysType = Schema.Schema.Type<typeof ArraysSchema>;
|
|
192
185
|
|
|
193
|
-
const ArraysStory = ({ values: initialValues }: FormProps<ArraysType>) => {
|
|
194
|
-
const [values, setValues] = useState(initialValues);
|
|
195
|
-
const handleSave = useCallback<NonNullable<FormProps<ArraysType>['onSave']>>((values) => {
|
|
196
|
-
setValues(values);
|
|
197
|
-
}, []);
|
|
198
|
-
|
|
199
|
-
return (
|
|
200
|
-
<TestLayout json={{ values, schema: ArraysSchema.ast.toJSON() }}>
|
|
201
|
-
<TestPanel>
|
|
202
|
-
<Form<ArraysType> schema={ArraysSchema} values={values} onSave={handleSave} />
|
|
203
|
-
</TestPanel>
|
|
204
|
-
</TestLayout>
|
|
205
|
-
);
|
|
206
|
-
};
|
|
207
|
-
|
|
208
186
|
export const Arrays: StoryObj<FormProps<ArraysType>> = {
|
|
209
|
-
render: ArraysStory,
|
|
210
187
|
args: {
|
|
188
|
+
schema: ArraysSchema,
|
|
189
|
+
readonly: false,
|
|
211
190
|
values: {
|
|
212
191
|
names: ['Alice', 'Bob'],
|
|
213
192
|
addresses: [],
|
|
@@ -217,30 +196,16 @@ export const Arrays: StoryObj<FormProps<ArraysType>> = {
|
|
|
217
196
|
|
|
218
197
|
const ColorSchema = Schema.Struct({
|
|
219
198
|
color: Schema.Union(Schema.Literal('red'), Schema.Literal('green'), Schema.Literal('blue')).annotations({
|
|
220
|
-
|
|
199
|
+
title: 'Color',
|
|
221
200
|
}),
|
|
222
201
|
}).pipe(Schema.mutable);
|
|
223
202
|
|
|
224
203
|
type ColorType = Schema.Schema.Type<typeof ColorSchema>;
|
|
225
204
|
|
|
226
|
-
const EnumStory = ({ values: initialValues }: FormProps<ColorType>) => {
|
|
227
|
-
const [values, setValues] = useState(initialValues);
|
|
228
|
-
const handleSave = useCallback<NonNullable<FormProps<ColorType>['onSave']>>((values) => {
|
|
229
|
-
setValues(values);
|
|
230
|
-
}, []);
|
|
231
|
-
|
|
232
|
-
return (
|
|
233
|
-
<TestLayout json={{ values, schema: ColorSchema.ast.toJSON() }}>
|
|
234
|
-
<TestPanel>
|
|
235
|
-
<Form<ColorType> schema={ColorSchema} values={values} onSave={handleSave} />
|
|
236
|
-
</TestPanel>
|
|
237
|
-
</TestLayout>
|
|
238
|
-
);
|
|
239
|
-
};
|
|
240
|
-
|
|
241
205
|
export const Enum: StoryObj<FormProps<ColorType>> = {
|
|
242
|
-
render: EnumStory,
|
|
243
206
|
args: {
|
|
207
|
+
schema: ColorSchema,
|
|
208
|
+
readonly: false,
|
|
244
209
|
values: {
|
|
245
210
|
color: 'red',
|
|
246
211
|
},
|
|
@@ -254,15 +219,15 @@ const RefSchema = Schema.Struct({
|
|
|
254
219
|
unknownExpando: Schema.optional(Ref(Expando).annotations({ title: 'Optional Ref to an Expando (DXN Input)' })),
|
|
255
220
|
});
|
|
256
221
|
|
|
257
|
-
const
|
|
222
|
+
const contact1 = live(ContactType, { identifiers: [] });
|
|
223
|
+
const contact2 = live(ContactType, { identifiers: [] });
|
|
224
|
+
|
|
225
|
+
const RefStory = ({ values: initialValues, readonly }: FormProps<any>) => {
|
|
258
226
|
const [values, setValues] = useState(initialValues);
|
|
259
227
|
const handleSave = useCallback<NonNullable<FormProps<any>['onSave']>>((values) => {
|
|
260
228
|
setValues(values);
|
|
261
229
|
}, []);
|
|
262
230
|
|
|
263
|
-
const contact1 = live(ContactType, { identifiers: [] });
|
|
264
|
-
const contact2 = live(ContactType, { identifiers: [] });
|
|
265
|
-
|
|
266
231
|
const onQueryRefOptions = useCallback((typeInfo: TypeAnnotation) => {
|
|
267
232
|
switch (typeInfo.typename) {
|
|
268
233
|
case ContactType.typename:
|
|
@@ -278,7 +243,13 @@ const RefStory = ({ values: initialValues }: FormProps<any>) => {
|
|
|
278
243
|
return (
|
|
279
244
|
<TestLayout json={{ values, schema: RefSchema.ast.toJSON() }}>
|
|
280
245
|
<TestPanel>
|
|
281
|
-
<Form
|
|
246
|
+
<Form
|
|
247
|
+
schema={RefSchema}
|
|
248
|
+
values={values}
|
|
249
|
+
readonly={readonly}
|
|
250
|
+
onSave={handleSave}
|
|
251
|
+
onQueryRefOptions={onQueryRefOptions}
|
|
252
|
+
/>
|
|
282
253
|
</TestPanel>
|
|
283
254
|
</TestLayout>
|
|
284
255
|
);
|
|
@@ -286,5 +257,10 @@ const RefStory = ({ values: initialValues }: FormProps<any>) => {
|
|
|
286
257
|
|
|
287
258
|
export const Refs: StoryObj<FormProps<ContactType>> = {
|
|
288
259
|
render: RefStory,
|
|
289
|
-
args: {
|
|
260
|
+
args: {
|
|
261
|
+
readonly: false,
|
|
262
|
+
values: {
|
|
263
|
+
refArray: [Ref.make(contact1), Ref.make(contact2)],
|
|
264
|
+
} as any,
|
|
265
|
+
},
|
|
290
266
|
};
|
|
@@ -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
|
};
|
|
@@ -7,6 +7,7 @@ import { capitalize } from 'effect/String';
|
|
|
7
7
|
import React, { useMemo } from 'react';
|
|
8
8
|
|
|
9
9
|
import { createJsonPath, findNode, getDiscriminatedType, isDiscriminatedUnion } from '@dxos/effect';
|
|
10
|
+
import { type ThemedClassName } from '@dxos/react-ui';
|
|
10
11
|
import { mx } from '@dxos/react-ui-theme';
|
|
11
12
|
import { getSchemaProperties, type SchemaProperty } from '@dxos/schema';
|
|
12
13
|
import { isNotFalsy } from '@dxos/util';
|
|
@@ -68,7 +69,7 @@ export const FormField = ({
|
|
|
68
69
|
});
|
|
69
70
|
|
|
70
71
|
if (FoundComponent) {
|
|
71
|
-
return
|
|
72
|
+
return FoundComponent;
|
|
72
73
|
}
|
|
73
74
|
|
|
74
75
|
const jsonPath = createJsonPath(path ?? []);
|
|
@@ -124,17 +125,15 @@ export const FormField = ({
|
|
|
124
125
|
const InputComponent = getInputComponent(type, format);
|
|
125
126
|
if (InputComponent) {
|
|
126
127
|
return (
|
|
127
|
-
<
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
/>
|
|
137
|
-
</div>
|
|
128
|
+
<InputComponent
|
|
129
|
+
type={type}
|
|
130
|
+
format={format}
|
|
131
|
+
label={label}
|
|
132
|
+
inputOnly={inline}
|
|
133
|
+
placeholder={placeholder}
|
|
134
|
+
disabled={readonly}
|
|
135
|
+
{...inputProps}
|
|
136
|
+
/>
|
|
138
137
|
);
|
|
139
138
|
}
|
|
140
139
|
|
|
@@ -144,18 +143,16 @@ export const FormField = ({
|
|
|
144
143
|
|
|
145
144
|
if (options) {
|
|
146
145
|
return (
|
|
147
|
-
<
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
/>
|
|
158
|
-
</div>
|
|
146
|
+
<SelectInput
|
|
147
|
+
type={type}
|
|
148
|
+
format={format}
|
|
149
|
+
disabled={readonly}
|
|
150
|
+
inputOnly={inline}
|
|
151
|
+
label={label}
|
|
152
|
+
options={options.map((option) => ({ value: option, label: String(option) }))}
|
|
153
|
+
placeholder={placeholder}
|
|
154
|
+
{...inputProps}
|
|
155
|
+
/>
|
|
159
156
|
);
|
|
160
157
|
}
|
|
161
158
|
|
|
@@ -171,8 +168,8 @@ export const FormField = ({
|
|
|
171
168
|
|
|
172
169
|
if (typeLiteral) {
|
|
173
170
|
return (
|
|
174
|
-
|
|
175
|
-
{!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>}
|
|
176
173
|
<FormFields
|
|
177
174
|
schema={Schema.make(typeLiteral)}
|
|
178
175
|
path={path}
|
|
@@ -181,7 +178,7 @@ export const FormField = ({
|
|
|
181
178
|
Custom={Custom}
|
|
182
179
|
lookupComponent={lookupComponent}
|
|
183
180
|
/>
|
|
184
|
-
|
|
181
|
+
</>
|
|
185
182
|
);
|
|
186
183
|
}
|
|
187
184
|
}
|
|
@@ -189,27 +186,28 @@ export const FormField = ({
|
|
|
189
186
|
return null;
|
|
190
187
|
};
|
|
191
188
|
|
|
192
|
-
export type
|
|
189
|
+
export type FormFieldsProps = ThemedClassName<{
|
|
190
|
+
Custom?: Partial<Record<string, InputComponent>>;
|
|
193
191
|
schema: Schema.Schema.All;
|
|
194
192
|
path?: (string | number)[];
|
|
195
193
|
filter?: (props: SchemaProperty<any>[]) => SchemaProperty<any>[];
|
|
196
194
|
sort?: string[];
|
|
197
195
|
readonly?: boolean;
|
|
198
|
-
onQueryRefOptions?: QueryRefOptions;
|
|
199
196
|
lookupComponent?: ComponentLookup;
|
|
200
|
-
|
|
201
|
-
}
|
|
197
|
+
onQueryRefOptions?: QueryRefOptions;
|
|
198
|
+
}>;
|
|
202
199
|
|
|
203
200
|
export const FormFields = ({
|
|
201
|
+
classNames,
|
|
202
|
+
Custom,
|
|
204
203
|
schema,
|
|
205
204
|
path,
|
|
206
205
|
filter,
|
|
207
206
|
sort,
|
|
208
207
|
readonly,
|
|
209
|
-
onQueryRefOptions,
|
|
210
208
|
lookupComponent,
|
|
211
|
-
|
|
212
|
-
}:
|
|
209
|
+
onQueryRefOptions,
|
|
210
|
+
}: FormFieldsProps) => {
|
|
213
211
|
const values = useFormValues(path);
|
|
214
212
|
|
|
215
213
|
const properties = useMemo(() => {
|
|
@@ -220,7 +218,8 @@ export const FormFields = ({
|
|
|
220
218
|
}, [schema, values, filter, sort]);
|
|
221
219
|
|
|
222
220
|
return (
|
|
223
|
-
|
|
221
|
+
// TODO(burdon): Consistent padding (with composite forms).
|
|
222
|
+
<div role='form' className={mx('p-2 is-full', classNames)}>
|
|
224
223
|
{properties
|
|
225
224
|
.map((property) => {
|
|
226
225
|
return (
|
|
@@ -10,6 +10,7 @@ import React, {
|
|
|
10
10
|
useMemo,
|
|
11
11
|
type FocusEvent,
|
|
12
12
|
type PropsWithChildren,
|
|
13
|
+
useCallback,
|
|
13
14
|
} from 'react';
|
|
14
15
|
|
|
15
16
|
import { raise } from '@dxos/debug';
|
|
@@ -67,12 +68,8 @@ export const FormProvider = ({
|
|
|
67
68
|
>) => {
|
|
68
69
|
const form = useForm(formOptions);
|
|
69
70
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
return;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
const handleKeyDown = (event: KeyboardEvent) => {
|
|
71
|
+
const handleKeyDown = useCallback(
|
|
72
|
+
(event: KeyboardEvent) => {
|
|
76
73
|
const keyIsEnter = event.key === 'Enter';
|
|
77
74
|
const modifierUsed = event.ctrlKey || event.altKey || event.metaKey || event.shiftKey;
|
|
78
75
|
const inputIsTextarea = (event.target as HTMLElement).tagName.toLowerCase() === 'textarea';
|
|
@@ -94,7 +91,14 @@ export const FormProvider = ({
|
|
|
94
91
|
(event.target as HTMLElement).blur();
|
|
95
92
|
}
|
|
96
93
|
}
|
|
97
|
-
}
|
|
94
|
+
},
|
|
95
|
+
[form, autoSave],
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
useEffect(() => {
|
|
99
|
+
if (!formRef?.current) {
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
98
102
|
|
|
99
103
|
const formElement = formRef.current;
|
|
100
104
|
|
|
@@ -34,7 +34,7 @@ export type InputHeaderProps = PropsWithChildren<{
|
|
|
34
34
|
|
|
35
35
|
export const InputHeader = ({ children, error }: InputHeaderProps) => {
|
|
36
36
|
return (
|
|
37
|
-
<div role='none' className='flex justify-between items-center
|
|
37
|
+
<div role='none' className='flex justify-between items-center mbs-2 mbe-1 first:mbs-0'>
|
|
38
38
|
{children}
|
|
39
39
|
{error && (
|
|
40
40
|
<Tooltip.Trigger asChild content={error} side='bottom'>
|
|
@@ -14,13 +14,16 @@ import {
|
|
|
14
14
|
} from '@dxos/echo-schema';
|
|
15
15
|
import { findAnnotation } from '@dxos/effect';
|
|
16
16
|
import { DXN } from '@dxos/keys';
|
|
17
|
-
import {
|
|
17
|
+
import { DxRefTag } from '@dxos/lit-ui/react';
|
|
18
|
+
import { Input, useTranslation } from '@dxos/react-ui';
|
|
18
19
|
import { TagPicker, type TagPickerMode, type TagPickerItemData } from '@dxos/react-ui-tag-picker';
|
|
20
|
+
import { descriptionText, mx } from '@dxos/react-ui-theme';
|
|
19
21
|
import { isNonNullable } from '@dxos/util';
|
|
20
22
|
|
|
21
23
|
import { TextInput } from './Defaults';
|
|
22
24
|
import { InputHeader, type InputProps } from './Input';
|
|
23
25
|
import { type QueryRefOptions, useQueryRefOptions } from '../../hooks';
|
|
26
|
+
import { translationKey } from '../../translations';
|
|
24
27
|
|
|
25
28
|
type RefFieldProps = InputProps & {
|
|
26
29
|
ast?: SchemaAST.AST;
|
|
@@ -42,6 +45,7 @@ export const RefField = ({
|
|
|
42
45
|
onValueChange,
|
|
43
46
|
...restInputProps
|
|
44
47
|
}: RefFieldProps) => {
|
|
48
|
+
const { t } = useTranslation(translationKey);
|
|
45
49
|
const refTypeInfo = useMemo(
|
|
46
50
|
() => (ast ? findAnnotation<ReferenceAnnotationValue>(ast, ReferenceAnnotationId) : undefined),
|
|
47
51
|
[ast],
|
|
@@ -127,7 +131,10 @@ export const RefField = ({
|
|
|
127
131
|
|
|
128
132
|
const { status, error } = restInputProps.getStatus();
|
|
129
133
|
|
|
130
|
-
|
|
134
|
+
const items = handleGetValue();
|
|
135
|
+
|
|
136
|
+
// NOTE(thure): I left both predicates in-place in case we decide to add variants which do render readonly but empty values.
|
|
137
|
+
return disabled && items.length < 1 ? null : (
|
|
131
138
|
<Input.Root validationValence={status}>
|
|
132
139
|
{!inputOnly && (
|
|
133
140
|
<InputHeader error={error}>
|
|
@@ -135,14 +142,27 @@ export const RefField = ({
|
|
|
135
142
|
</InputHeader>
|
|
136
143
|
)}
|
|
137
144
|
<div data-no-submit>
|
|
138
|
-
|
|
139
|
-
items
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
145
|
+
{disabled ? (
|
|
146
|
+
items.length < 1 ? (
|
|
147
|
+
<p className={mx(descriptionText, 'mbe-2')}>{t('empty readonly ref field label')}</p>
|
|
148
|
+
) : (
|
|
149
|
+
items.map((item) => (
|
|
150
|
+
<DxRefTag key={item.id} refid={item.id} rootclassname='mie-1' style={{ marginInlineEnd: '.25rem' }}>
|
|
151
|
+
{item.label}
|
|
152
|
+
</DxRefTag>
|
|
153
|
+
))
|
|
154
|
+
)
|
|
155
|
+
) : (
|
|
156
|
+
<TagPicker
|
|
157
|
+
readonly={disabled}
|
|
158
|
+
items={items}
|
|
159
|
+
mode={tagPickerMode}
|
|
160
|
+
onBlur={(event) => onBlur(event as unknown as FocusEvent<HTMLElement>)}
|
|
161
|
+
onUpdate={handleUpdate}
|
|
162
|
+
onSearch={handleSearch}
|
|
163
|
+
classNames='rounded-sm bg-inputSurface p-1.5'
|
|
164
|
+
/>
|
|
165
|
+
)}
|
|
146
166
|
</div>
|
|
147
167
|
{inputOnly && <Input.DescriptionAndValidation>{error}</Input.DescriptionAndValidation>}
|
|
148
168
|
</Input.Root>
|