@dxos/react-ui-form 0.8.3-main.672df60 → 0.8.3-staging.0fa589b

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 (63) hide show
  1. package/dist/lib/browser/index.mjs +190 -173
  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 +150 -133
  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 +190 -173
  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/ControlSection/ControlSection.d.ts +1 -1
  11. package/dist/types/src/components/ControlSection/ControlSection.d.ts.map +1 -1
  12. package/dist/types/src/components/ControlSection/ControlSection.stories.d.ts.map +1 -1
  13. package/dist/types/src/components/FieldEditor/FieldEditor.d.ts +3 -2
  14. package/dist/types/src/components/FieldEditor/FieldEditor.d.ts.map +1 -1
  15. package/dist/types/src/components/FieldEditor/FieldEditor.stories.d.ts +5 -0
  16. package/dist/types/src/components/FieldEditor/FieldEditor.stories.d.ts.map +1 -1
  17. package/dist/types/src/components/Form/Defaults.d.ts.map +1 -1
  18. package/dist/types/src/components/Form/Form.d.ts +6 -14
  19. package/dist/types/src/components/Form/Form.d.ts.map +1 -1
  20. package/dist/types/src/components/Form/Form.stories.d.ts +2 -2
  21. package/dist/types/src/components/Form/Form.stories.d.ts.map +1 -1
  22. package/dist/types/src/components/Form/FormActions.d.ts +3 -1
  23. package/dist/types/src/components/Form/FormActions.d.ts.map +1 -1
  24. package/dist/types/src/components/Form/FormContent.d.ts +30 -3
  25. package/dist/types/src/components/Form/FormContent.d.ts.map +1 -1
  26. package/dist/types/src/components/Form/Input.d.ts +6 -4
  27. package/dist/types/src/components/Form/Input.d.ts.map +1 -1
  28. package/dist/types/src/components/Form/RefField.d.ts.map +1 -1
  29. package/dist/types/src/components/Form/custom/GeoPointInput.d.ts.map +1 -1
  30. package/dist/types/src/components/Form/custom/SelectOptionsInput.d.ts.map +1 -1
  31. package/dist/types/src/components/Form/custom/SelectOptionsInput.stories.d.ts.map +1 -1
  32. package/dist/types/src/components/Form/custom/TupleInput.d.ts.map +1 -1
  33. package/dist/types/src/components/ViewEditor/ViewEditor.d.ts +3 -2
  34. package/dist/types/src/components/ViewEditor/ViewEditor.d.ts.map +1 -1
  35. package/dist/types/src/components/ViewEditor/ViewEditor.stories.d.ts +7 -0
  36. package/dist/types/src/components/ViewEditor/ViewEditor.stories.d.ts.map +1 -1
  37. package/dist/types/src/components/testing.d.ts +3 -1
  38. package/dist/types/src/components/testing.d.ts.map +1 -1
  39. package/dist/types/src/translations.d.ts +3 -0
  40. package/dist/types/src/translations.d.ts.map +1 -1
  41. package/package.json +28 -26
  42. package/src/components/ControlSection/ControlSection.stories.tsx +15 -3
  43. package/src/components/ControlSection/ControlSection.tsx +11 -9
  44. package/src/components/FieldEditor/FieldEditor.stories.tsx +25 -5
  45. package/src/components/FieldEditor/FieldEditor.test.tsx +21 -13
  46. package/src/components/FieldEditor/FieldEditor.tsx +11 -2
  47. package/src/components/Form/ArrayField.tsx +1 -1
  48. package/src/components/Form/Defaults.tsx +5 -25
  49. package/src/components/Form/Form.stories.tsx +5 -5
  50. package/src/components/Form/Form.tsx +44 -49
  51. package/src/components/Form/FormActions.tsx +17 -2
  52. package/src/components/Form/FormContent.tsx +46 -46
  53. package/src/components/Form/Input.tsx +13 -8
  54. package/src/components/Form/RefField.tsx +1 -5
  55. package/src/components/Form/custom/GeoPointInput.tsx +1 -3
  56. package/src/components/Form/custom/SelectOptionsInput.stories.tsx +7 -5
  57. package/src/components/Form/custom/SelectOptionsInput.tsx +1 -3
  58. package/src/components/Form/custom/TupleInput.tsx +1 -3
  59. package/src/components/ViewEditor/ViewEditor.stories.tsx +25 -9
  60. package/src/components/ViewEditor/ViewEditor.test.tsx +80 -42
  61. package/src/components/ViewEditor/ViewEditor.tsx +66 -66
  62. package/src/components/testing.tsx +12 -7
  63. package/src/translations.ts +4 -1
@@ -5,21 +5,36 @@
5
5
  import React from 'react';
6
6
 
7
7
  import { IconButton, useTranslation } from '@dxos/react-ui';
8
+ import { cardSpacing } from '@dxos/react-ui-stack';
9
+ import { mx } from '@dxos/react-ui-theme';
8
10
 
9
11
  import { useFormContext } from './FormContext';
10
12
  import { translationKey } from '../../translations';
11
13
 
14
+ export type FormOuterSpacing = boolean | 'blockStart-0' | 'scroll-fields';
15
+
12
16
  export type FormActionsProps = {
13
17
  readonly?: boolean;
14
18
  onCancel?: () => void;
19
+ outerSpacing?: FormOuterSpacing;
15
20
  };
16
21
 
17
- export const FormActions = ({ readonly, onCancel }: FormActionsProps) => {
22
+ export const FormActions = ({ readonly, onCancel, outerSpacing = true }: FormActionsProps) => {
18
23
  const { t } = useTranslation(translationKey);
19
24
  const { canSave, handleSave } = useFormContext();
20
25
 
21
26
  return (
22
- <div role='none' className='flex [&_button]:grow gap-1 pli-2 pbe-2'>
27
+ <div
28
+ role='none'
29
+ className={mx(
30
+ outerSpacing === 'scroll-fields'
31
+ ? 'pli-cardSpacingInline mbe-cardSpacingBlock'
32
+ : outerSpacing
33
+ ? cardSpacing
34
+ : 'mbs-cardSpacingBlock',
35
+ 'flex [&_button]:grow gap-1 first:mbs-0',
36
+ )}
37
+ >
23
38
  {onCancel && !readonly && (
24
39
  <IconButton data-testid='cancel-button' icon='ph--x--regular' label={t('button cancel')} onClick={onCancel} />
25
40
  )}
@@ -4,7 +4,7 @@
4
4
 
5
5
  import { Schema, SchemaAST, pipe } from 'effect';
6
6
  import { capitalize } from 'effect/String';
7
- import React, { useMemo } from 'react';
7
+ import React, { forwardRef, useMemo } from 'react';
8
8
 
9
9
  import { createJsonPath, findNode, getDiscriminatedType, isDiscriminatedUnion } from '@dxos/effect';
10
10
  import { type ThemedClassName } from '@dxos/react-ui';
@@ -42,8 +42,8 @@ export const FormField = ({
42
42
  lookupComponent,
43
43
  Custom,
44
44
  }: FormFieldProps) => {
45
- const inputProps = useInputProps(path);
46
45
  const { ast, name, type, format, title, description, options, examples, array } = property;
46
+ const inputProps = useInputProps(path);
47
47
 
48
48
  const label = useMemo(() => title ?? pipe(name, capitalize), [title, name]);
49
49
  const placeholder = useMemo(
@@ -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-inputSpacingBlock first:mbs-0')}>{label}</h3>}
173
173
  <FormFields
174
174
  schema={Schema.make(typeLiteral)}
175
175
  path={path}
@@ -187,54 +187,54 @@ export const FormField = ({
187
187
  };
188
188
 
189
189
  export type FormFieldsProps = ThemedClassName<{
190
- Custom?: Partial<Record<string, InputComponent>>;
190
+ testId?: string;
191
+ readonly?: boolean;
191
192
  schema: Schema.Schema.All;
193
+ /**
194
+ * Path to the current object from the root. Used with nested forms.
195
+ */
192
196
  path?: (string | number)[];
193
197
  filter?: (props: SchemaProperty<any>[]) => SchemaProperty<any>[];
194
198
  sort?: string[];
195
- readonly?: boolean;
196
199
  lookupComponent?: ComponentLookup;
200
+ /**
201
+ * Map of custom renderers for specific properties.
202
+ * Prefer lookupComponent for plugin specific input surfaces.
203
+ */
204
+ Custom?: Partial<Record<string, InputComponent>>;
197
205
  onQueryRefOptions?: QueryRefOptions;
198
206
  }>;
199
207
 
200
- export const FormFields = ({
201
- classNames,
202
- Custom,
203
- schema,
204
- path,
205
- filter,
206
- sort,
207
- readonly,
208
- lookupComponent,
209
- onQueryRefOptions,
210
- }: FormFieldsProps) => {
211
- const values = useFormValues(path);
212
-
213
- const properties = useMemo(() => {
214
- const props = getSchemaProperties(schema.ast, values);
215
- const filtered = filter ? filter(props) : props;
216
-
217
- return sort ? filtered.sort((a, b) => sort.indexOf(a.name) - sort.indexOf(b.name)) : filtered;
218
- }, [schema, values, filter, sort]);
219
-
220
- return (
221
- // TODO(burdon): Consistent padding (with composite forms).
222
- <div role='form' className={mx('p-2 is-full', classNames)}>
223
- {properties
224
- .map((property) => {
225
- return (
226
- <FormField
227
- key={property.name}
228
- property={property}
229
- path={[...(path ?? []), property.name]}
230
- readonly={readonly}
231
- onQueryRefOptions={onQueryRefOptions}
232
- lookupComponent={lookupComponent}
233
- Custom={Custom}
234
- />
235
- );
236
- })
237
- .filter(isNotFalsy)}
238
- </div>
239
- );
240
- };
208
+ export const FormFields = forwardRef<HTMLDivElement, FormFieldsProps>(
209
+ (
210
+ { classNames, testId, schema, path, filter, sort, readonly, lookupComponent, Custom, onQueryRefOptions },
211
+ forwardRef,
212
+ ) => {
213
+ const values = useFormValues(path);
214
+ const properties = useMemo(() => {
215
+ const props = getSchemaProperties(schema.ast, values);
216
+ const filtered = filter ? filter(props) : props;
217
+ return sort ? filtered.sort((a, b) => sort.indexOf(a.name) - sort.indexOf(b.name)) : filtered;
218
+ }, [schema, values, filter, sort]);
219
+
220
+ return (
221
+ <div role='form' className={mx('is-full', classNames)} ref={forwardRef}>
222
+ {properties
223
+ .map((property) => {
224
+ return (
225
+ <FormField
226
+ key={property.name}
227
+ property={property}
228
+ path={[...(path ?? []), property.name]}
229
+ readonly={readonly}
230
+ onQueryRefOptions={onQueryRefOptions}
231
+ lookupComponent={lookupComponent}
232
+ Custom={Custom}
233
+ />
234
+ );
235
+ })
236
+ .filter(isNotFalsy)}
237
+ </div>
238
+ );
239
+ },
240
+ );
@@ -2,12 +2,13 @@
2
2
  // Copyright 2024 DXOS.org
3
3
  //
4
4
 
5
- import React, { type FC, type PropsWithChildren } from 'react';
5
+ import React, { type FC } from 'react';
6
6
 
7
7
  import { type FormatEnum } from '@dxos/echo-schema';
8
8
  import { type SimpleType } from '@dxos/effect';
9
- import { Icon, Tooltip } from '@dxos/react-ui';
10
- import { errorText } from '@dxos/react-ui-theme';
9
+ import { Icon, Input, Tooltip } from '@dxos/react-ui';
10
+ import { labelSpacing } from '@dxos/react-ui-stack';
11
+ import { errorText, mx } from '@dxos/react-ui-theme';
11
12
 
12
13
  import { type FormInputStateProps } from './FormContext';
13
14
 
@@ -28,14 +29,18 @@ export type InputProps = {
28
29
  */
29
30
  export type InputComponent = FC<InputProps>;
30
31
 
31
- export type InputHeaderProps = PropsWithChildren<{
32
+ export type InputHeaderProps = {
32
33
  error?: string;
33
- }>;
34
+ label: string;
35
+ readonly?: boolean;
36
+ };
34
37
 
35
- export const InputHeader = ({ children, error }: InputHeaderProps) => {
38
+ export const InputHeader = ({ error, label, readonly }: InputHeaderProps) => {
39
+ const Label = readonly ? 'span' : Input.Label;
40
+ const labelProps = readonly ? {} : { classNames: '!mlb-0' };
36
41
  return (
37
- <div role='none' className='flex justify-between items-center mbs-2 mbe-1 first:mbs-0'>
38
- {children}
42
+ <div role='none' className={mx('flex justify-between items-center', labelSpacing)}>
43
+ <Label {...labelProps}>{label}</Label>
39
44
  {error && (
40
45
  <Tooltip.Trigger asChild content={error} side='bottom'>
41
46
  <Icon icon='ph--warning--regular' size={4} classNames={errorText} />
@@ -136,11 +136,7 @@ export const RefField = ({
136
136
  // NOTE(thure): I left both predicates in-place in case we decide to add variants which do render readonly but empty values.
137
137
  return disabled && items.length < 1 ? null : (
138
138
  <Input.Root validationValence={status}>
139
- {!inputOnly && (
140
- <InputHeader error={error}>
141
- <Input.Label>{label}</Input.Label>
142
- </InputHeader>
143
- )}
139
+ {!inputOnly && <InputHeader error={error} label={label} />}
144
140
  <div data-no-submit>
145
141
  {disabled ? (
146
142
  items.length < 1 ? (
@@ -46,9 +46,7 @@ export const GeoPointInput = ({ type, label, disabled, getStatus, getValue, onVa
46
46
 
47
47
  return (
48
48
  <Input.Root validationValence={status}>
49
- <InputHeader error={error}>
50
- <Input.Label>{label}</Input.Label>
51
- </InputHeader>
49
+ <InputHeader error={error} label={label} />
52
50
  <div role='none' className='grid grid-cols-2 gap-2'>
53
51
  <Input.TextInput
54
52
  type='text'
@@ -47,13 +47,15 @@ const DefaultStory = () => {
47
47
  const meta: Meta = {
48
48
  title: 'ui/react-ui-form/SelectOptionsInput',
49
49
  component: SelectOptionInput,
50
- decorators: [withTheme, withLayout()],
51
- parameters: { translations },
50
+ render: DefaultStory,
51
+ decorators: [withTheme, withLayout({ fullscreen: true })],
52
+ parameters: {
53
+ translations,
54
+ },
52
55
  };
53
56
 
54
57
  export default meta;
58
+
55
59
  type Story = StoryObj;
56
60
 
57
- export const Default: Story = {
58
- render: DefaultStory,
59
- };
61
+ export const Default: Story = {};
@@ -112,9 +112,7 @@ export const SelectOptionInput = ({ type, label, disabled, getStatus, getValue,
112
112
 
113
113
  return (
114
114
  <Input.Root validationValence={status}>
115
- <InputHeader error={error}>
116
- <Input.Label>{label}</Input.Label>
117
- </InputHeader>
115
+ <InputHeader error={error} label={label} />
118
116
  <div role='none'>
119
117
  {options && (
120
118
  <List.Root items={options} isItem={(item) => true} onMove={handleMove}>
@@ -29,9 +29,7 @@ export const TupleInput = ({
29
29
 
30
30
  return (
31
31
  <Input.Root validationValence={status}>
32
- <InputHeader error={error}>
33
- <Input.Label>{label}</Input.Label>
34
- </InputHeader>
32
+ <InputHeader error={error} label={label} />
35
33
  <div className={mx('grid gap-2', gridCols[binding.length - 1])}>
36
34
  {binding.map((prop) => (
37
35
  <Input.TextInput
@@ -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';
@@ -17,7 +17,14 @@ import { withTheme, withLayout } from '@dxos/storybook-utils';
17
17
 
18
18
  import { ViewEditor, type ViewEditorProps } from './ViewEditor';
19
19
  import translations from '../../translations';
20
- import { TestLayout, TestPanel } from '../testing';
20
+ import { TestLayout, TestPanel, VIEW_EDITOR_DEBUG_SYMBOL } from '../testing';
21
+
22
+ // Type definition for debug objects exposed to tests.
23
+ export type ViewEditorDebugObjects = {
24
+ schema: EchoSchema;
25
+ view: ViewType;
26
+ projection: ViewProjection;
27
+ };
21
28
 
22
29
  type StoryProps = Pick<ViewEditorProps, 'readonly'>;
23
30
 
@@ -62,12 +69,25 @@ const DefaultStory = (props: StoryProps) => {
62
69
 
63
70
  const handleDelete = useCallback((property: string) => projection?.deleteFieldProjection(property), [projection]);
64
71
 
72
+ // Expose objects on window for test access.
73
+ useEffect(() => {
74
+ if (typeof window !== 'undefined' && schema && view && projection) {
75
+ (window as any)[VIEW_EDITOR_DEBUG_SYMBOL] = { schema, view, projection } satisfies ViewEditorDebugObjects;
76
+ }
77
+ }, [schema, view, projection]);
78
+
79
+ // NOTE(ZaymonFC): This looks awkward but it resolves an infinite parsing issue with sb.
80
+ const json = useMemo(
81
+ () => JSON.parse(JSON.stringify({ schema, view, projection })),
82
+ [JSON.stringify(schema), JSON.stringify(view), JSON.stringify(projection)],
83
+ );
84
+
65
85
  if (!schema || !view || !projection) {
66
86
  return <div />;
67
87
  }
68
88
 
69
89
  return (
70
- <TestLayout json={{ schema, view, projection }}>
90
+ <TestLayout json={json}>
71
91
  <TestPanel>
72
92
  <ViewEditor
73
93
  schema={schema}
@@ -86,9 +106,7 @@ const meta: Meta<StoryProps> = {
86
106
  title: 'ui/react-ui-form/ViewEditor',
87
107
  render: DefaultStory,
88
108
  decorators: [withClientProvider({ createSpace: true }), withLayout({ fullscreen: true }), withTheme],
89
- parameters: {
90
- translations,
91
- },
109
+ parameters: { translations },
92
110
  };
93
111
 
94
112
  export default meta;
@@ -98,7 +116,5 @@ type Story = StoryObj<StoryProps>;
98
116
  export const Default: Story = {};
99
117
 
100
118
  export const Readonly: Story = {
101
- args: {
102
- readonly: true,
103
- },
119
+ args: { readonly: true },
104
120
  };
@@ -6,14 +6,31 @@ import { composeStories } from '@storybook/react';
6
6
  import { screen, cleanup, fireEvent, waitFor } from '@testing-library/react';
7
7
  import { afterEach, describe, expect, test } from 'vitest';
8
8
 
9
+ import { EchoSchema } from '@dxos/echo-schema';
10
+ import { ViewProjection, ViewType } from '@dxos/schema';
11
+
9
12
  import * as stories from './ViewEditor.stories';
13
+ import { type ViewEditorDebugObjects } from './ViewEditor.stories';
14
+ import { VIEW_EDITOR_DEBUG_SYMBOL } from '../testing';
10
15
 
11
16
  const { Default } = composeStories(stories);
12
17
 
18
+ const getViewEditorDebugObjects = (): ViewEditorDebugObjects => {
19
+ const debugObjects = (window as any)[VIEW_EDITOR_DEBUG_SYMBOL] as ViewEditorDebugObjects;
20
+ expect(debugObjects).toBeDefined();
21
+ expect(debugObjects.schema).toBeInstanceOf(EchoSchema);
22
+ expect(debugObjects.view).toBeInstanceOf(ViewType);
23
+ expect(debugObjects.projection).toBeInstanceOf(ViewProjection);
24
+ return debugObjects;
25
+ };
26
+
13
27
  const waitForViewEditor = async () => {
14
- await waitFor(() => {
15
- expect(screen.getByTestId('debug')).toBeInTheDocument();
16
- });
28
+ await waitFor(
29
+ () => {
30
+ expect(screen.getByTestId('debug')).toBeInTheDocument();
31
+ },
32
+ { timeout: 15000 },
33
+ );
17
34
  };
18
35
 
19
36
  describe('ViewEditor', () => {
@@ -41,26 +58,30 @@ describe('ViewEditor', () => {
41
58
 
42
59
  fireEvent.click(screen.getByTestId('save-button'));
43
60
 
44
- // Assert.
45
- expect(screen.getByText('new_property')).toBeInTheDocument();
61
+ // Wait for the changes to propagate to both UI and schema
62
+ await waitFor(() => {
63
+ expect(screen.getByText('new_property')).toBeInTheDocument();
64
+ });
46
65
 
47
- const debugInfo = JSON.parse(screen.getByTestId('debug').textContent!);
66
+ // Access the live objects directly from window using symbol.
67
+ const debugObjects = getViewEditorDebugObjects();
48
68
 
49
- // Check schema contains new_property.
50
- const schemaProperties = debugInfo.schema._storedSchema.jsonSchema.properties;
51
- expect(schemaProperties.new_property).toBeDefined();
69
+ // Check schema contains new_property
70
+ const schemaProperties = debugObjects.schema.jsonSchema.properties;
71
+ expect(schemaProperties).toBeDefined();
72
+ expect(schemaProperties!.new_property).toBeDefined();
73
+ expect(schemaProperties!.name).toBeUndefined();
52
74
 
53
- // Check view contains new_property field.
54
- const newPropertyField = debugInfo.view.fields.find((field: any) => field.path === 'new_property');
75
+ // Check view contains new_property field
76
+ const newPropertyField = debugObjects.view.fields.find((field: any) => field.path === 'new_property');
55
77
  expect(newPropertyField).toBeDefined();
56
- expect(newPropertyField.path).toBe('new_property');
78
+ expect(newPropertyField!.path).toBe('new_property');
57
79
 
58
- // Check projection contains new_property.
59
- const newPropertyProjection = debugInfo.projection._fieldProjections.find(
60
- (proj: any) => proj.field.path === 'new_property',
61
- );
80
+ // Check projection contains new_property
81
+ const fieldProjections = debugObjects.projection.getFieldProjections();
82
+ const newPropertyProjection = fieldProjections.find((proj: any) => proj.field.path === 'new_property');
62
83
  expect(newPropertyProjection).toBeDefined();
63
- expect(newPropertyProjection.props.property).toBe('new_property');
84
+ expect(newPropertyProjection!.props.property).toBe('new_property');
64
85
  });
65
86
 
66
87
  test('add new property', async () => {
@@ -85,26 +106,29 @@ describe('ViewEditor', () => {
85
106
  // Save the changes
86
107
  fireEvent.click(screen.getByTestId('save-button'));
87
108
 
88
- // Assert the new property exists
89
- expect(screen.getByText('added_property')).toBeInTheDocument();
109
+ // Wait for the changes to propagate
110
+ await waitFor(() => {
111
+ expect(screen.getByText('added_property')).toBeInTheDocument();
112
+ });
90
113
 
91
- const debugInfo = JSON.parse(screen.getByTestId('debug').textContent!);
114
+ // Access the live objects directly from window using symbol.
115
+ const debugObjects = getViewEditorDebugObjects();
92
116
 
93
117
  // Check schema contains added_property
94
- const schemaProperties = debugInfo.schema._storedSchema.jsonSchema.properties;
95
- expect(schemaProperties.added_property).toBeDefined();
118
+ const schemaProperties = debugObjects.schema.jsonSchema.properties;
119
+ expect(schemaProperties).toBeDefined();
120
+ expect(schemaProperties!.added_property).toBeDefined();
96
121
 
97
122
  // Check view contains added_property field
98
- const addedPropertyField = debugInfo.view.fields.find((field: any) => field.path === 'added_property');
123
+ const addedPropertyField = debugObjects.view.fields.find((field: any) => field.path === 'added_property');
99
124
  expect(addedPropertyField).toBeDefined();
100
- expect(addedPropertyField.path).toBe('added_property');
125
+ expect(addedPropertyField!.path).toBe('added_property');
101
126
 
102
127
  // Check projection contains added_property
103
- const addedPropertyProjection = debugInfo.projection._fieldProjections.find(
104
- (proj: any) => proj.field.path === 'added_property',
105
- );
128
+ const fieldProjections = debugObjects.projection.getFieldProjections();
129
+ const addedPropertyProjection = fieldProjections.find((proj: any) => proj.field.path === 'added_property');
106
130
  expect(addedPropertyProjection).toBeDefined();
107
- expect(addedPropertyProjection.props.property).toBe('added_property');
131
+ expect(addedPropertyProjection!.props.property).toBe('added_property');
108
132
  });
109
133
 
110
134
  test('delete property', async () => {
@@ -123,21 +147,26 @@ describe('ViewEditor', () => {
123
147
  // Click the delete button
124
148
  fireEvent.click(deleteButton);
125
149
 
126
- // Assert the property is no longer visible
127
- expect(screen.queryByText('name')).not.toBeInTheDocument();
150
+ // Wait for the changes to propagate
151
+ await waitFor(() => {
152
+ expect(screen.queryByText('name')).not.toBeInTheDocument();
153
+ });
128
154
 
129
- const debugInfo = JSON.parse(screen.getByTestId('debug').textContent!);
155
+ // Access the live objects directly from window using symbol.
156
+ const debugObjects = getViewEditorDebugObjects();
130
157
 
131
158
  // Check schema no longer contains the name property
132
- const schemaProperties = debugInfo.schema._storedSchema.jsonSchema.properties;
133
- expect(schemaProperties.name).toBeUndefined();
159
+ const schemaProperties = debugObjects.schema.jsonSchema.properties;
160
+ expect(schemaProperties).toBeDefined();
161
+ expect(schemaProperties!.name).toBeUndefined();
134
162
 
135
163
  // Check view no longer contains the name field
136
- const nameField = debugInfo.view.fields.find((field: any) => field.path === 'name');
164
+ const nameField = debugObjects.view.fields.find((field: any) => field.path === 'name');
137
165
  expect(nameField).toBeUndefined();
138
166
 
139
167
  // Check projection no longer contains the name property
140
- const nameProjection = debugInfo.projection._fieldProjections.find((proj: any) => proj.field.path === 'name');
168
+ const fieldProjections = debugObjects.projection.getFieldProjections();
169
+ const nameProjection = fieldProjections.find((proj: any) => proj.field.path === 'name');
141
170
  expect(nameProjection).toBeUndefined();
142
171
  });
143
172
 
@@ -150,17 +179,26 @@ describe('ViewEditor', () => {
150
179
  fireEvent.click(hideButtons[0]);
151
180
 
152
181
  // 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
182
  await waitFor(() => {
158
183
  expect(screen.getByTestId('show-field-button')).toBeInTheDocument();
159
184
  });
185
+
186
+ // Access the live objects directly from window using symbol.
187
+ const debugObjects = getViewEditorDebugObjects();
188
+
189
+ // Check that hiddenFields is non-empty
190
+ expect(debugObjects.view.hiddenFields).toBeDefined();
191
+ expect(debugObjects.view.hiddenFields!.length).toBeGreaterThan(0);
192
+
193
+ // Click the show button
160
194
  fireEvent.click(screen.getByTestId('show-field-button'));
161
195
 
162
- // Check that hiddenFields is empty again
163
- debugInfo = JSON.parse(screen.getByTestId('debug').textContent!);
164
- expect(debugInfo.view.hiddenFields.length).toBe(0);
196
+ // Wait for the data to update and check that hiddenFields is empty
197
+ await waitFor(() => {
198
+ expect(debugObjects.view.hiddenFields!.length).toBe(0);
199
+ });
200
+
201
+ // Also verify that the field is back in the visible fields list
202
+ expect(debugObjects.view.fields.length).toBeGreaterThan(0);
165
203
  });
166
204
  });