@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.
Files changed (30) hide show
  1. package/dist/lib/browser/index.mjs +19 -19
  2. package/dist/lib/browser/index.mjs.map +3 -3
  3. package/dist/lib/browser/meta.json +1 -1
  4. package/dist/lib/node/index.cjs +18 -18
  5. package/dist/lib/node/index.cjs.map +3 -3
  6. package/dist/lib/node/meta.json +1 -1
  7. package/dist/lib/node-esm/index.mjs +19 -19
  8. package/dist/lib/node-esm/index.mjs.map +3 -3
  9. package/dist/lib/node-esm/meta.json +1 -1
  10. package/dist/types/src/components/FieldEditor/FieldEditor.stories.d.ts +6 -0
  11. package/dist/types/src/components/FieldEditor/FieldEditor.stories.d.ts.map +1 -1
  12. package/dist/types/src/components/Form/Form.d.ts +2 -1
  13. package/dist/types/src/components/Form/Form.d.ts.map +1 -1
  14. package/dist/types/src/components/Form/Form.stories.d.ts.map +1 -1
  15. package/dist/types/src/components/ViewEditor/ViewEditor.d.ts.map +1 -1
  16. package/dist/types/src/components/ViewEditor/ViewEditor.stories.d.ts +8 -0
  17. package/dist/types/src/components/ViewEditor/ViewEditor.stories.d.ts.map +1 -1
  18. package/dist/types/src/components/ViewEditor/ViewEditor.test.d.ts +2 -0
  19. package/dist/types/src/components/ViewEditor/ViewEditor.test.d.ts.map +1 -0
  20. package/package.json +27 -26
  21. package/src/components/FieldEditor/FieldEditor.stories.tsx +27 -4
  22. package/src/components/FieldEditor/FieldEditor.test.tsx +20 -13
  23. package/src/components/Form/Form.stories.tsx +3 -3
  24. package/src/components/Form/Form.tsx +4 -1
  25. package/src/components/Form/FormActions.tsx +1 -1
  26. package/src/components/Form/FormContent.tsx +2 -2
  27. package/src/components/ViewEditor/ViewEditor.stories.tsx +27 -8
  28. package/src/components/ViewEditor/ViewEditor.test.tsx +203 -0
  29. package/src/components/ViewEditor/ViewEditor.tsx +25 -22
  30. 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 mbs-2 mbe-1 first:mbs-0'>{label}</h3>}
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('p-2 is-full', classNames)}>
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={{ schema, view, projection }}>
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
- <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
- )}
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
- <Form<ViewMetaType>
167
- autoSave
168
- schema={ViewMetaSchema}
169
- values={viewValues}
170
- onSave={handleUpdate}
171
- classNames='min-bs-0 overflow-y-auto'
172
- />
173
- </div>
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='p-2'>
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='p-2'>
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 icon='ph--eye--regular' onClick={() => handleShow(property)} />
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='p-2'>
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 { getDXN, type TypeAnnotation } from '@dxos/echo-schema';
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 = getDXN(object);
26
+ const dxn = getObjectDXN(object);
27
27
  if (!dxn) {
28
28
  return undefined;
29
29
  }