@dxos/react-ui-form 0.8.3 → 0.8.4-main.1068cf700f

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 (218) hide show
  1. package/dist/lib/browser/index.mjs +2267 -2088
  2. package/dist/lib/browser/index.mjs.map +4 -4
  3. package/dist/lib/browser/meta.json +1 -1
  4. package/dist/lib/node-esm/index.mjs +2267 -2088
  5. package/dist/lib/node-esm/index.mjs.map +4 -4
  6. package/dist/lib/node-esm/meta.json +1 -1
  7. package/dist/types/src/components/FieldEditor/FieldEditor.d.ts +8 -8
  8. package/dist/types/src/components/FieldEditor/FieldEditor.d.ts.map +1 -1
  9. package/dist/types/src/components/FieldEditor/FieldEditor.stories.d.ts +84 -7
  10. package/dist/types/src/components/FieldEditor/FieldEditor.stories.d.ts.map +1 -1
  11. package/dist/types/src/components/Form/Form-old.stories.d.ts +131 -0
  12. package/dist/types/src/components/Form/Form-old.stories.d.ts.map +1 -0
  13. package/dist/types/src/components/Form/Form.d.ts +81 -25
  14. package/dist/types/src/components/Form/Form.d.ts.map +1 -1
  15. package/dist/types/src/components/Form/Form.stories.d.ts +127 -55
  16. package/dist/types/src/components/Form/Form.stories.d.ts.map +1 -1
  17. package/dist/types/src/components/Form/FormField.d.ts +37 -0
  18. package/dist/types/src/components/Form/FormField.d.ts.map +1 -0
  19. package/dist/types/src/components/Form/FormFieldComponent.d.ts +78 -0
  20. package/dist/types/src/components/Form/FormFieldComponent.d.ts.map +1 -0
  21. package/dist/types/src/components/Form/FormFieldComponent.stories.d.ts +9 -0
  22. package/dist/types/src/components/Form/FormFieldComponent.stories.d.ts.map +1 -0
  23. package/dist/types/src/components/Form/FormFieldSet.d.ts +19 -0
  24. package/dist/types/src/components/Form/FormFieldSet.d.ts.map +1 -0
  25. package/dist/types/src/components/Form/fields/ArrayField.d.ts +18 -0
  26. package/dist/types/src/components/Form/fields/ArrayField.d.ts.map +1 -0
  27. package/dist/types/src/components/Form/fields/BooleanField.d.ts +4 -0
  28. package/dist/types/src/components/Form/fields/BooleanField.d.ts.map +1 -0
  29. package/dist/types/src/components/Form/fields/GeoPointField.d.ts +5 -0
  30. package/dist/types/src/components/Form/fields/GeoPointField.d.ts.map +1 -0
  31. package/dist/types/src/components/Form/fields/MarkdownField.d.ts +4 -0
  32. package/dist/types/src/components/Form/fields/MarkdownField.d.ts.map +1 -0
  33. package/dist/types/src/components/Form/fields/NumberField.d.ts +4 -0
  34. package/dist/types/src/components/Form/fields/NumberField.d.ts.map +1 -0
  35. package/dist/types/src/components/Form/fields/RefField.d.ts +14 -0
  36. package/dist/types/src/components/Form/fields/RefField.d.ts.map +1 -0
  37. package/dist/types/src/components/Form/fields/SelectField.d.ts +10 -0
  38. package/dist/types/src/components/Form/fields/SelectField.d.ts.map +1 -0
  39. package/dist/types/src/components/Form/fields/SelectOptionField.d.ts +5 -0
  40. package/dist/types/src/components/Form/fields/SelectOptionField.d.ts.map +1 -0
  41. package/dist/types/src/components/Form/fields/SelectOptionField.stories.d.ts +82 -0
  42. package/dist/types/src/components/Form/fields/SelectOptionField.stories.d.ts.map +1 -0
  43. package/dist/types/src/components/Form/fields/TextAreaField.d.ts +4 -0
  44. package/dist/types/src/components/Form/fields/TextAreaField.d.ts.map +1 -0
  45. package/dist/types/src/components/Form/fields/TextField.d.ts +4 -0
  46. package/dist/types/src/components/Form/fields/TextField.d.ts.map +1 -0
  47. package/dist/types/src/components/Form/fields/TupleField.d.ts +6 -0
  48. package/dist/types/src/components/Form/fields/TupleField.d.ts.map +1 -0
  49. package/dist/types/src/components/Form/fields/index.d.ts +12 -0
  50. package/dist/types/src/components/Form/fields/index.d.ts.map +1 -0
  51. package/dist/types/src/components/Form/index.d.ts +2 -4
  52. package/dist/types/src/components/Form/index.d.ts.map +1 -1
  53. package/dist/types/src/components/ObjectPicker/ObjectPicker.d.ts +46 -0
  54. package/dist/types/src/components/ObjectPicker/ObjectPicker.d.ts.map +1 -0
  55. package/dist/types/src/components/ObjectPicker/ObjectPicker.stories.d.ts +85 -0
  56. package/dist/types/src/components/ObjectPicker/ObjectPicker.stories.d.ts.map +1 -0
  57. package/dist/types/src/components/ObjectPicker/index.d.ts +2 -0
  58. package/dist/types/src/components/ObjectPicker/index.d.ts.map +1 -0
  59. package/dist/types/src/components/Settings/Settings.d.ts +56 -0
  60. package/dist/types/src/components/Settings/Settings.d.ts.map +1 -0
  61. package/dist/types/src/components/Settings/Settings.stories.d.ts +81 -0
  62. package/dist/types/src/components/Settings/Settings.stories.d.ts.map +1 -0
  63. package/dist/types/src/components/Settings/index.d.ts +2 -0
  64. package/dist/types/src/components/Settings/index.d.ts.map +1 -0
  65. package/dist/types/src/components/ViewEditor/ViewEditor.d.ts +21 -9
  66. package/dist/types/src/components/ViewEditor/ViewEditor.d.ts.map +1 -1
  67. package/dist/types/src/components/ViewEditor/ViewEditor.stories.d.ts +84 -9
  68. package/dist/types/src/components/ViewEditor/ViewEditor.stories.d.ts.map +1 -1
  69. package/dist/types/src/components/index.d.ts +2 -2
  70. package/dist/types/src/components/index.d.ts.map +1 -1
  71. package/dist/types/src/components/testing.d.ts +4 -3
  72. package/dist/types/src/components/testing.d.ts.map +1 -1
  73. package/dist/types/src/hooks/index.d.ts +2 -3
  74. package/dist/types/src/hooks/index.d.ts.map +1 -1
  75. package/dist/types/src/hooks/useFormHandler.d.ts +82 -0
  76. package/dist/types/src/hooks/useFormHandler.d.ts.map +1 -0
  77. package/dist/types/src/hooks/useKeyHandler.d.ts +6 -0
  78. package/dist/types/src/hooks/useKeyHandler.d.ts.map +1 -0
  79. package/dist/types/src/index.d.ts +1 -1
  80. package/dist/types/src/index.d.ts.map +1 -1
  81. package/dist/types/src/translations.d.ts +66 -58
  82. package/dist/types/src/translations.d.ts.map +1 -1
  83. package/dist/types/src/util/echo.d.ts +6 -0
  84. package/dist/types/src/util/echo.d.ts.map +1 -0
  85. package/dist/types/src/util/field.d.ts +3 -3
  86. package/dist/types/src/util/field.d.ts.map +1 -1
  87. package/dist/types/src/util/formatting.d.ts +2 -2
  88. package/dist/types/src/util/formatting.d.ts.map +1 -1
  89. package/dist/types/src/util/index.d.ts +2 -1
  90. package/dist/types/src/util/index.d.ts.map +1 -1
  91. package/dist/types/src/util/properties.d.ts +7 -0
  92. package/dist/types/src/util/properties.d.ts.map +1 -0
  93. package/dist/types/src/util/refs.d.ts +2 -3
  94. package/dist/types/src/util/refs.d.ts.map +1 -1
  95. package/dist/types/tsconfig.tsbuildinfo +1 -1
  96. package/package.json +51 -42
  97. package/src/components/FieldEditor/FieldEditor.stories.tsx +54 -31
  98. package/src/components/FieldEditor/FieldEditor.test.tsx +14 -13
  99. package/src/components/FieldEditor/FieldEditor.tsx +87 -92
  100. package/src/components/Form/Form-old.stories.tsx +314 -0
  101. package/src/components/Form/Form.stories.tsx +162 -223
  102. package/src/components/Form/Form.tsx +329 -96
  103. package/src/components/Form/FormField.tsx +286 -0
  104. package/src/components/Form/FormFieldComponent.stories.tsx +65 -0
  105. package/src/components/Form/FormFieldComponent.tsx +174 -0
  106. package/src/components/Form/FormFieldSet.tsx +127 -0
  107. package/src/components/Form/README.md +3 -0
  108. package/src/components/Form/fields/ArrayField.tsx +154 -0
  109. package/src/components/Form/fields/BooleanField.tsx +22 -0
  110. package/src/components/Form/fields/GeoPointField.tsx +120 -0
  111. package/src/components/Form/fields/MarkdownField.tsx +78 -0
  112. package/src/components/Form/fields/NumberField.tsx +39 -0
  113. package/src/components/Form/fields/RefField.tsx +189 -0
  114. package/src/components/Form/fields/SelectField.tsx +65 -0
  115. package/src/components/Form/fields/SelectOptionField.stories.tsx +64 -0
  116. package/src/components/Form/{custom/SelectOptionsInput.tsx → fields/SelectOptionField.tsx} +47 -42
  117. package/src/components/Form/fields/TextAreaField.tsx +38 -0
  118. package/src/components/Form/fields/TextField.tsx +38 -0
  119. package/src/components/Form/{custom/TupleInput.tsx → fields/TupleField.tsx} +16 -9
  120. package/src/components/Form/fields/index.ts +15 -0
  121. package/src/components/Form/index.ts +3 -4
  122. package/src/components/ObjectPicker/ObjectPicker.stories.tsx +214 -0
  123. package/src/components/ObjectPicker/ObjectPicker.tsx +175 -0
  124. package/src/components/{ControlSection → ObjectPicker}/index.ts +1 -1
  125. package/src/components/Settings/Settings.stories.tsx +56 -0
  126. package/src/components/Settings/Settings.tsx +234 -0
  127. package/src/components/Settings/index.ts +5 -0
  128. package/src/components/ViewEditor/ViewEditor.stories.tsx +143 -61
  129. package/src/components/ViewEditor/ViewEditor.test.tsx +43 -43
  130. package/src/components/ViewEditor/ViewEditor.tsx +314 -216
  131. package/src/components/index.ts +2 -3
  132. package/src/components/testing.tsx +19 -11
  133. package/src/hooks/index.ts +2 -3
  134. package/src/hooks/useFormHandler.ts +362 -0
  135. package/src/hooks/useKeyHandler.tsx +37 -0
  136. package/src/index.ts +1 -1
  137. package/src/translations.ts +25 -14
  138. package/src/util/echo.ts +25 -0
  139. package/src/util/field.ts +33 -43
  140. package/src/util/formatting.ts +22 -22
  141. package/src/util/index.ts +2 -1
  142. package/src/util/properties.ts +18 -0
  143. package/src/util/refs.ts +12 -18
  144. package/dist/lib/node/index.cjs +0 -2416
  145. package/dist/lib/node/index.cjs.map +0 -7
  146. package/dist/lib/node/meta.json +0 -1
  147. package/dist/types/src/components/ControlSection/ControlSection.d.ts +0 -23
  148. package/dist/types/src/components/ControlSection/ControlSection.d.ts.map +0 -1
  149. package/dist/types/src/components/ControlSection/ControlSection.stories.d.ts +0 -8
  150. package/dist/types/src/components/ControlSection/ControlSection.stories.d.ts.map +0 -1
  151. package/dist/types/src/components/ControlSection/index.d.ts +0 -2
  152. package/dist/types/src/components/ControlSection/index.d.ts.map +0 -1
  153. package/dist/types/src/components/Form/ArrayField.d.ts +0 -16
  154. package/dist/types/src/components/Form/ArrayField.d.ts.map +0 -1
  155. package/dist/types/src/components/Form/Defaults.d.ts +0 -14
  156. package/dist/types/src/components/Form/Defaults.d.ts.map +0 -1
  157. package/dist/types/src/components/Form/FormActions.d.ts +0 -9
  158. package/dist/types/src/components/Form/FormActions.d.ts.map +0 -1
  159. package/dist/types/src/components/Form/FormContent.d.ts +0 -57
  160. package/dist/types/src/components/Form/FormContent.d.ts.map +0 -1
  161. package/dist/types/src/components/Form/FormContext.d.ts +0 -23
  162. package/dist/types/src/components/Form/FormContext.d.ts.map +0 -1
  163. package/dist/types/src/components/Form/Input.d.ts +0 -26
  164. package/dist/types/src/components/Form/Input.d.ts.map +0 -1
  165. package/dist/types/src/components/Form/RefField.d.ts +0 -12
  166. package/dist/types/src/components/Form/RefField.d.ts.map +0 -1
  167. package/dist/types/src/components/Form/custom/GeoPointInput.d.ts +0 -4
  168. package/dist/types/src/components/Form/custom/GeoPointInput.d.ts.map +0 -1
  169. package/dist/types/src/components/Form/custom/SelectOptionsInput.d.ts +0 -4
  170. package/dist/types/src/components/Form/custom/SelectOptionsInput.d.ts.map +0 -1
  171. package/dist/types/src/components/Form/custom/SelectOptionsInput.stories.d.ts +0 -7
  172. package/dist/types/src/components/Form/custom/SelectOptionsInput.stories.d.ts.map +0 -1
  173. package/dist/types/src/components/Form/custom/TupleInput.d.ts +0 -6
  174. package/dist/types/src/components/Form/custom/TupleInput.d.ts.map +0 -1
  175. package/dist/types/src/components/Form/custom/index.d.ts +0 -4
  176. package/dist/types/src/components/Form/custom/index.d.ts.map +0 -1
  177. package/dist/types/src/components/Form/factory.d.ts +0 -8
  178. package/dist/types/src/components/Form/factory.d.ts.map +0 -1
  179. package/dist/types/src/components/ViewEditor/Test.stories.d.ts +0 -10
  180. package/dist/types/src/components/ViewEditor/Test.stories.d.ts.map +0 -1
  181. package/dist/types/src/components/deprecated/Form.d.ts +0 -15
  182. package/dist/types/src/components/deprecated/Form.d.ts.map +0 -1
  183. package/dist/types/src/components/deprecated/Form.stories.d.ts +0 -13
  184. package/dist/types/src/components/deprecated/Form.stories.d.ts.map +0 -1
  185. package/dist/types/src/components/deprecated/FormInput.d.ts +0 -16
  186. package/dist/types/src/components/deprecated/FormInput.d.ts.map +0 -1
  187. package/dist/types/src/components/deprecated/index.d.ts +0 -2
  188. package/dist/types/src/components/deprecated/index.d.ts.map +0 -1
  189. package/dist/types/src/hooks/useForm.d.ts +0 -67
  190. package/dist/types/src/hooks/useForm.d.ts.map +0 -1
  191. package/dist/types/src/hooks/useQueryRefOptions.d.ts +0 -23
  192. package/dist/types/src/hooks/useQueryRefOptions.d.ts.map +0 -1
  193. package/dist/types/src/hooks/useRefQueryLookupHandler.d.ts +0 -8
  194. package/dist/types/src/hooks/useRefQueryLookupHandler.d.ts.map +0 -1
  195. package/dist/types/src/util/schema.d.ts +0 -3
  196. package/dist/types/src/util/schema.d.ts.map +0 -1
  197. package/src/components/ControlSection/ControlSection.stories.tsx +0 -41
  198. package/src/components/ControlSection/ControlSection.tsx +0 -139
  199. package/src/components/Form/ArrayField.tsx +0 -124
  200. package/src/components/Form/Defaults.tsx +0 -203
  201. package/src/components/Form/FormActions.tsx +0 -53
  202. package/src/components/Form/FormContent.tsx +0 -240
  203. package/src/components/Form/FormContext.tsx +0 -110
  204. package/src/components/Form/Input.tsx +0 -51
  205. package/src/components/Form/RefField.tsx +0 -213
  206. package/src/components/Form/custom/GeoPointInput.tsx +0 -72
  207. package/src/components/Form/custom/SelectOptionsInput.stories.tsx +0 -61
  208. package/src/components/Form/custom/index.ts +0 -7
  209. package/src/components/Form/factory.tsx +0 -31
  210. package/src/components/ViewEditor/Test.stories.tsx +0 -45
  211. package/src/components/deprecated/Form.stories.tsx +0 -62
  212. package/src/components/deprecated/Form.tsx +0 -119
  213. package/src/components/deprecated/FormInput.tsx +0 -71
  214. package/src/components/deprecated/index.ts +0 -5
  215. package/src/hooks/useForm.ts +0 -291
  216. package/src/hooks/useQueryRefOptions.ts +0 -53
  217. package/src/hooks/useRefQueryLookupHandler.ts +0 -39
  218. package/src/util/schema.ts +0 -14
@@ -2,284 +2,382 @@
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 { Filter, Format, Obj, Query, QueryAST, type SchemaRegistry } from '@dxos/echo';
13
+ import { EchoSchema, type JsonProp, isMutable, toJsonSchema } from '@dxos/echo/internal';
10
14
  import { invariant } from '@dxos/invariant';
11
- import { Callout, IconButton, type ThemedClassName, useTranslation } from '@dxos/react-ui';
15
+ import { useObject } from '@dxos/react-client/echo';
16
+ import { Callout, IconButton, Input, type ThemedClassName, useTranslation } from '@dxos/react-ui';
17
+ import { QueryForm, type QueryFormProps } from '@dxos/react-ui-components';
12
18
  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, type ViewType, ViewProjection, VIEW_FIELD_LIMIT } from '@dxos/schema';
19
+ import {
20
+ FieldSchema,
21
+ type FieldType,
22
+ ProjectionModel,
23
+ VIEW_FIELD_LIMIT,
24
+ type View,
25
+ createEchoChangeCallback,
26
+ getTypenameFromQuery,
27
+ } from '@dxos/schema';
28
+ import { mx, osTranslations, subtleHover } from '@dxos/ui-theme';
16
29
 
17
30
  import { translationKey } from '../../translations';
18
31
  import { FieldEditor } from '../FieldEditor';
19
- import { Form, type FormProps } from '../Form';
32
+ import {
33
+ Form,
34
+ type FormFieldComponent,
35
+ type FormFieldComponentProps,
36
+ FormFieldLabel,
37
+ type FormFieldMap,
38
+ } from '../Form';
20
39
 
21
- const grid = 'grid grid-cols-[32px_1fr_32px_32px] min-bs-[2.5rem]';
22
-
23
- const ViewMetaSchema = Schema.Struct({
24
- name: Schema.String.annotations({
25
- title: 'View',
26
- }),
27
- typename: Format.URL.annotations({
28
- title: 'Typename',
29
- }),
30
- }).pipe(Schema.mutable);
31
-
32
- type ViewMetaType = Schema.Schema.Type<typeof ViewMetaSchema>;
40
+ const listGrid = 'grid grid-cols-[min-content_1fr_min-content_min-content_min-content]';
41
+ const listItemGrid = 'grid grid-cols-subgrid col-span-5';
33
42
 
34
43
  export type ViewEditorProps = ThemedClassName<
35
44
  {
36
45
  schema: Schema.Schema.AnyNoContext;
37
- view: ViewType;
38
- registry?: SchemaRegistry;
46
+ view: View.View;
47
+ mode?: 'schema' | 'tag';
48
+ registry?: SchemaRegistry.SchemaRegistry;
39
49
  readonly?: boolean;
40
50
  showHeading?: boolean;
41
- onTypenameChanged?: (typename: string) => void;
51
+ onQueryChanged?: (query: QueryAST.Query, target?: string) => void;
42
52
  onDelete?: (fieldId: string) => void;
43
- } & Pick<FormProps<any>, 'outerSpacing'>
53
+ } & Pick<QueryFormProps, 'types' | 'tags'>
44
54
  >;
45
55
 
46
56
  /**
47
57
  * Schema-based object form.
48
58
  */
49
- export const ViewEditor = ({
50
- classNames,
51
- schema,
52
- view,
53
- registry,
54
- readonly: _readonly,
55
- showHeading = false,
56
- onTypenameChanged,
57
- onDelete,
58
- outerSpacing = true,
59
- }: ViewEditorProps) => {
59
+ export const ViewEditor = forwardRef<ProjectionModel, ViewEditorProps>(
60
+ (
61
+ {
62
+ classNames,
63
+ schema,
64
+ view,
65
+ mode = 'schema',
66
+ registry,
67
+ readonly,
68
+ showHeading = false,
69
+ types,
70
+ tags,
71
+ onQueryChanged,
72
+ onDelete,
73
+ },
74
+ forwardedRef,
75
+ ) => {
76
+ const atomRegistry = useContext(RegistryContext);
77
+ const schemaReadonly = !isMutable(schema);
78
+ const { t } = useTranslation(translationKey);
79
+
80
+ const projectionModel = useMemo(() => {
81
+ // Use reactive and mutable version of json schema when schema is mutable.
82
+ const jsonSchema = schema instanceof EchoSchema ? schema.jsonSchema : toJsonSchema(schema);
83
+
84
+ // Always use createEchoChangeCallback since the view is ECHO-backed.
85
+ // Pass schema only when mutable to allow schema mutations.
86
+ const change = createEchoChangeCallback(view, schema instanceof EchoSchema ? schema : undefined);
87
+
88
+ const model = new ProjectionModel({
89
+ registry: atomRegistry,
90
+ view,
91
+ baseSchema: jsonSchema,
92
+ change,
93
+ });
94
+
95
+ return model;
96
+ }, [atomRegistry, schema, view]);
97
+
98
+ useImperativeHandle(forwardedRef, () => projectionModel, [projectionModel]);
99
+
100
+ const queueTarget = Match.value(view.query.ast).pipe(
101
+ Match.when({ type: 'options' }, ({ options }) => {
102
+ return Option.fromNullable(options.queues).pipe(
103
+ Option.flatMap((queues) => Array.head(queues)),
104
+ Option.getOrUndefined,
105
+ );
106
+ }),
107
+ Match.orElse(() => undefined),
108
+ );
109
+
110
+ const viewSchema = useMemo(() => {
111
+ const base = Schema.Struct({
112
+ query:
113
+ mode === 'schema'
114
+ ? Format.URL.annotations({ title: 'Record type' })
115
+ : QueryAST.Query.annotations({ title: 'Query' }),
116
+ });
117
+
118
+ if (mode === 'tag') {
119
+ return Schema.Struct({
120
+ ...base.fields,
121
+ target: Schema.optional(Schema.String.annotations({ title: 'Target Queue' })),
122
+ }).pipe(Schema.mutable);
123
+ }
124
+
125
+ return base.pipe(Schema.mutable);
126
+ }, [mode]);
127
+
128
+ // TODO(burdon): Need to warn user of possible consequences of editing.
129
+ // TODO(burdon): Settings should have domain name owned by user.
130
+ const viewValues = useMemo(
131
+ () => ({
132
+ query: mode === 'schema' ? getTypenameFromQuery(view.query.ast) : view.query.ast,
133
+ target: queueTarget,
134
+ }),
135
+ [mode, view.query.ast, queueTarget],
136
+ );
137
+
138
+ const fieldMap = useMemo<FormFieldMap | undefined>(
139
+ () => (mode === 'tag' ? customFields({ types, tags }) : undefined),
140
+ [mode, types, tags],
141
+ );
142
+
143
+ const handleUpdate = useCallback(
144
+ (values: any) => {
145
+ requestAnimationFrame(() => {
146
+ const query = mode === 'schema' ? Query.select(Filter.typename(values.query)).ast : values.query;
147
+ onQueryChanged?.(query, values.target);
148
+ });
149
+ },
150
+ [onQueryChanged, view, queueTarget, mode],
151
+ );
152
+
153
+ const handleDelete = useCallback(
154
+ (fieldId: string) => {
155
+ invariant(!readonly);
156
+ onDelete?.(fieldId);
157
+ },
158
+ [onDelete, readonly],
159
+ );
160
+
161
+ return (
162
+ <div role='none' className={mx(classNames)}>
163
+ {/* If readonly is set, then the callout is not needed. */}
164
+ {schemaReadonly && !readonly && (
165
+ <Callout.Root valence='info' classNames='mlb-cardSpacingBlock'>
166
+ <Callout.Title>{t('system schema description')}</Callout.Title>
167
+ </Callout.Root>
168
+ )}
169
+
170
+ {/* TODO(burdon): Is the form read-only or just the schema? */}
171
+ <Form.Root schema={viewSchema} values={viewValues} fieldMap={fieldMap} autoSave onSave={handleUpdate}>
172
+ <Form.FieldSet />
173
+
174
+ <FormFieldLabel label={t('fields label')} asChild />
175
+ <FieldList
176
+ schema={schema}
177
+ view={view}
178
+ registry={registry}
179
+ readonly={readonly}
180
+ showHeading={showHeading}
181
+ onDelete={handleDelete}
182
+ />
183
+ </Form.Root>
184
+ </div>
185
+ );
186
+ },
187
+ );
188
+
189
+ type FieldListProps = Pick<ViewEditorProps, 'schema' | 'view' | 'registry' | 'readonly' | 'showHeading' | 'onDelete'>;
190
+
191
+ const FieldList = ({ schema, view, registry, readonly, showHeading = false, onDelete }: FieldListProps) => {
192
+ const atomRegistry = useContext(RegistryContext);
193
+ const schemaReadonly = !isMutable(schema);
60
194
  const { t } = useTranslation(translationKey);
61
- const projection = useMemo(() => {
195
+
196
+ // Subscribe to view changes for reactivity.
197
+ const [viewSnapshot] = useObject(view);
198
+
199
+ const projectionModel = useMemo(() => {
62
200
  // Use reactive and mutable version of json schema when schema is mutable.
63
201
  const jsonSchema = schema instanceof EchoSchema ? schema.jsonSchema : toJsonSchema(schema);
64
- return new ViewProjection(jsonSchema, view);
65
- }, [schema, view]);
66
- const [field, setField] = useState<FieldType>();
67
- const readonly = _readonly || !isMutable(schema);
68
-
69
- // TODO(burdon): Should be reactive.
70
- const viewValues = useMemo(() => {
71
- return {
72
- name: view.name,
73
- // TODO(burdon): Need to warn user of possible consequences of editing.
74
- // TODO(burdon): Settings should have domain name owned by user.
75
- typename: view.query.typename,
76
- };
77
- }, [view]);
78
-
79
- const handleSelect = useCallback(
80
- (field: FieldType) => {
81
- if (readonly) {
82
- return;
83
- }
84
- setField((f) => (f === field ? undefined : field));
85
- },
86
- [readonly],
87
- );
88
202
 
89
- // TODO(burdon): Check if mutable; variant of useCallback that return undefined if readonly?
203
+ // Always use createEchoChangeCallback since the view is ECHO-backed.
204
+ // Pass schema only when mutable to allow schema mutations.
205
+ const change = createEchoChangeCallback(view, schema instanceof EchoSchema ? schema : undefined);
206
+
207
+ const model = new ProjectionModel({
208
+ registry: atomRegistry,
209
+ view,
210
+ baseSchema: jsonSchema,
211
+ change,
212
+ });
213
+
214
+ return model;
215
+ }, [atomRegistry, schema, view]);
216
+
217
+ const [expandedField, setExpandedField] = useState<FieldType['id']>();
90
218
 
91
219
  const handleAdd = useCallback(() => {
92
220
  invariant(!readonly);
93
- const field = projection.createFieldProjection();
94
- setField(field);
95
- }, [schema, view, readonly]);
221
+ const field = projectionModel.createFieldProjection();
222
+ setExpandedField(field.id);
223
+ }, [projectionModel, readonly]);
96
224
 
97
- const handleUpdate = useCallback(
98
- ({ name, typename }: ViewMetaType) => {
99
- invariant(!readonly);
100
- requestAnimationFrame(() => {
101
- if (view.name !== name) {
102
- view.name = name;
103
- }
104
-
105
- if (view.query.typename !== typename && !readonly) {
106
- onTypenameChanged?.(typename);
107
- }
108
- });
225
+ const handleToggleField = useCallback(
226
+ (field: FieldType) => {
227
+ setExpandedField((prevExpandedFieldId) => (prevExpandedFieldId === field.id ? undefined : field.id));
109
228
  },
110
- [schema, view, onTypenameChanged, readonly],
229
+ [readonly],
111
230
  );
112
231
 
113
232
  const handleDelete = useCallback(
114
233
  (fieldId: string) => {
115
234
  invariant(!readonly);
116
- if (fieldId === field?.id) {
117
- setField(undefined);
235
+ if (fieldId === expandedField) {
236
+ setExpandedField(undefined);
118
237
  }
119
238
 
120
239
  onDelete?.(fieldId);
121
240
  },
122
- [schema, field, onDelete, readonly],
241
+ [expandedField, onDelete, readonly],
123
242
  );
124
243
 
125
244
  const handleMove = useCallback(
126
245
  (fromIndex: number, toIndex: number) => {
127
246
  invariant(!readonly);
128
- // NOTE(ZaymonFC): Using arrayMove here causes a race condition with the kanban model.
129
- const fields = [...view.fields];
130
- const [moved] = fields.splice(fromIndex, 1);
131
- fields.splice(toIndex, 0, moved);
132
- view.fields = fields;
247
+ Obj.change(view, (v) => {
248
+ // NOTE(ZaymonFC): Using arrayMove here causes a race condition with the kanban model.
249
+ const fields = [...v.projection.fields];
250
+ const [moved] = fields.splice(fromIndex, 1);
251
+ fields.splice(toIndex, 0, moved);
252
+ v.projection.fields = fields;
253
+ });
133
254
  },
134
- [schema, view.fields, readonly],
255
+ [view, readonly],
135
256
  );
136
257
 
137
- const handleClose = useCallback(() => setField(undefined), []);
138
-
139
- const hiddenProperties = projection.getHiddenProperties();
258
+ const handleClose = useCallback(() => setExpandedField(undefined), []);
140
259
 
141
260
  const handleHide = useCallback(
142
261
  (fieldId: string) => {
143
- setField(undefined);
144
- projection.hideFieldProjection(fieldId);
262
+ setExpandedField(undefined);
263
+ projectionModel.hideFieldProjection(fieldId);
145
264
  },
146
- [projection],
265
+ [projectionModel],
147
266
  );
148
267
 
149
268
  const handleShow = useCallback(
150
269
  (property: string) => {
151
- setField(undefined);
152
- projection.showFieldProjection(property as JsonProp);
270
+ setExpandedField(undefined);
271
+ projectionModel.showFieldProjection(property as JsonProp);
153
272
  },
154
- [projection],
273
+ [projectionModel],
155
274
  );
156
275
 
157
- return (
158
- <div role='none' className={mx(classNames)}>
159
- {readonly && (
160
- <div role='none' className={mx('is-full', outerSpacing && 'plb-cardSpacingBlock pli-cardSpacingInline')}>
161
- <Callout.Root valence='info'>
162
- <Callout.Title>{t('system schema description')}</Callout.Title>
163
- </Callout.Root>
164
- </div>
165
- )}
276
+ if (!viewSnapshot) {
277
+ return null;
278
+ }
166
279
 
167
- {/* TODO(burdon): Is the form read-only or just the schema? */}
168
- {/* TODO(burdon): Readonly fields should take up the same space as editable fields (just be ghosted). */}
169
- <Form<ViewMetaType>
170
- autoSave
171
- schema={ViewMetaSchema}
172
- values={viewValues}
173
- readonly={readonly}
174
- onSave={handleUpdate}
175
- outerSpacing={outerSpacing}
176
- />
177
-
178
- <div role='none' className={outerSpacing ? cardSpacing : 'mlb-cardSpacingBlock'}>
179
- <label className={mx(inputTextLabel)}>{t('fields label')}</label>
180
-
181
- <List.Root<FieldType>
182
- items={view.fields}
183
- isItem={Schema.is(FieldSchema)}
184
- getId={(field) => field.id}
185
- readonly={readonly}
186
- onMove={readonly ? undefined : handleMove}
187
- >
188
- {({ items: fields }) => (
189
- <>
190
- {showHeading && (
191
- <div role='heading' className={grid}>
192
- <div />
193
- <div className='flex items-center text-sm'>{t('field path label')}</div>
194
- </div>
195
- )}
196
-
197
- <div role='list' className='flex flex-col is-full'>
198
- {fields?.map((field) => (
199
- <List.Item<FieldType>
200
- key={field.id}
201
- item={field}
202
- classNames={mx(grid, ghostHover, 'overflow-hidden', !readonly && 'cursor-pointer')}
203
- >
204
- <List.ItemDragHandle />
205
- <List.ItemTitle onClick={() => handleSelect(field)}>{field.path}</List.ItemTitle>
280
+ return (
281
+ <List.Root<FieldType>
282
+ items={viewSnapshot.projection.fields as FieldType[]}
283
+ isItem={Schema.is(FieldSchema)}
284
+ getId={(field) => field.id}
285
+ onMove={readonly ? undefined : handleMove}
286
+ readonly={readonly}
287
+ >
288
+ {({ items: fields }) => (
289
+ <>
290
+ {showHeading && <h3 className='text-sm'>{t('field path label')}</h3>}
291
+ <div role='list' className={listGrid}>
292
+ {fields?.map((field) => {
293
+ const hidden = field.visible === false;
294
+ return (
295
+ <List.Item<FieldType>
296
+ key={field.id}
297
+ item={field}
298
+ classNames={listItemGrid}
299
+ aria-expanded={expandedField === field.id}
300
+ >
301
+ <div role='none' className={mx(subtleHover, listItemGrid, 'rounded-sm cursor-pointer min-bs-10')}>
302
+ <List.ItemDragHandle disabled={readonly || schemaReadonly} />
303
+ <List.ItemTitle classNames={hidden && 'text-subdued'} onClick={() => handleToggleField(field)}>
304
+ {field.path}
305
+ </List.ItemTitle>
206
306
  <List.ItemButton
207
- data-testid='hide-field-button'
208
- icon='ph--eye-slash--regular'
209
- // TDOO(burdon): Is this the correct test?
210
- disabled={view.fields.length <= 1}
211
- onClick={() => handleHide(field.id)}
307
+ label={t(hidden ? 'show field label' : 'hide field label')}
308
+ data-testid={hidden ? 'show-field-button' : 'hide-field-button'}
309
+ icon={hidden ? 'ph--eye-closed--regular' : 'ph--eye--regular'}
310
+ autoHide={false}
311
+ disabled={readonly || (!hidden && projectionModel.getFields().length <= 1)}
312
+ onClick={() => (hidden ? handleShow(field.path) : handleHide(field.id))}
212
313
  />
213
- {/* TODO(burdon): Remove unless implement undo. */}
214
314
  {!readonly && (
215
- <List.ItemDeleteButton
216
- disabled={view.fields.length <= 1}
217
- onClick={() => handleDelete(field.id)}
218
- />
315
+ <>
316
+ <List.ItemDeleteButton
317
+ label={t('delete field label')}
318
+ autoHide={false}
319
+ disabled={readonly || schemaReadonly || viewSnapshot.projection.fields.length <= 1}
320
+ onClick={() => handleDelete(field.id)}
321
+ data-testid='field.delete'
322
+ />
323
+ <IconButton
324
+ iconOnly
325
+ variant='ghost'
326
+ label={t('toggle expand label', { ns: osTranslations })}
327
+ icon={expandedField === field.id ? 'ph--caret-down--regular' : 'ph--caret-right--regular'}
328
+ onClick={() => handleToggleField(field)}
329
+ data-testid='field.toggle'
330
+ />
331
+ </>
219
332
  )}
220
- </List.Item>
221
- ))}
222
- </div>
223
- </>
224
- )}
225
- </List.Root>
226
-
227
- {hiddenProperties.length > 0 && (
228
- <>
229
- <label className={mx(inputTextLabel)}>{t('hidden fields label')}</label>
230
-
231
- <List.Root<string>
232
- items={hiddenProperties}
233
- isItem={(item): item is string => typeof item === 'string'}
234
- getId={(property) => property}
235
- >
236
- {({ items: properties }) => (
237
- <div role='list' className='flex flex-col is-full'>
238
- {properties?.map((property) => (
239
- <List.Item<string>
240
- key={property}
241
- item={property}
242
- classNames={mx(grid, ghostHover, !readonly && 'cursor-pointer')}
243
- >
244
- <List.ItemDragHandle disabled />
245
- <List.ItemTitle>{property}</List.ItemTitle>
246
- <List.ItemButton
247
- icon='ph--eye--regular'
248
- onClick={() => handleShow(property)}
249
- data-testid='show-field-button'
333
+ </div>
334
+ {expandedField === field.id && !readonly && (
335
+ <div role='none' className='col-span-5 mbs-1 mbe-1 border border-separator rounded-md'>
336
+ <FieldEditor
337
+ readonly={readonly || schemaReadonly}
338
+ registry={registry}
339
+ projection={projectionModel}
340
+ field={field}
341
+ onSave={handleClose}
250
342
  />
251
- </List.Item>
252
- ))}
253
- </div>
254
- )}
255
- </List.Root>
256
- </>
257
- )}
258
- </div>
259
-
260
- {field && (
261
- <FieldEditor
262
- key={field.id}
263
- view={view}
264
- projection={projection}
265
- field={field}
266
- registry={registry}
267
- onSave={handleClose}
268
- />
269
- )}
270
-
271
- {!readonly && !field && (
272
- <div role='none' className={outerSpacing ? cardSpacing : 'mlb-cardSpacingBlock'}>
273
- <IconButton
274
- icon='ph--plus--regular'
275
- label={t('button add property')}
276
- onClick={readonly ? undefined : handleAdd}
277
- // TODO(burdon): Show field limit in ux (not tooltip).
278
- disabled={view.fields.length >= VIEW_FIELD_LIMIT}
279
- classNames='flex is-full'
280
- />
281
- </div>
343
+ </div>
344
+ )}
345
+ </List.Item>
346
+ );
347
+ })}
348
+ </div>
349
+ {!readonly && !expandedField && (
350
+ <div role='none' className='mlb-cardSpacingBlock'>
351
+ <IconButton
352
+ icon='ph--plus--regular'
353
+ label={t('add property button label')}
354
+ onClick={handleAdd}
355
+ disabled={viewSnapshot.projection.fields.length >= VIEW_FIELD_LIMIT}
356
+ classNames='is-full'
357
+ />
358
+ </div>
359
+ )}
360
+ </>
282
361
  )}
283
- </div>
362
+ </List.Root>
284
363
  );
285
364
  };
365
+
366
+ const customFields = ({
367
+ types,
368
+ tags,
369
+ }: Pick<ViewEditorProps, 'types' | 'tags'>): Record<string, FormFieldComponent> => ({
370
+ query: ({ type, readonly, label, getValue, onValueChange }: FormFieldComponentProps) => {
371
+ const handleChange = useCallback<NonNullable<QueryFormProps['onChange']>>(
372
+ (query) => onValueChange(type, query.ast),
373
+ [onValueChange],
374
+ );
375
+
376
+ return (
377
+ <Input.Root>
378
+ <FormFieldLabel readonly={readonly} label={label} />
379
+ <QueryForm initialQuery={getValue()} types={types} tags={tags} onChange={handleChange} />
380
+ </Input.Root>
381
+ );
382
+ },
383
+ });
@@ -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';
@@ -5,20 +5,28 @@
5
5
  import React, { type PropsWithChildren } from 'react';
6
6
 
7
7
  import { type ThemedClassName } from '@dxos/react-ui';
8
- import { Card } from '@dxos/react-ui-stack';
8
+ import { Card } from '@dxos/react-ui-mosaic';
9
9
  import { JsonFilter } from '@dxos/react-ui-syntax-highlighter';
10
- import { mx, textBlockWidth } from '@dxos/react-ui-theme';
10
+ import { textBlockWidth } from '@dxos/ui-theme';
11
11
 
12
- export const TestLayout = ({ classNames, json, children }: ThemedClassName<PropsWithChildren<{ json?: any }>>) => (
13
- <div className='w-full h-full grid grid-cols-[1fr_1fr] gap-2 overflow-hidden'>
14
- <div className={mx('p-4 justify-center overflow-y-auto', classNames)}>{children}</div>
15
- <JsonFilter data={json} classNames='w-full text-xs' testId='debug' />
16
- </div>
17
- );
12
+ type TestLayoutProps = ThemedClassName<PropsWithChildren<{ json?: any }>>;
18
13
 
19
- export const TestPanel = ({ classNames, children }: ThemedClassName<PropsWithChildren>) => (
20
- <Card.Content classNames={[textBlockWidth, classNames]}>{children}</Card.Content>
21
- );
14
+ export const TestLayout = ({ classNames, children, json }: TestLayoutProps) => {
15
+ return (
16
+ <div className='is-full bs-full grid grid-cols-[1fr_1fr] p-4 gap-4 overflow-hidden'>
17
+ <TestPanel classNames={['flex flex-col bs-full overflow-y-auto', classNames]}>{children}</TestPanel>
18
+ <TestPanel classNames={'overflow-hidden'}>
19
+ <JsonFilter testId='debug' data={json} classNames='bs-full text-xs' />
20
+ </TestPanel>
21
+ </div>
22
+ );
23
+ };
24
+
25
+ type TestPanelProps = ThemedClassName<PropsWithChildren>;
26
+
27
+ const TestPanel = ({ classNames, children }: TestPanelProps) => {
28
+ return <Card.Root classNames={['bs-full overflow-y-auto', textBlockWidth, classNames]}>{children}</Card.Root>;
29
+ };
22
30
 
23
31
  // Symbol for accessing debug objects in tests.
24
32
  export const VIEW_EDITOR_DEBUG_SYMBOL = Symbol('viewEditorDebug');
@@ -2,6 +2,5 @@
2
2
  // Copyright 2025 DXOS.org
3
3
  //
4
4
 
5
- export * from './useForm';
6
- export * from './useRefQueryLookupHandler';
7
- export * from './useQueryRefOptions';
5
+ export * from './useFormHandler';
6
+ export * from './useKeyHandler';