@dxos/react-ui-form 0.8.2-main.5ca3450 → 0.8.2-main.600d381
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/Form/ArrayField.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/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/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/hooks/index.d.ts +1 -0
- package/dist/types/src/hooks/index.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/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/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
|
@@ -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
|
+
};
|
|
@@ -8,11 +8,9 @@ import { getDXN, type TypeAnnotation } from '@dxos/echo-schema';
|
|
|
8
8
|
import { Filter, type Space } from '@dxos/react-client/echo';
|
|
9
9
|
import { isNonNullable } from '@dxos/util';
|
|
10
10
|
|
|
11
|
-
import { type QueryRefOptions } from '
|
|
11
|
+
import { type QueryRefOptions } from './useQueryRefOptions';
|
|
12
12
|
|
|
13
|
-
type UseRefQueryLookupProps = {
|
|
14
|
-
space?: Space;
|
|
15
|
-
};
|
|
13
|
+
type UseRefQueryLookupProps = { space?: Space };
|
|
16
14
|
|
|
17
15
|
export const useRefQueryLookupHandler = ({ space }: UseRefQueryLookupProps): QueryRefOptions => {
|
|
18
16
|
const handleRefQueryLookup = useCallback(
|
package/src/util/index.ts
CHANGED
package/src/util/refs.ts
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2025 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { type SchemaAST } from 'effect';
|
|
6
|
+
|
|
7
|
+
import { FormatEnum, ReferenceAnnotationId } from '@dxos/echo-schema';
|
|
8
|
+
import { findAnnotation } from '@dxos/effect';
|
|
9
|
+
import { type SchemaProperty } from '@dxos/schema';
|
|
10
|
+
|
|
11
|
+
import { findArrayElementType } from './schema';
|
|
12
|
+
|
|
13
|
+
type RefProps = {
|
|
14
|
+
ast: SchemaAST.AST;
|
|
15
|
+
isArray: boolean;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export const getRefProps = (property: SchemaProperty<any>): RefProps | undefined => {
|
|
19
|
+
const { ast, format, array } = property;
|
|
20
|
+
|
|
21
|
+
// Direct reference.
|
|
22
|
+
if (format === FormatEnum.Ref) {
|
|
23
|
+
return { ast, isArray: false };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Array of references.
|
|
27
|
+
if (array) {
|
|
28
|
+
const elementType = findArrayElementType(ast);
|
|
29
|
+
if (elementType) {
|
|
30
|
+
const annotation = findAnnotation(elementType, ReferenceAnnotationId);
|
|
31
|
+
if (annotation) {
|
|
32
|
+
return { ast: elementType, isArray: true };
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return undefined;
|
|
38
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2025 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { SchemaAST } from 'effect';
|
|
6
|
+
|
|
7
|
+
import { findNode } from '@dxos/effect';
|
|
8
|
+
|
|
9
|
+
export const findArrayElementType = (ast: SchemaAST.AST) => {
|
|
10
|
+
const tupleType = findNode(ast, SchemaAST.isTupleType);
|
|
11
|
+
const elementType = (tupleType as SchemaAST.TupleType | undefined)?.rest[0]?.type;
|
|
12
|
+
|
|
13
|
+
return elementType;
|
|
14
|
+
};
|