@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.
Files changed (115) hide show
  1. package/dist/lib/browser/index.mjs +1817 -1309
  2. package/dist/lib/browser/index.mjs.map +4 -4
  3. package/dist/lib/browser/meta.json +1 -1
  4. package/dist/lib/node/index.cjs +1773 -1273
  5. package/dist/lib/node/index.cjs.map +4 -4
  6. package/dist/lib/node/meta.json +1 -1
  7. package/dist/lib/node-esm/index.mjs +1817 -1309
  8. package/dist/lib/node-esm/index.mjs.map +4 -4
  9. package/dist/lib/node-esm/meta.json +1 -1
  10. package/dist/types/src/components/{ControlSection.d.ts → ControlSection/ControlSection.d.ts} +9 -6
  11. package/dist/types/src/components/ControlSection/ControlSection.d.ts.map +1 -0
  12. package/dist/types/src/components/ControlSection/ControlSection.stories.d.ts +8 -0
  13. package/dist/types/src/components/ControlSection/ControlSection.stories.d.ts.map +1 -0
  14. package/dist/types/src/components/ControlSection/index.d.ts +2 -0
  15. package/dist/types/src/components/ControlSection/index.d.ts.map +1 -0
  16. package/dist/types/src/components/FieldEditor/FieldEditor.d.ts.map +1 -1
  17. package/dist/types/src/components/FieldEditor/FieldEditor.test.d.ts +2 -0
  18. package/dist/types/src/components/FieldEditor/FieldEditor.test.d.ts.map +1 -0
  19. package/dist/types/src/components/Form/ArrayField.d.ts.map +1 -1
  20. package/dist/types/src/components/Form/Defaults.d.ts +4 -4
  21. package/dist/types/src/components/Form/Defaults.d.ts.map +1 -1
  22. package/dist/types/src/components/Form/Form.d.ts +6 -4
  23. package/dist/types/src/components/Form/Form.d.ts.map +1 -1
  24. package/dist/types/src/components/Form/Form.stories.d.ts +38 -36
  25. package/dist/types/src/components/Form/Form.stories.d.ts.map +1 -1
  26. package/dist/types/src/components/Form/FormActions.d.ts +1 -1
  27. package/dist/types/src/components/Form/FormActions.d.ts.map +1 -1
  28. package/dist/types/src/components/Form/FormContent.d.ts +16 -12
  29. package/dist/types/src/components/Form/FormContent.d.ts.map +1 -1
  30. package/dist/types/src/components/Form/FormContext.d.ts +2 -2
  31. package/dist/types/src/components/Form/FormContext.d.ts.map +1 -1
  32. package/dist/types/src/components/Form/Input.d.ts.map +1 -1
  33. package/dist/types/src/components/Form/RefField.d.ts +12 -0
  34. package/dist/types/src/components/Form/RefField.d.ts.map +1 -0
  35. package/dist/types/src/components/Form/custom/GeoPointInput.d.ts.map +1 -1
  36. package/dist/types/src/components/Form/custom/SelectOptionsInput.d.ts.map +1 -1
  37. package/dist/types/src/components/Form/custom/SelectOptionsInput.stories.d.ts.map +1 -1
  38. package/dist/types/src/components/Form/custom/TupleInput.d.ts.map +1 -1
  39. package/dist/types/src/components/Form/factory.d.ts +1 -1
  40. package/dist/types/src/components/Form/factory.d.ts.map +1 -1
  41. package/dist/types/src/components/Form/index.d.ts.map +1 -1
  42. package/dist/types/src/components/ViewEditor/Test.stories.d.ts.map +1 -1
  43. package/dist/types/src/components/ViewEditor/ViewEditor.d.ts +2 -2
  44. package/dist/types/src/components/ViewEditor/ViewEditor.d.ts.map +1 -1
  45. package/dist/types/src/components/ViewEditor/ViewEditor.stories.d.ts +5 -3
  46. package/dist/types/src/components/ViewEditor/ViewEditor.stories.d.ts.map +1 -1
  47. package/dist/types/src/components/ViewEditor/ViewEditor.test.d.ts +2 -0
  48. package/dist/types/src/components/ViewEditor/ViewEditor.test.d.ts.map +1 -0
  49. package/dist/types/src/components/deprecated/Form.d.ts +2 -2
  50. package/dist/types/src/components/deprecated/Form.d.ts.map +1 -1
  51. package/dist/types/src/components/deprecated/Form.stories.d.ts.map +1 -1
  52. package/dist/types/src/components/deprecated/FormInput.d.ts.map +1 -1
  53. package/dist/types/src/components/index.d.ts.map +1 -1
  54. package/dist/types/src/components/testing.d.ts.map +1 -1
  55. package/dist/types/src/hooks/index.d.ts +2 -0
  56. package/dist/types/src/hooks/index.d.ts.map +1 -1
  57. package/dist/types/src/hooks/useForm.d.ts +2 -2
  58. package/dist/types/src/hooks/useForm.d.ts.map +1 -1
  59. package/dist/types/src/hooks/useQueryRefOptions.d.ts +23 -0
  60. package/dist/types/src/hooks/useQueryRefOptions.d.ts.map +1 -0
  61. package/dist/types/src/hooks/useRefQueryLookupHandler.d.ts +8 -0
  62. package/dist/types/src/hooks/useRefQueryLookupHandler.d.ts.map +1 -0
  63. package/dist/types/src/translations.d.ts +5 -0
  64. package/dist/types/src/translations.d.ts.map +1 -1
  65. package/dist/types/src/util/field.d.ts.map +1 -1
  66. package/dist/types/src/util/formatting.d.ts.map +1 -1
  67. package/dist/types/src/util/index.d.ts +2 -0
  68. package/dist/types/src/util/index.d.ts.map +1 -1
  69. package/dist/types/src/util/refs.d.ts +9 -0
  70. package/dist/types/src/util/refs.d.ts.map +1 -0
  71. package/dist/types/src/util/schema.d.ts +3 -0
  72. package/dist/types/src/util/schema.d.ts.map +1 -0
  73. package/dist/types/src/vitest-setup.d.ts +2 -0
  74. package/dist/types/src/vitest-setup.d.ts.map +1 -0
  75. package/dist/types/tsconfig.tsbuildinfo +1 -1
  76. package/package.json +28 -25
  77. package/src/components/ControlSection/ControlSection.stories.tsx +29 -0
  78. package/src/components/{ControlSection.tsx → ControlSection/ControlSection.tsx} +40 -16
  79. package/src/components/ControlSection/index.ts +5 -0
  80. package/src/components/FieldEditor/FieldEditor.stories.tsx +1 -1
  81. package/src/components/FieldEditor/FieldEditor.test.tsx +49 -0
  82. package/src/components/FieldEditor/FieldEditor.tsx +3 -2
  83. package/src/components/Form/ArrayField.tsx +63 -50
  84. package/src/components/Form/Defaults.tsx +77 -48
  85. package/src/components/Form/Form.stories.tsx +134 -109
  86. package/src/components/Form/Form.tsx +8 -5
  87. package/src/components/Form/FormActions.tsx +15 -18
  88. package/src/components/Form/FormContent.tsx +116 -46
  89. package/src/components/Form/FormContext.tsx +24 -10
  90. package/src/components/Form/Input.tsx +4 -10
  91. package/src/components/Form/RefField.tsx +217 -0
  92. package/src/components/Form/custom/GeoPointInput.tsx +2 -2
  93. package/src/components/Form/custom/SelectOptionsInput.stories.tsx +6 -5
  94. package/src/components/Form/custom/SelectOptionsInput.tsx +2 -2
  95. package/src/components/Form/factory.tsx +1 -1
  96. package/src/components/Form/index.ts +0 -1
  97. package/src/components/ViewEditor/ViewEditor.stories.tsx +19 -9
  98. package/src/components/ViewEditor/ViewEditor.test.tsx +166 -0
  99. package/src/components/ViewEditor/ViewEditor.tsx +63 -30
  100. package/src/components/deprecated/Form.stories.tsx +6 -6
  101. package/src/components/deprecated/Form.tsx +6 -6
  102. package/src/components/deprecated/FormInput.tsx +1 -1
  103. package/src/components/index.ts +1 -0
  104. package/src/components/testing.tsx +5 -9
  105. package/src/hooks/index.ts +3 -1
  106. package/src/hooks/useForm.ts +4 -3
  107. package/src/hooks/useQueryRefOptions.ts +53 -0
  108. package/src/hooks/useRefQueryLookupHandler.ts +39 -0
  109. package/src/translations.ts +9 -2
  110. package/src/util/formatting.ts +15 -3
  111. package/src/util/index.ts +3 -1
  112. package/src/util/refs.ts +38 -0
  113. package/src/util/schema.ts +14 -0
  114. package/src/vitest-setup.ts +11 -0
  115. 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, S, toJsonSchema, TypedObject } from '@dxos/echo-schema';
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
- const DefaultStory = () => {
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: S.String,
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.schema(ViewType));
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<typeof ViewEditor> = {
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, tooltips: true }), withTheme],
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 { AST, Format, type BaseSchema, S, type JsonProp } from '@dxos/echo-schema';
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 = S.Struct({
22
- name: S.String.annotations({
23
- [AST.TitleAnnotationId]: 'View',
22
+ const ViewMetaSchema = Schema.Struct({
23
+ name: Schema.String.annotations({
24
+ title: 'View',
24
25
  }),
25
26
  typename: Format.URL.annotations({
26
- [AST.TitleAnnotationId]: 'Typename',
27
+ title: 'Typename',
27
28
  }),
28
- }).pipe(S.mutable);
29
+ }).pipe(Schema.mutable);
29
30
 
30
- type ViewMetaType = S.Schema.Type<typeof ViewMetaSchema>;
31
+ type ViewMetaType = Schema.Schema.Type<typeof ViewMetaSchema>;
31
32
 
32
33
  export type ViewEditorProps = ThemedClassName<{
33
- schema: BaseSchema;
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(() => new ViewProjection(schema.jsonSchema, view), [schema, view]);
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((field: FieldType) => {
70
- setField((f) => (f === field ? undefined : field));
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(!schema.readonly);
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(!schema.readonly);
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(!schema.readonly);
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(!schema.readonly);
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('flex flex-col w-full divide-y divide-separator', classNames)}>
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={schema.readonly ? undefined : handleUpdate}
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={S.is(FieldSchema)}
181
+ isItem={Schema.is(FieldSchema)}
159
182
  getId={(field) => field.id}
160
- onMove={schema.readonly ? undefined : handleMove}
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> key={field.id} item={field} classNames={mx(grid, ghostHover, 'cursor-pointer')}>
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
- {!schema.readonly && (
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 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
+ />
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='flex p-2 justify-center'>
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={schema.readonly ? undefined : handleAdd}
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 { create } from '@dxos/live-object';
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: create(testData),
43
- view: create(testView),
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: create(testView),
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: create(testData),
58
- view: create(testView),
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 as AST, type Schema as S, Option, pipe } from 'effect';
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?: S.Schema<T>;
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(AST.getTitleAnnotation(prop), Option.getOrUndefined)) ?? '';
35
- const description = (prop && pipe(AST.getDescriptionAnnotation(prop), Option.getOrUndefined)) ?? title;
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) ?? (AST.isNumberKeyword(prop) && RealNumberFormat))) || undefined;
37
+ // (prop && (getPatternAnnotation(prop) ?? (SchemaAST.isNumberKeyword(prop) && RealNumberFormat))) || undefined;
38
38
 
39
39
  //
40
40
  // Boolean
41
41
  //
42
42
 
43
- if (prop && AST.isBooleanKeyword(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='w-full p-4 justify-center overflow-x-hidden overflow-y-auto'>
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>
@@ -3,6 +3,7 @@
3
3
  //
4
4
 
5
5
  export * from './deprecated';
6
+
6
7
  export * from './ControlSection';
7
8
  export * from './FieldEditor';
8
9
  export * from './Form';
@@ -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 { SyntaxHighlighter } from '@dxos/react-ui-syntax-highlighter';
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='flex flex-col p-4'>
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-3'>
19
- <div className={mx('flex col-span-2 w-full justify-center overflow-y-auto', classNames)}>{children}</div>
20
- <SyntaxHighlighter language='json' className='w-full text-xs font-thin'>
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
  );
@@ -1,5 +1,7 @@
1
1
  //
2
- // Copyright 2024 DXOS.org
2
+ // Copyright 2025 DXOS.org
3
3
  //
4
4
 
5
5
  export * from './useForm';
6
+ export * from './useRefQueryLookupHandler';
7
+ export * from './useQueryRefOptions';
@@ -2,7 +2,7 @@
2
2
  // Copyright 2025 DXOS.org
3
3
  //
4
4
 
5
- import { SchemaAST as AST, type Schema as S } from 'effect';
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: S.Schema<T>;
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(AST.isTypeLiteral(schema.ast));
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]);