@dxos/react-ui-form 0.8.4-main.3eb6e50203 → 0.8.4-main.3fbcb4aa9b
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 +1134 -682
- 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 +1134 -682
- 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 +101 -54
- package/src/components/Form/FormField.tsx +39 -19
- package/src/components/Form/FormFieldComponent.stories.tsx +4 -4
- package/src/components/Form/FormFieldComponent.tsx +11 -11
- package/src/components/Form/FormFieldSet.tsx +121 -104
- package/src/components/Form/fields/ArrayField.tsx +10 -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 +85 -45
- 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 +13 -13
- 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 +33 -187
- package/src/components/Settings/SettingsFieldSet.tsx +257 -0
- package/src/components/Settings/SettingsItem.tsx +58 -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 -50
- 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,14 @@ 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
|
+
className={mx(withColumn.center(), 'grid grid-flow-col gap-form-gap auto-cols-fr py-form-padding', classNames)}
|
|
270
|
+
>
|
|
251
271
|
{onCancel && (
|
|
252
272
|
<IconButton
|
|
253
273
|
icon='ph--x--regular'
|
|
254
274
|
iconEnd
|
|
255
|
-
label={t('cancel
|
|
275
|
+
label={t('cancel-button.label')}
|
|
256
276
|
onClick={onCancel}
|
|
257
277
|
data-testid='cancel-button'
|
|
258
278
|
/>
|
|
@@ -264,7 +284,7 @@ const FormActions = ({ classNames }: FormActionsProps) => {
|
|
|
264
284
|
disabled={!canSave}
|
|
265
285
|
icon='ph--check--regular'
|
|
266
286
|
iconEnd
|
|
267
|
-
label={t('save
|
|
287
|
+
label={t('save-button.label')}
|
|
268
288
|
onClick={onSave}
|
|
269
289
|
data-testid='save-button'
|
|
270
290
|
/>
|
|
@@ -275,6 +295,31 @@ const FormActions = ({ classNames }: FormActionsProps) => {
|
|
|
275
295
|
|
|
276
296
|
FormActions.displayName = FORM_ACTIONS_NAME;
|
|
277
297
|
|
|
298
|
+
//
|
|
299
|
+
// Section
|
|
300
|
+
//
|
|
301
|
+
|
|
302
|
+
const FORM_SECTION_NAME = 'Form.Section';
|
|
303
|
+
|
|
304
|
+
type FormSectionProps = ThemedClassName<{ label: string; description?: string }>;
|
|
305
|
+
|
|
306
|
+
const FormSection = composable<HTMLDivElement, FormSectionProps>(
|
|
307
|
+
({ children, label, description, ...props }, forwardedRef) => {
|
|
308
|
+
return (
|
|
309
|
+
<div
|
|
310
|
+
{...composableProps(props, { classNames: 'flex flex-col pt-form-section-gap first:pt-0' })}
|
|
311
|
+
ref={forwardedRef}
|
|
312
|
+
>
|
|
313
|
+
<h2 className='text-lg'>{label}</h2>
|
|
314
|
+
{description && <p className='text-description'>{description}</p>}
|
|
315
|
+
{children}
|
|
316
|
+
</div>
|
|
317
|
+
);
|
|
318
|
+
},
|
|
319
|
+
);
|
|
320
|
+
|
|
321
|
+
FormSection.displayName = FORM_SECTION_NAME;
|
|
322
|
+
|
|
278
323
|
//
|
|
279
324
|
// Submit
|
|
280
325
|
//
|
|
@@ -296,14 +341,14 @@ const FormSubmit = ({ classNames, label, icon, disabled }: FormSubmitProps) => {
|
|
|
296
341
|
}
|
|
297
342
|
|
|
298
343
|
return (
|
|
299
|
-
<div
|
|
344
|
+
<div className={mx('flex w-full pt-form-padding', classNames)}>
|
|
300
345
|
<IconButton
|
|
301
|
-
classNames='
|
|
346
|
+
classNames='w-full'
|
|
302
347
|
type='submit'
|
|
303
348
|
variant='primary'
|
|
304
349
|
disabled={disabled ?? !canSave}
|
|
305
350
|
icon={icon ?? 'ph--check--regular'}
|
|
306
|
-
label={label ?? t('save
|
|
351
|
+
label={label ?? t('save-button.label')}
|
|
307
352
|
onClick={onSave}
|
|
308
353
|
data-testid='save-button'
|
|
309
354
|
/>
|
|
@@ -322,10 +367,11 @@ export const Form = {
|
|
|
322
367
|
Root: FormRoot,
|
|
323
368
|
Viewport: FormViewport,
|
|
324
369
|
Content: FormContent,
|
|
370
|
+
Section: FormSection,
|
|
325
371
|
FieldSet: FormFieldSet,
|
|
372
|
+
Label: FormFieldLabel,
|
|
326
373
|
Actions: FormActions,
|
|
327
374
|
Submit: FormSubmit,
|
|
328
|
-
Label: FormFieldLabel,
|
|
329
375
|
};
|
|
330
376
|
|
|
331
377
|
export { useFormContext, useFormValues, useFormFieldState };
|
|
@@ -334,8 +380,9 @@ export type {
|
|
|
334
380
|
FormRootProps,
|
|
335
381
|
FormViewportProps,
|
|
336
382
|
FormContentProps,
|
|
383
|
+
FormSectionProps,
|
|
337
384
|
FormFieldSetProps,
|
|
385
|
+
FormFieldLabelProps,
|
|
338
386
|
FormActionsProps,
|
|
339
387
|
FormSubmitProps,
|
|
340
|
-
FormFieldLabelProps as LabelProps,
|
|
341
388
|
};
|
|
@@ -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 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
|
|
|
@@ -116,7 +116,7 @@ export const FormFieldWrapper = <T,>(props: FormFieldWrapperProps<T>) => {
|
|
|
116
116
|
const str = String(value ?? '');
|
|
117
117
|
|
|
118
118
|
return (
|
|
119
|
-
<div
|
|
119
|
+
<div className='contents'>
|
|
120
120
|
<Input.Root validationValence={status}>
|
|
121
121
|
{layout !== 'inline' && <FormFieldLabel error={error} readonly={readonly} label={label} />}
|
|
122
122
|
{layout === 'static' ? <p>{str}</p> : children ? children({ value }) : null}
|
|
@@ -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
|
);
|