@dxos/react-ui-form 0.8.4-main.9735255 → 0.8.4-main.9be5663bfe
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 +989 -674
- package/dist/lib/browser/index.mjs.map +4 -4
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/node-esm/index.mjs +989 -674
- package/dist/lib/node-esm/index.mjs.map +4 -4
- package/dist/lib/node-esm/meta.json +1 -1
- 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 +62 -61
- package/dist/types/src/components/FieldEditor/FieldEditor.stories.d.ts.map +1 -1
- package/dist/types/src/components/Form/Form.d.ts +23 -18
- package/dist/types/src/components/Form/Form.d.ts.map +1 -1
- package/dist/types/src/components/Form/Form.stories.d.ts +68 -67
- package/dist/types/src/components/Form/Form.stories.d.ts.map +1 -1
- package/dist/types/src/components/Form/FormField.d.ts +7 -1
- package/dist/types/src/components/Form/FormField.d.ts.map +1 -1
- package/dist/types/src/components/Form/FormFieldComponent.d.ts +0 -1
- 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 +9 -10
- package/dist/types/src/components/Form/FormFieldSet.d.ts.map +1 -1
- package/dist/types/src/components/Form/fields/ArrayField.d.ts.map +1 -1
- package/dist/types/src/components/Form/fields/RefField.d.ts +8 -6
- package/dist/types/src/components/Form/fields/RefField.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 +62 -61
- package/dist/types/src/components/Form/fields/SelectOptionField.stories.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 +10 -2
- package/dist/types/src/components/ObjectPicker/ObjectPicker.d.ts.map +1 -1
- package/dist/types/src/components/ObjectPicker/ObjectPicker.stories.d.ts +62 -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 +83 -0
- package/dist/types/src/components/ObjectProperties/ObjectProperties.stories.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 +35 -0
- package/dist/types/src/components/Settings/Settings.d.ts.map +1 -0
- package/dist/types/src/components/Settings/Settings.stories.d.ts +82 -0
- package/dist/types/src/components/Settings/Settings.stories.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 -0
- package/dist/types/src/components/ViewEditor/ViewEditor.d.ts +5 -5
- package/dist/types/src/components/ViewEditor/ViewEditor.d.ts.map +1 -1
- package/dist/types/src/components/ViewEditor/ViewEditor.stories.d.ts +66 -64
- package/dist/types/src/components/ViewEditor/ViewEditor.stories.d.ts.map +1 -1
- package/dist/types/src/components/index.d.ts +3 -1
- package/dist/types/src/components/index.d.ts.map +1 -1
- 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/translations.d.ts +62 -61
- package/dist/types/src/translations.d.ts.map +1 -1
- package/dist/types/src/util/index.d.ts +0 -1
- package/dist/types/src/util/index.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 +37 -38
- package/src/components/FieldEditor/FieldEditor.stories.tsx +7 -8
- package/src/components/FieldEditor/FieldEditor.test.tsx +0 -1
- package/src/components/FieldEditor/FieldEditor.tsx +3 -4
- package/src/components/Form/AUDIT.md +17 -0
- package/src/components/Form/Form.stories.tsx +25 -26
- package/src/components/Form/Form.tsx +100 -66
- package/src/components/Form/FormField.tsx +32 -14
- package/src/components/Form/FormFieldComponent.stories.tsx +5 -5
- package/src/components/Form/FormFieldComponent.tsx +17 -20
- package/src/components/Form/FormFieldSet.tsx +126 -105
- package/src/components/Form/fields/ArrayField.tsx +11 -8
- package/src/components/Form/fields/GeoPointField.tsx +6 -6
- package/src/components/Form/fields/MarkdownField.tsx +1 -1
- package/src/components/Form/fields/RefField.tsx +37 -20
- package/src/components/Form/fields/SelectField.tsx +1 -1
- package/src/components/Form/fields/SelectOptionField.stories.tsx +2 -3
- package/src/components/Form/fields/SelectOptionField.tsx +9 -10
- package/src/components/Form/fields/TupleField.tsx +1 -1
- package/src/components/ObjectForm/ObjectForm.tsx +113 -0
- package/src/components/{ControlSection → ObjectForm}/index.ts +1 -1
- package/src/components/ObjectPicker/ObjectPicker.stories.tsx +13 -18
- package/src/components/ObjectPicker/ObjectPicker.tsx +71 -60
- package/src/components/ObjectProperties/ObjectProperties.stories.tsx +63 -0
- package/src/components/ObjectProperties/ObjectProperties.tsx +129 -0
- package/src/components/ObjectProperties/index.ts +5 -0
- package/src/components/Settings/Settings.stories.tsx +55 -0
- package/src/components/Settings/Settings.tsx +129 -0
- package/src/components/Settings/index.ts +5 -0
- package/src/components/ViewEditor/ViewEditor.stories.tsx +36 -33
- package/src/components/ViewEditor/ViewEditor.test.tsx +3 -7
- package/src/components/ViewEditor/ViewEditor.tsx +99 -70
- package/src/components/index.ts +3 -1
- package/src/components/testing.tsx +20 -8
- package/src/hooks/useFormHandler.ts +42 -24
- package/src/hooks/useKeyHandler.tsx +7 -7
- package/src/translations.ts +63 -63
- package/src/util/index.ts +0 -1
- package/src/util/refs.ts +7 -3
- package/dist/types/src/components/ControlSection/ControlSection.d.ts +0 -23
- package/dist/types/src/components/ControlSection/ControlSection.d.ts.map +0 -1
- package/dist/types/src/components/ControlSection/ControlSection.stories.d.ts +0 -87
- package/dist/types/src/components/ControlSection/ControlSection.stories.d.ts.map +0 -1
- package/dist/types/src/components/ControlSection/index.d.ts +0 -2
- package/dist/types/src/components/ControlSection/index.d.ts.map +0 -1
- 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/ControlSection/ControlSection.stories.tsx +0 -38
- package/src/components/ControlSection/ControlSection.tsx +0 -153
- package/src/components/Form/Form-old.stories.tsx +0 -314
- package/src/util/echo.ts +0 -25
|
@@ -2,124 +2,145 @@
|
|
|
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
|
-
|
|
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
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
16
|
+
const FORM_FIELDSET_NAME = 'Form.FieldSet';
|
|
17
|
+
|
|
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
|
+
| 'schemaHook'
|
|
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
|
+
|
|
59
|
+
const properties = useFormFieldSetProperties({ schema, values, exclude, sort, projection });
|
|
60
|
+
|
|
61
|
+
if ((readonly || layout === 'static') && values == null) {
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return (
|
|
66
|
+
<>
|
|
67
|
+
{layout !== 'inline' && label && <FormFieldLabel label={label} asChild />}
|
|
68
|
+
{properties.map((property) => {
|
|
69
|
+
const name = property.name.toString();
|
|
70
|
+
return (
|
|
71
|
+
<FormFieldErrorBoundary key={name} path={[...(path ?? []), name]}>
|
|
72
|
+
<FormField
|
|
73
|
+
type={property.type}
|
|
74
|
+
name={name}
|
|
75
|
+
path={[...(path ?? []), name]}
|
|
76
|
+
readonly={readonly}
|
|
77
|
+
layout={layout}
|
|
78
|
+
projection={projection}
|
|
79
|
+
{...props}
|
|
80
|
+
/>
|
|
81
|
+
</FormFieldErrorBoundary>
|
|
82
|
+
);
|
|
83
|
+
})}
|
|
84
|
+
</>
|
|
85
|
+
);
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
FormFieldSet.displayName = FORM_FIELDSET_NAME;
|
|
89
|
+
|
|
90
|
+
type UseFormFieldSetPropertiesParams = Pick<FormFieldSetProps<any>, 'schema' | 'exclude' | 'projection' | 'sort'> & {
|
|
91
|
+
values: AnyProperties | undefined;
|
|
92
|
+
sort?: string[];
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Resolves ordered schema properties for a field set (projection order, exclude, or sort).
|
|
97
|
+
*/
|
|
98
|
+
const useFormFieldSetProperties = ({
|
|
99
|
+
schema,
|
|
100
|
+
exclude,
|
|
101
|
+
projection,
|
|
102
|
+
values,
|
|
103
|
+
sort,
|
|
104
|
+
}: UseFormFieldSetPropertiesParams): SchemaProperty[] => {
|
|
105
|
+
// TODO(burdon): Updates on every value change.
|
|
106
|
+
// Remove values dep if can remove from getSchemaProperties.
|
|
107
|
+
return useMemo(() => {
|
|
108
|
+
if (!schema) {
|
|
109
|
+
return [];
|
|
110
|
+
}
|
|
55
111
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
112
|
+
// TODO(wittjosiah): Reconcile FormInputAnnotation with projection hidden properties & exclude function.
|
|
113
|
+
const schemaProps = getFormProperties(schema.ast);
|
|
114
|
+
const filteredProps = exclude ? exclude(schemaProps) : schemaProps;
|
|
115
|
+
|
|
116
|
+
// Use projection-based field management when view and projection are available.
|
|
117
|
+
if (projection) {
|
|
118
|
+
const fieldProjections = projection.getFieldProjections();
|
|
119
|
+
const hiddenProperties = new Set(projection.getHiddenProperties());
|
|
120
|
+
|
|
121
|
+
// Filter properties to only include visible ones and order by projection.
|
|
122
|
+
const visibleProps = filteredProps.filter((prop) => !hiddenProperties.has(prop.name.toString()));
|
|
123
|
+
const orderedProps: SchemaProperty[] = [];
|
|
124
|
+
|
|
125
|
+
// Add properties in projection field order.
|
|
126
|
+
for (const fieldProjection of fieldProjections) {
|
|
127
|
+
const fieldPath = String(fieldProjection.field.path);
|
|
128
|
+
const prop = visibleProps.find((prop) => prop.name === fieldPath);
|
|
129
|
+
if (prop) {
|
|
130
|
+
orderedProps.push(prop);
|
|
75
131
|
}
|
|
76
|
-
|
|
77
|
-
// Add any remaining properties not in projection.
|
|
78
|
-
const projectionPaths = new Set(fieldProjections.map((projection) => String(projection.field.path)));
|
|
79
|
-
const remainingProps = visibleProps.filter((prop) => !projectionPaths.has(prop.name.toString()));
|
|
80
|
-
orderedProps.push(...remainingProps);
|
|
81
|
-
return orderedProps;
|
|
82
132
|
}
|
|
83
133
|
|
|
84
|
-
//
|
|
85
|
-
const
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
}, [schema, values, exclude, sort, projection?.fields]);
|
|
90
|
-
|
|
91
|
-
if ((readonly || layout === 'static') && values == null) {
|
|
92
|
-
return null;
|
|
134
|
+
// Add any remaining properties not in projection.
|
|
135
|
+
const projectionPaths = new Set(fieldProjections.map((projection) => String(projection.field.path)));
|
|
136
|
+
const remainingProps = visibleProps.filter((prop) => !projectionPaths.has(prop.name.toString()));
|
|
137
|
+
orderedProps.push(...remainingProps);
|
|
138
|
+
return orderedProps;
|
|
93
139
|
}
|
|
94
140
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
{layout !== 'inline' && label && <FormFieldLabel label={label} asChild />}
|
|
102
|
-
{properties
|
|
103
|
-
.map((property) => {
|
|
104
|
-
const name = property.name.toString();
|
|
105
|
-
return (
|
|
106
|
-
<FormFieldErrorBoundary key={name} path={[...(path ?? []), name]}>
|
|
107
|
-
<FormField
|
|
108
|
-
type={property.type}
|
|
109
|
-
name={name}
|
|
110
|
-
path={[...(path ?? []), name]}
|
|
111
|
-
readonly={readonly}
|
|
112
|
-
layout={layout}
|
|
113
|
-
projection={projection}
|
|
114
|
-
{...props}
|
|
115
|
-
/>
|
|
116
|
-
</FormFieldErrorBoundary>
|
|
117
|
-
);
|
|
118
|
-
})
|
|
119
|
-
.filter(isTruthy)}
|
|
120
|
-
</div>
|
|
121
|
-
);
|
|
122
|
-
},
|
|
123
|
-
);
|
|
124
|
-
|
|
125
|
-
FormFieldSet.displayName = 'Form.Fields';
|
|
141
|
+
// Fallback to legacy filter/sort behavior.
|
|
142
|
+
return sort
|
|
143
|
+
? [...filteredProps].sort(({ name: a }, { name: b }) => sort.indexOf(a.toString()) - sort.indexOf(b.toString()))
|
|
144
|
+
: filteredProps;
|
|
145
|
+
}, [schema, values, exclude, sort, projection]);
|
|
146
|
+
};
|
|
@@ -4,7 +4,7 @@
|
|
|
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';
|
|
@@ -77,10 +77,10 @@ export const ArrayField = ({
|
|
|
77
77
|
<FormFieldLabel readonly={readonly} label={label} asChild />
|
|
78
78
|
)}
|
|
79
79
|
|
|
80
|
-
<div role='none' className='flex flex-col gap-
|
|
80
|
+
<div role='none' className='flex flex-col gap-form-gap'>
|
|
81
81
|
{values?.map((_, index) => {
|
|
82
82
|
return (
|
|
83
|
-
<div role='none' key={index} className='
|
|
83
|
+
<div role='none' key={index} className='flex gap-form-gap last:mb-form-gap items-center'>
|
|
84
84
|
<FormField
|
|
85
85
|
autoFocus={index === values.length - 1}
|
|
86
86
|
type={elementType}
|
|
@@ -91,13 +91,13 @@ export const ArrayField = ({
|
|
|
91
91
|
/>
|
|
92
92
|
|
|
93
93
|
{!readonly && layout !== 'static' && (
|
|
94
|
-
<div role='none' className='flex flex-col
|
|
94
|
+
<div role='none' className='flex flex-col h-full justify-end'>
|
|
95
95
|
{/* NOTE: Aligns with center of last field if multi-field object. */}
|
|
96
|
-
<div role='none' className='flex items-center
|
|
96
|
+
<div role='none' className='flex items-center h-[2rem]'>
|
|
97
97
|
<IconButton
|
|
98
98
|
icon='ph--x--regular'
|
|
99
99
|
iconOnly
|
|
100
|
-
label={t('button
|
|
100
|
+
label={t('remove.button')}
|
|
101
101
|
onClick={() => handleDelete(index)}
|
|
102
102
|
classNames='self-center'
|
|
103
103
|
/>
|
|
@@ -112,9 +112,9 @@ export const ArrayField = ({
|
|
|
112
112
|
{/* TODO(burdon): Get label from schema. */}
|
|
113
113
|
{!readonly && layout !== 'static' && (
|
|
114
114
|
<IconButton
|
|
115
|
-
classNames='flex
|
|
115
|
+
classNames='flex w-full'
|
|
116
116
|
icon='ph--plus--regular'
|
|
117
|
-
label={t('add
|
|
117
|
+
label={t('add-field.button')}
|
|
118
118
|
onClick={handleAdd}
|
|
119
119
|
/>
|
|
120
120
|
)}
|
|
@@ -140,6 +140,9 @@ export const getDefaultValue = (ast?: SchemaAST.AST): any => {
|
|
|
140
140
|
case 'BooleanKeyword': {
|
|
141
141
|
return false;
|
|
142
142
|
}
|
|
143
|
+
case 'Suspend': {
|
|
144
|
+
return getDefaultValue(ast.f());
|
|
145
|
+
}
|
|
143
146
|
default: {
|
|
144
147
|
if (ast && isNestedType(ast)) {
|
|
145
148
|
return {};
|
|
@@ -62,17 +62,17 @@ export const GeoPointField = ({
|
|
|
62
62
|
{layout === 'static' ? (
|
|
63
63
|
<LatLng {...value} />
|
|
64
64
|
) : (
|
|
65
|
-
<div role='none' className='grid grid-cols-2 gap-
|
|
65
|
+
<div role='none' className='grid grid-cols-2 gap-form-gap'>
|
|
66
66
|
<div>
|
|
67
67
|
<Input.Root>
|
|
68
|
-
{layout !== 'inline' && <Input.Label>{t('latitude
|
|
68
|
+
{layout !== 'inline' && <Input.Label>{t('latitude.label')}</Input.Label>}
|
|
69
69
|
<Input.TextInput
|
|
70
70
|
type='number'
|
|
71
71
|
step='0.00001'
|
|
72
72
|
min='-90'
|
|
73
73
|
max='90'
|
|
74
74
|
disabled={!!readonly}
|
|
75
|
-
placeholder={t('latitude
|
|
75
|
+
placeholder={t('latitude.placeholder')}
|
|
76
76
|
value={latitudeText ?? ''}
|
|
77
77
|
onChange={handleChange('latitude', setLatitudeText)}
|
|
78
78
|
onBlur={onBlur}
|
|
@@ -81,14 +81,14 @@ export const GeoPointField = ({
|
|
|
81
81
|
</div>
|
|
82
82
|
<div>
|
|
83
83
|
<Input.Root>
|
|
84
|
-
{layout !== 'inline' && <Input.Label>{t('longitude
|
|
84
|
+
{layout !== 'inline' && <Input.Label>{t('longitude.label')}</Input.Label>}
|
|
85
85
|
<Input.TextInput
|
|
86
86
|
type='number'
|
|
87
87
|
step='0.00001'
|
|
88
88
|
min='-180'
|
|
89
89
|
max='180'
|
|
90
90
|
disabled={!!readonly}
|
|
91
|
-
placeholder={t('longitude
|
|
91
|
+
placeholder={t('longitude.placeholder')}
|
|
92
92
|
value={longitudeText ?? ''}
|
|
93
93
|
onChange={handleChange('longitude', setLongitudeText)}
|
|
94
94
|
onBlur={onBlur}
|
|
@@ -106,7 +106,7 @@ const LatLng = ({ latitude = 0, longitude = 0 }: GeoLocation) => {
|
|
|
106
106
|
const lngHem = longitude >= 0 ? 'E' : 'W';
|
|
107
107
|
|
|
108
108
|
return (
|
|
109
|
-
<span className='inline-flex items-center gap-
|
|
109
|
+
<span className='inline-flex items-center gap-form-gap'>
|
|
110
110
|
<span>
|
|
111
111
|
<span>{Math.abs(latitude).toFixed(5)}</span>
|
|
112
112
|
<span className='text-subdued'>°{latHem}</span>
|
|
@@ -65,7 +65,7 @@ export const MarkdownField = ({
|
|
|
65
65
|
disabled={!!readonly}
|
|
66
66
|
placeholder={placeholder}
|
|
67
67
|
value={value ?? ''}
|
|
68
|
-
classNames={'min-
|
|
68
|
+
classNames={'min-h-auto max-h-40 overflow-auto'}
|
|
69
69
|
onChange={handleChange}
|
|
70
70
|
onBlur={onBlur}
|
|
71
71
|
style={{ resize: 'none' }}
|
|
@@ -2,18 +2,18 @@
|
|
|
2
2
|
// Copyright 2025 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import '@dxos/lit-ui/dx-tag-picker.pcss';
|
|
6
|
-
|
|
7
5
|
import React, { useCallback, useMemo } from 'react';
|
|
8
6
|
|
|
9
|
-
import
|
|
10
|
-
import {
|
|
7
|
+
import '@dxos/lit-ui/dx-tag-picker.pcss';
|
|
8
|
+
import { type Database, Entity, Filter, Obj, Ref, Type } from '@dxos/echo';
|
|
11
9
|
import { useQuery, useSchema as useSchema$ } from '@dxos/echo-react';
|
|
10
|
+
import { ReferenceAnnotationId, type ReferenceAnnotationValue } from '@dxos/echo/internal';
|
|
12
11
|
import { findAnnotation } from '@dxos/effect';
|
|
13
12
|
import { DXN } from '@dxos/keys';
|
|
14
13
|
import { DxAnchor } from '@dxos/lit-ui/react';
|
|
15
14
|
import { Button, Icon, Input, useTranslation } from '@dxos/react-ui';
|
|
16
|
-
import {
|
|
15
|
+
import { ParentLabelAnnotationId } from '@dxos/schema';
|
|
16
|
+
import { mx } from '@dxos/ui-theme';
|
|
17
17
|
|
|
18
18
|
import { translationKey } from '../../../translations';
|
|
19
19
|
import { ObjectPicker, type ObjectPickerContentProps, type RefOption } from '../../ObjectPicker';
|
|
@@ -25,23 +25,35 @@ const isRefSnapshot = (val: any): val is { '/': string } => {
|
|
|
25
25
|
return typeof val === 'object' && typeof (val as any)?.['/'] === 'string';
|
|
26
26
|
};
|
|
27
27
|
|
|
28
|
-
const defaultGetOptions: NonNullable<RefFieldProps['getOptions']> = (results) =>
|
|
28
|
+
const defaultGetOptions: NonNullable<RefFieldProps['getOptions']> = (results, { parentLabel } = {}) =>
|
|
29
29
|
results.map((result) => {
|
|
30
30
|
const id = Entity.getDXN(result).toString();
|
|
31
|
-
const
|
|
31
|
+
const parent = parentLabel ? Obj.getParent(result as Obj.Unknown) : undefined;
|
|
32
|
+
const label = parent ? Entity.getLabel(parent) : Entity.getLabel(result);
|
|
32
33
|
return { id, label: label ?? id };
|
|
33
34
|
});
|
|
34
35
|
|
|
35
36
|
const defaultResultsHook: NonNullable<RefFieldProps['resultsHook']> = (db, typename) =>
|
|
36
|
-
useQuery(
|
|
37
|
+
useQuery(
|
|
38
|
+
db,
|
|
39
|
+
typename
|
|
40
|
+
? // For Ref.Ref(Obj.Unknown) we want to show all objects.
|
|
41
|
+
typename === Type.getTypename(Obj.Unknown)
|
|
42
|
+
? Filter.everything()
|
|
43
|
+
: Filter.typename(typename)
|
|
44
|
+
: Filter.nothing(),
|
|
45
|
+
);
|
|
37
46
|
|
|
38
47
|
export type RefFieldProps = FormFieldComponentProps &
|
|
39
|
-
Pick<
|
|
48
|
+
Pick<
|
|
49
|
+
ObjectPickerContentProps,
|
|
50
|
+
'createOptionLabel' | 'createOptionIcon' | 'createInitialValuePath' | 'createFieldMap'
|
|
51
|
+
> & {
|
|
40
52
|
db?: Database.Database;
|
|
41
53
|
resultsHook?: (db?: Database.Database, typename?: string) => Entity.Any[];
|
|
42
|
-
schemaHook?: (db?: Database.Database, typename?: string) => Type.
|
|
43
|
-
getOptions?: (objects: Entity.Any[]) => RefOption[];
|
|
44
|
-
onCreate?: (schema: Type.
|
|
54
|
+
schemaHook?: (db?: Database.Database, typename?: string) => Type.AnyEntity;
|
|
55
|
+
getOptions?: (objects: Entity.Any[], options?: { parentLabel?: boolean }) => RefOption[];
|
|
56
|
+
onCreate?: (schema: Type.AnyEntity, values: any) => void;
|
|
45
57
|
};
|
|
46
58
|
|
|
47
59
|
export const RefField = (props: RefFieldProps) => {
|
|
@@ -56,6 +68,7 @@ export const RefField = (props: RefFieldProps) => {
|
|
|
56
68
|
createOptionLabel,
|
|
57
69
|
createOptionIcon,
|
|
58
70
|
createInitialValuePath,
|
|
71
|
+
createFieldMap,
|
|
59
72
|
db,
|
|
60
73
|
resultsHook: useResults = defaultResultsHook,
|
|
61
74
|
schemaHook: useSchema = useSchema$,
|
|
@@ -72,7 +85,10 @@ export const RefField = (props: RefFieldProps) => {
|
|
|
72
85
|
);
|
|
73
86
|
|
|
74
87
|
const results = useResults(db, typename);
|
|
75
|
-
const options = useMemo(() =>
|
|
88
|
+
const options = useMemo(() => {
|
|
89
|
+
const parentLabel = type ? findAnnotation<boolean>(type, ParentLabelAnnotationId) === true : false;
|
|
90
|
+
return getOptions(results, { parentLabel });
|
|
91
|
+
}, [results, getOptions, type]);
|
|
76
92
|
|
|
77
93
|
const handleGetValue = useCallback(() => {
|
|
78
94
|
const formValue = getValue();
|
|
@@ -135,9 +151,9 @@ export const RefField = (props: RefFieldProps) => {
|
|
|
135
151
|
<div>
|
|
136
152
|
{readonly ? (
|
|
137
153
|
!item ? (
|
|
138
|
-
<p className={mx(
|
|
154
|
+
<p className={mx('text-description', 'mb-2')}>{t('empty-readonly-ref-field.label')}</p>
|
|
139
155
|
) : (
|
|
140
|
-
<DxAnchor key={item.id}
|
|
156
|
+
<DxAnchor key={item.id} dxn={item.id} rootclassname='me-1'>
|
|
141
157
|
{item.label}
|
|
142
158
|
</DxAnchor>
|
|
143
159
|
)
|
|
@@ -145,15 +161,15 @@ export const RefField = (props: RefFieldProps) => {
|
|
|
145
161
|
<ObjectPicker.Root>
|
|
146
162
|
<ObjectPicker.Trigger asChild classNames='p-0'>
|
|
147
163
|
{item ? (
|
|
148
|
-
<div className='flex gap-
|
|
164
|
+
<div className='flex gap-form-gap w-full'>
|
|
149
165
|
<Input.Root key={item.id}>
|
|
150
|
-
<Input.TextInput value={item.label} readOnly classNames='
|
|
166
|
+
<Input.TextInput value={item.label} readOnly classNames='w-full' />
|
|
151
167
|
</Input.Root>
|
|
152
168
|
</div>
|
|
153
169
|
) : (
|
|
154
|
-
<Button classNames='
|
|
170
|
+
<Button classNames='w-full text-start gap-form-gap'>
|
|
155
171
|
<div role='none' className='grow overflow-hidden'>
|
|
156
|
-
<span className='flex truncate text-description'>{placeholder ?? t('ref
|
|
172
|
+
<span className='flex truncate text-description'>{placeholder ?? t('ref-field.placeholder')}</span>
|
|
157
173
|
</div>
|
|
158
174
|
<Icon size={3} icon='ph--caret-down--bold' />
|
|
159
175
|
</Button>
|
|
@@ -161,13 +177,14 @@ export const RefField = (props: RefFieldProps) => {
|
|
|
161
177
|
</ObjectPicker.Trigger>
|
|
162
178
|
<ObjectPicker.Portal>
|
|
163
179
|
<ObjectPicker.Content
|
|
164
|
-
classNames='
|
|
180
|
+
classNames='dx-card-popover-width'
|
|
165
181
|
options={options}
|
|
166
182
|
selectedIds={selectedIds}
|
|
167
183
|
createSchema={createSchema && omitId(createSchema)}
|
|
168
184
|
createOptionLabel={createOptionLabel}
|
|
169
185
|
createOptionIcon={createOptionIcon}
|
|
170
186
|
createInitialValuePath={createInitialValuePath}
|
|
187
|
+
createFieldMap={createFieldMap}
|
|
171
188
|
onCreate={handleCreate}
|
|
172
189
|
onSelect={handleSelect}
|
|
173
190
|
/>
|
|
@@ -43,7 +43,7 @@ export const SelectField = ({
|
|
|
43
43
|
) : (
|
|
44
44
|
<Select.Root value={value} onValueChange={handleValueChange}>
|
|
45
45
|
{/* TODO(burdon): Placeholder not working? */}
|
|
46
|
-
<Select.TriggerButton classNames='
|
|
46
|
+
<Select.TriggerButton classNames='w-full' disabled={!!readonly} placeholder={placeholder} />
|
|
47
47
|
<Select.Portal>
|
|
48
48
|
<Select.Content>
|
|
49
49
|
<Select.Viewport>
|
|
@@ -7,12 +7,11 @@ import * as Schema from 'effect/Schema';
|
|
|
7
7
|
import React, { useState } from 'react';
|
|
8
8
|
|
|
9
9
|
import { SelectOption } from '@dxos/echo/internal';
|
|
10
|
-
import { withTheme } from '@dxos/react-ui/testing';
|
|
10
|
+
import { withLayout, withTheme } from '@dxos/react-ui/testing';
|
|
11
11
|
|
|
12
12
|
import { translations } from '../../../translations';
|
|
13
13
|
import { TestLayout } from '../../testing';
|
|
14
14
|
import { Form } from '../Form';
|
|
15
|
-
|
|
16
15
|
import { SelectOptionField } from './SelectOptionField';
|
|
17
16
|
|
|
18
17
|
const schema = Schema.Struct({
|
|
@@ -50,7 +49,7 @@ const meta = {
|
|
|
50
49
|
title: 'ui/react-ui-form/SelectOptionField',
|
|
51
50
|
component: SelectOptionField as any,
|
|
52
51
|
render: DefaultStory,
|
|
53
|
-
decorators: [withTheme],
|
|
52
|
+
decorators: [withTheme(), withLayout({ layout: 'fullscreen' })],
|
|
54
53
|
parameters: {
|
|
55
54
|
layout: 'fullscreen',
|
|
56
55
|
translations,
|
|
@@ -128,13 +128,13 @@ export const SelectOptionField = ({
|
|
|
128
128
|
readonly={!!readonly}
|
|
129
129
|
>
|
|
130
130
|
{({ items }) => (
|
|
131
|
-
<div role='list' className='
|
|
131
|
+
<div role='list' className='w-full overflow-auto'>
|
|
132
132
|
{items?.map((item) => (
|
|
133
133
|
<div key={item.id}>
|
|
134
134
|
<List.Item
|
|
135
135
|
role='button'
|
|
136
136
|
item={item}
|
|
137
|
-
classNames={[subtleHover, 'flex flex-col cursor-pointer rounded-
|
|
137
|
+
classNames={[subtleHover, 'flex flex-col cursor-pointer rounded-xs']}
|
|
138
138
|
aria-expanded={selected === item.id}
|
|
139
139
|
>
|
|
140
140
|
<div className='flex items-center'>
|
|
@@ -152,13 +152,12 @@ export const SelectOptionField = ({
|
|
|
152
152
|
/>
|
|
153
153
|
</div>
|
|
154
154
|
{selected === item.id && (
|
|
155
|
-
<div className='
|
|
156
|
-
{
|
|
157
|
-
<
|
|
158
|
-
<div className='grid grid-cols-[1fr_min-content_min-content] gap-1'>
|
|
155
|
+
<div className='flex flex-col p-form-padding gap-form-gap dx-density-fine'>
|
|
156
|
+
<Input.Label classNames='text-xs'>{t('select-option.label')}</Input.Label>
|
|
157
|
+
<div className='grid grid-cols-[1fr_min-content_min-content] gap-form-gap'>
|
|
159
158
|
<Input.TextInput
|
|
160
159
|
disabled={!!readonly}
|
|
161
|
-
placeholder={t('select
|
|
160
|
+
placeholder={t('select-option-label.placeholder')}
|
|
162
161
|
ref={selected === item.id ? inputRef : undefined}
|
|
163
162
|
value={item.title}
|
|
164
163
|
onChange={handleTitleChange(item.id)}
|
|
@@ -170,7 +169,7 @@ export const SelectOptionField = ({
|
|
|
170
169
|
disabled={!!readonly}
|
|
171
170
|
icon='ph--trash--fill'
|
|
172
171
|
iconOnly
|
|
173
|
-
label={t('select
|
|
172
|
+
label={t('select-option-delete.button')}
|
|
174
173
|
onClick={() => handleDelete(item.id)}
|
|
175
174
|
/>
|
|
176
175
|
</div>
|
|
@@ -182,10 +181,10 @@ export const SelectOptionField = ({
|
|
|
182
181
|
<IconButton
|
|
183
182
|
variant='ghost'
|
|
184
183
|
icon='ph--plus--regular'
|
|
185
|
-
label={t('select
|
|
184
|
+
label={t('select-option-add.button')}
|
|
186
185
|
onClick={handleAdd}
|
|
187
186
|
disabled={!!readonly}
|
|
188
|
-
classNames='
|
|
187
|
+
classNames='w-full'
|
|
189
188
|
/>
|
|
190
189
|
</div>
|
|
191
190
|
)}
|
|
@@ -32,7 +32,7 @@ export const TupleField = ({
|
|
|
32
32
|
return (
|
|
33
33
|
<Input.Root validationValence={status}>
|
|
34
34
|
<FormFieldLabel error={error} readonly={readonly} label={label} />
|
|
35
|
-
<div className={mx('grid gap-
|
|
35
|
+
<div className={mx('grid gap-form-gap', gridCols[binding.length - 1])}>
|
|
36
36
|
{binding.map((prop) => (
|
|
37
37
|
<Input.TextInput
|
|
38
38
|
key={prop}
|