@dxos/react-ui-form 0.8.4-main.72ec0f3 → 0.8.4-main.765dc60934

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