@dxos/react-ui-form 0.8.4-main.dedc0f3 → 0.8.4-main.dfabb4ec29

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 (234) hide show
  1. package/README.md +0 -1
  2. package/dist/lib/browser/index.mjs +2772 -2208
  3. package/dist/lib/browser/index.mjs.map +4 -4
  4. package/dist/lib/browser/meta.json +1 -1
  5. package/dist/lib/browser/translations.mjs +82 -0
  6. package/dist/lib/browser/translations.mjs.map +7 -0
  7. package/dist/lib/node-esm/index.mjs +2772 -2208
  8. package/dist/lib/node-esm/index.mjs.map +4 -4
  9. package/dist/lib/node-esm/meta.json +1 -1
  10. package/dist/lib/node-esm/translations.mjs +84 -0
  11. package/dist/lib/node-esm/translations.mjs.map +7 -0
  12. package/dist/types/src/components/FieldEditor/FieldEditor.d.ts +8 -7
  13. package/dist/types/src/components/FieldEditor/FieldEditor.d.ts.map +1 -1
  14. package/dist/types/src/components/FieldEditor/FieldEditor.stories.d.ts +69 -60
  15. package/dist/types/src/components/FieldEditor/FieldEditor.stories.d.ts.map +1 -1
  16. package/dist/types/src/components/Form/Form.d.ts +109 -26
  17. package/dist/types/src/components/Form/Form.d.ts.map +1 -1
  18. package/dist/types/src/components/Form/Form.stories.d.ts +116 -138
  19. package/dist/types/src/components/Form/Form.stories.d.ts.map +1 -1
  20. package/dist/types/src/components/Form/FormField.d.ts +43 -0
  21. package/dist/types/src/components/Form/FormField.d.ts.map +1 -0
  22. package/dist/types/src/components/Form/FormFieldComponent.d.ts +79 -0
  23. package/dist/types/src/components/Form/FormFieldComponent.d.ts.map +1 -0
  24. package/dist/types/src/components/Form/FormFieldComponent.stories.d.ts +9 -0
  25. package/dist/types/src/components/Form/FormFieldComponent.stories.d.ts.map +1 -0
  26. package/dist/types/src/components/Form/FormFieldSet.d.ts +15 -0
  27. package/dist/types/src/components/Form/FormFieldSet.d.ts.map +1 -0
  28. package/dist/types/src/components/Form/fields/ArrayField.d.ts +18 -0
  29. package/dist/types/src/components/Form/fields/ArrayField.d.ts.map +1 -0
  30. package/dist/types/src/components/Form/fields/BooleanField.d.ts +4 -0
  31. package/dist/types/src/components/Form/fields/BooleanField.d.ts.map +1 -0
  32. package/dist/types/src/components/Form/fields/GeoPointField.d.ts +5 -0
  33. package/dist/types/src/components/Form/fields/GeoPointField.d.ts.map +1 -0
  34. package/dist/types/src/components/Form/fields/MarkdownField.d.ts +11 -0
  35. package/dist/types/src/components/Form/fields/MarkdownField.d.ts.map +1 -0
  36. package/dist/types/src/components/Form/fields/MarkdownField.stories.d.ts +84 -0
  37. package/dist/types/src/components/Form/fields/MarkdownField.stories.d.ts.map +1 -0
  38. package/dist/types/src/components/Form/fields/NumberField.d.ts +4 -0
  39. package/dist/types/src/components/Form/fields/NumberField.d.ts.map +1 -0
  40. package/dist/types/src/components/Form/fields/RefField.d.ts +22 -0
  41. package/dist/types/src/components/Form/fields/RefField.d.ts.map +1 -0
  42. package/dist/types/src/components/Form/fields/SelectField.d.ts +10 -0
  43. package/dist/types/src/components/Form/fields/SelectField.d.ts.map +1 -0
  44. package/dist/types/src/components/Form/fields/SelectOptionField.d.ts +5 -0
  45. package/dist/types/src/components/Form/fields/SelectOptionField.d.ts.map +1 -0
  46. package/dist/types/src/components/Form/fields/SelectOptionField.stories.d.ts +84 -0
  47. package/dist/types/src/components/Form/fields/SelectOptionField.stories.d.ts.map +1 -0
  48. package/dist/types/src/components/Form/fields/TextAreaField.d.ts +4 -0
  49. package/dist/types/src/components/Form/fields/TextAreaField.d.ts.map +1 -0
  50. package/dist/types/src/components/Form/fields/TextField.d.ts +4 -0
  51. package/dist/types/src/components/Form/fields/TextField.d.ts.map +1 -0
  52. package/dist/types/src/components/Form/fields/TupleField.d.ts +6 -0
  53. package/dist/types/src/components/Form/fields/TupleField.d.ts.map +1 -0
  54. package/dist/types/src/components/Form/fields/index.d.ts +12 -0
  55. package/dist/types/src/components/Form/fields/index.d.ts.map +1 -0
  56. package/dist/types/src/components/Form/index.d.ts +2 -4
  57. package/dist/types/src/components/Form/index.d.ts.map +1 -1
  58. package/dist/types/src/components/ObjectForm/ObjectForm.d.ts +9 -0
  59. package/dist/types/src/components/ObjectForm/ObjectForm.d.ts.map +1 -0
  60. package/dist/types/src/components/ObjectForm/index.d.ts +2 -0
  61. package/dist/types/src/components/ObjectForm/index.d.ts.map +1 -0
  62. package/dist/types/src/components/ObjectPicker/ObjectPicker.d.ts +68 -0
  63. package/dist/types/src/components/ObjectPicker/ObjectPicker.d.ts.map +1 -0
  64. package/dist/types/src/components/ObjectPicker/ObjectPicker.stories.d.ts +87 -0
  65. package/dist/types/src/components/ObjectPicker/ObjectPicker.stories.d.ts.map +1 -0
  66. package/dist/types/src/components/ObjectPicker/index.d.ts +2 -0
  67. package/dist/types/src/components/ObjectPicker/index.d.ts.map +1 -0
  68. package/dist/types/src/components/ObjectProperties/ObjectProperties.d.ts +9 -0
  69. package/dist/types/src/components/ObjectProperties/ObjectProperties.d.ts.map +1 -0
  70. package/dist/types/src/components/ObjectProperties/ObjectProperties.stories.d.ts +127 -0
  71. package/dist/types/src/components/ObjectProperties/ObjectProperties.stories.d.ts.map +1 -0
  72. package/dist/types/src/components/ObjectProperties/ObjectProperties.test.d.ts +2 -0
  73. package/dist/types/src/components/ObjectProperties/ObjectProperties.test.d.ts.map +1 -0
  74. package/dist/types/src/components/ObjectProperties/index.d.ts +2 -0
  75. package/dist/types/src/components/ObjectProperties/index.d.ts.map +1 -0
  76. package/dist/types/src/components/Settings/Settings.d.ts +51 -0
  77. package/dist/types/src/components/Settings/Settings.d.ts.map +1 -0
  78. package/dist/types/src/components/Settings/Settings.stories.d.ts +84 -0
  79. package/dist/types/src/components/Settings/Settings.stories.d.ts.map +1 -0
  80. package/dist/types/src/components/Settings/SettingsFieldSet.d.ts +43 -0
  81. package/dist/types/src/components/Settings/SettingsFieldSet.d.ts.map +1 -0
  82. package/dist/types/src/components/Settings/SettingsItem.d.ts +16 -0
  83. package/dist/types/src/components/Settings/SettingsItem.d.ts.map +1 -0
  84. package/dist/types/src/components/Settings/SettingsSelectField.d.ts +18 -0
  85. package/dist/types/src/components/Settings/SettingsSelectField.d.ts.map +1 -0
  86. package/dist/types/src/components/Settings/index.d.ts +4 -0
  87. package/dist/types/src/components/Settings/index.d.ts.map +1 -0
  88. package/dist/types/src/components/ViewEditor/ViewEditor.d.ts +22 -11
  89. package/dist/types/src/components/ViewEditor/ViewEditor.d.ts.map +1 -1
  90. package/dist/types/src/components/ViewEditor/ViewEditor.stories.d.ts +72 -65
  91. package/dist/types/src/components/ViewEditor/ViewEditor.stories.d.ts.map +1 -1
  92. package/dist/types/src/components/index.d.ts +4 -1
  93. package/dist/types/src/components/index.d.ts.map +1 -1
  94. package/dist/types/src/components/testing.d.ts +4 -6
  95. package/dist/types/src/components/testing.d.ts.map +1 -1
  96. package/dist/types/src/hooks/index.d.ts +2 -3
  97. package/dist/types/src/hooks/index.d.ts.map +1 -1
  98. package/dist/types/src/hooks/useFormHandler.d.ts +82 -0
  99. package/dist/types/src/hooks/useFormHandler.d.ts.map +1 -0
  100. package/dist/types/src/hooks/useKeyHandler.d.ts +7 -0
  101. package/dist/types/src/hooks/useKeyHandler.d.ts.map +1 -0
  102. package/dist/types/src/index.d.ts +0 -2
  103. package/dist/types/src/index.d.ts.map +1 -1
  104. package/dist/types/src/translations.d.ts +65 -59
  105. package/dist/types/src/translations.d.ts.map +1 -1
  106. package/dist/types/src/util/field-type.d.ts +19 -0
  107. package/dist/types/src/util/field-type.d.ts.map +1 -0
  108. package/dist/types/src/util/field.d.ts +3 -3
  109. package/dist/types/src/util/field.d.ts.map +1 -1
  110. package/dist/types/src/util/formatting.d.ts +2 -2
  111. package/dist/types/src/util/formatting.d.ts.map +1 -1
  112. package/dist/types/src/util/index.d.ts +2 -1
  113. package/dist/types/src/util/index.d.ts.map +1 -1
  114. package/dist/types/src/util/properties.d.ts +7 -0
  115. package/dist/types/src/util/properties.d.ts.map +1 -0
  116. package/dist/types/src/util/refs.d.ts +3 -3
  117. package/dist/types/src/util/refs.d.ts.map +1 -1
  118. package/dist/types/tsconfig.tsbuildinfo +1 -1
  119. package/package.json +56 -50
  120. package/src/components/FieldEditor/FieldEditor.stories.tsx +46 -26
  121. package/src/components/FieldEditor/FieldEditor.test.tsx +0 -1
  122. package/src/components/FieldEditor/FieldEditor.tsx +80 -80
  123. package/src/components/Form/AUDIT.md +17 -0
  124. package/src/components/Form/Form.stories.tsx +142 -254
  125. package/src/components/Form/Form.tsx +375 -99
  126. package/src/components/Form/FormField.tsx +306 -0
  127. package/src/components/Form/FormFieldComponent.stories.tsx +65 -0
  128. package/src/components/Form/FormFieldComponent.tsx +174 -0
  129. package/src/components/Form/FormFieldSet.tsx +144 -0
  130. package/src/components/Form/README.md +3 -0
  131. package/src/components/Form/fields/ArrayField.tsx +155 -0
  132. package/src/components/Form/fields/BooleanField.tsx +22 -0
  133. package/src/components/Form/fields/GeoPointField.tsx +121 -0
  134. package/src/components/Form/fields/MarkdownField.stories.tsx +206 -0
  135. package/src/components/Form/fields/MarkdownField.tsx +168 -0
  136. package/src/components/Form/fields/NumberField.tsx +39 -0
  137. package/src/components/Form/fields/RefField.tsx +229 -0
  138. package/src/components/Form/fields/SelectField.tsx +65 -0
  139. package/src/components/Form/fields/SelectOptionField.stories.tsx +66 -0
  140. package/src/components/Form/{custom/SelectOptionsInput.tsx → fields/SelectOptionField.tsx} +31 -25
  141. package/src/components/Form/fields/TextAreaField.tsx +38 -0
  142. package/src/components/Form/fields/TextField.tsx +38 -0
  143. package/src/components/Form/{custom/TupleInput.tsx → fields/TupleField.tsx} +16 -9
  144. package/src/components/Form/fields/index.ts +15 -0
  145. package/src/components/Form/index.ts +3 -4
  146. package/src/components/ObjectForm/ObjectForm.tsx +114 -0
  147. package/src/components/{ControlSection → ObjectForm}/index.ts +1 -1
  148. package/src/components/ObjectPicker/ObjectPicker.stories.tsx +212 -0
  149. package/src/components/ObjectPicker/ObjectPicker.tsx +202 -0
  150. package/src/components/ObjectPicker/index.ts +5 -0
  151. package/src/components/ObjectProperties/ObjectProperties.stories.tsx +362 -0
  152. package/src/components/ObjectProperties/ObjectProperties.test.tsx +101 -0
  153. package/src/components/ObjectProperties/ObjectProperties.tsx +141 -0
  154. package/src/components/ObjectProperties/index.ts +5 -0
  155. package/src/components/Settings/AUDIT.md +52 -0
  156. package/src/components/Settings/Settings.stories.tsx +104 -0
  157. package/src/components/Settings/Settings.tsx +80 -0
  158. package/src/components/Settings/SettingsFieldSet.tsx +257 -0
  159. package/src/components/Settings/SettingsItem.tsx +58 -0
  160. package/src/components/Settings/SettingsSelectField.tsx +59 -0
  161. package/src/components/Settings/index.ts +7 -0
  162. package/src/components/ViewEditor/ViewEditor.stories.tsx +126 -75
  163. package/src/components/ViewEditor/ViewEditor.test.tsx +10 -9
  164. package/src/components/ViewEditor/ViewEditor.tsx +326 -195
  165. package/src/components/index.ts +4 -1
  166. package/src/components/testing.tsx +24 -11
  167. package/src/hooks/index.ts +2 -3
  168. package/src/hooks/useFormHandler.ts +380 -0
  169. package/src/hooks/useKeyHandler.tsx +37 -0
  170. package/src/index.ts +0 -4
  171. package/src/translations.ts +70 -64
  172. package/src/util/field-type.ts +60 -0
  173. package/src/util/field.ts +25 -25
  174. package/src/util/formatting.ts +22 -22
  175. package/src/util/index.ts +2 -1
  176. package/src/util/properties.ts +18 -0
  177. package/src/util/refs.ts +17 -19
  178. package/dist/types/src/components/ControlSection/ControlSection.d.ts +0 -23
  179. package/dist/types/src/components/ControlSection/ControlSection.d.ts.map +0 -1
  180. package/dist/types/src/components/ControlSection/ControlSection.stories.d.ts +0 -83
  181. package/dist/types/src/components/ControlSection/ControlSection.stories.d.ts.map +0 -1
  182. package/dist/types/src/components/ControlSection/index.d.ts +0 -2
  183. package/dist/types/src/components/ControlSection/index.d.ts.map +0 -1
  184. package/dist/types/src/components/Form/ArrayField.d.ts +0 -16
  185. package/dist/types/src/components/Form/ArrayField.d.ts.map +0 -1
  186. package/dist/types/src/components/Form/Defaults.d.ts +0 -14
  187. package/dist/types/src/components/Form/Defaults.d.ts.map +0 -1
  188. package/dist/types/src/components/Form/FormActions.d.ts +0 -9
  189. package/dist/types/src/components/Form/FormActions.d.ts.map +0 -1
  190. package/dist/types/src/components/Form/FormContent.d.ts +0 -63
  191. package/dist/types/src/components/Form/FormContent.d.ts.map +0 -1
  192. package/dist/types/src/components/Form/FormContext.d.ts +0 -23
  193. package/dist/types/src/components/Form/FormContext.d.ts.map +0 -1
  194. package/dist/types/src/components/Form/Input.d.ts +0 -29
  195. package/dist/types/src/components/Form/Input.d.ts.map +0 -1
  196. package/dist/types/src/components/Form/RefField.d.ts +0 -18
  197. package/dist/types/src/components/Form/RefField.d.ts.map +0 -1
  198. package/dist/types/src/components/Form/custom/GeoPointInput.d.ts +0 -4
  199. package/dist/types/src/components/Form/custom/GeoPointInput.d.ts.map +0 -1
  200. package/dist/types/src/components/Form/custom/SelectOptionsInput.d.ts +0 -4
  201. package/dist/types/src/components/Form/custom/SelectOptionsInput.d.ts.map +0 -1
  202. package/dist/types/src/components/Form/custom/SelectOptionsInput.stories.d.ts +0 -78
  203. package/dist/types/src/components/Form/custom/SelectOptionsInput.stories.d.ts.map +0 -1
  204. package/dist/types/src/components/Form/custom/TupleInput.d.ts +0 -6
  205. package/dist/types/src/components/Form/custom/TupleInput.d.ts.map +0 -1
  206. package/dist/types/src/components/Form/custom/index.d.ts +0 -4
  207. package/dist/types/src/components/Form/custom/index.d.ts.map +0 -1
  208. package/dist/types/src/components/Form/factory.d.ts +0 -8
  209. package/dist/types/src/components/Form/factory.d.ts.map +0 -1
  210. package/dist/types/src/hooks/useForm.d.ts +0 -67
  211. package/dist/types/src/hooks/useForm.d.ts.map +0 -1
  212. package/dist/types/src/hooks/useQueryRefOptions.d.ts +0 -23
  213. package/dist/types/src/hooks/useQueryRefOptions.d.ts.map +0 -1
  214. package/dist/types/src/hooks/useRefQueryLookupHandler.d.ts +0 -8
  215. package/dist/types/src/hooks/useRefQueryLookupHandler.d.ts.map +0 -1
  216. package/dist/types/src/util/schema.d.ts +0 -3
  217. package/dist/types/src/util/schema.d.ts.map +0 -1
  218. package/src/components/ControlSection/ControlSection.stories.tsx +0 -39
  219. package/src/components/ControlSection/ControlSection.tsx +0 -153
  220. package/src/components/Form/ArrayField.tsx +0 -124
  221. package/src/components/Form/Defaults.tsx +0 -205
  222. package/src/components/Form/FormActions.tsx +0 -54
  223. package/src/components/Form/FormContent.tsx +0 -302
  224. package/src/components/Form/FormContext.tsx +0 -110
  225. package/src/components/Form/Input.tsx +0 -54
  226. package/src/components/Form/RefField.tsx +0 -299
  227. package/src/components/Form/custom/GeoPointInput.tsx +0 -80
  228. package/src/components/Form/custom/SelectOptionsInput.stories.tsx +0 -62
  229. package/src/components/Form/custom/index.ts +0 -7
  230. package/src/components/Form/factory.tsx +0 -31
  231. package/src/hooks/useForm.ts +0 -293
  232. package/src/hooks/useQueryRefOptions.ts +0 -53
  233. package/src/hooks/useRefQueryLookupHandler.ts +0 -39
  234. package/src/util/schema.ts +0 -14
@@ -2,122 +2,262 @@
2
2
  // Copyright 2024 DXOS.org
3
3
  //
4
4
 
5
- import { Match, Schema } from 'effect';
6
- import React, { useCallback, useMemo, useState } from 'react';
5
+ import { RegistryContext } from '@effect-atom/atom-react';
6
+ import * as Array from 'effect/Array';
7
+ import * as Match from 'effect/Match';
8
+ import * as Option from 'effect/Option';
9
+ import * as Schema from 'effect/Schema';
10
+ import React, { forwardRef, useCallback, useContext, useImperativeHandle, useMemo, useState } from 'react';
7
11
 
8
- import { type SchemaRegistry } from '@dxos/echo-db';
9
- import { EchoSchema, Format, type JsonProp, isMutable, toJsonSchema } from '@dxos/echo-schema';
12
+ import { Entity, Feed, Filter, Format, Obj, Query, QueryAST, Ref, type SchemaRegistry } from '@dxos/echo';
13
+ import { View } from '@dxos/echo';
14
+ import { EchoSchema, type JsonProp, isMutable, toJsonSchema } from '@dxos/echo/internal';
10
15
  import { invariant } from '@dxos/invariant';
11
- import { getSpace } from '@dxos/react-client/echo';
12
- import { Callout, IconButton, type ThemedClassName, useTranslation } from '@dxos/react-ui';
13
- import { QueryEditor, QuerySerializer, createExpression } from '@dxos/react-ui-components';
16
+ import { useObject, useQuery } from '@dxos/react-client/echo';
17
+ import { IconButton, Input, Message, type ThemedClassName, useTranslation } from '@dxos/react-ui';
18
+ import { QueryForm, type QueryFormProps } from '@dxos/react-ui-components';
14
19
  import { List } from '@dxos/react-ui-list';
15
- import { cardSpacing } from '@dxos/react-ui-stack';
16
- import { inputTextLabel, mx, subtleHover } from '@dxos/react-ui-theme';
17
20
  import {
18
- type DataType,
19
- FieldSchema,
20
- type FieldType,
21
+ ParentLabelAnnotation,
21
22
  ProjectionModel,
22
23
  VIEW_FIELD_LIMIT,
23
- typenameFromQuery,
24
+ createEchoChangeCallback,
25
+ getTypenameFromQuery,
24
26
  } from '@dxos/schema';
27
+ import { mx, osTranslations, subtleHover } from '@dxos/ui-theme';
25
28
 
26
- import { translationKey } from '../../translations';
27
- import { FieldEditor } from '../FieldEditor';
28
- import { Form, type FormProps, type InputComponent, type InputProps } from '../Form';
29
+ import { translationKey } from '#translations';
29
30
 
30
- const listGrid = 'grid grid-cols-[min-content_1fr_min-content_min-content_min-content]';
31
- const listItemGrid = 'grid grid-cols-subgrid col-span-5';
31
+ import { FieldEditor } from '../FieldEditor';
32
+ import {
33
+ Form,
34
+ type FormFieldComponent,
35
+ type FormFieldComponentProps,
36
+ FormFieldLabel,
37
+ type FormFieldMap,
38
+ type FormRootProps,
39
+ } from '../Form';
32
40
 
33
41
  export type ViewEditorProps = ThemedClassName<
34
42
  {
35
43
  schema: Schema.Schema.AnyNoContext;
36
- view: DataType.View;
37
- kind?: 'basic' | 'advanced';
38
- registry?: SchemaRegistry;
39
- readonly?: boolean;
44
+ view: View.View;
45
+ mode?: 'schema' | 'tag';
46
+ registry?: SchemaRegistry.SchemaRegistry;
40
47
  showHeading?: boolean;
41
- onQueryChanged?: (query: string) => void;
48
+ onQueryChanged?: (query: QueryAST.Query, target?: string) => void;
42
49
  onDelete?: (fieldId: string) => void;
43
- } & Pick<FormProps<any>, 'outerSpacing'>
50
+ } & Pick<QueryFormProps, 'types' | 'tags'> &
51
+ Pick<FormRootProps<any>, 'readonly' | 'db'>
44
52
  >;
45
53
 
46
54
  /**
47
55
  * Schema-based object form.
48
56
  */
49
- export const ViewEditor = ({
50
- classNames,
51
- schema,
52
- view,
53
- kind = 'basic',
54
- registry,
55
- readonly,
56
- showHeading = false,
57
- onQueryChanged,
58
- onDelete,
59
- outerSpacing = true,
60
- }: ViewEditorProps) => {
61
- const space = getSpace(view);
57
+ export const ViewEditor = forwardRef<ProjectionModel, ViewEditorProps>(
58
+ (
59
+ {
60
+ classNames,
61
+ schema,
62
+ view,
63
+ mode = 'schema',
64
+ registry,
65
+ db,
66
+ readonly,
67
+ showHeading = false,
68
+ types,
69
+ tags,
70
+ onQueryChanged,
71
+ onDelete,
72
+ },
73
+ forwardedRef,
74
+ ) => {
75
+ const atomRegistry = useContext(RegistryContext);
76
+ const schemaReadonly = !isMutable(schema);
77
+ const { t } = useTranslation(translationKey);
78
+
79
+ const projectionModel = useMemo(() => {
80
+ // Use reactive and mutable version of json schema when schema is mutable.
81
+ const jsonSchema = schema instanceof EchoSchema ? schema.jsonSchema : toJsonSchema(schema);
82
+
83
+ // Always use createEchoChangeCallback since the view is ECHO-backed.
84
+ // Pass schema only when mutable to allow schema mutations.
85
+ const change = createEchoChangeCallback(view, schema instanceof EchoSchema ? schema : undefined);
86
+
87
+ const model = new ProjectionModel({
88
+ registry: atomRegistry,
89
+ view,
90
+ baseSchema: jsonSchema,
91
+ change,
92
+ });
93
+
94
+ return model;
95
+ }, [atomRegistry, schema, view]);
96
+
97
+ useImperativeHandle(forwardedRef, () => projectionModel, [projectionModel]);
98
+
99
+ const queueTarget = Match.value(view.query.ast).pipe(
100
+ Match.when({ type: 'from' }, ({ from }) => {
101
+ if (from._tag !== 'scope') {
102
+ return undefined;
103
+ }
104
+ return Option.fromNullable(from.scope.queues).pipe(
105
+ Option.flatMap((queues) => Array.head(queues)),
106
+ Option.map(String),
107
+ Option.getOrUndefined,
108
+ );
109
+ }),
110
+ Match.orElse(() => undefined),
111
+ );
112
+
113
+ const feeds = useQuery(db, Filter.type(Feed.Feed));
114
+
115
+ const targetRef = useMemo(() => {
116
+ if (!queueTarget) {
117
+ return undefined;
118
+ }
119
+ const feed = feeds.find((feed) => Feed.getQueueDxn(feed)?.toString() === queueTarget);
120
+ return feed ? Ref.fromDXN(Entity.getDXN(feed)) : undefined;
121
+ }, [queueTarget, feeds]);
122
+
123
+ const viewSchema = useMemo(() => {
124
+ const base = Schema.Struct({
125
+ query:
126
+ mode === 'schema'
127
+ ? Format.URL.annotations({ title: 'Record type' })
128
+ : QueryAST.Query.annotations({ title: 'Query' }),
129
+ });
130
+
131
+ if (mode === 'tag') {
132
+ return Schema.Struct({
133
+ ...base.fields,
134
+ // TODO(wittjosiah): Replace Type.Feed with Dataset.Dataset when Ref.Ref supports unions.
135
+ target: Ref.Ref(Feed.Feed).pipe(
136
+ Schema.annotations({ title: 'Target Feed' }),
137
+ ParentLabelAnnotation.set(true),
138
+ Schema.optional,
139
+ ),
140
+ }).pipe(Schema.mutable);
141
+ }
142
+
143
+ return base.pipe(Schema.mutable);
144
+ }, [mode]);
145
+
146
+ // TODO(burdon): Need to warn user of possible consequences of editing.
147
+ // TODO(burdon): Settings should have domain name owned by user.
148
+ const viewValues = useMemo(
149
+ () => ({
150
+ query: mode === 'schema' ? getTypenameFromQuery(view.query.ast) : view.query.ast,
151
+ target: targetRef,
152
+ }),
153
+ [mode, view.query.ast, targetRef],
154
+ );
155
+
156
+ const fieldMap = useMemo<FormFieldMap | undefined>(
157
+ () => (mode === 'tag' ? customFields({ types, tags }) : undefined),
158
+ [mode, types, tags],
159
+ );
160
+
161
+ const handleUpdate = useCallback(
162
+ (values: any) => {
163
+ const targetValue = values.target;
164
+ let queueDxn: string | undefined;
165
+
166
+ if (Ref.isRef(targetValue)) {
167
+ const feedDxn = targetValue.dxn.toString();
168
+ const feed = feeds.find((feed) => Obj.getDXN(feed).toString() === feedDxn);
169
+ if (feed) {
170
+ queueDxn = Feed.getQueueDxn(feed)?.toString();
171
+ }
172
+ }
173
+
174
+ // TODO(wittjosiah): Deep-clone the AST to plain JS or ECHO proxy arrays become objects with numeric keys.
175
+ const query =
176
+ mode === 'schema'
177
+ ? Query.select(Filter.typename(values.query)).ast
178
+ : JSON.parse(JSON.stringify(values.query));
179
+ onQueryChanged?.(query, queueDxn);
180
+ },
181
+ [onQueryChanged, mode, feeds],
182
+ );
183
+
184
+ const handleDelete = useCallback(
185
+ (fieldId: string) => {
186
+ invariant(!readonly);
187
+ onDelete?.(fieldId);
188
+ },
189
+ [onDelete, readonly],
190
+ );
191
+
192
+ return (
193
+ <div className={mx(classNames)}>
194
+ {/* If readonly is set, then the callout is not needed. */}
195
+ {schemaReadonly && !readonly && (
196
+ <Message.Root valence='info' classNames='my-form-padding'>
197
+ <Message.Title>{t('system-schema.description')}</Message.Title>
198
+ </Message.Root>
199
+ )}
200
+
201
+ {/* TODO(burdon): Is the form read-only or just the schema? */}
202
+ <Form.Root schema={viewSchema} values={viewValues} fieldMap={fieldMap} db={db} onValuesChanged={handleUpdate}>
203
+ <Form.FieldSet />
204
+
205
+ <FormFieldLabel label={t('fields.label')} asChild />
206
+ <FieldList
207
+ schema={schema}
208
+ view={view}
209
+ registry={registry}
210
+ readonly={readonly}
211
+ showHeading={showHeading}
212
+ onDelete={handleDelete}
213
+ />
214
+ </Form.Root>
215
+ </div>
216
+ );
217
+ },
218
+ );
219
+
220
+ type FieldListProps = Pick<ViewEditorProps, 'schema' | 'view' | 'registry' | 'readonly' | 'showHeading' | 'onDelete'>;
221
+
222
+ const FieldList = ({ schema, view, registry, readonly, showHeading = false, onDelete }: FieldListProps) => {
223
+ const atomRegistry = useContext(RegistryContext);
62
224
  const schemaReadonly = !isMutable(schema);
63
225
  const { t } = useTranslation(translationKey);
226
+
227
+ // Subscribe to view changes for reactivity.
228
+ const [viewSnapshot] = useObject(view);
229
+
64
230
  const projectionModel = useMemo(() => {
65
231
  // Use reactive and mutable version of json schema when schema is mutable.
66
232
  const jsonSchema = schema instanceof EchoSchema ? schema.jsonSchema : toJsonSchema(schema);
67
- return new ProjectionModel(jsonSchema, view.projection);
68
- }, [schema, view.projection]);
69
- const [expandedField, setExpandedField] = useState<FieldType['id']>();
70
-
71
- const serializedQuery = Match.value(kind).pipe(
72
- Match.when('basic', () => typenameFromQuery(view.query)),
73
- Match.when('advanced', () => {
74
- if (view.query.type !== 'select') {
75
- return '';
76
- }
77
233
 
78
- const serializer = new QuerySerializer();
79
- return serializer.serialize(createExpression(view.query.filter));
80
- }),
81
- Match.exhaustive,
82
- );
234
+ // Always use createEchoChangeCallback since the view is ECHO-backed.
235
+ // Pass schema only when mutable to allow schema mutations.
236
+ const change = createEchoChangeCallback(view, schema instanceof EchoSchema ? schema : undefined);
83
237
 
84
- const viewSchema = useMemo(() => {
85
- return Schema.Struct({
86
- query:
87
- kind === 'basic'
88
- ? Format.URL.annotations({ title: 'Record type' })
89
- : Schema.String.annotations({ title: 'Query' }),
90
- }).pipe(Schema.mutable);
91
- }, [kind]);
92
- // TODO(burdon): Need to warn user of possible consequences of editing.
93
- // TODO(burdon): Settings should have domain name owned by user.
94
- const viewValues = useMemo(() => ({ query: serializedQuery }), [serializedQuery]);
238
+ const model = new ProjectionModel({
239
+ registry: atomRegistry,
240
+ view,
241
+ baseSchema: jsonSchema,
242
+ change,
243
+ });
95
244
 
96
- const handleToggleField = useCallback(
97
- (field: FieldType) => {
98
- setExpandedField((prevExpandedFieldId) => (prevExpandedFieldId === field.id ? undefined : field.id));
99
- },
100
- [readonly],
101
- );
245
+ return model;
246
+ }, [atomRegistry, schema, view]);
102
247
 
103
- // TODO(burdon): Check if mutable; variant of useCallback that return undefined if readonly?
248
+ const [expandedField, setExpandedField] = useState<View.FieldType['id']>();
104
249
 
105
250
  const handleAdd = useCallback(() => {
106
251
  invariant(!readonly);
107
252
  const field = projectionModel.createFieldProjection();
108
253
  setExpandedField(field.id);
109
- }, [schema, projectionModel, readonly]);
254
+ }, [projectionModel, readonly]);
110
255
 
111
- const handleUpdate = useCallback(
112
- ({ query }: Schema.Schema.Type<typeof viewSchema>) => {
113
- invariant(!readonly);
114
- requestAnimationFrame(() => {
115
- if (serializedQuery !== query && !readonly) {
116
- onQueryChanged?.(query);
117
- }
118
- });
256
+ const handleToggleField = useCallback(
257
+ (field: View.FieldType) => {
258
+ setExpandedField((prevExpandedFieldId) => (prevExpandedFieldId === field.id ? undefined : field.id));
119
259
  },
120
- [serializedQuery, onQueryChanged, readonly],
260
+ [readonly],
121
261
  );
122
262
 
123
263
  const handleDelete = useCallback(
@@ -135,13 +275,15 @@ export const ViewEditor = ({
135
275
  const handleMove = useCallback(
136
276
  (fromIndex: number, toIndex: number) => {
137
277
  invariant(!readonly);
138
- // NOTE(ZaymonFC): Using arrayMove here causes a race condition with the kanban model.
139
- const fields = [...view.projection.fields];
140
- const [moved] = fields.splice(fromIndex, 1);
141
- fields.splice(toIndex, 0, moved);
142
- view.projection.fields = fields;
278
+ Obj.update(view, (view) => {
279
+ // NOTE(ZaymonFC): Using arrayMove here causes a race condition with the kanban model.
280
+ const fields = [...view.projection.fields];
281
+ const [moved] = fields.splice(fromIndex, 1);
282
+ fields.splice(toIndex, 0, moved);
283
+ view.projection.fields = fields;
284
+ });
143
285
  },
144
- [view.projection.fields, readonly],
286
+ [view, readonly],
145
287
  );
146
288
 
147
289
  const handleClose = useCallback(() => setExpandedField(undefined), []);
@@ -162,128 +304,117 @@ export const ViewEditor = ({
162
304
  [projectionModel],
163
305
  );
164
306
 
165
- const custom: Partial<Record<string, InputComponent>> = useMemo(
166
- () =>
167
- kind === 'advanced'
168
- ? {
169
- ['query' satisfies keyof Schema.Schema.Type<typeof viewSchema>]: (props: InputProps) => {
170
- const handleChange = useCallback(
171
- (text: string) => props.onValueChange('string', text),
172
- [props.onValueChange],
173
- );
174
-
175
- // TODO(wittjosiah): Add label & input styles.
176
- return <QueryEditor space={space} initialValue={props.getValue()} onChange={handleChange} />;
177
- },
178
- }
179
- : {},
180
- [],
181
- );
307
+ if (!viewSnapshot) {
308
+ return null;
309
+ }
182
310
 
183
311
  return (
184
- <div role='none' className={mx(classNames)}>
185
- {schemaReadonly && (
186
- <Callout.Root valence='info' classNames={['mlb-cardSpacingBlock', outerSpacing && 'mli-cardSpacingInline']}>
187
- <Callout.Title>{t('system schema description')}</Callout.Title>
188
- </Callout.Root>
189
- )}
190
-
191
- {/* TODO(burdon): Is the form read-only or just the schema? */}
192
- {/* TODO(burdon): Readonly fields should take up the same space as editable fields (just be ghosted). */}
193
- <Form<Schema.Schema.Type<typeof viewSchema>>
194
- autoSave
195
- schema={viewSchema}
196
- values={viewValues}
197
- readonly={readonly ? 'disabled-input' : false}
198
- onSave={handleUpdate}
199
- outerSpacing={outerSpacing}
200
- Custom={custom}
201
- />
202
-
203
- <div role='none' className={outerSpacing ? cardSpacing : 'mlb-cardSpacingBlock'}>
204
- <h2 className={mx(inputTextLabel)}>{t('fields label')}</h2>
205
-
206
- <List.Root<FieldType>
207
- items={view.projection.fields}
208
- isItem={Schema.is(FieldSchema)}
209
- getId={(field) => field.id}
210
- onMove={readonly ? undefined : handleMove}
211
- readonly={readonly}
212
- >
213
- {({ items: fields }) => (
214
- <>
215
- {showHeading && <h3 className='text-sm'>{t('field path label')}</h3>}
216
- <div role='list' className={listGrid}>
217
- {fields?.map((field) => {
218
- const hidden = field.visible === false;
219
- return (
220
- <List.Item<FieldType>
221
- key={field.id}
222
- item={field}
223
- classNames={listItemGrid}
224
- aria-expanded={expandedField === field.id}
225
- >
226
- <div role='none' className={mx(subtleHover, listItemGrid, 'rounded-sm cursor-pointer min-bs-10')}>
227
- <List.ItemDragHandle disabled={readonly} />
228
- <List.ItemTitle classNames={hidden && 'text-subdued'} onClick={() => handleToggleField(field)}>
229
- {field.path}
230
- </List.ItemTitle>
231
- <List.ItemButton
232
- label={t(hidden ? 'show field label' : 'hide field label')}
233
- data-testid={hidden ? 'show-field-button' : 'hide-field-button'}
234
- icon={hidden ? 'ph--eye-closed--regular' : 'ph--eye--regular'}
235
- autoHide={false}
236
- disabled={readonly || (!hidden && projectionModel.fields.length <= 1)}
237
- onClick={() => (hidden ? handleShow(field.path) : handleHide(field.id))}
238
- />
239
- {/* TODO(burdon): Remove unless implement undo. */}
312
+ <List.Root<View.FieldType>
313
+ items={viewSnapshot.projection.fields as View.FieldType[]}
314
+ isItem={Schema.is(View.FieldSchema)}
315
+ getId={(field) => field.id}
316
+ onMove={readonly ? undefined : handleMove}
317
+ readonly={readonly}
318
+ >
319
+ {({ items: fields }) => (
320
+ <>
321
+ {showHeading && <h3 className='text-sm'>{t('field-path.label')}</h3>}
322
+ <div role='list' className='grid grid-cols-[min-content_1fr_min-content_min-content_min-content]'>
323
+ {fields?.map((field) => {
324
+ const hidden = field.visible === false;
325
+ return (
326
+ <List.Item<View.FieldType>
327
+ key={field.id}
328
+ item={field}
329
+ classNames={'grid grid-cols-subgrid col-span-5'}
330
+ aria-expanded={expandedField === field.id}
331
+ >
332
+ <div
333
+ className={mx(
334
+ subtleHover,
335
+ 'grid grid-cols-subgrid col-span-5',
336
+ 'rounded-xs cursor-pointer min-h-10',
337
+ )}
338
+ >
339
+ <List.ItemDragHandle disabled={readonly || schemaReadonly} />
340
+ <List.ItemTitle classNames={hidden && 'text-subdued'} onClick={() => handleToggleField(field)}>
341
+ {field.path}
342
+ </List.ItemTitle>
343
+ <List.ItemIconButton
344
+ label={t(hidden ? 'show-field.label' : 'hide-field.label')}
345
+ data-testid={hidden ? 'show-field-button' : 'hide-field-button'}
346
+ icon={hidden ? 'ph--eye-closed--regular' : 'ph--eye--regular'}
347
+ autoHide={false}
348
+ disabled={readonly || (!hidden && projectionModel.getFields().length <= 1)}
349
+ onClick={() => (hidden ? handleShow(field.path) : handleHide(field.id))}
350
+ />
351
+ {!readonly && (
352
+ <>
240
353
  <List.ItemDeleteButton
241
- label={t('delete field label')}
354
+ label={t('delete-field.label')}
242
355
  autoHide={false}
243
- disabled={readonly || schemaReadonly || view.projection.fields.length <= 1}
356
+ disabled={readonly || schemaReadonly || viewSnapshot.projection.fields.length <= 1}
244
357
  onClick={() => handleDelete(field.id)}
245
358
  data-testid='field.delete'
246
359
  />
247
- <IconButton
360
+ <List.ItemIconButton
248
361
  iconOnly
249
362
  variant='ghost'
250
- label={t('toggle expand label', { ns: 'os' })}
363
+ label={t('toggle-expand.label', { ns: osTranslations })}
251
364
  icon={expandedField === field.id ? 'ph--caret-down--regular' : 'ph--caret-right--regular'}
252
365
  onClick={() => handleToggleField(field)}
366
+ data-testid='field.toggle'
253
367
  />
254
- </div>
255
- {expandedField === field.id && (
256
- <div role='none' className='col-span-5 mbs-1 mbe-1 border border-separator rounded-md'>
257
- <FieldEditor
258
- readonly={readonly || schemaReadonly ? 'disabled-input' : false}
259
- projection={projectionModel}
260
- field={field}
261
- registry={registry}
262
- onSave={handleClose}
263
- />
264
- </div>
265
- )}
266
- </List.Item>
267
- );
268
- })}
269
- </div>
270
- </>
368
+ </>
369
+ )}
370
+ </div>
371
+ {expandedField === field.id && !readonly && (
372
+ <div className='col-span-full mt-1 mb-1 border border-separator rounded-md'>
373
+ <FieldEditor
374
+ readonly={readonly || schemaReadonly}
375
+ registry={registry}
376
+ projection={projectionModel}
377
+ field={field}
378
+ onSave={handleClose}
379
+ />
380
+ </div>
381
+ )}
382
+ </List.Item>
383
+ );
384
+ })}
385
+ </div>
386
+ {!readonly && !expandedField && (
387
+ <div className='my-form-padding'>
388
+ <IconButton
389
+ icon='ph--plus--regular'
390
+ label={t('add-property-button.label')}
391
+ onClick={handleAdd}
392
+ disabled={viewSnapshot.projection.fields.length >= VIEW_FIELD_LIMIT}
393
+ classNames='w-full'
394
+ />
395
+ </div>
271
396
  )}
272
- </List.Root>
273
- </div>
274
-
275
- {!readonly && !expandedField && (
276
- <div role='none' className={outerSpacing ? cardSpacing : 'mlb-cardSpacingBlock'}>
277
- <IconButton
278
- icon='ph--plus--regular'
279
- label={t('button add property')}
280
- onClick={readonly ? undefined : handleAdd}
281
- // TODO(burdon): Show field limit in ux (not tooltip).
282
- disabled={view.projection.fields.length >= VIEW_FIELD_LIMIT}
283
- classNames='is-full'
284
- />
285
- </div>
397
+ </>
286
398
  )}
287
- </div>
399
+ </List.Root>
288
400
  );
289
401
  };
402
+
403
+ const customFields = ({
404
+ types,
405
+ tags,
406
+ }: Pick<ViewEditorProps, 'types' | 'tags'>): Record<string, FormFieldComponent> => ({
407
+ query: ({ type, readonly, label, getValue, onValueChange }: FormFieldComponentProps) => {
408
+ const handleChange = useCallback<NonNullable<QueryFormProps['onChange']>>(
409
+ (query) => onValueChange(type, query.ast),
410
+ [onValueChange],
411
+ );
412
+
413
+ return (
414
+ <Input.Root>
415
+ <FormFieldLabel readonly={readonly} label={label} />
416
+ <QueryForm initialQuery={getValue()} types={types} tags={tags} onChange={handleChange} />
417
+ </Input.Root>
418
+ );
419
+ },
420
+ });
@@ -2,7 +2,10 @@
2
2
  // Copyright 2024 DXOS.org
3
3
  //
4
4
 
5
- export * from './ControlSection';
6
5
  export * from './FieldEditor';
7
6
  export * from './Form';
7
+ export * from './ObjectForm';
8
+ export * from './ObjectPicker';
9
+ export * from './ObjectProperties';
10
+ export * from './Settings';
8
11
  export * from './ViewEditor';
@@ -5,27 +5,40 @@
5
5
  import React, { type PropsWithChildren } from 'react';
6
6
 
7
7
  import { type ThemedClassName } from '@dxos/react-ui';
8
- import { Card } from '@dxos/react-ui-stack';
9
- import { JsonFilter } from '@dxos/react-ui-syntax-highlighter';
10
- import { mx, textBlockWidth } from '@dxos/react-ui-theme';
8
+ import { Syntax } from '@dxos/react-ui-syntax-highlighter';
9
+ import { composableProps, slottable } from '@dxos/ui-theme';
11
10
 
12
- type TestLayoutProps = ThemedClassName<PropsWithChildren<{ json?: any }>>;
11
+ type TestLayoutProps = PropsWithChildren<{ json?: any }>;
13
12
 
14
- export const TestLayout = ({ classNames, children, json }: TestLayoutProps) => {
13
+ export const TestLayout = ({ children, json }: TestLayoutProps) => {
15
14
  return (
16
- <div className='w-full h-full grid grid-cols-[1fr_1fr] gap-2 overflow-hidden'>
17
- <div className={mx('p-4 justify-center overflow-y-auto', classNames)}>{children}</div>
18
- <JsonFilter data={json} classNames='w-full text-xs' testId='debug' />
15
+ <div className='dx-container grid grid-cols-[1fr_1fr] p-4 gap-4'>
16
+ <TestPanel>{children}</TestPanel>
17
+ <TestPanel>
18
+ <Syntax.Root data={json}>
19
+ <Syntax.Content>
20
+ <Syntax.Filter />
21
+ <Syntax.Viewport>
22
+ <Syntax.Code testId='debug' classNames='text-xs' />
23
+ </Syntax.Viewport>
24
+ </Syntax.Content>
25
+ </Syntax.Root>
26
+ </TestPanel>
19
27
  </div>
20
28
  );
21
29
  };
22
30
 
23
31
  type TestPanelProps = ThemedClassName<PropsWithChildren>;
24
32
 
25
- export const TestPanel = ({ classNames, children }: TestPanelProps) => {
26
- return <Card.StaticRoot classNames={[textBlockWidth, classNames]}>{children}</Card.StaticRoot>;
27
- };
33
+ const TestPanel = slottable<HTMLDivElement, TestPanelProps>(({ children }, forwardedRef) => {
34
+ return (
35
+ <div {...composableProps({ classNames: 'dx-container bg-modal-surface rounded-md' })} ref={forwardedRef}>
36
+ {children}
37
+ </div>
38
+ );
39
+ });
28
40
 
29
41
  // Symbol for accessing debug objects in tests.
30
42
  export const VIEW_EDITOR_DEBUG_SYMBOL = Symbol('viewEditorDebug');
31
43
  export const FIELD_EDITOR_DEBUG_SYMBOL = Symbol('fieldEditorDebug');
44
+ export const OBJECT_PROPERTIES_DEBUG_SYMBOL = Symbol('objectPropertiesDebug');
@@ -2,6 +2,5 @@
2
2
  // Copyright 2025 DXOS.org
3
3
  //
4
4
 
5
- export * from './useForm';
6
- export * from './useRefQueryLookupHandler';
7
- export * from './useQueryRefOptions';
5
+ export * from './useFormHandler';
6
+ export * from './useKeyHandler';