@dxos/react-ui-form 0.8.4-main.8360d9e660 → 0.8.4-main.8baae0fced

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 (147) hide show
  1. package/LICENSE +102 -5
  2. package/dist/lib/browser/index.mjs +1009 -612
  3. package/dist/lib/browser/index.mjs.map +4 -4
  4. package/dist/lib/browser/meta.json +1 -1
  5. package/dist/lib/browser/translations.mjs +82 -0
  6. package/dist/lib/browser/translations.mjs.map +7 -0
  7. package/dist/lib/node-esm/index.mjs +1009 -612
  8. package/dist/lib/node-esm/index.mjs.map +4 -4
  9. package/dist/lib/node-esm/meta.json +1 -1
  10. package/dist/lib/node-esm/translations.mjs +84 -0
  11. package/dist/lib/node-esm/translations.mjs.map +7 -0
  12. package/dist/types/src/components/FieldEditor/FieldEditor.d.ts.map +1 -1
  13. package/dist/types/src/components/FieldEditor/FieldEditor.stories.d.ts +63 -61
  14. package/dist/types/src/components/FieldEditor/FieldEditor.stories.d.ts.map +1 -1
  15. package/dist/types/src/components/Form/Form.d.ts +40 -12
  16. package/dist/types/src/components/Form/Form.d.ts.map +1 -1
  17. package/dist/types/src/components/Form/Form.stories.d.ts +76 -74
  18. package/dist/types/src/components/Form/Form.stories.d.ts.map +1 -1
  19. package/dist/types/src/components/Form/FormField.d.ts +11 -5
  20. package/dist/types/src/components/Form/FormField.d.ts.map +1 -1
  21. package/dist/types/src/components/Form/FormFieldComponent.d.ts +8 -6
  22. package/dist/types/src/components/Form/FormFieldComponent.d.ts.map +1 -1
  23. package/dist/types/src/components/Form/FormFieldComponent.stories.d.ts.map +1 -1
  24. package/dist/types/src/components/Form/FormFieldSet.d.ts +5 -8
  25. package/dist/types/src/components/Form/FormFieldSet.d.ts.map +1 -1
  26. package/dist/types/src/components/Form/fields/ArrayField.d.ts +4 -4
  27. package/dist/types/src/components/Form/fields/ArrayField.d.ts.map +1 -1
  28. package/dist/types/src/components/Form/fields/BooleanField.d.ts.map +1 -1
  29. package/dist/types/src/components/Form/fields/GeoPointField.d.ts.map +1 -1
  30. package/dist/types/src/components/Form/fields/MarkdownField.d.ts +8 -1
  31. package/dist/types/src/components/Form/fields/MarkdownField.d.ts.map +1 -1
  32. package/dist/types/src/components/Form/fields/MarkdownField.stories.d.ts +84 -0
  33. package/dist/types/src/components/Form/fields/MarkdownField.stories.d.ts.map +1 -0
  34. package/dist/types/src/components/Form/fields/NumberField.d.ts.map +1 -1
  35. package/dist/types/src/components/Form/fields/RefField.d.ts +12 -6
  36. package/dist/types/src/components/Form/fields/RefField.d.ts.map +1 -1
  37. package/dist/types/src/components/Form/fields/SelectField.d.ts.map +1 -1
  38. package/dist/types/src/components/Form/fields/SelectOptionField.d.ts.map +1 -1
  39. package/dist/types/src/components/Form/fields/SelectOptionField.stories.d.ts +63 -61
  40. package/dist/types/src/components/Form/fields/SelectOptionField.stories.d.ts.map +1 -1
  41. package/dist/types/src/components/Form/fields/TextAreaField.d.ts.map +1 -1
  42. package/dist/types/src/components/Form/fields/TextField.d.ts +1 -1
  43. package/dist/types/src/components/Form/fields/TextField.d.ts.map +1 -1
  44. package/dist/types/src/components/Form/fields/TupleField.d.ts.map +1 -1
  45. package/dist/types/src/components/ObjectForm/ObjectForm.d.ts +9 -0
  46. package/dist/types/src/components/ObjectForm/ObjectForm.d.ts.map +1 -0
  47. package/dist/types/src/components/ObjectForm/index.d.ts +2 -0
  48. package/dist/types/src/components/ObjectForm/index.d.ts.map +1 -0
  49. package/dist/types/src/components/ObjectPicker/ObjectPicker.d.ts +26 -4
  50. package/dist/types/src/components/ObjectPicker/ObjectPicker.d.ts.map +1 -1
  51. package/dist/types/src/components/ObjectPicker/ObjectPicker.stories.d.ts +63 -61
  52. package/dist/types/src/components/ObjectPicker/ObjectPicker.stories.d.ts.map +1 -1
  53. package/dist/types/src/components/ObjectProperties/ObjectProperties.d.ts +9 -0
  54. package/dist/types/src/components/ObjectProperties/ObjectProperties.d.ts.map +1 -0
  55. package/dist/types/src/components/ObjectProperties/ObjectProperties.stories.d.ts +127 -0
  56. package/dist/types/src/components/ObjectProperties/ObjectProperties.stories.d.ts.map +1 -0
  57. package/dist/types/src/components/ObjectProperties/ObjectProperties.test.d.ts +2 -0
  58. package/dist/types/src/components/ObjectProperties/ObjectProperties.test.d.ts.map +1 -0
  59. package/dist/types/src/components/ObjectProperties/index.d.ts +2 -0
  60. package/dist/types/src/components/ObjectProperties/index.d.ts.map +1 -0
  61. package/dist/types/src/components/Settings/Settings.d.ts +28 -33
  62. package/dist/types/src/components/Settings/Settings.d.ts.map +1 -1
  63. package/dist/types/src/components/Settings/Settings.stories.d.ts +64 -61
  64. package/dist/types/src/components/Settings/Settings.stories.d.ts.map +1 -1
  65. package/dist/types/src/components/Settings/SettingsFieldSet.d.ts +43 -0
  66. package/dist/types/src/components/Settings/SettingsFieldSet.d.ts.map +1 -0
  67. package/dist/types/src/components/Settings/SettingsItem.d.ts +16 -0
  68. package/dist/types/src/components/Settings/SettingsItem.d.ts.map +1 -0
  69. package/dist/types/src/components/Settings/SettingsSelectField.d.ts +18 -0
  70. package/dist/types/src/components/Settings/SettingsSelectField.d.ts.map +1 -0
  71. package/dist/types/src/components/Settings/index.d.ts +2 -0
  72. package/dist/types/src/components/Settings/index.d.ts.map +1 -1
  73. package/dist/types/src/components/ViewEditor/ViewEditor.d.ts +3 -4
  74. package/dist/types/src/components/ViewEditor/ViewEditor.d.ts.map +1 -1
  75. package/dist/types/src/components/ViewEditor/ViewEditor.stories.d.ts +65 -63
  76. package/dist/types/src/components/ViewEditor/ViewEditor.stories.d.ts.map +1 -1
  77. package/dist/types/src/components/index.d.ts +2 -0
  78. package/dist/types/src/components/index.d.ts.map +1 -1
  79. package/dist/types/src/components/testing.d.ts +4 -4
  80. package/dist/types/src/components/testing.d.ts.map +1 -1
  81. package/dist/types/src/hooks/useFormHandler.d.ts.map +1 -1
  82. package/dist/types/src/hooks/useKeyHandler.d.ts +2 -1
  83. package/dist/types/src/hooks/useKeyHandler.d.ts.map +1 -1
  84. package/dist/types/src/index.d.ts +0 -2
  85. package/dist/types/src/index.d.ts.map +1 -1
  86. package/dist/types/src/translations.d.ts +63 -61
  87. package/dist/types/src/translations.d.ts.map +1 -1
  88. package/dist/types/src/util/field-type.d.ts +19 -0
  89. package/dist/types/src/util/field-type.d.ts.map +1 -0
  90. package/dist/types/src/util/field.d.ts.map +1 -1
  91. package/dist/types/src/util/formatting.d.ts.map +1 -1
  92. package/dist/types/src/util/index.d.ts +1 -0
  93. package/dist/types/src/util/index.d.ts.map +1 -1
  94. package/dist/types/src/util/properties.d.ts +7 -1
  95. package/dist/types/src/util/properties.d.ts.map +1 -1
  96. package/dist/types/src/util/properties.test.d.ts +2 -0
  97. package/dist/types/src/util/properties.test.d.ts.map +1 -0
  98. package/dist/types/src/util/refs.d.ts +1 -0
  99. package/dist/types/src/util/refs.d.ts.map +1 -1
  100. package/dist/types/tsconfig.tsbuildinfo +1 -1
  101. package/package.json +43 -41
  102. package/src/components/FieldEditor/FieldEditor.stories.tsx +2 -2
  103. package/src/components/FieldEditor/FieldEditor.test.tsx +0 -1
  104. package/src/components/FieldEditor/FieldEditor.tsx +4 -3
  105. package/src/components/Form/AUDIT.md +17 -0
  106. package/src/components/Form/Form.stories.tsx +23 -36
  107. package/src/components/Form/Form.tsx +77 -48
  108. package/src/components/Form/FormField.tsx +27 -15
  109. package/src/components/Form/FormFieldComponent.tsx +6 -3
  110. package/src/components/Form/FormFieldSet.tsx +49 -32
  111. package/src/components/Form/fields/ArrayField.tsx +14 -8
  112. package/src/components/Form/fields/GeoPointField.tsx +8 -7
  113. package/src/components/Form/fields/MarkdownField.stories.tsx +206 -0
  114. package/src/components/Form/fields/MarkdownField.tsx +135 -45
  115. package/src/components/Form/fields/RefField.tsx +70 -35
  116. package/src/components/Form/fields/SelectOptionField.stories.tsx +8 -6
  117. package/src/components/Form/fields/SelectOptionField.tsx +12 -12
  118. package/src/components/Form/fields/TextField.tsx +2 -2
  119. package/src/components/Form/fields/TupleField.tsx +1 -1
  120. package/src/components/ObjectForm/ObjectForm.tsx +114 -0
  121. package/src/components/ObjectForm/index.ts +5 -0
  122. package/src/components/ObjectPicker/ObjectPicker.stories.tsx +12 -10
  123. package/src/components/ObjectPicker/ObjectPicker.tsx +92 -65
  124. package/src/components/ObjectProperties/ObjectProperties.stories.tsx +362 -0
  125. package/src/components/ObjectProperties/ObjectProperties.test.tsx +101 -0
  126. package/src/components/ObjectProperties/ObjectProperties.tsx +148 -0
  127. package/src/components/ObjectProperties/index.ts +5 -0
  128. package/src/components/Settings/AUDIT.md +51 -0
  129. package/src/components/Settings/Settings.stories.tsx +69 -21
  130. package/src/components/Settings/Settings.tsx +33 -183
  131. package/src/components/Settings/SettingsFieldSet.tsx +257 -0
  132. package/src/components/Settings/SettingsItem.tsx +58 -0
  133. package/src/components/Settings/SettingsSelectField.tsx +59 -0
  134. package/src/components/Settings/index.ts +2 -0
  135. package/src/components/ViewEditor/ViewEditor.stories.tsx +12 -13
  136. package/src/components/ViewEditor/ViewEditor.test.tsx +0 -1
  137. package/src/components/ViewEditor/ViewEditor.tsx +25 -32
  138. package/src/components/index.ts +2 -0
  139. package/src/components/testing.tsx +19 -11
  140. package/src/hooks/useKeyHandler.tsx +7 -7
  141. package/src/index.ts +0 -4
  142. package/src/translations.ts +66 -63
  143. package/src/util/field-type.ts +60 -0
  144. package/src/util/index.ts +1 -0
  145. package/src/util/properties.test.ts +63 -0
  146. package/src/util/properties.ts +14 -3
  147. package/src/util/refs.ts +7 -3
@@ -6,8 +6,7 @@ import { type Meta, type StoryObj } from '@storybook/react-vite';
6
6
  import * as Schema from 'effect/Schema';
7
7
  import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
8
8
 
9
- import { DXN, Filter, JsonSchema, Obj, Query, type QueryAST, Tag, Type } from '@dxos/echo';
10
- import { type View } from '@dxos/echo';
9
+ import { DXN, Filter, JsonSchema, Obj, Query, type QueryAST, Tag, Type, type View } from '@dxos/echo';
11
10
  import { type EchoSchema, Format, type Mutable } from '@dxos/echo/internal';
12
11
  import { useQuery } from '@dxos/react-client/echo';
13
12
  import { useClientStory, withClientProvider } from '@dxos/react-client/testing';
@@ -16,9 +15,9 @@ import { withLayout, withTheme } from '@dxos/react-ui/testing';
16
15
  import { type ProjectionModel, ViewModel, getTypenameFromQuery } from '@dxos/schema';
17
16
  import { Employer, Organization, Person, Pipeline } from '@dxos/types';
18
17
 
19
- import { translations } from '../../translations';
20
- import { TestLayout, VIEW_EDITOR_DEBUG_SYMBOL } from '../testing';
18
+ import { translations } from '#translations';
21
19
 
20
+ import { TestLayout, VIEW_EDITOR_DEBUG_SYMBOL } from '../testing';
22
21
  import { ViewEditor, type ViewEditorProps } from './ViewEditor';
23
22
 
24
23
  const types = [
@@ -36,9 +35,9 @@ export type ViewEditorDebugObjects = {
36
35
  projection: ProjectionModel;
37
36
  };
38
37
 
39
- type StoryProps = Pick<ViewEditorProps, 'readonly' | 'mode'>;
38
+ type DefaultStoryProps = Pick<ViewEditorProps, 'readonly' | 'mode'>;
40
39
 
41
- const DefaultStory = (props: StoryProps) => {
40
+ const DefaultStory = (props: DefaultStoryProps) => {
42
41
  const { space } = useClientStory();
43
42
  const [schema, setSchema] = useState<EchoSchema>();
44
43
  const [view, setView] = useState<View.View>();
@@ -90,9 +89,9 @@ const DefaultStory = (props: StoryProps) => {
90
89
 
91
90
  if (props.mode === 'tag') {
92
91
  const queue = target && DXN.tryParse(target) ? target : undefined;
93
- const query = queue ? Query.fromAst(newQuery).from({ queues: [queue] }) : Query.fromAst(newQuery);
94
- Obj.change(view, (v) => {
95
- v.query.ast = query.ast as Mutable<typeof query.ast>;
92
+ const query = queue ? Query.fromAst(newQuery).from({ feeds: [queue] }) : Query.fromAst(newQuery);
93
+ Obj.update(view, (view) => {
94
+ view.query.ast = query.ast as Mutable<typeof query.ast>;
96
95
  });
97
96
 
98
97
  const typename = getTypenameFromQuery(query.ast);
@@ -105,13 +104,13 @@ const DefaultStory = (props: StoryProps) => {
105
104
  query,
106
105
  jsonSchema: newSchema.jsonSchema,
107
106
  });
108
- Obj.change(view, (v) => {
109
- v.projection = Obj.getSnapshot(newView).projection as Mutable<typeof v.projection>;
107
+ Obj.update(view, (view) => {
108
+ view.projection = Obj.getSnapshot(newView).projection as Mutable<typeof view.projection>;
110
109
  });
111
110
  setSchema(() => newSchema);
112
111
  } else {
113
- Obj.change(view, (v) => {
114
- v.query.ast = newQuery as Mutable<typeof newQuery>;
112
+ Obj.update(view, (view) => {
113
+ view.query.ast = newQuery as Mutable<typeof newQuery>;
115
114
  });
116
115
  schema.updateTypename(getTypenameFromQuery(newQuery));
117
116
  }
@@ -11,7 +11,6 @@ import { EchoSchema, isInstanceOf } from '@dxos/echo/internal';
11
11
  import { ProjectionModel } from '@dxos/schema';
12
12
 
13
13
  import { VIEW_EDITOR_DEBUG_SYMBOL } from '../testing';
14
-
15
14
  import * as stories from './ViewEditor.stories';
16
15
  import { type ViewEditorDebugObjects } from './ViewEditor.stories';
17
16
 
@@ -9,8 +9,7 @@ import * as Option from 'effect/Option';
9
9
  import * as Schema from 'effect/Schema';
10
10
  import React, { forwardRef, useCallback, useContext, useImperativeHandle, useMemo, useState } from 'react';
11
11
 
12
- import { Entity, Feed, Filter, Format, Obj, Query, QueryAST, Ref, type SchemaRegistry } from '@dxos/echo';
13
- import { View } from '@dxos/echo';
12
+ import { Entity, Feed, Filter, Format, Obj, Query, QueryAST, Ref, type SchemaRegistry, View } from '@dxos/echo';
14
13
  import { EchoSchema, type JsonProp, isMutable, toJsonSchema } from '@dxos/echo/internal';
15
14
  import { invariant } from '@dxos/invariant';
16
15
  import { useObject, useQuery } from '@dxos/react-client/echo';
@@ -24,9 +23,10 @@ import {
24
23
  createEchoChangeCallback,
25
24
  getTypenameFromQuery,
26
25
  } from '@dxos/schema';
27
- import { mx, osTranslations, subtleHover } from '@dxos/ui-theme';
26
+ import { mx, osTranslations } from '@dxos/ui-theme';
27
+
28
+ import { translationKey } from '#translations';
28
29
 
29
- import { translationKey } from '../../translations';
30
30
  import { FieldEditor } from '../FieldEditor';
31
31
  import {
32
32
  Form,
@@ -100,8 +100,8 @@ export const ViewEditor = forwardRef<ProjectionModel, ViewEditorProps>(
100
100
  if (from._tag !== 'scope') {
101
101
  return undefined;
102
102
  }
103
- return Option.fromNullable(from.scope.queues).pipe(
104
- Option.flatMap((queues) => Array.head(queues)),
103
+ return Option.fromNullable(from.scope.feeds).pipe(
104
+ Option.flatMap((feeds) => Array.head(feeds)),
105
105
  Option.map(String),
106
106
  Option.getOrUndefined,
107
107
  );
@@ -160,13 +160,13 @@ export const ViewEditor = forwardRef<ProjectionModel, ViewEditorProps>(
160
160
  const handleUpdate = useCallback(
161
161
  (values: any) => {
162
162
  const targetValue = values.target;
163
- let queueDxn: string | undefined;
163
+ let feedDXN: string | undefined;
164
164
 
165
165
  if (Ref.isRef(targetValue)) {
166
- const feedDxn = targetValue.dxn.toString();
167
- const feed = feeds.find((feed) => Obj.getDXN(feed).toString() === feedDxn);
166
+ const targetDXN = targetValue.dxn.toString();
167
+ const feed = feeds.find((feed) => Obj.getDXN(feed).toString() === targetDXN);
168
168
  if (feed) {
169
- queueDxn = Feed.getQueueDxn(feed)?.toString();
169
+ feedDXN = Feed.getQueueDxn(feed)?.toString();
170
170
  }
171
171
  }
172
172
 
@@ -175,7 +175,7 @@ export const ViewEditor = forwardRef<ProjectionModel, ViewEditorProps>(
175
175
  mode === 'schema'
176
176
  ? Query.select(Filter.typename(values.query)).ast
177
177
  : JSON.parse(JSON.stringify(values.query));
178
- onQueryChanged?.(query, queueDxn);
178
+ onQueryChanged?.(query, feedDXN);
179
179
  },
180
180
  [onQueryChanged, mode, feeds],
181
181
  );
@@ -189,11 +189,11 @@ export const ViewEditor = forwardRef<ProjectionModel, ViewEditorProps>(
189
189
  );
190
190
 
191
191
  return (
192
- <div role='none' className={mx(classNames)}>
192
+ <div className={mx(classNames)}>
193
193
  {/* If readonly is set, then the callout is not needed. */}
194
194
  {schemaReadonly && !readonly && (
195
195
  <Message.Root valence='info' classNames='my-form-padding'>
196
- <Message.Title>{t('system schema description')}</Message.Title>
196
+ <Message.Title>{t('system-schema.description')}</Message.Title>
197
197
  </Message.Root>
198
198
  )}
199
199
 
@@ -201,7 +201,7 @@ export const ViewEditor = forwardRef<ProjectionModel, ViewEditorProps>(
201
201
  <Form.Root schema={viewSchema} values={viewValues} fieldMap={fieldMap} db={db} onValuesChanged={handleUpdate}>
202
202
  <Form.FieldSet />
203
203
 
204
- <FormFieldLabel label={t('fields label')} asChild />
204
+ <FormFieldLabel label={t('fields.label')} asChild />
205
205
  <FieldList
206
206
  schema={schema}
207
207
  view={view}
@@ -274,12 +274,12 @@ const FieldList = ({ schema, view, registry, readonly, showHeading = false, onDe
274
274
  const handleMove = useCallback(
275
275
  (fromIndex: number, toIndex: number) => {
276
276
  invariant(!readonly);
277
- Obj.change(view, (v) => {
277
+ Obj.update(view, (view) => {
278
278
  // NOTE(ZaymonFC): Using arrayMove here causes a race condition with the kanban model.
279
- const fields = [...v.projection.fields];
279
+ const fields = [...view.projection.fields];
280
280
  const [moved] = fields.splice(fromIndex, 1);
281
281
  fields.splice(toIndex, 0, moved);
282
- v.projection.fields = fields;
282
+ view.projection.fields = fields;
283
283
  });
284
284
  },
285
285
  [view, readonly],
@@ -317,7 +317,7 @@ const FieldList = ({ schema, view, registry, readonly, showHeading = false, onDe
317
317
  >
318
318
  {({ items: fields }) => (
319
319
  <>
320
- {showHeading && <h3 className='text-sm'>{t('field path label')}</h3>}
320
+ {showHeading && <h3 className='text-sm'>{t('field-path.label')}</h3>}
321
321
  <div role='list' className='grid grid-cols-[min-content_1fr_min-content_min-content_min-content]'>
322
322
  {fields?.map((field) => {
323
323
  const hidden = field.visible === false;
@@ -328,20 +328,13 @@ const FieldList = ({ schema, view, registry, readonly, showHeading = false, onDe
328
328
  classNames={'grid grid-cols-subgrid col-span-5'}
329
329
  aria-expanded={expandedField === field.id}
330
330
  >
331
- <div
332
- role='none'
333
- className={mx(
334
- subtleHover,
335
- 'grid grid-cols-subgrid col-span-5',
336
- 'rounded-xs cursor-pointer min-h-10',
337
- )}
338
- >
331
+ <div className='grid grid-cols-subgrid col-span-5 rounded-xs cursor-pointer min-h-10 dx-hover'>
339
332
  <List.ItemDragHandle disabled={readonly || schemaReadonly} />
340
333
  <List.ItemTitle classNames={hidden && 'text-subdued'} onClick={() => handleToggleField(field)}>
341
334
  {field.path}
342
335
  </List.ItemTitle>
343
336
  <List.ItemIconButton
344
- label={t(hidden ? 'show field label' : 'hide field label')}
337
+ label={t(hidden ? 'show-field.label' : 'hide-field.label')}
345
338
  data-testid={hidden ? 'show-field-button' : 'hide-field-button'}
346
339
  icon={hidden ? 'ph--eye-closed--regular' : 'ph--eye--regular'}
347
340
  autoHide={false}
@@ -351,7 +344,7 @@ const FieldList = ({ schema, view, registry, readonly, showHeading = false, onDe
351
344
  {!readonly && (
352
345
  <>
353
346
  <List.ItemDeleteButton
354
- label={t('delete field label')}
347
+ label={t('delete-field.label')}
355
348
  autoHide={false}
356
349
  disabled={readonly || schemaReadonly || viewSnapshot.projection.fields.length <= 1}
357
350
  onClick={() => handleDelete(field.id)}
@@ -360,7 +353,7 @@ const FieldList = ({ schema, view, registry, readonly, showHeading = false, onDe
360
353
  <List.ItemIconButton
361
354
  iconOnly
362
355
  variant='ghost'
363
- label={t('toggle expand label', { ns: osTranslations })}
356
+ label={t('toggle-expand.label', { ns: osTranslations })}
364
357
  icon={expandedField === field.id ? 'ph--caret-down--regular' : 'ph--caret-right--regular'}
365
358
  onClick={() => handleToggleField(field)}
366
359
  data-testid='field.toggle'
@@ -369,7 +362,7 @@ const FieldList = ({ schema, view, registry, readonly, showHeading = false, onDe
369
362
  )}
370
363
  </div>
371
364
  {expandedField === field.id && !readonly && (
372
- <div role='none' className='col-span-5 mt-1 mb-1 border border-separator rounded-md'>
365
+ <div className='col-span-full mt-1 mb-1 border border-separator rounded-md'>
373
366
  <FieldEditor
374
367
  readonly={readonly || schemaReadonly}
375
368
  registry={registry}
@@ -384,10 +377,10 @@ const FieldList = ({ schema, view, registry, readonly, showHeading = false, onDe
384
377
  })}
385
378
  </div>
386
379
  {!readonly && !expandedField && (
387
- <div role='none' className='my-form-padding'>
380
+ <div className='my-form-padding'>
388
381
  <IconButton
389
382
  icon='ph--plus--regular'
390
- label={t('add property button label')}
383
+ label={t('add-property-button.label')}
391
384
  onClick={handleAdd}
392
385
  disabled={viewSnapshot.projection.fields.length >= VIEW_FIELD_LIMIT}
393
386
  classNames='w-full'
@@ -4,6 +4,8 @@
4
4
 
5
5
  export * from './FieldEditor';
6
6
  export * from './Form';
7
+ export * from './ObjectForm';
7
8
  export * from './ObjectPicker';
9
+ export * from './ObjectProperties';
8
10
  export * from './Settings';
9
11
  export * from './ViewEditor';
@@ -5,17 +5,24 @@
5
5
  import React, { type PropsWithChildren } from 'react';
6
6
 
7
7
  import { type ThemedClassName } from '@dxos/react-ui';
8
- import { JsonFilter } from '@dxos/react-ui-syntax-highlighter';
9
- import { mx } from '@dxos/ui-theme';
8
+ import { Syntax } from '@dxos/react-ui-syntax-highlighter';
9
+ import { composableProps, slottable } from '@dxos/ui-theme';
10
10
 
11
- type TestLayoutProps = ThemedClassName<PropsWithChildren<{ json?: any }>>;
11
+ type TestLayoutProps = PropsWithChildren<{ json?: any }>;
12
12
 
13
- export const TestLayout = ({ classNames, children, json }: TestLayoutProps) => {
13
+ export const TestLayout = ({ children, json }: TestLayoutProps) => {
14
14
  return (
15
- <div className='w-full h-full grid grid-cols-[1fr_1fr] p-4 gap-4 overflow-hidden'>
16
- <TestPanel classNames={['flex flex-col h-full overflow-y-auto', classNames]}>{children}</TestPanel>
17
- <TestPanel classNames={'overflow-hidden'}>
18
- <JsonFilter testId='debug' data={json} classNames='h-full text-xs' />
15
+ <div className='dx-container grid grid-cols-[1fr_1fr] p-4 gap-4'>
16
+ <TestPanel>{children}</TestPanel>
17
+ <TestPanel>
18
+ <Syntax.Root data={json}>
19
+ <Syntax.Content>
20
+ <Syntax.Filter />
21
+ <Syntax.Viewport>
22
+ <Syntax.Code testId='debug' classNames='text-xs' />
23
+ </Syntax.Viewport>
24
+ </Syntax.Content>
25
+ </Syntax.Root>
19
26
  </TestPanel>
20
27
  </div>
21
28
  );
@@ -23,14 +30,15 @@ export const TestLayout = ({ classNames, children, json }: TestLayoutProps) => {
23
30
 
24
31
  type TestPanelProps = ThemedClassName<PropsWithChildren>;
25
32
 
26
- const TestPanel = ({ classNames, children }: TestPanelProps) => {
33
+ const TestPanel = slottable<HTMLDivElement, TestPanelProps>(({ children }, forwardedRef) => {
27
34
  return (
28
- <div role='none' className={mx(['dx-document h-full bg-modal-surface rounded-md', classNames])}>
35
+ <div {...composableProps({ classNames: 'dx-container bg-modal-surface rounded-md' })} ref={forwardedRef}>
29
36
  {children}
30
37
  </div>
31
38
  );
32
- };
39
+ });
33
40
 
34
41
  // Symbol for accessing debug objects in tests.
35
42
  export const VIEW_EDITOR_DEBUG_SYMBOL = Symbol('viewEditorDebug');
36
43
  export const FIELD_EDITOR_DEBUG_SYMBOL = Symbol('fieldEditorDebug');
44
+ export const OBJECT_PROPERTIES_DEBUG_SYMBOL = Symbol('objectPropertiesDebug');
@@ -2,7 +2,7 @@
2
2
  // Copyright 2025 DXOS.org
3
3
  //
4
4
 
5
- import { useCallback, useEffect } from 'react';
5
+ import { type RefObject, useCallback, useLayoutEffect } from 'react';
6
6
 
7
7
  import { addEventListener } from '@dxos/async';
8
8
 
@@ -11,7 +11,7 @@ import { type FormHandler } from './useFormHandler';
11
11
  /**
12
12
  * Key handler.
13
13
  */
14
- export const useKeyHandler = (formElement: HTMLDivElement | null, form: FormHandler<any>) => {
14
+ export const useKeyHandler = (elRef: RefObject<HTMLDivElement | null>, form: FormHandler<any>) => {
15
15
  const handleKeyDown = useCallback(
16
16
  (event: KeyboardEvent) => {
17
17
  switch (event.key) {
@@ -26,12 +26,12 @@ export const useKeyHandler = (formElement: HTMLDivElement | null, form: FormHand
26
26
  [form.isValid, form.canSave, form.onSave, form.autoSave],
27
27
  );
28
28
 
29
- useEffect(() => {
30
- if (!formElement) {
29
+ useLayoutEffect(() => {
30
+ const el = elRef.current;
31
+ if (!el) {
31
32
  return;
32
33
  }
33
34
 
34
- // TODO(burdon): Move to @dxos/dom-util.
35
- return addEventListener(formElement, 'keydown', handleKeyDown);
36
- }, [formElement, handleKeyDown]);
35
+ return addEventListener(el, 'keydown', handleKeyDown);
36
+ }, [elRef, handleKeyDown]);
37
37
  };
package/src/index.ts CHANGED
@@ -2,10 +2,6 @@
2
2
  // Copyright 2024 DXOS.org
3
3
  //
4
4
 
5
- import { translations } from './translations';
6
-
7
5
  export * from './components';
8
6
  export * from './hooks';
9
7
  export * from './util';
10
-
11
- export { translations };
@@ -10,79 +10,82 @@ export const translations = [
10
10
  {
11
11
  'en-US': {
12
12
  [translationKey]: {
13
- 'fields label': 'Fields',
14
- 'hidden fields label': 'Hidden Fields',
15
- 'empty readonly ref field label': '(none)',
13
+ 'fields.label': 'Fields',
14
+ 'hidden-fields.label': 'Hidden Fields',
15
+ 'empty-readonly-ref-field.label': '(none)',
16
16
 
17
- // TODO(burdon): Standardize field/property.
18
- 'add property button label': 'Add property',
19
- 'field property label': 'Property',
20
- 'field property placeholder': 'Property name',
21
- 'field format label': 'Type',
22
- 'field path label': 'Field path',
23
- 'add field': 'Add field',
24
- 'delete field': 'Delete field',
25
- 'field limit reached': 'Maximum number of fields reached',
26
- 'boolean input true value': 'Yes',
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 combobox input placeholder': 'Search…',
32
- 'ref field placeholder_one': 'Select…',
33
- 'ref field placeholder_other': 'Select items…',
17
+ 'field-property.label': 'Property',
18
+ 'field-property.placeholder': 'Property name',
19
+ 'field-format.label': 'Type',
20
+ 'field-path.label': 'Field path',
34
21
 
35
- 'example placeholder': 'Example',
36
- 'latitude placeholder': 'Latitude (e.g., 40.7128)',
37
- 'longitude placeholder': 'Longitude (e.g., -74.0060)',
22
+ 'add-item.button': 'Add item',
23
+ 'add-property-button.label': 'Add property',
24
+ 'boolean-input-true.value': 'Yes',
25
+ 'boolean-input-false.value': 'No',
26
+ 'show-field.label': 'Show field',
27
+ 'hide-field.label': 'Hide field',
28
+ 'delete-field.label': 'Delete field',
29
+ 'create-option.label': 'Create',
30
+ 'add-tag.label': 'Add tag',
38
31
 
39
- 'cancel button label': 'Cancel',
40
- 'save button label': 'Save',
32
+ 'ref-field-combobox-input.placeholder': 'Search…',
33
+ 'ref-field.placeholder': 'Select…',
34
+ 'ref-field.placeholder_one': 'Select…',
35
+ 'ref-field.placeholder_other': 'Select items…',
41
36
 
42
- // FormatEnum
43
- 'format boolean': 'Boolean',
44
- 'format currency': 'Currency',
45
- 'format date': 'Date',
46
- 'format date-time': 'DateTime',
47
- 'format did': 'DID',
48
- 'format duration': 'Duration',
49
- 'format dxn': 'DXN',
50
- 'format email': 'Email',
51
- 'format formula': 'Formula',
52
- 'format hostname': 'Hostname',
53
- 'format integer': 'Integer',
54
- 'format json': 'JSON',
55
- 'format lnglat': 'Geopoint',
56
- 'format markdown': 'Markdown',
57
- 'format percent': 'Percent',
58
- 'format ref': 'Reference',
59
- 'format regex': 'RegExp',
60
- 'format string': 'String',
61
- 'format single-select': 'Select',
62
- 'format multi-select': 'Multi-select',
63
- 'format text': 'Long text',
64
- 'format time': 'Time',
65
- 'format timestamp': 'Timestamp',
66
- 'format user': 'User',
67
- 'format number': 'Number',
68
- 'format uri': 'URI',
69
- 'format url': 'URL',
70
- 'format uuid': 'UUID',
37
+ 'example.placeholder': 'Example',
38
+ 'latitude.placeholder': 'Latitude (e.g., 40.7128)',
39
+ 'longitude.placeholder': 'Longitude (e.g., -74.0060)',
40
+
41
+ 'cancel-button.label': 'Cancel',
42
+ 'save-button.label': 'Save',
43
+ 'create-text.label': 'Create text',
44
+
45
+ // TypeFormat
46
+ 'format.boolean.label': 'Boolean',
47
+ 'format.currency.label': 'Currency',
48
+ 'format.date.label': 'Date',
49
+ 'format.date-time.label': 'DateTime',
50
+ 'format.did.label': 'DID',
51
+ 'format.duration.label': 'Duration',
52
+ 'format.dxn.label': 'DXN',
53
+ 'format.email.label': 'Email',
54
+ 'format.formula.label': 'Formula',
55
+ 'format.hostname.label': 'Hostname',
56
+ 'format.integer.label': 'Integer',
57
+ 'format.json.label': 'JSON',
58
+ 'format.lnglat.label': 'Geopoint',
59
+ 'format.markdown.label': 'Markdown',
60
+ 'format.percent.label': 'Percent',
61
+ 'format.ref.label': 'Reference',
62
+ 'format.regex.label': 'RegExp',
63
+ 'format.string.label': 'String',
64
+ 'format.single-select.label': 'Select',
65
+ 'format.multi-select.label': 'Multi-select',
66
+ 'format.text.label': 'Long text',
67
+ 'format.time.label': 'Time',
68
+ 'format.timestamp.label': 'Timestamp',
69
+ 'format.user.label': 'User',
70
+ 'format.number.label': 'Number',
71
+ 'format.uri.label': 'URI',
72
+ 'format.url.label': 'URL',
73
+ 'format.uuid.label': 'UUID',
71
74
 
72
75
  // GeoPointField.
73
- 'latitude label': 'Latitude',
74
- 'longitude label': 'Longitude',
76
+ 'latitude.label': 'Latitude',
77
+ 'longitude.label': 'Longitude',
75
78
 
76
79
  // SelectOptionsField.
77
- 'select option label': 'Label',
78
- 'select option label placeholder': 'Option label',
79
- 'select option color': 'Color',
80
- 'select option delete': 'Delete',
81
- 'select option add': 'Add option',
80
+ 'select-option.label': 'Label',
81
+ 'select-option-label.placeholder': 'Option label',
82
+ 'select-option-delete.button': 'Delete',
83
+ 'select-option-add.button': 'Add option',
82
84
 
83
85
  // System schema message.
84
- 'system schema title': 'System type',
85
- 'system schema description': 'System type: cannot add or remove fields.',
86
+ 'system-schema.title': 'System type',
87
+ 'system-schema.description': 'System type: cannot add or remove fields.',
88
+ 'remove.button': 'Remove',
86
89
  },
87
90
  },
88
91
  },
@@ -0,0 +1,60 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import * as SchemaAST from 'effect/SchemaAST';
6
+
7
+ import { getAnnotation, isLiteralUnion } from '@dxos/effect';
8
+
9
+ /**
10
+ * Detected field type for a schema property.
11
+ */
12
+ export type SettingsFieldType = 'boolean' | 'string' | 'number' | 'select' | 'struct';
13
+
14
+ export type SelectOption = {
15
+ value: string | number;
16
+ label?: string;
17
+ };
18
+
19
+ /**
20
+ * Detect the field type from an Effect Schema AST node.
21
+ */
22
+ export const detectFieldType = (ast: SchemaAST.AST): SettingsFieldType | undefined => {
23
+ if (isLiteralUnion(ast)) {
24
+ return 'select';
25
+ }
26
+ if (SchemaAST.isTypeLiteral(ast)) {
27
+ return 'struct';
28
+ }
29
+
30
+ switch (ast._tag) {
31
+ case 'BooleanKeyword':
32
+ return 'boolean';
33
+ case 'StringKeyword':
34
+ return 'string';
35
+ case 'NumberKeyword':
36
+ return 'number';
37
+ }
38
+
39
+ return undefined;
40
+ };
41
+
42
+ /**
43
+ * Extract select options from a literal union AST node.
44
+ * Reads title annotations from individual literals for labels.
45
+ */
46
+ export const getSelectOptionsFromAst = (ast: SchemaAST.AST): SelectOption[] | undefined => {
47
+ if (!isLiteralUnion(ast)) {
48
+ return undefined;
49
+ }
50
+
51
+ return (ast as SchemaAST.Union<SchemaAST.Literal>).types.flatMap((literalNode) => {
52
+ const value = literalNode.literal;
53
+ if (typeof value !== 'string' && typeof value !== 'number') {
54
+ return [];
55
+ }
56
+ const title = getAnnotation<string>(SchemaAST.TitleAnnotationId, false)(literalNode);
57
+ const label = title ?? (typeof value === 'string' ? value.charAt(0).toUpperCase() + value.slice(1) : String(value));
58
+ return [{ value, label }];
59
+ });
60
+ };
package/src/util/index.ts CHANGED
@@ -3,6 +3,7 @@
3
3
  //
4
4
 
5
5
  export * from './field';
6
+ export * from './field-type';
6
7
  export * from './formatting';
7
8
  export * from './properties';
8
9
  export * from './refs';
@@ -0,0 +1,63 @@
1
+ //
2
+ // Copyright 2026 DXOS.org
3
+ //
4
+
5
+ import * as Schema from 'effect/Schema';
6
+ import { describe, test } from 'vitest';
7
+
8
+ import { Annotation, JsonSchema, Type } from '@dxos/echo';
9
+
10
+ import { getFormProperties } from './properties';
11
+
12
+ describe('getFormProperties', () => {
13
+ test('filters out keyword fields annotated FormInputAnnotation.set(false)', ({ expect }) => {
14
+ const TestSchema = Schema.Struct({
15
+ name: Schema.optional(Schema.String),
16
+ hidden: Schema.String.pipe(Annotation.FormInputAnnotation.set(false)),
17
+ }).pipe(Type.object({ typename: 'org.dxos.test.keyword-hidden', version: '0.1.0' }));
18
+
19
+ const names = getFormProperties(TestSchema.ast).map((prop) => prop.name);
20
+ expect(names).toContain('name');
21
+ expect(names).not.toContain('hidden');
22
+ });
23
+
24
+ test('filters out struct fields annotated FormInputAnnotation.set(false)', ({ expect }) => {
25
+ // Regression: JsonSchema fields are a Schema.Struct; encodedBoundAST strips
26
+ // annotations from non-keyword inner types, which previously bypassed the
27
+ // form-input filter (e.g., Routine.input).
28
+ const TestSchema = Schema.Struct({
29
+ name: Schema.optional(Schema.String),
30
+ schema: JsonSchema.JsonSchema.pipe(Annotation.FormInputAnnotation.set(false)),
31
+ }).pipe(Type.object({ typename: 'org.dxos.test.struct-hidden', version: '0.1.0' }));
32
+
33
+ const names = getFormProperties(TestSchema.ast).map((prop) => prop.name);
34
+ expect(names).toContain('name');
35
+ expect(names).not.toContain('schema');
36
+ });
37
+
38
+ test('filters out array fields annotated FormInputAnnotation.set(false)', ({ expect }) => {
39
+ const TestSchema = Schema.Struct({
40
+ name: Schema.optional(Schema.String),
41
+ context: Schema.Array(Schema.Any).pipe(Annotation.FormInputAnnotation.set(false)),
42
+ }).pipe(Type.object({ typename: 'org.dxos.test.array-hidden', version: '0.1.0' }));
43
+
44
+ const names = getFormProperties(TestSchema.ast).map((prop) => prop.name);
45
+ expect(names).toContain('name');
46
+ expect(names).not.toContain('context');
47
+ });
48
+
49
+ test('preserves annotation when chained with .annotations()', ({ expect }) => {
50
+ // Regression: `.pipe(FormInputAnnotation.set(false)).annotations({...})` must
51
+ // not lose the form-input annotation.
52
+ const TestSchema = Schema.Struct({
53
+ name: Schema.optional(Schema.String),
54
+ hidden: JsonSchema.JsonSchema.pipe(Annotation.FormInputAnnotation.set(false)).annotations({
55
+ description: 'Hidden field',
56
+ }),
57
+ }).pipe(Type.object({ typename: 'org.dxos.test.chained-hidden', version: '0.1.0' }));
58
+
59
+ const names = getFormProperties(TestSchema.ast).map((prop) => prop.name);
60
+ expect(names).toContain('name');
61
+ expect(names).not.toContain('hidden');
62
+ });
63
+ });