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

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