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

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 (94) hide show
  1. package/dist/lib/browser/index.mjs +601 -528
  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 +601 -528
  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 +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 +74 -4
  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 +3 -2
  41. package/dist/types/src/components/ViewEditor/ViewEditor.d.ts.map +1 -1
  42. package/dist/types/src/components/ViewEditor/ViewEditor.stories.d.ts +76 -5
  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/translations.d.ts +9 -4
  51. package/dist/types/src/translations.d.ts.map +1 -1
  52. package/dist/types/src/util/field.d.ts.map +1 -1
  53. package/dist/types/tsconfig.tsbuildinfo +1 -1
  54. package/package.json +35 -30
  55. package/src/components/ControlSection/ControlSection.stories.tsx +8 -10
  56. package/src/components/ControlSection/ControlSection.tsx +26 -12
  57. package/src/components/FieldEditor/FieldEditor.stories.tsx +8 -9
  58. package/src/components/FieldEditor/FieldEditor.test.tsx +3 -2
  59. package/src/components/FieldEditor/FieldEditor.tsx +22 -17
  60. package/src/components/Form/ArrayField.tsx +8 -8
  61. package/src/components/Form/Defaults.tsx +22 -20
  62. package/src/components/Form/Form.stories.tsx +129 -93
  63. package/src/components/Form/Form.tsx +16 -12
  64. package/src/components/Form/FormActions.tsx +2 -1
  65. package/src/components/Form/FormContent.tsx +96 -34
  66. package/src/components/Form/FormContext.tsx +5 -5
  67. package/src/components/Form/Input.tsx +4 -1
  68. package/src/components/Form/RefField.tsx +126 -40
  69. package/src/components/Form/custom/GeoPointInput.tsx +29 -21
  70. package/src/components/Form/custom/SelectOptionsInput.stories.tsx +7 -6
  71. package/src/components/Form/custom/SelectOptionsInput.tsx +29 -30
  72. package/src/components/Form/custom/TupleInput.tsx +2 -2
  73. package/src/components/Form/factory.tsx +1 -1
  74. package/src/components/ViewEditor/ViewEditor.stories.tsx +50 -23
  75. package/src/components/ViewEditor/ViewEditor.test.tsx +7 -10
  76. package/src/components/ViewEditor/ViewEditor.tsx +151 -132
  77. package/src/components/index.ts +0 -2
  78. package/src/components/testing.tsx +15 -9
  79. package/src/hooks/useForm.ts +5 -3
  80. package/src/hooks/useRefQueryLookupHandler.ts +1 -1
  81. package/src/translations.ts +11 -5
  82. package/src/util/field.ts +8 -18
  83. package/dist/types/src/components/deprecated/Form.d.ts +0 -15
  84. package/dist/types/src/components/deprecated/Form.d.ts.map +0 -1
  85. package/dist/types/src/components/deprecated/Form.stories.d.ts +0 -13
  86. package/dist/types/src/components/deprecated/Form.stories.d.ts.map +0 -1
  87. package/dist/types/src/components/deprecated/FormInput.d.ts +0 -16
  88. package/dist/types/src/components/deprecated/FormInput.d.ts.map +0 -1
  89. package/dist/types/src/components/deprecated/index.d.ts +0 -2
  90. package/dist/types/src/components/deprecated/index.d.ts.map +0 -1
  91. package/src/components/deprecated/Form.stories.tsx +0 -62
  92. package/src/components/deprecated/Form.tsx +0 -119
  93. package/src/components/deprecated/FormInput.tsx +0 -71
  94. package/src/components/deprecated/index.ts +0 -5
@@ -3,15 +3,16 @@
3
3
  //
4
4
 
5
5
  import { composeStories } from '@storybook/react';
6
- import { screen, cleanup, fireEvent, waitFor } from '@testing-library/react';
6
+ import { cleanup, fireEvent, screen, waitFor } from '@testing-library/react';
7
7
  import { afterEach, describe, expect, test } from 'vitest';
8
8
 
9
9
  import { EchoSchema, isInstanceOf } from '@dxos/echo-schema';
10
10
  import { DataType, ProjectionModel } from '@dxos/schema';
11
11
 
12
+ import { VIEW_EDITOR_DEBUG_SYMBOL } from '../testing';
13
+
12
14
  import * as stories from './ViewEditor.stories';
13
15
  import { type ViewEditorDebugObjects } from './ViewEditor.stories';
14
- import { VIEW_EDITOR_DEBUG_SYMBOL } from '../testing';
15
16
 
16
17
  const { Default } = composeStories(stories);
17
18
 
@@ -41,7 +42,7 @@ describe('ViewEditor', () => {
41
42
  test('renders view editor', async () => {
42
43
  await Default.run();
43
44
  await waitForViewEditor();
44
- expect(screen.getByText('Typename')).toBeInTheDocument();
45
+ expect(screen.getByText('Record type')).toBeInTheDocument();
45
46
  });
46
47
 
47
48
  test('change property name', async () => {
@@ -140,7 +141,7 @@ describe('ViewEditor', () => {
140
141
  // Find the delete button for the 'name' property.
141
142
  const nameProperty = screen.getByText('name');
142
143
  const propertyRow = nameProperty.closest('[role="listitem"]');
143
- const deleteButton = propertyRow?.querySelector('button:last-child');
144
+ const deleteButton = propertyRow?.querySelector('[data-testid="field.delete"]');
144
145
 
145
146
  if (!deleteButton) {
146
147
  throw new Error('Delete button not found');
@@ -188,19 +189,15 @@ describe('ViewEditor', () => {
188
189
  // Access the live objects directly from window using symbol.
189
190
  const debugObjects = getViewEditorDebugObjects();
190
191
 
191
- // Check that hiddenFields is non-empty.
192
- expect(debugObjects.view.projection.hiddenFields).toBeDefined();
193
- expect(debugObjects.view.projection.hiddenFields!.length).toBeGreaterThan(0);
194
-
195
192
  // Click the show button
196
193
  fireEvent.click(screen.getByTestId('show-field-button'));
197
194
 
198
195
  // Wait for the data to update and check that hiddenFields is empty.
199
196
  await waitFor(() => {
200
- expect(debugObjects.view.projection.hiddenFields!.length).toBe(0);
197
+ expect(debugObjects.view.projection.fields.filter(({ visible }) => visible === false)!.length).toBe(0);
201
198
  });
202
199
 
203
200
  // Also verify that the field is back in the visible fields list.
204
- expect(debugObjects.view.projection.fields.length).toBeGreaterThan(0);
201
+ expect(debugObjects.view.projection.fields.filter(({ visible }) => visible !== false).length).toBeGreaterThan(0);
205
202
  });
206
203
  });
@@ -2,40 +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, VIEW_FIELD_LIMIT, type DataType, ProjectionModel } 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
- typename: Format.URL.annotations({
25
- title: 'Typename',
26
- }),
27
- }).pipe(Schema.mutable);
28
-
29
- 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';
30
32
 
31
33
  export type ViewEditorProps = ThemedClassName<
32
34
  {
33
35
  schema: Schema.Schema.AnyNoContext;
34
36
  view: DataType.View;
37
+ kind?: 'basic' | 'advanced';
35
38
  registry?: SchemaRegistry;
36
39
  readonly?: boolean;
37
40
  showHeading?: boolean;
38
- onTypenameChanged?: (typename: string) => void;
41
+ onQueryChanged?: (query: string) => void;
39
42
  onDelete?: (fieldId: string) => void;
40
43
  } & Pick<FormProps<any>, 'outerSpacing'>
41
44
  >;
@@ -47,37 +50,52 @@ export const ViewEditor = ({
47
50
  classNames,
48
51
  schema,
49
52
  view,
53
+ kind = 'basic',
50
54
  registry,
51
- readonly: _readonly,
55
+ readonly,
52
56
  showHeading = false,
53
- onTypenameChanged,
57
+ onQueryChanged,
54
58
  onDelete,
55
59
  outerSpacing = true,
56
60
  }: ViewEditorProps) => {
61
+ const space = getSpace(view);
62
+ const schemaReadonly = !isMutable(schema);
57
63
  const { t } = useTranslation(translationKey);
58
- const projection = useMemo(() => {
64
+ const projectionModel = useMemo(() => {
59
65
  // Use reactive and mutable version of json schema when schema is mutable.
60
66
  const jsonSchema = schema instanceof EchoSchema ? schema.jsonSchema : toJsonSchema(schema);
61
67
  return new ProjectionModel(jsonSchema, view.projection);
62
68
  }, [schema, view.projection]);
63
- const [field, setField] = useState<FieldType>();
64
- const readonly = _readonly || !isMutable(schema);
69
+ const [expandedField, setExpandedField] = useState<FieldType['id']>();
70
+
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
+ }
77
+
78
+ const serializer = new QuerySerializer();
79
+ return serializer.serialize(createExpression(view.query.filter));
80
+ }),
81
+ Match.exhaustive,
82
+ );
65
83
 
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]);
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]);
74
95
 
75
- const handleSelect = useCallback(
96
+ const handleToggleField = useCallback(
76
97
  (field: FieldType) => {
77
- if (readonly) {
78
- return;
79
- }
80
- setField((f) => (f === field ? undefined : field));
98
+ setExpandedField((prevExpandedFieldId) => (prevExpandedFieldId === field.id ? undefined : field.id));
81
99
  },
82
100
  [readonly],
83
101
  );
@@ -86,32 +104,32 @@ export const ViewEditor = ({
86
104
 
87
105
  const handleAdd = useCallback(() => {
88
106
  invariant(!readonly);
89
- const field = projection.createFieldProjection();
90
- setField(field);
91
- }, [schema, projection, readonly]);
107
+ const field = projectionModel.createFieldProjection();
108
+ setExpandedField(field.id);
109
+ }, [schema, projectionModel, readonly]);
92
110
 
93
111
  const handleUpdate = useCallback(
94
- ({ typename }: ViewMetaType) => {
112
+ ({ query }: Schema.Schema.Type<typeof viewSchema>) => {
95
113
  invariant(!readonly);
96
114
  requestAnimationFrame(() => {
97
- if (view.query.typename !== typename && !readonly) {
98
- onTypenameChanged?.(typename);
115
+ if (serializedQuery !== query && !readonly) {
116
+ onQueryChanged?.(query);
99
117
  }
100
118
  });
101
119
  },
102
- [view.query.typename, onTypenameChanged, readonly],
120
+ [serializedQuery, onQueryChanged, readonly],
103
121
  );
104
122
 
105
123
  const handleDelete = useCallback(
106
124
  (fieldId: string) => {
107
125
  invariant(!readonly);
108
- if (fieldId === field?.id) {
109
- setField(undefined);
126
+ if (fieldId === expandedField) {
127
+ setExpandedField(undefined);
110
128
  }
111
129
 
112
130
  onDelete?.(fieldId);
113
131
  },
114
- [field, onDelete, readonly],
132
+ [expandedField, onDelete, readonly],
115
133
  );
116
134
 
117
135
  const handleMove = useCallback(
@@ -126,134 +144,135 @@ export const ViewEditor = ({
126
144
  [view.projection.fields, readonly],
127
145
  );
128
146
 
129
- const handleClose = useCallback(() => setField(undefined), []);
130
-
131
- const hiddenProperties = projection.getHiddenProperties();
147
+ const handleClose = useCallback(() => setExpandedField(undefined), []);
132
148
 
133
149
  const handleHide = useCallback(
134
150
  (fieldId: string) => {
135
- setField(undefined);
136
- projection.hideFieldProjection(fieldId);
151
+ setExpandedField(undefined);
152
+ projectionModel.hideFieldProjection(fieldId);
137
153
  },
138
- [projection],
154
+ [projectionModel],
139
155
  );
140
156
 
141
157
  const handleShow = useCallback(
142
158
  (property: string) => {
143
- setField(undefined);
144
- projection.showFieldProjection(property as JsonProp);
159
+ setExpandedField(undefined);
160
+ projectionModel.showFieldProjection(property as JsonProp);
145
161
  },
146
- [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
+ [],
147
181
  );
148
182
 
149
183
  return (
150
184
  <div role='none' className={mx(classNames)}>
151
- {readonly && (
152
- <div role='none' className={mx('is-full', outerSpacing && 'plb-cardSpacingBlock pli-cardSpacingInline')}>
153
- <Callout.Root valence='info'>
154
- <Callout.Title>{t('system schema description')}</Callout.Title>
155
- </Callout.Root>
156
- </div>
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>
157
189
  )}
158
190
 
159
191
  {/* TODO(burdon): Is the form read-only or just the schema? */}
160
192
  {/* TODO(burdon): Readonly fields should take up the same space as editable fields (just be ghosted). */}
161
- <Form<ViewMetaType>
193
+ <Form<Schema.Schema.Type<typeof viewSchema>>
162
194
  autoSave
163
- schema={ViewMetaSchema}
195
+ schema={viewSchema}
164
196
  values={viewValues}
165
- readonly={readonly}
197
+ readonly={readonly ? 'disabled-input' : false}
166
198
  onSave={handleUpdate}
167
199
  outerSpacing={outerSpacing}
200
+ Custom={custom}
168
201
  />
169
202
 
170
203
  <div role='none' className={outerSpacing ? cardSpacing : 'mlb-cardSpacingBlock'}>
171
- <label className={mx(inputTextLabel)}>{t('fields label')}</label>
204
+ <h2 className={mx(inputTextLabel)}>{t('fields label')}</h2>
172
205
 
173
206
  <List.Root<FieldType>
174
207
  items={view.projection.fields}
175
208
  isItem={Schema.is(FieldSchema)}
176
209
  getId={(field) => field.id}
177
- readonly={readonly}
178
210
  onMove={readonly ? undefined : handleMove}
211
+ readonly={readonly}
179
212
  >
180
213
  {({ items: fields }) => (
181
214
  <>
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
- ))}
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
+ })}
214
269
  </div>
215
270
  </>
216
271
  )}
217
272
  </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
- )}
250
273
  </div>
251
274
 
252
- {field && (
253
- <FieldEditor key={field.id} projection={projection} field={field} registry={registry} onSave={handleClose} />
254
- )}
255
-
256
- {!readonly && !field && (
275
+ {!readonly && !expandedField && (
257
276
  <div role='none' className={outerSpacing ? cardSpacing : 'mlb-cardSpacingBlock'}>
258
277
  <IconButton
259
278
  icon='ph--plus--regular'
@@ -261,7 +280,7 @@ export const ViewEditor = ({
261
280
  onClick={readonly ? undefined : handleAdd}
262
281
  // TODO(burdon): Show field limit in ux (not tooltip).
263
282
  disabled={view.projection.fields.length >= VIEW_FIELD_LIMIT}
264
- classNames='flex is-full'
283
+ classNames='is-full'
265
284
  />
266
285
  </div>
267
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.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');
@@ -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
 
@@ -25,6 +25,11 @@ export const translations = [
25
25
  'field limit reached': 'Maximum number of fields reached',
26
26
  'boolean input true value': 'Yes',
27
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…',
28
33
 
29
34
  // TODO(burdon): Factor out?
30
35
  'button cancel': 'Cancel',
@@ -60,9 +65,9 @@ export const translations = [
60
65
  'format url': 'URL',
61
66
  'format uuid': 'UUID',
62
67
 
63
- // Placeholders.
64
- 'placeholder latitude': 'Latitude',
65
- 'placeholder longitude': 'Longitude',
68
+ // GeoPointInput.
69
+ 'latitude label': 'Latitude',
70
+ 'longitude label': 'Longitude',
66
71
 
67
72
  // SelectOptionsInput.
68
73
  'select option label': 'Label',
@@ -72,8 +77,9 @@ export const translations = [
72
77
  'select option add': 'Add option',
73
78
 
74
79
  // System schema message.
75
- 'system schema title': 'System Schema',
76
- '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.',
77
83
  },
78
84
  },
79
85
  },
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
  }
@@ -1,15 +0,0 @@
1
- import { type Schema } from 'effect';
2
- import React from 'react';
3
- import { type ThemedClassName } from '@dxos/react-ui';
4
- import { type Projection } from '@dxos/schema';
5
- export type DeprecatedFormProps<T extends {} = {}> = ThemedClassName<{
6
- projection: Projection;
7
- object: T;
8
- schema?: Schema.Schema<T>;
9
- readonly?: boolean;
10
- }>;
11
- /**
12
- * Schema-based object form.
13
- */
14
- export declare const DeprecatedForm: <T extends {} = {}>({ classNames, projection, object, schema, readonly, }: DeprecatedFormProps<T>) => React.JSX.Element;
15
- //# sourceMappingURL=Form.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"Form.d.ts","sourceRoot":"","sources":["../../../../../src/components/deprecated/Form.tsx"],"names":[],"mappings":"AAIA,OAAO,EAAa,KAAK,MAAM,EAAgB,MAAM,QAAQ,CAAC;AAC9D,OAAO,KAAmB,MAAM,OAAO,CAAC;AAGxC,OAAO,EAAS,KAAK,eAAe,EAA+C,MAAM,gBAAgB,CAAC;AAE1G,OAAO,EAAgC,KAAK,UAAU,EAAE,MAAM,cAAc,CAAC;AAE7E,MAAM,MAAM,mBAAmB,CAAC,CAAC,SAAS,EAAE,GAAG,EAAE,IAAI,eAAe,CAAC;IACnE,UAAU,EAAE,UAAU,CAAC;IACvB,MAAM,EAAE,CAAC,CAAC;IACV,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC;IAC1B,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB,CAAC,CAAC;AAEH;;GAEG;AACH,eAAO,MAAM,cAAc,GAAI,CAAC,SAAS,EAAE,GAAG,EAAE,EAAE,uDAM/C,mBAAmB,CAAC,CAAC,CAAC,sBA0DxB,CAAC"}
@@ -1,13 +0,0 @@
1
- import '@dxos-theme';
2
- import { type Meta, type StoryObj } from '@storybook/react-vite';
3
- import React from 'react';
4
- import { type TestType } from '@dxos/schema/testing';
5
- import { type DeprecatedFormProps } from './Form';
6
- declare const DefaultStory: (props: DeprecatedFormProps) => React.JSX.Element;
7
- declare const meta: Meta<typeof DefaultStory>;
8
- export default meta;
9
- type Story = StoryObj<DeprecatedFormProps<TestType>>;
10
- export declare const Default: Story;
11
- export declare const Empty: Story;
12
- export declare const Readonly: Story;
13
- //# sourceMappingURL=Form.stories.d.ts.map