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