@dxos/react-ui-form 0.8.2-main.5885341 → 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 +720 -557
- package/dist/lib/browser/index.mjs.map +4 -4
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/node/index.cjs +734 -579
- package/dist/lib/node/index.cjs.map +4 -4
- package/dist/lib/node/meta.json +1 -1
- package/dist/lib/node-esm/index.mjs +720 -557
- 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 +4 -4
- package/dist/types/src/components/Form/Form.d.ts.map +1 -1
- package/dist/types/src/components/Form/Form.stories.d.ts +33 -32
- 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 +3 -3
- 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 +7 -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/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 +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 +5 -6
- package/src/components/Form/Defaults.tsx +4 -1
- package/src/components/Form/Form.stories.tsx +48 -42
- package/src/components/Form/Form.tsx +4 -4
- package/src/components/Form/FormContent.tsx +49 -22
- package/src/components/Form/FormContext.tsx +3 -1
- package/src/components/Form/Input.tsx +3 -9
- package/src/components/Form/RefField.tsx +145 -100
- package/src/components/Form/custom/SelectOptionsInput.stories.tsx +6 -5
- 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 +9 -8
- 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/index.ts +3 -1
- package/src/util/refs.ts +38 -0
- package/src/util/schema.ts +14 -0
|
@@ -2,126 +2,179 @@
|
|
|
2
2
|
// Copyright 2025 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import
|
|
5
|
+
import { type SchemaAST } from 'effect';
|
|
6
|
+
import React, { useCallback, useMemo, type FocusEvent } from 'react';
|
|
6
7
|
|
|
7
8
|
import {
|
|
8
|
-
type AST,
|
|
9
9
|
Expando,
|
|
10
10
|
getTypeAnnotation,
|
|
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
|
);
|
|
@@ -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
|
|
|
@@ -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;
|
|
@@ -156,7 +157,7 @@ export const ViewEditor = ({
|
|
|
156
157
|
|
|
157
158
|
<List.Root<FieldType>
|
|
158
159
|
items={view.fields}
|
|
159
|
-
isItem={
|
|
160
|
+
isItem={Schema.is(FieldSchema)}
|
|
160
161
|
getId={(field) => field.id}
|
|
161
162
|
onMove={schema.readonly ? undefined : handleMove}
|
|
162
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);
|
package/src/hooks/index.ts
CHANGED
package/src/hooks/useForm.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// Copyright 2025 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import { SchemaAST
|
|
5
|
+
import { SchemaAST, type Schema } from 'effect';
|
|
6
6
|
import { useCallback, useEffect, useMemo, useState } from 'react';
|
|
7
7
|
|
|
8
8
|
import { type BaseObject, getValue, setValue } from '@dxos/echo-schema';
|
|
@@ -45,7 +45,7 @@ export interface FormOptions<T extends BaseObject> {
|
|
|
45
45
|
/**
|
|
46
46
|
* Effect schema (Type literal).
|
|
47
47
|
*/
|
|
48
|
-
schema:
|
|
48
|
+
schema: Schema.Schema<T, any>;
|
|
49
49
|
|
|
50
50
|
/**
|
|
51
51
|
* Initial values (which may not pass validation).
|
|
@@ -90,7 +90,7 @@ export const useForm = <T extends BaseObject>({
|
|
|
90
90
|
onValid,
|
|
91
91
|
onSave,
|
|
92
92
|
}: FormOptions<T>): FormHandler<T> => {
|
|
93
|
-
invariant(
|
|
93
|
+
invariant(SchemaAST.isTypeLiteral(schema.ast));
|
|
94
94
|
|
|
95
95
|
const [values, setValues] = useState<Partial<T>>(initialValues);
|
|
96
96
|
useEffect(() => {
|
|
@@ -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
|
+
};
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2025 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { useCallback } from 'react';
|
|
6
|
+
|
|
7
|
+
import { getDXN, type TypeAnnotation } from '@dxos/echo-schema';
|
|
8
|
+
import { Filter, type Space } from '@dxos/react-client/echo';
|
|
9
|
+
import { isNonNullable } from '@dxos/util';
|
|
10
|
+
|
|
11
|
+
import { type QueryRefOptions } from './useQueryRefOptions';
|
|
12
|
+
|
|
13
|
+
type UseRefQueryLookupProps = { space?: Space };
|
|
14
|
+
|
|
15
|
+
export const useRefQueryLookupHandler = ({ space }: UseRefQueryLookupProps): QueryRefOptions => {
|
|
16
|
+
const handleRefQueryLookup = useCallback(
|
|
17
|
+
async (typeInfo: TypeAnnotation) => {
|
|
18
|
+
if (!space) {
|
|
19
|
+
return [];
|
|
20
|
+
}
|
|
21
|
+
const query = space.db.query(Filter.typename(typeInfo.typename));
|
|
22
|
+
const { objects } = await query.run();
|
|
23
|
+
|
|
24
|
+
return objects
|
|
25
|
+
.map((object) => {
|
|
26
|
+
const dxn = getDXN(object);
|
|
27
|
+
if (!dxn) {
|
|
28
|
+
return undefined;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// TODO(Zaymon): Better fallback object names?
|
|
32
|
+
const item = { dxn, label: object?.name ?? object?.id ?? '' };
|
|
33
|
+
return item;
|
|
34
|
+
})
|
|
35
|
+
.filter(isNonNullable);
|
|
36
|
+
},
|
|
37
|
+
[space],
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
return handleRefQueryLookup;
|
|
41
|
+
};
|
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
|
+
};
|