@dxos/react-ui-form 0.8.2-staging.7ac8446 → 0.8.3-main.672df60
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 +1817 -1309
- package/dist/lib/browser/index.mjs.map +4 -4
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/node/index.cjs +1773 -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 +1817 -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/ViewEditor/ViewEditor.test.d.ts +2 -0
- package/dist/types/src/components/ViewEditor/ViewEditor.test.d.ts.map +1 -0
- 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.test.tsx +166 -0
- package/src/components/ViewEditor/ViewEditor.tsx +63 -30
- 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
|
+
};
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2025 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { composeStories } from '@storybook/react';
|
|
6
|
+
import { screen, cleanup, fireEvent, waitFor } from '@testing-library/react';
|
|
7
|
+
import { afterEach, describe, expect, test } from 'vitest';
|
|
8
|
+
|
|
9
|
+
import * as stories from './ViewEditor.stories';
|
|
10
|
+
|
|
11
|
+
const { Default } = composeStories(stories);
|
|
12
|
+
|
|
13
|
+
const waitForViewEditor = async () => {
|
|
14
|
+
await waitFor(() => {
|
|
15
|
+
expect(screen.getByTestId('debug')).toBeInTheDocument();
|
|
16
|
+
});
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
describe('ViewEditor', () => {
|
|
20
|
+
afterEach(() => {
|
|
21
|
+
cleanup();
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test('renders view editor', async () => {
|
|
25
|
+
await Default.run();
|
|
26
|
+
await waitForViewEditor();
|
|
27
|
+
expect(screen.getByText('View')).toBeInTheDocument();
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test('change property name', async () => {
|
|
31
|
+
// Arrange.
|
|
32
|
+
await Default.run();
|
|
33
|
+
await waitForViewEditor();
|
|
34
|
+
|
|
35
|
+
// Act.
|
|
36
|
+
const nameField = screen.getByText('name');
|
|
37
|
+
fireEvent.click(nameField);
|
|
38
|
+
|
|
39
|
+
const fieldInput = screen.getByPlaceholderText('Field name.');
|
|
40
|
+
fireEvent.change(fieldInput, { target: { value: 'new_property' } });
|
|
41
|
+
|
|
42
|
+
fireEvent.click(screen.getByTestId('save-button'));
|
|
43
|
+
|
|
44
|
+
// Assert.
|
|
45
|
+
expect(screen.getByText('new_property')).toBeInTheDocument();
|
|
46
|
+
|
|
47
|
+
const debugInfo = JSON.parse(screen.getByTestId('debug').textContent!);
|
|
48
|
+
|
|
49
|
+
// Check schema contains new_property.
|
|
50
|
+
const schemaProperties = debugInfo.schema._storedSchema.jsonSchema.properties;
|
|
51
|
+
expect(schemaProperties.new_property).toBeDefined();
|
|
52
|
+
|
|
53
|
+
// Check view contains new_property field.
|
|
54
|
+
const newPropertyField = debugInfo.view.fields.find((field: any) => field.path === 'new_property');
|
|
55
|
+
expect(newPropertyField).toBeDefined();
|
|
56
|
+
expect(newPropertyField.path).toBe('new_property');
|
|
57
|
+
|
|
58
|
+
// Check projection contains new_property.
|
|
59
|
+
const newPropertyProjection = debugInfo.projection._fieldProjections.find(
|
|
60
|
+
(proj: any) => proj.field.path === 'new_property',
|
|
61
|
+
);
|
|
62
|
+
expect(newPropertyProjection).toBeDefined();
|
|
63
|
+
expect(newPropertyProjection.props.property).toBe('new_property');
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test('add new property', async () => {
|
|
67
|
+
await Default.run();
|
|
68
|
+
await waitForViewEditor();
|
|
69
|
+
|
|
70
|
+
// Click the add property button
|
|
71
|
+
const addButton = screen.getByText('Add property');
|
|
72
|
+
fireEvent.click(addButton);
|
|
73
|
+
|
|
74
|
+
// Fill out the property field
|
|
75
|
+
const fieldInput = screen.getByPlaceholderText('Field name.');
|
|
76
|
+
fireEvent.change(fieldInput, { target: { value: 'added_property' } });
|
|
77
|
+
|
|
78
|
+
// Click the format combo box and select the first option
|
|
79
|
+
const formatCombo = screen.getByRole('combobox');
|
|
80
|
+
fireEvent.click(formatCombo);
|
|
81
|
+
|
|
82
|
+
const firstOption = screen.getAllByRole('option')[0];
|
|
83
|
+
fireEvent.click(firstOption);
|
|
84
|
+
|
|
85
|
+
// Save the changes
|
|
86
|
+
fireEvent.click(screen.getByTestId('save-button'));
|
|
87
|
+
|
|
88
|
+
// Assert the new property exists
|
|
89
|
+
expect(screen.getByText('added_property')).toBeInTheDocument();
|
|
90
|
+
|
|
91
|
+
const debugInfo = JSON.parse(screen.getByTestId('debug').textContent!);
|
|
92
|
+
|
|
93
|
+
// Check schema contains added_property
|
|
94
|
+
const schemaProperties = debugInfo.schema._storedSchema.jsonSchema.properties;
|
|
95
|
+
expect(schemaProperties.added_property).toBeDefined();
|
|
96
|
+
|
|
97
|
+
// Check view contains added_property field
|
|
98
|
+
const addedPropertyField = debugInfo.view.fields.find((field: any) => field.path === 'added_property');
|
|
99
|
+
expect(addedPropertyField).toBeDefined();
|
|
100
|
+
expect(addedPropertyField.path).toBe('added_property');
|
|
101
|
+
|
|
102
|
+
// Check projection contains added_property
|
|
103
|
+
const addedPropertyProjection = debugInfo.projection._fieldProjections.find(
|
|
104
|
+
(proj: any) => proj.field.path === 'added_property',
|
|
105
|
+
);
|
|
106
|
+
expect(addedPropertyProjection).toBeDefined();
|
|
107
|
+
expect(addedPropertyProjection.props.property).toBe('added_property');
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
test('delete property', async () => {
|
|
111
|
+
await Default.run();
|
|
112
|
+
await waitForViewEditor();
|
|
113
|
+
|
|
114
|
+
// Find the delete button for the 'name' property
|
|
115
|
+
const nameProperty = screen.getByText('name');
|
|
116
|
+
const propertyRow = nameProperty.closest('[role="listitem"]');
|
|
117
|
+
const deleteButton = propertyRow?.querySelector('button:last-child');
|
|
118
|
+
|
|
119
|
+
if (!deleteButton) {
|
|
120
|
+
throw new Error('Delete button not found');
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Click the delete button
|
|
124
|
+
fireEvent.click(deleteButton);
|
|
125
|
+
|
|
126
|
+
// Assert the property is no longer visible
|
|
127
|
+
expect(screen.queryByText('name')).not.toBeInTheDocument();
|
|
128
|
+
|
|
129
|
+
const debugInfo = JSON.parse(screen.getByTestId('debug').textContent!);
|
|
130
|
+
|
|
131
|
+
// Check schema no longer contains the name property
|
|
132
|
+
const schemaProperties = debugInfo.schema._storedSchema.jsonSchema.properties;
|
|
133
|
+
expect(schemaProperties.name).toBeUndefined();
|
|
134
|
+
|
|
135
|
+
// Check view no longer contains the name field
|
|
136
|
+
const nameField = debugInfo.view.fields.find((field: any) => field.path === 'name');
|
|
137
|
+
expect(nameField).toBeUndefined();
|
|
138
|
+
|
|
139
|
+
// Check projection no longer contains the name property
|
|
140
|
+
const nameProjection = debugInfo.projection._fieldProjections.find((proj: any) => proj.field.path === 'name');
|
|
141
|
+
expect(nameProjection).toBeUndefined();
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
test('hide and show property', async () => {
|
|
145
|
+
await Default.run();
|
|
146
|
+
await waitForViewEditor();
|
|
147
|
+
|
|
148
|
+
// Click the first hide button
|
|
149
|
+
const hideButtons = screen.getAllByTestId('hide-field-button');
|
|
150
|
+
fireEvent.click(hideButtons[0]);
|
|
151
|
+
|
|
152
|
+
// Check that hiddenFields is non-empty
|
|
153
|
+
let debugInfo = JSON.parse(screen.getByTestId('debug').textContent!);
|
|
154
|
+
expect(debugInfo.view.hiddenFields.length).toBeGreaterThan(0);
|
|
155
|
+
|
|
156
|
+
// Wait for the show button to appear and click it
|
|
157
|
+
await waitFor(() => {
|
|
158
|
+
expect(screen.getByTestId('show-field-button')).toBeInTheDocument();
|
|
159
|
+
});
|
|
160
|
+
fireEvent.click(screen.getByTestId('show-field-button'));
|
|
161
|
+
|
|
162
|
+
// Check that hiddenFields is empty again
|
|
163
|
+
debugInfo = JSON.parse(screen.getByTestId('debug').textContent!);
|
|
164
|
+
expect(debugInfo.view.hiddenFields.length).toBe(0);
|
|
165
|
+
});
|
|
166
|
+
});
|
|
@@ -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,26 @@ export const ViewEditor = ({
|
|
|
139
151
|
);
|
|
140
152
|
|
|
141
153
|
return (
|
|
142
|
-
<div role='none' className={mx('
|
|
154
|
+
<div role='none' className={mx('overflow-y-auto', classNames)}>
|
|
155
|
+
{immutable && (
|
|
156
|
+
<div role='none' className='mbe-2'>
|
|
157
|
+
<Message.Root valence='neutral' className='rounded'>
|
|
158
|
+
<Message.Title>
|
|
159
|
+
<Icon icon='ph--info--regular' size={5} classNames='inline' /> {t('system schema title')}
|
|
160
|
+
</Message.Title>
|
|
161
|
+
<Message.Body>{t('system schema description')}</Message.Body>
|
|
162
|
+
</Message.Root>
|
|
163
|
+
</div>
|
|
164
|
+
)}
|
|
143
165
|
<Form<ViewMetaType>
|
|
144
166
|
autoSave
|
|
145
167
|
schema={ViewMetaSchema}
|
|
146
168
|
values={viewValues}
|
|
147
|
-
onSave={
|
|
169
|
+
onSave={handleUpdate}
|
|
170
|
+
classNames='min-bs-0 overflow-y-auto'
|
|
148
171
|
/>
|
|
149
172
|
|
|
150
|
-
<div>
|
|
173
|
+
<div role='none' className='min-bs-0 overflow-y-auto'>
|
|
151
174
|
{/* TODO(burdon): Clean up common form ux. */}
|
|
152
175
|
<div role='none' className='p-2'>
|
|
153
176
|
<label className={mx(inputTextLabel)}>{t('fields label')}</label>
|
|
@@ -155,9 +178,9 @@ export const ViewEditor = ({
|
|
|
155
178
|
|
|
156
179
|
<List.Root<FieldType>
|
|
157
180
|
items={view.fields}
|
|
158
|
-
isItem={
|
|
181
|
+
isItem={Schema.is(FieldSchema)}
|
|
159
182
|
getId={(field) => field.id}
|
|
160
|
-
onMove={
|
|
183
|
+
onMove={immutable ? undefined : handleMove}
|
|
161
184
|
>
|
|
162
185
|
{({ items: fields }) => (
|
|
163
186
|
<>
|
|
@@ -170,7 +193,11 @@ export const ViewEditor = ({
|
|
|
170
193
|
|
|
171
194
|
<div role='list' className='flex flex-col w-full'>
|
|
172
195
|
{fields?.map((field) => (
|
|
173
|
-
<List.Item<FieldType>
|
|
196
|
+
<List.Item<FieldType>
|
|
197
|
+
key={field.id}
|
|
198
|
+
item={field}
|
|
199
|
+
classNames={mx(grid, ghostHover, !immutable && 'cursor-pointer')}
|
|
200
|
+
>
|
|
174
201
|
<List.ItemDragHandle />
|
|
175
202
|
<List.ItemTitle onClick={() => handleSelect(field)}>{field.path}</List.ItemTitle>
|
|
176
203
|
<div className='flex items-center gap-2 -ml-4'>
|
|
@@ -178,8 +205,9 @@ export const ViewEditor = ({
|
|
|
178
205
|
icon='ph--eye-slash--regular'
|
|
179
206
|
disabled={view.fields.length <= 1}
|
|
180
207
|
onClick={() => handleHide(field.id)}
|
|
208
|
+
data-testid='hide-field-button'
|
|
181
209
|
/>
|
|
182
|
-
{!
|
|
210
|
+
{!immutable && (
|
|
183
211
|
<List.ItemDeleteButton
|
|
184
212
|
icon='ph--trash--regular'
|
|
185
213
|
disabled={view.fields.length <= 1}
|
|
@@ -211,11 +239,15 @@ export const ViewEditor = ({
|
|
|
211
239
|
<List.Item<string>
|
|
212
240
|
key={property}
|
|
213
241
|
item={property}
|
|
214
|
-
classNames={mx(grid, ghostHover, 'cursor-pointer')}
|
|
242
|
+
classNames={mx(grid, ghostHover, !immutable && 'cursor-pointer')}
|
|
215
243
|
>
|
|
216
244
|
<div />
|
|
217
245
|
<List.ItemTitle>{property}</List.ItemTitle>
|
|
218
|
-
<List.ItemButton
|
|
246
|
+
<List.ItemButton
|
|
247
|
+
icon='ph--eye--regular'
|
|
248
|
+
onClick={() => handleShow(property)}
|
|
249
|
+
data-testid='show-field-button'
|
|
250
|
+
/>
|
|
219
251
|
</List.Item>
|
|
220
252
|
))}
|
|
221
253
|
</div>
|
|
@@ -237,13 +269,14 @@ export const ViewEditor = ({
|
|
|
237
269
|
)}
|
|
238
270
|
|
|
239
271
|
{!readonly && !field && (
|
|
240
|
-
<div className='
|
|
272
|
+
<div role='none' className='p-2'>
|
|
241
273
|
<IconButton
|
|
242
274
|
icon='ph--plus--regular'
|
|
243
275
|
label={t('button add property')}
|
|
244
|
-
onClick={
|
|
276
|
+
onClick={immutable ? undefined : handleAdd}
|
|
245
277
|
// TODO(burdon): Show field limit in ux (not tooltip).
|
|
246
278
|
disabled={view.fields.length >= VIEW_FIELD_LIMIT}
|
|
279
|
+
classNames='flex is-full'
|
|
247
280
|
/>
|
|
248
281
|
</div>
|
|
249
282
|
)}
|
|
@@ -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]);
|