@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
|
@@ -2,126 +2,143 @@
|
|
|
2
2
|
// Copyright 2025 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import React, {
|
|
5
|
+
import React, { useMemo } from 'react';
|
|
6
6
|
|
|
7
7
|
import { type AnyProperties } from '@dxos/echo/internal';
|
|
8
8
|
import { type SchemaProperty } from '@dxos/effect';
|
|
9
|
-
import { type ThemedClassName } from '@dxos/react-ui';
|
|
10
|
-
import { mx } from '@dxos/ui-theme';
|
|
11
|
-
import { isTruthy } from '@dxos/util';
|
|
12
9
|
|
|
13
10
|
import { type FormHandlerProps } from '../../hooks';
|
|
14
11
|
import { getFormProperties } from '../../util';
|
|
15
|
-
|
|
16
12
|
import { useFormValues } from './Form';
|
|
17
13
|
import { FormField, type FormFieldProps } from './FormField';
|
|
18
14
|
import { FormFieldErrorBoundary, FormFieldLabel } from './FormFieldComponent';
|
|
19
15
|
|
|
20
|
-
export type FormFieldSetProps<T extends AnyProperties> = ThemedClassName<
|
|
21
|
-
{
|
|
22
|
-
label?: string;
|
|
23
|
-
exclude?: (props: SchemaProperty[]) => SchemaProperty[];
|
|
24
|
-
sort?: string[];
|
|
25
|
-
} & Pick<FormHandlerProps<T>, 'schema'> &
|
|
26
|
-
Pick<
|
|
27
|
-
FormFieldProps,
|
|
28
|
-
| 'path'
|
|
29
|
-
| 'autoFocus'
|
|
30
|
-
| 'readonly'
|
|
31
|
-
| 'layout'
|
|
32
|
-
| 'projection'
|
|
33
|
-
| 'fieldMap'
|
|
34
|
-
| 'fieldProvider'
|
|
35
|
-
| 'createOptionLabel'
|
|
36
|
-
| 'createOptionIcon'
|
|
37
|
-
| 'createInitialValuePath'
|
|
38
|
-
| 'db'
|
|
39
|
-
| 'schemaHook'
|
|
40
|
-
| 'getOptions'
|
|
41
|
-
| 'onCreate'
|
|
42
|
-
>
|
|
43
|
-
>;
|
|
44
|
-
|
|
45
16
|
const FORM_FIELDSET_NAME = 'Form.FieldSet';
|
|
46
17
|
|
|
47
|
-
export
|
|
48
|
-
|
|
49
|
-
|
|
18
|
+
export type FormFieldSetProps<T extends AnyProperties> = {
|
|
19
|
+
label?: string;
|
|
20
|
+
sort?: string[];
|
|
21
|
+
exclude?: (props: SchemaProperty[]) => SchemaProperty[];
|
|
22
|
+
} & Pick<FormHandlerProps<T>, 'schema'> &
|
|
23
|
+
Pick<
|
|
24
|
+
FormFieldProps,
|
|
25
|
+
| 'path'
|
|
26
|
+
| 'autoFocus'
|
|
27
|
+
| 'readonly'
|
|
28
|
+
| 'layout'
|
|
29
|
+
| 'projection'
|
|
30
|
+
| 'fieldMap'
|
|
31
|
+
| 'fieldProvider'
|
|
32
|
+
| 'createTypename'
|
|
33
|
+
| 'createOptionLabel'
|
|
34
|
+
| 'createOptionIcon'
|
|
35
|
+
| 'createInitialValuePath'
|
|
36
|
+
| 'createFieldMap'
|
|
37
|
+
| 'db'
|
|
38
|
+
| 'useSchema'
|
|
39
|
+
| 'getOptions'
|
|
40
|
+
| 'onCreate'
|
|
41
|
+
>;
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Renders a set of form fields derived from a schema object.
|
|
45
|
+
*/
|
|
46
|
+
export const FormFieldSet = ({
|
|
47
|
+
label,
|
|
48
|
+
schema,
|
|
49
|
+
readonly,
|
|
50
|
+
path,
|
|
51
|
+
sort,
|
|
52
|
+
exclude,
|
|
53
|
+
projection,
|
|
54
|
+
layout,
|
|
55
|
+
...props
|
|
56
|
+
}: FormFieldSetProps<any>) => {
|
|
57
|
+
const values = useFormValues(FORM_FIELDSET_NAME, path);
|
|
58
|
+
const properties = useFormFieldSetProperties({ schema, values, exclude, sort, projection });
|
|
59
|
+
if ((readonly || layout === 'static') && values == null) {
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return (
|
|
64
|
+
<>
|
|
65
|
+
{layout !== 'inline' && label && <FormFieldLabel label={label} asChild />}
|
|
66
|
+
{properties.map((property) => {
|
|
67
|
+
const name = property.name.toString();
|
|
68
|
+
return (
|
|
69
|
+
<FormFieldErrorBoundary key={name} path={[...(path ?? []), name]}>
|
|
70
|
+
<FormField
|
|
71
|
+
type={property.type}
|
|
72
|
+
name={name}
|
|
73
|
+
path={[...(path ?? []), name]}
|
|
74
|
+
readonly={readonly}
|
|
75
|
+
layout={layout}
|
|
76
|
+
projection={projection}
|
|
77
|
+
{...props}
|
|
78
|
+
/>
|
|
79
|
+
</FormFieldErrorBoundary>
|
|
80
|
+
);
|
|
81
|
+
})}
|
|
82
|
+
</>
|
|
83
|
+
);
|
|
84
|
+
};
|
|
50
85
|
|
|
51
|
-
|
|
52
|
-
// Remove values dep if can remove from getSchemaProperties.
|
|
53
|
-
const properties = useMemo(() => {
|
|
54
|
-
if (!schema) {
|
|
55
|
-
return [];
|
|
56
|
-
}
|
|
86
|
+
FormFieldSet.displayName = FORM_FIELDSET_NAME;
|
|
57
87
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
88
|
+
type UseFormFieldSetPropertiesParams = Pick<FormFieldSetProps<any>, 'schema' | 'exclude' | 'projection' | 'sort'> & {
|
|
89
|
+
values: AnyProperties | undefined;
|
|
90
|
+
sort?: string[];
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Resolves ordered schema properties for a field set (projection order, exclude, or sort).
|
|
95
|
+
*/
|
|
96
|
+
const useFormFieldSetProperties = ({
|
|
97
|
+
schema,
|
|
98
|
+
exclude,
|
|
99
|
+
projection,
|
|
100
|
+
values,
|
|
101
|
+
sort,
|
|
102
|
+
}: UseFormFieldSetPropertiesParams): SchemaProperty[] => {
|
|
103
|
+
// TODO(burdon): Updates on every value change.
|
|
104
|
+
// Remove values dep if can remove from getSchemaProperties.
|
|
105
|
+
return useMemo(() => {
|
|
106
|
+
if (!schema) {
|
|
107
|
+
return [];
|
|
108
|
+
}
|
|
78
109
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
110
|
+
// TODO(wittjosiah): Reconcile FormInputAnnotation with projection hidden properties & exclude function.
|
|
111
|
+
const schemaProps = getFormProperties(schema.ast);
|
|
112
|
+
const filteredProps = exclude ? exclude(schemaProps) : schemaProps;
|
|
113
|
+
|
|
114
|
+
// Use projection-based field management when view and projection are available.
|
|
115
|
+
if (projection) {
|
|
116
|
+
const fieldProjections = projection.getFieldProjections();
|
|
117
|
+
const hiddenProperties = new Set(projection.getHiddenProperties());
|
|
118
|
+
|
|
119
|
+
// Filter properties to only include visible ones and order by projection.
|
|
120
|
+
const visibleProps = filteredProps.filter((prop) => !hiddenProperties.has(prop.name.toString()));
|
|
121
|
+
const orderedProps: SchemaProperty[] = [];
|
|
122
|
+
|
|
123
|
+
// Add properties in projection field order.
|
|
124
|
+
for (const fieldProjection of fieldProjections) {
|
|
125
|
+
const fieldPath = String(fieldProjection.field.path);
|
|
126
|
+
const prop = visibleProps.find((prop) => prop.name === fieldPath);
|
|
127
|
+
if (prop) {
|
|
128
|
+
orderedProps.push(prop);
|
|
129
|
+
}
|
|
84
130
|
}
|
|
85
131
|
|
|
86
|
-
//
|
|
87
|
-
const
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
}, [schema, values, exclude, sort, projection?.fields]);
|
|
92
|
-
|
|
93
|
-
if ((readonly || layout === 'static') && values == null) {
|
|
94
|
-
return null;
|
|
132
|
+
// Add any remaining properties not in projection.
|
|
133
|
+
const projectionPaths = new Set(fieldProjections.map((projection) => String(projection.field.path)));
|
|
134
|
+
const remainingProps = visibleProps.filter((prop) => !projectionPaths.has(prop.name.toString()));
|
|
135
|
+
orderedProps.push(...remainingProps);
|
|
136
|
+
return orderedProps;
|
|
95
137
|
}
|
|
96
138
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
{layout !== 'inline' && label && <FormFieldLabel label={label} asChild />}
|
|
104
|
-
{properties
|
|
105
|
-
.map((property) => {
|
|
106
|
-
const name = property.name.toString();
|
|
107
|
-
return (
|
|
108
|
-
<FormFieldErrorBoundary key={name} path={[...(path ?? []), name]}>
|
|
109
|
-
<FormField
|
|
110
|
-
type={property.type}
|
|
111
|
-
name={name}
|
|
112
|
-
path={[...(path ?? []), name]}
|
|
113
|
-
readonly={readonly}
|
|
114
|
-
layout={layout}
|
|
115
|
-
projection={projection}
|
|
116
|
-
{...props}
|
|
117
|
-
/>
|
|
118
|
-
</FormFieldErrorBoundary>
|
|
119
|
-
);
|
|
120
|
-
})
|
|
121
|
-
.filter(isTruthy)}
|
|
122
|
-
</div>
|
|
123
|
-
);
|
|
124
|
-
},
|
|
125
|
-
);
|
|
126
|
-
|
|
127
|
-
FormFieldSet.displayName = FORM_FIELDSET_NAME;
|
|
139
|
+
// Fallback to legacy filter/sort behavior.
|
|
140
|
+
return sort
|
|
141
|
+
? [...filteredProps].sort(({ name: a }, { name: b }) => sort.indexOf(a.toString()) - sort.indexOf(b.toString()))
|
|
142
|
+
: filteredProps;
|
|
143
|
+
}, [schema, values, exclude, sort, projection]);
|
|
144
|
+
};
|
|
@@ -4,12 +4,13 @@
|
|
|
4
4
|
|
|
5
5
|
import * as Option from 'effect/Option';
|
|
6
6
|
import * as SchemaAST from 'effect/SchemaAST';
|
|
7
|
-
import React, {
|
|
7
|
+
import React, { useCallback } from 'react';
|
|
8
8
|
|
|
9
9
|
import { findNode, getArrayElementType, getDiscriminatedType, isDiscriminatedUnion, isNestedType } from '@dxos/effect';
|
|
10
10
|
import { IconButton, useTranslation } from '@dxos/react-ui';
|
|
11
11
|
|
|
12
|
-
import { translationKey } from '
|
|
12
|
+
import { translationKey } from '#translations';
|
|
13
|
+
|
|
13
14
|
import { getFormProperties } from '../../../util';
|
|
14
15
|
import { useFormValues } from '../Form';
|
|
15
16
|
import { FormField, type FormFieldProps } from '../FormField';
|
|
@@ -77,10 +78,10 @@ export const ArrayField = ({
|
|
|
77
78
|
<FormFieldLabel readonly={readonly} label={label} asChild />
|
|
78
79
|
)}
|
|
79
80
|
|
|
80
|
-
<div
|
|
81
|
+
<div className='flex flex-col gap-form-gap'>
|
|
81
82
|
{values?.map((_, index) => {
|
|
82
83
|
return (
|
|
83
|
-
<div
|
|
84
|
+
<div key={index} className='grid grid-cols-[1fr_min-content] gap-form-gap last:mb-form-gap items-center'>
|
|
84
85
|
<FormField
|
|
85
86
|
autoFocus={index === values.length - 1}
|
|
86
87
|
type={elementType}
|
|
@@ -91,13 +92,13 @@ export const ArrayField = ({
|
|
|
91
92
|
/>
|
|
92
93
|
|
|
93
94
|
{!readonly && layout !== 'static' && (
|
|
94
|
-
<div
|
|
95
|
+
<div className='flex flex-col h-full justify-end'>
|
|
95
96
|
{/* NOTE: Aligns with center of last field if multi-field object. */}
|
|
96
|
-
<div
|
|
97
|
+
<div className='flex items-center h-[2rem]'>
|
|
97
98
|
<IconButton
|
|
98
99
|
icon='ph--x--regular'
|
|
99
100
|
iconOnly
|
|
100
|
-
label={t('button
|
|
101
|
+
label={t('remove.button')}
|
|
101
102
|
onClick={() => handleDelete(index)}
|
|
102
103
|
classNames='self-center'
|
|
103
104
|
/>
|
|
@@ -112,9 +113,9 @@ export const ArrayField = ({
|
|
|
112
113
|
{/* TODO(burdon): Get label from schema. */}
|
|
113
114
|
{!readonly && layout !== 'static' && (
|
|
114
115
|
<IconButton
|
|
115
|
-
classNames='flex
|
|
116
|
+
classNames='flex w-full'
|
|
116
117
|
icon='ph--plus--regular'
|
|
117
|
-
label={t('add
|
|
118
|
+
label={t('add-item.button')}
|
|
118
119
|
onClick={handleAdd}
|
|
119
120
|
/>
|
|
120
121
|
)}
|
|
@@ -8,7 +8,8 @@ import { GeoLocation, type GeoPoint } from '@dxos/echo/internal';
|
|
|
8
8
|
import { Input, useTranslation } from '@dxos/react-ui';
|
|
9
9
|
import { safeParseFloat } from '@dxos/util';
|
|
10
10
|
|
|
11
|
-
import { translationKey } from '
|
|
11
|
+
import { translationKey } from '#translations';
|
|
12
|
+
|
|
12
13
|
import { type FormFieldComponentProps, FormFieldLabel } from '../FormFieldComponent';
|
|
13
14
|
|
|
14
15
|
export const GeoPointField = ({
|
|
@@ -62,17 +63,17 @@ export const GeoPointField = ({
|
|
|
62
63
|
{layout === 'static' ? (
|
|
63
64
|
<LatLng {...value} />
|
|
64
65
|
) : (
|
|
65
|
-
<div
|
|
66
|
+
<div className='grid grid-cols-2 gap-form-gap'>
|
|
66
67
|
<div>
|
|
67
68
|
<Input.Root>
|
|
68
|
-
{layout !== 'inline' && <Input.Label>{t('latitude
|
|
69
|
+
{layout !== 'inline' && <Input.Label>{t('latitude.label')}</Input.Label>}
|
|
69
70
|
<Input.TextInput
|
|
70
71
|
type='number'
|
|
71
72
|
step='0.00001'
|
|
72
73
|
min='-90'
|
|
73
74
|
max='90'
|
|
74
75
|
disabled={!!readonly}
|
|
75
|
-
placeholder={t('latitude
|
|
76
|
+
placeholder={t('latitude.placeholder')}
|
|
76
77
|
value={latitudeText ?? ''}
|
|
77
78
|
onChange={handleChange('latitude', setLatitudeText)}
|
|
78
79
|
onBlur={onBlur}
|
|
@@ -81,14 +82,14 @@ export const GeoPointField = ({
|
|
|
81
82
|
</div>
|
|
82
83
|
<div>
|
|
83
84
|
<Input.Root>
|
|
84
|
-
{layout !== 'inline' && <Input.Label>{t('longitude
|
|
85
|
+
{layout !== 'inline' && <Input.Label>{t('longitude.label')}</Input.Label>}
|
|
85
86
|
<Input.TextInput
|
|
86
87
|
type='number'
|
|
87
88
|
step='0.00001'
|
|
88
89
|
min='-180'
|
|
89
90
|
max='180'
|
|
90
91
|
disabled={!!readonly}
|
|
91
|
-
placeholder={t('longitude
|
|
92
|
+
placeholder={t('longitude.placeholder')}
|
|
92
93
|
value={longitudeText ?? ''}
|
|
93
94
|
onChange={handleChange('longitude', setLongitudeText)}
|
|
94
95
|
onBlur={onBlur}
|
|
@@ -106,7 +107,7 @@ const LatLng = ({ latitude = 0, longitude = 0 }: GeoLocation) => {
|
|
|
106
107
|
const lngHem = longitude >= 0 ? 'E' : 'W';
|
|
107
108
|
|
|
108
109
|
return (
|
|
109
|
-
<span className='inline-flex items-center gap-
|
|
110
|
+
<span className='inline-flex items-center gap-form-gap'>
|
|
110
111
|
<span>
|
|
111
112
|
<span>{Math.abs(latitude).toFixed(5)}</span>
|
|
112
113
|
<span className='text-subdued'>°{latHem}</span>
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2026 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { type Meta, type StoryObj } from '@storybook/react-vite';
|
|
6
|
+
import * as Schema from 'effect/Schema';
|
|
7
|
+
import React, { useMemo } from 'react';
|
|
8
|
+
import { expect, userEvent } from 'storybook/test';
|
|
9
|
+
|
|
10
|
+
import { Filter, Format, Obj, Ref } from '@dxos/echo';
|
|
11
|
+
import { useQuery } from '@dxos/react-client/echo';
|
|
12
|
+
import { useClientStory, withClientProvider } from '@dxos/react-client/testing';
|
|
13
|
+
import { Panel } from '@dxos/react-ui';
|
|
14
|
+
import { withLayout, withTheme } from '@dxos/react-ui/testing';
|
|
15
|
+
import { Text } from '@dxos/schema';
|
|
16
|
+
|
|
17
|
+
import { translations } from '#translations';
|
|
18
|
+
|
|
19
|
+
import { TestLayout } from '../../testing';
|
|
20
|
+
import { Form } from '../Form';
|
|
21
|
+
|
|
22
|
+
//
|
|
23
|
+
// String-backed markdown.
|
|
24
|
+
//
|
|
25
|
+
|
|
26
|
+
const StringSchema = Schema.Struct({
|
|
27
|
+
notes: Schema.String.pipe(Format.FormatAnnotation.set(Format.TypeFormat.Markdown)).annotations({ title: 'Notes' }),
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
const StringStory = () => (
|
|
31
|
+
<TestLayout>
|
|
32
|
+
<Form.Root schema={StringSchema} defaultValues={{ notes: '# Hello\n\nMarkdown text *here*.' }}>
|
|
33
|
+
<Form.Viewport>
|
|
34
|
+
<Form.Content>
|
|
35
|
+
<Form.FieldSet />
|
|
36
|
+
</Form.Content>
|
|
37
|
+
</Form.Viewport>
|
|
38
|
+
</Form.Root>
|
|
39
|
+
</TestLayout>
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
//
|
|
43
|
+
// Ref<Text>-backed markdown.
|
|
44
|
+
//
|
|
45
|
+
|
|
46
|
+
const TextNoteHolder = Schema.Struct({
|
|
47
|
+
instructions: Ref.Ref(Text.Text)
|
|
48
|
+
.pipe(Format.FormatAnnotation.set(Format.TypeFormat.Markdown))
|
|
49
|
+
.annotations({ title: 'Instructions' }),
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
const RefStory = () => {
|
|
53
|
+
const { space } = useClientStory();
|
|
54
|
+
const [text] = useQuery(space?.db, Filter.type(Text.Text));
|
|
55
|
+
const values = useMemo(() => (text ? { instructions: Ref.make(text) } : undefined), [text]);
|
|
56
|
+
|
|
57
|
+
if (!space || !values) {
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return (
|
|
62
|
+
<TestLayout>
|
|
63
|
+
<Panel.Root>
|
|
64
|
+
<Panel.Content asChild>
|
|
65
|
+
<Form.Root schema={TextNoteHolder} values={values as any} db={space.db}>
|
|
66
|
+
<Form.Viewport>
|
|
67
|
+
<Form.Content>
|
|
68
|
+
<Form.FieldSet />
|
|
69
|
+
</Form.Content>
|
|
70
|
+
</Form.Viewport>
|
|
71
|
+
</Form.Root>
|
|
72
|
+
</Panel.Content>
|
|
73
|
+
</Panel.Root>
|
|
74
|
+
</TestLayout>
|
|
75
|
+
);
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
//
|
|
79
|
+
// Ref<Text> empty — shows the create button.
|
|
80
|
+
//
|
|
81
|
+
|
|
82
|
+
const EmptyRefSchema = Schema.Struct({
|
|
83
|
+
instructions: Schema.optional(
|
|
84
|
+
Ref.Ref(Text.Text)
|
|
85
|
+
.pipe(Format.FormatAnnotation.set(Format.TypeFormat.Markdown))
|
|
86
|
+
.annotations({ title: 'Instructions' }),
|
|
87
|
+
),
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
const EmptyRefStory = () => {
|
|
91
|
+
const { space } = useClientStory();
|
|
92
|
+
if (!space) {
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return (
|
|
97
|
+
<TestLayout>
|
|
98
|
+
<Panel.Root>
|
|
99
|
+
<Panel.Content asChild>
|
|
100
|
+
<Form.Root schema={EmptyRefSchema} defaultValues={{}} db={space.db}>
|
|
101
|
+
<Form.Viewport>
|
|
102
|
+
<Form.Content>
|
|
103
|
+
<Form.FieldSet />
|
|
104
|
+
</Form.Content>
|
|
105
|
+
</Form.Viewport>
|
|
106
|
+
</Form.Root>
|
|
107
|
+
</Panel.Content>
|
|
108
|
+
</Panel.Root>
|
|
109
|
+
</TestLayout>
|
|
110
|
+
);
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
const meta = {
|
|
114
|
+
title: 'ui/react-ui-form/MarkdownField',
|
|
115
|
+
decorators: [withTheme(), withLayout({ layout: 'column' })],
|
|
116
|
+
parameters: {
|
|
117
|
+
layout: 'fullscreen',
|
|
118
|
+
translations,
|
|
119
|
+
},
|
|
120
|
+
} satisfies Meta;
|
|
121
|
+
|
|
122
|
+
export default meta;
|
|
123
|
+
|
|
124
|
+
type Story = StoryObj<typeof meta>;
|
|
125
|
+
|
|
126
|
+
export const StringBacked: Story = {
|
|
127
|
+
render: () => <StringStory />,
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
// Regression test: keystrokes into a string-backed markdown field must not
|
|
131
|
+
// blur the editor. Previously the EditorView was destroyed and recreated on
|
|
132
|
+
// every change because `onChange` was a useTextEditor dep and form callbacks
|
|
133
|
+
// are unstable across renders.
|
|
134
|
+
export const StringBackedKeepsFocus: Story = {
|
|
135
|
+
render: () => (
|
|
136
|
+
<TestLayout>
|
|
137
|
+
<Form.Root schema={StringSchema} defaultValues={{ notes: '' }}>
|
|
138
|
+
<Form.Viewport>
|
|
139
|
+
<Form.Content>
|
|
140
|
+
<Form.FieldSet />
|
|
141
|
+
</Form.Content>
|
|
142
|
+
</Form.Viewport>
|
|
143
|
+
</Form.Root>
|
|
144
|
+
</TestLayout>
|
|
145
|
+
),
|
|
146
|
+
play: async ({ canvasElement }) => {
|
|
147
|
+
const waitFor = async <T,>(predicate: () => T | null | undefined, message: string): Promise<T> => {
|
|
148
|
+
for (let attempt = 0; attempt < 50; attempt++) {
|
|
149
|
+
const value = predicate();
|
|
150
|
+
if (value) {
|
|
151
|
+
return value;
|
|
152
|
+
}
|
|
153
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
154
|
+
}
|
|
155
|
+
throw new Error(message);
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
const editorRoot = await waitFor(
|
|
159
|
+
() => canvasElement.querySelector<HTMLElement>('.cm-editor'),
|
|
160
|
+
'CodeMirror editor root (.cm-editor) not found',
|
|
161
|
+
);
|
|
162
|
+
const editorContent = editorRoot.querySelector<HTMLElement>('.cm-content')!;
|
|
163
|
+
await expect(editorContent).toBeVisible();
|
|
164
|
+
|
|
165
|
+
// Focus editor and wait for focus to settle inside the editor root.
|
|
166
|
+
editorContent.focus();
|
|
167
|
+
await waitFor(() => (editorRoot.contains(document.activeElement) ? true : null), 'Editor did not receive focus');
|
|
168
|
+
|
|
169
|
+
// Type several characters; focus must be preserved inside the editor root
|
|
170
|
+
// after every keystroke. Regression: previously the EditorView was
|
|
171
|
+
// destroyed and recreated on every change, which detached the focused
|
|
172
|
+
// contentDOM and blurred the input.
|
|
173
|
+
const phrase = 'hello world';
|
|
174
|
+
for (const ch of phrase) {
|
|
175
|
+
await userEvent.keyboard(ch);
|
|
176
|
+
await expect(editorRoot.contains(document.activeElement)).toBe(true);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
await expect(editorContent).toHaveTextContent(phrase);
|
|
180
|
+
},
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
export const RefPopulated: Story = {
|
|
184
|
+
render: () => <RefStory />,
|
|
185
|
+
decorators: [
|
|
186
|
+
withClientProvider({
|
|
187
|
+
createIdentity: true,
|
|
188
|
+
createSpace: true,
|
|
189
|
+
types: [Text.Text],
|
|
190
|
+
onCreateSpace: async ({ space }) => {
|
|
191
|
+
space.db.add(Obj.make(Text.Text, { content: '# Magazine brief\n\nLong-form text describing what to gather.' }));
|
|
192
|
+
},
|
|
193
|
+
}),
|
|
194
|
+
],
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
export const RefEmpty: Story = {
|
|
198
|
+
render: () => <EmptyRefStory />,
|
|
199
|
+
decorators: [
|
|
200
|
+
withClientProvider({
|
|
201
|
+
createIdentity: true,
|
|
202
|
+
createSpace: true,
|
|
203
|
+
types: [Text.Text],
|
|
204
|
+
}),
|
|
205
|
+
],
|
|
206
|
+
};
|