@dxos/react-ui-form 0.8.4-main.d05673bc65 → 0.8.4-main.d9fc60f731

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 (209) hide show
  1. package/LICENSE +102 -5
  2. package/dist/lib/browser/index.mjs +2008 -840
  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 +83 -0
  6. package/dist/lib/browser/translations.mjs.map +7 -0
  7. package/dist/lib/node-esm/index.mjs +2008 -840
  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 +85 -0
  11. package/dist/lib/node-esm/translations.mjs.map +7 -0
  12. package/dist/types/src/components/FieldEditor/FieldEditor.d.ts +2 -2
  13. package/dist/types/src/components/FieldEditor/FieldEditor.d.ts.map +1 -1
  14. package/dist/types/src/components/FieldEditor/FieldEditor.stories.d.ts +64 -61
  15. package/dist/types/src/components/FieldEditor/FieldEditor.stories.d.ts.map +1 -1
  16. package/dist/types/src/components/Form/Form.d.ts +71 -17
  17. package/dist/types/src/components/Form/Form.d.ts.map +1 -1
  18. package/dist/types/src/components/Form/Form.stories.d.ts +148 -86
  19. package/dist/types/src/components/Form/Form.stories.d.ts.map +1 -1
  20. package/dist/types/src/components/Form/FormField.d.ts +24 -8
  21. package/dist/types/src/components/Form/FormField.d.ts.map +1 -1
  22. package/dist/types/src/components/Form/FormFieldComponent.d.ts +26 -13
  23. package/dist/types/src/components/Form/FormFieldComponent.d.ts.map +1 -1
  24. package/dist/types/src/components/Form/FormFieldComponent.stories.d.ts.map +1 -1
  25. package/dist/types/src/components/Form/FormFieldSet.d.ts +11 -9
  26. package/dist/types/src/components/Form/FormFieldSet.d.ts.map +1 -1
  27. package/dist/types/src/components/Form/FormTooltipsContext.d.ts +19 -0
  28. package/dist/types/src/components/Form/FormTooltipsContext.d.ts.map +1 -0
  29. package/dist/types/src/components/Form/Layout/FormLayout.d.ts +50 -0
  30. package/dist/types/src/components/Form/Layout/FormLayout.d.ts.map +1 -0
  31. package/dist/types/src/components/Form/Layout/FormLayout.stories.d.ts +119 -0
  32. package/dist/types/src/components/Form/Layout/FormLayout.stories.d.ts.map +1 -0
  33. package/dist/types/src/components/Form/Layout/FormLayout.stories.test.d.ts +2 -0
  34. package/dist/types/src/components/Form/Layout/FormLayout.stories.test.d.ts.map +1 -0
  35. package/dist/types/src/components/Form/Layout/FormLayout.test.d.ts +2 -0
  36. package/dist/types/src/components/Form/Layout/FormLayout.test.d.ts.map +1 -0
  37. package/dist/types/src/components/Form/Layout/annotation.d.ts +30 -0
  38. package/dist/types/src/components/Form/Layout/annotation.d.ts.map +1 -0
  39. package/dist/types/src/components/Form/Layout/index.d.ts +4 -0
  40. package/dist/types/src/components/Form/Layout/index.d.ts.map +1 -0
  41. package/dist/types/src/components/Form/Layout/parser.d.ts +31 -0
  42. package/dist/types/src/components/Form/Layout/parser.d.ts.map +1 -0
  43. package/dist/types/src/components/Form/Layout/parser.test.d.ts +2 -0
  44. package/dist/types/src/components/Form/Layout/parser.test.d.ts.map +1 -0
  45. package/dist/types/src/components/Form/fields/ArrayField.d.ts +4 -4
  46. package/dist/types/src/components/Form/fields/ArrayField.d.ts.map +1 -1
  47. package/dist/types/src/components/Form/fields/BooleanField.d.ts.map +1 -1
  48. package/dist/types/src/components/Form/fields/DateField.d.ts +4 -0
  49. package/dist/types/src/components/Form/fields/DateField.d.ts.map +1 -0
  50. package/dist/types/src/components/Form/fields/GeoPointField.d.ts +1 -1
  51. package/dist/types/src/components/Form/fields/GeoPointField.d.ts.map +1 -1
  52. package/dist/types/src/components/Form/fields/InlineRefField.d.ts +13 -0
  53. package/dist/types/src/components/Form/fields/InlineRefField.d.ts.map +1 -0
  54. package/dist/types/src/components/Form/fields/MarkdownField.d.ts +8 -1
  55. package/dist/types/src/components/Form/fields/MarkdownField.d.ts.map +1 -1
  56. package/dist/types/src/components/Form/fields/MarkdownField.stories.d.ts +85 -0
  57. package/dist/types/src/components/Form/fields/MarkdownField.stories.d.ts.map +1 -0
  58. package/dist/types/src/components/Form/fields/NumberField.d.ts.map +1 -1
  59. package/dist/types/src/components/Form/fields/RefField.d.ts +12 -6
  60. package/dist/types/src/components/Form/fields/RefField.d.ts.map +1 -1
  61. package/dist/types/src/components/Form/fields/SelectField.d.ts +4 -1
  62. package/dist/types/src/components/Form/fields/SelectField.d.ts.map +1 -1
  63. package/dist/types/src/components/Form/fields/SelectOptionField.d.ts +1 -1
  64. package/dist/types/src/components/Form/fields/SelectOptionField.d.ts.map +1 -1
  65. package/dist/types/src/components/Form/fields/SelectOptionField.stories.d.ts +64 -61
  66. package/dist/types/src/components/Form/fields/SelectOptionField.stories.d.ts.map +1 -1
  67. package/dist/types/src/components/Form/fields/TextAreaField.d.ts.map +1 -1
  68. package/dist/types/src/components/Form/fields/TextField.d.ts +1 -1
  69. package/dist/types/src/components/Form/fields/TextField.d.ts.map +1 -1
  70. package/dist/types/src/components/Form/fields/TupleField.d.ts +1 -1
  71. package/dist/types/src/components/Form/fields/TupleField.d.ts.map +1 -1
  72. package/dist/types/src/components/Form/fields/index.d.ts +2 -0
  73. package/dist/types/src/components/Form/fields/index.d.ts.map +1 -1
  74. package/dist/types/src/components/Form/index.d.ts +3 -0
  75. package/dist/types/src/components/Form/index.d.ts.map +1 -1
  76. package/dist/types/src/components/Form/meta-tags.d.ts +30 -0
  77. package/dist/types/src/components/Form/meta-tags.d.ts.map +1 -0
  78. package/dist/types/src/components/Form/meta-tags.test.d.ts +2 -0
  79. package/dist/types/src/components/Form/meta-tags.test.d.ts.map +1 -0
  80. package/dist/types/src/components/ObjectForm/ObjectForm.d.ts +8 -0
  81. package/dist/types/src/components/ObjectForm/ObjectForm.d.ts.map +1 -0
  82. package/dist/types/src/components/ObjectForm/index.d.ts +2 -0
  83. package/dist/types/src/components/ObjectForm/index.d.ts.map +1 -0
  84. package/dist/types/src/components/ObjectPicker/ObjectPicker.d.ts +28 -6
  85. package/dist/types/src/components/ObjectPicker/ObjectPicker.d.ts.map +1 -1
  86. package/dist/types/src/components/ObjectPicker/ObjectPicker.stories.d.ts +64 -61
  87. package/dist/types/src/components/ObjectPicker/ObjectPicker.stories.d.ts.map +1 -1
  88. package/dist/types/src/components/ObjectProperties/ObjectProperties.d.ts +9 -0
  89. package/dist/types/src/components/ObjectProperties/ObjectProperties.d.ts.map +1 -0
  90. package/dist/types/src/components/ObjectProperties/ObjectProperties.stories.d.ts +128 -0
  91. package/dist/types/src/components/ObjectProperties/ObjectProperties.stories.d.ts.map +1 -0
  92. package/dist/types/src/components/ObjectProperties/ObjectProperties.test.d.ts +2 -0
  93. package/dist/types/src/components/ObjectProperties/ObjectProperties.test.d.ts.map +1 -0
  94. package/dist/types/src/components/ObjectProperties/index.d.ts +2 -0
  95. package/dist/types/src/components/ObjectProperties/index.d.ts.map +1 -0
  96. package/dist/types/src/components/ObjectTree/ObjectTree.d.ts +9 -0
  97. package/dist/types/src/components/ObjectTree/ObjectTree.d.ts.map +1 -0
  98. package/dist/types/src/components/ObjectTree/ObjectTree.stories.d.ts +87 -0
  99. package/dist/types/src/components/ObjectTree/ObjectTree.stories.d.ts.map +1 -0
  100. package/dist/types/src/components/ObjectTree/index.d.ts +2 -0
  101. package/dist/types/src/components/ObjectTree/index.d.ts.map +1 -0
  102. package/dist/types/src/components/Settings/Settings.d.ts +28 -33
  103. package/dist/types/src/components/Settings/Settings.d.ts.map +1 -1
  104. package/dist/types/src/components/Settings/Settings.stories.d.ts +65 -61
  105. package/dist/types/src/components/Settings/Settings.stories.d.ts.map +1 -1
  106. package/dist/types/src/components/Settings/SettingsFieldSet.d.ts +43 -0
  107. package/dist/types/src/components/Settings/SettingsFieldSet.d.ts.map +1 -0
  108. package/dist/types/src/components/Settings/SettingsItem.d.ts +16 -0
  109. package/dist/types/src/components/Settings/SettingsItem.d.ts.map +1 -0
  110. package/dist/types/src/components/Settings/SettingsSelectField.d.ts +18 -0
  111. package/dist/types/src/components/Settings/SettingsSelectField.d.ts.map +1 -0
  112. package/dist/types/src/components/Settings/index.d.ts +2 -0
  113. package/dist/types/src/components/Settings/index.d.ts.map +1 -1
  114. package/dist/types/src/components/ViewEditor/ViewEditor.d.ts +9 -11
  115. package/dist/types/src/components/ViewEditor/ViewEditor.d.ts.map +1 -1
  116. package/dist/types/src/components/ViewEditor/ViewEditor.stories.d.ts +68 -66
  117. package/dist/types/src/components/ViewEditor/ViewEditor.stories.d.ts.map +1 -1
  118. package/dist/types/src/components/index.d.ts +3 -0
  119. package/dist/types/src/components/index.d.ts.map +1 -1
  120. package/dist/types/src/components/testing.d.ts +13 -4
  121. package/dist/types/src/components/testing.d.ts.map +1 -1
  122. package/dist/types/src/hooks/useFormHandler.d.ts.map +1 -1
  123. package/dist/types/src/hooks/useKeyHandler.d.ts +2 -1
  124. package/dist/types/src/hooks/useKeyHandler.d.ts.map +1 -1
  125. package/dist/types/src/index.d.ts +0 -2
  126. package/dist/types/src/index.d.ts.map +1 -1
  127. package/dist/types/src/translations.d.ts +64 -61
  128. package/dist/types/src/translations.d.ts.map +1 -1
  129. package/dist/types/src/util/field-type.d.ts +19 -0
  130. package/dist/types/src/util/field-type.d.ts.map +1 -0
  131. package/dist/types/src/util/field.d.ts.map +1 -1
  132. package/dist/types/src/util/formatting.d.ts.map +1 -1
  133. package/dist/types/src/util/index.d.ts +1 -0
  134. package/dist/types/src/util/index.d.ts.map +1 -1
  135. package/dist/types/src/util/properties.d.ts +18 -1
  136. package/dist/types/src/util/properties.d.ts.map +1 -1
  137. package/dist/types/src/util/properties.test.d.ts +2 -0
  138. package/dist/types/src/util/properties.test.d.ts.map +1 -0
  139. package/dist/types/src/util/refs.d.ts +1 -0
  140. package/dist/types/src/util/refs.d.ts.map +1 -1
  141. package/dist/types/tsconfig.tsbuildinfo +1 -1
  142. package/package.json +48 -41
  143. package/src/components/FieldEditor/FieldEditor.stories.tsx +4 -4
  144. package/src/components/FieldEditor/FieldEditor.test.tsx +4 -2
  145. package/src/components/FieldEditor/FieldEditor.tsx +23 -17
  146. package/src/components/Form/AUDIT.md +17 -0
  147. package/src/components/Form/Form.stories.tsx +46 -65
  148. package/src/components/Form/Form.tsx +176 -60
  149. package/src/components/Form/FormField.tsx +107 -38
  150. package/src/components/Form/FormFieldComponent.tsx +77 -20
  151. package/src/components/Form/FormFieldSet.tsx +128 -78
  152. package/src/components/Form/FormTooltipsContext.ts +25 -0
  153. package/src/components/Form/Layout/FormLayout.stories.test.tsx +51 -0
  154. package/src/components/Form/Layout/FormLayout.stories.tsx +422 -0
  155. package/src/components/Form/Layout/FormLayout.test.ts +59 -0
  156. package/src/components/Form/Layout/FormLayout.tsx +251 -0
  157. package/src/components/Form/Layout/annotation.ts +38 -0
  158. package/src/components/Form/Layout/index.ts +7 -0
  159. package/src/components/Form/Layout/parser.test.ts +120 -0
  160. package/src/components/Form/Layout/parser.ts +167 -0
  161. package/src/components/Form/fields/ArrayField.tsx +64 -35
  162. package/src/components/Form/fields/DateField.tsx +101 -0
  163. package/src/components/Form/fields/GeoPointField.tsx +12 -8
  164. package/src/components/Form/fields/InlineRefField.tsx +146 -0
  165. package/src/components/Form/fields/MarkdownField.stories.tsx +206 -0
  166. package/src/components/Form/fields/MarkdownField.tsx +135 -45
  167. package/src/components/Form/fields/RefField.tsx +83 -42
  168. package/src/components/Form/fields/SelectField.tsx +17 -5
  169. package/src/components/Form/fields/SelectOptionField.stories.tsx +8 -6
  170. package/src/components/Form/fields/SelectOptionField.tsx +14 -13
  171. package/src/components/Form/fields/TextAreaField.tsx +1 -1
  172. package/src/components/Form/fields/TextField.tsx +2 -2
  173. package/src/components/Form/fields/TupleField.tsx +3 -2
  174. package/src/components/Form/fields/index.ts +2 -0
  175. package/src/components/Form/index.ts +3 -0
  176. package/src/components/Form/meta-tags.test.ts +47 -0
  177. package/src/components/Form/meta-tags.ts +33 -0
  178. package/src/components/ObjectForm/ObjectForm.tsx +109 -0
  179. package/src/components/ObjectForm/index.ts +5 -0
  180. package/src/components/ObjectPicker/ObjectPicker.stories.tsx +13 -11
  181. package/src/components/ObjectPicker/ObjectPicker.tsx +93 -66
  182. package/src/components/ObjectProperties/ObjectProperties.stories.tsx +350 -0
  183. package/src/components/ObjectProperties/ObjectProperties.test.tsx +111 -0
  184. package/src/components/ObjectProperties/ObjectProperties.tsx +144 -0
  185. package/src/components/ObjectProperties/index.ts +5 -0
  186. package/src/components/ObjectTree/ObjectTree.stories.tsx +134 -0
  187. package/src/components/ObjectTree/ObjectTree.tsx +190 -0
  188. package/src/components/ObjectTree/index.ts +5 -0
  189. package/src/components/Settings/AUDIT.md +51 -0
  190. package/src/components/Settings/Settings.stories.tsx +69 -21
  191. package/src/components/Settings/Settings.tsx +32 -181
  192. package/src/components/Settings/SettingsFieldSet.tsx +257 -0
  193. package/src/components/Settings/SettingsItem.tsx +58 -0
  194. package/src/components/Settings/SettingsSelectField.tsx +59 -0
  195. package/src/components/Settings/index.ts +2 -0
  196. package/src/components/ViewEditor/ViewEditor.stories.tsx +45 -51
  197. package/src/components/ViewEditor/ViewEditor.test.tsx +11 -9
  198. package/src/components/ViewEditor/ViewEditor.tsx +45 -55
  199. package/src/components/index.ts +3 -0
  200. package/src/components/testing.tsx +19 -11
  201. package/src/hooks/useKeyHandler.tsx +7 -7
  202. package/src/index.ts +0 -4
  203. package/src/translations.ts +67 -63
  204. package/src/util/field-type.ts +60 -0
  205. package/src/util/index.ts +1 -0
  206. package/src/util/properties.test.ts +121 -0
  207. package/src/util/properties.ts +47 -4
  208. package/src/util/refs.ts +7 -3
  209. package/src/vite-env.d.ts +10 -0
@@ -2,77 +2,167 @@
2
2
  // Copyright 2024 DXOS.org
3
3
  //
4
4
 
5
- import React, { useCallback, useEffect, useRef } from 'react';
5
+ import { useAtomValue } from '@effect-atom/atom-react';
6
+ import React, { useCallback, useMemo } from 'react';
6
7
 
7
- import { Input, type TextAreaProps } from '@dxos/react-ui';
8
+ import { type Database, Obj, Ref } from '@dxos/echo';
9
+ import { AtomRef } from '@dxos/echo-atom';
10
+ import { createDocAccessor } from '@dxos/echo-db';
11
+ import { Button, Icon, Input, useTranslation } from '@dxos/react-ui';
12
+ import { Editor, useBasicMarkdownExtensions } from '@dxos/react-ui-editor';
13
+ import { Text } from '@dxos/schema';
14
+ import { createDataExtensions } from '@dxos/ui-editor';
15
+
16
+ import { translationKey } from '#translations';
8
17
 
9
18
  import { type FormFieldComponentProps, FormFieldLabel } from '../FormFieldComponent';
10
19
 
20
+ const editorClassNames =
21
+ 'transition-colors bg-input-surface focus-within:bg-focus-surface border border-separator rounded-xs p-1 px-2';
22
+
23
+ /**
24
+ * Form field that edits a markdown value in a CodeMirror editor.
25
+ * Supports two backings:
26
+ * - `Schema.String` annotated with `Format.TypeFormat.Markdown`: edited as plain text via `getValue`/`onValueChange`.
27
+ * - `Ref<Text>` annotated with `Format.TypeFormat.Markdown`: edited in-place via an Automerge doc accessor.
28
+ * When the ref is empty and a database is available, a create button is shown to allocate a new Text.
29
+ */
11
30
  export const MarkdownField = ({
12
31
  type,
13
32
  readonly,
14
33
  label,
34
+ jsonPath,
15
35
  placeholder,
16
36
  layout,
37
+ db,
17
38
  getStatus,
18
39
  getValue,
19
40
  onValueChange,
20
- onBlur,
21
41
  }: FormFieldComponentProps) => {
22
42
  const { status, error } = getStatus();
23
- const textareaRef = useRef<HTMLTextAreaElement>(null);
43
+ const isRef = Ref.isRefType(type);
44
+ const value = getValue();
45
+
46
+ if ((readonly || layout === 'static') && value == null) {
47
+ return null;
48
+ }
49
+
50
+ const renderEditor = () => {
51
+ if (isRef) {
52
+ const reference = value as Ref.Ref<any> | undefined;
53
+ if (reference) {
54
+ return <RefMarkdownEditor reference={reference} placeholder={placeholder} />;
55
+ }
24
56
 
25
- // TODO(ZaymonFC): If we start using `Enter` for form submission, we should prevent default behavior here
26
- // since we're using a textarea element and the user needs to be able to enter new lines.
57
+ if (readonly) {
58
+ return null;
59
+ }
27
60
 
28
- const adjustHeight = useCallback(() => {
29
- const textarea = textareaRef.current;
30
- if (textarea) {
31
- textarea.style.blockSize = 'auto'; // For measurement.
32
- textarea.style.blockSize = `${textarea.scrollHeight + 2}px`;
61
+ return <CreateTextButton db={db} disabled={!db} onCreate={(ref) => onValueChange(type, ref)} />;
33
62
  }
34
- }, []);
35
63
 
36
- // Adjust height on initial render.
37
- useEffect(() => {
38
- adjustHeight();
39
- }, [adjustHeight]);
64
+ return (
65
+ <StringMarkdownEditor
66
+ value={(value as string | undefined) ?? ''}
67
+ placeholder={placeholder}
68
+ readonly={!!readonly}
69
+ onChange={(next) => onValueChange(type, next)}
70
+ />
71
+ );
72
+ };
40
73
 
41
- // Adjust height when the content changes.
42
- useEffect(() => {
43
- adjustHeight();
44
- }, [getValue(), adjustHeight]);
74
+ const renderStatic = () => {
75
+ if (isRef) {
76
+ const reference = value as Ref.Ref<any> | undefined;
77
+ return reference ? <RefStaticText reference={reference} /> : null;
78
+ }
79
+ return <p className='whitespace-pre-wrap'>{(value as string | undefined) ?? ''}</p>;
80
+ };
45
81
 
46
- const handleChange = useCallback<NonNullable<TextAreaProps['onChange']>>(
47
- (event) => onValueChange(type, event.target.value),
48
- [type, onValueChange],
82
+ return (
83
+ <Input.Root validationValence={status}>
84
+ {layout !== 'inline' && <FormFieldLabel error={error} readonly={readonly} label={label} path={jsonPath} />}
85
+ {layout === 'static' ? renderStatic() : renderEditor()}
86
+ {layout === 'full' && <Input.Validation>{error}</Input.Validation>}
87
+ </Input.Root>
49
88
  );
89
+ };
50
90
 
51
- const value = getValue();
52
- if ((readonly || layout === 'static') && !value) {
91
+ /** Read-only static rendering for a `Ref<Text>` value: resolve the ref and print its content. */
92
+ const RefStaticText = ({ reference }: { reference: Ref.Ref<any> }) => {
93
+ const text = useAtomValue(useMemo(() => AtomRef.make(reference), [reference]));
94
+ if (!text?.content) {
95
+ return null;
96
+ }
97
+ return <p className='whitespace-pre-wrap'>{text.content}</p>;
98
+ };
99
+
100
+ type RefMarkdownEditorProps = {
101
+ reference: Ref.Ref<any>;
102
+ placeholder?: string;
103
+ };
104
+
105
+ const RefMarkdownEditor = ({ reference, placeholder }: RefMarkdownEditorProps) => {
106
+ const text = useAtomValue(useMemo(() => AtomRef.make(reference), [reference]));
107
+ const dataExtensions = useMemo(
108
+ () => (text ? [createDataExtensions({ id: reference.uri, text: createDocAccessor(text, ['content']) })] : []),
109
+ [text, reference],
110
+ );
111
+ const extensions = useBasicMarkdownExtensions({ placeholder, extensions: dataExtensions });
112
+ if (!text) {
53
113
  return null;
54
114
  }
55
115
 
56
- // TODO(burdon): Use Markdown Editor.
57
116
  return (
58
- <Input.Root validationValence={status}>
59
- {layout !== 'inline' && <FormFieldLabel error={error} readonly={readonly} label={label} />}
60
- {layout === 'static' ? (
61
- <p>{value}</p>
62
- ) : (
63
- <Input.TextArea
64
- ref={textareaRef}
65
- disabled={!!readonly}
66
- placeholder={placeholder}
67
- value={value ?? ''}
68
- classNames={'min-h-auto max-h-40 overflow-auto'}
69
- onChange={handleChange}
70
- onBlur={onBlur}
71
- style={{ resize: 'none' }}
72
- spellCheck={false}
73
- />
74
- )}
75
- {layout === 'full' && <Input.Validation>{error}</Input.Validation>}
76
- </Input.Root>
117
+ <Editor.Root>
118
+ <Editor.View classNames={editorClassNames} extensions={extensions} />
119
+ </Editor.Root>
120
+ );
121
+ };
122
+
123
+ type StringMarkdownEditorProps = {
124
+ value: string;
125
+ placeholder?: string;
126
+ readonly?: boolean;
127
+ onChange: (value: string) => void;
128
+ };
129
+
130
+ const StringMarkdownEditor = ({ value, placeholder, readonly, onChange }: StringMarkdownEditorProps) => {
131
+ const extensions = useBasicMarkdownExtensions({ placeholder });
132
+ const handleChange = useCallback((next: string) => onChange(next), [onChange]);
133
+
134
+ return (
135
+ <Editor.Root>
136
+ <Editor.View
137
+ classNames={editorClassNames}
138
+ extensions={extensions}
139
+ value={value}
140
+ onChange={readonly ? undefined : handleChange}
141
+ />
142
+ </Editor.Root>
143
+ );
144
+ };
145
+
146
+ type CreateTextButtonProps = {
147
+ db?: Database.Database;
148
+ disabled?: boolean;
149
+ onCreate: (ref: Ref.Ref<Text.Text>) => void;
150
+ };
151
+
152
+ const CreateTextButton = ({ db, disabled, onCreate }: CreateTextButtonProps) => {
153
+ const { t } = useTranslation(translationKey);
154
+ const handleClick = useCallback(() => {
155
+ if (!db) {
156
+ return;
157
+ }
158
+ const text = db.add(Obj.make(Text.Text, { content: '' }));
159
+ onCreate(Ref.make(text));
160
+ }, [db, onCreate]);
161
+
162
+ return (
163
+ <Button onClick={handleClick} disabled={disabled}>
164
+ <Icon icon='ph--plus--regular' size={4} />
165
+ <span>{t('create-text.label')}</span>
166
+ </Button>
77
167
  );
78
168
  };
@@ -2,23 +2,22 @@
2
2
  // Copyright 2025 DXOS.org
3
3
  //
4
4
 
5
- import '@dxos/lit-ui/dx-tag-picker.pcss';
6
-
7
- import React, { useCallback, useMemo } from 'react';
5
+ import React, { useCallback, useMemo, useState } from 'react';
8
6
 
7
+ import '@dxos/lit-ui/dx-tag-picker.pcss';
9
8
  import { type Database, Entity, Filter, Obj, Ref, Type } from '@dxos/echo';
10
- import { ReferenceAnnotationId, type ReferenceAnnotationValue } from '@dxos/echo/internal';
11
- import { useQuery, useSchema as useSchema$ } from '@dxos/echo-react';
9
+ import { useQuery, useType as defaultUseType } from '@dxos/echo-react';
10
+ import { ANY_OBJECT_TYPENAME, ReferenceAnnotationId, type ReferenceAnnotationValue } from '@dxos/echo/internal';
12
11
  import { findAnnotation } from '@dxos/effect';
13
- import { DXN } from '@dxos/keys';
12
+ import { URI } from '@dxos/keys';
14
13
  import { DxAnchor } from '@dxos/lit-ui/react';
15
14
  import { Button, Icon, Input, useTranslation } from '@dxos/react-ui';
16
15
  import { ParentLabelAnnotationId } from '@dxos/schema';
17
- import { mx } from '@dxos/ui-theme';
18
16
 
19
- import { translationKey } from '../../../translations';
17
+ import { translationKey } from '#translations';
18
+
20
19
  import { ObjectPicker, type ObjectPickerContentProps, type RefOption } from '../../ObjectPicker';
21
- import { omitId } from '../Form';
20
+ import { omitHiddenFormFields, omitId } from '../Form';
22
21
  import { type FormFieldComponentProps, FormFieldLabel } from '../FormFieldComponent';
23
22
 
24
23
  // TODO(burdon): Factor out.
@@ -28,30 +27,40 @@ const isRefSnapshot = (val: any): val is { '/': string } => {
28
27
 
29
28
  const defaultGetOptions: NonNullable<RefFieldProps['getOptions']> = (results, { parentLabel } = {}) =>
30
29
  results.map((result) => {
31
- const id = Entity.getDXN(result).toString();
30
+ const id = Entity.getURI(result);
32
31
  const parent = parentLabel ? Obj.getParent(result as Obj.Unknown) : undefined;
33
32
  const label = parent ? Entity.getLabel(parent) : Entity.getLabel(result);
34
33
  return { id, label: label ?? id };
35
34
  });
36
35
 
37
- const defaultResultsHook: NonNullable<RefFieldProps['resultsHook']> = (db, typename) =>
36
+ const defaultUseResults: NonNullable<RefFieldProps['useResults']> = (db, typename) =>
38
37
  useQuery(
39
38
  db,
40
39
  typename
41
40
  ? // For Ref.Ref(Obj.Unknown) we want to show all objects.
42
- typename === Type.getTypename(Obj.Unknown)
41
+ typename === ANY_OBJECT_TYPENAME
43
42
  ? Filter.everything()
44
43
  : Filter.typename(typename)
45
44
  : Filter.nothing(),
46
45
  );
47
46
 
48
47
  export type RefFieldProps = FormFieldComponentProps &
49
- Pick<ObjectPickerContentProps, 'createOptionLabel' | 'createOptionIcon' | 'createInitialValuePath'> & {
48
+ Pick<
49
+ ObjectPickerContentProps,
50
+ 'createOptionLabel' | 'createOptionIcon' | 'createInitialValuePath' | 'createFieldMap'
51
+ > & {
50
52
  db?: Database.Database;
51
- resultsHook?: (db?: Database.Database, typename?: string) => Entity.Any[];
52
- schemaHook?: (db?: Database.Database, typename?: string) => Type.AnyEntity;
53
+ // TODO(burdon): Replace hooks with callbacks.
54
+ useType?: (db?: Database.Database, typename?: string) => Type.AnyEntity;
55
+ useResults?: (db?: Database.Database, typename?: string) => Entity.Any[];
53
56
  getOptions?: (objects: Entity.Any[], options?: { parentLabel?: boolean }) => RefOption[];
54
- onCreate?: (schema: Type.AnyEntity, values: any) => void;
57
+ /**
58
+ * Persist a newly-created object. Called after the user fills out the
59
+ * inline create form and clicks Save. Should add the object to the
60
+ * database and return it (sync or async). The returned object is then
61
+ * wired into this slot's form value as a Ref.
62
+ */
63
+ onCreate?: (schema: Type.AnyEntity, values: any) => Obj.Unknown | Promise<Obj.Unknown> | undefined | void;
55
64
  };
56
65
 
57
66
  export const RefField = (props: RefFieldProps) => {
@@ -59,6 +68,7 @@ export const RefField = (props: RefFieldProps) => {
59
68
  type,
60
69
  readonly,
61
70
  label,
71
+ jsonPath,
62
72
  placeholder,
63
73
  layout,
64
74
  getStatus,
@@ -66,9 +76,10 @@ export const RefField = (props: RefFieldProps) => {
66
76
  createOptionLabel,
67
77
  createOptionIcon,
68
78
  createInitialValuePath,
79
+ createFieldMap,
69
80
  db,
70
- resultsHook: useResults = defaultResultsHook,
71
- schemaHook: useSchema = useSchema$,
81
+ useType = defaultUseType,
82
+ useResults = defaultUseResults,
72
83
  getOptions = defaultGetOptions,
73
84
  onCreate,
74
85
  onValueChange,
@@ -89,42 +100,66 @@ export const RefField = (props: RefFieldProps) => {
89
100
 
90
101
  const handleGetValue = useCallback(() => {
91
102
  const formValue = getValue();
103
+ // Match form-value Refs against options by the bare entity id, not by full
104
+ // URI string. A just-created object's Ref (`Ref.make`) carries the LOCAL
105
+ // EID `echo:/<id>`, while options derived from `Entity.getURI` carry the
106
+ // qualified EID `echo://<spaceId>/<id>`; encoded snapshots use the DXN
107
+ // form `dxn:echo:<space|@>:<id>`. The entity id (a ULID) contains neither
108
+ // `:` nor `/`, so it is always the final delimiter-separated segment —
109
+ // splitting on both reconciles all three forms. Comparing full strings (or
110
+ // splitting on `:` alone, which leaves `/`-delimited EIDs intact) makes the
111
+ // just-created Ref's lookup fail and the slot render empty even though the
112
+ // underlying form value IS set.
113
+ const dxnToEntityId = (dxn: string): string => dxn.split(/[:/]/).filter(Boolean).pop() ?? dxn;
92
114
 
93
115
  const unknownToRefOption = (value: unknown) => {
94
116
  const isRef = Ref.isRef(value);
95
117
  if (isRef || isRefSnapshot(value)) {
96
- const dxnString = isRef ? value.dxn.toString() : value['/'];
97
- const matchingOption = options.find((option) => option.id === dxnString);
118
+ const dxnString = isRef ? value.uri : value['/'];
119
+ const objectId = dxnToEntityId(dxnString);
120
+ const matchingOption = options.find((option) => dxnToEntityId(option.id) === objectId);
98
121
  if (matchingOption) {
99
122
  return matchingOption;
100
123
  }
101
124
  }
125
+
102
126
  return undefined;
103
127
  };
104
128
 
105
129
  return unknownToRefOption(formValue);
106
130
  }, [options, getValue]);
107
131
 
108
- const handleUpdate = useCallback(
109
- (id: string | undefined) => {
110
- const item = options.find((option) => option.id === id);
111
- const ref = item ? Ref.fromDXN(DXN.parse(item.id)) : undefined;
112
- onValueChange(type, ref);
113
- },
114
- [options, type, onValueChange],
115
- );
116
-
117
132
  const item = handleGetValue();
118
133
  const selectedIds = useMemo(() => (item ? [item.id] : []), [item]);
119
- const createSchema = useSchema(db, typename);
134
+ const createSchema = useType(db, typename);
135
+
136
+ // Lift the popover open state so we can dismiss it after a successful create.
137
+ const [open, setOpen] = useState(false);
120
138
 
121
139
  const handleCreate = useCallback(
122
- (values: any) => {
123
- if (createSchema && onCreate) {
124
- onCreate(createSchema, values);
140
+ async (values: any) => {
141
+ if (!createSchema || !onCreate) {
142
+ return;
143
+ }
144
+ const newObject = await onCreate(createSchema, values);
145
+ if (newObject) {
146
+ // Wire the newly-created object into this slot's form value. ArrayField
147
+ // owns the array; this RefField represents a single slot, so writing a
148
+ // Ref via `onValueChange` populates that slot.
149
+ onValueChange(type, Ref.make(newObject));
125
150
  }
151
+ setOpen(false);
126
152
  },
127
- [createSchema, onCreate],
153
+ [createSchema, onCreate, type, onValueChange],
154
+ );
155
+
156
+ const handleUpdate = useCallback(
157
+ (id: string | undefined) => {
158
+ const item = options.find((option) => option.id === id);
159
+ const ref = item ? Ref.fromURI(URI.make(item.id)) : undefined;
160
+ onValueChange(type, ref);
161
+ },
162
+ [options, type, onValueChange],
128
163
  );
129
164
 
130
165
  const handleSelect = useCallback(
@@ -144,29 +179,31 @@ export const RefField = (props: RefFieldProps) => {
144
179
 
145
180
  return (
146
181
  <Input.Root validationValence={status}>
147
- {layout !== 'inline' && <FormFieldLabel error={error} readonly={readonly} label={label} />}
182
+ {layout !== 'inline' && <FormFieldLabel error={error} readonly={readonly} label={label} path={jsonPath} />}
148
183
  <div>
149
184
  {readonly ? (
150
185
  !item ? (
151
- <p className={mx('text-description', 'mb-2')}>{t('empty readonly ref field label')}</p>
186
+ <p className='text-description mb-2'>{t('empty-readonly-ref-field.label')}</p>
152
187
  ) : (
153
188
  <DxAnchor key={item.id} dxn={item.id} rootclassname='me-1'>
154
189
  {item.label}
155
190
  </DxAnchor>
156
191
  )
157
192
  ) : (
158
- <ObjectPicker.Root>
193
+ <ObjectPicker.Root open={open} onOpenChange={setOpen}>
159
194
  <ObjectPicker.Trigger asChild classNames='p-0'>
160
195
  {item ? (
161
- <div className='flex gap-2 w-full'>
196
+ <div className='flex gap-form-gap w-full'>
162
197
  <Input.Root key={item.id}>
163
198
  <Input.TextInput value={item.label} readOnly classNames='w-full' />
164
199
  </Input.Root>
165
200
  </div>
166
201
  ) : (
167
- <Button classNames='w-full text-start gap-2'>
168
- <div role='none' className='grow overflow-hidden'>
169
- <span className='flex truncate text-description'>{placeholder ?? t('ref field placeholder')}</span>
202
+ <Button classNames='w-full text-start gap-form-gap'>
203
+ <div className='grow overflow-hidden'>
204
+ <span className='truncate text-description'>
205
+ {placeholder || label || t('ref-field.placeholder')}
206
+ </span>
170
207
  </div>
171
208
  <Icon size={3} icon='ph--caret-down--bold' />
172
209
  </Button>
@@ -177,10 +214,14 @@ export const RefField = (props: RefFieldProps) => {
177
214
  classNames='dx-card-popover-width'
178
215
  options={options}
179
216
  selectedIds={selectedIds}
180
- createSchema={createSchema && omitId(createSchema)}
217
+ // Strip hidden (`FormInputAnnotation.set(false)`) fields so the
218
+ // form's validator doesn't reject required-but-hidden fields
219
+ // such as backing-object refs supplied by a `FactoryAnnotation`.
220
+ createSchema={createSchema && omitHiddenFormFields(omitId(Type.getSchema(createSchema)))}
181
221
  createOptionLabel={createOptionLabel}
182
222
  createOptionIcon={createOptionIcon}
183
223
  createInitialValuePath={createInitialValuePath}
224
+ createFieldMap={createFieldMap}
184
225
  onCreate={handleCreate}
185
226
  onSelect={handleSelect}
186
227
  />
@@ -4,18 +4,20 @@
4
4
 
5
5
  import React, { useCallback } from 'react';
6
6
 
7
- import { Input, Select, type SelectRootProps } from '@dxos/react-ui';
7
+ import { Icon, Input, Select, type SelectRootProps } from '@dxos/react-ui';
8
+ import { getStyles } from '@dxos/ui-theme';
8
9
 
9
10
  import { type FormFieldComponentProps, FormFieldLabel } from '../FormFieldComponent';
10
11
 
11
12
  export type SelectFieldOptions = FormFieldComponentProps & {
12
- options?: Array<{ value: string | number; label?: string }>;
13
+ options?: Array<{ value: string | number; label?: string; secondaryLabel?: string; icon?: string; iconHue?: string }>;
13
14
  };
14
15
 
15
16
  export const SelectField = ({
16
17
  type,
17
18
  readonly,
18
19
  label,
20
+ jsonPath,
19
21
  layout,
20
22
  placeholder,
21
23
  options,
@@ -37,7 +39,7 @@ export const SelectField = ({
37
39
 
38
40
  return (
39
41
  <Input.Root validationValence={status}>
40
- {layout !== 'inline' && <FormFieldLabel error={error} readonly={readonly} label={label} />}
42
+ {layout !== 'inline' && <FormFieldLabel error={error} readonly={readonly} label={label} path={jsonPath} />}
41
43
  {layout === 'static' ? (
42
44
  <p>{options?.find(({ value: optionValue }) => optionValue === value)?.label ?? String(value)}</p>
43
45
  ) : (
@@ -47,10 +49,14 @@ export const SelectField = ({
47
49
  <Select.Portal>
48
50
  <Select.Content>
49
51
  <Select.Viewport>
50
- {options?.map(({ value, label }) => (
52
+ {options?.map(({ value, label, secondaryLabel, icon, iconHue }) => (
51
53
  // NOTE: Numeric values are converted to and from strings.
52
54
  <Select.Option key={String(value)} value={String(value)}>
53
- {label ?? String(value)}
55
+ <span className='flex items-center flex-row gap-2'>
56
+ {icon && <Icon icon={icon} classNames={getIconHueStyles(iconHue)} />}
57
+ {label ?? String(value)}
58
+ {secondaryLabel && <span className='text-subdued text-xs'>{secondaryLabel}</span>}
59
+ </span>
54
60
  </Select.Option>
55
61
  ))}
56
62
  </Select.Viewport>
@@ -63,3 +69,9 @@ export const SelectField = ({
63
69
  </Input.Root>
64
70
  );
65
71
  };
72
+
73
+ const getIconHueStyles = (iconHue?: string): string | undefined => {
74
+ const styles = iconHue ? getStyles(iconHue) : undefined;
75
+
76
+ return styles?.fg;
77
+ };
@@ -9,10 +9,10 @@ import React, { useState } from 'react';
9
9
  import { SelectOption } from '@dxos/echo/internal';
10
10
  import { withLayout, withTheme } from '@dxos/react-ui/testing';
11
11
 
12
- import { translations } from '../../../translations';
12
+ import { translations } from '#translations';
13
+
13
14
  import { TestLayout } from '../../testing';
14
15
  import { Form } from '../Form';
15
-
16
16
  import { SelectOptionField } from './SelectOptionField';
17
17
 
18
18
  const schema = Schema.Struct({
@@ -37,10 +37,12 @@ const DefaultStory = () => {
37
37
  fieldMap={{ options: SelectOptionField }}
38
38
  onSave={(values) => setValues(values)}
39
39
  >
40
- <Form.Content>
41
- <Form.FieldSet />
42
- <Form.Actions />
43
- </Form.Content>
40
+ <Form.Viewport>
41
+ <Form.Content>
42
+ <Form.FieldSet />
43
+ <Form.Actions />
44
+ </Form.Content>
45
+ </Form.Viewport>
44
46
  </Form.Root>
45
47
  </TestLayout>
46
48
  );
@@ -9,15 +9,17 @@ import { PublicKey } from '@dxos/keys';
9
9
  import { type ChromaticPalette, IconButton, Input, Tag, useTranslation } from '@dxos/react-ui';
10
10
  import { List } from '@dxos/react-ui-list';
11
11
  import { HuePicker } from '@dxos/react-ui-pickers';
12
- import { hues, osTranslations, subtleHover } from '@dxos/ui-theme';
12
+ import { hues, osTranslations } from '@dxos/ui-theme';
13
+
14
+ import { translationKey } from '#translations';
13
15
 
14
- import { translationKey } from '../../../translations';
15
16
  import { type FormFieldComponentProps, FormFieldLabel } from '../FormFieldComponent';
16
17
 
17
18
  export const SelectOptionField = ({
18
19
  type,
19
20
  readonly,
20
21
  label,
22
+ jsonPath,
21
23
  getStatus,
22
24
  getValue,
23
25
  onValueChange,
@@ -118,8 +120,8 @@ export const SelectOptionField = ({
118
120
 
119
121
  return (
120
122
  <Input.Root validationValence={status}>
121
- <FormFieldLabel error={error} readonly={readonly} label={label} />
122
- <div role='none'>
123
+ <FormFieldLabel error={error} readonly={readonly} label={label} path={jsonPath} />
124
+ <div>
123
125
  {options && (
124
126
  <List.Root
125
127
  items={options}
@@ -134,7 +136,7 @@ export const SelectOptionField = ({
134
136
  <List.Item
135
137
  role='button'
136
138
  item={item}
137
- classNames={[subtleHover, 'flex flex-col cursor-pointer rounded-xs']}
139
+ classNames='flex flex-col cursor-pointer rounded-xs dx-hover'
138
140
  aria-expanded={selected === item.id}
139
141
  >
140
142
  <div className='flex items-center'>
@@ -146,19 +148,18 @@ export const SelectOptionField = ({
146
148
  <IconButton
147
149
  iconOnly
148
150
  variant='ghost'
149
- label={t(selected === item.id ? 'collapse label' : 'expand label', { ns: osTranslations })}
151
+ label={t(selected === item.id ? 'collapse.label' : 'expand.label', { ns: osTranslations })}
150
152
  icon={selected === item.id ? 'ph--caret-down--regular' : 'ph--caret-right--regular'}
151
153
  onClick={() => handleClick(item.id)}
152
154
  />
153
155
  </div>
154
156
  {selected === item.id && (
155
- <div className='flex flex-col p-form-padding gap-1 dx-density-fine'>
156
- {/* 16px to match drag handle width. */}
157
- <Input.Label classNames='text-xs'>{t('select option label')}</Input.Label>
158
- <div className='grid grid-cols-[1fr_min-content_min-content] gap-1'>
157
+ <div className='flex flex-col p-form-padding gap-form-gap dx-density-md'>
158
+ <Input.Label classNames='text-sm'>{t('select-option.label')}</Input.Label>
159
+ <div className='grid grid-cols-[1fr_min-content_min-content] gap-form-gap'>
159
160
  <Input.TextInput
160
161
  disabled={!!readonly}
161
- placeholder={t('select option label placeholder')}
162
+ placeholder={t('select-option-label.placeholder')}
162
163
  ref={selected === item.id ? inputRef : undefined}
163
164
  value={item.title}
164
165
  onChange={handleTitleChange(item.id)}
@@ -170,7 +171,7 @@ export const SelectOptionField = ({
170
171
  disabled={!!readonly}
171
172
  icon='ph--trash--fill'
172
173
  iconOnly
173
- label={t('select option delete')}
174
+ label={t('select-option-delete.button')}
174
175
  onClick={() => handleDelete(item.id)}
175
176
  />
176
177
  </div>
@@ -182,7 +183,7 @@ export const SelectOptionField = ({
182
183
  <IconButton
183
184
  variant='ghost'
184
185
  icon='ph--plus--regular'
185
- label={t('select option add')}
186
+ label={t('select-option-add.button')}
186
187
  onClick={handleAdd}
187
188
  disabled={!!readonly}
188
189
  classNames='w-full'
@@ -25,7 +25,7 @@ export const TextAreaField = ({
25
25
  <FormFieldWrapper<string> readonly={readonly} {...props}>
26
26
  {({ value = '' }) => (
27
27
  <Input.TextArea
28
- rows={3}
28
+ rows={5}
29
29
  disabled={!!readonly}
30
30
  placeholder={placeholder}
31
31
  value={value}
@@ -12,8 +12,8 @@ export const TextField = ({
12
12
  type,
13
13
  readonly,
14
14
  placeholder,
15
- onValueChange,
16
15
  onBlur,
16
+ onValueChange,
17
17
  ...props
18
18
  }: FormFieldComponentProps<string>) => {
19
19
  const handleChange = useCallback<NonNullable<TextInputProps['onChange']>>(
@@ -29,8 +29,8 @@ export const TextField = ({
29
29
  disabled={!!readonly}
30
30
  placeholder={placeholder}
31
31
  value={value}
32
- onChange={handleChange}
33
32
  onBlur={onBlur}
33
+ onChange={handleChange}
34
34
  />
35
35
  )}
36
36
  </FormFieldWrapper>