@dxos/react-ui-form 0.8.4-main.c85a9c8dae → 0.8.4-main.d05539e30a

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 +1028 -625
  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 +1028 -625
  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 -7
  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 -9
  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 +15 -7
  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 +25 -38
  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 -4
  110. package/src/components/Form/FormFieldSet.tsx +98 -77
  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 +79 -39
  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 +14 -15
  136. package/src/components/ViewEditor/ViewEditor.test.tsx +0 -1
  137. package/src/components/ViewEditor/ViewEditor.tsx +40 -36
  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
@@ -0,0 +1,59 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import React from 'react';
6
+
7
+ import { Select } from '@dxos/react-ui';
8
+
9
+ import { type SettingsFieldProps } from './SettingsFieldSet';
10
+
11
+ export type SelectFieldOption = {
12
+ value: string;
13
+ label?: string;
14
+ };
15
+
16
+ export type CreateSelectFieldOptions = {
17
+ /** Options to display. Strings are used as both value and label. */
18
+ options: ReadonlyArray<string | SelectFieldOption>;
19
+ /** Label for the sentinel option that maps to `undefined`. Defaults to `'Default'`. Pass `null` to omit. */
20
+ defaultLabel?: string | null;
21
+ };
22
+
23
+ /**
24
+ * Factory for a `SettingsFieldSet` `fieldMap` entry that renders a Select control
25
+ * over a fixed list of options, with an optional sentinel for `undefined`.
26
+ */
27
+ export const createSelectField = ({
28
+ options,
29
+ defaultLabel = 'Default',
30
+ }: CreateSelectFieldOptions): React.FC<SettingsFieldProps<string | undefined>> => {
31
+ const normalized = options.map((option) => (typeof option === 'string' ? { value: option, label: option } : option));
32
+ const hasDefault = defaultLabel !== null;
33
+ // Per-instance sentinel avoids collisions with real option values; empty string when the
34
+ // sentinel is omitted so `undefined` maps to a value Select.Root treats as "no selection".
35
+ const sentinel = hasDefault ? `__default__${Math.random().toString(36).slice(2)}` : '';
36
+
37
+ return ({ value, onChange, readonly }) => (
38
+ <Select.Root
39
+ disabled={readonly}
40
+ value={value ?? sentinel}
41
+ onValueChange={(next) => onChange(hasDefault && next === sentinel ? undefined : next)}
42
+ >
43
+ <Select.TriggerButton disabled={readonly} />
44
+ <Select.Portal>
45
+ <Select.Content>
46
+ <Select.Viewport>
47
+ {hasDefault && <Select.Option value={sentinel}>{defaultLabel}</Select.Option>}
48
+ {normalized.map((option) => (
49
+ <Select.Option key={option.value} value={option.value}>
50
+ {option.label ?? option.value}
51
+ </Select.Option>
52
+ ))}
53
+ </Select.Viewport>
54
+ <Select.Arrow />
55
+ </Select.Content>
56
+ </Select.Portal>
57
+ </Select.Root>
58
+ );
59
+ };
@@ -3,3 +3,5 @@
3
3
  //
4
4
 
5
5
  export * from './Settings';
6
+ export { type SettingsFieldMap, type SettingsFieldProps, type SettingsFieldSetProps } from './SettingsFieldSet';
7
+ export { createSelectField, type CreateSelectFieldOptions, type SelectFieldOption } from './SettingsSelectField';
@@ -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>();
@@ -54,7 +53,7 @@ const DefaultStory = (props: StoryProps) => {
54
53
  salary: Format.Currency(),
55
54
  }).pipe(
56
55
  Type.object({
57
- typename: 'example.com/type/Test',
56
+ typename: 'com.example.type.test',
58
57
  version: '0.1.0',
59
58
  }),
60
59
  );
@@ -65,7 +64,7 @@ const DefaultStory = (props: StoryProps) => {
65
64
  completed: Schema.Boolean,
66
65
  }).pipe(
67
66
  Type.object({
68
- typename: 'example.com/type/Alternate',
67
+ typename: 'com.example.type.alternate',
69
68
  version: '0.1.0',
70
69
  }),
71
70
  );
@@ -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,18 +9,24 @@ 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';
17
16
  import { IconButton, Input, Message, type ThemedClassName, useTranslation } from '@dxos/react-ui';
18
17
  import { QueryForm, type QueryFormProps } from '@dxos/react-ui-components';
19
18
  import { List } from '@dxos/react-ui-list';
20
- import { ProjectionModel, VIEW_FIELD_LIMIT, createEchoChangeCallback, getTypenameFromQuery } from '@dxos/schema';
21
- import { mx, osTranslations, subtleHover } from '@dxos/ui-theme';
19
+ import {
20
+ ParentLabelAnnotation,
21
+ ProjectionModel,
22
+ VIEW_FIELD_LIMIT,
23
+ createEchoChangeCallback,
24
+ getTypenameFromQuery,
25
+ } from '@dxos/schema';
26
+ import { mx, osTranslations } from '@dxos/ui-theme';
27
+
28
+ import { translationKey } from '#translations';
22
29
 
23
- import { translationKey } from '../../translations';
24
30
  import { FieldEditor } from '../FieldEditor';
25
31
  import {
26
32
  Form,
@@ -94,8 +100,8 @@ export const ViewEditor = forwardRef<ProjectionModel, ViewEditorProps>(
94
100
  if (from._tag !== 'scope') {
95
101
  return undefined;
96
102
  }
97
- return Option.fromNullable(from.scope.queues).pipe(
98
- Option.flatMap((queues) => Array.head(queues)),
103
+ return Option.fromNullable(from.scope.feeds).pipe(
104
+ Option.flatMap((feeds) => Array.head(feeds)),
99
105
  Option.map(String),
100
106
  Option.getOrUndefined,
101
107
  );
@@ -124,7 +130,12 @@ export const ViewEditor = forwardRef<ProjectionModel, ViewEditorProps>(
124
130
  if (mode === 'tag') {
125
131
  return Schema.Struct({
126
132
  ...base.fields,
127
- target: Schema.optional(Ref.Ref(Feed.Feed).annotations({ title: 'Target Feed' })),
133
+ // TODO(wittjosiah): Replace Type.Feed with Dataset.Dataset when Ref.Ref supports unions.
134
+ target: Ref.Ref(Feed.Feed).pipe(
135
+ Schema.annotations({ title: 'Target Feed' }),
136
+ ParentLabelAnnotation.set(true),
137
+ Schema.optional,
138
+ ),
128
139
  }).pipe(Schema.mutable);
129
140
  }
130
141
 
@@ -149,13 +160,13 @@ export const ViewEditor = forwardRef<ProjectionModel, ViewEditorProps>(
149
160
  const handleUpdate = useCallback(
150
161
  (values: any) => {
151
162
  const targetValue = values.target;
152
- let queueDxn: string | undefined;
163
+ let feedDXN: string | undefined;
153
164
 
154
165
  if (Ref.isRef(targetValue)) {
155
- const feedDxn = targetValue.dxn.toString();
156
- 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);
157
168
  if (feed) {
158
- queueDxn = Feed.getQueueDxn(feed)?.toString();
169
+ feedDXN = Feed.getQueueDxn(feed)?.toString();
159
170
  }
160
171
  }
161
172
 
@@ -164,7 +175,7 @@ export const ViewEditor = forwardRef<ProjectionModel, ViewEditorProps>(
164
175
  mode === 'schema'
165
176
  ? Query.select(Filter.typename(values.query)).ast
166
177
  : JSON.parse(JSON.stringify(values.query));
167
- onQueryChanged?.(query, queueDxn);
178
+ onQueryChanged?.(query, feedDXN);
168
179
  },
169
180
  [onQueryChanged, mode, feeds],
170
181
  );
@@ -178,11 +189,11 @@ export const ViewEditor = forwardRef<ProjectionModel, ViewEditorProps>(
178
189
  );
179
190
 
180
191
  return (
181
- <div role='none' className={mx(classNames)}>
192
+ <div className={mx(classNames)}>
182
193
  {/* If readonly is set, then the callout is not needed. */}
183
194
  {schemaReadonly && !readonly && (
184
195
  <Message.Root valence='info' classNames='my-form-padding'>
185
- <Message.Title>{t('system schema description')}</Message.Title>
196
+ <Message.Title>{t('system-schema.description')}</Message.Title>
186
197
  </Message.Root>
187
198
  )}
188
199
 
@@ -190,7 +201,7 @@ export const ViewEditor = forwardRef<ProjectionModel, ViewEditorProps>(
190
201
  <Form.Root schema={viewSchema} values={viewValues} fieldMap={fieldMap} db={db} onValuesChanged={handleUpdate}>
191
202
  <Form.FieldSet />
192
203
 
193
- <FormFieldLabel label={t('fields label')} asChild />
204
+ <FormFieldLabel label={t('fields.label')} asChild />
194
205
  <FieldList
195
206
  schema={schema}
196
207
  view={view}
@@ -263,12 +274,12 @@ const FieldList = ({ schema, view, registry, readonly, showHeading = false, onDe
263
274
  const handleMove = useCallback(
264
275
  (fromIndex: number, toIndex: number) => {
265
276
  invariant(!readonly);
266
- Obj.change(view, (v) => {
277
+ Obj.update(view, (view) => {
267
278
  // NOTE(ZaymonFC): Using arrayMove here causes a race condition with the kanban model.
268
- const fields = [...v.projection.fields];
279
+ const fields = [...view.projection.fields];
269
280
  const [moved] = fields.splice(fromIndex, 1);
270
281
  fields.splice(toIndex, 0, moved);
271
- v.projection.fields = fields;
282
+ view.projection.fields = fields;
272
283
  });
273
284
  },
274
285
  [view, readonly],
@@ -306,7 +317,7 @@ const FieldList = ({ schema, view, registry, readonly, showHeading = false, onDe
306
317
  >
307
318
  {({ items: fields }) => (
308
319
  <>
309
- {showHeading && <h3 className='text-sm'>{t('field path label')}</h3>}
320
+ {showHeading && <h3 className='text-sm'>{t('field-path.label')}</h3>}
310
321
  <div role='list' className='grid grid-cols-[min-content_1fr_min-content_min-content_min-content]'>
311
322
  {fields?.map((field) => {
312
323
  const hidden = field.visible === false;
@@ -317,20 +328,13 @@ const FieldList = ({ schema, view, registry, readonly, showHeading = false, onDe
317
328
  classNames={'grid grid-cols-subgrid col-span-5'}
318
329
  aria-expanded={expandedField === field.id}
319
330
  >
320
- <div
321
- role='none'
322
- className={mx(
323
- subtleHover,
324
- 'grid grid-cols-subgrid col-span-5',
325
- 'rounded-xs cursor-pointer min-h-10',
326
- )}
327
- >
331
+ <div className='grid grid-cols-subgrid col-span-5 rounded-xs cursor-pointer min-h-10 dx-hover'>
328
332
  <List.ItemDragHandle disabled={readonly || schemaReadonly} />
329
333
  <List.ItemTitle classNames={hidden && 'text-subdued'} onClick={() => handleToggleField(field)}>
330
334
  {field.path}
331
335
  </List.ItemTitle>
332
- <List.ItemButton
333
- label={t(hidden ? 'show field label' : 'hide field label')}
336
+ <List.ItemIconButton
337
+ label={t(hidden ? 'show-field.label' : 'hide-field.label')}
334
338
  data-testid={hidden ? 'show-field-button' : 'hide-field-button'}
335
339
  icon={hidden ? 'ph--eye-closed--regular' : 'ph--eye--regular'}
336
340
  autoHide={false}
@@ -340,16 +344,16 @@ const FieldList = ({ schema, view, registry, readonly, showHeading = false, onDe
340
344
  {!readonly && (
341
345
  <>
342
346
  <List.ItemDeleteButton
343
- label={t('delete field label')}
347
+ label={t('delete-field.label')}
344
348
  autoHide={false}
345
349
  disabled={readonly || schemaReadonly || viewSnapshot.projection.fields.length <= 1}
346
350
  onClick={() => handleDelete(field.id)}
347
351
  data-testid='field.delete'
348
352
  />
349
- <IconButton
353
+ <List.ItemIconButton
350
354
  iconOnly
351
355
  variant='ghost'
352
- label={t('toggle expand label', { ns: osTranslations })}
356
+ label={t('toggle-expand.label', { ns: osTranslations })}
353
357
  icon={expandedField === field.id ? 'ph--caret-down--regular' : 'ph--caret-right--regular'}
354
358
  onClick={() => handleToggleField(field)}
355
359
  data-testid='field.toggle'
@@ -358,7 +362,7 @@ const FieldList = ({ schema, view, registry, readonly, showHeading = false, onDe
358
362
  )}
359
363
  </div>
360
364
  {expandedField === field.id && !readonly && (
361
- <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'>
362
366
  <FieldEditor
363
367
  readonly={readonly || schemaReadonly}
364
368
  registry={registry}
@@ -373,10 +377,10 @@ const FieldList = ({ schema, view, registry, readonly, showHeading = false, onDe
373
377
  })}
374
378
  </div>
375
379
  {!readonly && !expandedField && (
376
- <div role='none' className='my-form-padding'>
380
+ <div className='my-form-padding'>
377
381
  <IconButton
378
382
  icon='ph--plus--regular'
379
- label={t('add property button label')}
383
+ label={t('add-property-button.label')}
380
384
  onClick={handleAdd}
381
385
  disabled={viewSnapshot.projection.fields.length >= VIEW_FIELD_LIMIT}
382
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, textBlockWidth } 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(['h-full bg-modal-surface rounded-md', textBlockWidth, 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
  },