@dxos/react-ui-form 0.8.4-main.f5c0578 → 0.8.4-main.fcc0d83b33
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 +2774 -2137
- 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 +2774 -2137
- 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 +8 -8
- package/dist/types/src/components/FieldEditor/FieldEditor.d.ts.map +1 -1
- package/dist/types/src/components/FieldEditor/FieldEditor.stories.d.ts +84 -5
- package/dist/types/src/components/FieldEditor/FieldEditor.stories.d.ts.map +1 -1
- package/dist/types/src/components/Form/Form.d.ts +109 -25
- package/dist/types/src/components/Form/Form.d.ts.map +1 -1
- package/dist/types/src/components/Form/Form.stories.d.ts +130 -56
- package/dist/types/src/components/Form/Form.stories.d.ts.map +1 -1
- package/dist/types/src/components/Form/FormField.d.ts +43 -0
- package/dist/types/src/components/Form/FormField.d.ts.map +1 -0
- package/dist/types/src/components/Form/FormFieldComponent.d.ts +79 -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 +15 -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 +11 -0
- package/dist/types/src/components/Form/fields/MarkdownField.d.ts.map +1 -0
- package/dist/types/src/components/Form/fields/MarkdownField.stories.d.ts +83 -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 +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 +22 -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 +84 -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/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 +68 -0
- package/dist/types/src/components/ObjectPicker/ObjectPicker.d.ts.map +1 -0
- package/dist/types/src/components/ObjectPicker/ObjectPicker.stories.d.ts +87 -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/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 +51 -0
- package/dist/types/src/components/Settings/Settings.d.ts.map +1 -0
- package/dist/types/src/components/Settings/Settings.stories.d.ts +84 -0
- package/dist/types/src/components/Settings/Settings.stories.d.ts.map +1 -0
- 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 +4 -0
- package/dist/types/src/components/Settings/index.d.ts.map +1 -0
- package/dist/types/src/components/ViewEditor/ViewEditor.d.ts +22 -10
- package/dist/types/src/components/ViewEditor/ViewEditor.d.ts.map +1 -1
- package/dist/types/src/components/ViewEditor/ViewEditor.stories.d.ts +86 -8
- package/dist/types/src/components/ViewEditor/ViewEditor.stories.d.ts.map +1 -1
- package/dist/types/src/components/index.d.ts +4 -2
- package/dist/types/src/components/index.d.ts.map +1 -1
- package/dist/types/src/components/testing.d.ts +5 -4
- package/dist/types/src/components/testing.d.ts.map +1 -1
- package/dist/types/src/hooks/index.d.ts +2 -3
- 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 +7 -0
- package/dist/types/src/hooks/useKeyHandler.d.ts.map +1 -0
- 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 +65 -55
- 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 +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 +2 -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 +3 -3
- package/dist/types/src/util/refs.d.ts.map +1 -1
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/package.json +56 -46
- package/src/components/FieldEditor/FieldEditor.stories.tsx +49 -31
- package/src/components/FieldEditor/FieldEditor.test.tsx +0 -1
- package/src/components/FieldEditor/FieldEditor.tsx +86 -91
- package/src/components/Form/AUDIT.md +17 -0
- package/src/components/Form/Form.stories.tsx +147 -224
- package/src/components/Form/Form.tsx +376 -96
- package/src/components/Form/FormField.tsx +306 -0
- package/src/components/Form/FormFieldComponent.stories.tsx +65 -0
- package/src/components/Form/FormFieldComponent.tsx +174 -0
- package/src/components/Form/FormFieldSet.tsx +144 -0
- package/src/components/Form/README.md +3 -0
- package/src/components/Form/fields/ArrayField.tsx +159 -0
- package/src/components/Form/fields/BooleanField.tsx +22 -0
- package/src/components/Form/fields/GeoPointField.tsx +121 -0
- package/src/components/Form/fields/MarkdownField.stories.tsx +152 -0
- package/src/components/Form/fields/MarkdownField.tsx +168 -0
- package/src/components/Form/fields/NumberField.tsx +39 -0
- package/src/components/Form/fields/RefField.tsx +229 -0
- package/src/components/Form/fields/SelectField.tsx +65 -0
- package/src/components/Form/fields/SelectOptionField.stories.tsx +66 -0
- package/src/components/Form/{custom/SelectOptionsInput.tsx → fields/SelectOptionField.tsx} +37 -31
- 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} +17 -10
- package/src/components/Form/fields/index.ts +15 -0
- package/src/components/Form/index.ts +3 -4
- package/src/components/ObjectForm/ObjectForm.tsx +114 -0
- package/src/components/{ControlSection → ObjectForm}/index.ts +1 -1
- package/src/components/ObjectPicker/ObjectPicker.stories.tsx +212 -0
- package/src/components/ObjectPicker/ObjectPicker.tsx +194 -0
- package/src/components/ObjectPicker/index.ts +5 -0
- 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 +104 -0
- package/src/components/Settings/Settings.tsx +82 -0
- package/src/components/Settings/SettingsFieldSet.tsx +257 -0
- package/src/components/Settings/SettingsItem.tsx +62 -0
- package/src/components/Settings/SettingsSelectField.tsx +59 -0
- package/src/components/Settings/index.ts +7 -0
- package/src/components/ViewEditor/ViewEditor.stories.tsx +140 -63
- package/src/components/ViewEditor/ViewEditor.test.tsx +12 -15
- package/src/components/ViewEditor/ViewEditor.tsx +348 -212
- package/src/components/index.ts +4 -3
- package/src/components/testing.tsx +31 -12
- package/src/hooks/index.ts +2 -3
- package/src/hooks/useFormHandler.ts +380 -0
- package/src/hooks/useKeyHandler.tsx +37 -0
- package/src/index.ts +0 -4
- package/src/translations.ts +70 -59
- package/src/util/field-type.ts +60 -0
- package/src/util/field.ts +33 -43
- package/src/util/formatting.ts +22 -22
- package/src/util/index.ts +2 -1
- package/src/util/properties.ts +18 -0
- package/src/util/refs.ts +17 -19
- 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 -8
- 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/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/useQueryRefOptions.d.ts +0 -23
- package/dist/types/src/hooks/useQueryRefOptions.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/ControlSection/ControlSection.stories.tsx +0 -42
- package/src/components/ControlSection/ControlSection.tsx +0 -139
- package/src/components/Form/ArrayField.tsx +0 -125
- package/src/components/Form/Defaults.tsx +0 -204
- package/src/components/Form/FormActions.tsx +0 -54
- package/src/components/Form/FormContent.tsx +0 -241
- package/src/components/Form/FormContext.tsx +0 -110
- package/src/components/Form/Input.tsx +0 -51
- package/src/components/Form/RefField.tsx +0 -214
- package/src/components/Form/custom/GeoPointInput.tsx +0 -72
- package/src/components/Form/custom/SelectOptionsInput.stories.tsx +0 -62
- 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 -63
- 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 -293
- package/src/hooks/useQueryRefOptions.ts +0 -53
- package/src/hooks/useRefQueryLookupHandler.ts +0 -39
- package/src/util/schema.ts +0 -14
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2025 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import React, { useCallback, useMemo, useState } from 'react';
|
|
6
|
+
|
|
7
|
+
import '@dxos/lit-ui/dx-tag-picker.pcss';
|
|
8
|
+
import { type Database, Entity, Filter, Obj, Ref, Type } from '@dxos/echo';
|
|
9
|
+
import { useQuery, useSchema as defaultUseSchema } from '@dxos/echo-react';
|
|
10
|
+
import { ReferenceAnnotationId, type ReferenceAnnotationValue } from '@dxos/echo/internal';
|
|
11
|
+
import { findAnnotation } from '@dxos/effect';
|
|
12
|
+
import { DXN } from '@dxos/keys';
|
|
13
|
+
import { DxAnchor } from '@dxos/lit-ui/react';
|
|
14
|
+
import { Button, Icon, Input, useTranslation } from '@dxos/react-ui';
|
|
15
|
+
import { ParentLabelAnnotationId } from '@dxos/schema';
|
|
16
|
+
|
|
17
|
+
import { translationKey } from '#translations';
|
|
18
|
+
|
|
19
|
+
import { ObjectPicker, type ObjectPickerContentProps, type RefOption } from '../../ObjectPicker';
|
|
20
|
+
import { omitHiddenFormFields, omitId } from '../Form';
|
|
21
|
+
import { type FormFieldComponentProps, FormFieldLabel } from '../FormFieldComponent';
|
|
22
|
+
|
|
23
|
+
// TODO(burdon): Factor out.
|
|
24
|
+
const isRefSnapshot = (val: any): val is { '/': string } => {
|
|
25
|
+
return typeof val === 'object' && typeof (val as any)?.['/'] === 'string';
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const defaultGetOptions: NonNullable<RefFieldProps['getOptions']> = (results, { parentLabel } = {}) =>
|
|
29
|
+
results.map((result) => {
|
|
30
|
+
const id = Entity.getDXN(result).toString();
|
|
31
|
+
const parent = parentLabel ? Obj.getParent(result as Obj.Unknown) : undefined;
|
|
32
|
+
const label = parent ? Entity.getLabel(parent) : Entity.getLabel(result);
|
|
33
|
+
return { id, label: label ?? id };
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
const defaultUseResults: NonNullable<RefFieldProps['useResults']> = (db, typename) =>
|
|
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
|
+
);
|
|
46
|
+
|
|
47
|
+
export type RefFieldProps = FormFieldComponentProps &
|
|
48
|
+
Pick<
|
|
49
|
+
ObjectPickerContentProps,
|
|
50
|
+
'createOptionLabel' | 'createOptionIcon' | 'createInitialValuePath' | 'createFieldMap'
|
|
51
|
+
> & {
|
|
52
|
+
db?: Database.Database;
|
|
53
|
+
// TODO(burdon): Replace hooks with callbacks.
|
|
54
|
+
useSchema?: (db?: Database.Database, typename?: string) => Type.AnyEntity;
|
|
55
|
+
useResults?: (db?: Database.Database, typename?: string) => Entity.Any[];
|
|
56
|
+
getOptions?: (objects: Entity.Any[], options?: { parentLabel?: boolean }) => RefOption[];
|
|
57
|
+
/**
|
|
58
|
+
* Persist a newly-created object. Called after the user fills out the
|
|
59
|
+
* inline create form and clicks Save. Should add the object to the
|
|
60
|
+
* database and return it (sync or async). The returned object is then
|
|
61
|
+
* wired into this slot's form value as a Ref.
|
|
62
|
+
*/
|
|
63
|
+
onCreate?: (schema: Type.AnyEntity, values: any) => Obj.Unknown | Promise<Obj.Unknown> | undefined | void;
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
export const RefField = (props: RefFieldProps) => {
|
|
67
|
+
const {
|
|
68
|
+
type,
|
|
69
|
+
readonly,
|
|
70
|
+
label,
|
|
71
|
+
placeholder,
|
|
72
|
+
layout,
|
|
73
|
+
getStatus,
|
|
74
|
+
getValue,
|
|
75
|
+
createOptionLabel,
|
|
76
|
+
createOptionIcon,
|
|
77
|
+
createInitialValuePath,
|
|
78
|
+
createFieldMap,
|
|
79
|
+
db,
|
|
80
|
+
useSchema = defaultUseSchema,
|
|
81
|
+
useResults = defaultUseResults,
|
|
82
|
+
getOptions = defaultGetOptions,
|
|
83
|
+
onCreate,
|
|
84
|
+
onValueChange,
|
|
85
|
+
} = props;
|
|
86
|
+
const { t } = useTranslation(translationKey);
|
|
87
|
+
const { status, error } = getStatus();
|
|
88
|
+
|
|
89
|
+
const typename = useMemo(
|
|
90
|
+
() => (type ? findAnnotation<ReferenceAnnotationValue>(type, ReferenceAnnotationId)?.typename : undefined),
|
|
91
|
+
[type],
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
const results = useResults(db, typename);
|
|
95
|
+
const options = useMemo(() => {
|
|
96
|
+
const parentLabel = type ? findAnnotation<boolean>(type, ParentLabelAnnotationId) === true : false;
|
|
97
|
+
return getOptions(results, { parentLabel });
|
|
98
|
+
}, [results, getOptions, type]);
|
|
99
|
+
|
|
100
|
+
const handleGetValue = useCallback(() => {
|
|
101
|
+
const formValue = getValue();
|
|
102
|
+
// Match form-value Refs against options by the bare object id (last DXN
|
|
103
|
+
// segment), not by full DXN string. Ref.make uses
|
|
104
|
+
// `DXN.fromLocalObjectId(id)` (`dxn:echo:@:<id>`), but
|
|
105
|
+
// `Entity.getDXN(obj)` on a registered object produces the space-scoped
|
|
106
|
+
// form (`dxn:echo:<spaceId>:<id>`). String-comparing the two never
|
|
107
|
+
// matches, so the just-created Ref's option lookup fails and the slot
|
|
108
|
+
// displays as empty even though the underlying form value IS set.
|
|
109
|
+
const dxnToObjectId = (dxn: string): string => dxn.split(':').pop() ?? dxn;
|
|
110
|
+
|
|
111
|
+
const unknownToRefOption = (value: unknown) => {
|
|
112
|
+
const isRef = Ref.isRef(value);
|
|
113
|
+
if (isRef || isRefSnapshot(value)) {
|
|
114
|
+
const dxnString = isRef ? value.dxn.toString() : value['/'];
|
|
115
|
+
const objectId = dxnToObjectId(dxnString);
|
|
116
|
+
const matchingOption = options.find((option) => dxnToObjectId(option.id) === objectId);
|
|
117
|
+
if (matchingOption) {
|
|
118
|
+
return matchingOption;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return undefined;
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
return unknownToRefOption(formValue);
|
|
126
|
+
}, [options, getValue]);
|
|
127
|
+
|
|
128
|
+
const item = handleGetValue();
|
|
129
|
+
const selectedIds = useMemo(() => (item ? [item.id] : []), [item]);
|
|
130
|
+
const createSchema = useSchema(db, typename);
|
|
131
|
+
|
|
132
|
+
// Lift the popover open state so we can dismiss it after a successful create.
|
|
133
|
+
const [open, setOpen] = useState(false);
|
|
134
|
+
|
|
135
|
+
const handleCreate = useCallback(
|
|
136
|
+
async (values: any) => {
|
|
137
|
+
if (!createSchema || !onCreate) {
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
const newObject = await onCreate(createSchema, values);
|
|
141
|
+
if (newObject) {
|
|
142
|
+
// Wire the newly-created object into this slot's form value. ArrayField
|
|
143
|
+
// owns the array; this RefField represents a single slot, so writing a
|
|
144
|
+
// Ref via `onValueChange` populates that slot.
|
|
145
|
+
onValueChange(type, Ref.make(newObject));
|
|
146
|
+
}
|
|
147
|
+
setOpen(false);
|
|
148
|
+
},
|
|
149
|
+
[createSchema, onCreate, type, onValueChange],
|
|
150
|
+
);
|
|
151
|
+
|
|
152
|
+
const handleUpdate = useCallback(
|
|
153
|
+
(id: string | undefined) => {
|
|
154
|
+
const item = options.find((option) => option.id === id);
|
|
155
|
+
const ref = item ? Ref.fromDXN(DXN.parse(item.id)) : undefined;
|
|
156
|
+
onValueChange(type, ref);
|
|
157
|
+
},
|
|
158
|
+
[options, type, onValueChange],
|
|
159
|
+
);
|
|
160
|
+
|
|
161
|
+
const handleSelect = useCallback(
|
|
162
|
+
(id: string) => {
|
|
163
|
+
if (item?.id === id) {
|
|
164
|
+
handleUpdate(undefined);
|
|
165
|
+
} else {
|
|
166
|
+
handleUpdate(id);
|
|
167
|
+
}
|
|
168
|
+
},
|
|
169
|
+
[item, handleUpdate],
|
|
170
|
+
);
|
|
171
|
+
|
|
172
|
+
if (!typename || ((readonly || layout === 'static') && !item)) {
|
|
173
|
+
return null;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return (
|
|
177
|
+
<Input.Root validationValence={status}>
|
|
178
|
+
{layout !== 'inline' && <FormFieldLabel error={error} readonly={readonly} label={label} />}
|
|
179
|
+
<div>
|
|
180
|
+
{readonly ? (
|
|
181
|
+
!item ? (
|
|
182
|
+
<p className='text-description mb-2'>{t('empty-readonly-ref-field.label')}</p>
|
|
183
|
+
) : (
|
|
184
|
+
<DxAnchor key={item.id} dxn={item.id} rootclassname='me-1'>
|
|
185
|
+
{item.label}
|
|
186
|
+
</DxAnchor>
|
|
187
|
+
)
|
|
188
|
+
) : (
|
|
189
|
+
<ObjectPicker.Root open={open} onOpenChange={setOpen}>
|
|
190
|
+
<ObjectPicker.Trigger asChild classNames='p-0'>
|
|
191
|
+
{item ? (
|
|
192
|
+
<div role='none' className='flex gap-form-gap w-full'>
|
|
193
|
+
<Input.Root key={item.id}>
|
|
194
|
+
<Input.TextInput value={item.label} readOnly classNames='w-full' />
|
|
195
|
+
</Input.Root>
|
|
196
|
+
</div>
|
|
197
|
+
) : (
|
|
198
|
+
<Button classNames='w-full text-start gap-form-gap'>
|
|
199
|
+
<div role='none' className='grow overflow-hidden'>
|
|
200
|
+
<span className='truncate text-description'>{placeholder ?? t('ref-field.placeholder')}</span>
|
|
201
|
+
</div>
|
|
202
|
+
<Icon size={3} icon='ph--caret-down--bold' />
|
|
203
|
+
</Button>
|
|
204
|
+
)}
|
|
205
|
+
</ObjectPicker.Trigger>
|
|
206
|
+
<ObjectPicker.Portal>
|
|
207
|
+
<ObjectPicker.Content
|
|
208
|
+
classNames='dx-card-popover-width'
|
|
209
|
+
options={options}
|
|
210
|
+
selectedIds={selectedIds}
|
|
211
|
+
// Strip hidden (`FormInputAnnotation.set(false)`) fields so the
|
|
212
|
+
// form's validator doesn't reject required-but-hidden fields
|
|
213
|
+
// such as backing-object refs supplied by a `FactoryAnnotation`.
|
|
214
|
+
createSchema={createSchema && omitHiddenFormFields(omitId(createSchema))}
|
|
215
|
+
createOptionLabel={createOptionLabel}
|
|
216
|
+
createOptionIcon={createOptionIcon}
|
|
217
|
+
createInitialValuePath={createInitialValuePath}
|
|
218
|
+
createFieldMap={createFieldMap}
|
|
219
|
+
onCreate={handleCreate}
|
|
220
|
+
onSelect={handleSelect}
|
|
221
|
+
/>
|
|
222
|
+
</ObjectPicker.Portal>
|
|
223
|
+
</ObjectPicker.Root>
|
|
224
|
+
)}
|
|
225
|
+
</div>
|
|
226
|
+
{layout === 'full' && <Input.DescriptionAndValidation>{error}</Input.DescriptionAndValidation>}
|
|
227
|
+
</Input.Root>
|
|
228
|
+
);
|
|
229
|
+
};
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2024 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import React, { useCallback } from 'react';
|
|
6
|
+
|
|
7
|
+
import { Input, Select, type SelectRootProps } from '@dxos/react-ui';
|
|
8
|
+
|
|
9
|
+
import { type FormFieldComponentProps, FormFieldLabel } from '../FormFieldComponent';
|
|
10
|
+
|
|
11
|
+
export type SelectFieldOptions = FormFieldComponentProps & {
|
|
12
|
+
options?: Array<{ value: string | number; label?: string }>;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export const SelectField = ({
|
|
16
|
+
type,
|
|
17
|
+
readonly,
|
|
18
|
+
label,
|
|
19
|
+
layout,
|
|
20
|
+
placeholder,
|
|
21
|
+
options,
|
|
22
|
+
getStatus,
|
|
23
|
+
getValue,
|
|
24
|
+
onValueChange,
|
|
25
|
+
}: SelectFieldOptions) => {
|
|
26
|
+
const { status, error } = getStatus();
|
|
27
|
+
const value = getValue() as string | undefined;
|
|
28
|
+
|
|
29
|
+
const handleValueChange = useCallback<NonNullable<SelectRootProps['onValueChange']>>(
|
|
30
|
+
(value) => onValueChange(type, value),
|
|
31
|
+
[type, onValueChange],
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
if ((readonly || layout === 'static') && value == null) {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return (
|
|
39
|
+
<Input.Root validationValence={status}>
|
|
40
|
+
{layout !== 'inline' && <FormFieldLabel error={error} readonly={readonly} label={label} />}
|
|
41
|
+
{layout === 'static' ? (
|
|
42
|
+
<p>{options?.find(({ value: optionValue }) => optionValue === value)?.label ?? String(value)}</p>
|
|
43
|
+
) : (
|
|
44
|
+
<Select.Root value={value} onValueChange={handleValueChange}>
|
|
45
|
+
{/* TODO(burdon): Placeholder not working? */}
|
|
46
|
+
<Select.TriggerButton classNames='w-full' disabled={!!readonly} placeholder={placeholder} />
|
|
47
|
+
<Select.Portal>
|
|
48
|
+
<Select.Content>
|
|
49
|
+
<Select.Viewport>
|
|
50
|
+
{options?.map(({ value, label }) => (
|
|
51
|
+
// NOTE: Numeric values are converted to and from strings.
|
|
52
|
+
<Select.Option key={String(value)} value={String(value)}>
|
|
53
|
+
{label ?? String(value)}
|
|
54
|
+
</Select.Option>
|
|
55
|
+
))}
|
|
56
|
+
</Select.Viewport>
|
|
57
|
+
<Select.Arrow />
|
|
58
|
+
</Select.Content>
|
|
59
|
+
</Select.Portal>
|
|
60
|
+
</Select.Root>
|
|
61
|
+
)}
|
|
62
|
+
{layout === 'full' && <Input.DescriptionAndValidation>{error}</Input.DescriptionAndValidation>}
|
|
63
|
+
</Input.Root>
|
|
64
|
+
);
|
|
65
|
+
};
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2025 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { type Meta, type StoryObj } from '@storybook/react-vite';
|
|
6
|
+
import * as Schema from 'effect/Schema';
|
|
7
|
+
import React, { useState } from 'react';
|
|
8
|
+
|
|
9
|
+
import { SelectOption } from '@dxos/echo/internal';
|
|
10
|
+
import { withLayout, withTheme } from '@dxos/react-ui/testing';
|
|
11
|
+
|
|
12
|
+
import { translations } from '#translations';
|
|
13
|
+
|
|
14
|
+
import { TestLayout } from '../../testing';
|
|
15
|
+
import { Form } from '../Form';
|
|
16
|
+
import { SelectOptionField } from './SelectOptionField';
|
|
17
|
+
|
|
18
|
+
const schema = Schema.Struct({
|
|
19
|
+
options: Schema.Array(SelectOption).pipe(Schema.mutable),
|
|
20
|
+
}).pipe(Schema.mutable);
|
|
21
|
+
|
|
22
|
+
const DefaultStory = () => {
|
|
23
|
+
const [values, setValues] = useState<{ options: SelectOption[] }>({
|
|
24
|
+
// TODO(Zaymon): Pick colors from our limited set.
|
|
25
|
+
options: [
|
|
26
|
+
{ id: 'opt_1', title: 'First option', color: 'amber' },
|
|
27
|
+
{ id: 'opt_2', title: 'Second option', color: 'emerald' },
|
|
28
|
+
{ id: 'opt_3', title: 'Third option', color: 'rose' },
|
|
29
|
+
],
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
return (
|
|
33
|
+
<TestLayout>
|
|
34
|
+
<Form.Root
|
|
35
|
+
schema={schema}
|
|
36
|
+
values={values}
|
|
37
|
+
fieldMap={{ options: SelectOptionField }}
|
|
38
|
+
onSave={(values) => setValues(values)}
|
|
39
|
+
>
|
|
40
|
+
<Form.Viewport>
|
|
41
|
+
<Form.Content>
|
|
42
|
+
<Form.FieldSet />
|
|
43
|
+
<Form.Actions />
|
|
44
|
+
</Form.Content>
|
|
45
|
+
</Form.Viewport>
|
|
46
|
+
</Form.Root>
|
|
47
|
+
</TestLayout>
|
|
48
|
+
);
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const meta = {
|
|
52
|
+
title: 'ui/react-ui-form/SelectOptionField',
|
|
53
|
+
component: SelectOptionField as any,
|
|
54
|
+
render: DefaultStory,
|
|
55
|
+
decorators: [withTheme(), withLayout({ layout: 'fullscreen' })],
|
|
56
|
+
parameters: {
|
|
57
|
+
layout: 'fullscreen',
|
|
58
|
+
translations,
|
|
59
|
+
},
|
|
60
|
+
} satisfies Meta<typeof DefaultStory>;
|
|
61
|
+
|
|
62
|
+
export default meta;
|
|
63
|
+
|
|
64
|
+
type Story = StoryObj<typeof meta>;
|
|
65
|
+
|
|
66
|
+
export const Default: Story = {};
|
|
@@ -4,30 +4,38 @@
|
|
|
4
4
|
|
|
5
5
|
import React, { type ChangeEvent, type KeyboardEvent, useCallback, useEffect, useRef, useState } from 'react';
|
|
6
6
|
|
|
7
|
-
import { type SelectOption } from '@dxos/echo
|
|
7
|
+
import { type SelectOption } from '@dxos/echo/internal';
|
|
8
8
|
import { PublicKey } from '@dxos/keys';
|
|
9
9
|
import { type ChromaticPalette, IconButton, Input, Tag, useTranslation } from '@dxos/react-ui';
|
|
10
10
|
import { List } from '@dxos/react-ui-list';
|
|
11
11
|
import { HuePicker } from '@dxos/react-ui-pickers';
|
|
12
|
-
import { hues, subtleHover } from '@dxos/
|
|
12
|
+
import { hues, osTranslations, subtleHover } from '@dxos/ui-theme';
|
|
13
13
|
|
|
14
|
-
import { translationKey } from '
|
|
15
|
-
import { InputHeader, type InputProps } from '../Input';
|
|
14
|
+
import { translationKey } from '#translations';
|
|
16
15
|
|
|
17
|
-
|
|
16
|
+
import { type FormFieldComponentProps, FormFieldLabel } from '../FormFieldComponent';
|
|
17
|
+
|
|
18
|
+
export const SelectOptionField = ({
|
|
19
|
+
type,
|
|
20
|
+
readonly,
|
|
21
|
+
label,
|
|
22
|
+
getStatus,
|
|
23
|
+
getValue,
|
|
24
|
+
onValueChange,
|
|
25
|
+
}: FormFieldComponentProps<SelectOption[] | undefined>) => {
|
|
26
|
+
const { t } = useTranslation(translationKey);
|
|
27
|
+
const { status, error } = getStatus();
|
|
18
28
|
const [selected, setSelectedId] = useState<string | null>(null);
|
|
19
29
|
const [isNewOption, setIsNewOption] = useState(false);
|
|
20
30
|
const inputRef = useRef<HTMLInputElement>(null);
|
|
21
|
-
const
|
|
22
|
-
const { status, error } = getStatus();
|
|
23
|
-
const options = getValue<SelectOption[] | undefined>();
|
|
31
|
+
const options = getValue();
|
|
24
32
|
|
|
25
33
|
// Initialization.
|
|
26
34
|
useEffect(() => {
|
|
27
35
|
if (options === undefined) {
|
|
28
36
|
onValueChange(type, []);
|
|
29
37
|
}
|
|
30
|
-
}, [options,
|
|
38
|
+
}, [options, type, onValueChange]);
|
|
31
39
|
|
|
32
40
|
const randomHue = () => {
|
|
33
41
|
const usedHues = new Set(options?.map((option) => option.color) ?? []);
|
|
@@ -88,8 +96,8 @@ export const SelectOptionInput = ({ type, label, disabled, getStatus, getValue,
|
|
|
88
96
|
[options, type, onValueChange],
|
|
89
97
|
);
|
|
90
98
|
|
|
91
|
-
const handleKeyDown = useCallback((
|
|
92
|
-
if (
|
|
99
|
+
const handleKeyDown = useCallback((ev: KeyboardEvent<HTMLInputElement>) => {
|
|
100
|
+
if (ev.key === 'Enter') {
|
|
93
101
|
setSelectedId(null);
|
|
94
102
|
}
|
|
95
103
|
}, []);
|
|
@@ -111,14 +119,14 @@ export const SelectOptionInput = ({ type, label, disabled, getStatus, getValue,
|
|
|
111
119
|
|
|
112
120
|
return (
|
|
113
121
|
<Input.Root validationValence={status}>
|
|
114
|
-
<
|
|
122
|
+
<FormFieldLabel error={error} readonly={readonly} label={label} />
|
|
115
123
|
<div role='none'>
|
|
116
124
|
{options && (
|
|
117
125
|
<List.Root
|
|
118
126
|
items={options}
|
|
119
|
-
isItem={(
|
|
120
|
-
onMove={
|
|
121
|
-
readonly={
|
|
127
|
+
isItem={(_item) => true}
|
|
128
|
+
onMove={readonly ? undefined : handleMove}
|
|
129
|
+
readonly={!!readonly}
|
|
122
130
|
>
|
|
123
131
|
{({ items }) => (
|
|
124
132
|
<div role='list' className='w-full overflow-auto'>
|
|
@@ -127,11 +135,11 @@ export const SelectOptionInput = ({ type, label, disabled, getStatus, getValue,
|
|
|
127
135
|
<List.Item
|
|
128
136
|
role='button'
|
|
129
137
|
item={item}
|
|
130
|
-
classNames={[subtleHover, 'flex flex-col cursor-pointer rounded-
|
|
138
|
+
classNames={[subtleHover, 'flex flex-col cursor-pointer rounded-xs']}
|
|
131
139
|
aria-expanded={selected === item.id}
|
|
132
140
|
>
|
|
133
141
|
<div className='flex items-center'>
|
|
134
|
-
<List.ItemDragHandle disabled={
|
|
142
|
+
<List.ItemDragHandle disabled={!!readonly} />
|
|
135
143
|
<List.ItemTitle onClick={() => handleClick(item.id)} classNames='flex-1'>
|
|
136
144
|
{/* TODO(ZaymonFC): Move spacer into Tag component. */}
|
|
137
145
|
<Tag palette={item.color as ChromaticPalette}>{item.title || '\u200b'}</Tag>
|
|
@@ -139,32 +147,30 @@ export const SelectOptionInput = ({ type, label, disabled, getStatus, getValue,
|
|
|
139
147
|
<IconButton
|
|
140
148
|
iconOnly
|
|
141
149
|
variant='ghost'
|
|
142
|
-
label={t(selected === item.id ? 'collapse label' : 'expand label', { ns:
|
|
150
|
+
label={t(selected === item.id ? 'collapse label' : 'expand label', { ns: osTranslations })}
|
|
143
151
|
icon={selected === item.id ? 'ph--caret-down--regular' : 'ph--caret-right--regular'}
|
|
144
152
|
onClick={() => handleClick(item.id)}
|
|
145
153
|
/>
|
|
146
154
|
</div>
|
|
147
155
|
{selected === item.id && (
|
|
148
|
-
<div className='
|
|
149
|
-
{
|
|
150
|
-
<
|
|
151
|
-
<div className='grid grid-cols-[1fr_min-content_min-content] gap-1'>
|
|
156
|
+
<div className='flex flex-col p-form-padding gap-form-gap dx-density-fine'>
|
|
157
|
+
<Input.Label classNames='text-xs'>{t('select-option.label')}</Input.Label>
|
|
158
|
+
<div className='grid grid-cols-[1fr_min-content_min-content] gap-form-gap'>
|
|
152
159
|
<Input.TextInput
|
|
153
|
-
disabled={
|
|
154
|
-
placeholder={t('select
|
|
160
|
+
disabled={!!readonly}
|
|
161
|
+
placeholder={t('select-option-label.placeholder')}
|
|
155
162
|
ref={selected === item.id ? inputRef : undefined}
|
|
156
163
|
value={item.title}
|
|
157
164
|
onChange={handleTitleChange(item.id)}
|
|
158
165
|
onKeyDown={handleKeyDown}
|
|
159
166
|
classNames='flex-1'
|
|
160
|
-
data-no-submit
|
|
161
167
|
/>
|
|
162
|
-
<HuePicker disabled={
|
|
168
|
+
<HuePicker disabled={!!readonly} value={item.color} onChange={handleColorChange(item.id)} />
|
|
163
169
|
<IconButton
|
|
164
|
-
disabled={
|
|
170
|
+
disabled={!!readonly}
|
|
165
171
|
icon='ph--trash--fill'
|
|
166
172
|
iconOnly
|
|
167
|
-
label={t('select
|
|
173
|
+
label={t('select-option-delete.button')}
|
|
168
174
|
onClick={() => handleDelete(item.id)}
|
|
169
175
|
/>
|
|
170
176
|
</div>
|
|
@@ -176,10 +182,10 @@ export const SelectOptionInput = ({ type, label, disabled, getStatus, getValue,
|
|
|
176
182
|
<IconButton
|
|
177
183
|
variant='ghost'
|
|
178
184
|
icon='ph--plus--regular'
|
|
179
|
-
label={t('select
|
|
185
|
+
label={t('select-option-add.button')}
|
|
180
186
|
onClick={handleAdd}
|
|
181
|
-
disabled={
|
|
182
|
-
classNames='
|
|
187
|
+
disabled={!!readonly}
|
|
188
|
+
classNames='w-full'
|
|
183
189
|
/>
|
|
184
190
|
</div>
|
|
185
191
|
)}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2024 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import React, { useCallback } from 'react';
|
|
6
|
+
|
|
7
|
+
import { Input, type TextAreaProps } from '@dxos/react-ui';
|
|
8
|
+
|
|
9
|
+
import { type FormFieldComponentProps, FormFieldWrapper } from '../FormFieldComponent';
|
|
10
|
+
|
|
11
|
+
export const TextAreaField = ({
|
|
12
|
+
type,
|
|
13
|
+
readonly,
|
|
14
|
+
placeholder,
|
|
15
|
+
onValueChange,
|
|
16
|
+
onBlur,
|
|
17
|
+
...props
|
|
18
|
+
}: FormFieldComponentProps<string>) => {
|
|
19
|
+
const handleChange = useCallback<NonNullable<TextAreaProps['onChange']>>(
|
|
20
|
+
(event) => onValueChange(type, event.target.value),
|
|
21
|
+
[type, onValueChange],
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
return (
|
|
25
|
+
<FormFieldWrapper<string> readonly={readonly} {...props}>
|
|
26
|
+
{({ value = '' }) => (
|
|
27
|
+
<Input.TextArea
|
|
28
|
+
rows={3}
|
|
29
|
+
disabled={!!readonly}
|
|
30
|
+
placeholder={placeholder}
|
|
31
|
+
value={value}
|
|
32
|
+
onChange={handleChange}
|
|
33
|
+
onBlur={onBlur}
|
|
34
|
+
/>
|
|
35
|
+
)}
|
|
36
|
+
</FormFieldWrapper>
|
|
37
|
+
);
|
|
38
|
+
};
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2024 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import React, { useCallback } from 'react';
|
|
6
|
+
|
|
7
|
+
import { Input, type TextInputProps } from '@dxos/react-ui';
|
|
8
|
+
|
|
9
|
+
import { type FormFieldComponentProps, FormFieldWrapper } from '../FormFieldComponent';
|
|
10
|
+
|
|
11
|
+
export const TextField = ({
|
|
12
|
+
type,
|
|
13
|
+
readonly,
|
|
14
|
+
placeholder,
|
|
15
|
+
onBlur,
|
|
16
|
+
onValueChange,
|
|
17
|
+
...props
|
|
18
|
+
}: FormFieldComponentProps<string>) => {
|
|
19
|
+
const handleChange = useCallback<NonNullable<TextInputProps['onChange']>>(
|
|
20
|
+
(event) => onValueChange(type, event.target.value),
|
|
21
|
+
[type, onValueChange],
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
return (
|
|
25
|
+
<FormFieldWrapper<string> readonly={readonly} {...props}>
|
|
26
|
+
{({ value = '' }) => (
|
|
27
|
+
<Input.TextInput
|
|
28
|
+
noAutoFill
|
|
29
|
+
disabled={!!readonly}
|
|
30
|
+
placeholder={placeholder}
|
|
31
|
+
value={value}
|
|
32
|
+
onBlur={onBlur}
|
|
33
|
+
onChange={handleChange}
|
|
34
|
+
/>
|
|
35
|
+
)}
|
|
36
|
+
</FormFieldWrapper>
|
|
37
|
+
);
|
|
38
|
+
};
|
|
@@ -5,39 +5,46 @@
|
|
|
5
5
|
import React from 'react';
|
|
6
6
|
|
|
7
7
|
import { Input } from '@dxos/react-ui';
|
|
8
|
-
import { mx } from '@dxos/
|
|
8
|
+
import { mx } from '@dxos/ui-theme';
|
|
9
9
|
import { safeParseFloat } from '@dxos/util';
|
|
10
10
|
|
|
11
|
-
import {
|
|
11
|
+
import { type FormFieldComponentProps, FormFieldLabel } from '../FormFieldComponent';
|
|
12
12
|
|
|
13
13
|
const gridCols = ['grid-cols-1', 'grid-cols-2', 'grid-cols-3', 'grid-cols-4'];
|
|
14
14
|
|
|
15
|
-
export const
|
|
15
|
+
export const TupleField = ({
|
|
16
16
|
binding,
|
|
17
17
|
type,
|
|
18
|
+
readonly,
|
|
18
19
|
label,
|
|
19
|
-
disabled,
|
|
20
20
|
getStatus,
|
|
21
21
|
getValue,
|
|
22
22
|
onValueChange,
|
|
23
23
|
onBlur,
|
|
24
|
-
}:
|
|
24
|
+
}: FormFieldComponentProps<Record<string, number>> & {
|
|
25
|
+
binding: string[];
|
|
26
|
+
}) => {
|
|
25
27
|
const { status, error } = getStatus();
|
|
26
28
|
|
|
27
29
|
// TODO(burdon): Generalize number/float/string, etc.
|
|
28
|
-
const values: Record<string, number> = getValue
|
|
30
|
+
const values: Record<string, number> = getValue() ?? {};
|
|
29
31
|
|
|
30
32
|
return (
|
|
31
33
|
<Input.Root validationValence={status}>
|
|
32
|
-
<
|
|
33
|
-
<div className={mx('grid gap-
|
|
34
|
+
<FormFieldLabel error={error} readonly={readonly} label={label} />
|
|
35
|
+
<div className={mx('grid gap-form-gap', gridCols[binding.length - 1])}>
|
|
34
36
|
{binding.map((prop) => (
|
|
35
37
|
<Input.TextInput
|
|
36
38
|
key={prop}
|
|
37
39
|
type='number'
|
|
38
|
-
disabled={
|
|
40
|
+
disabled={!!readonly}
|
|
39
41
|
value={values[prop]}
|
|
40
|
-
onChange={(event) =>
|
|
42
|
+
onChange={(event) => {
|
|
43
|
+
onValueChange(type, {
|
|
44
|
+
...values,
|
|
45
|
+
[prop]: safeParseFloat(event.target.value, 0),
|
|
46
|
+
});
|
|
47
|
+
}}
|
|
41
48
|
onBlur={onBlur}
|
|
42
49
|
/>
|
|
43
50
|
))}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2024 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
export * from './ArrayField';
|
|
6
|
+
export * from './BooleanField';
|
|
7
|
+
export * from './GeoPointField';
|
|
8
|
+
export * from './MarkdownField';
|
|
9
|
+
export * from './NumberField';
|
|
10
|
+
export * from './RefField';
|
|
11
|
+
export * from './SelectField';
|
|
12
|
+
export * from './SelectOptionField';
|
|
13
|
+
export * from './TextField';
|
|
14
|
+
export * from './TextAreaField';
|
|
15
|
+
export * from './TupleField';
|