@dxos/react-ui-form 0.7.5-labs.e27f9b9 → 0.7.5-main.2567c87
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 +467 -726
- package/dist/lib/browser/index.mjs.map +4 -4
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/node/index.cjs +506 -750
- package/dist/lib/node/index.cjs.map +4 -4
- package/dist/lib/node/meta.json +1 -1
- package/dist/lib/node-esm/index.mjs +467 -726
- package/dist/lib/node-esm/index.mjs.map +4 -4
- package/dist/lib/node-esm/meta.json +1 -1
- package/dist/types/src/components/FieldEditor/FieldEditor.d.ts.map +1 -1
- package/dist/types/src/components/Form/Defaults.d.ts +6 -5
- package/dist/types/src/components/Form/Defaults.d.ts.map +1 -1
- package/dist/types/src/components/Form/Form.d.ts +21 -12
- 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/Input.d.ts +6 -5
- package/dist/types/src/components/Form/Input.d.ts.map +1 -1
- package/dist/types/src/components/Form/custom/GeoPointInput.d.ts +2 -1
- package/dist/types/src/components/Form/custom/GeoPointInput.d.ts.map +1 -1
- package/dist/types/src/components/Form/custom/TupleInput.d.ts +2 -1
- package/dist/types/src/components/Form/custom/TupleInput.d.ts.map +1 -1
- package/dist/types/src/components/Form/custom/index.d.ts +0 -1
- package/dist/types/src/components/Form/custom/index.d.ts.map +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/ViewEditor/ViewEditor.d.ts.map +1 -1
- package/dist/types/src/components/deprecated/FormInput.d.ts +0 -5
- package/dist/types/src/components/deprecated/FormInput.d.ts.map +1 -1
- package/dist/types/src/hooks/useForm.d.ts +11 -10
- package/dist/types/src/hooks/useForm.d.ts.map +1 -1
- package/dist/types/src/translations.d.ts +0 -6
- package/dist/types/src/translations.d.ts.map +1 -1
- package/package.json +27 -28
- package/src/components/FieldEditor/FieldEditor.stories.tsx +1 -1
- package/src/components/FieldEditor/FieldEditor.tsx +6 -7
- package/src/components/Form/Defaults.tsx +33 -20
- package/src/components/Form/Form.stories.tsx +5 -15
- package/src/components/Form/Form.tsx +355 -46
- package/src/components/Form/Input.tsx +6 -5
- package/src/components/Form/custom/GeoPointInput.tsx +15 -6
- package/src/components/Form/custom/TupleInput.tsx +9 -5
- package/src/components/Form/custom/index.ts +0 -1
- package/src/components/Form/factory.tsx +5 -2
- package/src/components/ViewEditor/ViewEditor.tsx +2 -5
- package/src/components/deprecated/FormInput.tsx +7 -20
- package/src/hooks/useForm.ts +49 -57
- package/src/translations.ts +0 -8
- package/dist/types/src/components/Form/ArrayField.d.ts +0 -16
- package/dist/types/src/components/Form/ArrayField.d.ts.map +0 -1
- package/dist/types/src/components/Form/FormActions.d.ts +0 -7
- package/dist/types/src/components/Form/FormActions.d.ts.map +0 -1
- package/dist/types/src/components/Form/FormContent.d.ts +0 -26
- package/dist/types/src/components/Form/FormContent.d.ts.map +0 -1
- package/dist/types/src/components/Form/FormContext.d.ts +0 -20
- package/dist/types/src/components/Form/FormContext.d.ts.map +0 -1
- package/dist/types/src/components/Form/custom/SelectOptionsInput.d.ts +0 -4
- package/dist/types/src/components/Form/custom/SelectOptionsInput.d.ts.map +0 -1
- package/dist/types/src/components/Form/custom/SelectOptionsInput.stories.d.ts +0 -7
- package/dist/types/src/components/Form/custom/SelectOptionsInput.stories.d.ts.map +0 -1
- package/src/components/Form/ArrayField.tsx +0 -111
- package/src/components/Form/FormActions.tsx +0 -41
- package/src/components/Form/FormContent.tsx +0 -173
- package/src/components/Form/FormContext.tsx +0 -48
- package/src/components/Form/custom/SelectOptionsInput.stories.tsx +0 -58
- package/src/components/Form/custom/SelectOptionsInput.tsx +0 -192
|
@@ -20,7 +20,7 @@ import {
|
|
|
20
20
|
} from '@dxos/schema';
|
|
21
21
|
|
|
22
22
|
import { translationKey } from '../../translations';
|
|
23
|
-
import { Form, type FormProps, type InputComponent, SelectInput
|
|
23
|
+
import { Form, type FormProps, type InputComponent, SelectInput } from '../Form';
|
|
24
24
|
|
|
25
25
|
export type FieldEditorProps = {
|
|
26
26
|
view: ViewType;
|
|
@@ -95,7 +95,7 @@ export const FieldEditor = ({ view, projection, field, registry, onSave, onCance
|
|
|
95
95
|
return { ...props, ..._props, type };
|
|
96
96
|
}
|
|
97
97
|
|
|
98
|
-
return
|
|
98
|
+
return props;
|
|
99
99
|
});
|
|
100
100
|
},
|
|
101
101
|
[schemas, props.format, props.referenceSchema],
|
|
@@ -129,10 +129,10 @@ export const FieldEditor = ({ view, projection, field, registry, onSave, onCance
|
|
|
129
129
|
onSave();
|
|
130
130
|
}, [onSave]);
|
|
131
131
|
|
|
132
|
-
const Custom: Partial<Record<string, InputComponent
|
|
132
|
+
const Custom: Partial<Record<string, InputComponent<PropertyType>>> = useMemo(
|
|
133
133
|
() => ({
|
|
134
134
|
['format' satisfies keyof PropertyType]: (props) => (
|
|
135
|
-
<SelectInput
|
|
135
|
+
<SelectInput<PropertyType>
|
|
136
136
|
{...props}
|
|
137
137
|
options={FormatEnums.filter((value) => value !== FormatEnum.None).map((value) => ({
|
|
138
138
|
value,
|
|
@@ -141,7 +141,7 @@ export const FieldEditor = ({ view, projection, field, registry, onSave, onCance
|
|
|
141
141
|
/>
|
|
142
142
|
),
|
|
143
143
|
['referenceSchema' satisfies keyof PropertyType]: (props) => (
|
|
144
|
-
<SelectInput
|
|
144
|
+
<SelectInput<PropertyType>
|
|
145
145
|
{...props}
|
|
146
146
|
options={schemas.map((schema) => ({
|
|
147
147
|
value: schema.typename,
|
|
@@ -149,7 +149,7 @@ export const FieldEditor = ({ view, projection, field, registry, onSave, onCance
|
|
|
149
149
|
/>
|
|
150
150
|
),
|
|
151
151
|
['referencePath' satisfies keyof PropertyType]: (props) => (
|
|
152
|
-
<SelectInput
|
|
152
|
+
<SelectInput<PropertyType>
|
|
153
153
|
{...props}
|
|
154
154
|
options={
|
|
155
155
|
referenceSchema
|
|
@@ -160,7 +160,6 @@ export const FieldEditor = ({ view, projection, field, registry, onSave, onCance
|
|
|
160
160
|
}
|
|
161
161
|
/>
|
|
162
162
|
),
|
|
163
|
-
['options' satisfies keyof PropertyType]: (props) => <SelectOptionInput {...props} />,
|
|
164
163
|
}),
|
|
165
164
|
[t, schemas, referenceSchema],
|
|
166
165
|
);
|
|
@@ -4,11 +4,13 @@
|
|
|
4
4
|
|
|
5
5
|
import React from 'react';
|
|
6
6
|
|
|
7
|
+
import { type BaseObject } from '@dxos/echo-schema';
|
|
7
8
|
import { Input, Select } from '@dxos/react-ui';
|
|
8
9
|
|
|
9
10
|
import { type InputProps, InputHeader } from './Input';
|
|
10
11
|
|
|
11
|
-
export const TextInput = ({
|
|
12
|
+
export const TextInput = <T extends BaseObject>({
|
|
13
|
+
property,
|
|
12
14
|
type,
|
|
13
15
|
label,
|
|
14
16
|
inputOnly,
|
|
@@ -18,8 +20,8 @@ export const TextInput = ({
|
|
|
18
20
|
getValue,
|
|
19
21
|
onValueChange,
|
|
20
22
|
onBlur,
|
|
21
|
-
}: InputProps) => {
|
|
22
|
-
const { status, error } = getStatus();
|
|
23
|
+
}: InputProps<T>) => {
|
|
24
|
+
const { status, error } = getStatus?.(property);
|
|
23
25
|
|
|
24
26
|
return (
|
|
25
27
|
<Input.Root validationValence={status}>
|
|
@@ -31,8 +33,8 @@ export const TextInput = ({
|
|
|
31
33
|
<Input.TextInput
|
|
32
34
|
disabled={disabled}
|
|
33
35
|
placeholder={placeholder}
|
|
34
|
-
value={getValue() ?? ''}
|
|
35
|
-
onChange={(event) => onValueChange(type, event.target.value)}
|
|
36
|
+
value={getValue(property) ?? ''}
|
|
37
|
+
onChange={(event) => onValueChange(property, type, event.target.value)}
|
|
36
38
|
onBlur={onBlur}
|
|
37
39
|
/>
|
|
38
40
|
{inputOnly && <Input.Validation>{error}</Input.Validation>}
|
|
@@ -40,7 +42,8 @@ export const TextInput = ({
|
|
|
40
42
|
);
|
|
41
43
|
};
|
|
42
44
|
|
|
43
|
-
export const NumberInput = ({
|
|
45
|
+
export const NumberInput = <T extends BaseObject>({
|
|
46
|
+
property,
|
|
44
47
|
type,
|
|
45
48
|
label,
|
|
46
49
|
inputOnly,
|
|
@@ -50,8 +53,8 @@ export const NumberInput = ({
|
|
|
50
53
|
getValue,
|
|
51
54
|
onValueChange,
|
|
52
55
|
onBlur,
|
|
53
|
-
}: InputProps) => {
|
|
54
|
-
const { status, error } = getStatus();
|
|
56
|
+
}: InputProps<T>) => {
|
|
57
|
+
const { status, error } = getStatus?.(property);
|
|
55
58
|
|
|
56
59
|
return (
|
|
57
60
|
<Input.Root validationValence={status}>
|
|
@@ -64,8 +67,8 @@ export const NumberInput = ({
|
|
|
64
67
|
type='number'
|
|
65
68
|
disabled={disabled}
|
|
66
69
|
placeholder={placeholder}
|
|
67
|
-
value={getValue() ?? 0}
|
|
68
|
-
onChange={(event) => onValueChange(type, event.target.value)}
|
|
70
|
+
value={getValue(property) ?? 0}
|
|
71
|
+
onChange={(event) => onValueChange(property, type, event.target.value)}
|
|
69
72
|
onBlur={onBlur}
|
|
70
73
|
/>
|
|
71
74
|
{inputOnly && <Input.DescriptionAndValidation>{error}</Input.DescriptionAndValidation>}
|
|
@@ -73,11 +76,17 @@ export const NumberInput = ({
|
|
|
73
76
|
);
|
|
74
77
|
};
|
|
75
78
|
|
|
76
|
-
export const BooleanInput =
|
|
77
|
-
|
|
79
|
+
export const BooleanInput = <T extends BaseObject>({
|
|
80
|
+
property,
|
|
81
|
+
type,
|
|
82
|
+
label,
|
|
83
|
+
inputOnly,
|
|
84
|
+
getStatus,
|
|
85
|
+
getValue,
|
|
86
|
+
onValueChange,
|
|
87
|
+
}: InputProps<T>) => {
|
|
88
|
+
const { status, error } = getStatus?.(property);
|
|
78
89
|
|
|
79
|
-
// TODO(burdon): Runtime error:
|
|
80
|
-
// Warning: Unknown event handler property `onCheckedChange`. It will be ignored.
|
|
81
90
|
return (
|
|
82
91
|
<Input.Root validationValence={status}>
|
|
83
92
|
{!inputOnly && (
|
|
@@ -85,17 +94,21 @@ export const BooleanInput = ({ type, label, inputOnly, getStatus, getValue, onVa
|
|
|
85
94
|
<Input.Label>{label}</Input.Label>
|
|
86
95
|
</InputHeader>
|
|
87
96
|
)}
|
|
88
|
-
<Input.Switch
|
|
97
|
+
<Input.Switch
|
|
98
|
+
checked={getValue<boolean>(property)}
|
|
99
|
+
onCheckedChange={(value) => onValueChange(property, type, value)}
|
|
100
|
+
/>
|
|
89
101
|
{inputOnly && <Input.DescriptionAndValidation>{error}</Input.DescriptionAndValidation>}
|
|
90
102
|
</Input.Root>
|
|
91
103
|
);
|
|
92
104
|
};
|
|
93
105
|
|
|
94
|
-
export type SelectInputOptions = InputProps & {
|
|
106
|
+
export type SelectInputOptions<T extends BaseObject> = InputProps<T> & {
|
|
95
107
|
options?: Array<{ value: string | number; label?: string }>;
|
|
96
108
|
};
|
|
97
109
|
|
|
98
|
-
export const SelectInput = ({
|
|
110
|
+
export const SelectInput = <T extends BaseObject>({
|
|
111
|
+
property,
|
|
99
112
|
type,
|
|
100
113
|
label,
|
|
101
114
|
inputOnly,
|
|
@@ -105,8 +118,8 @@ export const SelectInput = ({
|
|
|
105
118
|
getStatus,
|
|
106
119
|
getValue,
|
|
107
120
|
onValueChange,
|
|
108
|
-
}: SelectInputOptions) => {
|
|
109
|
-
const { status, error } = getStatus();
|
|
121
|
+
}: SelectInputOptions<T>) => {
|
|
122
|
+
const { status, error } = getStatus?.(property);
|
|
110
123
|
|
|
111
124
|
return (
|
|
112
125
|
<Input.Root validationValence={status}>
|
|
@@ -115,7 +128,7 @@ export const SelectInput = ({
|
|
|
115
128
|
<Input.Label>{label}</Input.Label>
|
|
116
129
|
</InputHeader>
|
|
117
130
|
)}
|
|
118
|
-
<Select.Root value={getValue()} onValueChange={(value) => onValueChange(type, value)}>
|
|
131
|
+
<Select.Root value={getValue(property)} onValueChange={(value) => onValueChange(property, type, value)}>
|
|
119
132
|
{/* TODO(burdon): Placeholder not working? */}
|
|
120
133
|
<Select.TriggerButton classNames='is-full' disabled={disabled} placeholder={placeholder} />
|
|
121
134
|
<Select.Portal>
|
|
@@ -36,7 +36,7 @@ type TestType = S.Schema.Type<typeof TestSchema>;
|
|
|
36
36
|
|
|
37
37
|
type StoryProps<T extends BaseObject> = { schema: S.Schema<T> } & FormProps<T>;
|
|
38
38
|
|
|
39
|
-
const DefaultStory = <T extends BaseObject>({ schema, values: initialValues
|
|
39
|
+
const DefaultStory = <T extends BaseObject>({ schema, values: initialValues }: StoryProps<T>) => {
|
|
40
40
|
const [values, setValues] = useState(initialValues);
|
|
41
41
|
const handleSave = useCallback<NonNullable<FormProps<T>['onSave']>>((values) => {
|
|
42
42
|
setValues(values);
|
|
@@ -45,7 +45,7 @@ const DefaultStory = <T extends BaseObject>({ schema, values: initialValues, ...
|
|
|
45
45
|
return (
|
|
46
46
|
<TestLayout json={{ values, schema: schema.ast.toJSON() }}>
|
|
47
47
|
<TestPanel>
|
|
48
|
-
<Form<T> schema={schema} values={values} onSave={handleSave}
|
|
48
|
+
<Form<T> schema={schema} values={values} onSave={handleSave} />
|
|
49
49
|
</TestPanel>
|
|
50
50
|
</TestLayout>
|
|
51
51
|
);
|
|
@@ -78,6 +78,7 @@ export const Default: Story<TestType> = {
|
|
|
78
78
|
},
|
|
79
79
|
};
|
|
80
80
|
|
|
81
|
+
// TODO(burdon): Should accept partial values.
|
|
81
82
|
export const Org: Story<Testing.OrgSchemaType> = {
|
|
82
83
|
args: {
|
|
83
84
|
schema: Testing.OrgSchema,
|
|
@@ -88,17 +89,6 @@ export const Org: Story<Testing.OrgSchemaType> = {
|
|
|
88
89
|
},
|
|
89
90
|
};
|
|
90
91
|
|
|
91
|
-
export const OrgAutoSave: Story<Testing.OrgSchemaType> = {
|
|
92
|
-
args: {
|
|
93
|
-
schema: Testing.OrgSchema,
|
|
94
|
-
values: {
|
|
95
|
-
name: 'DXOS',
|
|
96
|
-
// website: 'https://dxos.org',
|
|
97
|
-
},
|
|
98
|
-
autoSave: true,
|
|
99
|
-
},
|
|
100
|
-
};
|
|
101
|
-
|
|
102
92
|
// TODO(burdon): Type issue with employer reference.
|
|
103
93
|
// TODO(burdon): Test table/form with compound values (e.g., address).
|
|
104
94
|
// export const Contact: Story<Testing.ContactSchemaType> = {
|
|
@@ -124,7 +114,7 @@ const ShapeSchema = S.Struct({
|
|
|
124
114
|
}),
|
|
125
115
|
S.Struct({
|
|
126
116
|
type: S.Literal('square').annotations({ [AST.TitleAnnotationId]: 'Type' }),
|
|
127
|
-
size: S.optional(S.Number.
|
|
117
|
+
size: S.optional(S.Number.annotations({ [AST.TitleAnnotationId]: 'Size' })),
|
|
128
118
|
}),
|
|
129
119
|
).annotations({ [AST.TitleAnnotationId]: 'Shape' }),
|
|
130
120
|
),
|
|
@@ -149,7 +139,7 @@ const DiscriminatedUnionStory = ({ values: initialValues }: DiscriminatedUnionSt
|
|
|
149
139
|
onSave={handleSave}
|
|
150
140
|
Custom={{
|
|
151
141
|
['shape.type' as const]: (props) => (
|
|
152
|
-
<SelectInput
|
|
142
|
+
<SelectInput<ShapeType>
|
|
153
143
|
{...props}
|
|
154
144
|
options={['circle', 'square'].map((value) => ({
|
|
155
145
|
value,
|
|
@@ -2,28 +2,29 @@
|
|
|
2
2
|
// Copyright 2024 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import
|
|
5
|
+
import { pipe } from 'effect';
|
|
6
|
+
import { capitalize } from 'effect/String';
|
|
7
|
+
import React, { useEffect, useMemo } from 'react';
|
|
6
8
|
|
|
7
|
-
import { type BaseObject,
|
|
8
|
-
import {
|
|
9
|
+
import { AST, type BaseObject, S, type PropertyKey, type FormatEnum } from '@dxos/echo-schema';
|
|
10
|
+
import { findNode, getDiscriminatedType, isDiscriminatedUnion, SimpleType } from '@dxos/effect';
|
|
11
|
+
import { log } from '@dxos/log';
|
|
12
|
+
import { IconButton, type ThemedClassName, useTranslation } from '@dxos/react-ui';
|
|
9
13
|
import { mx } from '@dxos/react-ui-theme';
|
|
10
|
-
import { type SchemaProperty } from '@dxos/schema';
|
|
14
|
+
import { getSchemaProperties, type SchemaProperty } from '@dxos/schema';
|
|
15
|
+
import { isNotFalsy } from '@dxos/util';
|
|
11
16
|
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
15
|
-
import { type
|
|
16
|
-
import {
|
|
17
|
+
import { SelectInput } from './Defaults';
|
|
18
|
+
import { InputHeader, type InputProps, type InputComponent } from './Input';
|
|
19
|
+
import { getInputComponent } from './factory';
|
|
20
|
+
import { type FormHandler, type FormOptions, useForm } from '../../hooks';
|
|
21
|
+
import { translationKey } from '../../translations';
|
|
17
22
|
|
|
18
|
-
|
|
23
|
+
// TODO(burdon): Rename package react-ui-form; delete react-ui-card.
|
|
19
24
|
|
|
20
|
-
|
|
21
|
-
prop: string;
|
|
22
|
-
schema: S.Schema<any>;
|
|
23
|
-
inputProps: InputProps;
|
|
24
|
-
}) => ReactElement | undefined;
|
|
25
|
+
const padding = 'px-2';
|
|
25
26
|
|
|
26
|
-
export type
|
|
27
|
+
export type PropsFilter<T extends BaseObject> = (props: SchemaProperty<T>[]) => SchemaProperty<T>[];
|
|
27
28
|
|
|
28
29
|
export type FormProps<T extends BaseObject> = ThemedClassName<
|
|
29
30
|
{
|
|
@@ -41,21 +42,29 @@ export type FormProps<T extends BaseObject> = ThemedClassName<
|
|
|
41
42
|
autoSave?: boolean;
|
|
42
43
|
testId?: string;
|
|
43
44
|
onCancel?: () => void;
|
|
44
|
-
|
|
45
|
+
|
|
46
|
+
lookupComponent?: (args: {
|
|
47
|
+
prop: string;
|
|
48
|
+
schema: S.Schema<any>;
|
|
49
|
+
inputProps: InputProps<T>;
|
|
50
|
+
}) => React.ReactElement | undefined;
|
|
45
51
|
/**
|
|
46
52
|
* Map of custom renderers for specific properties.
|
|
47
|
-
*
|
|
53
|
+
* @deprecated Use lookupComponent instead.
|
|
48
54
|
*/
|
|
49
|
-
Custom?:
|
|
55
|
+
Custom?: Partial<Record<string, InputComponent<T>>>;
|
|
50
56
|
} & Pick<FormOptions<T>, 'schema' | 'onValuesChanged' | 'onValidate' | 'onSave'>
|
|
51
57
|
>;
|
|
52
58
|
|
|
59
|
+
/**
|
|
60
|
+
* General purpose form component that displays field controls based on the given schema.
|
|
61
|
+
*/
|
|
62
|
+
// TODO(burdon): Area to show general validation errors.
|
|
53
63
|
export const Form = <T extends BaseObject>({
|
|
54
64
|
classNames,
|
|
55
65
|
schema,
|
|
56
66
|
values: initialValues,
|
|
57
67
|
path = [],
|
|
58
|
-
autoFocus,
|
|
59
68
|
readonly,
|
|
60
69
|
filter,
|
|
61
70
|
sort,
|
|
@@ -69,38 +78,338 @@ export const Form = <T extends BaseObject>({
|
|
|
69
78
|
Custom,
|
|
70
79
|
}: FormProps<T>) => {
|
|
71
80
|
const onValid = useMemo(() => (autoSave ? onSave : undefined), [autoSave, onSave]);
|
|
81
|
+
const { canSave, values, errors, handleSave, ...inputProps } = useForm<T>({
|
|
82
|
+
schema,
|
|
83
|
+
initialValues,
|
|
84
|
+
onValuesChanged,
|
|
85
|
+
onValidate,
|
|
86
|
+
onValid,
|
|
87
|
+
onSave,
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
// Filter and sort props.
|
|
91
|
+
// TODO(burdon): Move into useForm?
|
|
92
|
+
const properties = useMemo(() => {
|
|
93
|
+
const props = getSchemaProperties<T>(schema.ast, values);
|
|
94
|
+
const filtered = filter ? filter(props) : props;
|
|
95
|
+
const findIndex = (props: PropertyKey<T>[], prop: PropertyKey<T>) => {
|
|
96
|
+
const idx = props.findIndex((p) => p === prop);
|
|
97
|
+
return idx === -1 ? Infinity : idx;
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
return sort ? filtered.sort(({ name: a }, { name: b }) => findIndex(sort, a) - findIndex(sort, b)) : filtered;
|
|
101
|
+
}, [schema, filter, values]);
|
|
72
102
|
|
|
73
|
-
// TODO(burdon):
|
|
103
|
+
// TODO(burdon): Highlight in UX.
|
|
74
104
|
useEffect(() => {
|
|
75
|
-
if (
|
|
76
|
-
|
|
77
|
-
if (input) {
|
|
78
|
-
input.focus();
|
|
79
|
-
}
|
|
105
|
+
if (errors && Object.keys(errors).length) {
|
|
106
|
+
log.warn('validation', { errors });
|
|
80
107
|
}
|
|
81
|
-
}, [
|
|
108
|
+
}, [errors]);
|
|
82
109
|
|
|
83
110
|
return (
|
|
84
|
-
<
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
111
|
+
<div role='form' className={mx('flex flex-col w-full gap-2 py-2', classNames)} data-testid={testId}>
|
|
112
|
+
{properties
|
|
113
|
+
.map((property) => {
|
|
114
|
+
const { ast, name, type, format, title, description, examples, options, array } = property;
|
|
115
|
+
const key = [...path, name];
|
|
116
|
+
const label = title ?? pipe(name, capitalize);
|
|
117
|
+
const placeholder = examples?.length ? `Example: "${examples[0]}"` : description;
|
|
118
|
+
|
|
119
|
+
const FoundComponent = lookupComponent?.({
|
|
120
|
+
prop: name,
|
|
121
|
+
schema: S.make(ast),
|
|
122
|
+
inputProps: {
|
|
123
|
+
property: name,
|
|
124
|
+
type,
|
|
125
|
+
format,
|
|
126
|
+
label,
|
|
127
|
+
disabled: readonly,
|
|
128
|
+
placeholder,
|
|
129
|
+
...inputProps,
|
|
130
|
+
},
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
if (FoundComponent) {
|
|
134
|
+
return (
|
|
135
|
+
<div key={name} role='none' className={padding}>
|
|
136
|
+
{FoundComponent}
|
|
137
|
+
</div>
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Get generic input.
|
|
142
|
+
// TODO(ZaymonFC): We might need to switch this to using globs since we're now indexing into arrays.
|
|
143
|
+
let InputComponent = Custom?.[key.join('.')];
|
|
144
|
+
if (!InputComponent) {
|
|
145
|
+
// Select.
|
|
146
|
+
if (options) {
|
|
147
|
+
return (
|
|
148
|
+
<div key={name} role='none' className={padding}>
|
|
149
|
+
<SelectInput
|
|
150
|
+
type={type}
|
|
151
|
+
format={format}
|
|
152
|
+
property={name}
|
|
153
|
+
disabled={readonly}
|
|
154
|
+
label={label}
|
|
155
|
+
options={options.map((option) => ({ value: option, label: String(option) }))}
|
|
156
|
+
placeholder={placeholder}
|
|
157
|
+
{...inputProps}
|
|
158
|
+
/>
|
|
159
|
+
</div>
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
InputComponent = getInputComponent<T>(type, format);
|
|
164
|
+
|
|
165
|
+
// TODO(ZaymonFC): Array handling is inelegant. We need to push this complexity down into SDK/Schema.
|
|
166
|
+
if (array) {
|
|
167
|
+
return (
|
|
168
|
+
<ArrayField
|
|
169
|
+
key={name}
|
|
170
|
+
name={name}
|
|
171
|
+
type={type}
|
|
172
|
+
array={true}
|
|
173
|
+
label={label}
|
|
174
|
+
ast={ast}
|
|
175
|
+
values={values}
|
|
176
|
+
format={format}
|
|
177
|
+
readonly={readonly}
|
|
178
|
+
placeholder={placeholder}
|
|
179
|
+
inputProps={inputProps}
|
|
180
|
+
Custom={Custom}
|
|
181
|
+
/>
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if (!InputComponent) {
|
|
187
|
+
// Recursively render form.
|
|
188
|
+
if (type === 'object') {
|
|
189
|
+
const baseNode = findNode(ast, isDiscriminatedUnion);
|
|
190
|
+
const typeLiteral = baseNode
|
|
191
|
+
? getDiscriminatedType(baseNode, values[name] as any)
|
|
192
|
+
: findNode(ast, AST.isTypeLiteral);
|
|
193
|
+
|
|
194
|
+
if (typeLiteral) {
|
|
195
|
+
const schema = S.make(typeLiteral);
|
|
196
|
+
return (
|
|
197
|
+
<div key={name} role='none'>
|
|
198
|
+
<div className={padding}>{label}</div>
|
|
199
|
+
<Form<any>
|
|
200
|
+
schema={schema}
|
|
201
|
+
path={key}
|
|
202
|
+
values={values[name] ?? {}}
|
|
203
|
+
onValuesChanged={(childValues) => {
|
|
204
|
+
inputProps.onValueChange(name, 'object', childValues);
|
|
205
|
+
}}
|
|
206
|
+
Custom={Custom as any}
|
|
207
|
+
/>
|
|
208
|
+
</div>
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
log('no renderer for property', { name, type });
|
|
214
|
+
return null;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
return (
|
|
218
|
+
<div key={name} role='none' className={padding}>
|
|
219
|
+
<InputComponent
|
|
220
|
+
type={type}
|
|
221
|
+
format={format}
|
|
222
|
+
property={name}
|
|
223
|
+
disabled={readonly}
|
|
224
|
+
label={label}
|
|
225
|
+
placeholder={placeholder}
|
|
226
|
+
{...inputProps}
|
|
227
|
+
/>
|
|
228
|
+
</div>
|
|
229
|
+
);
|
|
230
|
+
})
|
|
231
|
+
.filter(isNotFalsy)}
|
|
232
|
+
{(onCancel || onSave) && !autoSave && (
|
|
233
|
+
<FormActions onCancel={onCancel} onSubmit={handleSave} canSubmit={canSave} readonly={readonly} />
|
|
234
|
+
)}
|
|
235
|
+
</div>
|
|
236
|
+
);
|
|
237
|
+
};
|
|
238
|
+
|
|
239
|
+
type ArrayFieldProps<T extends BaseObject> = {
|
|
240
|
+
name: PropertyKey<T>;
|
|
241
|
+
type: SimpleType;
|
|
242
|
+
array: true;
|
|
243
|
+
label: string;
|
|
244
|
+
ast: AST.AST;
|
|
245
|
+
values: Record<string, any>;
|
|
246
|
+
format?: FormatEnum;
|
|
247
|
+
readonly?: boolean;
|
|
248
|
+
placeholder?: string;
|
|
249
|
+
inputProps: Pick<FormHandler<T>, 'getStatus' | 'getValue' | 'onValueChange' | 'onBlur'>;
|
|
250
|
+
Custom?: Partial<Record<string, InputComponent<T>>>;
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
const ArrayField = <T extends BaseObject>({
|
|
254
|
+
name,
|
|
255
|
+
type,
|
|
256
|
+
label,
|
|
257
|
+
ast,
|
|
258
|
+
values,
|
|
259
|
+
format,
|
|
260
|
+
readonly,
|
|
261
|
+
placeholder,
|
|
262
|
+
inputProps,
|
|
263
|
+
Custom,
|
|
264
|
+
}: ArrayFieldProps<T>) => {
|
|
265
|
+
const { t } = useTranslation(translationKey);
|
|
266
|
+
const arrayValues = (values[name] ?? []) as any[];
|
|
267
|
+
const key = [name];
|
|
268
|
+
|
|
269
|
+
// TODO(ZaymonFC): Should this unwrapping happen at a lower level?
|
|
270
|
+
const tupleType = findNode(ast, AST.isTupleType);
|
|
271
|
+
const elementType = (tupleType as AST.TupleType | undefined)?.rest[0]?.type;
|
|
272
|
+
|
|
273
|
+
if (type === 'object' && elementType) {
|
|
274
|
+
const baseNode = findNode(elementType, isDiscriminatedUnion);
|
|
275
|
+
const typeLiteral = baseNode
|
|
276
|
+
? getDiscriminatedType(baseNode, values[name] as any)
|
|
277
|
+
: findNode(elementType, AST.isTypeLiteral);
|
|
278
|
+
|
|
279
|
+
if (typeLiteral) {
|
|
280
|
+
const schema = S.make(typeLiteral);
|
|
281
|
+
|
|
282
|
+
return (
|
|
283
|
+
<div key={name} role='none' className={mx(padding)}>
|
|
284
|
+
<InputHeader>{label}</InputHeader>
|
|
285
|
+
<div role='none' className='flex flex-col gap-1'>
|
|
286
|
+
{arrayValues.map((value, index) => {
|
|
287
|
+
return (
|
|
288
|
+
<div key={index} role='none' className='flex items-center gap-1'>
|
|
289
|
+
<div role='none' className='flex-1'>
|
|
290
|
+
<Form<any>
|
|
291
|
+
schema={schema}
|
|
292
|
+
path={[...key, `[${index}]`]}
|
|
293
|
+
values={value}
|
|
294
|
+
onValuesChanged={(childValues) => {
|
|
295
|
+
const newValues = [...arrayValues];
|
|
296
|
+
newValues[index] = childValues;
|
|
297
|
+
inputProps.onValueChange(name, 'object', newValues);
|
|
298
|
+
}}
|
|
299
|
+
Custom={Custom as any}
|
|
300
|
+
/>
|
|
301
|
+
</div>
|
|
302
|
+
<IconButton
|
|
303
|
+
icon='ph--trash--regular'
|
|
304
|
+
iconOnly
|
|
305
|
+
label={t('button remove')}
|
|
306
|
+
onClick={() => {
|
|
307
|
+
const newValues = arrayValues.filter((_, i) => i !== index);
|
|
308
|
+
inputProps.onValueChange(name, type, newValues);
|
|
309
|
+
}}
|
|
310
|
+
/>
|
|
311
|
+
</div>
|
|
312
|
+
);
|
|
313
|
+
})}
|
|
314
|
+
</div>
|
|
315
|
+
<div role='none' className='flex justify-between items-center plb-1'>
|
|
316
|
+
<IconButton
|
|
317
|
+
icon='ph--plus--regular'
|
|
318
|
+
iconOnly
|
|
319
|
+
label={t('button add')}
|
|
320
|
+
onClick={() => {
|
|
321
|
+
const newValues = [...arrayValues, {}];
|
|
322
|
+
inputProps.onValueChange(name, type, newValues);
|
|
323
|
+
}}
|
|
324
|
+
/>
|
|
325
|
+
</div>
|
|
326
|
+
</div>
|
|
327
|
+
);
|
|
328
|
+
}
|
|
329
|
+
return (
|
|
330
|
+
<div key={name} role='none'>
|
|
331
|
+
Nested form not supported in arrays, yet.
|
|
332
|
+
</div>
|
|
333
|
+
);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
const InputComponent = getInputComponent<T>(type, format);
|
|
337
|
+
if (!InputComponent) {
|
|
338
|
+
return null;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
return (
|
|
342
|
+
<div role='none' key='meta-input' className={mx(padding)}>
|
|
343
|
+
<InputHeader>{label}</InputHeader>
|
|
344
|
+
<div role='none' className='flex flex-col gap-1'>
|
|
345
|
+
{arrayValues.map((_, index) => (
|
|
346
|
+
<div key={index} role='none' className='flex items-start gap-1'>
|
|
347
|
+
<div role='none' className='flex-1'>
|
|
348
|
+
<InputComponent
|
|
349
|
+
type={type}
|
|
350
|
+
format={format}
|
|
351
|
+
label={label}
|
|
352
|
+
inputOnly
|
|
353
|
+
property={`${name}[${index}]` as PropertyKey<T>}
|
|
354
|
+
disabled={readonly}
|
|
355
|
+
placeholder={placeholder}
|
|
356
|
+
{...inputProps}
|
|
357
|
+
/>
|
|
358
|
+
</div>
|
|
359
|
+
<IconButton
|
|
360
|
+
icon='ph--trash--regular'
|
|
361
|
+
iconOnly
|
|
362
|
+
label={t('button remove')}
|
|
363
|
+
onClick={() => {
|
|
364
|
+
const newValues = arrayValues.filter((_, i) => i !== index);
|
|
365
|
+
inputProps.onValueChange(name, type, newValues);
|
|
366
|
+
}}
|
|
367
|
+
/>
|
|
368
|
+
</div>
|
|
369
|
+
))}
|
|
370
|
+
</div>
|
|
371
|
+
<div role='none' className='flex justify-between items-center plb-1'>
|
|
372
|
+
<IconButton
|
|
373
|
+
icon='ph--plus--regular'
|
|
374
|
+
iconOnly
|
|
375
|
+
label={t('button add')}
|
|
376
|
+
onClick={() => {
|
|
377
|
+
const newValues = [...arrayValues, SimpleType.getDefaultValue(type)];
|
|
378
|
+
inputProps.onValueChange(name, type, newValues);
|
|
379
|
+
}}
|
|
101
380
|
/>
|
|
102
|
-
{(onCancel || onSave) && !autoSave && <FormActions onCancel={onCancel} readonly={readonly} />}
|
|
103
381
|
</div>
|
|
104
|
-
</
|
|
382
|
+
</div>
|
|
383
|
+
);
|
|
384
|
+
};
|
|
385
|
+
|
|
386
|
+
type FormActionsProps = {
|
|
387
|
+
readonly?: boolean;
|
|
388
|
+
canSubmit: boolean;
|
|
389
|
+
onCancel?: () => void;
|
|
390
|
+
onSubmit: () => void;
|
|
391
|
+
};
|
|
392
|
+
|
|
393
|
+
export const FormActions = ({ onCancel, onSubmit, canSubmit, readonly }: FormActionsProps) => {
|
|
394
|
+
const { t } = useTranslation(translationKey);
|
|
395
|
+
|
|
396
|
+
return (
|
|
397
|
+
<div role='none' className='flex justify-center'>
|
|
398
|
+
<div role='none' className={mx(onCancel && !readonly && 'grid grid-cols-2 gap-2')}>
|
|
399
|
+
{onCancel && !readonly && (
|
|
400
|
+
<IconButton data-testid='cancel-button' icon='ph--x--regular' label={t('button cancel')} onClick={onCancel} />
|
|
401
|
+
)}
|
|
402
|
+
{onSubmit && (
|
|
403
|
+
<IconButton
|
|
404
|
+
type='submit'
|
|
405
|
+
data-testid='save-button'
|
|
406
|
+
disabled={!canSubmit}
|
|
407
|
+
icon='ph--check--regular'
|
|
408
|
+
label={t('button save')}
|
|
409
|
+
onClick={onSubmit}
|
|
410
|
+
/>
|
|
411
|
+
)}
|
|
412
|
+
</div>
|
|
413
|
+
</div>
|
|
105
414
|
);
|
|
106
415
|
};
|