@dxos/react-ui-form 0.8.3 → 0.8.4-main.1f223c7

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 (103) hide show
  1. package/dist/lib/browser/index.mjs +648 -581
  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 +648 -581
  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 +79 -4
  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 +76 -6
  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 +126 -30
  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 +1 -1
  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 +10 -4
  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 +74 -3
  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 +5 -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 +78 -7
  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/index.d.ts +1 -1
  51. package/dist/types/src/index.d.ts.map +1 -1
  52. package/dist/types/src/translations.d.ts +61 -57
  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 +36 -33
  57. package/src/components/ControlSection/ControlSection.stories.tsx +10 -12
  58. package/src/components/ControlSection/ControlSection.tsx +26 -12
  59. package/src/components/FieldEditor/FieldEditor.stories.tsx +21 -20
  60. package/src/components/FieldEditor/FieldEditor.test.tsx +14 -13
  61. package/src/components/FieldEditor/FieldEditor.tsx +17 -22
  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 +131 -95
  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 +96 -34
  68. package/src/components/Form/FormContext.tsx +5 -5
  69. package/src/components/Form/Input.tsx +4 -1
  70. package/src/components/Form/RefField.tsx +126 -40
  71. package/src/components/Form/custom/GeoPointInput.tsx +29 -21
  72. package/src/components/Form/custom/SelectOptionsInput.stories.tsx +9 -8
  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 +70 -32
  77. package/src/components/ViewEditor/ViewEditor.test.tsx +38 -39
  78. package/src/components/ViewEditor/ViewEditor.tsx +159 -155
  79. package/src/components/index.ts +0 -2
  80. package/src/components/testing.tsx +15 -9
  81. package/src/hooks/useForm.ts +5 -3
  82. package/src/hooks/useRefQueryLookupHandler.ts +1 -1
  83. package/src/index.ts +1 -1
  84. package/src/translations.ts +15 -7
  85. package/src/util/field.ts +8 -18
  86. package/dist/lib/node/index.cjs +0 -2416
  87. package/dist/lib/node/index.cjs.map +0 -7
  88. package/dist/lib/node/meta.json +0 -1
  89. package/dist/types/src/components/ViewEditor/Test.stories.d.ts +0 -10
  90. package/dist/types/src/components/ViewEditor/Test.stories.d.ts.map +0 -1
  91. package/dist/types/src/components/deprecated/Form.d.ts +0 -15
  92. package/dist/types/src/components/deprecated/Form.d.ts.map +0 -1
  93. package/dist/types/src/components/deprecated/Form.stories.d.ts +0 -13
  94. package/dist/types/src/components/deprecated/Form.stories.d.ts.map +0 -1
  95. package/dist/types/src/components/deprecated/FormInput.d.ts +0 -16
  96. package/dist/types/src/components/deprecated/FormInput.d.ts.map +0 -1
  97. package/dist/types/src/components/deprecated/index.d.ts +0 -2
  98. package/dist/types/src/components/deprecated/index.d.ts.map +0 -1
  99. package/src/components/ViewEditor/Test.stories.tsx +0 -45
  100. package/src/components/deprecated/Form.stories.tsx +0 -62
  101. package/src/components/deprecated/Form.tsx +0 -119
  102. package/src/components/deprecated/FormInput.tsx +0 -71
  103. package/src/components/deprecated/index.ts +0 -5
@@ -2,43 +2,43 @@
2
2
  // Copyright 2024 DXOS.org
3
3
  //
4
4
 
5
- import { Schema } from 'effect';
5
+ import { Match, Schema } from 'effect';
6
6
  import React, { useCallback, 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 { getSpace } from '@dxos/react-client/echo';
11
12
  import { Callout, IconButton, type ThemedClassName, useTranslation } from '@dxos/react-ui';
13
+ import { QueryEditor, QuerySerializer, createExpression } from '@dxos/react-ui-components';
12
14
  import { List } from '@dxos/react-ui-list';
13
15
  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';
16
+ import { inputTextLabel, mx, subtleHover } from '@dxos/react-ui-theme';
17
+ import {
18
+ type DataType,
19
+ FieldSchema,
20
+ type FieldType,
21
+ ProjectionModel,
22
+ VIEW_FIELD_LIMIT,
23
+ typenameFromQuery,
24
+ } from '@dxos/schema';
16
25
 
17
26
  import { translationKey } from '../../translations';
18
27
  import { FieldEditor } from '../FieldEditor';
19
- import { Form, type FormProps } from '../Form';
28
+ import { Form, type FormProps, type InputComponent, type InputProps } from '../Form';
20
29
 
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>;
30
+ const listGrid = 'grid grid-cols-[min-content_1fr_min-content_min-content_min-content]';
31
+ const listItemGrid = 'grid grid-cols-subgrid col-span-5';
33
32
 
34
33
  export type ViewEditorProps = ThemedClassName<
35
34
  {
36
35
  schema: Schema.Schema.AnyNoContext;
37
- view: ViewType;
36
+ view: DataType.View;
37
+ kind?: 'basic' | 'advanced';
38
38
  registry?: SchemaRegistry;
39
39
  readonly?: boolean;
40
40
  showHeading?: boolean;
41
- onTypenameChanged?: (typename: string) => void;
41
+ onQueryChanged?: (query: string) => void;
42
42
  onDelete?: (fieldId: string) => void;
43
43
  } & Pick<FormProps<any>, 'outerSpacing'>
44
44
  >;
@@ -50,38 +50,52 @@ export const ViewEditor = ({
50
50
  classNames,
51
51
  schema,
52
52
  view,
53
+ kind = 'basic',
53
54
  registry,
54
- readonly: _readonly,
55
+ readonly,
55
56
  showHeading = false,
56
- onTypenameChanged,
57
+ onQueryChanged,
57
58
  onDelete,
58
59
  outerSpacing = true,
59
60
  }: ViewEditorProps) => {
61
+ const space = getSpace(view);
62
+ const schemaReadonly = !isMutable(schema);
60
63
  const { t } = useTranslation(translationKey);
61
- const projection = useMemo(() => {
64
+ const projectionModel = useMemo(() => {
62
65
  // Use reactive and mutable version of json schema when schema is mutable.
63
66
  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);
67
+ return new ProjectionModel(jsonSchema, view.projection);
68
+ }, [schema, view.projection]);
69
+ const [expandedField, setExpandedField] = useState<FieldType['id']>();
68
70
 
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]);
71
+ const serializedQuery = Match.value(kind).pipe(
72
+ Match.when('basic', () => typenameFromQuery(view.query)),
73
+ Match.when('advanced', () => {
74
+ if (view.query.type !== 'select') {
75
+ return '';
76
+ }
78
77
 
79
- const handleSelect = useCallback(
78
+ const serializer = new QuerySerializer();
79
+ return serializer.serialize(createExpression(view.query.filter));
80
+ }),
81
+ Match.exhaustive,
82
+ );
83
+
84
+ const viewSchema = useMemo(() => {
85
+ return Schema.Struct({
86
+ query:
87
+ kind === 'basic'
88
+ ? Format.URL.annotations({ title: 'Record type' })
89
+ : Schema.String.annotations({ title: 'Query' }),
90
+ }).pipe(Schema.mutable);
91
+ }, [kind]);
92
+ // TODO(burdon): Need to warn user of possible consequences of editing.
93
+ // TODO(burdon): Settings should have domain name owned by user.
94
+ const viewValues = useMemo(() => ({ query: serializedQuery }), [serializedQuery]);
95
+
96
+ const handleToggleField = useCallback(
80
97
  (field: FieldType) => {
81
- if (readonly) {
82
- return;
83
- }
84
- setField((f) => (f === field ? undefined : field));
98
+ setExpandedField((prevExpandedFieldId) => (prevExpandedFieldId === field.id ? undefined : field.id));
85
99
  },
86
100
  [readonly],
87
101
  );
@@ -90,193 +104,183 @@ export const ViewEditor = ({
90
104
 
91
105
  const handleAdd = useCallback(() => {
92
106
  invariant(!readonly);
93
- const field = projection.createFieldProjection();
94
- setField(field);
95
- }, [schema, view, readonly]);
107
+ const field = projectionModel.createFieldProjection();
108
+ setExpandedField(field.id);
109
+ }, [schema, projectionModel, readonly]);
96
110
 
97
111
  const handleUpdate = useCallback(
98
- ({ name, typename }: ViewMetaType) => {
112
+ ({ query }: Schema.Schema.Type<typeof viewSchema>) => {
99
113
  invariant(!readonly);
100
114
  requestAnimationFrame(() => {
101
- if (view.name !== name) {
102
- view.name = name;
103
- }
104
-
105
- if (view.query.typename !== typename && !readonly) {
106
- onTypenameChanged?.(typename);
115
+ if (serializedQuery !== query && !readonly) {
116
+ onQueryChanged?.(query);
107
117
  }
108
118
  });
109
119
  },
110
- [schema, view, onTypenameChanged, readonly],
120
+ [serializedQuery, onQueryChanged, readonly],
111
121
  );
112
122
 
113
123
  const handleDelete = useCallback(
114
124
  (fieldId: string) => {
115
125
  invariant(!readonly);
116
- if (fieldId === field?.id) {
117
- setField(undefined);
126
+ if (fieldId === expandedField) {
127
+ setExpandedField(undefined);
118
128
  }
119
129
 
120
130
  onDelete?.(fieldId);
121
131
  },
122
- [schema, field, onDelete, readonly],
132
+ [expandedField, onDelete, readonly],
123
133
  );
124
134
 
125
135
  const handleMove = useCallback(
126
136
  (fromIndex: number, toIndex: number) => {
127
137
  invariant(!readonly);
128
138
  // NOTE(ZaymonFC): Using arrayMove here causes a race condition with the kanban model.
129
- const fields = [...view.fields];
139
+ const fields = [...view.projection.fields];
130
140
  const [moved] = fields.splice(fromIndex, 1);
131
141
  fields.splice(toIndex, 0, moved);
132
- view.fields = fields;
142
+ view.projection.fields = fields;
133
143
  },
134
- [schema, view.fields, readonly],
144
+ [view.projection.fields, readonly],
135
145
  );
136
146
 
137
- const handleClose = useCallback(() => setField(undefined), []);
138
-
139
- const hiddenProperties = projection.getHiddenProperties();
147
+ const handleClose = useCallback(() => setExpandedField(undefined), []);
140
148
 
141
149
  const handleHide = useCallback(
142
150
  (fieldId: string) => {
143
- setField(undefined);
144
- projection.hideFieldProjection(fieldId);
151
+ setExpandedField(undefined);
152
+ projectionModel.hideFieldProjection(fieldId);
145
153
  },
146
- [projection],
154
+ [projectionModel],
147
155
  );
148
156
 
149
157
  const handleShow = useCallback(
150
158
  (property: string) => {
151
- setField(undefined);
152
- projection.showFieldProjection(property as JsonProp);
159
+ setExpandedField(undefined);
160
+ projectionModel.showFieldProjection(property as JsonProp);
153
161
  },
154
- [projection],
162
+ [projectionModel],
163
+ );
164
+
165
+ const custom: Partial<Record<string, InputComponent>> = useMemo(
166
+ () =>
167
+ kind === 'advanced'
168
+ ? {
169
+ ['query' satisfies keyof Schema.Schema.Type<typeof viewSchema>]: (props: InputProps) => {
170
+ const handleChange = useCallback(
171
+ (text: string) => props.onValueChange('string', text),
172
+ [props.onValueChange],
173
+ );
174
+
175
+ // TODO(wittjosiah): Add label & input styles.
176
+ return <QueryEditor space={space} initialValue={props.getValue()} onChange={handleChange} />;
177
+ },
178
+ }
179
+ : {},
180
+ [],
155
181
  );
156
182
 
157
183
  return (
158
184
  <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>
185
+ {schemaReadonly && (
186
+ <Callout.Root valence='info' classNames={['mlb-cardSpacingBlock', outerSpacing && 'mli-cardSpacingInline']}>
187
+ <Callout.Title>{t('system schema description')}</Callout.Title>
188
+ </Callout.Root>
165
189
  )}
166
190
 
167
191
  {/* TODO(burdon): Is the form read-only or just the schema? */}
168
192
  {/* TODO(burdon): Readonly fields should take up the same space as editable fields (just be ghosted). */}
169
- <Form<ViewMetaType>
193
+ <Form<Schema.Schema.Type<typeof viewSchema>>
170
194
  autoSave
171
- schema={ViewMetaSchema}
195
+ schema={viewSchema}
172
196
  values={viewValues}
173
- readonly={readonly}
197
+ readonly={readonly ? 'disabled-input' : false}
174
198
  onSave={handleUpdate}
175
199
  outerSpacing={outerSpacing}
200
+ Custom={custom}
176
201
  />
177
202
 
178
203
  <div role='none' className={outerSpacing ? cardSpacing : 'mlb-cardSpacingBlock'}>
179
- <label className={mx(inputTextLabel)}>{t('fields label')}</label>
204
+ <h2 className={mx(inputTextLabel)}>{t('fields label')}</h2>
180
205
 
181
206
  <List.Root<FieldType>
182
- items={view.fields}
207
+ items={view.projection.fields}
183
208
  isItem={Schema.is(FieldSchema)}
184
209
  getId={(field) => field.id}
185
- readonly={readonly}
186
210
  onMove={readonly ? undefined : handleMove}
211
+ readonly={readonly}
187
212
  >
188
213
  {({ items: fields }) => (
189
214
  <>
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
- ))}
215
+ {showHeading && <h3 className='text-sm'>{t('field path label')}</h3>}
216
+ <div role='list' className={listGrid}>
217
+ {fields?.map((field) => {
218
+ const hidden = field.visible === false;
219
+ return (
220
+ <List.Item<FieldType>
221
+ key={field.id}
222
+ item={field}
223
+ classNames={listItemGrid}
224
+ aria-expanded={expandedField === field.id}
225
+ >
226
+ <div role='none' className={mx(subtleHover, listItemGrid, 'rounded-sm cursor-pointer min-bs-10')}>
227
+ <List.ItemDragHandle disabled={readonly} />
228
+ <List.ItemTitle classNames={hidden && 'text-subdued'} onClick={() => handleToggleField(field)}>
229
+ {field.path}
230
+ </List.ItemTitle>
231
+ <List.ItemButton
232
+ label={t(hidden ? 'show field label' : 'hide field label')}
233
+ data-testid={hidden ? 'show-field-button' : 'hide-field-button'}
234
+ icon={hidden ? 'ph--eye-closed--regular' : 'ph--eye--regular'}
235
+ autoHide={false}
236
+ disabled={readonly || (!hidden && projectionModel.fields.length <= 1)}
237
+ onClick={() => (hidden ? handleShow(field.path) : handleHide(field.id))}
238
+ />
239
+ {/* TODO(burdon): Remove unless implement undo. */}
240
+ <List.ItemDeleteButton
241
+ label={t('delete field label')}
242
+ autoHide={false}
243
+ disabled={readonly || schemaReadonly || view.projection.fields.length <= 1}
244
+ onClick={() => handleDelete(field.id)}
245
+ data-testid='field.delete'
246
+ />
247
+ <IconButton
248
+ iconOnly
249
+ variant='ghost'
250
+ label={t('toggle expand label', { ns: 'os' })}
251
+ icon={expandedField === field.id ? 'ph--caret-down--regular' : 'ph--caret-right--regular'}
252
+ onClick={() => handleToggleField(field)}
253
+ />
254
+ </div>
255
+ {expandedField === field.id && (
256
+ <div role='none' className='col-span-5 mbs-1 mbe-1 border border-separator rounded-md'>
257
+ <FieldEditor
258
+ readonly={readonly || schemaReadonly ? 'disabled-input' : false}
259
+ projection={projectionModel}
260
+ field={field}
261
+ registry={registry}
262
+ onSave={handleClose}
263
+ />
264
+ </div>
265
+ )}
266
+ </List.Item>
267
+ );
268
+ })}
222
269
  </div>
223
270
  </>
224
271
  )}
225
272
  </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
- )}
258
273
  </div>
259
274
 
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
- )}
270
-
271
- {!readonly && !field && (
275
+ {!readonly && !expandedField && (
272
276
  <div role='none' className={outerSpacing ? cardSpacing : 'mlb-cardSpacingBlock'}>
273
277
  <IconButton
274
278
  icon='ph--plus--regular'
275
279
  label={t('button add property')}
276
280
  onClick={readonly ? undefined : handleAdd}
277
281
  // TODO(burdon): Show field limit in ux (not tooltip).
278
- disabled={view.fields.length >= VIEW_FIELD_LIMIT}
279
- classNames='flex is-full'
282
+ disabled={view.projection.fields.length >= VIEW_FIELD_LIMIT}
283
+ classNames='is-full'
280
284
  />
281
285
  </div>
282
286
  )}
@@ -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.Content classNames={[textBlockWidth, classNames]}>{children}</Card.Content>
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');
@@ -2,14 +2,14 @@
2
2
  // Copyright 2025 DXOS.org
3
3
  //
4
4
 
5
- import { SchemaAST, type Schema } from 'effect';
5
+ import { type Schema, SchemaAST } from 'effect';
6
6
  import { useCallback, useEffect, useMemo, useState } from 'react';
7
7
 
8
8
  import { type BaseObject, getValue, setValue } from '@dxos/echo-schema';
9
- import { type SimpleType, type JsonPath, createJsonPath, fromEffectValidationPath } from '@dxos/effect';
9
+ import { type JsonPath, type SimpleType, createJsonPath, fromEffectValidationPath } from '@dxos/effect';
10
10
  import { invariant } from '@dxos/invariant';
11
11
  import { log } from '@dxos/log';
12
- import { validateSchema, type ValidationError } from '@dxos/schema';
12
+ import { type ValidationError, validateSchema } from '@dxos/schema';
13
13
  import { type MaybePromise } from '@dxos/util';
14
14
 
15
15
  /**
@@ -26,6 +26,8 @@ export type FormHandler<T extends BaseObject> = {
26
26
  changed: Record<JsonPath, boolean>;
27
27
  canSave: boolean;
28
28
  formIsValid: boolean;
29
+
30
+ // TODO(burdon): Return onSave.
29
31
  handleSave: () => void;
30
32
 
31
33
  //
@@ -4,7 +4,7 @@
4
4
 
5
5
  import { useCallback } from 'react';
6
6
 
7
- import { getObjectDXN, type TypeAnnotation } from '@dxos/echo-schema';
7
+ import { type TypeAnnotation, getObjectDXN } from '@dxos/echo-schema';
8
8
  import { Filter, type Space } from '@dxos/react-client/echo';
9
9
  import { isNonNullable } from '@dxos/util';
10
10
 
package/src/index.ts CHANGED
@@ -2,7 +2,7 @@
2
2
  // Copyright 2024 DXOS.org
3
3
  //
4
4
 
5
- import translations from './translations';
5
+ import { translations } from './translations';
6
6
 
7
7
  export * from './components';
8
8
  export * from './hooks';
@@ -2,9 +2,11 @@
2
2
  // Copyright 2023 DXOS.org
3
3
  //
4
4
 
5
+ import { type Resource } from '@dxos/react-ui';
6
+
5
7
  export const translationKey = 'react-ui-form';
6
8
 
7
- export default [
9
+ export const translations = [
8
10
  {
9
11
  'en-US': {
10
12
  [translationKey]: {
@@ -23,6 +25,11 @@ export default [
23
25
  'field limit reached': 'Maximum number of fields reached',
24
26
  'boolean input true value': 'Yes',
25
27
  'boolean input false value': 'No',
28
+ 'show field label': 'Show field',
29
+ 'hide field label': 'Hide field',
30
+ 'delete field label': 'Delete field',
31
+ 'ref field placeholder_one': 'Select…',
32
+ 'ref field placeholder_other': 'Select items…',
26
33
 
27
34
  // TODO(burdon): Factor out?
28
35
  'button cancel': 'Cancel',
@@ -58,9 +65,9 @@ export default [
58
65
  'format url': 'URL',
59
66
  'format uuid': 'UUID',
60
67
 
61
- // Placeholders.
62
- 'placeholder latitude': 'Latitude',
63
- 'placeholder longitude': 'Longitude',
68
+ // GeoPointInput.
69
+ 'latitude label': 'Latitude',
70
+ 'longitude label': 'Longitude',
64
71
 
65
72
  // SelectOptionsInput.
66
73
  'select option label': 'Label',
@@ -70,9 +77,10 @@ export default [
70
77
  'select option add': 'Add option',
71
78
 
72
79
  // System schema message.
73
- 'system schema title': 'System Schema',
74
- 'system schema description': 'This is a system schema and cannot be modified.',
80
+ 'system schema title': 'System record type',
81
+ 'system schema description':
82
+ 'This is a system record type and cannot be modified, though the view can still be configured.',
75
83
  },
76
84
  },
77
85
  },
78
- ];
86
+ ] as const satisfies Resource[];
package/src/util/field.ts CHANGED
@@ -112,24 +112,7 @@ export type CellClassesForFieldTypeProps = {
112
112
  };
113
113
 
114
114
  export const cellClassesForFieldType = ({ type, format }: CellClassesForFieldTypeProps): string[] | undefined => {
115
- if (!format) {
116
- switch (type) {
117
- case TypeEnum.Number:
118
- return ['text-right', 'font-mono'];
119
- case TypeEnum.Boolean:
120
- return ['text-right', 'font-mono'];
121
- case TypeEnum.String:
122
- return undefined;
123
- case TypeEnum.Ref:
124
- return undefined;
125
-
126
- default: {
127
- return undefined;
128
- }
129
- }
130
- }
131
-
132
- switch (format) {
115
+ switch (format || type) {
133
116
  case FormatEnum.Markdown:
134
117
  return undefined;
135
118
  case FormatEnum.Time:
@@ -143,6 +126,13 @@ export const cellClassesForFieldType = ({ type, format }: CellClassesForFieldTyp
143
126
  case FormatEnum.JSON:
144
127
  case FormatEnum.DID:
145
128
  return ['font-mono'];
129
+ case TypeEnum.Number:
130
+ case TypeEnum.Boolean:
131
+ return ['text-right', 'font-mono'];
132
+ case TypeEnum.String:
133
+ return undefined;
134
+ case TypeEnum.Ref:
135
+ return undefined;
146
136
  default:
147
137
  return undefined;
148
138
  }