@dxos/react-ui-form 0.8.2-main.12df754 → 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 +823 -524
- package/dist/lib/browser/index.mjs.map +4 -4
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/node/index.cjs +661 -372
- package/dist/lib/node/index.cjs.map +4 -4
- package/dist/lib/node/meta.json +1 -1
- package/dist/lib/node-esm/index.mjs +823 -524
- 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 +6 -4
- package/dist/types/src/components/Form/Form.d.ts.map +1 -1
- package/dist/types/src/components/Form/Form.stories.d.ts +37 -34
- 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 +14 -11
- 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.map +1 -1
- 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/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/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 +26 -25
- package/src/components/ControlSection/ControlSection.stories.tsx +1 -1
- package/src/components/FieldEditor/FieldEditor.stories.tsx +1 -1
- package/src/components/Form/ArrayField.tsx +8 -7
- package/src/components/Form/Defaults.tsx +4 -1
- package/src/components/Form/Form.stories.tsx +92 -43
- package/src/components/Form/Form.tsx +7 -3
- package/src/components/Form/FormContent.tsx +90 -19
- package/src/components/Form/FormContext.tsx +13 -3
- package/src/components/Form/Input.tsx +3 -9
- package/src/components/Form/RefField.tsx +197 -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 +4 -3
- package/src/components/ViewEditor/ViewEditor.tsx +12 -10
- package/src/components/deprecated/Form.tsx +6 -6
- package/src/hooks/index.ts +3 -1
- package/src/hooks/useForm.ts +3 -3
- package/src/hooks/useQueryRefOptions.ts +53 -0
- package/src/hooks/useRefQueryLookupHandler.ts +41 -0
- 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
|
@@ -1,12 +1,11 @@
|
|
|
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';
|
|
11
10
|
import { mx } from '@dxos/react-ui-theme';
|
|
12
11
|
import { getSchemaProperties, type SchemaProperty } from '@dxos/schema';
|
|
@@ -17,17 +16,10 @@ import { SelectInput } from './Defaults';
|
|
|
17
16
|
import { type ComponentLookup } from './Form';
|
|
18
17
|
import { useInputProps, useFormValues } from './FormContext';
|
|
19
18
|
import { type InputComponent } from './Input';
|
|
19
|
+
import { RefField } from './RefField';
|
|
20
20
|
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
|
-
};
|
|
21
|
+
import { type QueryRefOptions } from '../../hooks';
|
|
22
|
+
import { getRefProps } from '../../util';
|
|
31
23
|
|
|
32
24
|
export type FormFieldProps = {
|
|
33
25
|
property: SchemaProperty<any>;
|
|
@@ -35,11 +27,20 @@ export type FormFieldProps = {
|
|
|
35
27
|
readonly?: boolean;
|
|
36
28
|
/** Used to indicate if input should be presented inline (e.g. for array items). */
|
|
37
29
|
inline?: boolean;
|
|
30
|
+
onQueryRefOptions?: QueryRefOptions;
|
|
38
31
|
lookupComponent?: ComponentLookup;
|
|
39
32
|
Custom?: Partial<Record<string, InputComponent>>;
|
|
40
33
|
};
|
|
41
34
|
|
|
42
|
-
export const FormField = ({
|
|
35
|
+
export const FormField = ({
|
|
36
|
+
property,
|
|
37
|
+
path,
|
|
38
|
+
readonly,
|
|
39
|
+
inline,
|
|
40
|
+
onQueryRefOptions,
|
|
41
|
+
lookupComponent,
|
|
42
|
+
Custom,
|
|
43
|
+
}: FormFieldProps) => {
|
|
43
44
|
const inputProps = useInputProps(path);
|
|
44
45
|
const { ast, name, type, format, title, description, options, examples, array } = property;
|
|
45
46
|
|
|
@@ -49,9 +50,13 @@ export const FormField = ({ property, path, readonly, inline, lookupComponent, C
|
|
|
49
50
|
[examples, description],
|
|
50
51
|
);
|
|
51
52
|
|
|
53
|
+
//
|
|
54
|
+
// Registry and Custom.
|
|
55
|
+
//
|
|
56
|
+
|
|
52
57
|
const FoundComponent = lookupComponent?.({
|
|
53
58
|
prop: name,
|
|
54
|
-
schema:
|
|
59
|
+
schema: Schema.make(ast),
|
|
55
60
|
inputProps: {
|
|
56
61
|
type,
|
|
57
62
|
format,
|
|
@@ -82,10 +87,40 @@ export const FormField = ({ property, path, readonly, inline, lookupComponent, C
|
|
|
82
87
|
);
|
|
83
88
|
}
|
|
84
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
|
+
|
|
85
116
|
if (array) {
|
|
86
117
|
return <ArrayField property={property} path={path} inputProps={inputProps} readonly={readonly} Custom={Custom} />;
|
|
87
118
|
}
|
|
88
119
|
|
|
120
|
+
//
|
|
121
|
+
// Standard Inputs.
|
|
122
|
+
//
|
|
123
|
+
|
|
89
124
|
const InputComponent = getInputComponent(type, format);
|
|
90
125
|
if (InputComponent) {
|
|
91
126
|
return (
|
|
@@ -103,6 +138,10 @@ export const FormField = ({ property, path, readonly, inline, lookupComponent, C
|
|
|
103
138
|
);
|
|
104
139
|
}
|
|
105
140
|
|
|
141
|
+
//
|
|
142
|
+
// Select.
|
|
143
|
+
//
|
|
144
|
+
|
|
106
145
|
if (options) {
|
|
107
146
|
return (
|
|
108
147
|
<div role='none'>
|
|
@@ -120,17 +159,28 @@ export const FormField = ({ property, path, readonly, inline, lookupComponent, C
|
|
|
120
159
|
);
|
|
121
160
|
}
|
|
122
161
|
|
|
162
|
+
//
|
|
163
|
+
// Nested Objects.
|
|
164
|
+
//
|
|
165
|
+
|
|
123
166
|
if (type === 'object') {
|
|
124
167
|
const baseNode = findNode(ast, isDiscriminatedUnion);
|
|
125
168
|
const typeLiteral = baseNode
|
|
126
169
|
? getDiscriminatedType(baseNode, inputProps.getValue() as any)
|
|
127
|
-
: findNode(ast,
|
|
170
|
+
: findNode(ast, SchemaAST.isTypeLiteral);
|
|
128
171
|
|
|
129
172
|
if (typeLiteral) {
|
|
130
173
|
return (
|
|
131
174
|
<div role='none'>
|
|
132
|
-
{!inline && <
|
|
133
|
-
<FormFields
|
|
175
|
+
{!inline && <h3 className='text-lg mbs-2 mbe-1'>{label}</h3>}
|
|
176
|
+
<FormFields
|
|
177
|
+
schema={Schema.make(typeLiteral)}
|
|
178
|
+
path={path}
|
|
179
|
+
readonly={readonly}
|
|
180
|
+
onQueryRefOptions={onQueryRefOptions}
|
|
181
|
+
Custom={Custom}
|
|
182
|
+
lookupComponent={lookupComponent}
|
|
183
|
+
/>
|
|
134
184
|
</div>
|
|
135
185
|
);
|
|
136
186
|
}
|
|
@@ -139,7 +189,27 @@ export const FormField = ({ property, path, readonly, inline, lookupComponent, C
|
|
|
139
189
|
return null;
|
|
140
190
|
};
|
|
141
191
|
|
|
142
|
-
export
|
|
192
|
+
export type FormContentProps = {
|
|
193
|
+
schema: Schema.Schema.All;
|
|
194
|
+
path?: (string | number)[];
|
|
195
|
+
filter?: (props: SchemaProperty<any>[]) => SchemaProperty<any>[];
|
|
196
|
+
sort?: string[];
|
|
197
|
+
readonly?: boolean;
|
|
198
|
+
onQueryRefOptions?: QueryRefOptions;
|
|
199
|
+
lookupComponent?: ComponentLookup;
|
|
200
|
+
Custom?: Partial<Record<string, InputComponent>>;
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
export const FormFields = ({
|
|
204
|
+
schema,
|
|
205
|
+
path,
|
|
206
|
+
filter,
|
|
207
|
+
sort,
|
|
208
|
+
readonly,
|
|
209
|
+
onQueryRefOptions,
|
|
210
|
+
lookupComponent,
|
|
211
|
+
Custom,
|
|
212
|
+
}: FormContentProps) => {
|
|
143
213
|
const values = useFormValues(path);
|
|
144
214
|
|
|
145
215
|
const properties = useMemo(() => {
|
|
@@ -159,6 +229,7 @@ export const FormFields = ({ schema, path, filter, sort, readonly, lookupCompone
|
|
|
159
229
|
property={property}
|
|
160
230
|
path={[...(path ?? []), property.name]}
|
|
161
231
|
readonly={readonly}
|
|
232
|
+
onQueryRefOptions={onQueryRefOptions}
|
|
162
233
|
lookupComponent={lookupComponent}
|
|
163
234
|
Custom={Custom}
|
|
164
235
|
/>
|
|
@@ -2,7 +2,15 @@
|
|
|
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
|
+
} from 'react';
|
|
6
14
|
|
|
7
15
|
import { raise } from '@dxos/debug';
|
|
8
16
|
import { type BaseObject, getValue } from '@dxos/echo-schema';
|
|
@@ -53,7 +61,7 @@ export const FormProvider = ({
|
|
|
53
61
|
...formOptions
|
|
54
62
|
}: PropsWithChildren<
|
|
55
63
|
FormOptions<any> & {
|
|
56
|
-
formRef?:
|
|
64
|
+
formRef?: RefObject<HTMLDivElement>;
|
|
57
65
|
autoSave?: boolean;
|
|
58
66
|
}
|
|
59
67
|
>) => {
|
|
@@ -68,7 +76,9 @@ export const FormProvider = ({
|
|
|
68
76
|
const keyIsEnter = event.key === 'Enter';
|
|
69
77
|
const modifierUsed = event.ctrlKey || event.altKey || event.metaKey || event.shiftKey;
|
|
70
78
|
const inputIsTextarea = (event.target as HTMLElement).tagName.toLowerCase() === 'textarea';
|
|
71
|
-
const inputOptOut =
|
|
79
|
+
const inputOptOut =
|
|
80
|
+
(event.target as HTMLElement).hasAttribute('data-no-submit') ||
|
|
81
|
+
(event.target as HTMLElement).closest('[data-no-submit]') !== null;
|
|
72
82
|
|
|
73
83
|
// Regular inputs: Submit on Enter (no modifiers).
|
|
74
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
|
);
|
|
@@ -0,0 +1,197 @@
|
|
|
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 { 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;
|
|
28
|
+
onQueryRefOptions?: QueryRefOptions;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export const RefField = ({
|
|
32
|
+
type,
|
|
33
|
+
label,
|
|
34
|
+
disabled,
|
|
35
|
+
placeholder,
|
|
36
|
+
inputOnly,
|
|
37
|
+
array,
|
|
38
|
+
ast,
|
|
39
|
+
getValue,
|
|
40
|
+
onBlur,
|
|
41
|
+
onQueryRefOptions,
|
|
42
|
+
onValueChange,
|
|
43
|
+
...restInputProps
|
|
44
|
+
}: RefFieldProps) => {
|
|
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();
|
|
62
|
+
|
|
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
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return undefined;
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
if (array && Array.isArray(formValue)) {
|
|
75
|
+
return formValue.map(unknownToRefOption).filter(isNonNullable) ?? [];
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const option = unknownToRefOption(formValue);
|
|
79
|
+
if (option) {
|
|
80
|
+
return [option];
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return [];
|
|
84
|
+
}, [getValue, availableOptions, array]);
|
|
85
|
+
|
|
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
|
+
}
|
|
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]);
|
|
117
|
+
}
|
|
118
|
+
},
|
|
119
|
+
[availableOptions, array, onValueChange],
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
const tagPickerMode: TagPickerMode = useMemo(() => (array ? 'multi-select' : 'single-select'), [array]);
|
|
123
|
+
|
|
124
|
+
if (!refTypeInfo) {
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
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
|
+
|
|
173
|
+
const handleGetValue = () => {
|
|
174
|
+
const formValue = getValue();
|
|
175
|
+
if (typeof formValue === 'string') {
|
|
176
|
+
return formValue;
|
|
177
|
+
}
|
|
178
|
+
if (Ref.isRef(formValue)) {
|
|
179
|
+
return formValue.dxn.toString();
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return undefined;
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
return (
|
|
186
|
+
<TextInput
|
|
187
|
+
type={type}
|
|
188
|
+
label={label}
|
|
189
|
+
disabled={disabled}
|
|
190
|
+
placeholder={placeholder}
|
|
191
|
+
inputOnly={inputOnly}
|
|
192
|
+
getValue={handleGetValue as <V>() => V | undefined}
|
|
193
|
+
onValueChange={handleOnValueChange}
|
|
194
|
+
{...restInputProps}
|
|
195
|
+
/>
|
|
196
|
+
);
|
|
197
|
+
};
|
|
@@ -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;
|
|
@@ -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, { useCallback, useMemo, useState } from 'react';
|
|
9
10
|
|
|
10
|
-
import { Format, type EchoSchema,
|
|
11
|
+
import { Format, type EchoSchema, toJsonSchema, TypedObject } from '@dxos/echo-schema';
|
|
11
12
|
import { Filter, useQuery, useSpace } from '@dxos/react-client/echo';
|
|
12
13
|
import { withClientProvider } from '@dxos/react-client/testing';
|
|
13
14
|
import { useAsyncEffect } from '@dxos/react-ui';
|
|
@@ -26,7 +27,7 @@ const DefaultStory = () => {
|
|
|
26
27
|
useAsyncEffect(async () => {
|
|
27
28
|
if (space) {
|
|
28
29
|
class TestSchema extends TypedObject({ typename: 'example.com/type/Test', version: '0.1.0' })({
|
|
29
|
-
name:
|
|
30
|
+
name: Schema.String,
|
|
30
31
|
email: Format.Email,
|
|
31
32
|
salary: Format.Currency(),
|
|
32
33
|
}) {}
|
|
@@ -81,7 +82,7 @@ const meta: Meta<typeof ViewEditor> = {
|
|
|
81
82
|
title: 'ui/react-ui-form/ViewEditor',
|
|
82
83
|
component: ViewEditor,
|
|
83
84
|
render: DefaultStory,
|
|
84
|
-
decorators: [withClientProvider({ createSpace: true }), withLayout({ fullscreen: true
|
|
85
|
+
decorators: [withClientProvider({ createSpace: true }), withLayout({ fullscreen: true }), withTheme],
|
|
85
86
|
parameters: {
|
|
86
87
|
translations,
|
|
87
88
|
},
|
|
@@ -2,10 +2,11 @@
|
|
|
2
2
|
// Copyright 2024 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
+
import { Schema, SchemaAST } from 'effect';
|
|
5
6
|
import React, { useCallback, useMemo, useState } from 'react';
|
|
6
7
|
|
|
7
8
|
import { type SchemaRegistry } from '@dxos/echo-db';
|
|
8
|
-
import {
|
|
9
|
+
import { Format, type BaseSchema, type JsonProp } from '@dxos/echo-schema';
|
|
9
10
|
import { invariant } from '@dxos/invariant';
|
|
10
11
|
import { IconButton, type ThemedClassName, useTranslation } from '@dxos/react-ui';
|
|
11
12
|
import { List } from '@dxos/react-ui-list';
|
|
@@ -18,16 +19,16 @@ import { Form } from '../Form';
|
|
|
18
19
|
|
|
19
20
|
const grid = 'grid grid-cols-[32px_1fr_32px] min-bs-[2.5rem]';
|
|
20
21
|
|
|
21
|
-
const ViewMetaSchema =
|
|
22
|
-
name:
|
|
23
|
-
[
|
|
22
|
+
const ViewMetaSchema = Schema.Struct({
|
|
23
|
+
name: Schema.String.annotations({
|
|
24
|
+
[SchemaAST.TitleAnnotationId]: 'View',
|
|
24
25
|
}),
|
|
25
26
|
typename: Format.URL.annotations({
|
|
26
|
-
[
|
|
27
|
+
[SchemaAST.TitleAnnotationId]: 'Typename',
|
|
27
28
|
}),
|
|
28
|
-
}).pipe(
|
|
29
|
+
}).pipe(Schema.mutable);
|
|
29
30
|
|
|
30
|
-
type ViewMetaType =
|
|
31
|
+
type ViewMetaType = Schema.Schema.Type<typeof ViewMetaSchema>;
|
|
31
32
|
|
|
32
33
|
export type ViewEditorProps = ThemedClassName<{
|
|
33
34
|
schema: BaseSchema;
|
|
@@ -139,15 +140,16 @@ export const ViewEditor = ({
|
|
|
139
140
|
);
|
|
140
141
|
|
|
141
142
|
return (
|
|
142
|
-
<div role='none' className={mx('
|
|
143
|
+
<div role='none' className={mx('grid grid-rows-[1fr_1fr] min-bs-0', classNames)}>
|
|
143
144
|
<Form<ViewMetaType>
|
|
144
145
|
autoSave
|
|
145
146
|
schema={ViewMetaSchema}
|
|
146
147
|
values={viewValues}
|
|
147
148
|
onSave={schema.readonly ? undefined : handleUpdate}
|
|
149
|
+
classNames='min-bs-0 overflow-y-auto'
|
|
148
150
|
/>
|
|
149
151
|
|
|
150
|
-
<div>
|
|
152
|
+
<div role='none' className='border-bs border-separator min-bs-0 overflow-y-auto'>
|
|
151
153
|
{/* TODO(burdon): Clean up common form ux. */}
|
|
152
154
|
<div role='none' className='p-2'>
|
|
153
155
|
<label className={mx(inputTextLabel)}>{t('fields label')}</label>
|
|
@@ -155,7 +157,7 @@ export const ViewEditor = ({
|
|
|
155
157
|
|
|
156
158
|
<List.Root<FieldType>
|
|
157
159
|
items={view.fields}
|
|
158
|
-
isItem={
|
|
160
|
+
isItem={Schema.is(FieldSchema)}
|
|
159
161
|
getId={(field) => field.id}
|
|
160
162
|
onMove={schema.readonly ? undefined : handleMove}
|
|
161
163
|
>
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// Copyright 2024 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import { SchemaAST
|
|
5
|
+
import { SchemaAST, type Schema, Option, pipe } from 'effect';
|
|
6
6
|
import React, { useState } from 'react';
|
|
7
7
|
|
|
8
8
|
import { findProperty } from '@dxos/effect';
|
|
@@ -13,7 +13,7 @@ import { type ViewType, getFieldValue, setFieldValue } from '@dxos/schema';
|
|
|
13
13
|
export type DeprecatedFormProps<T extends {} = {}> = ThemedClassName<{
|
|
14
14
|
view: ViewType;
|
|
15
15
|
object: T;
|
|
16
|
-
schema?:
|
|
16
|
+
schema?: Schema.Schema<T>;
|
|
17
17
|
readonly?: boolean;
|
|
18
18
|
}>;
|
|
19
19
|
|
|
@@ -31,16 +31,16 @@ export const DeprecatedForm = <T extends {} = {}>({
|
|
|
31
31
|
<div role='none' className={mx('flex flex-col w-full gap-2 p-2', classNames)}>
|
|
32
32
|
{view.fields.map((field) => {
|
|
33
33
|
const prop = schema && findProperty(schema, field.path);
|
|
34
|
-
const title = (prop && pipe(
|
|
35
|
-
const description = (prop && pipe(
|
|
34
|
+
const title = (prop && pipe(SchemaAST.getTitleAnnotation(prop), Option.getOrUndefined)) ?? '';
|
|
35
|
+
const description = (prop && pipe(SchemaAST.getDescriptionAnnotation(prop), Option.getOrUndefined)) ?? title;
|
|
36
36
|
// const format =
|
|
37
|
-
// (prop && (getPatternAnnotation(prop) ?? (
|
|
37
|
+
// (prop && (getPatternAnnotation(prop) ?? (SchemaAST.isNumberKeyword(prop) && RealNumberFormat))) || undefined;
|
|
38
38
|
|
|
39
39
|
//
|
|
40
40
|
// Boolean
|
|
41
41
|
//
|
|
42
42
|
|
|
43
|
-
if (prop &&
|
|
43
|
+
if (prop && SchemaAST.isBooleanKeyword(prop)) {
|
|
44
44
|
const value = object && getFieldValue(object, field);
|
|
45
45
|
const onChange = (checked: boolean) => {
|
|
46
46
|
setFieldValue(object, field, checked);
|