@dxos/react-ui-form 0.8.2-staging.7ac8446 → 0.8.2
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 +1818 -1309
- package/dist/lib/browser/index.mjs.map +4 -4
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/node/index.cjs +1774 -1273
- package/dist/lib/node/index.cjs.map +4 -4
- package/dist/lib/node/meta.json +1 -1
- package/dist/lib/node-esm/index.mjs +1818 -1309
- 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.d.ts → ControlSection/ControlSection.d.ts} +9 -6
- package/dist/types/src/components/ControlSection/ControlSection.d.ts.map +1 -0
- package/dist/types/src/components/ControlSection/ControlSection.stories.d.ts +8 -0
- package/dist/types/src/components/ControlSection/ControlSection.stories.d.ts.map +1 -0
- package/dist/types/src/components/ControlSection/index.d.ts +2 -0
- package/dist/types/src/components/ControlSection/index.d.ts.map +1 -0
- package/dist/types/src/components/FieldEditor/FieldEditor.d.ts.map +1 -1
- package/dist/types/src/components/FieldEditor/FieldEditor.test.d.ts +2 -0
- package/dist/types/src/components/FieldEditor/FieldEditor.test.d.ts.map +1 -0
- package/dist/types/src/components/Form/ArrayField.d.ts.map +1 -1
- package/dist/types/src/components/Form/Defaults.d.ts +4 -4
- package/dist/types/src/components/Form/Defaults.d.ts.map +1 -1
- package/dist/types/src/components/Form/Form.d.ts +6 -4
- package/dist/types/src/components/Form/Form.d.ts.map +1 -1
- package/dist/types/src/components/Form/Form.stories.d.ts +38 -36
- package/dist/types/src/components/Form/Form.stories.d.ts.map +1 -1
- package/dist/types/src/components/Form/FormActions.d.ts +1 -1
- package/dist/types/src/components/Form/FormActions.d.ts.map +1 -1
- package/dist/types/src/components/Form/FormContent.d.ts +16 -12
- package/dist/types/src/components/Form/FormContent.d.ts.map +1 -1
- package/dist/types/src/components/Form/FormContext.d.ts +2 -2
- 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 +12 -0
- package/dist/types/src/components/Form/RefField.d.ts.map +1 -0
- 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/SelectOptionsInput.stories.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.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 +2 -2
- package/dist/types/src/components/ViewEditor/ViewEditor.d.ts.map +1 -1
- package/dist/types/src/components/ViewEditor/ViewEditor.stories.d.ts +5 -3
- package/dist/types/src/components/ViewEditor/ViewEditor.stories.d.ts.map +1 -1
- package/dist/types/src/components/deprecated/Form.d.ts +2 -2
- 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/index.d.ts.map +1 -1
- package/dist/types/src/components/testing.d.ts.map +1 -1
- package/dist/types/src/hooks/index.d.ts +2 -0
- package/dist/types/src/hooks/index.d.ts.map +1 -1
- package/dist/types/src/hooks/useForm.d.ts +2 -2
- 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 +8 -0
- package/dist/types/src/hooks/useRefQueryLookupHandler.d.ts.map +1 -0
- package/dist/types/src/translations.d.ts +5 -0
- package/dist/types/src/translations.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/src/vitest-setup.d.ts +2 -0
- package/dist/types/src/vitest-setup.d.ts.map +1 -0
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/package.json +28 -25
- package/src/components/ControlSection/ControlSection.stories.tsx +29 -0
- package/src/components/{ControlSection.tsx → ControlSection/ControlSection.tsx} +40 -16
- package/src/components/ControlSection/index.ts +5 -0
- package/src/components/FieldEditor/FieldEditor.stories.tsx +1 -1
- package/src/components/FieldEditor/FieldEditor.test.tsx +49 -0
- package/src/components/FieldEditor/FieldEditor.tsx +3 -2
- package/src/components/Form/ArrayField.tsx +63 -50
- package/src/components/Form/Defaults.tsx +77 -48
- package/src/components/Form/Form.stories.tsx +134 -109
- package/src/components/Form/Form.tsx +8 -5
- package/src/components/Form/FormActions.tsx +15 -18
- package/src/components/Form/FormContent.tsx +116 -46
- package/src/components/Form/FormContext.tsx +24 -10
- package/src/components/Form/Input.tsx +4 -10
- package/src/components/Form/RefField.tsx +217 -0
- package/src/components/Form/custom/GeoPointInput.tsx +2 -2
- package/src/components/Form/custom/SelectOptionsInput.stories.tsx +6 -5
- package/src/components/Form/custom/SelectOptionsInput.tsx +2 -2
- package/src/components/Form/factory.tsx +1 -1
- package/src/components/Form/index.ts +0 -1
- package/src/components/ViewEditor/ViewEditor.stories.tsx +19 -9
- package/src/components/ViewEditor/ViewEditor.tsx +65 -35
- package/src/components/deprecated/Form.stories.tsx +6 -6
- package/src/components/deprecated/Form.tsx +6 -6
- package/src/components/deprecated/FormInput.tsx +1 -1
- package/src/components/index.ts +1 -0
- package/src/components/testing.tsx +5 -9
- package/src/hooks/index.ts +3 -1
- package/src/hooks/useForm.ts +4 -3
- package/src/hooks/useQueryRefOptions.ts +53 -0
- package/src/hooks/useRefQueryLookupHandler.ts +39 -0
- package/src/translations.ts +9 -2
- package/src/util/formatting.ts +15 -3
- package/src/util/index.ts +3 -1
- package/src/util/refs.ts +38 -0
- package/src/util/schema.ts +14 -0
- package/src/vitest-setup.ts +11 -0
- package/dist/types/src/components/ControlSection.d.ts.map +0 -1
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
//
|
|
2
|
-
// Copyright
|
|
2
|
+
// Copyright 2025 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import { pipe } from 'effect';
|
|
5
|
+
import { Schema, SchemaAST, pipe } from 'effect';
|
|
6
6
|
import { capitalize } from 'effect/String';
|
|
7
7
|
import React, { useMemo } from 'react';
|
|
8
8
|
|
|
9
|
-
import { AST, S } from '@dxos/echo-schema';
|
|
10
9
|
import { createJsonPath, findNode, getDiscriminatedType, isDiscriminatedUnion } from '@dxos/effect';
|
|
10
|
+
import { type ThemedClassName } from '@dxos/react-ui';
|
|
11
11
|
import { mx } from '@dxos/react-ui-theme';
|
|
12
12
|
import { getSchemaProperties, type SchemaProperty } from '@dxos/schema';
|
|
13
13
|
import { isNotFalsy } from '@dxos/util';
|
|
@@ -17,17 +17,10 @@ import { SelectInput } from './Defaults';
|
|
|
17
17
|
import { type ComponentLookup } from './Form';
|
|
18
18
|
import { useInputProps, useFormValues } from './FormContext';
|
|
19
19
|
import { type InputComponent } from './Input';
|
|
20
|
+
import { RefField } from './RefField';
|
|
20
21
|
import { getInputComponent } from './factory';
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
schema: S.Schema.All;
|
|
24
|
-
path?: (string | number)[];
|
|
25
|
-
filter?: (props: SchemaProperty<any>[]) => SchemaProperty<any>[];
|
|
26
|
-
sort?: string[];
|
|
27
|
-
readonly?: boolean;
|
|
28
|
-
lookupComponent?: ComponentLookup;
|
|
29
|
-
Custom?: Partial<Record<string, InputComponent>>;
|
|
30
|
-
};
|
|
22
|
+
import { type QueryRefOptions } from '../../hooks';
|
|
23
|
+
import { getRefProps } from '../../util';
|
|
31
24
|
|
|
32
25
|
export type FormFieldProps = {
|
|
33
26
|
property: SchemaProperty<any>;
|
|
@@ -35,11 +28,20 @@ export type FormFieldProps = {
|
|
|
35
28
|
readonly?: boolean;
|
|
36
29
|
/** Used to indicate if input should be presented inline (e.g. for array items). */
|
|
37
30
|
inline?: boolean;
|
|
31
|
+
onQueryRefOptions?: QueryRefOptions;
|
|
38
32
|
lookupComponent?: ComponentLookup;
|
|
39
33
|
Custom?: Partial<Record<string, InputComponent>>;
|
|
40
34
|
};
|
|
41
35
|
|
|
42
|
-
export const FormField = ({
|
|
36
|
+
export const FormField = ({
|
|
37
|
+
property,
|
|
38
|
+
path,
|
|
39
|
+
readonly,
|
|
40
|
+
inline,
|
|
41
|
+
onQueryRefOptions,
|
|
42
|
+
lookupComponent,
|
|
43
|
+
Custom,
|
|
44
|
+
}: FormFieldProps) => {
|
|
43
45
|
const inputProps = useInputProps(path);
|
|
44
46
|
const { ast, name, type, format, title, description, options, examples, array } = property;
|
|
45
47
|
|
|
@@ -49,9 +51,13 @@ export const FormField = ({ property, path, readonly, inline, lookupComponent, C
|
|
|
49
51
|
[examples, description],
|
|
50
52
|
);
|
|
51
53
|
|
|
54
|
+
//
|
|
55
|
+
// Registry and Custom.
|
|
56
|
+
//
|
|
57
|
+
|
|
52
58
|
const FoundComponent = lookupComponent?.({
|
|
53
59
|
prop: name,
|
|
54
|
-
schema:
|
|
60
|
+
schema: Schema.make(ast),
|
|
55
61
|
inputProps: {
|
|
56
62
|
type,
|
|
57
63
|
format,
|
|
@@ -63,7 +69,7 @@ export const FormField = ({ property, path, readonly, inline, lookupComponent, C
|
|
|
63
69
|
});
|
|
64
70
|
|
|
65
71
|
if (FoundComponent) {
|
|
66
|
-
return
|
|
72
|
+
return FoundComponent;
|
|
67
73
|
}
|
|
68
74
|
|
|
69
75
|
const jsonPath = createJsonPath(path ?? []);
|
|
@@ -82,56 +88,97 @@ export const FormField = ({ property, path, readonly, inline, lookupComponent, C
|
|
|
82
88
|
);
|
|
83
89
|
}
|
|
84
90
|
|
|
91
|
+
//
|
|
92
|
+
// Refs.
|
|
93
|
+
//
|
|
94
|
+
|
|
95
|
+
const refProps = getRefProps(property);
|
|
96
|
+
if (refProps) {
|
|
97
|
+
return (
|
|
98
|
+
<RefField
|
|
99
|
+
ast={refProps.ast}
|
|
100
|
+
array={refProps.isArray}
|
|
101
|
+
type={type}
|
|
102
|
+
format={format}
|
|
103
|
+
label={label}
|
|
104
|
+
placeholder={placeholder}
|
|
105
|
+
disabled={readonly}
|
|
106
|
+
inputOnly={inline}
|
|
107
|
+
onQueryRefOptions={onQueryRefOptions}
|
|
108
|
+
{...inputProps}
|
|
109
|
+
/>
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
//
|
|
114
|
+
// Arrays.
|
|
115
|
+
//
|
|
116
|
+
|
|
85
117
|
if (array) {
|
|
86
118
|
return <ArrayField property={property} path={path} inputProps={inputProps} readonly={readonly} Custom={Custom} />;
|
|
87
119
|
}
|
|
88
120
|
|
|
121
|
+
//
|
|
122
|
+
// Standard Inputs.
|
|
123
|
+
//
|
|
124
|
+
|
|
89
125
|
const InputComponent = getInputComponent(type, format);
|
|
90
126
|
if (InputComponent) {
|
|
91
127
|
return (
|
|
92
|
-
<
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
/>
|
|
102
|
-
</div>
|
|
128
|
+
<InputComponent
|
|
129
|
+
type={type}
|
|
130
|
+
format={format}
|
|
131
|
+
label={label}
|
|
132
|
+
inputOnly={inline}
|
|
133
|
+
placeholder={placeholder}
|
|
134
|
+
disabled={readonly}
|
|
135
|
+
{...inputProps}
|
|
136
|
+
/>
|
|
103
137
|
);
|
|
104
138
|
}
|
|
105
139
|
|
|
140
|
+
//
|
|
141
|
+
// Select.
|
|
142
|
+
//
|
|
143
|
+
|
|
106
144
|
if (options) {
|
|
107
145
|
return (
|
|
108
|
-
<
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
/>
|
|
119
|
-
</div>
|
|
146
|
+
<SelectInput
|
|
147
|
+
type={type}
|
|
148
|
+
format={format}
|
|
149
|
+
disabled={readonly}
|
|
150
|
+
inputOnly={inline}
|
|
151
|
+
label={label}
|
|
152
|
+
options={options.map((option) => ({ value: option, label: String(option) }))}
|
|
153
|
+
placeholder={placeholder}
|
|
154
|
+
{...inputProps}
|
|
155
|
+
/>
|
|
120
156
|
);
|
|
121
157
|
}
|
|
122
158
|
|
|
159
|
+
//
|
|
160
|
+
// Nested Objects.
|
|
161
|
+
//
|
|
162
|
+
|
|
123
163
|
if (type === 'object') {
|
|
124
164
|
const baseNode = findNode(ast, isDiscriminatedUnion);
|
|
125
165
|
const typeLiteral = baseNode
|
|
126
166
|
? getDiscriminatedType(baseNode, inputProps.getValue() as any)
|
|
127
|
-
: findNode(ast,
|
|
167
|
+
: findNode(ast, SchemaAST.isTypeLiteral);
|
|
128
168
|
|
|
129
169
|
if (typeLiteral) {
|
|
130
170
|
return (
|
|
131
|
-
|
|
132
|
-
{!inline && <
|
|
133
|
-
<FormFields
|
|
134
|
-
|
|
171
|
+
<>
|
|
172
|
+
{!inline && <h3 className='text-lg mbs-2 mbe-1 first:mbs-0'>{label}</h3>}
|
|
173
|
+
<FormFields
|
|
174
|
+
schema={Schema.make(typeLiteral)}
|
|
175
|
+
path={path}
|
|
176
|
+
readonly={readonly}
|
|
177
|
+
onQueryRefOptions={onQueryRefOptions}
|
|
178
|
+
Custom={Custom}
|
|
179
|
+
lookupComponent={lookupComponent}
|
|
180
|
+
/>
|
|
181
|
+
</>
|
|
135
182
|
);
|
|
136
183
|
}
|
|
137
184
|
}
|
|
@@ -139,7 +186,28 @@ export const FormField = ({ property, path, readonly, inline, lookupComponent, C
|
|
|
139
186
|
return null;
|
|
140
187
|
};
|
|
141
188
|
|
|
142
|
-
export
|
|
189
|
+
export type FormFieldsProps = ThemedClassName<{
|
|
190
|
+
Custom?: Partial<Record<string, InputComponent>>;
|
|
191
|
+
schema: Schema.Schema.All;
|
|
192
|
+
path?: (string | number)[];
|
|
193
|
+
filter?: (props: SchemaProperty<any>[]) => SchemaProperty<any>[];
|
|
194
|
+
sort?: string[];
|
|
195
|
+
readonly?: boolean;
|
|
196
|
+
lookupComponent?: ComponentLookup;
|
|
197
|
+
onQueryRefOptions?: QueryRefOptions;
|
|
198
|
+
}>;
|
|
199
|
+
|
|
200
|
+
export const FormFields = ({
|
|
201
|
+
classNames,
|
|
202
|
+
Custom,
|
|
203
|
+
schema,
|
|
204
|
+
path,
|
|
205
|
+
filter,
|
|
206
|
+
sort,
|
|
207
|
+
readonly,
|
|
208
|
+
lookupComponent,
|
|
209
|
+
onQueryRefOptions,
|
|
210
|
+
}: FormFieldsProps) => {
|
|
143
211
|
const values = useFormValues(path);
|
|
144
212
|
|
|
145
213
|
const properties = useMemo(() => {
|
|
@@ -150,7 +218,8 @@ export const FormFields = ({ schema, path, filter, sort, readonly, lookupCompone
|
|
|
150
218
|
}, [schema, values, filter, sort]);
|
|
151
219
|
|
|
152
220
|
return (
|
|
153
|
-
|
|
221
|
+
// TODO(burdon): Consistent padding (with composite forms).
|
|
222
|
+
<div role='form' className={mx('p-2 is-full', classNames)}>
|
|
154
223
|
{properties
|
|
155
224
|
.map((property) => {
|
|
156
225
|
return (
|
|
@@ -159,6 +228,7 @@ export const FormFields = ({ schema, path, filter, sort, readonly, lookupCompone
|
|
|
159
228
|
property={property}
|
|
160
229
|
path={[...(path ?? []), property.name]}
|
|
161
230
|
readonly={readonly}
|
|
231
|
+
onQueryRefOptions={onQueryRefOptions}
|
|
162
232
|
lookupComponent={lookupComponent}
|
|
163
233
|
Custom={Custom}
|
|
164
234
|
/>
|
|
@@ -2,7 +2,16 @@
|
|
|
2
2
|
// Copyright 2025 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import React, {
|
|
5
|
+
import React, {
|
|
6
|
+
createContext,
|
|
7
|
+
type RefObject,
|
|
8
|
+
useContext,
|
|
9
|
+
useEffect,
|
|
10
|
+
useMemo,
|
|
11
|
+
type FocusEvent,
|
|
12
|
+
type PropsWithChildren,
|
|
13
|
+
useCallback,
|
|
14
|
+
} from 'react';
|
|
6
15
|
|
|
7
16
|
import { raise } from '@dxos/debug';
|
|
8
17
|
import { type BaseObject, getValue } from '@dxos/echo-schema';
|
|
@@ -53,22 +62,20 @@ export const FormProvider = ({
|
|
|
53
62
|
...formOptions
|
|
54
63
|
}: PropsWithChildren<
|
|
55
64
|
FormOptions<any> & {
|
|
56
|
-
formRef?:
|
|
65
|
+
formRef?: RefObject<HTMLDivElement>;
|
|
57
66
|
autoSave?: boolean;
|
|
58
67
|
}
|
|
59
68
|
>) => {
|
|
60
69
|
const form = useForm(formOptions);
|
|
61
70
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
return;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
const handleKeyDown = (event: KeyboardEvent) => {
|
|
71
|
+
const handleKeyDown = useCallback(
|
|
72
|
+
(event: KeyboardEvent) => {
|
|
68
73
|
const keyIsEnter = event.key === 'Enter';
|
|
69
74
|
const modifierUsed = event.ctrlKey || event.altKey || event.metaKey || event.shiftKey;
|
|
70
75
|
const inputIsTextarea = (event.target as HTMLElement).tagName.toLowerCase() === 'textarea';
|
|
71
|
-
const inputOptOut =
|
|
76
|
+
const inputOptOut =
|
|
77
|
+
(event.target as HTMLElement).hasAttribute('data-no-submit') ||
|
|
78
|
+
(event.target as HTMLElement).closest('[data-no-submit]') !== null;
|
|
72
79
|
|
|
73
80
|
// Regular inputs: Submit on Enter (no modifiers).
|
|
74
81
|
const shouldSubmitRegularInput = !inputIsTextarea && keyIsEnter && !modifierUsed;
|
|
@@ -84,7 +91,14 @@ export const FormProvider = ({
|
|
|
84
91
|
(event.target as HTMLElement).blur();
|
|
85
92
|
}
|
|
86
93
|
}
|
|
87
|
-
}
|
|
94
|
+
},
|
|
95
|
+
[form, autoSave],
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
useEffect(() => {
|
|
99
|
+
if (!formRef?.current) {
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
88
102
|
|
|
89
103
|
const formElement = formRef.current;
|
|
90
104
|
|
|
@@ -34,18 +34,12 @@ export type InputHeaderProps = PropsWithChildren<{
|
|
|
34
34
|
|
|
35
35
|
export const InputHeader = ({ children, error }: InputHeaderProps) => {
|
|
36
36
|
return (
|
|
37
|
-
<div role='none' className='flex justify-between items-center
|
|
37
|
+
<div role='none' className='flex justify-between items-center mbs-2 mbe-1 first:mbs-0'>
|
|
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
|
);
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2025 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { type SchemaAST } from 'effect';
|
|
6
|
+
import React, { useCallback, useMemo, type FocusEvent } from 'react';
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
Expando,
|
|
10
|
+
getTypeAnnotation,
|
|
11
|
+
Ref,
|
|
12
|
+
ReferenceAnnotationId,
|
|
13
|
+
type ReferenceAnnotationValue,
|
|
14
|
+
} from '@dxos/echo-schema';
|
|
15
|
+
import { findAnnotation } from '@dxos/effect';
|
|
16
|
+
import { DXN } from '@dxos/keys';
|
|
17
|
+
import { DxRefTag } from '@dxos/lit-ui/react';
|
|
18
|
+
import { Input, useTranslation } from '@dxos/react-ui';
|
|
19
|
+
import { TagPicker, type TagPickerMode, type TagPickerItemData } from '@dxos/react-ui-tag-picker';
|
|
20
|
+
import { descriptionText, mx } from '@dxos/react-ui-theme';
|
|
21
|
+
import { isNonNullable } from '@dxos/util';
|
|
22
|
+
|
|
23
|
+
import { TextInput } from './Defaults';
|
|
24
|
+
import { InputHeader, type InputProps } from './Input';
|
|
25
|
+
import { type QueryRefOptions, useQueryRefOptions } from '../../hooks';
|
|
26
|
+
import { translationKey } from '../../translations';
|
|
27
|
+
|
|
28
|
+
type RefFieldProps = InputProps & {
|
|
29
|
+
ast?: SchemaAST.AST;
|
|
30
|
+
array?: boolean;
|
|
31
|
+
onQueryRefOptions?: QueryRefOptions;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export const RefField = ({
|
|
35
|
+
type,
|
|
36
|
+
label,
|
|
37
|
+
disabled,
|
|
38
|
+
placeholder,
|
|
39
|
+
inputOnly,
|
|
40
|
+
array,
|
|
41
|
+
ast,
|
|
42
|
+
getValue,
|
|
43
|
+
onBlur,
|
|
44
|
+
onQueryRefOptions,
|
|
45
|
+
onValueChange,
|
|
46
|
+
...restInputProps
|
|
47
|
+
}: RefFieldProps) => {
|
|
48
|
+
const { t } = useTranslation(translationKey);
|
|
49
|
+
const refTypeInfo = useMemo(
|
|
50
|
+
() => (ast ? findAnnotation<ReferenceAnnotationValue>(ast, ReferenceAnnotationId) : undefined),
|
|
51
|
+
[ast],
|
|
52
|
+
);
|
|
53
|
+
const { options: availableOptions, loading: _loading } = useQueryRefOptions({ refTypeInfo, onQueryRefOptions });
|
|
54
|
+
|
|
55
|
+
if ((refTypeInfo && refTypeInfo?.typename === getTypeAnnotation(Expando)?.typename) || !onQueryRefOptions) {
|
|
56
|
+
// If ref type is expando, fall back to taking a DXN in string format.
|
|
57
|
+
return (
|
|
58
|
+
<RefFieldFallback
|
|
59
|
+
{...{ type, label, placeholder, disabled, inputOnly, getValue, onBlur, onValueChange, ...restInputProps }}
|
|
60
|
+
/>
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const handleGetValue = useCallback((): TagPickerItemData[] => {
|
|
65
|
+
const formValue = getValue();
|
|
66
|
+
|
|
67
|
+
const unknownToRefOption = (val: unknown) => {
|
|
68
|
+
if (Ref.isRef(val)) {
|
|
69
|
+
const dxnString = val.dxn.toString();
|
|
70
|
+
const matchingOption = availableOptions.find((option) => option.id === dxnString);
|
|
71
|
+
if (matchingOption) {
|
|
72
|
+
return matchingOption;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return undefined;
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
if (array && Array.isArray(formValue)) {
|
|
79
|
+
return formValue.map(unknownToRefOption).filter(isNonNullable) ?? [];
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const option = unknownToRefOption(formValue);
|
|
83
|
+
if (option) {
|
|
84
|
+
return [option];
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return [];
|
|
88
|
+
}, [getValue, availableOptions, array]);
|
|
89
|
+
|
|
90
|
+
const handleSearch = useCallback(
|
|
91
|
+
(text: string, ids: string[]): TagPickerItemData[] =>
|
|
92
|
+
availableOptions
|
|
93
|
+
.filter((option) => !ids.includes(option.id))
|
|
94
|
+
.filter((option) => option.label.toLowerCase().includes(text.toLowerCase()))
|
|
95
|
+
.map((option) => ({ id: option.id, label: option.label, hue: option.hue as any })),
|
|
96
|
+
[availableOptions],
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
const handleUpdate = useCallback(
|
|
100
|
+
(ids: string[]) => {
|
|
101
|
+
if (ids.length === 0) {
|
|
102
|
+
onValueChange('object', undefined);
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const refs = ids
|
|
107
|
+
.map((id) => {
|
|
108
|
+
const item = availableOptions.find((option) => option.id === id);
|
|
109
|
+
if (item) {
|
|
110
|
+
const dxn = DXN.parse(item.id);
|
|
111
|
+
return Ref.fromDXN(dxn);
|
|
112
|
+
}
|
|
113
|
+
return null;
|
|
114
|
+
})
|
|
115
|
+
.filter(isNonNullable);
|
|
116
|
+
|
|
117
|
+
if (array) {
|
|
118
|
+
onValueChange('object', refs);
|
|
119
|
+
} else {
|
|
120
|
+
onValueChange('object', refs[0]);
|
|
121
|
+
}
|
|
122
|
+
},
|
|
123
|
+
[availableOptions, array, onValueChange],
|
|
124
|
+
);
|
|
125
|
+
|
|
126
|
+
const tagPickerMode: TagPickerMode = useMemo(() => (array ? 'multi-select' : 'single-select'), [array]);
|
|
127
|
+
|
|
128
|
+
if (!refTypeInfo) {
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const { status, error } = restInputProps.getStatus();
|
|
133
|
+
|
|
134
|
+
const items = handleGetValue();
|
|
135
|
+
|
|
136
|
+
// NOTE(thure): I left both predicates in-place in case we decide to add variants which do render readonly but empty values.
|
|
137
|
+
return disabled && items.length < 1 ? null : (
|
|
138
|
+
<Input.Root validationValence={status}>
|
|
139
|
+
{!inputOnly && (
|
|
140
|
+
<InputHeader error={error}>
|
|
141
|
+
<Input.Label>{label}</Input.Label>
|
|
142
|
+
</InputHeader>
|
|
143
|
+
)}
|
|
144
|
+
<div data-no-submit>
|
|
145
|
+
{disabled ? (
|
|
146
|
+
items.length < 1 ? (
|
|
147
|
+
<p className={mx(descriptionText, 'mbe-2')}>{t('empty readonly ref field label')}</p>
|
|
148
|
+
) : (
|
|
149
|
+
items.map((item) => (
|
|
150
|
+
<DxRefTag key={item.id} refid={item.id} rootclassname='mie-1' style={{ marginInlineEnd: '.25rem' }}>
|
|
151
|
+
{item.label}
|
|
152
|
+
</DxRefTag>
|
|
153
|
+
))
|
|
154
|
+
)
|
|
155
|
+
) : (
|
|
156
|
+
<TagPicker
|
|
157
|
+
readonly={disabled}
|
|
158
|
+
items={items}
|
|
159
|
+
mode={tagPickerMode}
|
|
160
|
+
onBlur={(event) => onBlur(event as unknown as FocusEvent<HTMLElement>)}
|
|
161
|
+
onUpdate={handleUpdate}
|
|
162
|
+
onSearch={handleSearch}
|
|
163
|
+
classNames='rounded-sm bg-inputSurface p-1.5'
|
|
164
|
+
/>
|
|
165
|
+
)}
|
|
166
|
+
</div>
|
|
167
|
+
{inputOnly && <Input.DescriptionAndValidation>{error}</Input.DescriptionAndValidation>}
|
|
168
|
+
</Input.Root>
|
|
169
|
+
);
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
const RefFieldFallback = ({
|
|
173
|
+
type,
|
|
174
|
+
label,
|
|
175
|
+
disabled,
|
|
176
|
+
placeholder,
|
|
177
|
+
inputOnly,
|
|
178
|
+
getValue,
|
|
179
|
+
onValueChange,
|
|
180
|
+
...restInputProps
|
|
181
|
+
}: InputProps) => {
|
|
182
|
+
const handleOnValueChange = (_type: any, dxnString: string) => {
|
|
183
|
+
const dxn = DXN.tryParse(dxnString);
|
|
184
|
+
if (dxn) {
|
|
185
|
+
onValueChange?.('object', Ref.fromDXN(dxn));
|
|
186
|
+
} else if (dxnString === '') {
|
|
187
|
+
onValueChange?.('object', undefined);
|
|
188
|
+
} else {
|
|
189
|
+
onValueChange?.('string', dxnString);
|
|
190
|
+
}
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
const handleGetValue = () => {
|
|
194
|
+
const formValue = getValue();
|
|
195
|
+
if (typeof formValue === 'string') {
|
|
196
|
+
return formValue;
|
|
197
|
+
}
|
|
198
|
+
if (Ref.isRef(formValue)) {
|
|
199
|
+
return formValue.dxn.toString();
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
return undefined;
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
return (
|
|
206
|
+
<TextInput
|
|
207
|
+
type={type}
|
|
208
|
+
label={label}
|
|
209
|
+
disabled={disabled}
|
|
210
|
+
placeholder={placeholder}
|
|
211
|
+
inputOnly={inputOnly}
|
|
212
|
+
getValue={handleGetValue as <V>() => V | undefined}
|
|
213
|
+
onValueChange={handleOnValueChange}
|
|
214
|
+
{...restInputProps}
|
|
215
|
+
/>
|
|
216
|
+
);
|
|
217
|
+
};
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// Copyright 2024 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import React, { useCallback, useState, useEffect, useMemo } from 'react';
|
|
5
|
+
import React, { useCallback, useState, useEffect, useMemo, type ChangeEvent } from 'react';
|
|
6
6
|
|
|
7
7
|
import { type GeoPoint, GeoLocation } from '@dxos/echo-schema';
|
|
8
8
|
import { Input, useTranslation } from '@dxos/react-ui';
|
|
@@ -28,7 +28,7 @@ export const GeoPointInput = ({ type, label, disabled, getStatus, getValue, onVa
|
|
|
28
28
|
|
|
29
29
|
const handleCoordinateChange = useCallback(
|
|
30
30
|
(coordinateType: keyof Pick<GeoLocation, 'longitude' | 'latitude'>, setText: (text: string) => void) =>
|
|
31
|
-
(event:
|
|
31
|
+
(event: ChangeEvent<HTMLInputElement>) => {
|
|
32
32
|
const inputText = event.target.value;
|
|
33
33
|
setText(inputText);
|
|
34
34
|
if (inputText !== '' && inputText !== '-') {
|
|
@@ -5,9 +5,10 @@
|
|
|
5
5
|
import '@dxos-theme';
|
|
6
6
|
|
|
7
7
|
import { type Meta, type StoryObj } from '@storybook/react';
|
|
8
|
+
import { Schema } from 'effect';
|
|
8
9
|
import React, { useState } from 'react';
|
|
9
10
|
|
|
10
|
-
import {
|
|
11
|
+
import { SelectOptionSchema, type SelectOption } from '@dxos/echo-schema';
|
|
11
12
|
import { withLayout, withTheme } from '@dxos/storybook-utils';
|
|
12
13
|
|
|
13
14
|
import { SelectOptionInput } from './SelectOptionsInput';
|
|
@@ -15,9 +16,9 @@ import translations from '../../../translations';
|
|
|
15
16
|
import { TestLayout, TestPanel } from '../../testing';
|
|
16
17
|
import { Form } from '../Form';
|
|
17
18
|
|
|
18
|
-
const schema =
|
|
19
|
-
options:
|
|
20
|
-
}).pipe(
|
|
19
|
+
const schema = Schema.Struct({
|
|
20
|
+
options: Schema.Array(SelectOptionSchema).pipe(Schema.mutable),
|
|
21
|
+
}).pipe(Schema.mutable);
|
|
21
22
|
|
|
22
23
|
const DefaultStory = () => {
|
|
23
24
|
const [values, setValues] = useState<{ options: SelectOption[] }>({
|
|
@@ -46,7 +47,7 @@ const DefaultStory = () => {
|
|
|
46
47
|
const meta: Meta = {
|
|
47
48
|
title: 'ui/react-ui-form/SelectOptionsInput',
|
|
48
49
|
component: SelectOptionInput,
|
|
49
|
-
decorators: [withTheme, withLayout(
|
|
50
|
+
decorators: [withTheme, withLayout()],
|
|
50
51
|
parameters: { translations },
|
|
51
52
|
};
|
|
52
53
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// Copyright 2025 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import React, { useCallback, useEffect, useState,
|
|
5
|
+
import React, { type ChangeEvent, type KeyboardEvent, useCallback, useEffect, useState, useRef } from 'react';
|
|
6
6
|
|
|
7
7
|
import { type SelectOption } from '@dxos/echo-schema';
|
|
8
8
|
import { PublicKey } from '@dxos/keys';
|
|
@@ -89,7 +89,7 @@ export const SelectOptionInput = ({ type, label, disabled, getStatus, getValue,
|
|
|
89
89
|
[options, type, onValueChange],
|
|
90
90
|
);
|
|
91
91
|
|
|
92
|
-
const handleKeyDown = useCallback((e:
|
|
92
|
+
const handleKeyDown = useCallback((e: KeyboardEvent<HTMLInputElement>) => {
|
|
93
93
|
if (e.key === 'Enter') {
|
|
94
94
|
setSelectedId(null);
|
|
95
95
|
}
|
|
@@ -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;
|