@dxos/react-ui-form 0.8.1-staging.9eaf14f → 0.8.2-main.10c050d
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 +1818 -1309
- package/dist/lib/browser/index.mjs.map +4 -4
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/node/index.cjs +1774 -1273
- package/dist/lib/node/index.cjs.map +4 -4
- package/dist/lib/node/meta.json +1 -1
- package/dist/lib/node-esm/index.mjs +1818 -1309
- 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} +9 -6
- 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/FieldEditor/FieldEditor.d.ts.map +1 -1
- package/dist/types/src/components/FieldEditor/FieldEditor.test.d.ts +2 -0
- package/dist/types/src/components/FieldEditor/FieldEditor.test.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 +4 -4
- package/dist/types/src/components/Form/Defaults.d.ts.map +1 -1
- package/dist/types/src/components/Form/Form.d.ts +6 -4
- package/dist/types/src/components/Form/Form.d.ts.map +1 -1
- package/dist/types/src/components/Form/Form.stories.d.ts +38 -36
- package/dist/types/src/components/Form/Form.stories.d.ts.map +1 -1
- package/dist/types/src/components/Form/FormActions.d.ts +1 -1
- package/dist/types/src/components/Form/FormActions.d.ts.map +1 -1
- package/dist/types/src/components/Form/FormContent.d.ts +16 -12
- package/dist/types/src/components/Form/FormContent.d.ts.map +1 -1
- package/dist/types/src/components/Form/FormContext.d.ts +2 -2
- package/dist/types/src/components/Form/FormContext.d.ts.map +1 -1
- package/dist/types/src/components/Form/Input.d.ts.map +1 -1
- package/dist/types/src/components/Form/RefField.d.ts +12 -0
- package/dist/types/src/components/Form/RefField.d.ts.map +1 -0
- package/dist/types/src/components/Form/custom/GeoPointInput.d.ts.map +1 -1
- package/dist/types/src/components/Form/custom/SelectOptionsInput.d.ts.map +1 -1
- package/dist/types/src/components/Form/custom/SelectOptionsInput.stories.d.ts.map +1 -1
- package/dist/types/src/components/Form/custom/TupleInput.d.ts.map +1 -1
- package/dist/types/src/components/Form/factory.d.ts +1 -1
- package/dist/types/src/components/Form/factory.d.ts.map +1 -1
- package/dist/types/src/components/Form/index.d.ts.map +1 -1
- package/dist/types/src/components/ViewEditor/Test.stories.d.ts.map +1 -1
- package/dist/types/src/components/ViewEditor/ViewEditor.d.ts +2 -2
- package/dist/types/src/components/ViewEditor/ViewEditor.d.ts.map +1 -1
- package/dist/types/src/components/ViewEditor/ViewEditor.stories.d.ts +5 -3
- 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 +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/translations.d.ts +5 -0
- package/dist/types/src/translations.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 +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/src/vitest-setup.d.ts +2 -0
- package/dist/types/src/vitest-setup.d.ts.map +1 -0
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/package.json +28 -25
- package/src/components/ControlSection/ControlSection.stories.tsx +29 -0
- package/src/components/{ControlSection.tsx → ControlSection/ControlSection.tsx} +40 -16
- package/src/components/ControlSection/index.ts +5 -0
- package/src/components/FieldEditor/FieldEditor.stories.tsx +1 -1
- package/src/components/FieldEditor/FieldEditor.test.tsx +49 -0
- package/src/components/FieldEditor/FieldEditor.tsx +3 -2
- package/src/components/Form/ArrayField.tsx +63 -50
- package/src/components/Form/Defaults.tsx +77 -48
- package/src/components/Form/Form.stories.tsx +134 -109
- package/src/components/Form/Form.tsx +8 -5
- package/src/components/Form/FormActions.tsx +15 -18
- package/src/components/Form/FormContent.tsx +116 -46
- package/src/components/Form/FormContext.tsx +24 -10
- package/src/components/Form/Input.tsx +4 -10
- package/src/components/Form/RefField.tsx +217 -0
- package/src/components/Form/custom/GeoPointInput.tsx +2 -2
- package/src/components/Form/custom/SelectOptionsInput.stories.tsx +6 -5
- package/src/components/Form/custom/SelectOptionsInput.tsx +2 -2
- package/src/components/Form/factory.tsx +1 -1
- package/src/components/Form/index.ts +0 -1
- package/src/components/ViewEditor/ViewEditor.stories.tsx +19 -9
- package/src/components/ViewEditor/ViewEditor.tsx +65 -35
- 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/components/testing.tsx +5 -9
- package/src/hooks/index.ts +3 -1
- package/src/hooks/useForm.ts +4 -3
- package/src/hooks/useQueryRefOptions.ts +53 -0
- package/src/hooks/useRefQueryLookupHandler.ts +39 -0
- package/src/translations.ts +9 -2
- package/src/util/formatting.ts +15 -3
- package/src/util/index.ts +3 -1
- package/src/util/refs.ts +38 -0
- package/src/util/schema.ts +14 -0
- package/src/vitest-setup.ts +11 -0
- package/dist/types/src/components/ControlSection.d.ts.map +0 -1
|
@@ -5,20 +5,23 @@
|
|
|
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';
|
|
14
15
|
import { ViewProjection, ViewType, createView } from '@dxos/schema';
|
|
15
16
|
import { withTheme, withLayout } from '@dxos/storybook-utils';
|
|
16
17
|
|
|
17
|
-
import { ViewEditor } from './ViewEditor';
|
|
18
|
+
import { ViewEditor, type ViewEditorProps } from './ViewEditor';
|
|
18
19
|
import translations from '../../translations';
|
|
19
20
|
import { TestLayout, TestPanel } from '../testing';
|
|
20
21
|
|
|
21
|
-
|
|
22
|
+
type StoryProps = Pick<ViewEditorProps, 'readonly'>;
|
|
23
|
+
|
|
24
|
+
const DefaultStory = (props: StoryProps) => {
|
|
22
25
|
const space = useSpace();
|
|
23
26
|
const [schema, setSchema] = useState<EchoSchema>();
|
|
24
27
|
const [view, setView] = useState<ViewType>();
|
|
@@ -26,7 +29,7 @@ const DefaultStory = () => {
|
|
|
26
29
|
useAsyncEffect(async () => {
|
|
27
30
|
if (space) {
|
|
28
31
|
class TestSchema extends TypedObject({ typename: 'example.com/type/Test', version: '0.1.0' })({
|
|
29
|
-
name:
|
|
32
|
+
name: Schema.String,
|
|
30
33
|
email: Format.Email,
|
|
31
34
|
salary: Format.Currency(),
|
|
32
35
|
}) {}
|
|
@@ -41,7 +44,7 @@ const DefaultStory = () => {
|
|
|
41
44
|
}
|
|
42
45
|
}, [space]);
|
|
43
46
|
|
|
44
|
-
const views = useQuery(space, Filter.
|
|
47
|
+
const views = useQuery(space, Filter.type(ViewType));
|
|
45
48
|
const currentTypename = useMemo(() => view?.query?.typename, [view]);
|
|
46
49
|
const updateViewTypename = useCallback(
|
|
47
50
|
(newTypename: string) => {
|
|
@@ -56,6 +59,7 @@ const DefaultStory = () => {
|
|
|
56
59
|
},
|
|
57
60
|
[views, schema],
|
|
58
61
|
);
|
|
62
|
+
|
|
59
63
|
const handleDelete = useCallback((property: string) => projection?.deleteFieldProjection(property), [projection]);
|
|
60
64
|
|
|
61
65
|
if (!schema || !view || !projection) {
|
|
@@ -69,6 +73,7 @@ const DefaultStory = () => {
|
|
|
69
73
|
schema={schema}
|
|
70
74
|
view={view}
|
|
71
75
|
registry={space?.db.schemaRegistry}
|
|
76
|
+
readonly={props.readonly}
|
|
72
77
|
onTypenameChanged={updateViewTypename}
|
|
73
78
|
onDelete={handleDelete}
|
|
74
79
|
/>
|
|
@@ -77,11 +82,10 @@ const DefaultStory = () => {
|
|
|
77
82
|
);
|
|
78
83
|
};
|
|
79
84
|
|
|
80
|
-
const meta: Meta<
|
|
85
|
+
const meta: Meta<StoryProps> = {
|
|
81
86
|
title: 'ui/react-ui-form/ViewEditor',
|
|
82
|
-
component: ViewEditor,
|
|
83
87
|
render: DefaultStory,
|
|
84
|
-
decorators: [withClientProvider({ createSpace: true }), withLayout({ fullscreen: true
|
|
88
|
+
decorators: [withClientProvider({ createSpace: true }), withLayout({ fullscreen: true }), withTheme],
|
|
85
89
|
parameters: {
|
|
86
90
|
translations,
|
|
87
91
|
},
|
|
@@ -89,6 +93,12 @@ const meta: Meta<typeof ViewEditor> = {
|
|
|
89
93
|
|
|
90
94
|
export default meta;
|
|
91
95
|
|
|
92
|
-
type Story = StoryObj
|
|
96
|
+
type Story = StoryObj<StoryProps>;
|
|
93
97
|
|
|
94
98
|
export const Default: Story = {};
|
|
99
|
+
|
|
100
|
+
export const Readonly: Story = {
|
|
101
|
+
args: {
|
|
102
|
+
readonly: true,
|
|
103
|
+
},
|
|
104
|
+
};
|
|
@@ -2,12 +2,13 @@
|
|
|
2
2
|
// Copyright 2024 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
+
import { Schema } 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 { EchoSchema, Format, type JsonProp, isMutable, toJsonSchema } from '@dxos/echo-schema';
|
|
9
10
|
import { invariant } from '@dxos/invariant';
|
|
10
|
-
import { IconButton, type ThemedClassName, useTranslation } from '@dxos/react-ui';
|
|
11
|
+
import { Icon, IconButton, Message, type ThemedClassName, useTranslation } from '@dxos/react-ui';
|
|
11
12
|
import { List } from '@dxos/react-ui-list';
|
|
12
13
|
import { ghostHover, inputTextLabel, mx } from '@dxos/react-ui-theme';
|
|
13
14
|
import { FieldSchema, type FieldType, type ViewType, ViewProjection, VIEW_FIELD_LIMIT } from '@dxos/schema';
|
|
@@ -18,19 +19,19 @@ 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
|
+
title: 'View',
|
|
24
25
|
}),
|
|
25
26
|
typename: Format.URL.annotations({
|
|
26
|
-
|
|
27
|
+
title: '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
|
-
schema:
|
|
34
|
+
schema: Schema.Schema.AnyNoContext;
|
|
34
35
|
view: ViewType;
|
|
35
36
|
registry?: SchemaRegistry;
|
|
36
37
|
readonly?: boolean;
|
|
@@ -53,8 +54,13 @@ export const ViewEditor = ({
|
|
|
53
54
|
onDelete,
|
|
54
55
|
}: ViewEditorProps) => {
|
|
55
56
|
const { t } = useTranslation(translationKey);
|
|
56
|
-
const projection = useMemo(() =>
|
|
57
|
+
const projection = useMemo(() => {
|
|
58
|
+
// Use reactive and mutable version of json schema when schema is mutable.
|
|
59
|
+
const jsonSchema = schema instanceof EchoSchema ? schema.jsonSchema : toJsonSchema(schema);
|
|
60
|
+
return new ViewProjection(jsonSchema, view);
|
|
61
|
+
}, [schema, view]);
|
|
57
62
|
const [field, setField] = useState<FieldType>();
|
|
63
|
+
const immutable = readonly || !isMutable(schema);
|
|
58
64
|
|
|
59
65
|
// TODO(burdon): Should be reactive.
|
|
60
66
|
const viewValues = useMemo(() => {
|
|
@@ -66,37 +72,43 @@ export const ViewEditor = ({
|
|
|
66
72
|
};
|
|
67
73
|
}, [view]);
|
|
68
74
|
|
|
69
|
-
const handleSelect = useCallback(
|
|
70
|
-
|
|
71
|
-
|
|
75
|
+
const handleSelect = useCallback(
|
|
76
|
+
(field: FieldType) => {
|
|
77
|
+
if (immutable) {
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
setField((f) => (f === field ? undefined : field));
|
|
81
|
+
},
|
|
82
|
+
[immutable],
|
|
83
|
+
);
|
|
72
84
|
|
|
73
85
|
// TODO(burdon): Check if mutable; variant of useCallback that return undefined if readonly?
|
|
74
86
|
|
|
75
87
|
const handleAdd = useCallback(() => {
|
|
76
|
-
invariant(!
|
|
88
|
+
invariant(!immutable);
|
|
77
89
|
const field = projection.createFieldProjection();
|
|
78
90
|
setField(field);
|
|
79
91
|
}, [schema, view]);
|
|
80
92
|
|
|
81
93
|
const handleUpdate = useCallback(
|
|
82
94
|
({ name, typename }: ViewMetaType) => {
|
|
83
|
-
invariant(!
|
|
95
|
+
invariant(!immutable);
|
|
84
96
|
requestAnimationFrame(() => {
|
|
85
97
|
if (view.name !== name) {
|
|
86
98
|
view.name = name;
|
|
87
99
|
}
|
|
88
100
|
|
|
89
|
-
if (view.query.typename !== typename) {
|
|
101
|
+
if (view.query.typename !== typename && !immutable) {
|
|
90
102
|
onTypenameChanged?.(typename);
|
|
91
103
|
}
|
|
92
104
|
});
|
|
93
105
|
},
|
|
94
|
-
[schema, view, onTypenameChanged],
|
|
106
|
+
[schema, view, onTypenameChanged, immutable],
|
|
95
107
|
);
|
|
96
108
|
|
|
97
109
|
const handleDelete = useCallback(
|
|
98
110
|
(fieldId: string) => {
|
|
99
|
-
invariant(!
|
|
111
|
+
invariant(!immutable);
|
|
100
112
|
if (fieldId === field?.id) {
|
|
101
113
|
setField(undefined);
|
|
102
114
|
}
|
|
@@ -108,7 +120,7 @@ export const ViewEditor = ({
|
|
|
108
120
|
|
|
109
121
|
const handleMove = useCallback(
|
|
110
122
|
(fromIndex: number, toIndex: number) => {
|
|
111
|
-
invariant(!
|
|
123
|
+
invariant(!immutable);
|
|
112
124
|
// NOTE(ZaymonFC): Using arrayMove here causes a race condition with the kanban model.
|
|
113
125
|
const fields = [...view.fields];
|
|
114
126
|
const [moved] = fields.splice(fromIndex, 1);
|
|
@@ -139,15 +151,28 @@ export const ViewEditor = ({
|
|
|
139
151
|
);
|
|
140
152
|
|
|
141
153
|
return (
|
|
142
|
-
<div role='none' className={mx('
|
|
143
|
-
<
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
154
|
+
<div role='none' className={mx('overflow-y-auto', classNames)}>
|
|
155
|
+
<div role='none' className='p-2'>
|
|
156
|
+
<div role='none' className='mbe-2'>
|
|
157
|
+
{immutable && (
|
|
158
|
+
<Message.Root valence='neutral' className='rounded'>
|
|
159
|
+
<Message.Title>
|
|
160
|
+
<Icon icon='ph--info--regular' size={5} classNames='inline' /> {t('system schema title')}
|
|
161
|
+
</Message.Title>
|
|
162
|
+
<Message.Body>{t('system schema description')}</Message.Body>
|
|
163
|
+
</Message.Root>
|
|
164
|
+
)}
|
|
165
|
+
</div>
|
|
166
|
+
<Form<ViewMetaType>
|
|
167
|
+
autoSave
|
|
168
|
+
schema={ViewMetaSchema}
|
|
169
|
+
values={viewValues}
|
|
170
|
+
onSave={handleUpdate}
|
|
171
|
+
classNames='min-bs-0 overflow-y-auto'
|
|
172
|
+
/>
|
|
173
|
+
</div>
|
|
174
|
+
|
|
175
|
+
<div role='none' className='min-bs-0 overflow-y-auto'>
|
|
151
176
|
{/* TODO(burdon): Clean up common form ux. */}
|
|
152
177
|
<div role='none' className='p-2'>
|
|
153
178
|
<label className={mx(inputTextLabel)}>{t('fields label')}</label>
|
|
@@ -155,9 +180,9 @@ export const ViewEditor = ({
|
|
|
155
180
|
|
|
156
181
|
<List.Root<FieldType>
|
|
157
182
|
items={view.fields}
|
|
158
|
-
isItem={
|
|
183
|
+
isItem={Schema.is(FieldSchema)}
|
|
159
184
|
getId={(field) => field.id}
|
|
160
|
-
onMove={
|
|
185
|
+
onMove={immutable ? undefined : handleMove}
|
|
161
186
|
>
|
|
162
187
|
{({ items: fields }) => (
|
|
163
188
|
<>
|
|
@@ -170,7 +195,11 @@ export const ViewEditor = ({
|
|
|
170
195
|
|
|
171
196
|
<div role='list' className='flex flex-col w-full'>
|
|
172
197
|
{fields?.map((field) => (
|
|
173
|
-
<List.Item<FieldType>
|
|
198
|
+
<List.Item<FieldType>
|
|
199
|
+
key={field.id}
|
|
200
|
+
item={field}
|
|
201
|
+
classNames={mx(grid, ghostHover, !immutable && 'cursor-pointer')}
|
|
202
|
+
>
|
|
174
203
|
<List.ItemDragHandle />
|
|
175
204
|
<List.ItemTitle onClick={() => handleSelect(field)}>{field.path}</List.ItemTitle>
|
|
176
205
|
<div className='flex items-center gap-2 -ml-4'>
|
|
@@ -179,7 +208,7 @@ export const ViewEditor = ({
|
|
|
179
208
|
disabled={view.fields.length <= 1}
|
|
180
209
|
onClick={() => handleHide(field.id)}
|
|
181
210
|
/>
|
|
182
|
-
{!
|
|
211
|
+
{!immutable && (
|
|
183
212
|
<List.ItemDeleteButton
|
|
184
213
|
icon='ph--trash--regular'
|
|
185
214
|
disabled={view.fields.length <= 1}
|
|
@@ -211,7 +240,7 @@ export const ViewEditor = ({
|
|
|
211
240
|
<List.Item<string>
|
|
212
241
|
key={property}
|
|
213
242
|
item={property}
|
|
214
|
-
classNames={mx(grid, ghostHover, 'cursor-pointer')}
|
|
243
|
+
classNames={mx(grid, ghostHover, !immutable && 'cursor-pointer')}
|
|
215
244
|
>
|
|
216
245
|
<div />
|
|
217
246
|
<List.ItemTitle>{property}</List.ItemTitle>
|
|
@@ -237,13 +266,14 @@ export const ViewEditor = ({
|
|
|
237
266
|
)}
|
|
238
267
|
|
|
239
268
|
{!readonly && !field && (
|
|
240
|
-
<div className='
|
|
269
|
+
<div role='none' className='p-2'>
|
|
241
270
|
<IconButton
|
|
242
271
|
icon='ph--plus--regular'
|
|
243
272
|
label={t('button add property')}
|
|
244
|
-
onClick={
|
|
273
|
+
onClick={immutable ? undefined : handleAdd}
|
|
245
274
|
// TODO(burdon): Show field limit in ux (not tooltip).
|
|
246
275
|
disabled={view.fields.length >= VIEW_FIELD_LIMIT}
|
|
276
|
+
classNames='flex is-full'
|
|
247
277
|
/>
|
|
248
278
|
</div>
|
|
249
279
|
)}
|
|
@@ -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
|
@@ -5,20 +5,16 @@
|
|
|
5
5
|
import React, { type PropsWithChildren } from 'react';
|
|
6
6
|
|
|
7
7
|
import { type ThemedClassName } from '@dxos/react-ui';
|
|
8
|
-
import {
|
|
8
|
+
import { JsonFilter } from '@dxos/react-ui-syntax-highlighter';
|
|
9
9
|
import { mx } from '@dxos/react-ui-theme';
|
|
10
10
|
|
|
11
11
|
export const TestPanel = ({ classNames, children }: ThemedClassName<PropsWithChildren>) => (
|
|
12
|
-
<div className='
|
|
13
|
-
<div className={mx('flex flex-col border border-separator rounded w-[300px]', classNames)}>{children}</div>
|
|
14
|
-
</div>
|
|
12
|
+
<div className={mx('w-full max-w-[30rem] border border-separator p-2 rounded', classNames)}>{children}</div>
|
|
15
13
|
);
|
|
16
14
|
|
|
17
15
|
export const TestLayout = ({ classNames, json, children }: ThemedClassName<PropsWithChildren<{ json?: any }>>) => (
|
|
18
|
-
<div className='w-full h-full grid grid-cols-
|
|
19
|
-
<div className={mx('
|
|
20
|
-
<
|
|
21
|
-
{JSON.stringify(json, null, 2)}
|
|
22
|
-
</SyntaxHighlighter>
|
|
16
|
+
<div className='w-full h-full grid grid-cols-[1fr_1fr]'>
|
|
17
|
+
<div className={mx('m-2 justify-center overflow-y-auto', classNames)}>{children}</div>
|
|
18
|
+
<JsonFilter data={json} classNames='w-full text-xs' testId='debug' />
|
|
23
19
|
</div>
|
|
24
20
|
);
|
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,9 +90,10 @@ 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
97
|
useEffect(() => {
|
|
97
98
|
setValues(initialValues);
|
|
98
99
|
}, [initialValues]);
|
|
@@ -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,39 @@
|
|
|
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
|
+
return 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
|
+
};
|
package/src/translations.ts
CHANGED
|
@@ -10,6 +10,7 @@ export default [
|
|
|
10
10
|
[translationKey]: {
|
|
11
11
|
'fields label': 'Fields',
|
|
12
12
|
'hidden fields label': 'Hidden Fields',
|
|
13
|
+
'empty readonly ref field label': '(none)',
|
|
13
14
|
|
|
14
15
|
// TODO(burdon): Standardize field/property.
|
|
15
16
|
'button add property': 'Add property',
|
|
@@ -20,6 +21,8 @@ export default [
|
|
|
20
21
|
'add field': 'Add field',
|
|
21
22
|
'delete field': 'Delete field',
|
|
22
23
|
'field limit reached': 'Maximum number of fields reached',
|
|
24
|
+
'boolean input true value': 'Yes',
|
|
25
|
+
'boolean input false value': 'No',
|
|
23
26
|
|
|
24
27
|
// TODO(burdon): Factor out?
|
|
25
28
|
'button cancel': 'Cancel',
|
|
@@ -52,16 +55,20 @@ export default [
|
|
|
52
55
|
'format uri': 'URI',
|
|
53
56
|
'format uuid': 'UUID',
|
|
54
57
|
|
|
55
|
-
// Placeholders
|
|
58
|
+
// Placeholders.
|
|
56
59
|
'placeholder latitude': 'Latitude',
|
|
57
60
|
'placeholder longitude': 'Longitude',
|
|
58
61
|
|
|
59
|
-
// SelectOptionsInput
|
|
62
|
+
// SelectOptionsInput.
|
|
60
63
|
'select option label': 'Label',
|
|
61
64
|
'select option label placeholder': 'Option label',
|
|
62
65
|
'select option color': 'Color',
|
|
63
66
|
'select option delete': 'Delete',
|
|
64
67
|
'select option add': 'Add option',
|
|
68
|
+
|
|
69
|
+
// System schema message.
|
|
70
|
+
'system schema title': 'System Schema',
|
|
71
|
+
'system schema description': 'This schema is managed by the system and cannot be modified.',
|
|
65
72
|
},
|
|
66
73
|
},
|
|
67
74
|
},
|
package/src/util/formatting.ts
CHANGED
|
@@ -64,13 +64,25 @@ export const formatForDisplay = ({ type, format, value, locale = undefined }: Va
|
|
|
64
64
|
});
|
|
65
65
|
}
|
|
66
66
|
case FormatEnum.Date: {
|
|
67
|
-
|
|
67
|
+
try {
|
|
68
|
+
return formatDate(new Date(value as number), 'yyyy-MM-dd');
|
|
69
|
+
} catch (error) {
|
|
70
|
+
return 'Invalid Date';
|
|
71
|
+
}
|
|
68
72
|
}
|
|
69
73
|
case FormatEnum.Time: {
|
|
70
|
-
|
|
74
|
+
try {
|
|
75
|
+
return formatDate(new Date(value as number), 'HH:mm:ss');
|
|
76
|
+
} catch (error) {
|
|
77
|
+
return 'Invalid Time';
|
|
78
|
+
}
|
|
71
79
|
}
|
|
72
80
|
case FormatEnum.DateTime: {
|
|
73
|
-
|
|
81
|
+
try {
|
|
82
|
+
return formatDate(new Date(value as number), 'yyyy-MM-dd HH:mm:ss');
|
|
83
|
+
} catch (error) {
|
|
84
|
+
return 'Invalid DateTime';
|
|
85
|
+
}
|
|
74
86
|
}
|
|
75
87
|
case FormatEnum.GeoPoint: {
|
|
76
88
|
if (value === null || value === undefined) {
|