@dxos/react-ui-form 0.8.4-main.1068cf700f → 0.8.4-main.1c7ec43d41
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/README.md +0 -1
- package/dist/lib/browser/index.mjs +1135 -663
- package/dist/lib/browser/index.mjs.map +4 -4
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/browser/translations.mjs +82 -0
- package/dist/lib/browser/translations.mjs.map +7 -0
- package/dist/lib/node-esm/index.mjs +1135 -663
- package/dist/lib/node-esm/index.mjs.map +4 -4
- package/dist/lib/node-esm/meta.json +1 -1
- package/dist/lib/node-esm/translations.mjs +84 -0
- package/dist/lib/node-esm/translations.mjs.map +7 -0
- package/dist/types/src/components/FieldEditor/FieldEditor.d.ts +3 -3
- package/dist/types/src/components/FieldEditor/FieldEditor.d.ts.map +1 -1
- package/dist/types/src/components/FieldEditor/FieldEditor.stories.d.ts +63 -61
- package/dist/types/src/components/FieldEditor/FieldEditor.stories.d.ts.map +1 -1
- package/dist/types/src/components/Form/Form.d.ts +44 -16
- package/dist/types/src/components/Form/Form.d.ts.map +1 -1
- package/dist/types/src/components/Form/Form.stories.d.ts +75 -73
- package/dist/types/src/components/Form/Form.stories.d.ts.map +1 -1
- package/dist/types/src/components/Form/FormField.d.ts +11 -5
- package/dist/types/src/components/Form/FormField.d.ts.map +1 -1
- package/dist/types/src/components/Form/FormFieldComponent.d.ts +8 -7
- package/dist/types/src/components/Form/FormFieldComponent.d.ts.map +1 -1
- package/dist/types/src/components/Form/FormFieldComponent.stories.d.ts +2 -2
- package/dist/types/src/components/Form/FormFieldComponent.stories.d.ts.map +1 -1
- package/dist/types/src/components/Form/FormFieldSet.d.ts +6 -10
- package/dist/types/src/components/Form/FormFieldSet.d.ts.map +1 -1
- package/dist/types/src/components/Form/fields/ArrayField.d.ts +4 -4
- package/dist/types/src/components/Form/fields/ArrayField.d.ts.map +1 -1
- package/dist/types/src/components/Form/fields/BooleanField.d.ts.map +1 -1
- package/dist/types/src/components/Form/fields/GeoPointField.d.ts.map +1 -1
- package/dist/types/src/components/Form/fields/MarkdownField.d.ts +8 -1
- package/dist/types/src/components/Form/fields/MarkdownField.d.ts.map +1 -1
- package/dist/types/src/components/Form/fields/MarkdownField.stories.d.ts +84 -0
- package/dist/types/src/components/Form/fields/MarkdownField.stories.d.ts.map +1 -0
- package/dist/types/src/components/Form/fields/NumberField.d.ts.map +1 -1
- package/dist/types/src/components/Form/fields/RefField.d.ts +15 -7
- package/dist/types/src/components/Form/fields/RefField.d.ts.map +1 -1
- package/dist/types/src/components/Form/fields/SelectField.d.ts.map +1 -1
- package/dist/types/src/components/Form/fields/SelectOptionField.d.ts.map +1 -1
- package/dist/types/src/components/Form/fields/SelectOptionField.stories.d.ts +63 -61
- package/dist/types/src/components/Form/fields/SelectOptionField.stories.d.ts.map +1 -1
- package/dist/types/src/components/Form/fields/TextAreaField.d.ts.map +1 -1
- package/dist/types/src/components/Form/fields/TextField.d.ts +1 -1
- package/dist/types/src/components/Form/fields/TextField.d.ts.map +1 -1
- package/dist/types/src/components/Form/fields/TupleField.d.ts.map +1 -1
- package/dist/types/src/components/ObjectForm/ObjectForm.d.ts +9 -0
- package/dist/types/src/components/ObjectForm/ObjectForm.d.ts.map +1 -0
- package/dist/types/src/components/ObjectForm/index.d.ts +2 -0
- package/dist/types/src/components/ObjectForm/index.d.ts.map +1 -0
- package/dist/types/src/components/ObjectPicker/ObjectPicker.d.ts +26 -4
- package/dist/types/src/components/ObjectPicker/ObjectPicker.d.ts.map +1 -1
- package/dist/types/src/components/ObjectPicker/ObjectPicker.stories.d.ts +63 -61
- package/dist/types/src/components/ObjectPicker/ObjectPicker.stories.d.ts.map +1 -1
- package/dist/types/src/components/ObjectProperties/ObjectProperties.d.ts +9 -0
- package/dist/types/src/components/ObjectProperties/ObjectProperties.d.ts.map +1 -0
- package/dist/types/src/components/ObjectProperties/ObjectProperties.stories.d.ts +127 -0
- package/dist/types/src/components/ObjectProperties/ObjectProperties.stories.d.ts.map +1 -0
- package/dist/types/src/components/ObjectProperties/ObjectProperties.test.d.ts +2 -0
- package/dist/types/src/components/ObjectProperties/ObjectProperties.test.d.ts.map +1 -0
- package/dist/types/src/components/ObjectProperties/index.d.ts +2 -0
- package/dist/types/src/components/ObjectProperties/index.d.ts.map +1 -0
- package/dist/types/src/components/Settings/Settings.d.ts +28 -33
- package/dist/types/src/components/Settings/Settings.d.ts.map +1 -1
- package/dist/types/src/components/Settings/Settings.stories.d.ts +64 -61
- package/dist/types/src/components/Settings/Settings.stories.d.ts.map +1 -1
- package/dist/types/src/components/Settings/SettingsFieldSet.d.ts +43 -0
- package/dist/types/src/components/Settings/SettingsFieldSet.d.ts.map +1 -0
- package/dist/types/src/components/Settings/SettingsItem.d.ts +16 -0
- package/dist/types/src/components/Settings/SettingsItem.d.ts.map +1 -0
- package/dist/types/src/components/Settings/SettingsSelectField.d.ts +18 -0
- package/dist/types/src/components/Settings/SettingsSelectField.d.ts.map +1 -0
- package/dist/types/src/components/Settings/index.d.ts +2 -0
- package/dist/types/src/components/Settings/index.d.ts.map +1 -1
- package/dist/types/src/components/ViewEditor/ViewEditor.d.ts +6 -6
- package/dist/types/src/components/ViewEditor/ViewEditor.d.ts.map +1 -1
- package/dist/types/src/components/ViewEditor/ViewEditor.stories.d.ts +67 -64
- package/dist/types/src/components/ViewEditor/ViewEditor.stories.d.ts.map +1 -1
- package/dist/types/src/components/index.d.ts +2 -0
- package/dist/types/src/components/index.d.ts.map +1 -1
- package/dist/types/src/components/testing.d.ts +4 -4
- package/dist/types/src/components/testing.d.ts.map +1 -1
- package/dist/types/src/hooks/useFormHandler.d.ts.map +1 -1
- package/dist/types/src/hooks/useKeyHandler.d.ts +2 -1
- package/dist/types/src/hooks/useKeyHandler.d.ts.map +1 -1
- package/dist/types/src/index.d.ts +0 -2
- package/dist/types/src/index.d.ts.map +1 -1
- package/dist/types/src/translations.d.ts +63 -61
- package/dist/types/src/translations.d.ts.map +1 -1
- package/dist/types/src/util/field-type.d.ts +19 -0
- package/dist/types/src/util/field-type.d.ts.map +1 -0
- 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/src/util/index.d.ts +1 -1
- package/dist/types/src/util/index.d.ts.map +1 -1
- package/dist/types/src/util/properties.d.ts.map +1 -1
- package/dist/types/src/util/refs.d.ts +1 -0
- package/dist/types/src/util/refs.d.ts.map +1 -1
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/package.json +42 -42
- package/src/components/FieldEditor/FieldEditor.stories.tsx +6 -8
- package/src/components/FieldEditor/FieldEditor.test.tsx +0 -1
- package/src/components/FieldEditor/FieldEditor.tsx +6 -6
- package/src/components/Form/AUDIT.md +17 -0
- package/src/components/Form/Form.stories.tsx +29 -44
- package/src/components/Form/Form.tsx +102 -54
- package/src/components/Form/FormField.tsx +39 -19
- package/src/components/Form/FormFieldComponent.stories.tsx +4 -4
- package/src/components/Form/FormFieldComponent.tsx +10 -10
- package/src/components/Form/FormFieldSet.tsx +121 -104
- package/src/components/Form/fields/ArrayField.tsx +14 -9
- package/src/components/Form/fields/GeoPointField.tsx +8 -7
- package/src/components/Form/fields/MarkdownField.stories.tsx +206 -0
- package/src/components/Form/fields/MarkdownField.tsx +135 -45
- package/src/components/Form/fields/RefField.tsx +84 -44
- package/src/components/Form/fields/SelectField.tsx +1 -1
- package/src/components/Form/fields/SelectOptionField.stories.tsx +10 -8
- package/src/components/Form/fields/SelectOptionField.tsx +12 -12
- package/src/components/Form/fields/TextField.tsx +2 -2
- package/src/components/Form/fields/TupleField.tsx +1 -1
- package/src/components/ObjectForm/ObjectForm.tsx +114 -0
- package/src/components/ObjectForm/index.ts +5 -0
- package/src/components/ObjectPicker/ObjectPicker.stories.tsx +16 -18
- package/src/components/ObjectPicker/ObjectPicker.tsx +92 -65
- package/src/components/ObjectProperties/ObjectProperties.stories.tsx +362 -0
- package/src/components/ObjectProperties/ObjectProperties.test.tsx +101 -0
- package/src/components/ObjectProperties/ObjectProperties.tsx +141 -0
- package/src/components/ObjectProperties/index.ts +5 -0
- package/src/components/Settings/AUDIT.md +52 -0
- package/src/components/Settings/Settings.stories.tsx +69 -21
- package/src/components/Settings/Settings.tsx +35 -187
- package/src/components/Settings/SettingsFieldSet.tsx +257 -0
- package/src/components/Settings/SettingsItem.tsx +62 -0
- package/src/components/Settings/SettingsSelectField.tsx +59 -0
- package/src/components/Settings/index.ts +2 -0
- package/src/components/ViewEditor/ViewEditor.stories.tsx +21 -19
- package/src/components/ViewEditor/ViewEditor.test.tsx +2 -2
- package/src/components/ViewEditor/ViewEditor.tsx +87 -49
- package/src/components/index.ts +2 -0
- package/src/components/testing.tsx +23 -12
- package/src/hooks/useFormHandler.ts +42 -24
- package/src/hooks/useKeyHandler.tsx +7 -7
- package/src/index.ts +0 -4
- package/src/translations.ts +66 -63
- package/src/util/field-type.ts +60 -0
- package/src/util/index.ts +1 -1
- package/src/util/refs.ts +7 -3
- package/dist/types/src/components/Form/Form-old.stories.d.ts +0 -131
- package/dist/types/src/components/Form/Form-old.stories.d.ts.map +0 -1
- package/dist/types/src/util/echo.d.ts +0 -6
- package/dist/types/src/util/echo.d.ts.map +0 -1
- package/src/components/Form/Form-old.stories.tsx +0 -314
- package/src/util/echo.ts +0 -25
|
@@ -3,14 +3,25 @@
|
|
|
3
3
|
//
|
|
4
4
|
|
|
5
5
|
import { createContext } from '@radix-ui/react-context';
|
|
6
|
+
import * as Option from 'effect/Option';
|
|
6
7
|
import * as Schema from 'effect/Schema';
|
|
7
|
-
import
|
|
8
|
+
import * as SchemaAST from 'effect/SchemaAST';
|
|
8
9
|
import React, { type PropsWithChildren, useEffect, useMemo, useRef } from 'react';
|
|
9
10
|
|
|
11
|
+
import { Annotation as EchoAnnotation } from '@dxos/echo';
|
|
10
12
|
import { type AnyProperties } from '@dxos/echo/internal';
|
|
11
13
|
import { createJsonPath, getValue as getValue$ } from '@dxos/effect';
|
|
12
|
-
import {
|
|
13
|
-
|
|
14
|
+
import {
|
|
15
|
+
IconButton,
|
|
16
|
+
type IconButtonProps,
|
|
17
|
+
ScrollArea,
|
|
18
|
+
type ThemedClassName,
|
|
19
|
+
useMergeRefs,
|
|
20
|
+
useTranslation,
|
|
21
|
+
} from '@dxos/react-ui';
|
|
22
|
+
import { composable, composableProps, mx, withColumn } from '@dxos/ui-theme';
|
|
23
|
+
|
|
24
|
+
import { translationKey } from '#translations';
|
|
14
25
|
|
|
15
26
|
import {
|
|
16
27
|
type FormHandler,
|
|
@@ -19,38 +30,35 @@ import {
|
|
|
19
30
|
useFormHandler,
|
|
20
31
|
useKeyHandler,
|
|
21
32
|
} from '../../hooks';
|
|
22
|
-
import { translationKey } from '../../translations';
|
|
23
|
-
import { setValueEchoAware } from '../../util';
|
|
24
|
-
|
|
25
33
|
import { FormFieldLabel, type FormFieldLabelProps, type FormFieldStateProps } from './FormFieldComponent';
|
|
26
34
|
import {
|
|
27
35
|
FormFieldSet as NaturalFormFieldSet,
|
|
28
36
|
type FormFieldSetProps as NaturalFormFieldSetProps,
|
|
29
37
|
} from './FormFieldSet';
|
|
30
38
|
|
|
31
|
-
//
|
|
32
|
-
// [x] Unify readonly/inline modes
|
|
33
|
-
// [ ] Don't call save/autoSave if value hasn't changed
|
|
34
|
-
// [ ] Fix onCancel (restore values)
|
|
35
|
-
// [ ] Fix useSchema Type.Obj.Any cast
|
|
36
|
-
// [ ] Remove @dxos/echo-db deps
|
|
37
|
-
// [ ] TableCellEditor (handleEnter/ModalController).
|
|
38
|
-
// [ ] Use FormFieldWrapper uniformly
|
|
39
|
-
// [ ] Inline tables for object arrays
|
|
40
|
-
// [ ] Defer query until popover
|
|
41
|
-
// [ ] Omit id from sub properties.
|
|
42
|
-
// [x] Refs
|
|
43
|
-
// [x] Single-select (fix popover)
|
|
44
|
-
// [x] Multi-select (array)
|
|
45
|
-
// [ ] auto save doesn't work for combobox + select due to only firing on blur (workaround is to use onValuesChanged)
|
|
39
|
+
// TODO(burdon): Move styles to form.ts (as with ui-theme).
|
|
46
40
|
|
|
47
|
-
// TODO(burdon):
|
|
41
|
+
// TODO(burdon): Reconcile with @dxos/echo.
|
|
48
42
|
export type ExcludeId<S extends Schema.Schema.AnyNoContext> = Omit<Schema.Schema.Type<S>, 'id'>;
|
|
49
43
|
|
|
50
44
|
// TODO(burdon): Move to @dxos/schema (re-export here).
|
|
51
45
|
export const omitId = <S extends Schema.Schema.AnyNoContext>(schema: S): Schema.Schema<ExcludeId<S>, ExcludeId<S>> =>
|
|
52
46
|
schema.pipe(Schema.omit('id')) as any;
|
|
53
47
|
|
|
48
|
+
/**
|
|
49
|
+
* Drop fields annotated with `FormInputAnnotation.set(false)` from a schema so
|
|
50
|
+
* the form's validator doesn't trip on required-but-hidden fields. Used by
|
|
51
|
+
* the picker's inline create form, where a `FactoryAnnotation` typically
|
|
52
|
+
* supplies the hidden values (e.g. a backing-object Ref) outside the form.
|
|
53
|
+
*/
|
|
54
|
+
export const omitHiddenFormFields = <S extends Schema.Schema.AnyNoContext>(schema: S): S => {
|
|
55
|
+
const properties = SchemaAST.getPropertySignatures(schema.ast);
|
|
56
|
+
const hidden = properties
|
|
57
|
+
.filter((prop) => Option.getOrElse(EchoAnnotation.FormInputAnnotation.getFromAst(prop.type), () => true) === false)
|
|
58
|
+
.map((prop) => prop.name as string);
|
|
59
|
+
return hidden.length === 0 ? schema : (schema.pipe(Schema.omit(...(hidden as [string, ...string[]]))) as any);
|
|
60
|
+
};
|
|
61
|
+
|
|
54
62
|
//
|
|
55
63
|
// Context
|
|
56
64
|
//
|
|
@@ -70,7 +78,10 @@ type FormContextValue<T extends AnyProperties = any> = {
|
|
|
70
78
|
* Testing.
|
|
71
79
|
*/
|
|
72
80
|
testId?: string;
|
|
73
|
-
} & Pick<
|
|
81
|
+
} & Pick<
|
|
82
|
+
NaturalFormFieldSetProps<T>,
|
|
83
|
+
'readonly' | 'layout' | 'fieldMap' | 'fieldProvider' | 'projection' | 'createTypename' | 'createFieldMap' | 'db'
|
|
84
|
+
>;
|
|
74
85
|
|
|
75
86
|
const [FormContextProvider, useFormContext] = createContext<FormContextValue>('Form');
|
|
76
87
|
|
|
@@ -84,19 +95,25 @@ const useFormValues: {
|
|
|
84
95
|
path: (string | number)[] | undefined,
|
|
85
96
|
defaultValue: () => T,
|
|
86
97
|
): T | undefined;
|
|
87
|
-
} = (componentName: string, path
|
|
88
|
-
const
|
|
98
|
+
} = (componentName: string, path?: (string | number)[], defaultValue?: () => any) => {
|
|
99
|
+
const stablePath = useMemo(() => path ?? [], [path ? path.join('.') : undefined]);
|
|
100
|
+
const jsonPath = createJsonPath(stablePath);
|
|
89
101
|
const {
|
|
90
|
-
form: { values },
|
|
102
|
+
form: { values, onValueChange },
|
|
91
103
|
} = useFormContext(componentName);
|
|
92
104
|
|
|
93
105
|
const value = getValue$(values, jsonPath);
|
|
94
106
|
|
|
107
|
+
// Apply default value once when the field has no value. lastAppliedPathRef prevents
|
|
108
|
+
// re-applying on every render (e.g. when defaultValue() returns null) and ensures
|
|
109
|
+
// we apply per path when the hook is used for different fields.
|
|
110
|
+
const lastAppliedPathRef = useRef<string | null>(null);
|
|
95
111
|
useEffect(() => {
|
|
96
|
-
if (
|
|
97
|
-
|
|
112
|
+
if (value == null && defaultValue && lastAppliedPathRef.current !== jsonPath) {
|
|
113
|
+
lastAppliedPathRef.current = jsonPath;
|
|
114
|
+
onValueChange(stablePath, SchemaAST.stringKeyword, defaultValue());
|
|
98
115
|
}
|
|
99
|
-
}, [value, defaultValue]);
|
|
116
|
+
}, [value, defaultValue, onValueChange, stablePath, jsonPath]);
|
|
100
117
|
|
|
101
118
|
return value;
|
|
102
119
|
};
|
|
@@ -166,16 +183,15 @@ FormRoot.displayName = 'Form.Root';
|
|
|
166
183
|
|
|
167
184
|
const FORM_VIEWPORT_NAME = 'Form.Viewport';
|
|
168
185
|
|
|
169
|
-
type FormViewportProps =
|
|
186
|
+
type FormViewportProps = {};
|
|
170
187
|
|
|
171
|
-
|
|
172
|
-
const FormViewport = ({ children }: FormViewportProps) => {
|
|
188
|
+
const FormViewport = composable<HTMLDivElement>(({ children, ...props }, forwardedRef) => {
|
|
173
189
|
return (
|
|
174
|
-
<ScrollArea.Root orientation='vertical'>
|
|
190
|
+
<ScrollArea.Root {...composableProps(props)} orientation='vertical' centered padding thin ref={forwardedRef}>
|
|
175
191
|
<ScrollArea.Viewport>{children}</ScrollArea.Viewport>
|
|
176
192
|
</ScrollArea.Root>
|
|
177
193
|
);
|
|
178
|
-
};
|
|
194
|
+
});
|
|
179
195
|
|
|
180
196
|
FormViewport.displayName = FORM_VIEWPORT_NAME;
|
|
181
197
|
|
|
@@ -187,23 +203,25 @@ const FORM_CONTENT_NAME = 'Form.Content';
|
|
|
187
203
|
|
|
188
204
|
type FormContentProps = ThemedClassName<PropsWithChildren<{}>>;
|
|
189
205
|
|
|
190
|
-
|
|
191
|
-
const FormContent = ({ classNames, children }: FormContentProps) => {
|
|
206
|
+
const FormContent = composable<HTMLDivElement, FormContentProps>(({ children, ...props }, forwardedRef) => {
|
|
192
207
|
const { form, testId } = useFormContext(FORM_CONTENT_NAME);
|
|
193
|
-
const
|
|
194
|
-
|
|
208
|
+
const localRef = useRef<HTMLDivElement>(null);
|
|
209
|
+
const mergedRef = useMergeRefs([forwardedRef, localRef]);
|
|
210
|
+
useKeyHandler(localRef, form);
|
|
195
211
|
|
|
196
212
|
return (
|
|
197
213
|
<div
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
214
|
+
{...composableProps(props, {
|
|
215
|
+
role: 'form',
|
|
216
|
+
classNames: mx(withColumn.center(), 'flex flex-col w-full pb-form-gap'),
|
|
217
|
+
})}
|
|
201
218
|
data-testid={testId}
|
|
219
|
+
ref={mergedRef}
|
|
202
220
|
>
|
|
203
221
|
{children}
|
|
204
222
|
</div>
|
|
205
223
|
);
|
|
206
|
-
};
|
|
224
|
+
});
|
|
207
225
|
|
|
208
226
|
FormContent.displayName = FORM_CONTENT_NAME;
|
|
209
227
|
|
|
@@ -213,12 +231,12 @@ FormContent.displayName = FORM_CONTENT_NAME;
|
|
|
213
231
|
|
|
214
232
|
const FORM_FIELDSET_NAME = 'Form.FieldSet';
|
|
215
233
|
|
|
216
|
-
type FormFieldSetProps = ThemedClassName<
|
|
234
|
+
type FormFieldSetProps = ThemedClassName<NaturalFormFieldSetProps<any>>;
|
|
217
235
|
|
|
218
|
-
const FormFieldSet = (
|
|
219
|
-
const { form, ...
|
|
236
|
+
const FormFieldSet = (props: FormFieldSetProps) => {
|
|
237
|
+
const { form, ...contextProps } = useFormContext(FORM_FIELDSET_NAME);
|
|
220
238
|
|
|
221
|
-
return <NaturalFormFieldSet
|
|
239
|
+
return <NaturalFormFieldSet schema={form.schema} {...contextProps} {...props} />;
|
|
222
240
|
};
|
|
223
241
|
|
|
224
242
|
FormFieldSet.displayName = FORM_FIELDSET_NAME;
|
|
@@ -247,12 +265,15 @@ const FormActions = ({ classNames }: FormActionsProps) => {
|
|
|
247
265
|
// Deprecate FormSubmit ans use FormActions without Cancel button if no callback is supplied.
|
|
248
266
|
|
|
249
267
|
return (
|
|
250
|
-
<div
|
|
268
|
+
<div
|
|
269
|
+
role='none'
|
|
270
|
+
className={mx(withColumn.center(), 'grid grid-flow-col gap-form-gap auto-cols-fr py-form-padding', classNames)}
|
|
271
|
+
>
|
|
251
272
|
{onCancel && (
|
|
252
273
|
<IconButton
|
|
253
274
|
icon='ph--x--regular'
|
|
254
275
|
iconEnd
|
|
255
|
-
label={t('cancel
|
|
276
|
+
label={t('cancel-button.label')}
|
|
256
277
|
onClick={onCancel}
|
|
257
278
|
data-testid='cancel-button'
|
|
258
279
|
/>
|
|
@@ -264,7 +285,7 @@ const FormActions = ({ classNames }: FormActionsProps) => {
|
|
|
264
285
|
disabled={!canSave}
|
|
265
286
|
icon='ph--check--regular'
|
|
266
287
|
iconEnd
|
|
267
|
-
label={t('save
|
|
288
|
+
label={t('save-button.label')}
|
|
268
289
|
onClick={onSave}
|
|
269
290
|
data-testid='save-button'
|
|
270
291
|
/>
|
|
@@ -275,6 +296,31 @@ const FormActions = ({ classNames }: FormActionsProps) => {
|
|
|
275
296
|
|
|
276
297
|
FormActions.displayName = FORM_ACTIONS_NAME;
|
|
277
298
|
|
|
299
|
+
//
|
|
300
|
+
// Section
|
|
301
|
+
//
|
|
302
|
+
|
|
303
|
+
const FORM_SECTION_NAME = 'Form.Section';
|
|
304
|
+
|
|
305
|
+
type FormSectionProps = ThemedClassName<{ label: string; description?: string }>;
|
|
306
|
+
|
|
307
|
+
const FormSection = composable<HTMLDivElement, FormSectionProps>(
|
|
308
|
+
({ children, label, description, ...props }, forwardedRef) => {
|
|
309
|
+
return (
|
|
310
|
+
<div
|
|
311
|
+
{...composableProps(props, { classNames: 'flex flex-col pt-form-section-gap first:pt-0' })}
|
|
312
|
+
ref={forwardedRef}
|
|
313
|
+
>
|
|
314
|
+
<h2 className='text-lg'>{label}</h2>
|
|
315
|
+
{description && <p className='text-description'>{description}</p>}
|
|
316
|
+
{children}
|
|
317
|
+
</div>
|
|
318
|
+
);
|
|
319
|
+
},
|
|
320
|
+
);
|
|
321
|
+
|
|
322
|
+
FormSection.displayName = FORM_SECTION_NAME;
|
|
323
|
+
|
|
278
324
|
//
|
|
279
325
|
// Submit
|
|
280
326
|
//
|
|
@@ -296,14 +342,14 @@ const FormSubmit = ({ classNames, label, icon, disabled }: FormSubmitProps) => {
|
|
|
296
342
|
}
|
|
297
343
|
|
|
298
344
|
return (
|
|
299
|
-
<div role='none' className={mx('flex
|
|
345
|
+
<div role='none' className={mx('flex w-full pt-form-padding', classNames)}>
|
|
300
346
|
<IconButton
|
|
301
|
-
classNames='
|
|
347
|
+
classNames='w-full'
|
|
302
348
|
type='submit'
|
|
303
349
|
variant='primary'
|
|
304
350
|
disabled={disabled ?? !canSave}
|
|
305
351
|
icon={icon ?? 'ph--check--regular'}
|
|
306
|
-
label={label ?? t('save
|
|
352
|
+
label={label ?? t('save-button.label')}
|
|
307
353
|
onClick={onSave}
|
|
308
354
|
data-testid='save-button'
|
|
309
355
|
/>
|
|
@@ -322,10 +368,11 @@ export const Form = {
|
|
|
322
368
|
Root: FormRoot,
|
|
323
369
|
Viewport: FormViewport,
|
|
324
370
|
Content: FormContent,
|
|
371
|
+
Section: FormSection,
|
|
325
372
|
FieldSet: FormFieldSet,
|
|
373
|
+
Label: FormFieldLabel,
|
|
326
374
|
Actions: FormActions,
|
|
327
375
|
Submit: FormSubmit,
|
|
328
|
-
Label: FormFieldLabel,
|
|
329
376
|
};
|
|
330
377
|
|
|
331
378
|
export { useFormContext, useFormValues, useFormFieldState };
|
|
@@ -334,8 +381,9 @@ export type {
|
|
|
334
381
|
FormRootProps,
|
|
335
382
|
FormViewportProps,
|
|
336
383
|
FormContentProps,
|
|
384
|
+
FormSectionProps,
|
|
337
385
|
FormFieldSetProps,
|
|
386
|
+
FormFieldLabelProps,
|
|
338
387
|
FormActionsProps,
|
|
339
388
|
FormSubmitProps,
|
|
340
|
-
FormFieldLabelProps as LabelProps,
|
|
341
389
|
};
|
|
@@ -23,9 +23,9 @@ import {
|
|
|
23
23
|
import { useTranslation } from '@dxos/react-ui';
|
|
24
24
|
import { type ProjectionModel } from '@dxos/schema';
|
|
25
25
|
|
|
26
|
-
import { translationKey } from '
|
|
27
|
-
import { getRefProps } from '../../util';
|
|
26
|
+
import { translationKey } from '#translations';
|
|
28
27
|
|
|
28
|
+
import { getRefProps } from '../../util';
|
|
29
29
|
import {
|
|
30
30
|
ArrayField,
|
|
31
31
|
BooleanField,
|
|
@@ -78,14 +78,21 @@ export type FormFieldProps = {
|
|
|
78
78
|
* Function to lookup custom renderers for specific properties.
|
|
79
79
|
*/
|
|
80
80
|
fieldProvider?: FormFieldProvider;
|
|
81
|
+
/**
|
|
82
|
+
* Typename of the ref type that the create props apply to.
|
|
83
|
+
* When set, createOptionLabel/createOptionIcon/createInitialValuePath/createFieldMap/onCreate
|
|
84
|
+
* are only passed to ref fields whose typename matches.
|
|
85
|
+
*/
|
|
86
|
+
createTypename?: string;
|
|
81
87
|
} & Pick<FormFieldComponentProps, 'autoFocus' | 'readonly' | 'layout'> &
|
|
82
88
|
Pick<
|
|
83
89
|
RefFieldProps,
|
|
84
90
|
| 'createOptionLabel'
|
|
85
91
|
| 'createOptionIcon'
|
|
86
92
|
| 'createInitialValuePath'
|
|
93
|
+
| 'createFieldMap'
|
|
87
94
|
| 'db'
|
|
88
|
-
| '
|
|
95
|
+
| 'useSchema'
|
|
89
96
|
| 'getOptions'
|
|
90
97
|
| 'onCreate'
|
|
91
98
|
>;
|
|
@@ -102,12 +109,14 @@ export const FormField = (props: FormFieldProps) => {
|
|
|
102
109
|
layout,
|
|
103
110
|
|
|
104
111
|
// RefFieldProps
|
|
112
|
+
createTypename,
|
|
105
113
|
createOptionLabel,
|
|
106
114
|
createOptionIcon,
|
|
107
115
|
createInitialValuePath,
|
|
116
|
+
createFieldMap,
|
|
108
117
|
db,
|
|
109
|
-
schemaHook,
|
|
110
|
-
getOptions
|
|
118
|
+
useSchema: schemaHook,
|
|
119
|
+
getOptions,
|
|
111
120
|
onCreate,
|
|
112
121
|
} = props;
|
|
113
122
|
const { t } = useTranslation(translationKey);
|
|
@@ -117,7 +126,7 @@ export const FormField = (props: FormFieldProps) => {
|
|
|
117
126
|
|
|
118
127
|
const label = useMemo(() => title ?? String.capitalize(name), [title, name]);
|
|
119
128
|
const placeholder = useMemo(
|
|
120
|
-
() => (examples?.length ? `${t('example
|
|
129
|
+
() => (examples?.length ? `${t('example.placeholder')}: ${examples[0]}` : (description ?? label)),
|
|
121
130
|
[examples, description, label],
|
|
122
131
|
);
|
|
123
132
|
|
|
@@ -129,6 +138,7 @@ export const FormField = (props: FormFieldProps) => {
|
|
|
129
138
|
label,
|
|
130
139
|
placeholder,
|
|
131
140
|
layout,
|
|
141
|
+
db,
|
|
132
142
|
...fieldState,
|
|
133
143
|
};
|
|
134
144
|
|
|
@@ -169,15 +179,23 @@ export const FormField = (props: FormFieldProps) => {
|
|
|
169
179
|
// Select field.
|
|
170
180
|
//
|
|
171
181
|
|
|
172
|
-
const options =
|
|
182
|
+
const options = getSelectOptions(type);
|
|
173
183
|
if (options) {
|
|
184
|
+
// Resolve labels from projection metadata when available.
|
|
185
|
+
const fieldProjections = projection?.getFieldProjections();
|
|
186
|
+
const fieldProjection = fieldProjections?.find((fp) => fp.field.path === name);
|
|
187
|
+
const selectOptions = fieldProjection?.props.options;
|
|
188
|
+
|
|
174
189
|
return (
|
|
175
190
|
<SelectField
|
|
176
191
|
{...fieldProps}
|
|
177
|
-
options={options.map((option) =>
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
192
|
+
options={options.map((option) => {
|
|
193
|
+
const selectOption = selectOptions?.find((so) => so.id === globalThis.String(option));
|
|
194
|
+
return {
|
|
195
|
+
value: option,
|
|
196
|
+
label: selectOption?.title ?? option.toString(),
|
|
197
|
+
};
|
|
198
|
+
})}
|
|
181
199
|
/>
|
|
182
200
|
);
|
|
183
201
|
}
|
|
@@ -188,16 +206,18 @@ export const FormField = (props: FormFieldProps) => {
|
|
|
188
206
|
|
|
189
207
|
const refProps = getRefProps(type);
|
|
190
208
|
if (refProps) {
|
|
209
|
+
const isCreateTarget = !createTypename || refProps.typename === createTypename;
|
|
191
210
|
return (
|
|
192
211
|
<RefField
|
|
193
212
|
{...fieldProps}
|
|
194
213
|
{...refProps}
|
|
195
|
-
createOptionLabel={createOptionLabel}
|
|
196
|
-
createOptionIcon={createOptionIcon}
|
|
197
|
-
createInitialValuePath={createInitialValuePath}
|
|
214
|
+
createOptionLabel={isCreateTarget ? createOptionLabel : undefined}
|
|
215
|
+
createOptionIcon={isCreateTarget ? createOptionIcon : undefined}
|
|
216
|
+
createInitialValuePath={isCreateTarget ? createInitialValuePath : undefined}
|
|
217
|
+
createFieldMap={isCreateTarget ? createFieldMap : undefined}
|
|
198
218
|
db={db}
|
|
199
|
-
|
|
200
|
-
getOptions={
|
|
219
|
+
useSchema={schemaHook}
|
|
220
|
+
getOptions={getOptions}
|
|
201
221
|
onCreate={onCreate}
|
|
202
222
|
/>
|
|
203
223
|
);
|
|
@@ -229,8 +249,8 @@ export const FormField = (props: FormFieldProps) => {
|
|
|
229
249
|
createOptionIcon={createOptionIcon}
|
|
230
250
|
createInitialValuePath={createInitialValuePath}
|
|
231
251
|
db={db}
|
|
232
|
-
|
|
233
|
-
getOptions={
|
|
252
|
+
useSchema={schemaHook}
|
|
253
|
+
getOptions={getOptions}
|
|
234
254
|
onCreate={onCreate}
|
|
235
255
|
/>
|
|
236
256
|
);
|
|
@@ -277,7 +297,7 @@ const getFormField = ({ type, format }: FormFieldComponentProps): FormFieldCompo
|
|
|
277
297
|
}
|
|
278
298
|
};
|
|
279
299
|
|
|
280
|
-
const
|
|
300
|
+
const getSelectOptions = (ast: SchemaAST.AST): Format.Options[] | undefined => {
|
|
281
301
|
if (isLiteralUnion(ast)) {
|
|
282
302
|
return ast.types.map((type) => type.literal).filter((v): v is string | number => v !== null);
|
|
283
303
|
}
|
|
@@ -10,7 +10,7 @@ import { withLayout, withTheme } from '@dxos/react-ui/testing';
|
|
|
10
10
|
|
|
11
11
|
import { type FormFieldComponentProps, FormFieldWrapper } from './FormFieldComponent';
|
|
12
12
|
|
|
13
|
-
const
|
|
13
|
+
const DefaultStory = ({
|
|
14
14
|
type,
|
|
15
15
|
placeholder,
|
|
16
16
|
readonly,
|
|
@@ -24,7 +24,7 @@ const Component = ({
|
|
|
24
24
|
);
|
|
25
25
|
|
|
26
26
|
return (
|
|
27
|
-
<div className='
|
|
27
|
+
<div className='p-form-padding'>
|
|
28
28
|
<FormFieldWrapper<string> {...props}>
|
|
29
29
|
{({ value = '' }) => (
|
|
30
30
|
<Input.TextInput
|
|
@@ -41,9 +41,9 @@ const Component = ({
|
|
|
41
41
|
);
|
|
42
42
|
};
|
|
43
43
|
|
|
44
|
-
const meta: Meta<typeof
|
|
44
|
+
const meta: Meta<typeof DefaultStory> = {
|
|
45
45
|
title: 'ui/react-ui-form/FormFieldComponent',
|
|
46
|
-
component:
|
|
46
|
+
component: DefaultStory,
|
|
47
47
|
decorators: [withTheme(), withLayout({ layout: 'column' })],
|
|
48
48
|
parameters: {
|
|
49
49
|
layout: 'fullscreen',
|
|
@@ -13,14 +13,13 @@ import React, {
|
|
|
13
13
|
type ReactNode,
|
|
14
14
|
} from 'react';
|
|
15
15
|
|
|
16
|
+
import { type Database } from '@dxos/echo';
|
|
16
17
|
import { type Format } from '@dxos/echo/internal';
|
|
17
18
|
import { Icon, Input, Tooltip } from '@dxos/react-ui';
|
|
18
|
-
import {
|
|
19
|
+
import { inputTextLabel } from '@dxos/ui-theme';
|
|
19
20
|
|
|
20
21
|
import { type FormFieldStatus } from '../../hooks';
|
|
21
22
|
|
|
22
|
-
const labelSpacing = 'mbs-inputSpacingBlock mbe-labelSpacingBlock';
|
|
23
|
-
|
|
24
23
|
/**
|
|
25
24
|
* Dynamic props passed to input components.
|
|
26
25
|
*/
|
|
@@ -33,7 +32,6 @@ export type FormFieldStateProps<T = any> = {
|
|
|
33
32
|
|
|
34
33
|
/**
|
|
35
34
|
* Presentation mode.
|
|
36
|
-
*
|
|
37
35
|
* - full: Show label, control, and status.
|
|
38
36
|
* - compact: Show label and control.
|
|
39
37
|
* - inline: Control only.
|
|
@@ -53,6 +51,8 @@ export type FormFieldComponentProps<T = any> = {
|
|
|
53
51
|
placeholder?: string;
|
|
54
52
|
autoFocus?: boolean;
|
|
55
53
|
layout?: Presentation;
|
|
54
|
+
/** Database the form is editing against; populated for fields whose value is an ECHO object/Ref. */
|
|
55
|
+
db?: Database.Database;
|
|
56
56
|
} & FormFieldStateProps<T>;
|
|
57
57
|
|
|
58
58
|
/**
|
|
@@ -80,14 +80,14 @@ export type FormFieldLabelProps = {
|
|
|
80
80
|
export const FormFieldLabel = ({ label, error, readonly, asChild }: FormFieldLabelProps) => {
|
|
81
81
|
const Label = readonly || asChild ? 'span' : Input.Label;
|
|
82
82
|
return (
|
|
83
|
-
|
|
84
|
-
<Label>{label}</Label>
|
|
83
|
+
<div role='none' className='flex items-center justify-between'>
|
|
84
|
+
<Label className={inputTextLabel}>{label}</Label>
|
|
85
85
|
{error && (
|
|
86
86
|
<Tooltip.Trigger asChild content={error} side='bottom'>
|
|
87
|
-
<Icon icon='ph--warning--regular' size={4} classNames=
|
|
87
|
+
<Icon icon='ph--warning--regular' size={4} classNames='text-error-text' />
|
|
88
88
|
</Tooltip.Trigger>
|
|
89
89
|
)}
|
|
90
|
-
|
|
90
|
+
</div>
|
|
91
91
|
);
|
|
92
92
|
};
|
|
93
93
|
|
|
@@ -158,8 +158,8 @@ export class FormFieldErrorBoundary extends Component<FormFieldErrorBoundaryProp
|
|
|
158
158
|
override render() {
|
|
159
159
|
if (this.state.error) {
|
|
160
160
|
return (
|
|
161
|
-
<div className='flex gap-2 border border-
|
|
162
|
-
<span className='bg-
|
|
161
|
+
<div className='flex gap-2 border border-rose-fill font-mono text-sm'>
|
|
162
|
+
<span className='bg-rose-fill text-surface-text px-1 font-thin'>ERROR</span>
|
|
163
163
|
{String(this.props.path?.join('.'))}
|
|
164
164
|
</div>
|
|
165
165
|
);
|