@dxos/react-ui-form 0.8.1 → 0.8.2-main.2f9c567
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 +338 -149
- package/dist/lib/browser/index.mjs.map +4 -4
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/node/index.cjs +326 -138
- package/dist/lib/node/index.cjs.map +4 -4
- package/dist/lib/node/meta.json +1 -1
- package/dist/lib/node-esm/index.mjs +338 -149
- 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} +8 -5
- 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/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 -3
- 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/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/RefField.d.ts +24 -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/index.d.ts +1 -0
- package/dist/types/src/components/Form/index.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/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/useForm.d.ts +2 -2
- package/dist/types/src/hooks/useForm.d.ts.map +1 -1
- 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/formatting.d.ts.map +1 -1
- package/package.json +25 -25
- package/src/components/ControlSection/ControlSection.stories.tsx +29 -0
- package/src/components/{ControlSection.tsx → ControlSection/ControlSection.tsx} +38 -14
- package/src/components/ControlSection/index.ts +5 -0
- package/src/components/Form/ArrayField.tsx +8 -7
- package/src/components/Form/Defaults.tsx +4 -1
- package/src/components/Form/Form.stories.tsx +90 -42
- package/src/components/Form/Form.tsx +7 -2
- package/src/components/Form/FormContent.tsx +62 -19
- package/src/components/Form/FormContext.tsx +10 -2
- package/src/components/Form/RefField.tsx +152 -0
- package/src/components/Form/custom/GeoPointInput.tsx +2 -2
- package/src/components/Form/custom/SelectOptionsInput.stories.tsx +5 -4
- package/src/components/Form/custom/SelectOptionsInput.tsx +2 -2
- package/src/components/Form/index.ts +1 -1
- package/src/components/ViewEditor/ViewEditor.stories.tsx +3 -2
- package/src/components/ViewEditor/ViewEditor.tsx +12 -10
- 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/hooks/index.ts +1 -0
- package/src/hooks/useForm.ts +3 -3
- package/src/hooks/useRefQueryLookupHandler.ts +43 -0
- package/src/util/formatting.ts +15 -3
- package/dist/types/src/components/ControlSection.d.ts.map +0 -1
|
@@ -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,29 +16,29 @@ 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, type QueryRefOptions } from './RefField';
|
|
20
20
|
import { getInputComponent } from './factory';
|
|
21
21
|
|
|
22
|
-
export type FormContentProps = {
|
|
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
|
-
};
|
|
31
|
-
|
|
32
22
|
export type FormFieldProps = {
|
|
33
23
|
property: SchemaProperty<any>;
|
|
34
24
|
path?: (string | number)[];
|
|
35
25
|
readonly?: boolean;
|
|
36
26
|
/** Used to indicate if input should be presented inline (e.g. for array items). */
|
|
37
27
|
inline?: boolean;
|
|
28
|
+
onQueryRefOptions?: QueryRefOptions;
|
|
38
29
|
lookupComponent?: ComponentLookup;
|
|
39
30
|
Custom?: Partial<Record<string, InputComponent>>;
|
|
40
31
|
};
|
|
41
32
|
|
|
42
|
-
export const FormField = ({
|
|
33
|
+
export const FormField = ({
|
|
34
|
+
property,
|
|
35
|
+
path,
|
|
36
|
+
readonly,
|
|
37
|
+
inline,
|
|
38
|
+
onQueryRefOptions,
|
|
39
|
+
lookupComponent,
|
|
40
|
+
Custom,
|
|
41
|
+
}: FormFieldProps) => {
|
|
43
42
|
const inputProps = useInputProps(path);
|
|
44
43
|
const { ast, name, type, format, title, description, options, examples, array } = property;
|
|
45
44
|
|
|
@@ -51,7 +50,7 @@ export const FormField = ({ property, path, readonly, inline, lookupComponent, C
|
|
|
51
50
|
|
|
52
51
|
const FoundComponent = lookupComponent?.({
|
|
53
52
|
prop: name,
|
|
54
|
-
schema:
|
|
53
|
+
schema: Schema.make(ast),
|
|
55
54
|
inputProps: {
|
|
56
55
|
type,
|
|
57
56
|
format,
|
|
@@ -120,17 +119,40 @@ export const FormField = ({ property, path, readonly, inline, lookupComponent, C
|
|
|
120
119
|
);
|
|
121
120
|
}
|
|
122
121
|
|
|
122
|
+
// TODO(ZaymonFC): Extract this to it's own component.
|
|
123
|
+
if (format === 'ref') {
|
|
124
|
+
return (
|
|
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
|
+
}
|
|
137
|
+
|
|
123
138
|
if (type === 'object') {
|
|
124
139
|
const baseNode = findNode(ast, isDiscriminatedUnion);
|
|
125
140
|
const typeLiteral = baseNode
|
|
126
141
|
? getDiscriminatedType(baseNode, inputProps.getValue() as any)
|
|
127
|
-
: findNode(ast,
|
|
142
|
+
: findNode(ast, SchemaAST.isTypeLiteral);
|
|
128
143
|
|
|
129
144
|
if (typeLiteral) {
|
|
130
145
|
return (
|
|
131
146
|
<div role='none'>
|
|
132
|
-
{!inline && <
|
|
133
|
-
<FormFields
|
|
147
|
+
{!inline && <h3 className='text-lg mbs-2 mbe-1'>{label}</h3>}
|
|
148
|
+
<FormFields
|
|
149
|
+
schema={Schema.make(typeLiteral)}
|
|
150
|
+
path={path}
|
|
151
|
+
readonly={readonly}
|
|
152
|
+
onQueryRefOptions={onQueryRefOptions}
|
|
153
|
+
Custom={Custom}
|
|
154
|
+
lookupComponent={lookupComponent}
|
|
155
|
+
/>
|
|
134
156
|
</div>
|
|
135
157
|
);
|
|
136
158
|
}
|
|
@@ -139,7 +161,27 @@ export const FormField = ({ property, path, readonly, inline, lookupComponent, C
|
|
|
139
161
|
return null;
|
|
140
162
|
};
|
|
141
163
|
|
|
142
|
-
export
|
|
164
|
+
export type FormContentProps = {
|
|
165
|
+
schema: Schema.Schema.All;
|
|
166
|
+
path?: (string | number)[];
|
|
167
|
+
filter?: (props: SchemaProperty<any>[]) => SchemaProperty<any>[];
|
|
168
|
+
sort?: string[];
|
|
169
|
+
readonly?: boolean;
|
|
170
|
+
onQueryRefOptions?: QueryRefOptions;
|
|
171
|
+
lookupComponent?: ComponentLookup;
|
|
172
|
+
Custom?: Partial<Record<string, InputComponent>>;
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
export const FormFields = ({
|
|
176
|
+
schema,
|
|
177
|
+
path,
|
|
178
|
+
filter,
|
|
179
|
+
sort,
|
|
180
|
+
readonly,
|
|
181
|
+
onQueryRefOptions,
|
|
182
|
+
lookupComponent,
|
|
183
|
+
Custom,
|
|
184
|
+
}: FormContentProps) => {
|
|
143
185
|
const values = useFormValues(path);
|
|
144
186
|
|
|
145
187
|
const properties = useMemo(() => {
|
|
@@ -159,6 +201,7 @@ export const FormFields = ({ schema, path, filter, sort, readonly, lookupCompone
|
|
|
159
201
|
property={property}
|
|
160
202
|
path={[...(path ?? []), property.name]}
|
|
161
203
|
readonly={readonly}
|
|
204
|
+
onQueryRefOptions={onQueryRefOptions}
|
|
162
205
|
lookupComponent={lookupComponent}
|
|
163
206
|
Custom={Custom}
|
|
164
207
|
/>
|
|
@@ -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
|
>) => {
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2025 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { type SchemaAST } from 'effect';
|
|
6
|
+
import React, { useEffect, useState } from 'react';
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
Expando,
|
|
10
|
+
getTypeAnnotation,
|
|
11
|
+
Ref,
|
|
12
|
+
ReferenceAnnotationId,
|
|
13
|
+
type ReferenceAnnotationValue,
|
|
14
|
+
type TypeAnnotation,
|
|
15
|
+
} from '@dxos/echo-schema';
|
|
16
|
+
import { findAnnotation, type SimpleType } from '@dxos/effect';
|
|
17
|
+
import { DXN } from '@dxos/keys';
|
|
18
|
+
import { refFromDXN } from '@dxos/live-object';
|
|
19
|
+
import { log } from '@dxos/log';
|
|
20
|
+
import { type MaybePromise } from '@dxos/util';
|
|
21
|
+
|
|
22
|
+
import { SelectInput, TextInput } from './Defaults';
|
|
23
|
+
import { type FormInputStateProps } from './FormContext';
|
|
24
|
+
|
|
25
|
+
export type QueryRefOptions = (type: TypeAnnotation) => MaybePromise<{ dxn: DXN; label?: string }[]>;
|
|
26
|
+
|
|
27
|
+
type RefFieldProps = {
|
|
28
|
+
ast: SchemaAST.AST;
|
|
29
|
+
type: SimpleType;
|
|
30
|
+
label: string;
|
|
31
|
+
readonly?: boolean;
|
|
32
|
+
placeholder?: string;
|
|
33
|
+
inline?: boolean;
|
|
34
|
+
onQueryRefOptions?: QueryRefOptions;
|
|
35
|
+
inputProps: FormInputStateProps;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export const RefField = ({
|
|
39
|
+
ast,
|
|
40
|
+
type,
|
|
41
|
+
label,
|
|
42
|
+
readonly,
|
|
43
|
+
placeholder,
|
|
44
|
+
inline,
|
|
45
|
+
onQueryRefOptions,
|
|
46
|
+
inputProps,
|
|
47
|
+
}: RefFieldProps) => {
|
|
48
|
+
const { getValue, onValueChange, ...restInputProps } = inputProps;
|
|
49
|
+
const refTypeInfo = findAnnotation<ReferenceAnnotationValue>(ast, ReferenceAnnotationId);
|
|
50
|
+
const [refOptions, setRefOptions] = useState<Array<{ value: string; label?: string }>>([]);
|
|
51
|
+
const [loading, setLoading] = useState(false);
|
|
52
|
+
|
|
53
|
+
useEffect(() => {
|
|
54
|
+
if (!refTypeInfo || !onQueryRefOptions) {
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const fetchOptions = async () => {
|
|
59
|
+
setLoading(true);
|
|
60
|
+
try {
|
|
61
|
+
const options = await onQueryRefOptions(refTypeInfo);
|
|
62
|
+
setRefOptions(
|
|
63
|
+
options.map((option) => ({
|
|
64
|
+
label: option.label,
|
|
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);
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
void fetchOptions();
|
|
77
|
+
}, [refTypeInfo, onQueryRefOptions]);
|
|
78
|
+
|
|
79
|
+
if (!refTypeInfo) {
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// If ref type is expando, fall back to taking a DXN in string format.
|
|
84
|
+
if (refTypeInfo.typename === getTypeAnnotation(Expando)?.typename || !onQueryRefOptions) {
|
|
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
|
+
};
|
|
95
|
+
|
|
96
|
+
const handleGetValue = () => {
|
|
97
|
+
const formValue = getValue();
|
|
98
|
+
if (typeof formValue === 'string') {
|
|
99
|
+
return formValue;
|
|
100
|
+
}
|
|
101
|
+
if (Ref.isRef(formValue)) {
|
|
102
|
+
return formValue.dxn.toString();
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return undefined;
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
return (
|
|
109
|
+
<TextInput
|
|
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
|
+
);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const handleGetValue = () => {
|
|
123
|
+
const formValue = getValue();
|
|
124
|
+
|
|
125
|
+
if (Ref.isRef(formValue)) {
|
|
126
|
+
return formValue.dxn.toString();
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return undefined;
|
|
130
|
+
};
|
|
131
|
+
|
|
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
|
+
return (
|
|
140
|
+
<SelectInput
|
|
141
|
+
type={type}
|
|
142
|
+
label={label}
|
|
143
|
+
disabled={readonly || loading}
|
|
144
|
+
placeholder={loading ? 'Loading options...' : placeholder}
|
|
145
|
+
inputOnly={inline}
|
|
146
|
+
getValue={handleGetValue as <V>() => V | undefined}
|
|
147
|
+
onValueChange={handleValueChanged}
|
|
148
|
+
options={refOptions}
|
|
149
|
+
{...restInputProps}
|
|
150
|
+
/>
|
|
151
|
+
);
|
|
152
|
+
};
|
|
@@ -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[] }>({
|
|
@@ -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
|
}
|
|
@@ -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
|
}) {}
|
|
@@ -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
|
>
|
|
@@ -7,7 +7,7 @@ import '@dxos-theme';
|
|
|
7
7
|
import { type Meta, type StoryObj } from '@storybook/react';
|
|
8
8
|
import React from 'react';
|
|
9
9
|
|
|
10
|
-
import {
|
|
10
|
+
import { live } from '@dxos/live-object';
|
|
11
11
|
import { testData, TestSchema, testView, type TestType } from '@dxos/schema/testing';
|
|
12
12
|
import { withLayout, withTheme } from '@dxos/storybook-utils';
|
|
13
13
|
|
|
@@ -39,23 +39,23 @@ type Story = StoryObj<DeprecatedFormProps<TestType>>;
|
|
|
39
39
|
|
|
40
40
|
export const Default: Story = {
|
|
41
41
|
args: {
|
|
42
|
-
object:
|
|
43
|
-
view:
|
|
42
|
+
object: live(testData),
|
|
43
|
+
view: live(testView),
|
|
44
44
|
schema: TestSchema,
|
|
45
45
|
},
|
|
46
46
|
};
|
|
47
47
|
|
|
48
48
|
export const Empty: Story = {
|
|
49
49
|
args: {
|
|
50
|
-
view:
|
|
50
|
+
view: live(testView),
|
|
51
51
|
schema: TestSchema,
|
|
52
52
|
},
|
|
53
53
|
};
|
|
54
54
|
|
|
55
55
|
export const Readonly: Story = {
|
|
56
56
|
args: {
|
|
57
|
-
object:
|
|
58
|
-
view:
|
|
57
|
+
object: live(testData),
|
|
58
|
+
view: live(testView),
|
|
59
59
|
schema: TestSchema,
|
|
60
60
|
readonly: true,
|
|
61
61
|
},
|
|
@@ -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);
|
|
@@ -12,7 +12,7 @@ import { mx, textBlockWidth } from '@dxos/react-ui-theme';
|
|
|
12
12
|
*/
|
|
13
13
|
export const DeprecatedFormContainer = ({ children, classNames }: ThemedClassName<PropsWithChildren>) => {
|
|
14
14
|
return (
|
|
15
|
-
<div role='none' className='
|
|
15
|
+
<div role='none' className='container-max-width p-4 justify-center overflow-x-hidden overflow-y-auto'>
|
|
16
16
|
<div role='form' className={mx('flex flex-col', textBlockWidth, classNames)}>
|
|
17
17
|
{children}
|
|
18
18
|
</div>
|
package/src/components/index.ts
CHANGED
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,43 @@
|
|
|
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 '../components';
|
|
12
|
+
|
|
13
|
+
type UseRefQueryLookupProps = {
|
|
14
|
+
space?: Space;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export const useRefQueryLookupHandler = ({ space }: UseRefQueryLookupProps): QueryRefOptions => {
|
|
18
|
+
const handleRefQueryLookup = useCallback(
|
|
19
|
+
async (typeInfo: TypeAnnotation) => {
|
|
20
|
+
if (!space) {
|
|
21
|
+
return [];
|
|
22
|
+
}
|
|
23
|
+
const query = space.db.query(Filter.typename(typeInfo.typename));
|
|
24
|
+
const { objects } = await query.run();
|
|
25
|
+
|
|
26
|
+
return objects
|
|
27
|
+
.map((object) => {
|
|
28
|
+
const dxn = getDXN(object);
|
|
29
|
+
if (!dxn) {
|
|
30
|
+
return undefined;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// TODO(Zaymon): Better fallback object names?
|
|
34
|
+
const item = { dxn, label: object?.name ?? object?.id ?? '' };
|
|
35
|
+
return item;
|
|
36
|
+
})
|
|
37
|
+
.filter(isNonNullable);
|
|
38
|
+
},
|
|
39
|
+
[space],
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
return handleRefQueryLookup;
|
|
43
|
+
};
|