@dxos/react-ui-form 0.8.2-main.fbd8ed0 → 0.8.2-staging.7ac8446
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 +537 -846
- package/dist/lib/browser/index.mjs.map +4 -4
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/node/index.cjs +506 -807
- package/dist/lib/node/index.cjs.map +4 -4
- package/dist/lib/node/meta.json +1 -1
- package/dist/lib/node-esm/index.mjs +537 -846
- 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 → ControlSection.d.ts} +5 -8
- package/dist/types/src/components/ControlSection.d.ts.map +1 -0
- 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 -6
- package/dist/types/src/components/Form/Form.d.ts.map +1 -1
- package/dist/types/src/components/Form/Form.stories.d.ts +34 -37
- 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 +11 -14
- 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/custom/GeoPointInput.d.ts.map +1 -1
- package/dist/types/src/components/Form/custom/SelectOptionsInput.d.ts.map +1 -1
- package/dist/types/src/components/Form/custom/SelectOptionsInput.stories.d.ts.map +1 -1
- package/dist/types/src/components/Form/custom/TupleInput.d.ts.map +1 -1
- package/dist/types/src/components/Form/factory.d.ts +1 -1
- package/dist/types/src/components/Form/factory.d.ts.map +1 -1
- package/dist/types/src/components/Form/index.d.ts.map +1 -1
- package/dist/types/src/components/ViewEditor/Test.stories.d.ts.map +1 -1
- package/dist/types/src/components/ViewEditor/ViewEditor.d.ts +2 -2
- package/dist/types/src/components/ViewEditor/ViewEditor.d.ts.map +1 -1
- package/dist/types/src/components/ViewEditor/ViewEditor.stories.d.ts.map +1 -1
- package/dist/types/src/components/deprecated/Form.d.ts +2 -2
- package/dist/types/src/components/deprecated/Form.d.ts.map +1 -1
- package/dist/types/src/components/deprecated/Form.stories.d.ts.map +1 -1
- package/dist/types/src/components/deprecated/FormInput.d.ts.map +1 -1
- package/dist/types/src/components/index.d.ts.map +1 -1
- package/dist/types/src/components/testing.d.ts.map +1 -1
- package/dist/types/src/hooks/index.d.ts +0 -2
- 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/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 +0 -2
- package/dist/types/src/util/index.d.ts.map +1 -1
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/package.json +25 -26
- package/src/components/{ControlSection/ControlSection.tsx → ControlSection.tsx} +14 -38
- package/src/components/FieldEditor/FieldEditor.stories.tsx +1 -1
- package/src/components/Form/ArrayField.tsx +17 -13
- package/src/components/Form/Defaults.tsx +1 -4
- package/src/components/Form/Form.stories.tsx +43 -92
- package/src/components/Form/Form.tsx +3 -7
- package/src/components/Form/FormContent.tsx +19 -90
- package/src/components/Form/FormContext.tsx +3 -13
- package/src/components/Form/Input.tsx +9 -3
- package/src/components/Form/custom/GeoPointInput.tsx +2 -2
- package/src/components/Form/custom/SelectOptionsInput.stories.tsx +5 -6
- package/src/components/Form/custom/SelectOptionsInput.tsx +2 -2
- package/src/components/Form/factory.tsx +1 -1
- package/src/components/Form/index.ts +1 -0
- package/src/components/ViewEditor/ViewEditor.stories.tsx +4 -5
- package/src/components/ViewEditor/ViewEditor.tsx +20 -23
- 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 +0 -1
- package/src/hooks/index.ts +1 -3
- package/src/hooks/useForm.ts +3 -4
- package/src/util/formatting.ts +3 -15
- package/src/util/index.ts +1 -3
- package/dist/types/src/components/ControlSection/ControlSection.d.ts.map +0 -1
- package/dist/types/src/components/ControlSection/ControlSection.stories.d.ts +0 -8
- package/dist/types/src/components/ControlSection/ControlSection.stories.d.ts.map +0 -1
- package/dist/types/src/components/ControlSection/index.d.ts +0 -2
- package/dist/types/src/components/ControlSection/index.d.ts.map +0 -1
- package/dist/types/src/components/Form/RefField.d.ts +0 -12
- package/dist/types/src/components/Form/RefField.d.ts.map +0 -1
- package/dist/types/src/hooks/useQueryRefOptions.d.ts +0 -23
- package/dist/types/src/hooks/useQueryRefOptions.d.ts.map +0 -1
- package/dist/types/src/hooks/useRefQueryLookupHandler.d.ts +0 -8
- package/dist/types/src/hooks/useRefQueryLookupHandler.d.ts.map +0 -1
- package/dist/types/src/util/refs.d.ts +0 -9
- package/dist/types/src/util/refs.d.ts.map +0 -1
- package/dist/types/src/util/schema.d.ts +0 -3
- package/dist/types/src/util/schema.d.ts.map +0 -1
- package/src/components/ControlSection/ControlSection.stories.tsx +0 -29
- package/src/components/ControlSection/index.ts +0 -5
- package/src/components/Form/RefField.tsx +0 -197
- package/src/hooks/useQueryRefOptions.ts +0 -53
- package/src/hooks/useRefQueryLookupHandler.ts +0 -41
- package/src/util/refs.ts +0 -38
- package/src/util/schema.ts +0 -14
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
//
|
|
2
|
-
// Copyright
|
|
2
|
+
// Copyright 2024 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import {
|
|
5
|
+
import { 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';
|
|
9
10
|
import { createJsonPath, findNode, getDiscriminatedType, isDiscriminatedUnion } from '@dxos/effect';
|
|
10
11
|
import { mx } from '@dxos/react-ui-theme';
|
|
11
12
|
import { getSchemaProperties, type SchemaProperty } from '@dxos/schema';
|
|
@@ -16,10 +17,17 @@ import { SelectInput } from './Defaults';
|
|
|
16
17
|
import { type ComponentLookup } from './Form';
|
|
17
18
|
import { useInputProps, useFormValues } from './FormContext';
|
|
18
19
|
import { type InputComponent } from './Input';
|
|
19
|
-
import { RefField } from './RefField';
|
|
20
20
|
import { getInputComponent } from './factory';
|
|
21
|
-
|
|
22
|
-
|
|
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
|
+
};
|
|
23
31
|
|
|
24
32
|
export type FormFieldProps = {
|
|
25
33
|
property: SchemaProperty<any>;
|
|
@@ -27,20 +35,11 @@ export type FormFieldProps = {
|
|
|
27
35
|
readonly?: boolean;
|
|
28
36
|
/** Used to indicate if input should be presented inline (e.g. for array items). */
|
|
29
37
|
inline?: boolean;
|
|
30
|
-
onQueryRefOptions?: QueryRefOptions;
|
|
31
38
|
lookupComponent?: ComponentLookup;
|
|
32
39
|
Custom?: Partial<Record<string, InputComponent>>;
|
|
33
40
|
};
|
|
34
41
|
|
|
35
|
-
export const FormField = ({
|
|
36
|
-
property,
|
|
37
|
-
path,
|
|
38
|
-
readonly,
|
|
39
|
-
inline,
|
|
40
|
-
onQueryRefOptions,
|
|
41
|
-
lookupComponent,
|
|
42
|
-
Custom,
|
|
43
|
-
}: FormFieldProps) => {
|
|
42
|
+
export const FormField = ({ property, path, readonly, inline, lookupComponent, Custom }: FormFieldProps) => {
|
|
44
43
|
const inputProps = useInputProps(path);
|
|
45
44
|
const { ast, name, type, format, title, description, options, examples, array } = property;
|
|
46
45
|
|
|
@@ -50,13 +49,9 @@ export const FormField = ({
|
|
|
50
49
|
[examples, description],
|
|
51
50
|
);
|
|
52
51
|
|
|
53
|
-
//
|
|
54
|
-
// Registry and Custom.
|
|
55
|
-
//
|
|
56
|
-
|
|
57
52
|
const FoundComponent = lookupComponent?.({
|
|
58
53
|
prop: name,
|
|
59
|
-
schema:
|
|
54
|
+
schema: S.make(ast),
|
|
60
55
|
inputProps: {
|
|
61
56
|
type,
|
|
62
57
|
format,
|
|
@@ -87,40 +82,10 @@ export const FormField = ({
|
|
|
87
82
|
);
|
|
88
83
|
}
|
|
89
84
|
|
|
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
|
-
|
|
116
85
|
if (array) {
|
|
117
86
|
return <ArrayField property={property} path={path} inputProps={inputProps} readonly={readonly} Custom={Custom} />;
|
|
118
87
|
}
|
|
119
88
|
|
|
120
|
-
//
|
|
121
|
-
// Standard Inputs.
|
|
122
|
-
//
|
|
123
|
-
|
|
124
89
|
const InputComponent = getInputComponent(type, format);
|
|
125
90
|
if (InputComponent) {
|
|
126
91
|
return (
|
|
@@ -138,10 +103,6 @@ export const FormField = ({
|
|
|
138
103
|
);
|
|
139
104
|
}
|
|
140
105
|
|
|
141
|
-
//
|
|
142
|
-
// Select.
|
|
143
|
-
//
|
|
144
|
-
|
|
145
106
|
if (options) {
|
|
146
107
|
return (
|
|
147
108
|
<div role='none'>
|
|
@@ -159,28 +120,17 @@ export const FormField = ({
|
|
|
159
120
|
);
|
|
160
121
|
}
|
|
161
122
|
|
|
162
|
-
//
|
|
163
|
-
// Nested Objects.
|
|
164
|
-
//
|
|
165
|
-
|
|
166
123
|
if (type === 'object') {
|
|
167
124
|
const baseNode = findNode(ast, isDiscriminatedUnion);
|
|
168
125
|
const typeLiteral = baseNode
|
|
169
126
|
? getDiscriminatedType(baseNode, inputProps.getValue() as any)
|
|
170
|
-
: findNode(ast,
|
|
127
|
+
: findNode(ast, AST.isTypeLiteral);
|
|
171
128
|
|
|
172
129
|
if (typeLiteral) {
|
|
173
130
|
return (
|
|
174
131
|
<div role='none'>
|
|
175
|
-
{!inline && <
|
|
176
|
-
<FormFields
|
|
177
|
-
schema={Schema.make(typeLiteral)}
|
|
178
|
-
path={path}
|
|
179
|
-
readonly={readonly}
|
|
180
|
-
onQueryRefOptions={onQueryRefOptions}
|
|
181
|
-
Custom={Custom}
|
|
182
|
-
lookupComponent={lookupComponent}
|
|
183
|
-
/>
|
|
132
|
+
{!inline && <div>{label}</div>}
|
|
133
|
+
<FormFields schema={S.make(typeLiteral)} path={path} readonly={readonly} Custom={Custom} />
|
|
184
134
|
</div>
|
|
185
135
|
);
|
|
186
136
|
}
|
|
@@ -189,27 +139,7 @@ export const FormField = ({
|
|
|
189
139
|
return null;
|
|
190
140
|
};
|
|
191
141
|
|
|
192
|
-
export
|
|
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) => {
|
|
142
|
+
export const FormFields = ({ schema, path, filter, sort, readonly, lookupComponent, Custom }: FormContentProps) => {
|
|
213
143
|
const values = useFormValues(path);
|
|
214
144
|
|
|
215
145
|
const properties = useMemo(() => {
|
|
@@ -229,7 +159,6 @@ export const FormFields = ({
|
|
|
229
159
|
property={property}
|
|
230
160
|
path={[...(path ?? []), property.name]}
|
|
231
161
|
readonly={readonly}
|
|
232
|
-
onQueryRefOptions={onQueryRefOptions}
|
|
233
162
|
lookupComponent={lookupComponent}
|
|
234
163
|
Custom={Custom}
|
|
235
164
|
/>
|
|
@@ -2,15 +2,7 @@
|
|
|
2
2
|
// Copyright 2025 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import React, {
|
|
6
|
-
createContext,
|
|
7
|
-
type RefObject,
|
|
8
|
-
useContext,
|
|
9
|
-
useEffect,
|
|
10
|
-
useMemo,
|
|
11
|
-
type FocusEvent,
|
|
12
|
-
type PropsWithChildren,
|
|
13
|
-
} from 'react';
|
|
5
|
+
import React, { createContext, useContext, useEffect, useMemo, type FocusEvent, type PropsWithChildren } from 'react';
|
|
14
6
|
|
|
15
7
|
import { raise } from '@dxos/debug';
|
|
16
8
|
import { type BaseObject, getValue } from '@dxos/echo-schema';
|
|
@@ -61,7 +53,7 @@ export const FormProvider = ({
|
|
|
61
53
|
...formOptions
|
|
62
54
|
}: PropsWithChildren<
|
|
63
55
|
FormOptions<any> & {
|
|
64
|
-
formRef?: RefObject<HTMLDivElement>;
|
|
56
|
+
formRef?: React.RefObject<HTMLDivElement>;
|
|
65
57
|
autoSave?: boolean;
|
|
66
58
|
}
|
|
67
59
|
>) => {
|
|
@@ -76,9 +68,7 @@ export const FormProvider = ({
|
|
|
76
68
|
const keyIsEnter = event.key === 'Enter';
|
|
77
69
|
const modifierUsed = event.ctrlKey || event.altKey || event.metaKey || event.shiftKey;
|
|
78
70
|
const inputIsTextarea = (event.target as HTMLElement).tagName.toLowerCase() === 'textarea';
|
|
79
|
-
const inputOptOut =
|
|
80
|
-
(event.target as HTMLElement).hasAttribute('data-no-submit') ||
|
|
81
|
-
(event.target as HTMLElement).closest('[data-no-submit]') !== null;
|
|
71
|
+
const inputOptOut = (event.target as HTMLElement).hasAttribute('data-no-submit');
|
|
82
72
|
|
|
83
73
|
// Regular inputs: Submit on Enter (no modifiers).
|
|
84
74
|
const shouldSubmitRegularInput = !inputIsTextarea && keyIsEnter && !modifierUsed;
|
|
@@ -37,9 +37,15 @@ 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
|
-
|
|
40
|
+
<Tooltip.Root>
|
|
41
|
+
<Tooltip.Trigger asChild>
|
|
42
|
+
<Icon icon='ph--warning--regular' size={4} classNames={errorText} />
|
|
43
|
+
</Tooltip.Trigger>
|
|
44
|
+
<Tooltip.Content side='bottom'>
|
|
45
|
+
<Tooltip.Arrow />
|
|
46
|
+
{error}
|
|
47
|
+
</Tooltip.Content>
|
|
48
|
+
</Tooltip.Root>
|
|
43
49
|
)}
|
|
44
50
|
</div>
|
|
45
51
|
);
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// Copyright 2024 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import React, { useCallback, useState, useEffect, useMemo
|
|
5
|
+
import React, { useCallback, useState, useEffect, useMemo } 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: ChangeEvent<HTMLInputElement>) => {
|
|
31
|
+
(event: React.ChangeEvent<HTMLInputElement>) => {
|
|
32
32
|
const inputText = event.target.value;
|
|
33
33
|
setText(inputText);
|
|
34
34
|
if (inputText !== '' && inputText !== '-') {
|
|
@@ -5,10 +5,9 @@
|
|
|
5
5
|
import '@dxos-theme';
|
|
6
6
|
|
|
7
7
|
import { type Meta, type StoryObj } from '@storybook/react';
|
|
8
|
-
import { Schema } from 'effect';
|
|
9
8
|
import React, { useState } from 'react';
|
|
10
9
|
|
|
11
|
-
import { SelectOptionSchema, type SelectOption } from '@dxos/echo-schema';
|
|
10
|
+
import { S, SelectOptionSchema, type SelectOption } from '@dxos/echo-schema';
|
|
12
11
|
import { withLayout, withTheme } from '@dxos/storybook-utils';
|
|
13
12
|
|
|
14
13
|
import { SelectOptionInput } from './SelectOptionsInput';
|
|
@@ -16,9 +15,9 @@ import translations from '../../../translations';
|
|
|
16
15
|
import { TestLayout, TestPanel } from '../../testing';
|
|
17
16
|
import { Form } from '../Form';
|
|
18
17
|
|
|
19
|
-
const schema =
|
|
20
|
-
options:
|
|
21
|
-
}).pipe(
|
|
18
|
+
const schema = S.Struct({
|
|
19
|
+
options: S.Array(SelectOptionSchema).pipe(S.mutable),
|
|
20
|
+
}).pipe(S.mutable);
|
|
22
21
|
|
|
23
22
|
const DefaultStory = () => {
|
|
24
23
|
const [values, setValues] = useState<{ options: SelectOption[] }>({
|
|
@@ -47,7 +46,7 @@ const DefaultStory = () => {
|
|
|
47
46
|
const meta: Meta = {
|
|
48
47
|
title: 'ui/react-ui-form/SelectOptionsInput',
|
|
49
48
|
component: SelectOptionInput,
|
|
50
|
-
decorators: [withTheme, withLayout()],
|
|
49
|
+
decorators: [withTheme, withLayout({ tooltips: true })],
|
|
51
50
|
parameters: { translations },
|
|
52
51
|
};
|
|
53
52
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// Copyright 2025 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import React, {
|
|
5
|
+
import React, { useCallback, useEffect, useState, type ChangeEvent, 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: KeyboardEvent<HTMLInputElement>) => {
|
|
92
|
+
const handleKeyDown = useCallback((e: React.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
|
|
15
|
+
export const getInputComponent = (type: SimpleType, format?: FormatEnum): InputComponent | undefined => {
|
|
16
16
|
switch (format) {
|
|
17
17
|
case FormatEnum.GeoPoint:
|
|
18
18
|
return GeoPointInput;
|
|
@@ -5,10 +5,9 @@
|
|
|
5
5
|
import '@dxos-theme';
|
|
6
6
|
|
|
7
7
|
import { type Meta, type StoryObj } from '@storybook/react';
|
|
8
|
-
import { Schema } from 'effect';
|
|
9
8
|
import React, { useCallback, useMemo, useState } from 'react';
|
|
10
9
|
|
|
11
|
-
import { Format, type EchoSchema, toJsonSchema, TypedObject } from '@dxos/echo-schema';
|
|
10
|
+
import { Format, type EchoSchema, S, toJsonSchema, TypedObject } from '@dxos/echo-schema';
|
|
12
11
|
import { Filter, useQuery, useSpace } from '@dxos/react-client/echo';
|
|
13
12
|
import { withClientProvider } from '@dxos/react-client/testing';
|
|
14
13
|
import { useAsyncEffect } from '@dxos/react-ui';
|
|
@@ -27,7 +26,7 @@ const DefaultStory = () => {
|
|
|
27
26
|
useAsyncEffect(async () => {
|
|
28
27
|
if (space) {
|
|
29
28
|
class TestSchema extends TypedObject({ typename: 'example.com/type/Test', version: '0.1.0' })({
|
|
30
|
-
name:
|
|
29
|
+
name: S.String,
|
|
31
30
|
email: Format.Email,
|
|
32
31
|
salary: Format.Currency(),
|
|
33
32
|
}) {}
|
|
@@ -42,7 +41,7 @@ const DefaultStory = () => {
|
|
|
42
41
|
}
|
|
43
42
|
}, [space]);
|
|
44
43
|
|
|
45
|
-
const views = useQuery(space, Filter.
|
|
44
|
+
const views = useQuery(space, Filter.schema(ViewType));
|
|
46
45
|
const currentTypename = useMemo(() => view?.query?.typename, [view]);
|
|
47
46
|
const updateViewTypename = useCallback(
|
|
48
47
|
(newTypename: string) => {
|
|
@@ -82,7 +81,7 @@ const meta: Meta<typeof ViewEditor> = {
|
|
|
82
81
|
title: 'ui/react-ui-form/ViewEditor',
|
|
83
82
|
component: ViewEditor,
|
|
84
83
|
render: DefaultStory,
|
|
85
|
-
decorators: [withClientProvider({ createSpace: true }), withLayout({ fullscreen: true }), withTheme],
|
|
84
|
+
decorators: [withClientProvider({ createSpace: true }), withLayout({ fullscreen: true, tooltips: true }), withTheme],
|
|
86
85
|
parameters: {
|
|
87
86
|
translations,
|
|
88
87
|
},
|
|
@@ -2,11 +2,10 @@
|
|
|
2
2
|
// Copyright 2024 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import { Schema, SchemaAST } from 'effect';
|
|
6
5
|
import React, { useCallback, useMemo, useState } from 'react';
|
|
7
6
|
|
|
8
7
|
import { type SchemaRegistry } from '@dxos/echo-db';
|
|
9
|
-
import { Format, type
|
|
8
|
+
import { AST, Format, type BaseSchema, S, type JsonProp } from '@dxos/echo-schema';
|
|
10
9
|
import { invariant } from '@dxos/invariant';
|
|
11
10
|
import { IconButton, type ThemedClassName, useTranslation } from '@dxos/react-ui';
|
|
12
11
|
import { List } from '@dxos/react-ui-list';
|
|
@@ -19,19 +18,19 @@ import { Form } from '../Form';
|
|
|
19
18
|
|
|
20
19
|
const grid = 'grid grid-cols-[32px_1fr_32px] min-bs-[2.5rem]';
|
|
21
20
|
|
|
22
|
-
const ViewMetaSchema =
|
|
23
|
-
name:
|
|
24
|
-
[
|
|
21
|
+
const ViewMetaSchema = S.Struct({
|
|
22
|
+
name: S.String.annotations({
|
|
23
|
+
[AST.TitleAnnotationId]: 'View',
|
|
25
24
|
}),
|
|
26
25
|
typename: Format.URL.annotations({
|
|
27
|
-
[
|
|
26
|
+
[AST.TitleAnnotationId]: 'Typename',
|
|
28
27
|
}),
|
|
29
|
-
}).pipe(
|
|
28
|
+
}).pipe(S.mutable);
|
|
30
29
|
|
|
31
|
-
type ViewMetaType =
|
|
30
|
+
type ViewMetaType = S.Schema.Type<typeof ViewMetaSchema>;
|
|
32
31
|
|
|
33
32
|
export type ViewEditorProps = ThemedClassName<{
|
|
34
|
-
schema:
|
|
33
|
+
schema: BaseSchema;
|
|
35
34
|
view: ViewType;
|
|
36
35
|
registry?: SchemaRegistry;
|
|
37
36
|
readonly?: boolean;
|
|
@@ -54,9 +53,8 @@ export const ViewEditor = ({
|
|
|
54
53
|
onDelete,
|
|
55
54
|
}: ViewEditorProps) => {
|
|
56
55
|
const { t } = useTranslation(translationKey);
|
|
57
|
-
const projection = useMemo(() => new ViewProjection(
|
|
56
|
+
const projection = useMemo(() => new ViewProjection(schema.jsonSchema, view), [schema, view]);
|
|
58
57
|
const [field, setField] = useState<FieldType>();
|
|
59
|
-
const immutable = readonly || !isMutable(schema);
|
|
60
58
|
|
|
61
59
|
// TODO(burdon): Should be reactive.
|
|
62
60
|
const viewValues = useMemo(() => {
|
|
@@ -75,14 +73,14 @@ export const ViewEditor = ({
|
|
|
75
73
|
// TODO(burdon): Check if mutable; variant of useCallback that return undefined if readonly?
|
|
76
74
|
|
|
77
75
|
const handleAdd = useCallback(() => {
|
|
78
|
-
invariant(!
|
|
76
|
+
invariant(!schema.readonly);
|
|
79
77
|
const field = projection.createFieldProjection();
|
|
80
78
|
setField(field);
|
|
81
79
|
}, [schema, view]);
|
|
82
80
|
|
|
83
81
|
const handleUpdate = useCallback(
|
|
84
82
|
({ name, typename }: ViewMetaType) => {
|
|
85
|
-
invariant(!
|
|
83
|
+
invariant(!schema.readonly);
|
|
86
84
|
requestAnimationFrame(() => {
|
|
87
85
|
if (view.name !== name) {
|
|
88
86
|
view.name = name;
|
|
@@ -98,7 +96,7 @@ export const ViewEditor = ({
|
|
|
98
96
|
|
|
99
97
|
const handleDelete = useCallback(
|
|
100
98
|
(fieldId: string) => {
|
|
101
|
-
invariant(!
|
|
99
|
+
invariant(!schema.readonly);
|
|
102
100
|
if (fieldId === field?.id) {
|
|
103
101
|
setField(undefined);
|
|
104
102
|
}
|
|
@@ -110,7 +108,7 @@ export const ViewEditor = ({
|
|
|
110
108
|
|
|
111
109
|
const handleMove = useCallback(
|
|
112
110
|
(fromIndex: number, toIndex: number) => {
|
|
113
|
-
invariant(!
|
|
111
|
+
invariant(!schema.readonly);
|
|
114
112
|
// NOTE(ZaymonFC): Using arrayMove here causes a race condition with the kanban model.
|
|
115
113
|
const fields = [...view.fields];
|
|
116
114
|
const [moved] = fields.splice(fromIndex, 1);
|
|
@@ -141,16 +139,15 @@ export const ViewEditor = ({
|
|
|
141
139
|
);
|
|
142
140
|
|
|
143
141
|
return (
|
|
144
|
-
<div role='none' className={mx('
|
|
142
|
+
<div role='none' className={mx('flex flex-col w-full divide-y divide-separator', classNames)}>
|
|
145
143
|
<Form<ViewMetaType>
|
|
146
144
|
autoSave
|
|
147
145
|
schema={ViewMetaSchema}
|
|
148
146
|
values={viewValues}
|
|
149
|
-
onSave={
|
|
150
|
-
classNames='min-bs-0 overflow-y-auto'
|
|
147
|
+
onSave={schema.readonly ? undefined : handleUpdate}
|
|
151
148
|
/>
|
|
152
149
|
|
|
153
|
-
<div
|
|
150
|
+
<div>
|
|
154
151
|
{/* TODO(burdon): Clean up common form ux. */}
|
|
155
152
|
<div role='none' className='p-2'>
|
|
156
153
|
<label className={mx(inputTextLabel)}>{t('fields label')}</label>
|
|
@@ -158,9 +155,9 @@ export const ViewEditor = ({
|
|
|
158
155
|
|
|
159
156
|
<List.Root<FieldType>
|
|
160
157
|
items={view.fields}
|
|
161
|
-
isItem={
|
|
158
|
+
isItem={S.is(FieldSchema)}
|
|
162
159
|
getId={(field) => field.id}
|
|
163
|
-
onMove={
|
|
160
|
+
onMove={schema.readonly ? undefined : handleMove}
|
|
164
161
|
>
|
|
165
162
|
{({ items: fields }) => (
|
|
166
163
|
<>
|
|
@@ -182,7 +179,7 @@ export const ViewEditor = ({
|
|
|
182
179
|
disabled={view.fields.length <= 1}
|
|
183
180
|
onClick={() => handleHide(field.id)}
|
|
184
181
|
/>
|
|
185
|
-
{!
|
|
182
|
+
{!schema.readonly && (
|
|
186
183
|
<List.ItemDeleteButton
|
|
187
184
|
icon='ph--trash--regular'
|
|
188
185
|
disabled={view.fields.length <= 1}
|
|
@@ -244,7 +241,7 @@ export const ViewEditor = ({
|
|
|
244
241
|
<IconButton
|
|
245
242
|
icon='ph--plus--regular'
|
|
246
243
|
label={t('button add property')}
|
|
247
|
-
onClick={
|
|
244
|
+
onClick={schema.readonly ? undefined : handleAdd}
|
|
248
245
|
// TODO(burdon): Show field limit in ux (not tooltip).
|
|
249
246
|
disabled={view.fields.length >= VIEW_FIELD_LIMIT}
|
|
250
247
|
/>
|
|
@@ -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 { create } 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: create(testData),
|
|
43
|
+
view: create(testView),
|
|
44
44
|
schema: TestSchema,
|
|
45
45
|
},
|
|
46
46
|
};
|
|
47
47
|
|
|
48
48
|
export const Empty: Story = {
|
|
49
49
|
args: {
|
|
50
|
-
view:
|
|
50
|
+
view: create(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: create(testData),
|
|
58
|
+
view: create(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, type Schema, Option, pipe } from 'effect';
|
|
5
|
+
import { SchemaAST as AST, type Schema as S, 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?: S.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(AST.getTitleAnnotation(prop), Option.getOrUndefined)) ?? '';
|
|
35
|
+
const description = (prop && pipe(AST.getDescriptionAnnotation(prop), Option.getOrUndefined)) ?? title;
|
|
36
36
|
// const format =
|
|
37
|
-
// (prop && (getPatternAnnotation(prop) ?? (
|
|
37
|
+
// (prop && (getPatternAnnotation(prop) ?? (AST.isNumberKeyword(prop) && RealNumberFormat))) || undefined;
|
|
38
38
|
|
|
39
39
|
//
|
|
40
40
|
// Boolean
|
|
41
41
|
//
|
|
42
42
|
|
|
43
|
-
if (prop &&
|
|
43
|
+
if (prop && AST.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='w-full 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, type Schema } from 'effect';
|
|
5
|
+
import { SchemaAST as AST, type Schema as S } 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: S.Schema<T>;
|
|
49
49
|
|
|
50
50
|
/**
|
|
51
51
|
* Initial values (which may not pass validation).
|
|
@@ -90,10 +90,9 @@ export const useForm = <T extends BaseObject>({
|
|
|
90
90
|
onValid,
|
|
91
91
|
onSave,
|
|
92
92
|
}: FormOptions<T>): FormHandler<T> => {
|
|
93
|
-
invariant(
|
|
93
|
+
invariant(AST.isTypeLiteral(schema.ast));
|
|
94
94
|
|
|
95
95
|
const [values, setValues] = useState<Partial<T>>(initialValues);
|
|
96
|
-
|
|
97
96
|
useEffect(() => {
|
|
98
97
|
setValues(initialValues);
|
|
99
98
|
}, [initialValues]);
|