@dxos/react-ui-form 0.8.4-main.b97322e → 0.8.4-main.bcb3aa67d6

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 (205) hide show
  1. package/README.md +0 -1
  2. package/dist/lib/browser/index.mjs +2315 -2116
  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 +2315 -2116
  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 +82 -5
  11. package/dist/types/src/components/FieldEditor/FieldEditor.stories.d.ts.map +1 -1
  12. package/dist/types/src/components/Form/Form.d.ts +84 -25
  13. package/dist/types/src/components/Form/Form.d.ts.map +1 -1
  14. package/dist/types/src/components/Form/Form.stories.d.ts +127 -55
  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 +77 -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 +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 +16 -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/Form/fields/SelectOptionField.stories.d.ts +82 -0
  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 +54 -0
  53. package/dist/types/src/components/ObjectPicker/ObjectPicker.d.ts.map +1 -0
  54. package/dist/types/src/components/ObjectPicker/ObjectPicker.stories.d.ts +85 -0
  55. package/dist/types/src/components/ObjectPicker/ObjectPicker.stories.d.ts.map +1 -0
  56. package/dist/types/src/components/ObjectPicker/index.d.ts +2 -0
  57. package/dist/types/src/components/ObjectPicker/index.d.ts.map +1 -0
  58. package/dist/types/src/components/Settings/Settings.d.ts +39 -0
  59. package/dist/types/src/components/Settings/Settings.d.ts.map +1 -0
  60. package/dist/types/src/components/Settings/Settings.stories.d.ts +81 -0
  61. package/dist/types/src/components/Settings/Settings.stories.d.ts.map +1 -0
  62. package/dist/types/src/components/Settings/index.d.ts +2 -0
  63. package/dist/types/src/components/Settings/index.d.ts.map +1 -0
  64. package/dist/types/src/components/ViewEditor/ViewEditor.d.ts +22 -10
  65. package/dist/types/src/components/ViewEditor/ViewEditor.d.ts.map +1 -1
  66. package/dist/types/src/components/ViewEditor/ViewEditor.stories.d.ts +84 -8
  67. package/dist/types/src/components/ViewEditor/ViewEditor.stories.d.ts.map +1 -1
  68. package/dist/types/src/components/index.d.ts +2 -2
  69. package/dist/types/src/components/index.d.ts.map +1 -1
  70. package/dist/types/src/components/testing.d.ts +4 -3
  71. package/dist/types/src/components/testing.d.ts.map +1 -1
  72. package/dist/types/src/hooks/index.d.ts +2 -3
  73. package/dist/types/src/hooks/index.d.ts.map +1 -1
  74. package/dist/types/src/hooks/useFormHandler.d.ts +82 -0
  75. package/dist/types/src/hooks/useFormHandler.d.ts.map +1 -0
  76. package/dist/types/src/hooks/useKeyHandler.d.ts +6 -0
  77. package/dist/types/src/hooks/useKeyHandler.d.ts.map +1 -0
  78. package/dist/types/src/translations.d.ts +63 -54
  79. package/dist/types/src/translations.d.ts.map +1 -1
  80. package/dist/types/src/util/field.d.ts +3 -3
  81. package/dist/types/src/util/field.d.ts.map +1 -1
  82. package/dist/types/src/util/formatting.d.ts +2 -2
  83. package/dist/types/src/util/formatting.d.ts.map +1 -1
  84. package/dist/types/src/util/index.d.ts +1 -1
  85. package/dist/types/src/util/index.d.ts.map +1 -1
  86. package/dist/types/src/util/properties.d.ts +7 -0
  87. package/dist/types/src/util/properties.d.ts.map +1 -0
  88. package/dist/types/src/util/refs.d.ts +3 -3
  89. package/dist/types/src/util/refs.d.ts.map +1 -1
  90. package/dist/types/tsconfig.tsbuildinfo +1 -1
  91. package/package.json +52 -43
  92. package/src/components/FieldEditor/FieldEditor.stories.tsx +51 -32
  93. package/src/components/FieldEditor/FieldEditor.test.tsx +3 -2
  94. package/src/components/FieldEditor/FieldEditor.tsx +87 -83
  95. package/src/components/Form/AUDIT.md +17 -0
  96. package/src/components/Form/Form.stories.tsx +154 -219
  97. package/src/components/Form/Form.tsx +314 -97
  98. package/src/components/Form/FormField.tsx +305 -0
  99. package/src/components/Form/FormFieldComponent.stories.tsx +65 -0
  100. package/src/components/Form/FormFieldComponent.tsx +171 -0
  101. package/src/components/Form/FormFieldSet.tsx +129 -0
  102. package/src/components/Form/README.md +3 -0
  103. package/src/components/Form/fields/ArrayField.tsx +154 -0
  104. package/src/components/Form/fields/BooleanField.tsx +22 -0
  105. package/src/components/Form/fields/GeoPointField.tsx +120 -0
  106. package/src/components/Form/fields/MarkdownField.tsx +78 -0
  107. package/src/components/Form/fields/NumberField.tsx +39 -0
  108. package/src/components/Form/fields/RefField.tsx +199 -0
  109. package/src/components/Form/fields/SelectField.tsx +65 -0
  110. package/src/components/Form/fields/SelectOptionField.stories.tsx +64 -0
  111. package/src/components/Form/{custom/SelectOptionsInput.tsx → fields/SelectOptionField.tsx} +49 -45
  112. package/src/components/Form/fields/TextAreaField.tsx +38 -0
  113. package/src/components/Form/fields/TextField.tsx +38 -0
  114. package/src/components/Form/{custom/TupleInput.tsx → fields/TupleField.tsx} +17 -10
  115. package/src/components/Form/fields/index.ts +15 -0
  116. package/src/components/Form/index.ts +3 -4
  117. package/src/components/ObjectPicker/ObjectPicker.stories.tsx +210 -0
  118. package/src/components/ObjectPicker/ObjectPicker.tsx +186 -0
  119. package/src/components/{ControlSection → ObjectPicker}/index.ts +1 -1
  120. package/src/components/Settings/Settings.stories.tsx +56 -0
  121. package/src/components/Settings/Settings.tsx +129 -0
  122. package/src/components/Settings/index.ts +5 -0
  123. package/src/components/ViewEditor/ViewEditor.stories.tsx +141 -63
  124. package/src/components/ViewEditor/ViewEditor.test.tsx +17 -18
  125. package/src/components/ViewEditor/ViewEditor.tsx +353 -203
  126. package/src/components/index.ts +2 -3
  127. package/src/components/testing.tsx +33 -13
  128. package/src/hooks/index.ts +2 -3
  129. package/src/hooks/useFormHandler.ts +380 -0
  130. package/src/hooks/useKeyHandler.tsx +37 -0
  131. package/src/translations.ts +66 -58
  132. package/src/util/field.ts +33 -43
  133. package/src/util/formatting.ts +22 -22
  134. package/src/util/index.ts +1 -1
  135. package/src/util/properties.ts +18 -0
  136. package/src/util/refs.ts +17 -19
  137. package/dist/types/src/components/ControlSection/ControlSection.d.ts +0 -23
  138. package/dist/types/src/components/ControlSection/ControlSection.d.ts.map +0 -1
  139. package/dist/types/src/components/ControlSection/ControlSection.stories.d.ts +0 -8
  140. package/dist/types/src/components/ControlSection/ControlSection.stories.d.ts.map +0 -1
  141. package/dist/types/src/components/ControlSection/index.d.ts +0 -2
  142. package/dist/types/src/components/ControlSection/index.d.ts.map +0 -1
  143. package/dist/types/src/components/Form/ArrayField.d.ts +0 -16
  144. package/dist/types/src/components/Form/ArrayField.d.ts.map +0 -1
  145. package/dist/types/src/components/Form/Defaults.d.ts +0 -14
  146. package/dist/types/src/components/Form/Defaults.d.ts.map +0 -1
  147. package/dist/types/src/components/Form/FormActions.d.ts +0 -9
  148. package/dist/types/src/components/Form/FormActions.d.ts.map +0 -1
  149. package/dist/types/src/components/Form/FormContent.d.ts +0 -57
  150. package/dist/types/src/components/Form/FormContent.d.ts.map +0 -1
  151. package/dist/types/src/components/Form/FormContext.d.ts +0 -23
  152. package/dist/types/src/components/Form/FormContext.d.ts.map +0 -1
  153. package/dist/types/src/components/Form/Input.d.ts +0 -26
  154. package/dist/types/src/components/Form/Input.d.ts.map +0 -1
  155. package/dist/types/src/components/Form/RefField.d.ts +0 -12
  156. package/dist/types/src/components/Form/RefField.d.ts.map +0 -1
  157. package/dist/types/src/components/Form/custom/GeoPointInput.d.ts +0 -4
  158. package/dist/types/src/components/Form/custom/GeoPointInput.d.ts.map +0 -1
  159. package/dist/types/src/components/Form/custom/SelectOptionsInput.d.ts +0 -4
  160. package/dist/types/src/components/Form/custom/SelectOptionsInput.d.ts.map +0 -1
  161. package/dist/types/src/components/Form/custom/SelectOptionsInput.stories.d.ts +0 -7
  162. package/dist/types/src/components/Form/custom/SelectOptionsInput.stories.d.ts.map +0 -1
  163. package/dist/types/src/components/Form/custom/TupleInput.d.ts +0 -6
  164. package/dist/types/src/components/Form/custom/TupleInput.d.ts.map +0 -1
  165. package/dist/types/src/components/Form/custom/index.d.ts +0 -4
  166. package/dist/types/src/components/Form/custom/index.d.ts.map +0 -1
  167. package/dist/types/src/components/Form/factory.d.ts +0 -8
  168. package/dist/types/src/components/Form/factory.d.ts.map +0 -1
  169. package/dist/types/src/components/deprecated/Form.d.ts +0 -15
  170. package/dist/types/src/components/deprecated/Form.d.ts.map +0 -1
  171. package/dist/types/src/components/deprecated/Form.stories.d.ts +0 -13
  172. package/dist/types/src/components/deprecated/Form.stories.d.ts.map +0 -1
  173. package/dist/types/src/components/deprecated/FormInput.d.ts +0 -16
  174. package/dist/types/src/components/deprecated/FormInput.d.ts.map +0 -1
  175. package/dist/types/src/components/deprecated/index.d.ts +0 -2
  176. package/dist/types/src/components/deprecated/index.d.ts.map +0 -1
  177. package/dist/types/src/hooks/useForm.d.ts +0 -67
  178. package/dist/types/src/hooks/useForm.d.ts.map +0 -1
  179. package/dist/types/src/hooks/useQueryRefOptions.d.ts +0 -23
  180. package/dist/types/src/hooks/useQueryRefOptions.d.ts.map +0 -1
  181. package/dist/types/src/hooks/useRefQueryLookupHandler.d.ts +0 -8
  182. package/dist/types/src/hooks/useRefQueryLookupHandler.d.ts.map +0 -1
  183. package/dist/types/src/util/schema.d.ts +0 -3
  184. package/dist/types/src/util/schema.d.ts.map +0 -1
  185. package/src/components/ControlSection/ControlSection.stories.tsx +0 -41
  186. package/src/components/ControlSection/ControlSection.tsx +0 -139
  187. package/src/components/Form/ArrayField.tsx +0 -124
  188. package/src/components/Form/Defaults.tsx +0 -203
  189. package/src/components/Form/FormActions.tsx +0 -53
  190. package/src/components/Form/FormContent.tsx +0 -240
  191. package/src/components/Form/FormContext.tsx +0 -110
  192. package/src/components/Form/Input.tsx +0 -51
  193. package/src/components/Form/RefField.tsx +0 -213
  194. package/src/components/Form/custom/GeoPointInput.tsx +0 -72
  195. package/src/components/Form/custom/SelectOptionsInput.stories.tsx +0 -61
  196. package/src/components/Form/custom/index.ts +0 -7
  197. package/src/components/Form/factory.tsx +0 -31
  198. package/src/components/deprecated/Form.stories.tsx +0 -62
  199. package/src/components/deprecated/Form.tsx +0 -119
  200. package/src/components/deprecated/FormInput.tsx +0 -71
  201. package/src/components/deprecated/index.ts +0 -5
  202. package/src/hooks/useForm.ts +0 -291
  203. package/src/hooks/useQueryRefOptions.ts +0 -53
  204. package/src/hooks/useRefQueryLookupHandler.ts +0 -39
  205. package/src/util/schema.ts +0 -14
@@ -2,269 +2,419 @@
2
2
  // Copyright 2024 DXOS.org
3
3
  //
4
4
 
5
- import { Schema } from 'effect';
6
- import React, { useCallback, useMemo, useState } from 'react';
5
+ import { RegistryContext } from '@effect-atom/atom-react';
6
+ import * as Array from 'effect/Array';
7
+ import * as Match from 'effect/Match';
8
+ import * as Option from 'effect/Option';
9
+ import * as Schema from 'effect/Schema';
10
+ import React, { forwardRef, useCallback, useContext, useImperativeHandle, useMemo, useState } from 'react';
7
11
 
8
- import { type SchemaRegistry } from '@dxos/echo-db';
9
- import { EchoSchema, Format, type JsonProp, isMutable, toJsonSchema } from '@dxos/echo-schema';
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';
10
15
  import { invariant } from '@dxos/invariant';
11
- import { Callout, IconButton, 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';
18
+ import { QueryForm, type QueryFormProps } from '@dxos/react-ui-components';
12
19
  import { List } from '@dxos/react-ui-list';
13
- import { cardSpacing } from '@dxos/react-ui-stack';
14
- import { ghostHover, inputTextLabel, mx } from '@dxos/react-ui-theme';
15
- import { FieldSchema, type FieldType, VIEW_FIELD_LIMIT, type DataType, ProjectionModel } from '@dxos/schema';
20
+ import {
21
+ ParentLabelAnnotation,
22
+ ProjectionModel,
23
+ VIEW_FIELD_LIMIT,
24
+ createEchoChangeCallback,
25
+ getTypenameFromQuery,
26
+ } from '@dxos/schema';
27
+ import { mx, osTranslations, subtleHover } from '@dxos/ui-theme';
16
28
 
17
29
  import { translationKey } from '../../translations';
18
30
  import { FieldEditor } from '../FieldEditor';
19
- import { Form, type FormProps } from '../Form';
20
-
21
- const grid = 'grid grid-cols-[32px_1fr_32px_32px] min-bs-[2.5rem]';
22
-
23
- const ViewMetaSchema = Schema.Struct({
24
- typename: Format.URL.annotations({
25
- title: 'Typename',
26
- }),
27
- }).pipe(Schema.mutable);
28
-
29
- type ViewMetaType = Schema.Schema.Type<typeof ViewMetaSchema>;
31
+ import {
32
+ Form,
33
+ type FormFieldComponent,
34
+ type FormFieldComponentProps,
35
+ FormFieldLabel,
36
+ type FormFieldMap,
37
+ type FormRootProps,
38
+ } from '../Form';
30
39
 
31
40
  export type ViewEditorProps = ThemedClassName<
32
41
  {
33
42
  schema: Schema.Schema.AnyNoContext;
34
- view: DataType.View;
35
- registry?: SchemaRegistry;
36
- readonly?: boolean;
43
+ view: View.View;
44
+ mode?: 'schema' | 'tag';
45
+ registry?: SchemaRegistry.SchemaRegistry;
37
46
  showHeading?: boolean;
38
- onTypenameChanged?: (typename: string) => void;
47
+ onQueryChanged?: (query: QueryAST.Query, target?: string) => void;
39
48
  onDelete?: (fieldId: string) => void;
40
- } & Pick<FormProps<any>, 'outerSpacing'>
49
+ } & Pick<QueryFormProps, 'types' | 'tags'> &
50
+ Pick<FormRootProps<any>, 'readonly' | 'db'>
41
51
  >;
42
52
 
43
53
  /**
44
54
  * Schema-based object form.
45
55
  */
46
- export const ViewEditor = ({
47
- classNames,
48
- schema,
49
- view,
50
- registry,
51
- readonly: _readonly,
52
- showHeading = false,
53
- onTypenameChanged,
54
- onDelete,
55
- outerSpacing = true,
56
- }: ViewEditorProps) => {
56
+ export const ViewEditor = forwardRef<ProjectionModel, ViewEditorProps>(
57
+ (
58
+ {
59
+ classNames,
60
+ schema,
61
+ view,
62
+ mode = 'schema',
63
+ registry,
64
+ db,
65
+ readonly,
66
+ showHeading = false,
67
+ types,
68
+ tags,
69
+ onQueryChanged,
70
+ onDelete,
71
+ },
72
+ forwardedRef,
73
+ ) => {
74
+ const atomRegistry = useContext(RegistryContext);
75
+ const schemaReadonly = !isMutable(schema);
76
+ const { t } = useTranslation(translationKey);
77
+
78
+ const projectionModel = useMemo(() => {
79
+ // Use reactive and mutable version of json schema when schema is mutable.
80
+ const jsonSchema = schema instanceof EchoSchema ? schema.jsonSchema : toJsonSchema(schema);
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
+
96
+ useImperativeHandle(forwardedRef, () => projectionModel, [projectionModel]);
97
+
98
+ const queueTarget = Match.value(view.query.ast).pipe(
99
+ Match.when({ type: 'from' }, ({ from }) => {
100
+ if (from._tag !== 'scope') {
101
+ return undefined;
102
+ }
103
+ return Option.fromNullable(from.scope.queues).pipe(
104
+ Option.flatMap((queues) => Array.head(queues)),
105
+ Option.map(String),
106
+ Option.getOrUndefined,
107
+ );
108
+ }),
109
+ Match.orElse(() => undefined),
110
+ );
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
+
122
+ const viewSchema = useMemo(() => {
123
+ const base = Schema.Struct({
124
+ query:
125
+ mode === 'schema'
126
+ ? Format.URL.annotations({ title: 'Record type' })
127
+ : QueryAST.Query.annotations({ title: 'Query' }),
128
+ });
129
+
130
+ if (mode === 'tag') {
131
+ return Schema.Struct({
132
+ ...base.fields,
133
+ // TODO(wittjosiah): Replace Type.Feed with Dataset.Dataset when Ref.Ref supports unions.
134
+ target: Ref.Ref(Feed.Feed).pipe(
135
+ Schema.annotations({ title: 'Target Feed' }),
136
+ ParentLabelAnnotation.set(true),
137
+ Schema.optional,
138
+ ),
139
+ }).pipe(Schema.mutable);
140
+ }
141
+
142
+ return base.pipe(Schema.mutable);
143
+ }, [mode]);
144
+
145
+ // TODO(burdon): Need to warn user of possible consequences of editing.
146
+ // TODO(burdon): Settings should have domain name owned by user.
147
+ const viewValues = useMemo(
148
+ () => ({
149
+ query: mode === 'schema' ? getTypenameFromQuery(view.query.ast) : view.query.ast,
150
+ target: targetRef,
151
+ }),
152
+ [mode, view.query.ast, targetRef],
153
+ );
154
+
155
+ const fieldMap = useMemo<FormFieldMap | undefined>(
156
+ () => (mode === 'tag' ? customFields({ types, tags }) : undefined),
157
+ [mode, types, tags],
158
+ );
159
+
160
+ const handleUpdate = useCallback(
161
+ (values: any) => {
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();
170
+ }
171
+ }
172
+
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);
179
+ },
180
+ [onQueryChanged, mode, feeds],
181
+ );
182
+
183
+ const handleDelete = useCallback(
184
+ (fieldId: string) => {
185
+ invariant(!readonly);
186
+ onDelete?.(fieldId);
187
+ },
188
+ [onDelete, readonly],
189
+ );
190
+
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);
57
224
  const { t } = useTranslation(translationKey);
58
- const projection = useMemo(() => {
225
+
226
+ // Subscribe to view changes for reactivity.
227
+ const [viewSnapshot] = useObject(view);
228
+
229
+ const projectionModel = useMemo(() => {
59
230
  // Use reactive and mutable version of json schema when schema is mutable.
60
231
  const jsonSchema = schema instanceof EchoSchema ? schema.jsonSchema : toJsonSchema(schema);
61
- return new ProjectionModel(jsonSchema, view.projection);
62
- }, [schema, view.projection]);
63
- const [field, setField] = useState<FieldType>();
64
- const readonly = _readonly || !isMutable(schema);
65
-
66
- // TODO(burdon): Should be reactive.
67
- const viewValues = useMemo(() => {
68
- return {
69
- // TODO(burdon): Need to warn user of possible consequences of editing.
70
- // TODO(burdon): Settings should have domain name owned by user.
71
- typename: view.query.typename,
72
- };
73
- }, [view.query.typename]);
74
-
75
- const handleSelect = useCallback(
76
- (field: FieldType) => {
77
- if (readonly) {
78
- return;
79
- }
80
- setField((f) => (f === field ? undefined : field));
81
- },
82
- [readonly],
83
- );
84
232
 
85
- // TODO(burdon): Check if mutable; variant of useCallback that return undefined if readonly?
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']>();
86
248
 
87
249
  const handleAdd = useCallback(() => {
88
250
  invariant(!readonly);
89
- const field = projection.createFieldProjection();
90
- setField(field);
91
- }, [schema, projection, readonly]);
251
+ const field = projectionModel.createFieldProjection();
252
+ setExpandedField(field.id);
253
+ }, [projectionModel, readonly]);
92
254
 
93
- const handleUpdate = useCallback(
94
- ({ typename }: ViewMetaType) => {
95
- invariant(!readonly);
96
- requestAnimationFrame(() => {
97
- if (view.query.typename !== typename && !readonly) {
98
- onTypenameChanged?.(typename);
99
- }
100
- });
255
+ const handleToggleField = useCallback(
256
+ (field: View.FieldType) => {
257
+ setExpandedField((prevExpandedFieldId) => (prevExpandedFieldId === field.id ? undefined : field.id));
101
258
  },
102
- [view.query.typename, onTypenameChanged, readonly],
259
+ [readonly],
103
260
  );
104
261
 
105
262
  const handleDelete = useCallback(
106
263
  (fieldId: string) => {
107
264
  invariant(!readonly);
108
- if (fieldId === field?.id) {
109
- setField(undefined);
265
+ if (fieldId === expandedField) {
266
+ setExpandedField(undefined);
110
267
  }
111
268
 
112
269
  onDelete?.(fieldId);
113
270
  },
114
- [field, onDelete, readonly],
271
+ [expandedField, onDelete, readonly],
115
272
  );
116
273
 
117
274
  const handleMove = useCallback(
118
275
  (fromIndex: number, toIndex: number) => {
119
276
  invariant(!readonly);
120
- // NOTE(ZaymonFC): Using arrayMove here causes a race condition with the kanban model.
121
- const fields = [...view.projection.fields];
122
- const [moved] = fields.splice(fromIndex, 1);
123
- fields.splice(toIndex, 0, moved);
124
- view.projection.fields = fields;
277
+ Obj.change(view, (obj) => {
278
+ // NOTE(ZaymonFC): Using arrayMove here causes a race condition with the kanban model.
279
+ const fields = [...obj.projection.fields];
280
+ const [moved] = fields.splice(fromIndex, 1);
281
+ fields.splice(toIndex, 0, moved);
282
+ obj.projection.fields = fields;
283
+ });
125
284
  },
126
- [view.projection.fields, readonly],
285
+ [view, readonly],
127
286
  );
128
287
 
129
- const handleClose = useCallback(() => setField(undefined), []);
130
-
131
- const hiddenProperties = projection.getHiddenProperties();
288
+ const handleClose = useCallback(() => setExpandedField(undefined), []);
132
289
 
133
290
  const handleHide = useCallback(
134
291
  (fieldId: string) => {
135
- setField(undefined);
136
- projection.hideFieldProjection(fieldId);
292
+ setExpandedField(undefined);
293
+ projectionModel.hideFieldProjection(fieldId);
137
294
  },
138
- [projection],
295
+ [projectionModel],
139
296
  );
140
297
 
141
298
  const handleShow = useCallback(
142
299
  (property: string) => {
143
- setField(undefined);
144
- projection.showFieldProjection(property as JsonProp);
300
+ setExpandedField(undefined);
301
+ projectionModel.showFieldProjection(property as JsonProp);
145
302
  },
146
- [projection],
303
+ [projectionModel],
147
304
  );
148
305
 
149
- return (
150
- <div role='none' className={mx(classNames)}>
151
- {readonly && (
152
- <div role='none' className={mx('is-full', outerSpacing && 'plb-cardSpacingBlock pli-cardSpacingInline')}>
153
- <Callout.Root valence='info'>
154
- <Callout.Title>{t('system schema description')}</Callout.Title>
155
- </Callout.Root>
156
- </div>
157
- )}
306
+ if (!viewSnapshot) {
307
+ return null;
308
+ }
158
309
 
159
- {/* TODO(burdon): Is the form read-only or just the schema? */}
160
- {/* TODO(burdon): Readonly fields should take up the same space as editable fields (just be ghosted). */}
161
- <Form<ViewMetaType>
162
- autoSave
163
- schema={ViewMetaSchema}
164
- values={viewValues}
165
- readonly={readonly}
166
- onSave={handleUpdate}
167
- outerSpacing={outerSpacing}
168
- />
169
-
170
- <div role='none' className={outerSpacing ? cardSpacing : 'mlb-cardSpacingBlock'}>
171
- <label className={mx(inputTextLabel)}>{t('fields label')}</label>
172
-
173
- <List.Root<FieldType>
174
- items={view.projection.fields}
175
- isItem={Schema.is(FieldSchema)}
176
- getId={(field) => field.id}
177
- readonly={readonly}
178
- onMove={readonly ? undefined : handleMove}
179
- >
180
- {({ items: fields }) => (
181
- <>
182
- {showHeading && (
183
- <div role='heading' className={grid}>
184
- <div />
185
- <div className='flex items-center text-sm'>{t('field path label')}</div>
186
- </div>
187
- )}
188
-
189
- <div role='list' className='flex flex-col is-full'>
190
- {fields?.map((field) => (
191
- <List.Item<FieldType>
192
- key={field.id}
193
- item={field}
194
- classNames={mx(grid, ghostHover, 'overflow-hidden', !readonly && 'cursor-pointer')}
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
+ )}
195
338
  >
196
- <List.ItemDragHandle />
197
- <List.ItemTitle onClick={() => handleSelect(field)}>{field.path}</List.ItemTitle>
198
- <List.ItemButton
199
- data-testid='hide-field-button'
200
- icon='ph--eye-slash--regular'
201
- // TDOO(burdon): Is this the correct test?
202
- disabled={view.projection.fields.length <= 1}
203
- onClick={() => handleHide(field.id)}
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))}
204
350
  />
205
- {/* TODO(burdon): Remove unless implement undo. */}
206
351
  {!readonly && (
207
- <List.ItemDeleteButton
208
- disabled={view.projection.fields.length <= 1}
209
- onClick={() => handleDelete(field.id)}
210
- />
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
+ </>
211
369
  )}
212
- </List.Item>
213
- ))}
214
- </div>
215
- </>
216
- )}
217
- </List.Root>
218
-
219
- {hiddenProperties.length > 0 && (
220
- <>
221
- <label className={mx(inputTextLabel)}>{t('hidden fields label')}</label>
222
-
223
- <List.Root<string>
224
- items={hiddenProperties}
225
- isItem={(item): item is string => typeof item === 'string'}
226
- getId={(property) => property}
227
- >
228
- {({ items: properties }) => (
229
- <div role='list' className='flex flex-col is-full'>
230
- {properties?.map((property) => (
231
- <List.Item<string>
232
- key={property}
233
- item={property}
234
- classNames={mx(grid, ghostHover, !readonly && 'cursor-pointer')}
235
- >
236
- <List.ItemDragHandle disabled />
237
- <List.ItemTitle>{property}</List.ItemTitle>
238
- <List.ItemButton
239
- icon='ph--eye--regular'
240
- onClick={() => handleShow(property)}
241
- data-testid='show-field-button'
370
+ </div>
371
+ {expandedField === field.id && !readonly && (
372
+ <div role='none' className='col-span-5 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}
242
379
  />
243
- </List.Item>
244
- ))}
245
- </div>
246
- )}
247
- </List.Root>
248
- </>
249
- )}
250
- </div>
251
-
252
- {field && (
253
- <FieldEditor key={field.id} projection={projection} field={field} registry={registry} onSave={handleClose} />
254
- )}
255
-
256
- {!readonly && !field && (
257
- <div role='none' className={outerSpacing ? cardSpacing : 'mlb-cardSpacingBlock'}>
258
- <IconButton
259
- icon='ph--plus--regular'
260
- label={t('button add property')}
261
- onClick={readonly ? undefined : handleAdd}
262
- // TODO(burdon): Show field limit in ux (not tooltip).
263
- disabled={view.projection.fields.length >= VIEW_FIELD_LIMIT}
264
- classNames='flex is-full'
265
- />
266
- </div>
380
+ </div>
381
+ )}
382
+ </List.Item>
383
+ );
384
+ })}
385
+ </div>
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
+ </>
267
398
  )}
268
- </div>
399
+ </List.Root>
269
400
  );
270
401
  };
402
+
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],
411
+ );
412
+
413
+ return (
414
+ <Input.Root>
415
+ <FormFieldLabel readonly={readonly} label={label} />
416
+ <QueryForm initialQuery={getValue()} types={types} tags={tags} onChange={handleChange} />
417
+ </Input.Root>
418
+ );
419
+ },
420
+ });
@@ -2,9 +2,8 @@
2
2
  // Copyright 2024 DXOS.org
3
3
  //
4
4
 
5
- export * from './deprecated';
6
-
7
- export * from './ControlSection';
8
5
  export * from './FieldEditor';
9
6
  export * from './Form';
7
+ export * from './ObjectPicker';
8
+ export * from './Settings';
10
9
  export * from './ViewEditor';