@dxos/react-ui-form 0.8.4-main.ae835ea → 0.8.4-main.bc2380dfbc

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 +2734 -2312
  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 +2734 -2312
  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 -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 +114 -137
  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 -59
  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 +12 -15
  88. package/dist/types/src/components/ViewEditor/ViewEditor.d.ts.map +1 -1
  89. package/dist/types/src/components/ViewEditor/ViewEditor.stories.d.ts +70 -64
  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 -60
  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 -51
  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 -107
  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} +30 -24
  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 +30 -25
  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 +53 -50
  161. package/src/components/ViewEditor/ViewEditor.test.tsx +6 -6
  162. package/src/components/ViewEditor/ViewEditor.tsx +291 -208
  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 -65
  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 +16 -18
  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 -84
  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 -79
  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 -322
  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
- type DataType,
22
- FieldSchema,
23
- type FieldType,
20
+ ParentLabelAnnotation,
24
21
  ProjectionModel,
25
22
  VIEW_FIELD_LIMIT,
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
- view: DataType.View;
40
- mode?: 'schema' | 'query';
41
- registry?: SchemaRegistry;
42
- readonly?: boolean;
43
+ view: View.View;
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,11 +127,15 @@ export const ViewEditor = forwardRef<ProjectionModel, ViewEditorProps>(
96
127
  : QueryAST.Query.annotations({ title: 'Query' }),
97
128
  });
98
129
 
99
- if (mode === 'query') {
130
+ if (mode === 'tag') {
100
131
  return Schema.Struct({
101
- name: Schema.optional(Schema.String.annotations({ title: 'Name' })),
102
132
  ...base.fields,
103
- target: Schema.optional(Schema.String.annotations({ title: 'Target Queue' })),
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
+ ),
104
139
  }).pipe(Schema.mutable);
105
140
  }
106
141
 
@@ -111,219 +146,267 @@ export const ViewEditor = forwardRef<ProjectionModel, ViewEditorProps>(
111
146
  // TODO(burdon): Settings should have domain name owned by user.
112
147
  const viewValues = useMemo(
113
148
  () => ({
114
- name: view.name,
115
149
  query: mode === 'schema' ? getTypenameFromQuery(view.query.ast) : view.query.ast,
116
- target: queueTarget,
150
+ target: targetRef,
117
151
  }),
118
- [view.name, mode, view.query.ast, queueTarget],
152
+ [mode, view.query.ast, targetRef],
119
153
  );
120
154
 
121
- const handleToggleField = useCallback(
122
- (field: FieldType) => {
123
- setExpandedField((prevExpandedFieldId) => (prevExpandedFieldId === field.id ? undefined : field.id));
124
- },
125
- [readonly],
155
+ const fieldMap = useMemo<FormFieldMap | undefined>(
156
+ () => (mode === 'tag' ? customFields({ types, tags }) : undefined),
157
+ [mode, types, tags],
126
158
  );
127
159
 
128
- // TODO(burdon): Check if mutable; variant of useCallback that return undefined if readonly?
129
-
130
- const handleAdd = useCallback(() => {
131
- invariant(!readonly);
132
- const field = projectionModel.createFieldProjection();
133
- setExpandedField(field.id);
134
- }, [schema, projectionModel, readonly]);
135
-
136
160
  const handleUpdate = useCallback(
137
161
  (values: any) => {
138
- invariant(!readonly);
139
- requestAnimationFrame(() => {
140
- if ('name' in values && view.name !== values.name) {
141
- view.name = values.name;
142
- 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();
143
170
  }
171
+ }
144
172
 
145
- const query = mode === 'schema' ? Query.select(Filter.typename(values.query)).ast : values.query;
146
- onQueryChanged?.(query, values.target);
147
- });
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);
148
179
  },
149
- [onQueryChanged, readonly, view, queueTarget, mode],
180
+ [onQueryChanged, mode, feeds],
150
181
  );
151
182
 
152
183
  const handleDelete = useCallback(
153
184
  (fieldId: string) => {
154
185
  invariant(!readonly);
155
- if (fieldId === expandedField) {
156
- setExpandedField(undefined);
157
- }
158
-
159
186
  onDelete?.(fieldId);
160
187
  },
161
- [expandedField, onDelete, readonly],
188
+ [onDelete, readonly],
162
189
  );
163
190
 
164
- const handleMove = useCallback(
165
- (fromIndex: number, toIndex: number) => {
166
- 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) => {
167
278
  // NOTE(ZaymonFC): Using arrayMove here causes a race condition with the kanban model.
168
279
  const fields = [...view.projection.fields];
169
280
  const [moved] = fields.splice(fromIndex, 1);
170
281
  fields.splice(toIndex, 0, moved);
171
282
  view.projection.fields = fields;
172
- },
173
- [view.projection.fields, readonly],
174
- );
175
-
176
- const handleClose = useCallback(() => setExpandedField(undefined), []);
283
+ });
284
+ },
285
+ [view, readonly],
286
+ );
177
287
 
178
- const handleHide = useCallback(
179
- (fieldId: string) => {
180
- setExpandedField(undefined);
181
- projectionModel.hideFieldProjection(fieldId);
182
- },
183
- [projectionModel],
184
- );
288
+ const handleClose = useCallback(() => setExpandedField(undefined), []);
185
289
 
186
- const handleShow = useCallback(
187
- (property: string) => {
188
- setExpandedField(undefined);
189
- projectionModel.showFieldProjection(property as JsonProp);
190
- },
191
- [projectionModel],
192
- );
290
+ const handleHide = useCallback(
291
+ (fieldId: string) => {
292
+ setExpandedField(undefined);
293
+ projectionModel.hideFieldProjection(fieldId);
294
+ },
295
+ [projectionModel],
296
+ );
193
297
 
194
- 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
+ );
195
305
 
196
- return (
197
- <div role='none' className={mx(classNames)}>
198
- {schemaReadonly && mode === 'schema' && (
199
- <Callout.Root valence='info' classNames={['mlb-cardSpacingBlock', outerSpacing && 'mli-cardSpacingInline']}>
200
- <Callout.Title>{t('system schema description')}</Callout.Title>
201
- </Callout.Root>
202
- )}
306
+ if (!viewSnapshot) {
307
+ return null;
308
+ }
203
309
 
204
- {/* TODO(burdon): Is the form read-only or just the schema? */}
205
- {/* TODO(burdon): Readonly fields should take up the same space as editable fields (just be ghosted). */}
206
- <Form<Schema.Schema.Type<typeof viewSchema>>
207
- autoSave
208
- schema={viewSchema}
209
- values={viewValues}
210
- readonly={readonly ? 'disabled-input' : false}
211
- onSave={handleUpdate}
212
- outerSpacing={outerSpacing}
213
- Custom={custom}
214
- />
215
-
216
- <div role='none' className={outerSpacing ? cardSpacing : 'mlb-cardSpacingBlock'}>
217
- <h2 className={mx(inputTextLabel)}>{t('fields label')}</h2>
218
-
219
- <List.Root<FieldType>
220
- items={view.projection.fields}
221
- isItem={Schema.is(FieldSchema)}
222
- getId={(field) => field.id}
223
- onMove={readonly ? undefined : handleMove}
224
- readonly={readonly}
225
- >
226
- {({ items: fields }) => (
227
- <>
228
- {showHeading && <h3 className='text-sm'>{t('field path label')}</h3>}
229
- <div role='list' className={listGrid}>
230
- {fields?.map((field) => {
231
- const hidden = field.visible === false;
232
- return (
233
- <List.Item<FieldType>
234
- key={field.id}
235
- item={field}
236
- classNames={listItemGrid}
237
- aria-expanded={expandedField === field.id}
238
- >
239
- <div
240
- role='none'
241
- className={mx(subtleHover, listItemGrid, 'rounded-sm cursor-pointer min-bs-10')}
242
- >
243
- <List.ItemDragHandle disabled={readonly} />
244
- <List.ItemTitle
245
- classNames={hidden && 'text-subdued'}
246
- onClick={() => handleToggleField(field)}
247
- >
248
- {field.path}
249
- </List.ItemTitle>
250
- <List.ItemButton
251
- label={t(hidden ? 'show field label' : 'hide field label')}
252
- data-testid={hidden ? 'show-field-button' : 'hide-field-button'}
253
- icon={hidden ? 'ph--eye-closed--regular' : 'ph--eye--regular'}
254
- autoHide={false}
255
- disabled={readonly || (!hidden && projectionModel.fields.length <= 1)}
256
- onClick={() => (hidden ? handleShow(field.path) : handleHide(field.id))}
257
- />
258
- {mode === 'schema' && (
259
- <>
260
- <List.ItemDeleteButton
261
- label={t('delete field label')}
262
- autoHide={false}
263
- disabled={readonly || schemaReadonly || view.projection.fields.length <= 1}
264
- onClick={() => handleDelete(field.id)}
265
- data-testid='field.delete'
266
- />
267
- <IconButton
268
- iconOnly
269
- variant='ghost'
270
- label={t('toggle expand label', { ns: 'os' })}
271
- icon={
272
- expandedField === field.id ? 'ph--caret-down--regular' : 'ph--caret-right--regular'
273
- }
274
- onClick={() => handleToggleField(field)}
275
- />
276
- </>
277
- )}
278
- </div>
279
- {expandedField === field.id && mode === 'schema' && (
280
- <div role='none' className='col-span-5 mbs-1 mbe-1 border border-separator rounded-md'>
281
- <FieldEditor
282
- readonly={readonly || schemaReadonly ? 'disabled-input' : false}
283
- projection={projectionModel}
284
- field={field}
285
- registry={registry}
286
- onSave={handleClose}
287
- />
288
- </div>
289
- )}
290
- </List.Item>
291
- );
292
- })}
293
- </div>
294
- </>
295
- )}
296
- </List.Root>
297
- </div>
298
-
299
- {!readonly && !expandedField && mode === 'schema' && (
300
- <div role='none' className={outerSpacing ? cardSpacing : 'mlb-cardSpacingBlock'}>
301
- <IconButton
302
- icon='ph--plus--regular'
303
- label={t('button add property')}
304
- onClick={readonly ? undefined : handleAdd}
305
- // TODO(burdon): Show field limit in ux (not tooltip).
306
- disabled={view.projection.fields.length >= VIEW_FIELD_LIMIT}
307
- classNames='is-full'
308
- />
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
+ })}
309
378
  </div>
310
- )}
311
- </div>
312
- );
313
- },
314
- );
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
+ };
315
395
 
316
- const customFields = ({ types, tags }: Pick<ViewEditorProps, 'types' | 'tags'>): Record<string, InputComponent> => ({
317
- query: (props: InputProps) => {
318
- const handleChange = useCallback(
319
- (query: Query.Any) => props.onValueChange('object', query.ast),
320
- [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],
321
404
  );
322
405
 
323
406
  return (
324
407
  <Input.Root>
325
- <InputHeader label={props.label} />
326
- <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} />
327
410
  </Input.Root>
328
411
  );
329
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';