@dxos/react-ui-form 0.8.4-main.7ace549 → 0.8.4-main.8360d9e660

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