@dxos/react-ui-form 0.8.4-main.f9ba587 → 0.8.4-main.fffef41

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