@dxos/react-ui-form 0.8.2-main.2f9c567 → 0.8.2-main.30e4dbb
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/lib/browser/index.mjs +672 -543
- package/dist/lib/browser/index.mjs.map +4 -4
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/node/index.cjs +564 -442
- package/dist/lib/node/index.cjs.map +4 -4
- package/dist/lib/node/meta.json +1 -1
- package/dist/lib/node-esm/index.mjs +672 -543
- package/dist/lib/node-esm/index.mjs.map +4 -4
- package/dist/lib/node-esm/meta.json +1 -1
- package/dist/types/src/components/ControlSection/ControlSection.d.ts.map +1 -1
- package/dist/types/src/components/FieldEditor/FieldEditor.d.ts.map +1 -1
- package/dist/types/src/components/Form/ArrayField.d.ts.map +1 -1
- package/dist/types/src/components/Form/Defaults.d.ts.map +1 -1
- package/dist/types/src/components/Form/Form.d.ts +1 -2
- package/dist/types/src/components/Form/Form.d.ts.map +1 -1
- package/dist/types/src/components/Form/Form.stories.d.ts.map +1 -1
- package/dist/types/src/components/Form/FormActions.d.ts.map +1 -1
- package/dist/types/src/components/Form/FormContent.d.ts +1 -1
- package/dist/types/src/components/Form/FormContent.d.ts.map +1 -1
- package/dist/types/src/components/Form/FormContext.d.ts.map +1 -1
- package/dist/types/src/components/Form/Input.d.ts.map +1 -1
- package/dist/types/src/components/Form/RefField.d.ts +6 -18
- package/dist/types/src/components/Form/RefField.d.ts.map +1 -1
- package/dist/types/src/components/Form/custom/GeoPointInput.d.ts.map +1 -1
- package/dist/types/src/components/Form/custom/SelectOptionsInput.d.ts.map +1 -1
- package/dist/types/src/components/Form/custom/TupleInput.d.ts.map +1 -1
- package/dist/types/src/components/Form/factory.d.ts +1 -1
- package/dist/types/src/components/Form/factory.d.ts.map +1 -1
- package/dist/types/src/components/Form/index.d.ts +0 -1
- package/dist/types/src/components/Form/index.d.ts.map +1 -1
- package/dist/types/src/components/ViewEditor/Test.stories.d.ts.map +1 -1
- package/dist/types/src/components/ViewEditor/ViewEditor.d.ts.map +1 -1
- package/dist/types/src/components/deprecated/Form.d.ts.map +1 -1
- package/dist/types/src/components/deprecated/Form.stories.d.ts.map +1 -1
- package/dist/types/src/components/deprecated/FormInput.d.ts.map +1 -1
- package/dist/types/src/components/testing.d.ts.map +1 -1
- package/dist/types/src/hooks/index.d.ts +1 -0
- package/dist/types/src/hooks/index.d.ts.map +1 -1
- package/dist/types/src/hooks/useForm.d.ts.map +1 -1
- package/dist/types/src/hooks/useQueryRefOptions.d.ts +23 -0
- package/dist/types/src/hooks/useQueryRefOptions.d.ts.map +1 -0
- package/dist/types/src/hooks/useRefQueryLookupHandler.d.ts +1 -1
- package/dist/types/src/hooks/useRefQueryLookupHandler.d.ts.map +1 -1
- 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 +2 -0
- package/dist/types/src/util/index.d.ts.map +1 -1
- package/dist/types/src/util/refs.d.ts +9 -0
- package/dist/types/src/util/refs.d.ts.map +1 -0
- package/dist/types/src/util/schema.d.ts +3 -0
- package/dist/types/src/util/schema.d.ts.map +1 -0
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/package.json +25 -24
- package/src/components/ControlSection/ControlSection.stories.tsx +1 -1
- package/src/components/FieldEditor/FieldEditor.stories.tsx +1 -1
- package/src/components/Form/ArrayField.tsx +2 -2
- package/src/components/Form/Form.stories.tsx +2 -1
- package/src/components/Form/Form.tsx +1 -2
- package/src/components/Form/FormContent.tsx +44 -16
- package/src/components/Form/FormContext.tsx +3 -1
- package/src/components/Form/Input.tsx +3 -9
- package/src/components/Form/RefField.tsx +144 -99
- package/src/components/Form/custom/SelectOptionsInput.stories.tsx +1 -1
- package/src/components/Form/factory.tsx +1 -1
- package/src/components/Form/index.ts +0 -1
- package/src/components/ViewEditor/ViewEditor.stories.tsx +1 -1
- package/src/hooks/index.ts +2 -1
- package/src/hooks/useQueryRefOptions.ts +53 -0
- package/src/hooks/useRefQueryLookupHandler.ts +2 -4
- package/src/util/index.ts +3 -1
- package/src/util/refs.ts +38 -0
- package/src/util/schema.ts +14 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dxos/react-ui-form",
|
|
3
|
-
"version": "0.8.2-main.
|
|
3
|
+
"version": "0.8.2-main.30e4dbb",
|
|
4
4
|
"description": "Data components.",
|
|
5
5
|
"homepage": "https://dxos.org",
|
|
6
6
|
"bugs": "https://github.com/dxos/dxos/issues",
|
|
@@ -28,21 +28,22 @@
|
|
|
28
28
|
"@radix-ui/react-context": "1.1.1",
|
|
29
29
|
"date-fns": "^3.3.1",
|
|
30
30
|
"jsonpath": "^1.1.1",
|
|
31
|
-
"@dxos/
|
|
32
|
-
"@dxos/
|
|
33
|
-
"@dxos/
|
|
34
|
-
"@dxos/echo-
|
|
35
|
-
"@dxos/effect": "0.8.2-main.
|
|
36
|
-
"@dxos/invariant": "0.8.2-main.
|
|
37
|
-
"@dxos/
|
|
38
|
-
"@dxos/
|
|
39
|
-
"@dxos/
|
|
40
|
-
"@dxos/
|
|
41
|
-
"@dxos/react-ui-
|
|
42
|
-
"@dxos/
|
|
43
|
-
"@dxos/react-ui-
|
|
44
|
-
"@dxos/
|
|
45
|
-
"@dxos/
|
|
31
|
+
"@dxos/async": "0.8.2-main.30e4dbb",
|
|
32
|
+
"@dxos/debug": "0.8.2-main.30e4dbb",
|
|
33
|
+
"@dxos/echo-db": "0.8.2-main.30e4dbb",
|
|
34
|
+
"@dxos/echo-schema": "0.8.2-main.30e4dbb",
|
|
35
|
+
"@dxos/effect": "0.8.2-main.30e4dbb",
|
|
36
|
+
"@dxos/invariant": "0.8.2-main.30e4dbb",
|
|
37
|
+
"@dxos/keys": "0.8.2-main.30e4dbb",
|
|
38
|
+
"@dxos/log": "0.8.2-main.30e4dbb",
|
|
39
|
+
"@dxos/react-client": "0.8.2-main.30e4dbb",
|
|
40
|
+
"@dxos/live-object": "0.8.2-main.30e4dbb",
|
|
41
|
+
"@dxos/react-ui-list": "0.8.2-main.30e4dbb",
|
|
42
|
+
"@dxos/react-ui-pickers": "0.8.2-main.30e4dbb",
|
|
43
|
+
"@dxos/react-ui-tag-picker": "0.8.2-main.30e4dbb",
|
|
44
|
+
"@dxos/schema": "0.8.2-main.30e4dbb",
|
|
45
|
+
"@dxos/react-ui-types": "0.8.2-main.30e4dbb",
|
|
46
|
+
"@dxos/util": "0.8.2-main.30e4dbb"
|
|
46
47
|
},
|
|
47
48
|
"devDependencies": {
|
|
48
49
|
"@phosphor-icons/react": "^2.1.5",
|
|
@@ -54,20 +55,20 @@
|
|
|
54
55
|
"react": "~18.2.0",
|
|
55
56
|
"react-dom": "~18.2.0",
|
|
56
57
|
"vite": "5.4.7",
|
|
57
|
-
"@dxos/echo-signals": "0.8.2-main.
|
|
58
|
-
"@dxos/random": "0.8.2-main.
|
|
59
|
-
"@dxos/react-ui": "0.8.2-main.
|
|
60
|
-
"@dxos/react-ui-
|
|
61
|
-
"@dxos/
|
|
62
|
-
"@dxos/
|
|
58
|
+
"@dxos/echo-signals": "0.8.2-main.30e4dbb",
|
|
59
|
+
"@dxos/random": "0.8.2-main.30e4dbb",
|
|
60
|
+
"@dxos/react-ui": "0.8.2-main.30e4dbb",
|
|
61
|
+
"@dxos/react-ui-theme": "0.8.2-main.30e4dbb",
|
|
62
|
+
"@dxos/react-ui-syntax-highlighter": "0.8.2-main.30e4dbb",
|
|
63
|
+
"@dxos/storybook-utils": "0.8.2-main.30e4dbb"
|
|
63
64
|
},
|
|
64
65
|
"peerDependencies": {
|
|
65
66
|
"@phosphor-icons/react": "^2.1.5",
|
|
66
67
|
"effect": "3.13.3",
|
|
67
68
|
"react": "~18.2.0",
|
|
68
69
|
"react-dom": "~18.2.0",
|
|
69
|
-
"@dxos/react-ui
|
|
70
|
-
"@dxos/react-ui": "0.8.2-main.
|
|
70
|
+
"@dxos/react-ui": "0.8.2-main.30e4dbb",
|
|
71
|
+
"@dxos/react-ui-theme": "0.8.2-main.30e4dbb"
|
|
71
72
|
},
|
|
72
73
|
"publishConfig": {
|
|
73
74
|
"access": "public"
|
|
@@ -14,7 +14,7 @@ import translations from '../../translations';
|
|
|
14
14
|
const meta: Meta<ControlSectionProps> = {
|
|
15
15
|
title: 'ui/react-ui-form/ControlSection',
|
|
16
16
|
component: ControlSection,
|
|
17
|
-
decorators: [withLayout(
|
|
17
|
+
decorators: [withLayout(), withTheme],
|
|
18
18
|
parameters: {
|
|
19
19
|
translations,
|
|
20
20
|
},
|
|
@@ -37,7 +37,7 @@ const meta: Meta<StoryProps> = {
|
|
|
37
37
|
title: 'ui/react-ui-form/FieldEditor',
|
|
38
38
|
component: FieldEditor,
|
|
39
39
|
render: DefaultStory,
|
|
40
|
-
decorators: [withLayout({ fullscreen: true
|
|
40
|
+
decorators: [withLayout({ fullscreen: true }), withTheme],
|
|
41
41
|
parameters: {
|
|
42
42
|
translations,
|
|
43
43
|
},
|
|
@@ -17,6 +17,7 @@ import { FormField } from './FormContent';
|
|
|
17
17
|
import { useFormValues, type FormInputStateProps } from './FormContext';
|
|
18
18
|
import { InputHeader, type InputComponent } from './Input';
|
|
19
19
|
import { translationKey } from '../../translations';
|
|
20
|
+
import { findArrayElementType } from '../../util';
|
|
20
21
|
|
|
21
22
|
const padding = 'px-2';
|
|
22
23
|
|
|
@@ -38,8 +39,7 @@ export const ArrayField = ({ property, readonly, path, inputProps, Custom, looku
|
|
|
38
39
|
invariant(Array.isArray(values), `Values at path ${path?.join('.')} must be an array.`);
|
|
39
40
|
const label = title ?? pipe(name, capitalize);
|
|
40
41
|
|
|
41
|
-
const
|
|
42
|
-
const elementType = (tupleType as SchemaAST.TupleType | undefined)?.rest[0]?.type;
|
|
42
|
+
const elementType = findArrayElementType(ast);
|
|
43
43
|
|
|
44
44
|
const getDefaultObjectValue = (typeNode: SchemaAST.AST): any => {
|
|
45
45
|
const baseNode = findNode(typeNode, isDiscriminatedUnion);
|
|
@@ -60,7 +60,7 @@ const meta: Meta<StoryProps<any>> = {
|
|
|
60
60
|
title: 'ui/react-ui-form/Form',
|
|
61
61
|
component: Form,
|
|
62
62
|
render: DefaultStory,
|
|
63
|
-
decorators: [withLayout({ fullscreen: true
|
|
63
|
+
decorators: [withLayout({ fullscreen: true }), withTheme],
|
|
64
64
|
parameters: {
|
|
65
65
|
translations,
|
|
66
66
|
},
|
|
@@ -250,6 +250,7 @@ export const Enum: StoryObj<FormProps<ColorType>> = {
|
|
|
250
250
|
const RefSchema = Schema.Struct({
|
|
251
251
|
contact: Ref(ContactType).annotations({ title: 'Contact Reference' }),
|
|
252
252
|
optionalContact: Schema.optional(Ref(ContactType).annotations({ title: 'Optional Contact Reference' })),
|
|
253
|
+
refArray: Schema.optional(Schema.Array(Ref(ContactType))),
|
|
253
254
|
unknownExpando: Schema.optional(Ref(Expando).annotations({ title: 'Optional Ref to an Expando (DXN Input)' })),
|
|
254
255
|
});
|
|
255
256
|
|
|
@@ -14,8 +14,7 @@ import { FormActions } from './FormActions';
|
|
|
14
14
|
import { FormFields } from './FormContent';
|
|
15
15
|
import { FormProvider } from './FormContext';
|
|
16
16
|
import { type InputProps, type InputComponent } from './Input';
|
|
17
|
-
import { type QueryRefOptions } from '
|
|
18
|
-
import { type FormOptions } from '../../hooks';
|
|
17
|
+
import { type FormOptions, type QueryRefOptions } from '../../hooks';
|
|
19
18
|
|
|
20
19
|
export type PropsFilter<T extends BaseObject> = (props: SchemaProperty<T>[]) => SchemaProperty<T>[];
|
|
21
20
|
|
|
@@ -16,8 +16,10 @@ import { SelectInput } from './Defaults';
|
|
|
16
16
|
import { type ComponentLookup } from './Form';
|
|
17
17
|
import { useInputProps, useFormValues } from './FormContext';
|
|
18
18
|
import { type InputComponent } from './Input';
|
|
19
|
-
import { RefField
|
|
19
|
+
import { RefField } from './RefField';
|
|
20
20
|
import { getInputComponent } from './factory';
|
|
21
|
+
import { type QueryRefOptions } from '../../hooks';
|
|
22
|
+
import { getRefProps } from '../../util';
|
|
21
23
|
|
|
22
24
|
export type FormFieldProps = {
|
|
23
25
|
property: SchemaProperty<any>;
|
|
@@ -48,6 +50,10 @@ export const FormField = ({
|
|
|
48
50
|
[examples, description],
|
|
49
51
|
);
|
|
50
52
|
|
|
53
|
+
//
|
|
54
|
+
// Registry and Custom.
|
|
55
|
+
//
|
|
56
|
+
|
|
51
57
|
const FoundComponent = lookupComponent?.({
|
|
52
58
|
prop: name,
|
|
53
59
|
schema: Schema.make(ast),
|
|
@@ -81,10 +87,40 @@ export const FormField = ({
|
|
|
81
87
|
);
|
|
82
88
|
}
|
|
83
89
|
|
|
90
|
+
//
|
|
91
|
+
// Refs.
|
|
92
|
+
//
|
|
93
|
+
|
|
94
|
+
const refProps = getRefProps(property);
|
|
95
|
+
if (refProps) {
|
|
96
|
+
return (
|
|
97
|
+
<RefField
|
|
98
|
+
ast={refProps.ast}
|
|
99
|
+
array={refProps.isArray}
|
|
100
|
+
type={type}
|
|
101
|
+
format={format}
|
|
102
|
+
label={label}
|
|
103
|
+
placeholder={placeholder}
|
|
104
|
+
disabled={readonly}
|
|
105
|
+
inputOnly={inline}
|
|
106
|
+
onQueryRefOptions={onQueryRefOptions}
|
|
107
|
+
{...inputProps}
|
|
108
|
+
/>
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
//
|
|
113
|
+
// Arrays.
|
|
114
|
+
//
|
|
115
|
+
|
|
84
116
|
if (array) {
|
|
85
117
|
return <ArrayField property={property} path={path} inputProps={inputProps} readonly={readonly} Custom={Custom} />;
|
|
86
118
|
}
|
|
87
119
|
|
|
120
|
+
//
|
|
121
|
+
// Standard Inputs.
|
|
122
|
+
//
|
|
123
|
+
|
|
88
124
|
const InputComponent = getInputComponent(type, format);
|
|
89
125
|
if (InputComponent) {
|
|
90
126
|
return (
|
|
@@ -102,6 +138,10 @@ export const FormField = ({
|
|
|
102
138
|
);
|
|
103
139
|
}
|
|
104
140
|
|
|
141
|
+
//
|
|
142
|
+
// Select.
|
|
143
|
+
//
|
|
144
|
+
|
|
105
145
|
if (options) {
|
|
106
146
|
return (
|
|
107
147
|
<div role='none'>
|
|
@@ -119,21 +159,9 @@ export const FormField = ({
|
|
|
119
159
|
);
|
|
120
160
|
}
|
|
121
161
|
|
|
122
|
-
//
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
<RefField
|
|
126
|
-
ast={ast}
|
|
127
|
-
type={type}
|
|
128
|
-
label={label}
|
|
129
|
-
readonly={readonly}
|
|
130
|
-
placeholder={placeholder}
|
|
131
|
-
inline={inline}
|
|
132
|
-
onQueryRefOptions={onQueryRefOptions}
|
|
133
|
-
inputProps={inputProps}
|
|
134
|
-
/>
|
|
135
|
-
);
|
|
136
|
-
}
|
|
162
|
+
//
|
|
163
|
+
// Nested Objects.
|
|
164
|
+
//
|
|
137
165
|
|
|
138
166
|
if (type === 'object') {
|
|
139
167
|
const baseNode = findNode(ast, isDiscriminatedUnion);
|
|
@@ -76,7 +76,9 @@ export const FormProvider = ({
|
|
|
76
76
|
const keyIsEnter = event.key === 'Enter';
|
|
77
77
|
const modifierUsed = event.ctrlKey || event.altKey || event.metaKey || event.shiftKey;
|
|
78
78
|
const inputIsTextarea = (event.target as HTMLElement).tagName.toLowerCase() === 'textarea';
|
|
79
|
-
const inputOptOut =
|
|
79
|
+
const inputOptOut =
|
|
80
|
+
(event.target as HTMLElement).hasAttribute('data-no-submit') ||
|
|
81
|
+
(event.target as HTMLElement).closest('[data-no-submit]') !== null;
|
|
80
82
|
|
|
81
83
|
// Regular inputs: Submit on Enter (no modifiers).
|
|
82
84
|
const shouldSubmitRegularInput = !inputIsTextarea && keyIsEnter && !modifierUsed;
|
|
@@ -37,15 +37,9 @@ export const InputHeader = ({ children, error }: InputHeaderProps) => {
|
|
|
37
37
|
<div role='none' className='flex justify-between items-center mb-1'>
|
|
38
38
|
{children}
|
|
39
39
|
{error && (
|
|
40
|
-
<Tooltip.
|
|
41
|
-
<
|
|
42
|
-
|
|
43
|
-
</Tooltip.Trigger>
|
|
44
|
-
<Tooltip.Content side='bottom'>
|
|
45
|
-
<Tooltip.Arrow />
|
|
46
|
-
{error}
|
|
47
|
-
</Tooltip.Content>
|
|
48
|
-
</Tooltip.Root>
|
|
40
|
+
<Tooltip.Trigger asChild content={error} side='bottom'>
|
|
41
|
+
<Icon icon='ph--warning--regular' size={4} classNames={errorText} />
|
|
42
|
+
</Tooltip.Trigger>
|
|
49
43
|
)}
|
|
50
44
|
</div>
|
|
51
45
|
);
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
//
|
|
4
4
|
|
|
5
5
|
import { type SchemaAST } from 'effect';
|
|
6
|
-
import React, {
|
|
6
|
+
import React, { useCallback, useMemo, type FocusEvent } from 'react';
|
|
7
7
|
|
|
8
8
|
import {
|
|
9
9
|
Expando,
|
|
@@ -11,117 +11,170 @@ import {
|
|
|
11
11
|
Ref,
|
|
12
12
|
ReferenceAnnotationId,
|
|
13
13
|
type ReferenceAnnotationValue,
|
|
14
|
-
type TypeAnnotation,
|
|
15
14
|
} from '@dxos/echo-schema';
|
|
16
|
-
import { findAnnotation
|
|
15
|
+
import { findAnnotation } from '@dxos/effect';
|
|
17
16
|
import { DXN } from '@dxos/keys';
|
|
18
|
-
import {
|
|
19
|
-
import {
|
|
20
|
-
import {
|
|
21
|
-
|
|
22
|
-
import {
|
|
23
|
-
import { type
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
type: SimpleType;
|
|
30
|
-
label: string;
|
|
31
|
-
readonly?: boolean;
|
|
32
|
-
placeholder?: string;
|
|
33
|
-
inline?: boolean;
|
|
17
|
+
import { Input } from '@dxos/react-ui';
|
|
18
|
+
import { TagPicker, type TagPickerMode, type TagPickerItemData } from '@dxos/react-ui-tag-picker';
|
|
19
|
+
import { isNonNullable } from '@dxos/util';
|
|
20
|
+
|
|
21
|
+
import { TextInput } from './Defaults';
|
|
22
|
+
import { InputHeader, type InputProps } from './Input';
|
|
23
|
+
import { type QueryRefOptions, useQueryRefOptions } from '../../hooks';
|
|
24
|
+
|
|
25
|
+
type RefFieldProps = InputProps & {
|
|
26
|
+
ast?: SchemaAST.AST;
|
|
27
|
+
array?: boolean;
|
|
34
28
|
onQueryRefOptions?: QueryRefOptions;
|
|
35
|
-
inputProps: FormInputStateProps;
|
|
36
29
|
};
|
|
37
30
|
|
|
38
31
|
export const RefField = ({
|
|
39
|
-
ast,
|
|
40
32
|
type,
|
|
41
33
|
label,
|
|
42
|
-
|
|
34
|
+
disabled,
|
|
43
35
|
placeholder,
|
|
44
|
-
|
|
36
|
+
inputOnly,
|
|
37
|
+
array,
|
|
38
|
+
ast,
|
|
39
|
+
getValue,
|
|
40
|
+
onBlur,
|
|
45
41
|
onQueryRefOptions,
|
|
46
|
-
|
|
42
|
+
onValueChange,
|
|
43
|
+
...restInputProps
|
|
47
44
|
}: RefFieldProps) => {
|
|
48
|
-
const
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
45
|
+
const refTypeInfo = useMemo(
|
|
46
|
+
() => (ast ? findAnnotation<ReferenceAnnotationValue>(ast, ReferenceAnnotationId) : undefined),
|
|
47
|
+
[ast],
|
|
48
|
+
);
|
|
49
|
+
const { options: availableOptions, loading: _loading } = useQueryRefOptions({ refTypeInfo, onQueryRefOptions });
|
|
50
|
+
|
|
51
|
+
if ((refTypeInfo && refTypeInfo?.typename === getTypeAnnotation(Expando)?.typename) || !onQueryRefOptions) {
|
|
52
|
+
// If ref type is expando, fall back to taking a DXN in string format.
|
|
53
|
+
return (
|
|
54
|
+
<RefFieldFallback
|
|
55
|
+
{...{ type, label, placeholder, disabled, inputOnly, getValue, onBlur, onValueChange, ...restInputProps }}
|
|
56
|
+
/>
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const handleGetValue = useCallback((): TagPickerItemData[] => {
|
|
61
|
+
const formValue = getValue();
|
|
57
62
|
|
|
58
|
-
const
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
const
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
value: option.dxn.toString(),
|
|
66
|
-
})),
|
|
67
|
-
);
|
|
68
|
-
} catch (error) {
|
|
69
|
-
log.error('Failed to fetch ref options:', error);
|
|
70
|
-
setRefOptions([]);
|
|
71
|
-
} finally {
|
|
72
|
-
setLoading(false);
|
|
63
|
+
const unknownToRefOption = (val: unknown) => {
|
|
64
|
+
if (Ref.isRef(val)) {
|
|
65
|
+
const dxnString = val.dxn.toString();
|
|
66
|
+
const matchingOption = availableOptions.find((option) => option.id === dxnString);
|
|
67
|
+
if (matchingOption) {
|
|
68
|
+
return matchingOption;
|
|
69
|
+
}
|
|
73
70
|
}
|
|
71
|
+
return undefined;
|
|
74
72
|
};
|
|
75
73
|
|
|
76
|
-
|
|
77
|
-
|
|
74
|
+
if (array && Array.isArray(formValue)) {
|
|
75
|
+
return formValue.map(unknownToRefOption).filter(isNonNullable) ?? [];
|
|
76
|
+
}
|
|
78
77
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
78
|
+
const option = unknownToRefOption(formValue);
|
|
79
|
+
if (option) {
|
|
80
|
+
return [option];
|
|
81
|
+
}
|
|
82
82
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
const handleOnValueChange = (_type: any, dxnString: string) => {
|
|
86
|
-
const dxn = DXN.tryParse(dxnString);
|
|
87
|
-
if (dxn) {
|
|
88
|
-
onValueChange?.('object', refFromDXN(dxn));
|
|
89
|
-
} else if (dxnString === '') {
|
|
90
|
-
onValueChange?.('object', undefined);
|
|
91
|
-
} else {
|
|
92
|
-
onValueChange?.('string', dxnString);
|
|
93
|
-
}
|
|
94
|
-
};
|
|
83
|
+
return [];
|
|
84
|
+
}, [getValue, availableOptions, array]);
|
|
95
85
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
86
|
+
const handleSearch = useCallback(
|
|
87
|
+
(text: string, ids: string[]): TagPickerItemData[] =>
|
|
88
|
+
availableOptions
|
|
89
|
+
.filter((option) => !ids.includes(option.id))
|
|
90
|
+
.filter((option) => option.label.toLowerCase().includes(text.toLowerCase()))
|
|
91
|
+
.map((option) => ({ id: option.id, label: option.label, hue: option.hue as any })),
|
|
92
|
+
[availableOptions],
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
const handleUpdate = useCallback(
|
|
96
|
+
(ids: string[]) => {
|
|
97
|
+
if (ids.length === 0) {
|
|
98
|
+
onValueChange('object', undefined);
|
|
99
|
+
return;
|
|
100
100
|
}
|
|
101
|
-
|
|
102
|
-
|
|
101
|
+
|
|
102
|
+
const refs = ids
|
|
103
|
+
.map((id) => {
|
|
104
|
+
const item = availableOptions.find((option) => option.id === id);
|
|
105
|
+
if (item) {
|
|
106
|
+
const dxn = DXN.parse(item.id);
|
|
107
|
+
return Ref.fromDXN(dxn);
|
|
108
|
+
}
|
|
109
|
+
return null;
|
|
110
|
+
})
|
|
111
|
+
.filter(isNonNullable);
|
|
112
|
+
|
|
113
|
+
if (array) {
|
|
114
|
+
onValueChange('object', refs);
|
|
115
|
+
} else {
|
|
116
|
+
onValueChange('object', refs[0]);
|
|
103
117
|
}
|
|
118
|
+
},
|
|
119
|
+
[availableOptions, array, onValueChange],
|
|
120
|
+
);
|
|
104
121
|
|
|
105
|
-
|
|
106
|
-
};
|
|
122
|
+
const tagPickerMode: TagPickerMode = useMemo(() => (array ? 'multi-select' : 'single-select'), [array]);
|
|
107
123
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
type={type}
|
|
111
|
-
label={label}
|
|
112
|
-
disabled={readonly}
|
|
113
|
-
placeholder={placeholder}
|
|
114
|
-
inputOnly={inline}
|
|
115
|
-
getValue={handleGetValue as <V>() => V | undefined}
|
|
116
|
-
onValueChange={handleOnValueChange}
|
|
117
|
-
{...restInputProps}
|
|
118
|
-
/>
|
|
119
|
-
);
|
|
124
|
+
if (!refTypeInfo) {
|
|
125
|
+
return null;
|
|
120
126
|
}
|
|
121
127
|
|
|
128
|
+
const { status, error } = restInputProps.getStatus();
|
|
129
|
+
|
|
130
|
+
return (
|
|
131
|
+
<Input.Root validationValence={status}>
|
|
132
|
+
{!inputOnly && (
|
|
133
|
+
<InputHeader error={error}>
|
|
134
|
+
<Input.Label>{label}</Input.Label>
|
|
135
|
+
</InputHeader>
|
|
136
|
+
)}
|
|
137
|
+
<div data-no-submit>
|
|
138
|
+
<TagPicker
|
|
139
|
+
items={handleGetValue()}
|
|
140
|
+
mode={tagPickerMode}
|
|
141
|
+
onBlur={(event) => onBlur(event as unknown as FocusEvent<HTMLElement>)}
|
|
142
|
+
onUpdate={handleUpdate}
|
|
143
|
+
onSearch={handleSearch}
|
|
144
|
+
classNames='rounded-sm bg-input p-1.5'
|
|
145
|
+
/>
|
|
146
|
+
</div>
|
|
147
|
+
{inputOnly && <Input.DescriptionAndValidation>{error}</Input.DescriptionAndValidation>}
|
|
148
|
+
</Input.Root>
|
|
149
|
+
);
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
const RefFieldFallback = ({
|
|
153
|
+
type,
|
|
154
|
+
label,
|
|
155
|
+
disabled,
|
|
156
|
+
placeholder,
|
|
157
|
+
inputOnly,
|
|
158
|
+
getValue,
|
|
159
|
+
onValueChange,
|
|
160
|
+
...restInputProps
|
|
161
|
+
}: InputProps) => {
|
|
162
|
+
const handleOnValueChange = (_type: any, dxnString: string) => {
|
|
163
|
+
const dxn = DXN.tryParse(dxnString);
|
|
164
|
+
if (dxn) {
|
|
165
|
+
onValueChange?.('object', Ref.fromDXN(dxn));
|
|
166
|
+
} else if (dxnString === '') {
|
|
167
|
+
onValueChange?.('object', undefined);
|
|
168
|
+
} else {
|
|
169
|
+
onValueChange?.('string', dxnString);
|
|
170
|
+
}
|
|
171
|
+
};
|
|
172
|
+
|
|
122
173
|
const handleGetValue = () => {
|
|
123
174
|
const formValue = getValue();
|
|
124
|
-
|
|
175
|
+
if (typeof formValue === 'string') {
|
|
176
|
+
return formValue;
|
|
177
|
+
}
|
|
125
178
|
if (Ref.isRef(formValue)) {
|
|
126
179
|
return formValue.dxn.toString();
|
|
127
180
|
}
|
|
@@ -129,23 +182,15 @@ export const RefField = ({
|
|
|
129
182
|
return undefined;
|
|
130
183
|
};
|
|
131
184
|
|
|
132
|
-
const handleValueChanged = (_type: any, dxnString: string) => {
|
|
133
|
-
const dxn = DXN.parse(dxnString);
|
|
134
|
-
const ref = refFromDXN(dxn);
|
|
135
|
-
|
|
136
|
-
onValueChange('object', ref);
|
|
137
|
-
};
|
|
138
|
-
|
|
139
185
|
return (
|
|
140
|
-
<
|
|
186
|
+
<TextInput
|
|
141
187
|
type={type}
|
|
142
188
|
label={label}
|
|
143
|
-
disabled={
|
|
144
|
-
placeholder={
|
|
145
|
-
inputOnly={
|
|
189
|
+
disabled={disabled}
|
|
190
|
+
placeholder={placeholder}
|
|
191
|
+
inputOnly={inputOnly}
|
|
146
192
|
getValue={handleGetValue as <V>() => V | undefined}
|
|
147
|
-
onValueChange={
|
|
148
|
-
options={refOptions}
|
|
193
|
+
onValueChange={handleOnValueChange}
|
|
149
194
|
{...restInputProps}
|
|
150
195
|
/>
|
|
151
196
|
);
|
|
@@ -47,7 +47,7 @@ const DefaultStory = () => {
|
|
|
47
47
|
const meta: Meta = {
|
|
48
48
|
title: 'ui/react-ui-form/SelectOptionsInput',
|
|
49
49
|
component: SelectOptionInput,
|
|
50
|
-
decorators: [withTheme, withLayout(
|
|
50
|
+
decorators: [withTheme, withLayout()],
|
|
51
51
|
parameters: { translations },
|
|
52
52
|
};
|
|
53
53
|
|
|
@@ -12,7 +12,7 @@ import { GeoPointInput } from './custom';
|
|
|
12
12
|
/**
|
|
13
13
|
* Get property input component.
|
|
14
14
|
*/
|
|
15
|
-
export const getInputComponent = (type: SimpleType, format?: FormatEnum): InputComponent | undefined => {
|
|
15
|
+
export const getInputComponent = (type: SimpleType, format?: FormatEnum | string): InputComponent | undefined => {
|
|
16
16
|
switch (format) {
|
|
17
17
|
case FormatEnum.GeoPoint:
|
|
18
18
|
return GeoPointInput;
|
|
@@ -82,7 +82,7 @@ const meta: Meta<typeof ViewEditor> = {
|
|
|
82
82
|
title: 'ui/react-ui-form/ViewEditor',
|
|
83
83
|
component: ViewEditor,
|
|
84
84
|
render: DefaultStory,
|
|
85
|
-
decorators: [withClientProvider({ createSpace: true }), withLayout({ fullscreen: true
|
|
85
|
+
decorators: [withClientProvider({ createSpace: true }), withLayout({ fullscreen: true }), withTheme],
|
|
86
86
|
parameters: {
|
|
87
87
|
translations,
|
|
88
88
|
},
|
package/src/hooks/index.ts
CHANGED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2025 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { useEffect, useState } from 'react';
|
|
6
|
+
|
|
7
|
+
import { type TypeAnnotation } from '@dxos/echo-schema';
|
|
8
|
+
import { type DXN } from '@dxos/keys';
|
|
9
|
+
import { log } from '@dxos/log';
|
|
10
|
+
import { type TagPickerItemData } from '@dxos/react-ui-tag-picker';
|
|
11
|
+
import { type MaybePromise } from '@dxos/util';
|
|
12
|
+
|
|
13
|
+
export type RefOption = { dxn: DXN; label?: string };
|
|
14
|
+
export type QueryRefOptions = (type: TypeAnnotation) => MaybePromise<RefOption[]>;
|
|
15
|
+
|
|
16
|
+
type UseQueryRefOptionsProps = { refTypeInfo: TypeAnnotation | undefined; onQueryRefOptions?: QueryRefOptions };
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Hook to query reference options based on type information.
|
|
20
|
+
* Used internally within forms to fetch and format reference options for reference fields.
|
|
21
|
+
*/
|
|
22
|
+
export const useQueryRefOptions = ({ refTypeInfo, onQueryRefOptions }: UseQueryRefOptionsProps) => {
|
|
23
|
+
const [options, setOptions] = useState<TagPickerItemData[]>([]);
|
|
24
|
+
const [loading, setLoading] = useState(false);
|
|
25
|
+
|
|
26
|
+
useEffect(() => {
|
|
27
|
+
if (!refTypeInfo || !onQueryRefOptions) {
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const fetchOptions = async () => {
|
|
32
|
+
setLoading(true);
|
|
33
|
+
try {
|
|
34
|
+
const fetchedOptions = await onQueryRefOptions(refTypeInfo);
|
|
35
|
+
setOptions(
|
|
36
|
+
fetchedOptions.map((option) => {
|
|
37
|
+
const dxn = option.dxn.toString() as string;
|
|
38
|
+
return { id: dxn, label: option.label ?? dxn, hue: 'neutral' as any };
|
|
39
|
+
}),
|
|
40
|
+
);
|
|
41
|
+
} catch (error) {
|
|
42
|
+
log.error('Failed to fetch ref options:', error);
|
|
43
|
+
setOptions([]);
|
|
44
|
+
} finally {
|
|
45
|
+
setLoading(false);
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
void fetchOptions();
|
|
50
|
+
}, [refTypeInfo, onQueryRefOptions]);
|
|
51
|
+
|
|
52
|
+
return { options, loading };
|
|
53
|
+
};
|