@dxos/react-ui-form 0.8.4-main.84f28bd → 0.8.4-main.937b3ca

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