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