@dxos/react-ui-form 0.8.4-main.2e9d522 → 0.8.4-main.3c1ae3b

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