@dxos/react-ui-form 0.8.4-main.5ea62a8 → 0.8.4-main.60689f5b1c

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