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

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