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

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