@dxos/react-ui-form 0.8.4-main.d05539e30a → 0.8.4-main.d9fc60f731

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 (154) hide show
  1. package/dist/lib/browser/index.mjs +1335 -561
  2. package/dist/lib/browser/index.mjs.map +4 -4
  3. package/dist/lib/browser/meta.json +1 -1
  4. package/dist/lib/browser/translations.mjs +2 -1
  5. package/dist/lib/browser/translations.mjs.map +2 -2
  6. package/dist/lib/node-esm/index.mjs +1335 -561
  7. package/dist/lib/node-esm/index.mjs.map +4 -4
  8. package/dist/lib/node-esm/meta.json +1 -1
  9. package/dist/lib/node-esm/translations.mjs +2 -1
  10. package/dist/lib/node-esm/translations.mjs.map +2 -2
  11. package/dist/types/src/components/FieldEditor/FieldEditor.d.ts +2 -2
  12. package/dist/types/src/components/FieldEditor/FieldEditor.d.ts.map +1 -1
  13. package/dist/types/src/components/FieldEditor/FieldEditor.stories.d.ts +2 -1
  14. package/dist/types/src/components/FieldEditor/FieldEditor.stories.d.ts.map +1 -1
  15. package/dist/types/src/components/Form/Form.d.ts +35 -9
  16. package/dist/types/src/components/Form/Form.d.ts.map +1 -1
  17. package/dist/types/src/components/Form/Form.stories.d.ts +76 -16
  18. package/dist/types/src/components/Form/Form.stories.d.ts.map +1 -1
  19. package/dist/types/src/components/Form/FormField.d.ts +14 -4
  20. package/dist/types/src/components/Form/FormField.d.ts.map +1 -1
  21. package/dist/types/src/components/Form/FormFieldComponent.d.ts +22 -10
  22. package/dist/types/src/components/Form/FormFieldComponent.d.ts.map +1 -1
  23. package/dist/types/src/components/Form/FormFieldSet.d.ts +8 -2
  24. package/dist/types/src/components/Form/FormFieldSet.d.ts.map +1 -1
  25. package/dist/types/src/components/Form/FormTooltipsContext.d.ts +19 -0
  26. package/dist/types/src/components/Form/FormTooltipsContext.d.ts.map +1 -0
  27. package/dist/types/src/components/Form/Layout/FormLayout.d.ts +50 -0
  28. package/dist/types/src/components/Form/Layout/FormLayout.d.ts.map +1 -0
  29. package/dist/types/src/components/Form/Layout/FormLayout.stories.d.ts +119 -0
  30. package/dist/types/src/components/Form/Layout/FormLayout.stories.d.ts.map +1 -0
  31. package/dist/types/src/components/Form/Layout/FormLayout.stories.test.d.ts +2 -0
  32. package/dist/types/src/components/Form/Layout/FormLayout.stories.test.d.ts.map +1 -0
  33. package/dist/types/src/components/Form/Layout/FormLayout.test.d.ts +2 -0
  34. package/dist/types/src/components/Form/Layout/FormLayout.test.d.ts.map +1 -0
  35. package/dist/types/src/components/Form/Layout/annotation.d.ts +30 -0
  36. package/dist/types/src/components/Form/Layout/annotation.d.ts.map +1 -0
  37. package/dist/types/src/components/Form/Layout/index.d.ts +4 -0
  38. package/dist/types/src/components/Form/Layout/index.d.ts.map +1 -0
  39. package/dist/types/src/components/Form/Layout/parser.d.ts +31 -0
  40. package/dist/types/src/components/Form/Layout/parser.d.ts.map +1 -0
  41. package/dist/types/src/components/Form/Layout/parser.test.d.ts +2 -0
  42. package/dist/types/src/components/Form/Layout/parser.test.d.ts.map +1 -0
  43. package/dist/types/src/components/Form/fields/ArrayField.d.ts.map +1 -1
  44. package/dist/types/src/components/Form/fields/DateField.d.ts +4 -0
  45. package/dist/types/src/components/Form/fields/DateField.d.ts.map +1 -0
  46. package/dist/types/src/components/Form/fields/GeoPointField.d.ts +1 -1
  47. package/dist/types/src/components/Form/fields/GeoPointField.d.ts.map +1 -1
  48. package/dist/types/src/components/Form/fields/InlineRefField.d.ts +13 -0
  49. package/dist/types/src/components/Form/fields/InlineRefField.d.ts.map +1 -0
  50. package/dist/types/src/components/Form/fields/MarkdownField.d.ts +1 -1
  51. package/dist/types/src/components/Form/fields/MarkdownField.d.ts.map +1 -1
  52. package/dist/types/src/components/Form/fields/MarkdownField.stories.d.ts +2 -1
  53. package/dist/types/src/components/Form/fields/MarkdownField.stories.d.ts.map +1 -1
  54. package/dist/types/src/components/Form/fields/RefField.d.ts +1 -1
  55. package/dist/types/src/components/Form/fields/RefField.d.ts.map +1 -1
  56. package/dist/types/src/components/Form/fields/SelectField.d.ts +4 -1
  57. package/dist/types/src/components/Form/fields/SelectField.d.ts.map +1 -1
  58. package/dist/types/src/components/Form/fields/SelectOptionField.d.ts +1 -1
  59. package/dist/types/src/components/Form/fields/SelectOptionField.d.ts.map +1 -1
  60. package/dist/types/src/components/Form/fields/SelectOptionField.stories.d.ts +2 -1
  61. package/dist/types/src/components/Form/fields/SelectOptionField.stories.d.ts.map +1 -1
  62. package/dist/types/src/components/Form/fields/TupleField.d.ts +1 -1
  63. package/dist/types/src/components/Form/fields/TupleField.d.ts.map +1 -1
  64. package/dist/types/src/components/Form/fields/index.d.ts +2 -0
  65. package/dist/types/src/components/Form/fields/index.d.ts.map +1 -1
  66. package/dist/types/src/components/Form/index.d.ts +3 -0
  67. package/dist/types/src/components/Form/index.d.ts.map +1 -1
  68. package/dist/types/src/components/Form/meta-tags.d.ts +30 -0
  69. package/dist/types/src/components/Form/meta-tags.d.ts.map +1 -0
  70. package/dist/types/src/components/Form/meta-tags.test.d.ts +2 -0
  71. package/dist/types/src/components/Form/meta-tags.test.d.ts.map +1 -0
  72. package/dist/types/src/components/ObjectForm/ObjectForm.d.ts +3 -4
  73. package/dist/types/src/components/ObjectForm/ObjectForm.d.ts.map +1 -1
  74. package/dist/types/src/components/ObjectPicker/ObjectPicker.d.ts +2 -2
  75. package/dist/types/src/components/ObjectPicker/ObjectPicker.d.ts.map +1 -1
  76. package/dist/types/src/components/ObjectPicker/ObjectPicker.stories.d.ts +2 -1
  77. package/dist/types/src/components/ObjectPicker/ObjectPicker.stories.d.ts.map +1 -1
  78. package/dist/types/src/components/ObjectProperties/ObjectProperties.d.ts.map +1 -1
  79. package/dist/types/src/components/ObjectProperties/ObjectProperties.stories.d.ts +2 -1
  80. package/dist/types/src/components/ObjectProperties/ObjectProperties.stories.d.ts.map +1 -1
  81. package/dist/types/src/components/ObjectTree/ObjectTree.d.ts +9 -0
  82. package/dist/types/src/components/ObjectTree/ObjectTree.d.ts.map +1 -0
  83. package/dist/types/src/components/ObjectTree/ObjectTree.stories.d.ts +87 -0
  84. package/dist/types/src/components/ObjectTree/ObjectTree.stories.d.ts.map +1 -0
  85. package/dist/types/src/components/ObjectTree/index.d.ts +2 -0
  86. package/dist/types/src/components/ObjectTree/index.d.ts.map +1 -0
  87. package/dist/types/src/components/Settings/Settings.d.ts.map +1 -1
  88. package/dist/types/src/components/Settings/Settings.stories.d.ts +2 -1
  89. package/dist/types/src/components/Settings/Settings.stories.d.ts.map +1 -1
  90. package/dist/types/src/components/ViewEditor/ViewEditor.d.ts +7 -8
  91. package/dist/types/src/components/ViewEditor/ViewEditor.d.ts.map +1 -1
  92. package/dist/types/src/components/ViewEditor/ViewEditor.stories.d.ts +4 -4
  93. package/dist/types/src/components/ViewEditor/ViewEditor.stories.d.ts.map +1 -1
  94. package/dist/types/src/components/index.d.ts +1 -0
  95. package/dist/types/src/components/index.d.ts.map +1 -1
  96. package/dist/types/src/components/testing.d.ts +9 -0
  97. package/dist/types/src/components/testing.d.ts.map +1 -1
  98. package/dist/types/src/translations.d.ts +2 -1
  99. package/dist/types/src/translations.d.ts.map +1 -1
  100. package/dist/types/src/util/properties.d.ts +11 -0
  101. package/dist/types/src/util/properties.d.ts.map +1 -1
  102. package/dist/types/tsconfig.tsbuildinfo +1 -1
  103. package/package.json +37 -32
  104. package/src/components/FieldEditor/FieldEditor.stories.tsx +2 -2
  105. package/src/components/FieldEditor/FieldEditor.test.tsx +4 -1
  106. package/src/components/FieldEditor/FieldEditor.tsx +19 -14
  107. package/src/components/Form/Form.stories.tsx +21 -27
  108. package/src/components/Form/Form.tsx +106 -19
  109. package/src/components/Form/FormField.tsx +85 -28
  110. package/src/components/Form/FormFieldComponent.tsx +75 -20
  111. package/src/components/Form/FormFieldSet.tsx +32 -3
  112. package/src/components/Form/FormTooltipsContext.ts +25 -0
  113. package/src/components/Form/Layout/FormLayout.stories.test.tsx +51 -0
  114. package/src/components/Form/Layout/FormLayout.stories.tsx +422 -0
  115. package/src/components/Form/Layout/FormLayout.test.ts +59 -0
  116. package/src/components/Form/Layout/FormLayout.tsx +251 -0
  117. package/src/components/Form/Layout/annotation.ts +38 -0
  118. package/src/components/Form/Layout/index.ts +7 -0
  119. package/src/components/Form/Layout/parser.test.ts +120 -0
  120. package/src/components/Form/Layout/parser.ts +167 -0
  121. package/src/components/Form/fields/ArrayField.tsx +61 -38
  122. package/src/components/Form/fields/DateField.tsx +101 -0
  123. package/src/components/Form/fields/GeoPointField.tsx +4 -1
  124. package/src/components/Form/fields/InlineRefField.tsx +146 -0
  125. package/src/components/Form/fields/MarkdownField.tsx +3 -3
  126. package/src/components/Form/fields/RefField.tsx +29 -23
  127. package/src/components/Form/fields/SelectField.tsx +17 -5
  128. package/src/components/Form/fields/SelectOptionField.tsx +4 -3
  129. package/src/components/Form/fields/TextAreaField.tsx +1 -1
  130. package/src/components/Form/fields/TupleField.tsx +2 -1
  131. package/src/components/Form/fields/index.ts +2 -0
  132. package/src/components/Form/index.ts +3 -0
  133. package/src/components/Form/meta-tags.test.ts +47 -0
  134. package/src/components/Form/meta-tags.ts +33 -0
  135. package/src/components/ObjectForm/ObjectForm.tsx +21 -26
  136. package/src/components/ObjectPicker/ObjectPicker.stories.tsx +2 -2
  137. package/src/components/ObjectPicker/ObjectPicker.tsx +2 -2
  138. package/src/components/ObjectProperties/ObjectProperties.stories.tsx +14 -26
  139. package/src/components/ObjectProperties/ObjectProperties.test.tsx +20 -10
  140. package/src/components/ObjectProperties/ObjectProperties.tsx +26 -30
  141. package/src/components/ObjectTree/ObjectTree.stories.tsx +134 -0
  142. package/src/components/ObjectTree/ObjectTree.tsx +190 -0
  143. package/src/components/ObjectTree/index.ts +5 -0
  144. package/src/components/Settings/Settings.tsx +2 -1
  145. package/src/components/Settings/SettingsItem.tsx +1 -1
  146. package/src/components/ViewEditor/ViewEditor.stories.tsx +35 -40
  147. package/src/components/ViewEditor/ViewEditor.test.tsx +11 -8
  148. package/src/components/ViewEditor/ViewEditor.tsx +29 -32
  149. package/src/components/index.ts +1 -0
  150. package/src/components/testing.tsx +4 -4
  151. package/src/translations.ts +2 -1
  152. package/src/util/properties.test.ts +67 -9
  153. package/src/util/properties.ts +36 -4
  154. package/src/vite-env.d.ts +10 -0
@@ -3,14 +3,13 @@
3
3
  //
4
4
 
5
5
  import { RegistryContext } from '@effect-atom/atom-react';
6
- import * as Array from 'effect/Array';
7
6
  import * as Match from 'effect/Match';
8
7
  import * as Option from 'effect/Option';
9
8
  import * as Schema from 'effect/Schema';
10
9
  import React, { forwardRef, useCallback, useContext, useImperativeHandle, useMemo, useState } from 'react';
11
10
 
12
- import { Entity, Feed, Filter, Format, Obj, Query, QueryAST, Ref, type SchemaRegistry, View } from '@dxos/echo';
13
- import { EchoSchema, type JsonProp, isMutable, toJsonSchema } from '@dxos/echo/internal';
11
+ import { EID, Entity, Feed, Filter, Format, Obj, Query, QueryAST, Ref, type Registry, Type, View } from '@dxos/echo';
12
+ import { type JsonProp } from '@dxos/echo/internal';
14
13
  import { invariant } from '@dxos/invariant';
15
14
  import { useObject, useQuery } from '@dxos/react-client/echo';
16
15
  import { IconButton, Input, Message, type ThemedClassName, useTranslation } from '@dxos/react-ui';
@@ -39,12 +38,12 @@ import {
39
38
 
40
39
  export type ViewEditorProps = ThemedClassName<
41
40
  {
42
- schema: Schema.Schema.AnyNoContext;
41
+ type: Type.AnyEntity;
43
42
  view: View.View;
44
43
  mode?: 'schema' | 'tag';
45
- registry?: SchemaRegistry.SchemaRegistry;
44
+ registry?: Registry.Registry;
46
45
  showHeading?: boolean;
47
- onQueryChanged?: (query: QueryAST.Query, target?: string) => void;
46
+ onQueryChanged?: (query: QueryAST.Query, target?: EID.EID) => void;
48
47
  onDelete?: (fieldId: string) => void;
49
48
  } & Pick<QueryFormProps, 'types' | 'tags'> &
50
49
  Pick<FormRootProps<any>, 'readonly' | 'db'>
@@ -57,7 +56,7 @@ export const ViewEditor = forwardRef<ProjectionModel, ViewEditorProps>(
57
56
  (
58
57
  {
59
58
  classNames,
60
- schema,
59
+ type,
61
60
  view,
62
61
  mode = 'schema',
63
62
  registry,
@@ -72,16 +71,15 @@ export const ViewEditor = forwardRef<ProjectionModel, ViewEditorProps>(
72
71
  forwardedRef,
73
72
  ) => {
74
73
  const atomRegistry = useContext(RegistryContext);
75
- const schemaReadonly = !isMutable(schema);
74
+ const schemaReadonly = Type.getDatabase(type) == null;
76
75
  const { t } = useTranslation(translationKey);
77
76
 
78
77
  const projectionModel = useMemo(() => {
79
- // Use reactive and mutable version of json schema when schema is mutable.
80
- const jsonSchema = schema instanceof EchoSchema ? schema.jsonSchema : toJsonSchema(schema);
78
+ const jsonSchema = type.jsonSchema;
81
79
 
82
80
  // Always use createEchoChangeCallback since the view is ECHO-backed.
83
- // Pass schema only when mutable to allow schema mutations.
84
- const change = createEchoChangeCallback(view, schema instanceof EchoSchema ? schema : undefined);
81
+ // Pass type only when mutable to allow schema mutations.
82
+ const change = createEchoChangeCallback(view, Type.getDatabase(type) != null ? type : undefined);
85
83
 
86
84
  const model = new ProjectionModel({
87
85
  registry: atomRegistry,
@@ -91,7 +89,7 @@ export const ViewEditor = forwardRef<ProjectionModel, ViewEditorProps>(
91
89
  });
92
90
 
93
91
  return model;
94
- }, [atomRegistry, schema, view]);
92
+ }, [atomRegistry, type, view]);
95
93
 
96
94
  useImperativeHandle(forwardedRef, () => projectionModel, [projectionModel]);
97
95
 
@@ -100,9 +98,9 @@ export const ViewEditor = forwardRef<ProjectionModel, ViewEditorProps>(
100
98
  if (from._tag !== 'scope') {
101
99
  return undefined;
102
100
  }
103
- return Option.fromNullable(from.scope.feeds).pipe(
104
- Option.flatMap((feeds) => Array.head(feeds)),
105
- Option.map(String),
101
+ const feedScope = from.scopes.find((s) => s._tag === 'feed');
102
+ return Option.fromNullable(feedScope).pipe(
103
+ Option.map((s) => s.feedUri),
106
104
  Option.getOrUndefined,
107
105
  );
108
106
  }),
@@ -115,8 +113,8 @@ export const ViewEditor = forwardRef<ProjectionModel, ViewEditorProps>(
115
113
  if (!queueTarget) {
116
114
  return undefined;
117
115
  }
118
- const feed = feeds.find((feed) => Feed.getQueueDxn(feed)?.toString() === queueTarget);
119
- return feed ? Ref.fromDXN(Entity.getDXN(feed)) : undefined;
116
+ const feed = feeds.find((feed) => Feed.getQueueUri(feed)?.toString() === queueTarget);
117
+ return feed ? Ref.fromURI(Entity.getURI(feed)) : undefined;
120
118
  }, [queueTarget, feeds]);
121
119
 
122
120
  const viewSchema = useMemo(() => {
@@ -160,13 +158,13 @@ export const ViewEditor = forwardRef<ProjectionModel, ViewEditorProps>(
160
158
  const handleUpdate = useCallback(
161
159
  (values: any) => {
162
160
  const targetValue = values.target;
163
- let feedDXN: string | undefined;
161
+ let queueDxn: EID.EID | undefined;
164
162
 
165
163
  if (Ref.isRef(targetValue)) {
166
- const targetDXN = targetValue.dxn.toString();
167
- const feed = feeds.find((feed) => Obj.getDXN(feed).toString() === targetDXN);
164
+ const feedUri = targetValue.uri;
165
+ const feed = feeds.find((feed) => Obj.getURI(feed) === feedUri);
168
166
  if (feed) {
169
- feedDXN = Feed.getQueueDxn(feed)?.toString();
167
+ queueDxn = Feed.getQueueUri(feed);
170
168
  }
171
169
  }
172
170
 
@@ -175,7 +173,7 @@ export const ViewEditor = forwardRef<ProjectionModel, ViewEditorProps>(
175
173
  mode === 'schema'
176
174
  ? Query.select(Filter.typename(values.query)).ast
177
175
  : JSON.parse(JSON.stringify(values.query));
178
- onQueryChanged?.(query, feedDXN);
176
+ onQueryChanged?.(query, queueDxn);
179
177
  },
180
178
  [onQueryChanged, mode, feeds],
181
179
  );
@@ -203,7 +201,7 @@ export const ViewEditor = forwardRef<ProjectionModel, ViewEditorProps>(
203
201
 
204
202
  <FormFieldLabel label={t('fields.label')} asChild />
205
203
  <FieldList
206
- schema={schema}
204
+ type={type}
207
205
  view={view}
208
206
  registry={registry}
209
207
  readonly={readonly}
@@ -216,23 +214,22 @@ export const ViewEditor = forwardRef<ProjectionModel, ViewEditorProps>(
216
214
  },
217
215
  );
218
216
 
219
- type FieldListProps = Pick<ViewEditorProps, 'schema' | 'view' | 'registry' | 'readonly' | 'showHeading' | 'onDelete'>;
217
+ type FieldListProps = Pick<ViewEditorProps, 'type' | 'view' | 'registry' | 'readonly' | 'showHeading' | 'onDelete'>;
220
218
 
221
- const FieldList = ({ schema, view, registry, readonly, showHeading = false, onDelete }: FieldListProps) => {
219
+ const FieldList = ({ type, view, registry, readonly, showHeading = false, onDelete }: FieldListProps) => {
222
220
  const atomRegistry = useContext(RegistryContext);
223
- const schemaReadonly = !isMutable(schema);
221
+ const schemaReadonly = Type.getDatabase(type) == null;
224
222
  const { t } = useTranslation(translationKey);
225
223
 
226
224
  // Subscribe to view changes for reactivity.
227
225
  const [viewSnapshot] = useObject(view);
228
226
 
229
227
  const projectionModel = useMemo(() => {
230
- // Use reactive and mutable version of json schema when schema is mutable.
231
- const jsonSchema = schema instanceof EchoSchema ? schema.jsonSchema : toJsonSchema(schema);
228
+ const jsonSchema = type.jsonSchema;
232
229
 
233
230
  // Always use createEchoChangeCallback since the view is ECHO-backed.
234
- // Pass schema only when mutable to allow schema mutations.
235
- const change = createEchoChangeCallback(view, schema instanceof EchoSchema ? schema : undefined);
231
+ // Pass type only when mutable to allow schema mutations.
232
+ const change = createEchoChangeCallback(view, Type.getDatabase(type) != null ? type : undefined);
236
233
 
237
234
  const model = new ProjectionModel({
238
235
  registry: atomRegistry,
@@ -242,7 +239,7 @@ const FieldList = ({ schema, view, registry, readonly, showHeading = false, onDe
242
239
  });
243
240
 
244
241
  return model;
245
- }, [atomRegistry, schema, view]);
242
+ }, [atomRegistry, type, view]);
246
243
 
247
244
  const [expandedField, setExpandedField] = useState<View.FieldType['id']>();
248
245
 
@@ -7,5 +7,6 @@ export * from './Form';
7
7
  export * from './ObjectForm';
8
8
  export * from './ObjectPicker';
9
9
  export * from './ObjectProperties';
10
+ export * from './ObjectTree';
10
11
  export * from './Settings';
11
12
  export * from './ViewEditor';
@@ -5,8 +5,8 @@
5
5
  import React, { type PropsWithChildren } from 'react';
6
6
 
7
7
  import { type ThemedClassName } from '@dxos/react-ui';
8
+ import { composableProps, slottable } from '@dxos/react-ui';
8
9
  import { Syntax } from '@dxos/react-ui-syntax-highlighter';
9
- import { composableProps, slottable } from '@dxos/ui-theme';
10
10
 
11
11
  type TestLayoutProps = PropsWithChildren<{ json?: any }>;
12
12
 
@@ -19,7 +19,7 @@ export const TestLayout = ({ children, json }: TestLayoutProps) => {
19
19
  <Syntax.Content>
20
20
  <Syntax.Filter />
21
21
  <Syntax.Viewport>
22
- <Syntax.Code testId='debug' classNames='text-xs' />
22
+ <Syntax.Code testId='debug' classNames='text-sm' />
23
23
  </Syntax.Viewport>
24
24
  </Syntax.Content>
25
25
  </Syntax.Root>
@@ -30,9 +30,9 @@ export const TestLayout = ({ children, json }: TestLayoutProps) => {
30
30
 
31
31
  type TestPanelProps = ThemedClassName<PropsWithChildren>;
32
32
 
33
- const TestPanel = slottable<HTMLDivElement, TestPanelProps>(({ children }, forwardedRef) => {
33
+ export const TestPanel = slottable<HTMLDivElement, TestPanelProps>(({ children }, forwardedRef) => {
34
34
  return (
35
- <div {...composableProps({ classNames: 'dx-container bg-modal-surface rounded-md' })} ref={forwardedRef}>
35
+ <div {...composableProps({ classNames: 'dx-container bg-card-surface rounded-md' })} ref={forwardedRef}>
36
36
  {children}
37
37
  </div>
38
38
  );
@@ -19,6 +19,7 @@ export const translations = [
19
19
  'field-format.label': 'Type',
20
20
  'field-path.label': 'Field path',
21
21
 
22
+ 'remove-item.button': 'Delete item',
22
23
  'add-item.button': 'Add item',
23
24
  'add-property-button.label': 'Add property',
24
25
  'boolean-input-true.value': 'Yes',
@@ -85,7 +86,7 @@ export const translations = [
85
86
  // System schema message.
86
87
  'system-schema.title': 'System type',
87
88
  'system-schema.description': 'System type: cannot add or remove fields.',
88
- 'remove.button': 'Remove',
89
+ 'remove.button': 'Delete',
89
90
  },
90
91
  },
91
92
  },
@@ -3,9 +3,12 @@
3
3
  //
4
4
 
5
5
  import * as Schema from 'effect/Schema';
6
+ import * as SchemaAST from 'effect/SchemaAST';
6
7
  import { describe, test } from 'vitest';
7
8
 
8
- import { Annotation, JsonSchema, Type } from '@dxos/echo';
9
+ import { DXN, Annotation, JsonSchema, Type } from '@dxos/echo';
10
+ import { Format } from '@dxos/echo/internal';
11
+ import { findNode, getArrayElementType } from '@dxos/effect';
9
12
 
10
13
  import { getFormProperties } from './properties';
11
14
 
@@ -14,9 +17,9 @@ describe('getFormProperties', () => {
14
17
  const TestSchema = Schema.Struct({
15
18
  name: Schema.optional(Schema.String),
16
19
  hidden: Schema.String.pipe(Annotation.FormInputAnnotation.set(false)),
17
- }).pipe(Type.object({ typename: 'org.dxos.test.keyword-hidden', version: '0.1.0' }));
20
+ }).pipe(Type.makeObject(DXN.make('org.dxos.test.keywordHidden', '0.1.0')));
18
21
 
19
- const names = getFormProperties(TestSchema.ast).map((prop) => prop.name);
22
+ const names = getFormProperties(Type.getSchema(TestSchema).ast).map((prop) => prop.name);
20
23
  expect(names).toContain('name');
21
24
  expect(names).not.toContain('hidden');
22
25
  });
@@ -28,9 +31,9 @@ describe('getFormProperties', () => {
28
31
  const TestSchema = Schema.Struct({
29
32
  name: Schema.optional(Schema.String),
30
33
  schema: JsonSchema.JsonSchema.pipe(Annotation.FormInputAnnotation.set(false)),
31
- }).pipe(Type.object({ typename: 'org.dxos.test.struct-hidden', version: '0.1.0' }));
34
+ }).pipe(Type.makeObject(DXN.make('org.dxos.test.structHidden', '0.1.0')));
32
35
 
33
- const names = getFormProperties(TestSchema.ast).map((prop) => prop.name);
36
+ const names = getFormProperties(Type.getSchema(TestSchema).ast).map((prop) => prop.name);
34
37
  expect(names).toContain('name');
35
38
  expect(names).not.toContain('schema');
36
39
  });
@@ -39,13 +42,68 @@ describe('getFormProperties', () => {
39
42
  const TestSchema = Schema.Struct({
40
43
  name: Schema.optional(Schema.String),
41
44
  context: Schema.Array(Schema.Any).pipe(Annotation.FormInputAnnotation.set(false)),
42
- }).pipe(Type.object({ typename: 'org.dxos.test.array-hidden', version: '0.1.0' }));
45
+ }).pipe(Type.makeObject(DXN.make('org.dxos.test.arrayHidden', '0.1.0')));
43
46
 
44
- const names = getFormProperties(TestSchema.ast).map((prop) => prop.name);
47
+ const names = getFormProperties(Type.getSchema(TestSchema).ast).map((prop) => prop.name);
45
48
  expect(names).toContain('name');
46
49
  expect(names).not.toContain('context');
47
50
  });
48
51
 
52
+ test('filters out optional fields annotated FormInputAnnotation.set(false)', ({ expect }) => {
53
+ // Regression: optional fields' `prop.type` is the union `T | undefined`; the
54
+ // annotation lives on the inner `T`. Without unwrapping, the filter would
55
+ // miss the annotation and the hidden field would render. Covers the
56
+ // conventional pattern `Schema.X.pipe(FormInputAnnotation.set(false), Schema.optional)`.
57
+ const TestSchema = Schema.Struct({
58
+ name: Schema.optional(Schema.String),
59
+ userId: Schema.String.pipe(Annotation.FormInputAnnotation.set(false), Schema.optional),
60
+ }).pipe(Type.makeObject(DXN.make('org.dxos.test.optionalHidden', '0.1.0')));
61
+
62
+ const names = getFormProperties(Type.getSchema(TestSchema).ast).map((prop) => prop.name);
63
+ expect(names).toContain('name');
64
+ expect(names).not.toContain('userId');
65
+ });
66
+
67
+ test('preserves a deeply-nested FormInputAnnotation through unions and arrays', ({ expect }) => {
68
+ // Regression: a hidden field nested below a discriminated union and arrays of structs (e.g.
69
+ // Segment.details(road).routes[].legs[].geometry). `encodedBoundAST` strips annotations from
70
+ // non-keyword inner types, so without keeping container types raw the annotation is lost by
71
+ // the time the form recurses to the leaf field. The transformation element (GeoPoint) is the
72
+ // trigger that forces the encode rebuild.
73
+ const Leg = Schema.Struct({
74
+ distance: Schema.Number,
75
+ geometry: Schema.Array(Format.GeoPoint).pipe(Annotation.FormInputAnnotation.set(false)),
76
+ });
77
+ const Route = Schema.Struct({ legs: Schema.Array(Leg) });
78
+ const TestSchema = Schema.Struct({
79
+ details: Schema.Union(
80
+ Schema.TaggedStruct('road', { routes: Schema.optional(Schema.Array(Route)) }),
81
+ Schema.TaggedStruct('other', { note: Schema.optional(Schema.String) }),
82
+ ),
83
+ }).pipe(Type.makeObject(DXN.make('org.dxos.test.nestedHidden', '0.1.0')));
84
+
85
+ // Drill as the form does: getFormProperties at each level, descending via the property type
86
+ // (array element / type literal) returned by the previous level.
87
+ const findTypeLiteralWith = (ast: SchemaAST.AST, prop: string) =>
88
+ findNode(
89
+ ast,
90
+ (node) => SchemaAST.isTypeLiteral(node) && SchemaAST.getPropertySignatures(node).some((p) => p.name === prop),
91
+ )!;
92
+ const propType = (ast: SchemaAST.AST, name: string) => getFormProperties(ast).find((p) => p.name === name)!.type;
93
+
94
+ const detailsType = propType(Type.getSchema(TestSchema).ast, 'details');
95
+ const roadTypeLiteral = findTypeLiteralWith(detailsType, 'routes');
96
+ const routeTypeLiteral = findNode(
97
+ getArrayElementType(propType(roadTypeLiteral, 'routes'))!,
98
+ SchemaAST.isTypeLiteral,
99
+ )!;
100
+ const legTypeLiteral = findNode(getArrayElementType(propType(routeTypeLiteral, 'legs'))!, SchemaAST.isTypeLiteral)!;
101
+
102
+ const names = getFormProperties(legTypeLiteral).map((prop) => prop.name);
103
+ expect(names).toContain('distance');
104
+ expect(names).not.toContain('geometry');
105
+ });
106
+
49
107
  test('preserves annotation when chained with .annotations()', ({ expect }) => {
50
108
  // Regression: `.pipe(FormInputAnnotation.set(false)).annotations({...})` must
51
109
  // not lose the form-input annotation.
@@ -54,9 +112,9 @@ describe('getFormProperties', () => {
54
112
  hidden: JsonSchema.JsonSchema.pipe(Annotation.FormInputAnnotation.set(false)).annotations({
55
113
  description: 'Hidden field',
56
114
  }),
57
- }).pipe(Type.object({ typename: 'org.dxos.test.chained-hidden', version: '0.1.0' }));
115
+ }).pipe(Type.makeObject(DXN.make('org.dxos.test.chainedHidden', '0.1.0')));
58
116
 
59
- const names = getFormProperties(TestSchema.ast).map((prop) => prop.name);
117
+ const names = getFormProperties(Type.getSchema(TestSchema).ast).map((prop) => prop.name);
60
118
  expect(names).toContain('name');
61
119
  expect(names).not.toContain('hidden');
62
120
  });
@@ -6,7 +6,20 @@ import * as Option from 'effect/Option';
6
6
  import * as SchemaAST from 'effect/SchemaAST';
7
7
 
8
8
  import { Annotation } from '@dxos/echo';
9
- import { type SchemaProperty, getProperties } from '@dxos/effect';
9
+ import { type SchemaProperty, getProperties, isArrayType, isNestedType } from '@dxos/effect';
10
+
11
+ /** The property's type with an optional `T | undefined` union unwrapped to its inner `T`. */
12
+ const unwrapOptional = (prop: SchemaAST.PropertySignature): SchemaAST.AST => {
13
+ if (!prop.isOptional || !SchemaAST.isUnion(prop.type)) {
14
+ return prop.type;
15
+ }
16
+ // Drop the `undefined` member, preserving the remaining union (don't collapse `A | B | undefined` to `A`).
17
+ const defined = prop.type.types.filter((type) => type._tag !== 'UndefinedKeyword');
18
+ if (defined.length === 0) {
19
+ return prop.type;
20
+ }
21
+ return defined.length === 1 ? defined[0] : SchemaAST.Union.make(defined, prop.type.annotations);
22
+ };
10
23
 
11
24
  /**
12
25
  * Get the property types of an AST and filter out properties that are not form inputs.
@@ -16,14 +29,33 @@ import { type SchemaProperty, getProperties } from '@dxos/effect';
16
29
  * annotations from non-keyword inner types (e.g., `Schema.Struct`, `Schema.Array`).
17
30
  * Without this, fields like `Routine.input` (a JsonSchema struct) annotated
18
31
  * `FormInputAnnotation.set(false)` would still render in the form.
32
+ *
33
+ * For optional fields, `prop.type` is the union `T | undefined` — the annotation
34
+ * lives on the inner `T`. Unwrap before reading so that the conventional pattern
35
+ * `Schema.String.pipe(FormInputAnnotation.set(false), Schema.optional)` works.
36
+ *
37
+ * Container properties (nested structs, unions, arrays) keep their *raw* AST rather than
38
+ * the `encodedBoundAST` produced by `getProperties`. `encodedBoundAST` strips annotations
39
+ * from non-keyword inner types, so a `FormInputAnnotation` nested beneath an encoded container
40
+ * (e.g. `RouteLeg.geometry`, reached via `Segment → details → routes → Route → legs`) would
41
+ * otherwise be lost by the time recursion reaches it. Leaf/transformation fields stay encoded
42
+ * (e.g. a `DateTime` still renders as its encoded string input).
19
43
  */
20
44
  export const getFormProperties = (ast: SchemaAST.AST): SchemaProperty[] => {
45
+ const signatures = SchemaAST.getPropertySignatures(ast);
46
+ const rawByName = new Map<PropertyKey, SchemaAST.AST>(signatures.map((prop) => [prop.name, unwrapOptional(prop)]));
21
47
  const hidden = new Set(
22
- SchemaAST.getPropertySignatures(ast)
48
+ signatures
23
49
  .filter(
24
- (prop) => Annotation.FormInputAnnotation.getFromAst(prop.type).pipe(Option.getOrElse(() => true)) === false,
50
+ (prop) =>
51
+ Annotation.FormInputAnnotation.getFromAst(unwrapOptional(prop)).pipe(Option.getOrElse(() => true)) === false,
25
52
  )
26
53
  .map((prop) => prop.name),
27
54
  );
28
- return getProperties(ast).filter((prop) => !hidden.has(prop.name));
55
+ return getProperties(ast)
56
+ .filter((prop) => !hidden.has(prop.name))
57
+ .map((prop) => {
58
+ const raw = rawByName.get(prop.name);
59
+ return raw && (isNestedType(raw) || isArrayType(raw)) ? { ...prop, type: raw } : prop;
60
+ });
29
61
  };
@@ -0,0 +1,10 @@
1
+ /// <reference types="vite/client" />
2
+
3
+ //
4
+ // Copyright 2026 DXOS.org
5
+ //
6
+
7
+ declare module '*?raw' {
8
+ const content: string;
9
+ export default content;
10
+ }