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

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