@dxos/react-ui-form 0.8.4-main.2e9d522 → 0.8.4-main.3c1ae3b
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 +2138 -1738
- package/dist/lib/browser/index.mjs.map +4 -4
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/node-esm/index.mjs +2138 -1738
- 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 +2 -2
- 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 +5 -5
- package/dist/types/src/components/FieldEditor/FieldEditor.d.ts.map +1 -1
- package/dist/types/src/components/FieldEditor/FieldEditor.stories.d.ts +82 -5
- package/dist/types/src/components/FieldEditor/FieldEditor.stories.d.ts.map +1 -1
- package/dist/types/src/components/Form/Form-old.stories.d.ts +172 -0
- package/dist/types/src/components/Form/Form-old.stories.d.ts.map +1 -0
- package/dist/types/src/components/Form/Form.d.ts +78 -25
- package/dist/types/src/components/Form/Form.d.ts.map +1 -1
- package/dist/types/src/components/Form/Form.stories.d.ts +123 -51
- 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 +18 -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 +41 -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 +83 -8
- 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 +3 -2
- 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/hooks/useQueryRefOptions.d.ts +13 -6
- package/dist/types/src/hooks/useQueryRefOptions.d.ts.map +1 -1
- package/dist/types/src/hooks/useRefQueryOptions.d.ts +8 -0
- package/dist/types/src/hooks/useRefQueryOptions.d.ts.map +1 -0
- package/dist/types/src/translations.d.ts +17 -8
- package/dist/types/src/translations.d.ts.map +1 -1
- 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 +1 -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 +43 -36
- package/src/components/ControlSection/ControlSection.stories.tsx +11 -14
- package/src/components/ControlSection/ControlSection.tsx +26 -12
- package/src/components/FieldEditor/FieldEditor.stories.tsx +20 -20
- package/src/components/FieldEditor/FieldEditor.test.tsx +3 -2
- package/src/components/FieldEditor/FieldEditor.tsx +75 -77
- package/src/components/Form/Form-old.stories.tsx +314 -0
- package/src/components/Form/Form.stories.tsx +163 -219
- package/src/components/Form/Form.tsx +306 -98
- package/src/components/Form/FormField.tsx +280 -0
- package/src/components/Form/FormFieldComponent.stories.tsx +65 -0
- package/src/components/Form/FormFieldComponent.tsx +173 -0
- package/src/components/Form/FormFieldSet.tsx +124 -0
- package/src/components/Form/README.md +3 -0
- package/src/components/Form/fields/ArrayField.tsx +154 -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 +206 -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} +15 -8
- package/src/components/Form/fields/index.ts +15 -0
- package/src/components/Form/index.ts +3 -4
- package/src/components/ObjectPicker/ObjectPicker.stories.tsx +207 -0
- package/src/components/ObjectPicker/ObjectPicker.tsx +166 -0
- package/src/components/ObjectPicker/index.ts +5 -0
- package/src/components/ViewEditor/ViewEditor.stories.tsx +131 -61
- package/src/components/ViewEditor/ViewEditor.test.tsx +19 -17
- package/src/components/ViewEditor/ViewEditor.tsx +277 -197
- package/src/components/index.ts +1 -2
- package/src/components/testing.tsx +20 -10
- package/src/hooks/index.ts +3 -2
- package/src/hooks/useFormHandler.ts +348 -0
- package/src/hooks/useKeyHandler.tsx +37 -0
- package/src/hooks/useQueryRefOptions.ts +49 -32
- package/src/hooks/useRefQueryOptions.ts +37 -0
- package/src/translations.ts +20 -11
- package/src/util/field.ts +33 -43
- package/src/util/formatting.ts +22 -22
- package/src/util/index.ts +1 -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/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/useRefQueryLookupHandler.ts +0 -39
- package/src/util/schema.ts +0 -14
|
@@ -1,108 +1,316 @@
|
|
|
1
1
|
//
|
|
2
|
-
// Copyright
|
|
3
|
-
//
|
|
4
|
-
|
|
5
|
-
import {
|
|
6
|
-
import
|
|
7
|
-
|
|
8
|
-
import { type
|
|
9
|
-
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
|
|
13
|
-
import {
|
|
14
|
-
|
|
15
|
-
import {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
2
|
+
// Copyright 2025 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { createContext } from '@radix-ui/react-context';
|
|
6
|
+
import * as Schema from 'effect/Schema';
|
|
7
|
+
import type * as SchemaAST from 'effect/SchemaAST';
|
|
8
|
+
import React, { type PropsWithChildren, useMemo, useRef } from 'react';
|
|
9
|
+
|
|
10
|
+
import { type AnyProperties } from '@dxos/echo/internal';
|
|
11
|
+
import { createJsonPath, getValue as getValue$ } from '@dxos/effect';
|
|
12
|
+
import { IconButton, type IconButtonProps, ScrollArea, type ThemedClassName, useTranslation } from '@dxos/react-ui';
|
|
13
|
+
import { mx } from '@dxos/react-ui-theme';
|
|
14
|
+
|
|
15
|
+
import {
|
|
16
|
+
type FormHandler,
|
|
17
|
+
type FormHandlerProps,
|
|
18
|
+
type FormUpdateMeta,
|
|
19
|
+
useFormHandler,
|
|
20
|
+
useKeyHandler,
|
|
21
|
+
} from '../../hooks';
|
|
22
|
+
import { translationKey } from '../../translations';
|
|
23
|
+
|
|
24
|
+
import { FormFieldLabel, type FormFieldLabelProps, type FormFieldStateProps } from './FormFieldComponent';
|
|
25
|
+
import {
|
|
26
|
+
FormFieldSet as NaturalFormFieldSet,
|
|
27
|
+
type FormFieldSetProps as NaturalFormFieldSetProps,
|
|
28
|
+
} from './FormFieldSet';
|
|
29
|
+
|
|
30
|
+
// New features/polish
|
|
31
|
+
// [x] Unify readonly/inline modes
|
|
32
|
+
// [ ] Don't call save/autoSave if value hasn't changed
|
|
33
|
+
// [ ] Fix onCancel (restore values)
|
|
34
|
+
// [ ] Fix useSchema Type.Obj.Any cast
|
|
35
|
+
// [ ] Remove @dxos/echo-db deps
|
|
36
|
+
// [ ] TableCellEditor (handleEnter/ModalController).
|
|
37
|
+
// [ ] Use FormFieldWrapper uniformly
|
|
38
|
+
// [ ] Inline tables for object arrays
|
|
39
|
+
// [ ] Defer query until popover
|
|
40
|
+
// [ ] Omit id from sub properties.
|
|
41
|
+
// [x] Refs
|
|
42
|
+
// [x] Single-select (fix popover)
|
|
43
|
+
// [x] Multi-select (array)
|
|
44
|
+
// [ ] auto save doesn't work for combobox + select due to only firing on blur (workaround is to use onValuesChanged)
|
|
45
|
+
|
|
46
|
+
// TODO(burdon): Move to @dxos/schema (re-export here).
|
|
47
|
+
export type ExcludeId<S extends Schema.Schema.AnyNoContext> = Omit<Schema.Schema.Type<S>, 'id'>;
|
|
48
|
+
|
|
49
|
+
// TODO(burdon): Move to @dxos/schema (re-export here).
|
|
50
|
+
export const omitId = <S extends Schema.Schema.AnyNoContext>(schema: S): Schema.Schema<ExcludeId<S>, ExcludeId<S>> =>
|
|
51
|
+
schema.pipe(Schema.omit('id')) as any;
|
|
52
|
+
|
|
53
|
+
//
|
|
54
|
+
// Context
|
|
55
|
+
//
|
|
56
|
+
|
|
57
|
+
type FormContextValue<T extends AnyProperties = any> = {
|
|
58
|
+
/**
|
|
59
|
+
* Form handler.
|
|
60
|
+
*/
|
|
61
|
+
form: FormHandler<T>;
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Show debug info.
|
|
65
|
+
*/
|
|
66
|
+
debug?: boolean;
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Testing.
|
|
70
|
+
*/
|
|
71
|
+
testId?: string;
|
|
72
|
+
} & Pick<NaturalFormFieldSetProps<T>, 'readonly' | 'layout' | 'fieldMap' | 'fieldProvider'>;
|
|
73
|
+
|
|
74
|
+
const [FormContextProvider, useFormContext] = createContext<FormContextValue>('Form');
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Get the current form values.
|
|
78
|
+
*/
|
|
79
|
+
const useFormValues = <T extends AnyProperties>(componentName: string, path: (string | number)[] = []): T => {
|
|
80
|
+
const jsonPath = createJsonPath(path);
|
|
81
|
+
const {
|
|
82
|
+
form: { values },
|
|
83
|
+
} = useFormContext(componentName);
|
|
84
|
+
|
|
85
|
+
return getValue$(values, jsonPath) as T;
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Get the state props for the given field.
|
|
90
|
+
*/
|
|
91
|
+
const useFormFieldState = (componentName: string, path: (string | number)[] = []): FormFieldStateProps => {
|
|
92
|
+
const stablePath = useMemo(() => path, [Array.isArray(path) ? path.join('.') : path]);
|
|
93
|
+
const {
|
|
94
|
+
form: { getStatus, getValue, onBlur, onValueChange },
|
|
95
|
+
} = useFormContext(componentName);
|
|
96
|
+
|
|
97
|
+
return useMemo(
|
|
98
|
+
() => ({
|
|
99
|
+
getStatus: () => getStatus(stablePath),
|
|
100
|
+
getValue: () => getValue(stablePath),
|
|
101
|
+
onBlur: () => onBlur(stablePath),
|
|
102
|
+
onValueChange: (ast: SchemaAST.AST, value: any) => onValueChange(stablePath, ast, value),
|
|
103
|
+
}),
|
|
104
|
+
[getStatus, getValue, onBlur, onValueChange, stablePath],
|
|
105
|
+
);
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
//
|
|
109
|
+
// Root
|
|
110
|
+
//
|
|
111
|
+
|
|
112
|
+
type FormRootProps<T extends AnyProperties = AnyProperties> = PropsWithChildren<
|
|
113
|
+
{
|
|
114
|
+
/**
|
|
115
|
+
* Called when the form is submitted and passes validation.
|
|
116
|
+
*/
|
|
117
|
+
onSave?: (values: T, meta: FormUpdateMeta<T>) => void;
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Called when the form is canceled to abandon/undo any pending changes.
|
|
121
|
+
*/
|
|
122
|
+
onCancel?: () => void;
|
|
123
|
+
} & Omit<FormContextValue<T>, 'form'> &
|
|
124
|
+
Pick<FormHandlerProps<T>, 'schema' | 'autoSave' | 'values' | 'defaultValues' | 'onValidate' | 'onValuesChanged'> &
|
|
125
|
+
Omit<NaturalFormFieldSetProps<T>, 'schema' | 'path'>
|
|
126
|
+
>;
|
|
127
|
+
|
|
128
|
+
const FormRoot = <T extends AnyProperties = AnyProperties>({
|
|
129
|
+
children,
|
|
52
130
|
schema,
|
|
53
|
-
|
|
54
|
-
onValidate,
|
|
131
|
+
values,
|
|
55
132
|
onSave,
|
|
133
|
+
onCancel,
|
|
56
134
|
...props
|
|
57
|
-
}:
|
|
58
|
-
const
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
135
|
+
}: FormRootProps<T>) => {
|
|
136
|
+
const form = useFormHandler({ schema, values, onSave, onCancel, ...props });
|
|
137
|
+
|
|
138
|
+
return (
|
|
139
|
+
<FormContextProvider form={form} {...props}>
|
|
140
|
+
{children}
|
|
141
|
+
</FormContextProvider>
|
|
142
|
+
);
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
FormRoot.displayName = 'Form.Root';
|
|
146
|
+
|
|
147
|
+
//
|
|
148
|
+
// Viewport
|
|
149
|
+
//
|
|
150
|
+
|
|
151
|
+
type FormViewportProps = ThemedClassName<PropsWithChildren<{}>>;
|
|
152
|
+
|
|
153
|
+
const FormViewport = ({ classNames, children }: FormViewportProps) => {
|
|
154
|
+
return (
|
|
155
|
+
<ScrollArea.Root>
|
|
156
|
+
<ScrollArea.Viewport classNames={['plb-cardSpacingBlock', classNames]}>{children}</ScrollArea.Viewport>
|
|
157
|
+
<ScrollArea.Scrollbar orientation='vertical'>
|
|
158
|
+
<ScrollArea.Thumb />
|
|
159
|
+
</ScrollArea.Scrollbar>
|
|
160
|
+
</ScrollArea.Root>
|
|
161
|
+
);
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
FormViewport.displayName = 'Form.Viewport';
|
|
165
|
+
|
|
166
|
+
//
|
|
167
|
+
// Content
|
|
168
|
+
//
|
|
169
|
+
|
|
170
|
+
type FormContentProps = ThemedClassName<PropsWithChildren<{}>>;
|
|
171
|
+
|
|
172
|
+
// TOOD(burdon): Figure out nesting (indent and testId).
|
|
173
|
+
const FormContent = ({ classNames, children }: FormContentProps) => {
|
|
174
|
+
const { form, testId } = useFormContext(FormContent.displayName);
|
|
175
|
+
const ref = useRef<HTMLDivElement>(null);
|
|
176
|
+
useKeyHandler(ref.current, form);
|
|
72
177
|
|
|
73
178
|
return (
|
|
74
|
-
<
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
onValuesChanged={onValuesChanged}
|
|
80
|
-
onValidate={onValidate}
|
|
81
|
-
onValid={handleValid}
|
|
82
|
-
onSave={onSave}
|
|
179
|
+
<div
|
|
180
|
+
ref={ref}
|
|
181
|
+
role='form'
|
|
182
|
+
className={mx('flex flex-col is-full pli-cardSpacingInline', classNames)}
|
|
183
|
+
data-testid={testId}
|
|
83
184
|
>
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
185
|
+
{children}
|
|
186
|
+
</div>
|
|
187
|
+
);
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
FormContent.displayName = 'Form.Content';
|
|
191
|
+
|
|
192
|
+
//
|
|
193
|
+
// FieldSet
|
|
194
|
+
//
|
|
195
|
+
|
|
196
|
+
type FormFieldSetProps = ThemedClassName<{}>;
|
|
197
|
+
|
|
198
|
+
const FormFieldSet = ({ classNames }: FormFieldSetProps) => {
|
|
199
|
+
const { form, ...props } = useFormContext(FormFieldSet.displayName);
|
|
200
|
+
|
|
201
|
+
return <NaturalFormFieldSet classNames={classNames} schema={form.schema} {...props} />;
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
FormFieldSet.displayName = 'Form.FieldSet';
|
|
205
|
+
|
|
206
|
+
//
|
|
207
|
+
// Actions
|
|
208
|
+
//
|
|
209
|
+
|
|
210
|
+
type FormActionsProps = ThemedClassName<{}>;
|
|
211
|
+
|
|
212
|
+
const FormActions = ({ classNames }: FormActionsProps) => {
|
|
213
|
+
const { t } = useTranslation(translationKey);
|
|
214
|
+
const {
|
|
215
|
+
form: { isValid, onSave, onCancel },
|
|
216
|
+
readonly,
|
|
217
|
+
layout,
|
|
218
|
+
} = useFormContext(FormActions.displayName);
|
|
219
|
+
|
|
220
|
+
if (readonly || layout === 'static') {
|
|
221
|
+
return null;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// TODO(burdon): Currently onCancel is a no-op; implement "revert values".
|
|
225
|
+
// Deprecate FormSubmit ans use FormActions without Cancel button if no callback is supplied.
|
|
226
|
+
|
|
227
|
+
return (
|
|
228
|
+
<div role='none' className={mx('grid grid-flow-col auto-cols-fr gap-2 pbs-cardSpacingBlock', classNames)}>
|
|
229
|
+
{onCancel && (
|
|
230
|
+
<IconButton
|
|
231
|
+
icon='ph--x--regular'
|
|
232
|
+
iconEnd
|
|
233
|
+
label={t('cancel button label')}
|
|
234
|
+
onClick={onCancel}
|
|
235
|
+
data-testid='cancel-button'
|
|
236
|
+
/>
|
|
237
|
+
)}
|
|
238
|
+
{onSave && (
|
|
239
|
+
<IconButton
|
|
240
|
+
type='submit'
|
|
241
|
+
variant='primary'
|
|
242
|
+
disabled={!isValid}
|
|
243
|
+
icon='ph--check--regular'
|
|
244
|
+
iconEnd
|
|
245
|
+
label={t('save button label')}
|
|
246
|
+
onClick={onSave}
|
|
247
|
+
data-testid='save-button'
|
|
101
248
|
/>
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
249
|
+
)}
|
|
250
|
+
</div>
|
|
251
|
+
);
|
|
252
|
+
};
|
|
253
|
+
|
|
254
|
+
FormActions.displayName = 'Form.Actions';
|
|
255
|
+
|
|
256
|
+
//
|
|
257
|
+
// Submit
|
|
258
|
+
//
|
|
259
|
+
|
|
260
|
+
type FormSubmitProps = ThemedClassName<Partial<Pick<IconButtonProps, 'icon' | 'label'>>>;
|
|
261
|
+
|
|
262
|
+
const FormSubmit = ({ classNames, label, icon }: FormSubmitProps) => {
|
|
263
|
+
const { t } = useTranslation(translationKey);
|
|
264
|
+
const {
|
|
265
|
+
form: { isValid, onSave },
|
|
266
|
+
readonly,
|
|
267
|
+
layout,
|
|
268
|
+
} = useFormContext(FormSubmit.displayName);
|
|
269
|
+
|
|
270
|
+
if (readonly || layout === 'static') {
|
|
271
|
+
return null;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
return (
|
|
275
|
+
<div role='none' className={mx('flex is-full pbs-cardSpacingBlock', classNames)}>
|
|
276
|
+
<IconButton
|
|
277
|
+
classNames='is-full'
|
|
278
|
+
type='submit'
|
|
279
|
+
variant='primary'
|
|
280
|
+
disabled={!isValid}
|
|
281
|
+
icon={icon ?? 'ph--check--regular'}
|
|
282
|
+
label={label ?? t('save button label')}
|
|
283
|
+
onClick={onSave}
|
|
284
|
+
data-testid='save-button'
|
|
285
|
+
/>
|
|
286
|
+
</div>
|
|
107
287
|
);
|
|
108
288
|
};
|
|
289
|
+
|
|
290
|
+
FormSubmit.displayName = 'Form.Submit';
|
|
291
|
+
|
|
292
|
+
//
|
|
293
|
+
// Form
|
|
294
|
+
// https://www.radix-ui.com/primitives/docs/guides/composition
|
|
295
|
+
//
|
|
296
|
+
|
|
297
|
+
export const Form = {
|
|
298
|
+
Root: FormRoot,
|
|
299
|
+
Viewport: FormViewport,
|
|
300
|
+
Content: FormContent,
|
|
301
|
+
FieldSet: FormFieldSet,
|
|
302
|
+
Actions: FormActions,
|
|
303
|
+
Submit: FormSubmit,
|
|
304
|
+
Label: FormFieldLabel,
|
|
305
|
+
};
|
|
306
|
+
|
|
307
|
+
export { useFormContext, useFormValues, useFormFieldState };
|
|
308
|
+
|
|
309
|
+
export type {
|
|
310
|
+
FormRootProps,
|
|
311
|
+
FormViewportProps,
|
|
312
|
+
FormContentProps,
|
|
313
|
+
FormFieldSetProps,
|
|
314
|
+
FormActionsProps,
|
|
315
|
+
FormFieldLabelProps as LabelProps,
|
|
316
|
+
};
|
|
@@ -0,0 +1,280 @@
|
|
|
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<
|
|
82
|
+
RefFieldProps,
|
|
83
|
+
| 'autoFocus'
|
|
84
|
+
| 'readonly'
|
|
85
|
+
| 'layout'
|
|
86
|
+
| 'createSchema'
|
|
87
|
+
| 'createOptionLabel'
|
|
88
|
+
| 'createOptionIcon'
|
|
89
|
+
| 'createInitialValuePath'
|
|
90
|
+
| 'onCreate'
|
|
91
|
+
| 'onQueryRefOptions'
|
|
92
|
+
>;
|
|
93
|
+
|
|
94
|
+
export const FormField = (props: FormFieldProps) => {
|
|
95
|
+
const {
|
|
96
|
+
type,
|
|
97
|
+
name,
|
|
98
|
+
path,
|
|
99
|
+
projection,
|
|
100
|
+
fieldMap,
|
|
101
|
+
fieldProvider,
|
|
102
|
+
readonly,
|
|
103
|
+
layout,
|
|
104
|
+
createSchema,
|
|
105
|
+
createOptionLabel,
|
|
106
|
+
createOptionIcon,
|
|
107
|
+
createInitialValuePath,
|
|
108
|
+
onCreate,
|
|
109
|
+
onQueryRefOptions,
|
|
110
|
+
} = props;
|
|
111
|
+
const { t } = useTranslation(translationKey);
|
|
112
|
+
const title = getAnnotation<string>(SchemaAST.TitleAnnotationId)(type);
|
|
113
|
+
const description = getAnnotation<string>(SchemaAST.DescriptionAnnotationId)(type);
|
|
114
|
+
const examples = getAnnotation<string[]>(SchemaAST.ExamplesAnnotationId)(type);
|
|
115
|
+
|
|
116
|
+
const label = useMemo(() => title ?? String.capitalize(name), [title, name]);
|
|
117
|
+
const placeholder = useMemo(
|
|
118
|
+
() => (examples?.length ? `${t('example placeholder')}: ${examples[0]}` : (description ?? label)),
|
|
119
|
+
[examples, description, label],
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
const fieldState = useFormFieldState(FormField.displayName, path);
|
|
123
|
+
const fieldProps: FormFieldComponentProps = {
|
|
124
|
+
type,
|
|
125
|
+
format: Format.FormatAnnotation.getFromAst(type).pipe((annotation) => Option.getOrUndefined(annotation)),
|
|
126
|
+
readonly,
|
|
127
|
+
label,
|
|
128
|
+
placeholder,
|
|
129
|
+
layout,
|
|
130
|
+
...fieldState,
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
//
|
|
134
|
+
// Custom field.
|
|
135
|
+
//
|
|
136
|
+
|
|
137
|
+
const jsonPath = createJsonPath(path ?? []);
|
|
138
|
+
const CustomField = fieldMap?.[jsonPath];
|
|
139
|
+
if (CustomField) {
|
|
140
|
+
return <CustomField {...fieldProps} />;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// TODO(burdon): Expensive to create schema each time; pass AST?
|
|
144
|
+
const component = fieldProvider?.({ schema: Schema.make(type), prop: name, fieldProps });
|
|
145
|
+
if (component) {
|
|
146
|
+
return component;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
//
|
|
150
|
+
// Array field.
|
|
151
|
+
//
|
|
152
|
+
|
|
153
|
+
if (isArrayType(type)) {
|
|
154
|
+
return <ArrayField fieldProps={fieldState} label={label} {...props} />;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
//
|
|
158
|
+
// Regular field.
|
|
159
|
+
//
|
|
160
|
+
|
|
161
|
+
const Field = getFormField(fieldProps);
|
|
162
|
+
if (Field) {
|
|
163
|
+
return <Field {...fieldProps} />;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
//
|
|
167
|
+
// Select field.
|
|
168
|
+
//
|
|
169
|
+
|
|
170
|
+
const options = getOptions(type);
|
|
171
|
+
if (options) {
|
|
172
|
+
return (
|
|
173
|
+
<SelectField
|
|
174
|
+
{...fieldProps}
|
|
175
|
+
options={options.map((option) => ({
|
|
176
|
+
value: option,
|
|
177
|
+
label: option.toString(),
|
|
178
|
+
}))}
|
|
179
|
+
/>
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
//
|
|
184
|
+
// Ref field.
|
|
185
|
+
//
|
|
186
|
+
|
|
187
|
+
const refProps = getRefProps(type);
|
|
188
|
+
if (refProps) {
|
|
189
|
+
return (
|
|
190
|
+
<RefField
|
|
191
|
+
{...fieldProps}
|
|
192
|
+
{...refProps}
|
|
193
|
+
createSchema={createSchema}
|
|
194
|
+
createOptionLabel={createOptionLabel}
|
|
195
|
+
createOptionIcon={createOptionIcon}
|
|
196
|
+
createInitialValuePath={createInitialValuePath}
|
|
197
|
+
onCreate={onCreate}
|
|
198
|
+
onQueryRefOptions={onQueryRefOptions}
|
|
199
|
+
/>
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
//
|
|
204
|
+
// Nested Object field.
|
|
205
|
+
//
|
|
206
|
+
|
|
207
|
+
if (isNestedType(type)) {
|
|
208
|
+
const baseNode = findNode(type, isDiscriminatedUnion);
|
|
209
|
+
const typeLiteral = baseNode
|
|
210
|
+
? getDiscriminatedType(baseNode, fieldState.getValue() as any)
|
|
211
|
+
: findNode(type, SchemaAST.isTypeLiteral);
|
|
212
|
+
|
|
213
|
+
if (typeLiteral) {
|
|
214
|
+
const schema = Schema.make(typeLiteral);
|
|
215
|
+
return (
|
|
216
|
+
<FormFieldSet
|
|
217
|
+
schema={schema}
|
|
218
|
+
path={path}
|
|
219
|
+
readonly={readonly}
|
|
220
|
+
layout={layout}
|
|
221
|
+
label={label}
|
|
222
|
+
projection={projection}
|
|
223
|
+
fieldMap={fieldMap}
|
|
224
|
+
fieldProvider={fieldProvider}
|
|
225
|
+
createOptionLabel={createOptionLabel}
|
|
226
|
+
createOptionIcon={createOptionIcon}
|
|
227
|
+
onCreate={onCreate}
|
|
228
|
+
onQueryRefOptions={onQueryRefOptions}
|
|
229
|
+
/>
|
|
230
|
+
);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
return null;
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
FormField.displayName = 'Form.FormField';
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Get property input component.
|
|
241
|
+
*/
|
|
242
|
+
const getFormField = ({ type, format }: FormFieldComponentProps): FormFieldComponent | undefined => {
|
|
243
|
+
//
|
|
244
|
+
// Standard formats.
|
|
245
|
+
//
|
|
246
|
+
|
|
247
|
+
const formatField = Match.value(format).pipe(
|
|
248
|
+
Match.withReturnType<FormFieldComponent | undefined>(),
|
|
249
|
+
Match.when(Format.TypeFormat.GeoPoint, () => GeoPointField),
|
|
250
|
+
Match.when(Format.TypeFormat.Markdown, () => MarkdownField),
|
|
251
|
+
Match.when(Format.TypeFormat.Text, () => TextAreaField),
|
|
252
|
+
Match.orElse(() => undefined),
|
|
253
|
+
);
|
|
254
|
+
if (formatField) {
|
|
255
|
+
return formatField;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
//
|
|
259
|
+
// Standard types.
|
|
260
|
+
//
|
|
261
|
+
|
|
262
|
+
switch (type._tag) {
|
|
263
|
+
// TODO(wittjosiah): Schema.Any is currently used to represent template inputs.
|
|
264
|
+
case 'AnyKeyword':
|
|
265
|
+
case 'StringKeyword':
|
|
266
|
+
return TextField;
|
|
267
|
+
case 'NumberKeyword':
|
|
268
|
+
return NumberField;
|
|
269
|
+
case 'BooleanKeyword':
|
|
270
|
+
return BooleanField;
|
|
271
|
+
}
|
|
272
|
+
};
|
|
273
|
+
|
|
274
|
+
const getOptions = (ast: SchemaAST.AST): Format.Options[] | undefined => {
|
|
275
|
+
if (isLiteralUnion(ast)) {
|
|
276
|
+
return ast.types.map((type) => type.literal).filter((v): v is string | number => v !== null);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
return Format.OptionsAnnotation.getFromAst(ast).pipe((annotation) => Option.getOrUndefined(annotation));
|
|
280
|
+
};
|