@dxos/react-ui-form 0.8.4-main.406dc2a → 0.8.4-main.422d1c7879

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 (219) hide show
  1. package/README.md +0 -1
  2. package/dist/lib/browser/index.mjs +2856 -2309
  3. package/dist/lib/browser/index.mjs.map +4 -4
  4. package/dist/lib/browser/meta.json +1 -1
  5. package/dist/lib/node-esm/index.mjs +2856 -2309
  6. package/dist/lib/node-esm/index.mjs.map +4 -4
  7. package/dist/lib/node-esm/meta.json +1 -1
  8. package/dist/types/src/components/FieldEditor/FieldEditor.d.ts +8 -7
  9. package/dist/types/src/components/FieldEditor/FieldEditor.d.ts.map +1 -1
  10. package/dist/types/src/components/FieldEditor/FieldEditor.stories.d.ts +68 -60
  11. package/dist/types/src/components/FieldEditor/FieldEditor.stories.d.ts.map +1 -1
  12. package/dist/types/src/components/Form/Form.d.ts +93 -26
  13. package/dist/types/src/components/Form/Form.d.ts.map +1 -1
  14. package/dist/types/src/components/Form/Form.stories.d.ts +114 -137
  15. package/dist/types/src/components/Form/Form.stories.d.ts.map +1 -1
  16. package/dist/types/src/components/Form/FormField.d.ts +43 -0
  17. package/dist/types/src/components/Form/FormField.d.ts.map +1 -0
  18. package/dist/types/src/components/Form/FormFieldComponent.d.ts +80 -0
  19. package/dist/types/src/components/Form/FormFieldComponent.d.ts.map +1 -0
  20. package/dist/types/src/components/Form/FormFieldComponent.stories.d.ts +9 -0
  21. package/dist/types/src/components/Form/FormFieldComponent.stories.d.ts.map +1 -0
  22. package/dist/types/src/components/Form/FormFieldSet.d.ts +18 -0
  23. package/dist/types/src/components/Form/FormFieldSet.d.ts.map +1 -0
  24. package/dist/types/src/components/Form/fields/ArrayField.d.ts +18 -0
  25. package/dist/types/src/components/Form/fields/ArrayField.d.ts.map +1 -0
  26. package/dist/types/src/components/Form/fields/BooleanField.d.ts +4 -0
  27. package/dist/types/src/components/Form/fields/BooleanField.d.ts.map +1 -0
  28. package/dist/types/src/components/Form/fields/GeoPointField.d.ts +5 -0
  29. package/dist/types/src/components/Form/fields/GeoPointField.d.ts.map +1 -0
  30. package/dist/types/src/components/Form/fields/MarkdownField.d.ts +11 -0
  31. package/dist/types/src/components/Form/fields/MarkdownField.d.ts.map +1 -0
  32. package/dist/types/src/components/Form/fields/MarkdownField.stories.d.ts +83 -0
  33. package/dist/types/src/components/Form/fields/MarkdownField.stories.d.ts.map +1 -0
  34. package/dist/types/src/components/Form/fields/NumberField.d.ts +4 -0
  35. package/dist/types/src/components/Form/fields/NumberField.d.ts.map +1 -0
  36. package/dist/types/src/components/Form/fields/RefField.d.ts +22 -0
  37. package/dist/types/src/components/Form/fields/RefField.d.ts.map +1 -0
  38. package/dist/types/src/components/Form/fields/SelectField.d.ts +10 -0
  39. package/dist/types/src/components/Form/fields/SelectField.d.ts.map +1 -0
  40. package/dist/types/src/components/Form/fields/SelectOptionField.d.ts +5 -0
  41. package/dist/types/src/components/Form/fields/SelectOptionField.d.ts.map +1 -0
  42. package/dist/types/src/components/Form/fields/SelectOptionField.stories.d.ts +84 -0
  43. package/dist/types/src/components/Form/fields/SelectOptionField.stories.d.ts.map +1 -0
  44. package/dist/types/src/components/Form/fields/TextAreaField.d.ts +4 -0
  45. package/dist/types/src/components/Form/fields/TextAreaField.d.ts.map +1 -0
  46. package/dist/types/src/components/Form/fields/TextField.d.ts +4 -0
  47. package/dist/types/src/components/Form/fields/TextField.d.ts.map +1 -0
  48. package/dist/types/src/components/Form/fields/TupleField.d.ts +6 -0
  49. package/dist/types/src/components/Form/fields/TupleField.d.ts.map +1 -0
  50. package/dist/types/src/components/Form/fields/index.d.ts +12 -0
  51. package/dist/types/src/components/Form/fields/index.d.ts.map +1 -0
  52. package/dist/types/src/components/Form/index.d.ts +2 -4
  53. package/dist/types/src/components/Form/index.d.ts.map +1 -1
  54. package/dist/types/src/components/ObjectForm/ObjectForm.d.ts +9 -0
  55. package/dist/types/src/components/ObjectForm/ObjectForm.d.ts.map +1 -0
  56. package/dist/types/src/components/ObjectForm/index.d.ts +2 -0
  57. package/dist/types/src/components/ObjectForm/index.d.ts.map +1 -0
  58. package/dist/types/src/components/ObjectPicker/ObjectPicker.d.ts +37 -10
  59. package/dist/types/src/components/ObjectPicker/ObjectPicker.d.ts.map +1 -1
  60. package/dist/types/src/components/ObjectPicker/ObjectPicker.stories.d.ts +64 -59
  61. package/dist/types/src/components/ObjectPicker/ObjectPicker.stories.d.ts.map +1 -1
  62. package/dist/types/src/components/ObjectProperties/ObjectProperties.d.ts +9 -0
  63. package/dist/types/src/components/ObjectProperties/ObjectProperties.d.ts.map +1 -0
  64. package/dist/types/src/components/ObjectProperties/ObjectProperties.stories.d.ts +127 -0
  65. package/dist/types/src/components/ObjectProperties/ObjectProperties.stories.d.ts.map +1 -0
  66. package/dist/types/src/components/ObjectProperties/ObjectProperties.test.d.ts +2 -0
  67. package/dist/types/src/components/ObjectProperties/ObjectProperties.test.d.ts.map +1 -0
  68. package/dist/types/src/components/ObjectProperties/index.d.ts +2 -0
  69. package/dist/types/src/components/ObjectProperties/index.d.ts.map +1 -0
  70. package/dist/types/src/components/Settings/Settings.d.ts +43 -0
  71. package/dist/types/src/components/Settings/Settings.d.ts.map +1 -0
  72. package/dist/types/src/components/Settings/Settings.stories.d.ts +84 -0
  73. package/dist/types/src/components/Settings/Settings.stories.d.ts.map +1 -0
  74. package/dist/types/src/components/Settings/SettingsFieldSet.d.ts +38 -0
  75. package/dist/types/src/components/Settings/SettingsFieldSet.d.ts.map +1 -0
  76. package/dist/types/src/components/Settings/SettingsItem.d.ts +17 -0
  77. package/dist/types/src/components/Settings/SettingsItem.d.ts.map +1 -0
  78. package/dist/types/src/components/Settings/index.d.ts +3 -0
  79. package/dist/types/src/components/Settings/index.d.ts.map +1 -0
  80. package/dist/types/src/components/ViewEditor/ViewEditor.d.ts +13 -15
  81. package/dist/types/src/components/ViewEditor/ViewEditor.d.ts.map +1 -1
  82. package/dist/types/src/components/ViewEditor/ViewEditor.stories.d.ts +70 -64
  83. package/dist/types/src/components/ViewEditor/ViewEditor.stories.d.ts.map +1 -1
  84. package/dist/types/src/components/index.d.ts +3 -1
  85. package/dist/types/src/components/index.d.ts.map +1 -1
  86. package/dist/types/src/components/testing.d.ts +4 -6
  87. package/dist/types/src/components/testing.d.ts.map +1 -1
  88. package/dist/types/src/hooks/index.d.ts +2 -3
  89. package/dist/types/src/hooks/index.d.ts.map +1 -1
  90. package/dist/types/src/hooks/useFormHandler.d.ts +82 -0
  91. package/dist/types/src/hooks/useFormHandler.d.ts.map +1 -0
  92. package/dist/types/src/hooks/useKeyHandler.d.ts +7 -0
  93. package/dist/types/src/hooks/useKeyHandler.d.ts.map +1 -0
  94. package/dist/types/src/translations.d.ts +65 -60
  95. package/dist/types/src/translations.d.ts.map +1 -1
  96. package/dist/types/src/util/field-type.d.ts +19 -0
  97. package/dist/types/src/util/field-type.d.ts.map +1 -0
  98. package/dist/types/src/util/field.d.ts +3 -3
  99. package/dist/types/src/util/field.d.ts.map +1 -1
  100. package/dist/types/src/util/formatting.d.ts +2 -2
  101. package/dist/types/src/util/formatting.d.ts.map +1 -1
  102. package/dist/types/src/util/index.d.ts +2 -1
  103. package/dist/types/src/util/index.d.ts.map +1 -1
  104. package/dist/types/src/util/properties.d.ts +7 -0
  105. package/dist/types/src/util/properties.d.ts.map +1 -0
  106. package/dist/types/src/util/refs.d.ts +2 -2
  107. package/dist/types/src/util/refs.d.ts.map +1 -1
  108. package/dist/types/tsconfig.tsbuildinfo +1 -1
  109. package/package.json +47 -50
  110. package/src/components/FieldEditor/FieldEditor.stories.tsx +43 -25
  111. package/src/components/FieldEditor/FieldEditor.test.tsx +0 -1
  112. package/src/components/FieldEditor/FieldEditor.tsx +74 -78
  113. package/src/components/Form/AUDIT.md +17 -0
  114. package/src/components/Form/Form.stories.tsx +137 -250
  115. package/src/components/Form/Form.tsx +375 -107
  116. package/src/components/Form/FormField.tsx +305 -0
  117. package/src/components/Form/FormFieldComponent.stories.tsx +65 -0
  118. package/src/components/Form/FormFieldComponent.tsx +174 -0
  119. package/src/components/Form/FormFieldSet.tsx +144 -0
  120. package/src/components/Form/README.md +3 -0
  121. package/src/components/Form/fields/ArrayField.tsx +158 -0
  122. package/src/components/Form/fields/BooleanField.tsx +22 -0
  123. package/src/components/Form/fields/GeoPointField.tsx +120 -0
  124. package/src/components/Form/fields/MarkdownField.stories.tsx +151 -0
  125. package/src/components/Form/fields/MarkdownField.tsx +167 -0
  126. package/src/components/Form/fields/NumberField.tsx +39 -0
  127. package/src/components/Form/fields/RefField.tsx +228 -0
  128. package/src/components/Form/fields/SelectField.tsx +65 -0
  129. package/src/components/Form/{custom/SelectOptionsInput.stories.tsx → fields/SelectOptionField.stories.tsx} +21 -17
  130. package/src/components/Form/{custom/SelectOptionsInput.tsx → fields/SelectOptionField.tsx} +28 -23
  131. package/src/components/Form/fields/TextAreaField.tsx +38 -0
  132. package/src/components/Form/fields/TextField.tsx +38 -0
  133. package/src/components/Form/{custom/TupleInput.tsx → fields/TupleField.tsx} +16 -9
  134. package/src/components/Form/fields/index.ts +15 -0
  135. package/src/components/Form/index.ts +3 -4
  136. package/src/components/ObjectForm/ObjectForm.tsx +113 -0
  137. package/src/components/{ControlSection → ObjectForm}/index.ts +1 -1
  138. package/src/components/ObjectPicker/ObjectPicker.stories.tsx +23 -21
  139. package/src/components/ObjectPicker/ObjectPicker.tsx +112 -80
  140. package/src/components/ObjectProperties/ObjectProperties.stories.tsx +361 -0
  141. package/src/components/ObjectProperties/ObjectProperties.test.tsx +101 -0
  142. package/src/components/ObjectProperties/ObjectProperties.tsx +140 -0
  143. package/src/components/ObjectProperties/index.ts +5 -0
  144. package/src/components/Settings/Settings.stories.tsx +103 -0
  145. package/src/components/Settings/Settings.tsx +81 -0
  146. package/src/components/Settings/SettingsFieldSet.tsx +211 -0
  147. package/src/components/Settings/SettingsItem.tsx +62 -0
  148. package/src/components/Settings/index.ts +6 -0
  149. package/src/components/ViewEditor/ViewEditor.stories.tsx +53 -50
  150. package/src/components/ViewEditor/ViewEditor.test.tsx +6 -6
  151. package/src/components/ViewEditor/ViewEditor.tsx +296 -206
  152. package/src/components/index.ts +3 -1
  153. package/src/components/testing.tsx +24 -11
  154. package/src/hooks/index.ts +2 -3
  155. package/src/hooks/useFormHandler.ts +380 -0
  156. package/src/hooks/useKeyHandler.tsx +37 -0
  157. package/src/translations.ts +70 -65
  158. package/src/util/field-type.ts +57 -0
  159. package/src/util/field.ts +25 -25
  160. package/src/util/formatting.ts +22 -22
  161. package/src/util/index.ts +2 -1
  162. package/src/util/properties.ts +18 -0
  163. package/src/util/refs.ts +16 -18
  164. package/dist/types/src/components/ControlSection/ControlSection.d.ts +0 -23
  165. package/dist/types/src/components/ControlSection/ControlSection.d.ts.map +0 -1
  166. package/dist/types/src/components/ControlSection/ControlSection.stories.d.ts +0 -84
  167. package/dist/types/src/components/ControlSection/ControlSection.stories.d.ts.map +0 -1
  168. package/dist/types/src/components/ControlSection/index.d.ts +0 -2
  169. package/dist/types/src/components/ControlSection/index.d.ts.map +0 -1
  170. package/dist/types/src/components/Form/ArrayField.d.ts +0 -16
  171. package/dist/types/src/components/Form/ArrayField.d.ts.map +0 -1
  172. package/dist/types/src/components/Form/Defaults.d.ts +0 -14
  173. package/dist/types/src/components/Form/Defaults.d.ts.map +0 -1
  174. package/dist/types/src/components/Form/FormActions.d.ts +0 -9
  175. package/dist/types/src/components/Form/FormActions.d.ts.map +0 -1
  176. package/dist/types/src/components/Form/FormContent.d.ts +0 -63
  177. package/dist/types/src/components/Form/FormContent.d.ts.map +0 -1
  178. package/dist/types/src/components/Form/FormContext.d.ts +0 -23
  179. package/dist/types/src/components/Form/FormContext.d.ts.map +0 -1
  180. package/dist/types/src/components/Form/Input.d.ts +0 -29
  181. package/dist/types/src/components/Form/Input.d.ts.map +0 -1
  182. package/dist/types/src/components/Form/RefField.d.ts +0 -22
  183. package/dist/types/src/components/Form/RefField.d.ts.map +0 -1
  184. package/dist/types/src/components/Form/custom/GeoPointInput.d.ts +0 -4
  185. package/dist/types/src/components/Form/custom/GeoPointInput.d.ts.map +0 -1
  186. package/dist/types/src/components/Form/custom/SelectOptionsInput.d.ts +0 -4
  187. package/dist/types/src/components/Form/custom/SelectOptionsInput.d.ts.map +0 -1
  188. package/dist/types/src/components/Form/custom/SelectOptionsInput.stories.d.ts +0 -79
  189. package/dist/types/src/components/Form/custom/SelectOptionsInput.stories.d.ts.map +0 -1
  190. package/dist/types/src/components/Form/custom/TupleInput.d.ts +0 -6
  191. package/dist/types/src/components/Form/custom/TupleInput.d.ts.map +0 -1
  192. package/dist/types/src/components/Form/custom/index.d.ts +0 -4
  193. package/dist/types/src/components/Form/custom/index.d.ts.map +0 -1
  194. package/dist/types/src/components/Form/factory.d.ts +0 -8
  195. package/dist/types/src/components/Form/factory.d.ts.map +0 -1
  196. package/dist/types/src/hooks/useForm.d.ts +0 -67
  197. package/dist/types/src/hooks/useForm.d.ts.map +0 -1
  198. package/dist/types/src/hooks/useQueryRefOptions.d.ts +0 -29
  199. package/dist/types/src/hooks/useQueryRefOptions.d.ts.map +0 -1
  200. package/dist/types/src/hooks/useRefQueryLookupHandler.d.ts +0 -8
  201. package/dist/types/src/hooks/useRefQueryLookupHandler.d.ts.map +0 -1
  202. package/dist/types/src/util/schema.d.ts +0 -3
  203. package/dist/types/src/util/schema.d.ts.map +0 -1
  204. package/src/components/ControlSection/ControlSection.stories.tsx +0 -38
  205. package/src/components/ControlSection/ControlSection.tsx +0 -153
  206. package/src/components/Form/ArrayField.tsx +0 -125
  207. package/src/components/Form/Defaults.tsx +0 -205
  208. package/src/components/Form/FormActions.tsx +0 -54
  209. package/src/components/Form/FormContent.tsx +0 -322
  210. package/src/components/Form/FormContext.tsx +0 -110
  211. package/src/components/Form/Input.tsx +0 -54
  212. package/src/components/Form/RefField.tsx +0 -283
  213. package/src/components/Form/custom/GeoPointInput.tsx +0 -80
  214. package/src/components/Form/custom/index.ts +0 -7
  215. package/src/components/Form/factory.tsx +0 -31
  216. package/src/hooks/useForm.ts +0 -296
  217. package/src/hooks/useQueryRefOptions.ts +0 -62
  218. package/src/hooks/useRefQueryLookupHandler.ts +0 -39
  219. package/src/util/schema.ts +0 -12
@@ -6,11 +6,11 @@ import { composeStories } from '@storybook/react';
6
6
  import { cleanup, fireEvent, screen, waitFor } from '@testing-library/react';
7
7
  import { afterEach, describe, expect, test } from 'vitest';
8
8
 
9
+ import { View } from '@dxos/echo';
9
10
  import { EchoSchema, isInstanceOf } from '@dxos/echo/internal';
10
- import { DataType, ProjectionModel } from '@dxos/schema';
11
+ import { ProjectionModel } from '@dxos/schema';
11
12
 
12
13
  import { VIEW_EDITOR_DEBUG_SYMBOL } from '../testing';
13
-
14
14
  import * as stories from './ViewEditor.stories';
15
15
  import { type ViewEditorDebugObjects } from './ViewEditor.stories';
16
16
 
@@ -20,7 +20,7 @@ const getViewEditorDebugObjects = (): ViewEditorDebugObjects => {
20
20
  const debugObjects = (window as any)[VIEW_EDITOR_DEBUG_SYMBOL] as ViewEditorDebugObjects;
21
21
  expect(debugObjects).toBeDefined();
22
22
  expect(debugObjects.schema).toBeInstanceOf(EchoSchema);
23
- expect(isInstanceOf(DataType.View, debugObjects.view)).toBeTruthy();
23
+ expect(isInstanceOf(View.View, debugObjects.view)).toBeTruthy();
24
24
  expect(debugObjects.projection).toBeInstanceOf(ProjectionModel);
25
25
  return debugObjects;
26
26
  };
@@ -54,7 +54,7 @@ describe('ViewEditor', () => {
54
54
  const nameField = screen.getByText('name');
55
55
  fireEvent.click(nameField);
56
56
 
57
- const fieldInput = screen.getByPlaceholderText('Field name.');
57
+ const fieldInput = screen.getByPlaceholderText('Property name');
58
58
  fireEvent.change(fieldInput, { target: { value: 'new_property' } });
59
59
 
60
60
  fireEvent.click(screen.getByTestId('save-button'));
@@ -89,12 +89,12 @@ describe('ViewEditor', () => {
89
89
  await Default.run();
90
90
  await waitForViewEditor();
91
91
 
92
- // Click the add property button.
92
+ // Click the add property button. The new field is auto-expanded by handleAdd.
93
93
  const addButton = screen.getByText('Add property');
94
94
  fireEvent.click(addButton);
95
95
 
96
96
  // Fill out the property field.
97
- const fieldInput = screen.getByPlaceholderText('Field name.');
97
+ const fieldInput = screen.getByPlaceholderText('Property name');
98
98
  fireEvent.change(fieldInput, { target: { value: 'added_property' } });
99
99
 
100
100
  // Click the format combo box and select the first option.
@@ -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 } from '@dxos/echo';
13
+ import { View } from '@dxos/echo';
14
+ import { EchoSchema, type JsonProp, isMutable, toJsonSchema } from '@dxos/echo/internal';
14
15
  import { invariant } from '@dxos/invariant';
15
- import { Callout, IconButton, Input, type ThemedClassName, useTranslation } from '@dxos/react-ui';
16
+ import { useObject, useQuery } from '@dxos/react-client/echo';
17
+ import { IconButton, Input, Message, type ThemedClassName, useTranslation } from '@dxos/react-ui';
16
18
  import { QueryForm, type QueryFormProps } from '@dxos/react-ui-components';
17
19
  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
20
  import {
21
- type DataType,
22
- FieldSchema,
23
- type FieldType,
21
+ ParentLabelAnnotation,
24
22
  ProjectionModel,
25
23
  VIEW_FIELD_LIMIT,
24
+ createEchoChangeCallback,
26
25
  getTypenameFromQuery,
27
26
  } from '@dxos/schema';
27
+ import { mx, osTranslations, subtleHover } from '@dxos/ui-theme';
28
28
 
29
29
  import { translationKey } from '../../translations';
30
30
  import { FieldEditor } from '../FieldEditor';
31
- import { Form, type FormProps, type InputComponent, InputHeader, type InputProps } from '../Form';
32
-
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';
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(
99
+ Match.when({ type: 'from' }, ({ from }) => {
100
+ if (from._tag !== 'scope') {
101
+ return undefined;
102
+ }
103
+ return Option.fromNullable(from.scope.queues).pipe(
84
104
  Option.flatMap((queues) => Array.head(queues)),
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,274 @@ 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 queueDxn: string | undefined;
164
+
165
+ if (Ref.isRef(targetValue)) {
166
+ const feedDxn = targetValue.dxn.toString();
167
+ const feed = feeds.find((feed) => Obj.getDXN(feed).toString() === feedDxn);
168
+ if (feed) {
169
+ queueDxn = 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, queueDxn);
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 role='none' 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.change(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
- );
283
+ });
284
+ },
285
+ [view, readonly],
286
+ );
175
287
 
176
- const handleClose = useCallback(() => setExpandedField(undefined), []);
288
+ const handleClose = useCallback(() => setExpandedField(undefined), []);
177
289
 
178
- const handleHide = useCallback(
179
- (fieldId: string) => {
180
- setExpandedField(undefined);
181
- projectionModel.hideFieldProjection(fieldId);
182
- },
183
- [projectionModel],
184
- );
290
+ const handleHide = useCallback(
291
+ (fieldId: string) => {
292
+ setExpandedField(undefined);
293
+ projectionModel.hideFieldProjection(fieldId);
294
+ },
295
+ [projectionModel],
296
+ );
185
297
 
186
- const handleShow = useCallback(
187
- (property: string) => {
188
- setExpandedField(undefined);
189
- projectionModel.showFieldProjection(property as JsonProp);
190
- },
191
- [projectionModel],
192
- );
298
+ const handleShow = useCallback(
299
+ (property: string) => {
300
+ setExpandedField(undefined);
301
+ projectionModel.showFieldProjection(property as JsonProp);
302
+ },
303
+ [projectionModel],
304
+ );
193
305
 
194
- const custom = useMemo(() => (mode === 'query' ? customFields({ types, tags }) : undefined), [types, tags, mode]);
306
+ if (!viewSnapshot) {
307
+ return null;
308
+ }
195
309
 
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
- )}
203
-
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
332
+ role='none'
333
+ className={mx(
334
+ subtleHover,
335
+ 'grid grid-cols-subgrid col-span-5',
336
+ 'rounded-xs cursor-pointer min-h-10',
337
+ )}
338
+ >
339
+ <List.ItemDragHandle disabled={readonly || schemaReadonly} />
340
+ <List.ItemTitle classNames={hidden && 'text-subdued'} onClick={() => handleToggleField(field)}>
341
+ {field.path}
342
+ </List.ItemTitle>
343
+ <List.ItemIconButton
344
+ label={t(hidden ? 'show field label' : 'hide field label')}
345
+ data-testid={hidden ? 'show-field-button' : 'hide-field-button'}
346
+ icon={hidden ? 'ph--eye-closed--regular' : 'ph--eye--regular'}
347
+ autoHide={false}
348
+ disabled={readonly || (!hidden && projectionModel.getFields().length <= 1)}
349
+ onClick={() => (hidden ? handleShow(field.path) : handleHide(field.id))}
350
+ />
351
+ {!readonly && (
352
+ <>
353
+ <List.ItemDeleteButton
354
+ label={t('delete-field.label')}
355
+ autoHide={false}
356
+ disabled={readonly || schemaReadonly || viewSnapshot.projection.fields.length <= 1}
357
+ onClick={() => handleDelete(field.id)}
358
+ data-testid='field.delete'
359
+ />
360
+ <List.ItemIconButton
361
+ iconOnly
362
+ variant='ghost'
363
+ label={t('toggle-expand.label', { ns: osTranslations })}
364
+ icon={expandedField === field.id ? 'ph--caret-down--regular' : 'ph--caret-right--regular'}
365
+ onClick={() => handleToggleField(field)}
366
+ data-testid='field.toggle'
367
+ />
368
+ </>
369
+ )}
370
+ </div>
371
+ {expandedField === field.id && !readonly && (
372
+ <div role='none' className='col-span-full mt-1 mb-1 border border-separator rounded-md'>
373
+ <FieldEditor
374
+ readonly={readonly || schemaReadonly}
375
+ registry={registry}
376
+ projection={projectionModel}
377
+ field={field}
378
+ onSave={handleClose}
379
+ />
380
+ </div>
381
+ )}
382
+ </List.Item>
383
+ );
384
+ })}
309
385
  </div>
310
- )}
311
- </div>
312
- );
313
- },
314
- );
386
+ {!readonly && !expandedField && (
387
+ <div role='none' className='my-form-padding'>
388
+ <IconButton
389
+ icon='ph--plus--regular'
390
+ label={t('add-property-button.label')}
391
+ onClick={handleAdd}
392
+ disabled={viewSnapshot.projection.fields.length >= VIEW_FIELD_LIMIT}
393
+ classNames='w-full'
394
+ />
395
+ </div>
396
+ )}
397
+ </>
398
+ )}
399
+ </List.Root>
400
+ );
401
+ };
315
402
 
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],
403
+ const customFields = ({
404
+ types,
405
+ tags,
406
+ }: Pick<ViewEditorProps, 'types' | 'tags'>): Record<string, FormFieldComponent> => ({
407
+ query: ({ type, readonly, label, getValue, onValueChange }: FormFieldComponentProps) => {
408
+ const handleChange = useCallback<NonNullable<QueryFormProps['onChange']>>(
409
+ (query) => onValueChange(type, query.ast),
410
+ [onValueChange],
321
411
  );
322
412
 
323
413
  return (
324
414
  <Input.Root>
325
- <InputHeader label={props.label} />
326
- <QueryForm initialQuery={props.getValue()} types={types} tags={tags} onChange={handleChange} />
415
+ <FormFieldLabel readonly={readonly} label={label} />
416
+ <QueryForm initialQuery={getValue()} types={types} tags={tags} onChange={handleChange} />
327
417
  </Input.Root>
328
418
  );
329
419
  },