@dxos/react-ui-form 0.8.2 → 0.8.3-main.7f5a14c
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 +19 -19
- package/dist/lib/browser/index.mjs.map +3 -3
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/node/index.cjs +18 -18
- package/dist/lib/node/index.cjs.map +3 -3
- package/dist/lib/node/meta.json +1 -1
- package/dist/lib/node-esm/index.mjs +19 -19
- package/dist/lib/node-esm/index.mjs.map +3 -3
- package/dist/lib/node-esm/meta.json +1 -1
- package/dist/types/src/components/FieldEditor/FieldEditor.stories.d.ts +6 -0
- package/dist/types/src/components/FieldEditor/FieldEditor.stories.d.ts.map +1 -1
- package/dist/types/src/components/Form/Form.d.ts +2 -1
- package/dist/types/src/components/Form/Form.d.ts.map +1 -1
- package/dist/types/src/components/Form/Form.stories.d.ts.map +1 -1
- package/dist/types/src/components/ViewEditor/ViewEditor.d.ts.map +1 -1
- package/dist/types/src/components/ViewEditor/ViewEditor.stories.d.ts +8 -0
- 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/package.json +27 -26
- package/src/components/FieldEditor/FieldEditor.stories.tsx +27 -4
- package/src/components/FieldEditor/FieldEditor.test.tsx +20 -13
- package/src/components/Form/Form.stories.tsx +3 -3
- package/src/components/Form/Form.tsx +4 -1
- package/src/components/Form/FormActions.tsx +1 -1
- package/src/components/Form/FormContent.tsx +2 -2
- package/src/components/ViewEditor/ViewEditor.stories.tsx +27 -8
- package/src/components/ViewEditor/ViewEditor.test.tsx +203 -0
- package/src/components/ViewEditor/ViewEditor.tsx +25 -22
- package/src/hooks/useRefQueryLookupHandler.ts +2 -2
|
@@ -169,7 +169,7 @@ export const FormField = ({
|
|
|
169
169
|
if (typeLiteral) {
|
|
170
170
|
return (
|
|
171
171
|
<>
|
|
172
|
-
{!inline && <h3 className='text-lg
|
|
172
|
+
{!inline && <h3 className={mx('text-lg mlb-card-spacing-block first:mbs-0')}>{label}</h3>}
|
|
173
173
|
<FormFields
|
|
174
174
|
schema={Schema.make(typeLiteral)}
|
|
175
175
|
path={path}
|
|
@@ -219,7 +219,7 @@ export const FormFields = ({
|
|
|
219
219
|
|
|
220
220
|
return (
|
|
221
221
|
// TODO(burdon): Consistent padding (with composite forms).
|
|
222
|
-
<div role='form' className={mx('
|
|
222
|
+
<div role='form' className={mx('is-full', classNames)}>
|
|
223
223
|
{properties
|
|
224
224
|
.map((property) => {
|
|
225
225
|
return (
|
|
@@ -6,7 +6,7 @@ import '@dxos-theme';
|
|
|
6
6
|
|
|
7
7
|
import { type Meta, type StoryObj } from '@storybook/react';
|
|
8
8
|
import { Schema } from 'effect';
|
|
9
|
-
import React, { useCallback, useMemo, useState } from 'react';
|
|
9
|
+
import React, { useCallback, useMemo, useState, useEffect } from 'react';
|
|
10
10
|
|
|
11
11
|
import { Format, type EchoSchema, toJsonSchema, TypedObject } from '@dxos/echo-schema';
|
|
12
12
|
import { Filter, useQuery, useSpace } from '@dxos/react-client/echo';
|
|
@@ -19,6 +19,16 @@ import { ViewEditor, type ViewEditorProps } from './ViewEditor';
|
|
|
19
19
|
import translations from '../../translations';
|
|
20
20
|
import { TestLayout, TestPanel } from '../testing';
|
|
21
21
|
|
|
22
|
+
// Symbol for accessing debug objects in tests.
|
|
23
|
+
export const VIEW_EDITOR_DEBUG_SYMBOL = Symbol('viewEditorDebug');
|
|
24
|
+
|
|
25
|
+
// Type definition for debug objects exposed to tests.
|
|
26
|
+
export type ViewEditorDebugObjects = {
|
|
27
|
+
schema: EchoSchema;
|
|
28
|
+
view: ViewType;
|
|
29
|
+
projection: ViewProjection;
|
|
30
|
+
};
|
|
31
|
+
|
|
22
32
|
type StoryProps = Pick<ViewEditorProps, 'readonly'>;
|
|
23
33
|
|
|
24
34
|
const DefaultStory = (props: StoryProps) => {
|
|
@@ -62,12 +72,25 @@ const DefaultStory = (props: StoryProps) => {
|
|
|
62
72
|
|
|
63
73
|
const handleDelete = useCallback((property: string) => projection?.deleteFieldProjection(property), [projection]);
|
|
64
74
|
|
|
75
|
+
// Expose objects on window for test access.
|
|
76
|
+
useEffect(() => {
|
|
77
|
+
if (typeof window !== 'undefined' && schema && view && projection) {
|
|
78
|
+
(window as any)[VIEW_EDITOR_DEBUG_SYMBOL] = { schema, view, projection } satisfies ViewEditorDebugObjects;
|
|
79
|
+
}
|
|
80
|
+
}, [schema, view, projection]);
|
|
81
|
+
|
|
82
|
+
// NOTE(ZaymonFC): This looks awkward but it resolves an infinite parsing issue with sb.
|
|
83
|
+
const json = useMemo(
|
|
84
|
+
() => JSON.parse(JSON.stringify({ schema, view, projection })),
|
|
85
|
+
[JSON.stringify(schema), JSON.stringify(view), JSON.stringify(projection)],
|
|
86
|
+
);
|
|
87
|
+
|
|
65
88
|
if (!schema || !view || !projection) {
|
|
66
89
|
return <div />;
|
|
67
90
|
}
|
|
68
91
|
|
|
69
92
|
return (
|
|
70
|
-
<TestLayout json={
|
|
93
|
+
<TestLayout json={json}>
|
|
71
94
|
<TestPanel>
|
|
72
95
|
<ViewEditor
|
|
73
96
|
schema={schema}
|
|
@@ -86,9 +109,7 @@ const meta: Meta<StoryProps> = {
|
|
|
86
109
|
title: 'ui/react-ui-form/ViewEditor',
|
|
87
110
|
render: DefaultStory,
|
|
88
111
|
decorators: [withClientProvider({ createSpace: true }), withLayout({ fullscreen: true }), withTheme],
|
|
89
|
-
parameters: {
|
|
90
|
-
translations,
|
|
91
|
-
},
|
|
112
|
+
parameters: { translations },
|
|
92
113
|
};
|
|
93
114
|
|
|
94
115
|
export default meta;
|
|
@@ -98,7 +119,5 @@ type Story = StoryObj<StoryProps>;
|
|
|
98
119
|
export const Default: Story = {};
|
|
99
120
|
|
|
100
121
|
export const Readonly: Story = {
|
|
101
|
-
args: {
|
|
102
|
-
readonly: true,
|
|
103
|
-
},
|
|
122
|
+
args: { readonly: true },
|
|
104
123
|
};
|
|
@@ -0,0 +1,203 @@
|
|
|
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 { EchoSchema } from '@dxos/echo-schema';
|
|
10
|
+
import { ViewProjection, ViewType } from '@dxos/schema';
|
|
11
|
+
|
|
12
|
+
import * as stories from './ViewEditor.stories';
|
|
13
|
+
import { VIEW_EDITOR_DEBUG_SYMBOL, type ViewEditorDebugObjects } from './ViewEditor.stories';
|
|
14
|
+
|
|
15
|
+
const { Default } = composeStories(stories);
|
|
16
|
+
|
|
17
|
+
const getViewEditorDebugObjects = (): ViewEditorDebugObjects => {
|
|
18
|
+
const debugObjects = (window as any)[VIEW_EDITOR_DEBUG_SYMBOL] as ViewEditorDebugObjects;
|
|
19
|
+
expect(debugObjects).toBeDefined();
|
|
20
|
+
expect(debugObjects.schema).toBeInstanceOf(EchoSchema);
|
|
21
|
+
expect(debugObjects.view).toBeInstanceOf(ViewType);
|
|
22
|
+
expect(debugObjects.projection).toBeInstanceOf(ViewProjection);
|
|
23
|
+
return debugObjects;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const waitForViewEditor = async () => {
|
|
27
|
+
await waitFor(
|
|
28
|
+
() => {
|
|
29
|
+
expect(screen.getByTestId('debug')).toBeInTheDocument();
|
|
30
|
+
},
|
|
31
|
+
{ timeout: 15000 },
|
|
32
|
+
);
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
describe('ViewEditor', () => {
|
|
36
|
+
afterEach(() => {
|
|
37
|
+
cleanup();
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test('renders view editor', async () => {
|
|
41
|
+
await Default.run();
|
|
42
|
+
await waitForViewEditor();
|
|
43
|
+
expect(screen.getByText('View')).toBeInTheDocument();
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test('change property name', async () => {
|
|
47
|
+
// Arrange.
|
|
48
|
+
await Default.run();
|
|
49
|
+
await waitForViewEditor();
|
|
50
|
+
|
|
51
|
+
// Act.
|
|
52
|
+
const nameField = screen.getByText('name');
|
|
53
|
+
fireEvent.click(nameField);
|
|
54
|
+
|
|
55
|
+
const fieldInput = screen.getByPlaceholderText('Field name.');
|
|
56
|
+
fireEvent.change(fieldInput, { target: { value: 'new_property' } });
|
|
57
|
+
|
|
58
|
+
fireEvent.click(screen.getByTestId('save-button'));
|
|
59
|
+
|
|
60
|
+
// Wait for the changes to propagate to both UI and schema
|
|
61
|
+
await waitFor(() => {
|
|
62
|
+
expect(screen.getByText('new_property')).toBeInTheDocument();
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
// Access the live objects directly from window using symbol.
|
|
66
|
+
const debugObjects = getViewEditorDebugObjects();
|
|
67
|
+
|
|
68
|
+
// Check schema contains new_property
|
|
69
|
+
const schemaProperties = debugObjects.schema.jsonSchema.properties;
|
|
70
|
+
expect(schemaProperties).toBeDefined();
|
|
71
|
+
expect(schemaProperties!.new_property).toBeDefined();
|
|
72
|
+
expect(schemaProperties!.name).toBeUndefined();
|
|
73
|
+
|
|
74
|
+
// Check view contains new_property field
|
|
75
|
+
const newPropertyField = debugObjects.view.fields.find((field: any) => field.path === 'new_property');
|
|
76
|
+
expect(newPropertyField).toBeDefined();
|
|
77
|
+
expect(newPropertyField!.path).toBe('new_property');
|
|
78
|
+
|
|
79
|
+
// Check projection contains new_property
|
|
80
|
+
const fieldProjections = debugObjects.projection.getFieldProjections();
|
|
81
|
+
const newPropertyProjection = fieldProjections.find((proj: any) => proj.field.path === 'new_property');
|
|
82
|
+
expect(newPropertyProjection).toBeDefined();
|
|
83
|
+
expect(newPropertyProjection!.props.property).toBe('new_property');
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test('add new property', async () => {
|
|
87
|
+
await Default.run();
|
|
88
|
+
await waitForViewEditor();
|
|
89
|
+
|
|
90
|
+
// Click the add property button
|
|
91
|
+
const addButton = screen.getByText('Add property');
|
|
92
|
+
fireEvent.click(addButton);
|
|
93
|
+
|
|
94
|
+
// Fill out the property field
|
|
95
|
+
const fieldInput = screen.getByPlaceholderText('Field name.');
|
|
96
|
+
fireEvent.change(fieldInput, { target: { value: 'added_property' } });
|
|
97
|
+
|
|
98
|
+
// Click the format combo box and select the first option
|
|
99
|
+
const formatCombo = screen.getByRole('combobox');
|
|
100
|
+
fireEvent.click(formatCombo);
|
|
101
|
+
|
|
102
|
+
const firstOption = screen.getAllByRole('option')[0];
|
|
103
|
+
fireEvent.click(firstOption);
|
|
104
|
+
|
|
105
|
+
// Save the changes
|
|
106
|
+
fireEvent.click(screen.getByTestId('save-button'));
|
|
107
|
+
|
|
108
|
+
// Wait for the changes to propagate
|
|
109
|
+
await waitFor(() => {
|
|
110
|
+
expect(screen.getByText('added_property')).toBeInTheDocument();
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
// Access the live objects directly from window using symbol.
|
|
114
|
+
const debugObjects = getViewEditorDebugObjects();
|
|
115
|
+
|
|
116
|
+
// Check schema contains added_property
|
|
117
|
+
const schemaProperties = debugObjects.schema.jsonSchema.properties;
|
|
118
|
+
expect(schemaProperties).toBeDefined();
|
|
119
|
+
expect(schemaProperties!.added_property).toBeDefined();
|
|
120
|
+
|
|
121
|
+
// Check view contains added_property field
|
|
122
|
+
const addedPropertyField = debugObjects.view.fields.find((field: any) => field.path === 'added_property');
|
|
123
|
+
expect(addedPropertyField).toBeDefined();
|
|
124
|
+
expect(addedPropertyField!.path).toBe('added_property');
|
|
125
|
+
|
|
126
|
+
// Check projection contains added_property
|
|
127
|
+
const fieldProjections = debugObjects.projection.getFieldProjections();
|
|
128
|
+
const addedPropertyProjection = fieldProjections.find((proj: any) => proj.field.path === 'added_property');
|
|
129
|
+
expect(addedPropertyProjection).toBeDefined();
|
|
130
|
+
expect(addedPropertyProjection!.props.property).toBe('added_property');
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
test('delete property', async () => {
|
|
134
|
+
await Default.run();
|
|
135
|
+
await waitForViewEditor();
|
|
136
|
+
|
|
137
|
+
// Find the delete button for the 'name' property
|
|
138
|
+
const nameProperty = screen.getByText('name');
|
|
139
|
+
const propertyRow = nameProperty.closest('[role="listitem"]');
|
|
140
|
+
const deleteButton = propertyRow?.querySelector('button:last-child');
|
|
141
|
+
|
|
142
|
+
if (!deleteButton) {
|
|
143
|
+
throw new Error('Delete button not found');
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Click the delete button
|
|
147
|
+
fireEvent.click(deleteButton);
|
|
148
|
+
|
|
149
|
+
// Wait for the changes to propagate
|
|
150
|
+
await waitFor(() => {
|
|
151
|
+
expect(screen.queryByText('name')).not.toBeInTheDocument();
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
// Access the live objects directly from window using symbol.
|
|
155
|
+
const debugObjects = getViewEditorDebugObjects();
|
|
156
|
+
|
|
157
|
+
// Check schema no longer contains the name property
|
|
158
|
+
const schemaProperties = debugObjects.schema.jsonSchema.properties;
|
|
159
|
+
expect(schemaProperties).toBeDefined();
|
|
160
|
+
expect(schemaProperties!.name).toBeUndefined();
|
|
161
|
+
|
|
162
|
+
// Check view no longer contains the name field
|
|
163
|
+
const nameField = debugObjects.view.fields.find((field: any) => field.path === 'name');
|
|
164
|
+
expect(nameField).toBeUndefined();
|
|
165
|
+
|
|
166
|
+
// Check projection no longer contains the name property
|
|
167
|
+
const fieldProjections = debugObjects.projection.getFieldProjections();
|
|
168
|
+
const nameProjection = fieldProjections.find((proj: any) => proj.field.path === 'name');
|
|
169
|
+
expect(nameProjection).toBeUndefined();
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
test('hide and show property', async () => {
|
|
173
|
+
await Default.run();
|
|
174
|
+
await waitForViewEditor();
|
|
175
|
+
|
|
176
|
+
// Click the first hide button
|
|
177
|
+
const hideButtons = screen.getAllByTestId('hide-field-button');
|
|
178
|
+
fireEvent.click(hideButtons[0]);
|
|
179
|
+
|
|
180
|
+
// Check that hiddenFields is non-empty
|
|
181
|
+
await waitFor(() => {
|
|
182
|
+
expect(screen.getByTestId('show-field-button')).toBeInTheDocument();
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
// Access the live objects directly from window using symbol.
|
|
186
|
+
const debugObjects = getViewEditorDebugObjects();
|
|
187
|
+
|
|
188
|
+
// Check that hiddenFields is non-empty
|
|
189
|
+
expect(debugObjects.view.hiddenFields).toBeDefined();
|
|
190
|
+
expect(debugObjects.view.hiddenFields!.length).toBeGreaterThan(0);
|
|
191
|
+
|
|
192
|
+
// Click the show button
|
|
193
|
+
fireEvent.click(screen.getByTestId('show-field-button'));
|
|
194
|
+
|
|
195
|
+
// Wait for the data to update and check that hiddenFields is empty
|
|
196
|
+
await waitFor(() => {
|
|
197
|
+
expect(debugObjects.view.hiddenFields!.length).toBe(0);
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
// Also verify that the field is back in the visible fields list
|
|
201
|
+
expect(debugObjects.view.fields.length).toBeGreaterThan(0);
|
|
202
|
+
});
|
|
203
|
+
});
|
|
@@ -152,29 +152,27 @@ export const ViewEditor = ({
|
|
|
152
152
|
|
|
153
153
|
return (
|
|
154
154
|
<div role='none' className={mx('overflow-y-auto', classNames)}>
|
|
155
|
-
|
|
156
|
-
<div role='none' className='mbe-
|
|
157
|
-
|
|
158
|
-
<Message.
|
|
159
|
-
<
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
</Message.Root>
|
|
164
|
-
)}
|
|
155
|
+
{immutable && (
|
|
156
|
+
<div role='none' className='mbe-card-spacing-block'>
|
|
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>
|
|
165
163
|
</div>
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
164
|
+
)}
|
|
165
|
+
<Form<ViewMetaType>
|
|
166
|
+
autoSave
|
|
167
|
+
schema={ViewMetaSchema}
|
|
168
|
+
values={viewValues}
|
|
169
|
+
onSave={handleUpdate}
|
|
170
|
+
classNames='min-bs-0 overflow-y-auto'
|
|
171
|
+
/>
|
|
174
172
|
|
|
175
173
|
<div role='none' className='min-bs-0 overflow-y-auto'>
|
|
176
174
|
{/* TODO(burdon): Clean up common form ux. */}
|
|
177
|
-
<div role='none' className='
|
|
175
|
+
<div role='none' className='pli-card-spacing-inline mlb-card-spacing-block'>
|
|
178
176
|
<label className={mx(inputTextLabel)}>{t('fields label')}</label>
|
|
179
177
|
</div>
|
|
180
178
|
|
|
@@ -207,6 +205,7 @@ export const ViewEditor = ({
|
|
|
207
205
|
icon='ph--eye-slash--regular'
|
|
208
206
|
disabled={view.fields.length <= 1}
|
|
209
207
|
onClick={() => handleHide(field.id)}
|
|
208
|
+
data-testid='hide-field-button'
|
|
210
209
|
/>
|
|
211
210
|
{!immutable && (
|
|
212
211
|
<List.ItemDeleteButton
|
|
@@ -225,7 +224,7 @@ export const ViewEditor = ({
|
|
|
225
224
|
|
|
226
225
|
{hiddenProperties.length > 0 && (
|
|
227
226
|
<div>
|
|
228
|
-
<div role='none' className='
|
|
227
|
+
<div role='none' className='pli-card-spacing-inline mlb-card-spacing-block'>
|
|
229
228
|
<label className={mx(inputTextLabel)}>{t('hidden fields label')}</label>
|
|
230
229
|
</div>
|
|
231
230
|
|
|
@@ -244,7 +243,11 @@ export const ViewEditor = ({
|
|
|
244
243
|
>
|
|
245
244
|
<div />
|
|
246
245
|
<List.ItemTitle>{property}</List.ItemTitle>
|
|
247
|
-
<List.ItemButton
|
|
246
|
+
<List.ItemButton
|
|
247
|
+
icon='ph--eye--regular'
|
|
248
|
+
onClick={() => handleShow(property)}
|
|
249
|
+
data-testid='show-field-button'
|
|
250
|
+
/>
|
|
248
251
|
</List.Item>
|
|
249
252
|
))}
|
|
250
253
|
</div>
|
|
@@ -266,7 +269,7 @@ export const ViewEditor = ({
|
|
|
266
269
|
)}
|
|
267
270
|
|
|
268
271
|
{!readonly && !field && (
|
|
269
|
-
<div role='none' className='
|
|
272
|
+
<div role='none' className='pli-card-spacing-chrome mlb-card-spacing-chrome'>
|
|
270
273
|
<IconButton
|
|
271
274
|
icon='ph--plus--regular'
|
|
272
275
|
label={t('button add property')}
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
import { useCallback } from 'react';
|
|
6
6
|
|
|
7
|
-
import {
|
|
7
|
+
import { getObjectDXN, type TypeAnnotation } from '@dxos/echo-schema';
|
|
8
8
|
import { Filter, type Space } from '@dxos/react-client/echo';
|
|
9
9
|
import { isNonNullable } from '@dxos/util';
|
|
10
10
|
|
|
@@ -23,7 +23,7 @@ export const useRefQueryLookupHandler = ({ space }: UseRefQueryLookupProps): Que
|
|
|
23
23
|
|
|
24
24
|
return objects
|
|
25
25
|
.map((object) => {
|
|
26
|
-
const dxn =
|
|
26
|
+
const dxn = getObjectDXN(object);
|
|
27
27
|
if (!dxn) {
|
|
28
28
|
return undefined;
|
|
29
29
|
}
|