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

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