@dxos/react-ui-form 0.8.4-main.84f28bd → 0.8.4-main.9735255
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 +2218 -2062
- package/dist/lib/browser/index.mjs.map +4 -4
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/node-esm/index.mjs +2218 -2062
- package/dist/lib/node-esm/index.mjs.map +4 -4
- package/dist/lib/node-esm/meta.json +1 -1
- package/dist/types/src/components/ControlSection/ControlSection.d.ts +4 -4
- package/dist/types/src/components/ControlSection/ControlSection.d.ts.map +1 -1
- package/dist/types/src/components/ControlSection/ControlSection.stories.d.ts +84 -5
- package/dist/types/src/components/ControlSection/ControlSection.stories.d.ts.map +1 -1
- package/dist/types/src/components/FieldEditor/FieldEditor.d.ts +8 -8
- package/dist/types/src/components/FieldEditor/FieldEditor.d.ts.map +1 -1
- package/dist/types/src/components/FieldEditor/FieldEditor.stories.d.ts +84 -7
- package/dist/types/src/components/FieldEditor/FieldEditor.stories.d.ts.map +1 -1
- package/dist/types/src/components/Form/Form-old.stories.d.ts +131 -0
- package/dist/types/src/components/Form/Form-old.stories.d.ts.map +1 -0
- package/dist/types/src/components/Form/Form.d.ts +81 -25
- package/dist/types/src/components/Form/Form.d.ts.map +1 -1
- package/dist/types/src/components/Form/Form.stories.d.ts +127 -55
- package/dist/types/src/components/Form/Form.stories.d.ts.map +1 -1
- package/dist/types/src/components/Form/FormField.d.ts +37 -0
- package/dist/types/src/components/Form/FormField.d.ts.map +1 -0
- package/dist/types/src/components/Form/FormFieldComponent.d.ts +78 -0
- package/dist/types/src/components/Form/FormFieldComponent.d.ts.map +1 -0
- package/dist/types/src/components/Form/FormFieldComponent.stories.d.ts +9 -0
- package/dist/types/src/components/Form/FormFieldComponent.stories.d.ts.map +1 -0
- package/dist/types/src/components/Form/FormFieldSet.d.ts +19 -0
- package/dist/types/src/components/Form/FormFieldSet.d.ts.map +1 -0
- package/dist/types/src/components/Form/fields/ArrayField.d.ts +18 -0
- package/dist/types/src/components/Form/fields/ArrayField.d.ts.map +1 -0
- package/dist/types/src/components/Form/fields/BooleanField.d.ts +4 -0
- package/dist/types/src/components/Form/fields/BooleanField.d.ts.map +1 -0
- package/dist/types/src/components/Form/fields/GeoPointField.d.ts +5 -0
- package/dist/types/src/components/Form/fields/GeoPointField.d.ts.map +1 -0
- package/dist/types/src/components/Form/fields/MarkdownField.d.ts +4 -0
- package/dist/types/src/components/Form/fields/MarkdownField.d.ts.map +1 -0
- package/dist/types/src/components/Form/fields/NumberField.d.ts +4 -0
- package/dist/types/src/components/Form/fields/NumberField.d.ts.map +1 -0
- package/dist/types/src/components/Form/fields/RefField.d.ts +14 -0
- package/dist/types/src/components/Form/fields/RefField.d.ts.map +1 -0
- package/dist/types/src/components/Form/fields/SelectField.d.ts +10 -0
- package/dist/types/src/components/Form/fields/SelectField.d.ts.map +1 -0
- package/dist/types/src/components/Form/fields/SelectOptionField.d.ts +5 -0
- package/dist/types/src/components/Form/fields/SelectOptionField.d.ts.map +1 -0
- package/dist/types/src/components/Form/fields/SelectOptionField.stories.d.ts +82 -0
- package/dist/types/src/components/Form/fields/SelectOptionField.stories.d.ts.map +1 -0
- package/dist/types/src/components/Form/fields/TextAreaField.d.ts +4 -0
- package/dist/types/src/components/Form/fields/TextAreaField.d.ts.map +1 -0
- package/dist/types/src/components/Form/fields/TextField.d.ts +4 -0
- package/dist/types/src/components/Form/fields/TextField.d.ts.map +1 -0
- package/dist/types/src/components/Form/fields/TupleField.d.ts +6 -0
- package/dist/types/src/components/Form/fields/TupleField.d.ts.map +1 -0
- package/dist/types/src/components/Form/fields/index.d.ts +12 -0
- package/dist/types/src/components/Form/fields/index.d.ts.map +1 -0
- package/dist/types/src/components/Form/index.d.ts +2 -4
- package/dist/types/src/components/Form/index.d.ts.map +1 -1
- package/dist/types/src/components/ObjectPicker/ObjectPicker.d.ts +46 -0
- package/dist/types/src/components/ObjectPicker/ObjectPicker.d.ts.map +1 -0
- package/dist/types/src/components/ObjectPicker/ObjectPicker.stories.d.ts +85 -0
- package/dist/types/src/components/ObjectPicker/ObjectPicker.stories.d.ts.map +1 -0
- package/dist/types/src/components/ObjectPicker/index.d.ts +2 -0
- package/dist/types/src/components/ObjectPicker/index.d.ts.map +1 -0
- package/dist/types/src/components/ViewEditor/ViewEditor.d.ts +21 -9
- package/dist/types/src/components/ViewEditor/ViewEditor.d.ts.map +1 -1
- package/dist/types/src/components/ViewEditor/ViewEditor.stories.d.ts +84 -9
- package/dist/types/src/components/ViewEditor/ViewEditor.stories.d.ts.map +1 -1
- package/dist/types/src/components/index.d.ts +1 -1
- package/dist/types/src/components/index.d.ts.map +1 -1
- package/dist/types/src/components/testing.d.ts +4 -3
- package/dist/types/src/components/testing.d.ts.map +1 -1
- package/dist/types/src/hooks/index.d.ts +2 -3
- package/dist/types/src/hooks/index.d.ts.map +1 -1
- package/dist/types/src/hooks/useFormHandler.d.ts +82 -0
- package/dist/types/src/hooks/useFormHandler.d.ts.map +1 -0
- package/dist/types/src/hooks/useKeyHandler.d.ts +6 -0
- package/dist/types/src/hooks/useKeyHandler.d.ts.map +1 -0
- package/dist/types/src/translations.d.ts +19 -10
- package/dist/types/src/translations.d.ts.map +1 -1
- package/dist/types/src/util/echo.d.ts +6 -0
- package/dist/types/src/util/echo.d.ts.map +1 -0
- package/dist/types/src/util/field.d.ts +3 -3
- package/dist/types/src/util/field.d.ts.map +1 -1
- package/dist/types/src/util/formatting.d.ts +2 -2
- package/dist/types/src/util/formatting.d.ts.map +1 -1
- package/dist/types/src/util/index.d.ts +2 -1
- package/dist/types/src/util/index.d.ts.map +1 -1
- package/dist/types/src/util/properties.d.ts +7 -0
- package/dist/types/src/util/properties.d.ts.map +1 -0
- package/dist/types/src/util/refs.d.ts +2 -3
- package/dist/types/src/util/refs.d.ts.map +1 -1
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/package.json +51 -40
- package/src/components/ControlSection/ControlSection.stories.tsx +11 -14
- package/src/components/ControlSection/ControlSection.tsx +30 -16
- package/src/components/FieldEditor/FieldEditor.stories.tsx +53 -30
- package/src/components/FieldEditor/FieldEditor.test.tsx +14 -13
- package/src/components/FieldEditor/FieldEditor.tsx +87 -92
- package/src/components/Form/Form-old.stories.tsx +314 -0
- package/src/components/Form/Form.stories.tsx +155 -219
- package/src/components/Form/Form.tsx +320 -96
- package/src/components/Form/FormField.tsx +286 -0
- package/src/components/Form/FormFieldComponent.stories.tsx +65 -0
- package/src/components/Form/FormFieldComponent.tsx +174 -0
- package/src/components/Form/FormFieldSet.tsx +125 -0
- package/src/components/Form/README.md +3 -0
- package/src/components/Form/fields/ArrayField.tsx +151 -0
- package/src/components/Form/fields/BooleanField.tsx +22 -0
- package/src/components/Form/fields/GeoPointField.tsx +120 -0
- package/src/components/Form/fields/MarkdownField.tsx +78 -0
- package/src/components/Form/fields/NumberField.tsx +39 -0
- package/src/components/Form/fields/RefField.tsx +181 -0
- package/src/components/Form/fields/SelectField.tsx +65 -0
- package/src/components/Form/{custom/SelectOptionsInput.stories.tsx → fields/SelectOptionField.stories.tsx} +25 -22
- package/src/components/Form/{custom/SelectOptionsInput.tsx → fields/SelectOptionField.tsx} +47 -42
- package/src/components/Form/fields/TextAreaField.tsx +38 -0
- package/src/components/Form/fields/TextField.tsx +38 -0
- package/src/components/Form/{custom/TupleInput.tsx → fields/TupleField.tsx} +16 -9
- package/src/components/Form/fields/index.ts +15 -0
- package/src/components/Form/index.ts +3 -4
- package/src/components/ObjectPicker/ObjectPicker.stories.tsx +214 -0
- package/src/components/ObjectPicker/ObjectPicker.tsx +175 -0
- package/src/components/ObjectPicker/index.ts +5 -0
- package/src/components/ViewEditor/ViewEditor.stories.tsx +141 -59
- package/src/components/ViewEditor/ViewEditor.test.tsx +25 -21
- package/src/components/ViewEditor/ViewEditor.tsx +323 -217
- package/src/components/index.ts +1 -2
- package/src/components/testing.tsx +19 -11
- package/src/hooks/index.ts +2 -3
- package/src/hooks/useFormHandler.ts +362 -0
- package/src/hooks/useKeyHandler.tsx +37 -0
- package/src/translations.ts +21 -12
- package/src/util/echo.ts +25 -0
- package/src/util/field.ts +33 -43
- package/src/util/formatting.ts +22 -22
- package/src/util/index.ts +2 -1
- package/src/util/properties.ts +18 -0
- package/src/util/refs.ts +12 -18
- 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/Defaults.d.ts +0 -14
- package/dist/types/src/components/Form/Defaults.d.ts.map +0 -1
- package/dist/types/src/components/Form/FormActions.d.ts +0 -9
- package/dist/types/src/components/Form/FormActions.d.ts.map +0 -1
- package/dist/types/src/components/Form/FormContent.d.ts +0 -57
- package/dist/types/src/components/Form/FormContent.d.ts.map +0 -1
- package/dist/types/src/components/Form/FormContext.d.ts +0 -23
- package/dist/types/src/components/Form/FormContext.d.ts.map +0 -1
- package/dist/types/src/components/Form/Input.d.ts +0 -26
- package/dist/types/src/components/Form/Input.d.ts.map +0 -1
- package/dist/types/src/components/Form/RefField.d.ts +0 -12
- package/dist/types/src/components/Form/RefField.d.ts.map +0 -1
- package/dist/types/src/components/Form/custom/GeoPointInput.d.ts +0 -4
- package/dist/types/src/components/Form/custom/GeoPointInput.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/dist/types/src/components/Form/custom/TupleInput.d.ts +0 -6
- package/dist/types/src/components/Form/custom/TupleInput.d.ts.map +0 -1
- package/dist/types/src/components/Form/custom/index.d.ts +0 -4
- package/dist/types/src/components/Form/custom/index.d.ts.map +0 -1
- package/dist/types/src/components/Form/factory.d.ts +0 -8
- package/dist/types/src/components/Form/factory.d.ts.map +0 -1
- package/dist/types/src/components/deprecated/Form.d.ts +0 -15
- package/dist/types/src/components/deprecated/Form.d.ts.map +0 -1
- package/dist/types/src/components/deprecated/Form.stories.d.ts +0 -13
- package/dist/types/src/components/deprecated/Form.stories.d.ts.map +0 -1
- package/dist/types/src/components/deprecated/FormInput.d.ts +0 -16
- package/dist/types/src/components/deprecated/FormInput.d.ts.map +0 -1
- package/dist/types/src/components/deprecated/index.d.ts +0 -2
- package/dist/types/src/components/deprecated/index.d.ts.map +0 -1
- package/dist/types/src/hooks/useForm.d.ts +0 -67
- package/dist/types/src/hooks/useForm.d.ts.map +0 -1
- package/dist/types/src/hooks/useQueryRefOptions.d.ts +0 -23
- package/dist/types/src/hooks/useQueryRefOptions.d.ts.map +0 -1
- package/dist/types/src/hooks/useRefQueryLookupHandler.d.ts +0 -8
- package/dist/types/src/hooks/useRefQueryLookupHandler.d.ts.map +0 -1
- package/dist/types/src/util/schema.d.ts +0 -3
- package/dist/types/src/util/schema.d.ts.map +0 -1
- package/src/components/Form/ArrayField.tsx +0 -124
- package/src/components/Form/Defaults.tsx +0 -203
- package/src/components/Form/FormActions.tsx +0 -53
- package/src/components/Form/FormContent.tsx +0 -240
- package/src/components/Form/FormContext.tsx +0 -110
- package/src/components/Form/Input.tsx +0 -51
- package/src/components/Form/RefField.tsx +0 -213
- package/src/components/Form/custom/GeoPointInput.tsx +0 -72
- package/src/components/Form/custom/index.ts +0 -7
- package/src/components/Form/factory.tsx +0 -31
- package/src/components/deprecated/Form.stories.tsx +0 -62
- package/src/components/deprecated/Form.tsx +0 -119
- package/src/components/deprecated/FormInput.tsx +0 -71
- package/src/components/deprecated/index.ts +0 -5
- package/src/hooks/useForm.ts +0 -291
- package/src/hooks/useQueryRefOptions.ts +0 -53
- package/src/hooks/useRefQueryLookupHandler.ts +0 -39
- package/src/util/schema.ts +0 -14
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2025 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import * as Match from 'effect/Match';
|
|
6
|
+
import * as Option from 'effect/Option';
|
|
7
|
+
import * as Schema from 'effect/Schema';
|
|
8
|
+
import * as SchemaAST from 'effect/SchemaAST';
|
|
9
|
+
import * as String from 'effect/String';
|
|
10
|
+
import React, { useMemo } from 'react';
|
|
11
|
+
|
|
12
|
+
import { Format } from '@dxos/echo';
|
|
13
|
+
import {
|
|
14
|
+
createJsonPath,
|
|
15
|
+
findNode,
|
|
16
|
+
getAnnotation,
|
|
17
|
+
getDiscriminatedType,
|
|
18
|
+
isArrayType,
|
|
19
|
+
isDiscriminatedUnion,
|
|
20
|
+
isLiteralUnion,
|
|
21
|
+
isNestedType,
|
|
22
|
+
} from '@dxos/effect';
|
|
23
|
+
import { useTranslation } from '@dxos/react-ui';
|
|
24
|
+
import { type ProjectionModel } from '@dxos/schema';
|
|
25
|
+
|
|
26
|
+
import { translationKey } from '../../translations';
|
|
27
|
+
import { getRefProps } from '../../util';
|
|
28
|
+
|
|
29
|
+
import {
|
|
30
|
+
ArrayField,
|
|
31
|
+
BooleanField,
|
|
32
|
+
GeoPointField,
|
|
33
|
+
MarkdownField,
|
|
34
|
+
NumberField,
|
|
35
|
+
RefField,
|
|
36
|
+
type RefFieldProps,
|
|
37
|
+
SelectField,
|
|
38
|
+
TextAreaField,
|
|
39
|
+
TextField,
|
|
40
|
+
} from './fields';
|
|
41
|
+
import { useFormFieldState } from './Form';
|
|
42
|
+
import {
|
|
43
|
+
type FormFieldComponent,
|
|
44
|
+
type FormFieldComponentProps,
|
|
45
|
+
type FormFieldMap,
|
|
46
|
+
type FormFieldProvider,
|
|
47
|
+
} from './FormFieldComponent';
|
|
48
|
+
import { FormFieldSet } from './FormFieldSet';
|
|
49
|
+
|
|
50
|
+
export type FormFieldProps = {
|
|
51
|
+
/**
|
|
52
|
+
* AST of the property to render.
|
|
53
|
+
*/
|
|
54
|
+
type: SchemaAST.AST;
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Name of the property.
|
|
58
|
+
*/
|
|
59
|
+
name: string;
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Path to the current object from the root. Used with nested forms.
|
|
63
|
+
*/
|
|
64
|
+
path?: (string | number)[];
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Optional projection for projection-based field management.
|
|
68
|
+
*/
|
|
69
|
+
projection?: ProjectionModel;
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Map of custom renderers for specific properties.
|
|
73
|
+
* Prefer fieldProvider for plugin specific input surfaces.
|
|
74
|
+
*/
|
|
75
|
+
fieldMap?: FormFieldMap;
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Function to lookup custom renderers for specific properties.
|
|
79
|
+
*/
|
|
80
|
+
fieldProvider?: FormFieldProvider;
|
|
81
|
+
} & Pick<FormFieldComponentProps, 'autoFocus' | 'readonly' | 'layout'> &
|
|
82
|
+
Pick<
|
|
83
|
+
RefFieldProps,
|
|
84
|
+
| 'createOptionLabel'
|
|
85
|
+
| 'createOptionIcon'
|
|
86
|
+
| 'createInitialValuePath'
|
|
87
|
+
| 'db'
|
|
88
|
+
| 'schemaHook'
|
|
89
|
+
| 'getOptions'
|
|
90
|
+
| 'onCreate'
|
|
91
|
+
>;
|
|
92
|
+
|
|
93
|
+
export const FormField = (props: FormFieldProps) => {
|
|
94
|
+
const {
|
|
95
|
+
type,
|
|
96
|
+
name,
|
|
97
|
+
path,
|
|
98
|
+
projection,
|
|
99
|
+
fieldMap,
|
|
100
|
+
fieldProvider,
|
|
101
|
+
readonly,
|
|
102
|
+
layout,
|
|
103
|
+
|
|
104
|
+
// RefFieldProps
|
|
105
|
+
createOptionLabel,
|
|
106
|
+
createOptionIcon,
|
|
107
|
+
createInitialValuePath,
|
|
108
|
+
db,
|
|
109
|
+
schemaHook,
|
|
110
|
+
getOptions: getRefOptions,
|
|
111
|
+
onCreate,
|
|
112
|
+
} = props;
|
|
113
|
+
const { t } = useTranslation(translationKey);
|
|
114
|
+
const title = getAnnotation<string>(SchemaAST.TitleAnnotationId)(type);
|
|
115
|
+
const description = getAnnotation<string>(SchemaAST.DescriptionAnnotationId)(type);
|
|
116
|
+
const examples = getAnnotation<string[]>(SchemaAST.ExamplesAnnotationId)(type);
|
|
117
|
+
|
|
118
|
+
const label = useMemo(() => title ?? String.capitalize(name), [title, name]);
|
|
119
|
+
const placeholder = useMemo(
|
|
120
|
+
() => (examples?.length ? `${t('example placeholder')}: ${examples[0]}` : (description ?? label)),
|
|
121
|
+
[examples, description, label],
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
const fieldState = useFormFieldState(FormField.displayName, path);
|
|
125
|
+
const fieldProps: FormFieldComponentProps = {
|
|
126
|
+
type,
|
|
127
|
+
format: Format.FormatAnnotation.getFromAst(type).pipe((annotation) => Option.getOrUndefined(annotation)),
|
|
128
|
+
readonly,
|
|
129
|
+
label,
|
|
130
|
+
placeholder,
|
|
131
|
+
layout,
|
|
132
|
+
...fieldState,
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
//
|
|
136
|
+
// Custom field.
|
|
137
|
+
//
|
|
138
|
+
|
|
139
|
+
const jsonPath = createJsonPath(path ?? []);
|
|
140
|
+
const CustomField = fieldMap?.[jsonPath];
|
|
141
|
+
if (CustomField) {
|
|
142
|
+
return <CustomField {...fieldProps} />;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// TODO(burdon): Expensive to create schema each time; pass AST?
|
|
146
|
+
const component = fieldProvider?.({ schema: Schema.make(type), prop: name, fieldProps });
|
|
147
|
+
if (component) {
|
|
148
|
+
return component;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
//
|
|
152
|
+
// Array field.
|
|
153
|
+
//
|
|
154
|
+
|
|
155
|
+
if (isArrayType(type)) {
|
|
156
|
+
return <ArrayField fieldProps={fieldState} label={label} {...props} />;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
//
|
|
160
|
+
// Regular field.
|
|
161
|
+
//
|
|
162
|
+
|
|
163
|
+
const Field = getFormField(fieldProps);
|
|
164
|
+
if (Field) {
|
|
165
|
+
return <Field {...fieldProps} />;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
//
|
|
169
|
+
// Select field.
|
|
170
|
+
//
|
|
171
|
+
|
|
172
|
+
const options = getOptions(type);
|
|
173
|
+
if (options) {
|
|
174
|
+
return (
|
|
175
|
+
<SelectField
|
|
176
|
+
{...fieldProps}
|
|
177
|
+
options={options.map((option) => ({
|
|
178
|
+
value: option,
|
|
179
|
+
label: option.toString(),
|
|
180
|
+
}))}
|
|
181
|
+
/>
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
//
|
|
186
|
+
// Ref field.
|
|
187
|
+
//
|
|
188
|
+
|
|
189
|
+
const refProps = getRefProps(type);
|
|
190
|
+
if (refProps) {
|
|
191
|
+
return (
|
|
192
|
+
<RefField
|
|
193
|
+
{...fieldProps}
|
|
194
|
+
{...refProps}
|
|
195
|
+
createOptionLabel={createOptionLabel}
|
|
196
|
+
createOptionIcon={createOptionIcon}
|
|
197
|
+
createInitialValuePath={createInitialValuePath}
|
|
198
|
+
db={db}
|
|
199
|
+
schemaHook={schemaHook}
|
|
200
|
+
getOptions={getRefOptions}
|
|
201
|
+
onCreate={onCreate}
|
|
202
|
+
/>
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
//
|
|
207
|
+
// Nested Object field.
|
|
208
|
+
//
|
|
209
|
+
|
|
210
|
+
if (isNestedType(type)) {
|
|
211
|
+
const baseNode = findNode(type, isDiscriminatedUnion);
|
|
212
|
+
const typeLiteral = baseNode
|
|
213
|
+
? getDiscriminatedType(baseNode, fieldState.getValue() as any)
|
|
214
|
+
: findNode(type, SchemaAST.isTypeLiteral);
|
|
215
|
+
|
|
216
|
+
if (typeLiteral) {
|
|
217
|
+
const schema = Schema.make(typeLiteral);
|
|
218
|
+
return (
|
|
219
|
+
<FormFieldSet
|
|
220
|
+
schema={schema}
|
|
221
|
+
path={path}
|
|
222
|
+
readonly={readonly}
|
|
223
|
+
layout={layout}
|
|
224
|
+
label={label}
|
|
225
|
+
projection={projection}
|
|
226
|
+
fieldMap={fieldMap}
|
|
227
|
+
fieldProvider={fieldProvider}
|
|
228
|
+
createOptionLabel={createOptionLabel}
|
|
229
|
+
createOptionIcon={createOptionIcon}
|
|
230
|
+
createInitialValuePath={createInitialValuePath}
|
|
231
|
+
db={db}
|
|
232
|
+
schemaHook={schemaHook}
|
|
233
|
+
getOptions={getRefOptions}
|
|
234
|
+
onCreate={onCreate}
|
|
235
|
+
/>
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
return null;
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
FormField.displayName = 'Form.FormField';
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Get property input component.
|
|
247
|
+
*/
|
|
248
|
+
const getFormField = ({ type, format }: FormFieldComponentProps): FormFieldComponent | undefined => {
|
|
249
|
+
//
|
|
250
|
+
// Standard formats.
|
|
251
|
+
//
|
|
252
|
+
|
|
253
|
+
const formatField = Match.value(format).pipe(
|
|
254
|
+
Match.withReturnType<FormFieldComponent | undefined>(),
|
|
255
|
+
Match.when(Format.TypeFormat.GeoPoint, () => GeoPointField),
|
|
256
|
+
Match.when(Format.TypeFormat.Markdown, () => MarkdownField),
|
|
257
|
+
Match.when(Format.TypeFormat.Text, () => TextAreaField),
|
|
258
|
+
Match.orElse(() => undefined),
|
|
259
|
+
);
|
|
260
|
+
if (formatField) {
|
|
261
|
+
return formatField;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
//
|
|
265
|
+
// Standard types.
|
|
266
|
+
//
|
|
267
|
+
|
|
268
|
+
switch (type._tag) {
|
|
269
|
+
// TODO(wittjosiah): Schema.Any is currently used to represent template inputs.
|
|
270
|
+
case 'AnyKeyword':
|
|
271
|
+
case 'StringKeyword':
|
|
272
|
+
return TextField;
|
|
273
|
+
case 'NumberKeyword':
|
|
274
|
+
return NumberField;
|
|
275
|
+
case 'BooleanKeyword':
|
|
276
|
+
return BooleanField;
|
|
277
|
+
}
|
|
278
|
+
};
|
|
279
|
+
|
|
280
|
+
const getOptions = (ast: SchemaAST.AST): Format.Options[] | undefined => {
|
|
281
|
+
if (isLiteralUnion(ast)) {
|
|
282
|
+
return ast.types.map((type) => type.literal).filter((v): v is string | number => v !== null);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
return Format.OptionsAnnotation.getFromAst(ast).pipe((annotation) => Option.getOrUndefined(annotation));
|
|
286
|
+
};
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2025 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { type Meta, type StoryObj } from '@storybook/react-vite';
|
|
6
|
+
import React, { type ChangeEvent, useCallback } from 'react';
|
|
7
|
+
|
|
8
|
+
import { Input } from '@dxos/react-ui';
|
|
9
|
+
import { withLayout, withTheme } from '@dxos/react-ui/testing';
|
|
10
|
+
|
|
11
|
+
import { type FormFieldComponentProps, FormFieldWrapper } from './FormFieldComponent';
|
|
12
|
+
|
|
13
|
+
const Component = ({
|
|
14
|
+
type,
|
|
15
|
+
placeholder,
|
|
16
|
+
readonly,
|
|
17
|
+
onBlur,
|
|
18
|
+
onValueChange,
|
|
19
|
+
...props
|
|
20
|
+
}: FormFieldComponentProps<string>) => {
|
|
21
|
+
const handleChange = useCallback(
|
|
22
|
+
(event: ChangeEvent<HTMLInputElement>) => onValueChange(type, event.target.value),
|
|
23
|
+
[type, onValueChange],
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
return (
|
|
27
|
+
<div className='plb-cardSpacingBlock pli-cardSpacingInline'>
|
|
28
|
+
<FormFieldWrapper<string> {...props}>
|
|
29
|
+
{({ value = '' }) => (
|
|
30
|
+
<Input.TextInput
|
|
31
|
+
disabled={!!readonly}
|
|
32
|
+
placeholder={placeholder}
|
|
33
|
+
noAutoFill
|
|
34
|
+
value={value}
|
|
35
|
+
onChange={handleChange}
|
|
36
|
+
onBlur={onBlur}
|
|
37
|
+
/>
|
|
38
|
+
)}
|
|
39
|
+
</FormFieldWrapper>
|
|
40
|
+
</div>
|
|
41
|
+
);
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const meta: Meta<typeof Component> = {
|
|
45
|
+
title: 'ui/react-ui-form/FormFieldComponent',
|
|
46
|
+
component: Component,
|
|
47
|
+
decorators: [withTheme, withLayout({ layout: 'column' })],
|
|
48
|
+
parameters: {
|
|
49
|
+
layout: 'fullscreen',
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
export default meta;
|
|
54
|
+
|
|
55
|
+
type Story = StoryObj<typeof meta>;
|
|
56
|
+
|
|
57
|
+
export const Default: Story = {
|
|
58
|
+
args: {
|
|
59
|
+
label: 'Label',
|
|
60
|
+
getStatus: () => ({ status: 'error', error: 'Required field' }),
|
|
61
|
+
getValue: () => 'DXOS',
|
|
62
|
+
onBlur: () => {},
|
|
63
|
+
onValueChange: () => {},
|
|
64
|
+
},
|
|
65
|
+
};
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2024 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import type * as Schema from 'effect/Schema';
|
|
6
|
+
import type * as SchemaAST from 'effect/SchemaAST';
|
|
7
|
+
import React, {
|
|
8
|
+
Component,
|
|
9
|
+
type FC,
|
|
10
|
+
type FocusEvent,
|
|
11
|
+
type PropsWithChildren,
|
|
12
|
+
type ReactElement,
|
|
13
|
+
type ReactNode,
|
|
14
|
+
} from 'react';
|
|
15
|
+
|
|
16
|
+
import { type Format } from '@dxos/echo/internal';
|
|
17
|
+
import { Icon, Input, Tooltip } from '@dxos/react-ui';
|
|
18
|
+
import { errorText, mx } from '@dxos/ui-theme';
|
|
19
|
+
|
|
20
|
+
import { type FormFieldStatus } from '../../hooks';
|
|
21
|
+
|
|
22
|
+
const labelSpacing = 'mbs-inputSpacingBlock mbe-labelSpacingBlock';
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Dynamic props passed to input components.
|
|
26
|
+
*/
|
|
27
|
+
export type FormFieldStateProps<T = any> = {
|
|
28
|
+
getStatus: () => FormFieldStatus;
|
|
29
|
+
getValue: () => T | undefined;
|
|
30
|
+
onBlur: (event: FocusEvent<HTMLElement>) => void;
|
|
31
|
+
onValueChange: (type: SchemaAST.AST, value: T) => void;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Presentation mode.
|
|
36
|
+
*
|
|
37
|
+
* - full: Show label, control, and status.
|
|
38
|
+
* - compact: Show label and control.
|
|
39
|
+
* - inline: Control only.
|
|
40
|
+
* - static: Plain DOM; omit all undefined values.
|
|
41
|
+
*/
|
|
42
|
+
// TODO(burdon): 'inline' should be orthogonal to the layout (e.g., array of inline static).
|
|
43
|
+
export type Presentation = 'full' | 'compact' | 'inline' | 'static';
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Props passed to input components.
|
|
47
|
+
*/
|
|
48
|
+
export type FormFieldComponentProps<T = any> = {
|
|
49
|
+
type: SchemaAST.AST;
|
|
50
|
+
format?: Format.TypeFormat;
|
|
51
|
+
readonly?: boolean;
|
|
52
|
+
label: string;
|
|
53
|
+
placeholder?: string;
|
|
54
|
+
autoFocus?: boolean;
|
|
55
|
+
layout?: Presentation;
|
|
56
|
+
} & FormFieldStateProps<T>;
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Form field component.
|
|
60
|
+
*/
|
|
61
|
+
export type FormFieldComponent = FC<FormFieldComponentProps>;
|
|
62
|
+
|
|
63
|
+
export type FormFieldMap = Record<string, FormFieldComponent>;
|
|
64
|
+
|
|
65
|
+
export type FormFieldProvider = (props: {
|
|
66
|
+
prop: string; // TODO(burdon): Path?
|
|
67
|
+
schema: Schema.Schema<any>;
|
|
68
|
+
fieldProps: FormFieldComponentProps;
|
|
69
|
+
}) => ReactElement | undefined;
|
|
70
|
+
|
|
71
|
+
//
|
|
72
|
+
// FormFieldLabel
|
|
73
|
+
//
|
|
74
|
+
|
|
75
|
+
export type FormFieldLabelProps = {
|
|
76
|
+
asChild?: boolean;
|
|
77
|
+
error?: string;
|
|
78
|
+
} & Pick<FormFieldComponentProps, 'label' | 'readonly'>;
|
|
79
|
+
|
|
80
|
+
export const FormFieldLabel = ({ label, error, readonly, asChild }: FormFieldLabelProps) => {
|
|
81
|
+
const Label = readonly || asChild ? 'span' : Input.Label;
|
|
82
|
+
const labelProps = readonly || asChild ? { className: 'text-description text-sm' } : { classNames: '!mlb-0 text-sm' };
|
|
83
|
+
|
|
84
|
+
return (
|
|
85
|
+
<div role='none' className={mx('flex justify-between items-center', labelSpacing)}>
|
|
86
|
+
<Label {...labelProps}>{label}</Label>
|
|
87
|
+
{error && (
|
|
88
|
+
<Tooltip.Trigger asChild content={error} side='bottom'>
|
|
89
|
+
<Icon icon='ph--warning--regular' size={4} classNames={errorText} />
|
|
90
|
+
</Tooltip.Trigger>
|
|
91
|
+
)}
|
|
92
|
+
</div>
|
|
93
|
+
);
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
FormFieldLabel.displayName = 'Form.FieldLabel';
|
|
97
|
+
|
|
98
|
+
//
|
|
99
|
+
// FormFieldWrapper
|
|
100
|
+
//
|
|
101
|
+
|
|
102
|
+
export type FormFieldWrapperProps<T = any> = Pick<
|
|
103
|
+
FormFieldComponentProps,
|
|
104
|
+
'readonly' | 'label' | 'layout' | 'getStatus' | 'getValue'
|
|
105
|
+
> & {
|
|
106
|
+
children?: (props: { value: T }) => ReactNode;
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
export const FormFieldWrapper = <T,>(props: FormFieldWrapperProps<T>) => {
|
|
110
|
+
const { children, readonly, layout, label, getStatus, getValue } = props;
|
|
111
|
+
const { status, error } = getStatus();
|
|
112
|
+
|
|
113
|
+
const value = getValue();
|
|
114
|
+
if (layout === 'static' && value == null) {
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const str = String(value ?? '');
|
|
119
|
+
|
|
120
|
+
return (
|
|
121
|
+
<Input.Root validationValence={status}>
|
|
122
|
+
{layout !== 'inline' && <FormFieldLabel error={error} readonly={readonly} label={label} />}
|
|
123
|
+
{layout === 'static' ? <p>{str}</p> : children ? children({ value }) : null}
|
|
124
|
+
{layout === 'full' && (
|
|
125
|
+
<Input.DescriptionAndValidation>
|
|
126
|
+
<Input.Validation>{error}</Input.Validation>
|
|
127
|
+
</Input.DescriptionAndValidation>
|
|
128
|
+
)}
|
|
129
|
+
</Input.Root>
|
|
130
|
+
);
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
//
|
|
134
|
+
// FormFieldErrorBoundary
|
|
135
|
+
//
|
|
136
|
+
|
|
137
|
+
type FormFieldErrorState = {
|
|
138
|
+
error: Error | undefined;
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
type FormFieldErrorBoundaryProps = PropsWithChildren<{
|
|
142
|
+
path?: (string | number)[];
|
|
143
|
+
}>;
|
|
144
|
+
|
|
145
|
+
export class FormFieldErrorBoundary extends Component<FormFieldErrorBoundaryProps, FormFieldErrorState> {
|
|
146
|
+
static getDerivedStateFromError(error: Error): { error: Error } {
|
|
147
|
+
return { error };
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
override state = { error: undefined };
|
|
151
|
+
|
|
152
|
+
override componentDidUpdate(prevProps: FormFieldErrorBoundaryProps) {
|
|
153
|
+
if (prevProps.path !== this.props.path) {
|
|
154
|
+
this.resetError();
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
override render() {
|
|
159
|
+
if (this.state.error) {
|
|
160
|
+
return (
|
|
161
|
+
<div className='flex gap-2 border border-roseFill font-mono text-sm'>
|
|
162
|
+
<span className='bg-roseFill text-surfaceText pli-1 font-thin'>ERROR</span>
|
|
163
|
+
{String(this.props.path?.join('.'))}
|
|
164
|
+
</div>
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return this.props.children;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
private resetError() {
|
|
172
|
+
this.setState({ error: undefined });
|
|
173
|
+
}
|
|
174
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2025 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import React, { forwardRef, useMemo } from 'react';
|
|
6
|
+
|
|
7
|
+
import { type AnyProperties } from '@dxos/echo/internal';
|
|
8
|
+
import { type SchemaProperty } from '@dxos/effect';
|
|
9
|
+
import { type ThemedClassName } from '@dxos/react-ui';
|
|
10
|
+
import { mx } from '@dxos/ui-theme';
|
|
11
|
+
import { isTruthy } from '@dxos/util';
|
|
12
|
+
|
|
13
|
+
import { type FormHandlerProps } from '../../hooks';
|
|
14
|
+
import { getFormProperties } from '../../util';
|
|
15
|
+
|
|
16
|
+
import { useFormValues } from './Form';
|
|
17
|
+
import { FormField, type FormFieldProps } from './FormField';
|
|
18
|
+
import { FormFieldErrorBoundary, FormFieldLabel } from './FormFieldComponent';
|
|
19
|
+
|
|
20
|
+
export type FormFieldSetProps<T extends AnyProperties> = ThemedClassName<
|
|
21
|
+
{
|
|
22
|
+
label?: string;
|
|
23
|
+
exclude?: (props: SchemaProperty[]) => SchemaProperty[];
|
|
24
|
+
sort?: string[];
|
|
25
|
+
} & Pick<FormHandlerProps<T>, 'schema'> &
|
|
26
|
+
Pick<
|
|
27
|
+
FormFieldProps,
|
|
28
|
+
| 'path'
|
|
29
|
+
| 'autoFocus'
|
|
30
|
+
| 'readonly'
|
|
31
|
+
| 'layout'
|
|
32
|
+
| 'projection'
|
|
33
|
+
| 'fieldMap'
|
|
34
|
+
| 'fieldProvider'
|
|
35
|
+
| 'createOptionLabel'
|
|
36
|
+
| 'createOptionIcon'
|
|
37
|
+
| 'createInitialValuePath'
|
|
38
|
+
| 'db'
|
|
39
|
+
| 'schemaHook'
|
|
40
|
+
| 'getOptions'
|
|
41
|
+
| 'onCreate'
|
|
42
|
+
>
|
|
43
|
+
>;
|
|
44
|
+
|
|
45
|
+
export const FormFieldSet = forwardRef<HTMLDivElement, FormFieldSetProps<any>>(
|
|
46
|
+
({ classNames, label, schema, readonly, path, exclude, sort, projection, layout, ...props }, forwardRef) => {
|
|
47
|
+
const values = useFormValues(FormFieldSet.displayName!, path);
|
|
48
|
+
|
|
49
|
+
// TODO(burdon): Updates on every value change.
|
|
50
|
+
// Remove values dep if can remove from getSchemaProperties.
|
|
51
|
+
const properties = useMemo(() => {
|
|
52
|
+
if (!schema) {
|
|
53
|
+
return [];
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// TODO(wittjosiah): Reconcile FormInputAnnotation with projection hidden properties & exclude function.
|
|
57
|
+
const props = getFormProperties(schema.ast);
|
|
58
|
+
|
|
59
|
+
// Use projection-based field management when view and projection are available.
|
|
60
|
+
if (projection) {
|
|
61
|
+
const fieldProjections = projection.getFieldProjections();
|
|
62
|
+
const hiddenProperties = new Set(projection.getHiddenProperties());
|
|
63
|
+
|
|
64
|
+
// Filter properties to only include visible ones and order by projection.
|
|
65
|
+
const visibleProps = props.filter((prop) => !hiddenProperties.has(prop.name.toString()));
|
|
66
|
+
const orderedProps: SchemaProperty[] = [];
|
|
67
|
+
|
|
68
|
+
// Add properties in projection field order.
|
|
69
|
+
for (const fieldProjection of fieldProjections) {
|
|
70
|
+
const fieldPath = String(fieldProjection.field.path);
|
|
71
|
+
const prop = visibleProps.find((p) => p.name === fieldPath);
|
|
72
|
+
if (prop) {
|
|
73
|
+
orderedProps.push(prop);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Add any remaining properties not in projection.
|
|
78
|
+
const projectionPaths = new Set(fieldProjections.map((projection) => String(projection.field.path)));
|
|
79
|
+
const remainingProps = visibleProps.filter((prop) => !projectionPaths.has(prop.name.toString()));
|
|
80
|
+
orderedProps.push(...remainingProps);
|
|
81
|
+
return orderedProps;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Fallback to legacy filter/sort behavior.
|
|
85
|
+
const filtered = exclude ? exclude(props) : props;
|
|
86
|
+
return sort
|
|
87
|
+
? filtered.sort(({ name: a }, { name: b }) => sort.indexOf(a.toString()) - sort.indexOf(b.toString()))
|
|
88
|
+
: filtered;
|
|
89
|
+
}, [schema, values, exclude, sort, projection?.fields]);
|
|
90
|
+
|
|
91
|
+
if ((readonly || layout === 'static') && values == null) {
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return (
|
|
96
|
+
<div
|
|
97
|
+
role='form'
|
|
98
|
+
className={mx('is-full', layout === 'inline' && 'flex flex-col gap-2', classNames)}
|
|
99
|
+
ref={forwardRef}
|
|
100
|
+
>
|
|
101
|
+
{layout !== 'inline' && label && <FormFieldLabel label={label} asChild />}
|
|
102
|
+
{properties
|
|
103
|
+
.map((property) => {
|
|
104
|
+
const name = property.name.toString();
|
|
105
|
+
return (
|
|
106
|
+
<FormFieldErrorBoundary key={name} path={[...(path ?? []), name]}>
|
|
107
|
+
<FormField
|
|
108
|
+
type={property.type}
|
|
109
|
+
name={name}
|
|
110
|
+
path={[...(path ?? []), name]}
|
|
111
|
+
readonly={readonly}
|
|
112
|
+
layout={layout}
|
|
113
|
+
projection={projection}
|
|
114
|
+
{...props}
|
|
115
|
+
/>
|
|
116
|
+
</FormFieldErrorBoundary>
|
|
117
|
+
);
|
|
118
|
+
})
|
|
119
|
+
.filter(isTruthy)}
|
|
120
|
+
</div>
|
|
121
|
+
);
|
|
122
|
+
},
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
FormFieldSet.displayName = 'Form.Fields';
|