@dxos/react-ui-form 0.8.4-main.ead640a → 0.8.4-main.effb148878

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 (268) hide show
  1. package/LICENSE +102 -5
  2. package/README.md +0 -1
  3. package/dist/lib/browser/index.mjs +3252 -2315
  4. package/dist/lib/browser/index.mjs.map +4 -4
  5. package/dist/lib/browser/meta.json +1 -1
  6. package/dist/lib/browser/translations.mjs +83 -0
  7. package/dist/lib/browser/translations.mjs.map +7 -0
  8. package/dist/lib/node-esm/index.mjs +3252 -2315
  9. package/dist/lib/node-esm/index.mjs.map +4 -4
  10. package/dist/lib/node-esm/meta.json +1 -1
  11. package/dist/lib/node-esm/translations.mjs +85 -0
  12. package/dist/lib/node-esm/translations.mjs.map +7 -0
  13. package/dist/types/src/components/FieldEditor/FieldEditor.d.ts +8 -7
  14. package/dist/types/src/components/FieldEditor/FieldEditor.d.ts.map +1 -1
  15. package/dist/types/src/components/FieldEditor/FieldEditor.stories.d.ts +69 -60
  16. package/dist/types/src/components/FieldEditor/FieldEditor.stories.d.ts.map +1 -1
  17. package/dist/types/src/components/Form/Form.d.ts +123 -26
  18. package/dist/types/src/components/Form/Form.d.ts.map +1 -1
  19. package/dist/types/src/components/Form/Form.stories.d.ts +175 -138
  20. package/dist/types/src/components/Form/Form.stories.d.ts.map +1 -1
  21. package/dist/types/src/components/Form/FormField.d.ts +48 -0
  22. package/dist/types/src/components/Form/FormField.d.ts.map +1 -0
  23. package/dist/types/src/components/Form/FormFieldComponent.d.ts +95 -0
  24. package/dist/types/src/components/Form/FormFieldComponent.d.ts.map +1 -0
  25. package/dist/types/src/components/Form/FormFieldComponent.stories.d.ts +9 -0
  26. package/dist/types/src/components/Form/FormFieldComponent.stories.d.ts.map +1 -0
  27. package/dist/types/src/components/Form/FormFieldSet.d.ts +21 -0
  28. package/dist/types/src/components/Form/FormFieldSet.d.ts.map +1 -0
  29. package/dist/types/src/components/Form/FormTooltipsContext.d.ts +19 -0
  30. package/dist/types/src/components/Form/FormTooltipsContext.d.ts.map +1 -0
  31. package/dist/types/src/components/Form/Layout/FormLayout.d.ts +25 -0
  32. package/dist/types/src/components/Form/Layout/FormLayout.d.ts.map +1 -0
  33. package/dist/types/src/components/Form/Layout/FormLayout.stories.d.ts +116 -0
  34. package/dist/types/src/components/Form/Layout/FormLayout.stories.d.ts.map +1 -0
  35. package/dist/types/src/components/Form/Layout/annotation.d.ts +30 -0
  36. package/dist/types/src/components/Form/Layout/annotation.d.ts.map +1 -0
  37. package/dist/types/src/components/Form/Layout/index.d.ts +4 -0
  38. package/dist/types/src/components/Form/Layout/index.d.ts.map +1 -0
  39. package/dist/types/src/components/Form/Layout/parser.d.ts +31 -0
  40. package/dist/types/src/components/Form/Layout/parser.d.ts.map +1 -0
  41. package/dist/types/src/components/Form/Layout/parser.test.d.ts +2 -0
  42. package/dist/types/src/components/Form/Layout/parser.test.d.ts.map +1 -0
  43. package/dist/types/src/components/Form/fields/ArrayField.d.ts +18 -0
  44. package/dist/types/src/components/Form/fields/ArrayField.d.ts.map +1 -0
  45. package/dist/types/src/components/Form/fields/BooleanField.d.ts +4 -0
  46. package/dist/types/src/components/Form/fields/BooleanField.d.ts.map +1 -0
  47. package/dist/types/src/components/Form/fields/DateField.d.ts +4 -0
  48. package/dist/types/src/components/Form/fields/DateField.d.ts.map +1 -0
  49. package/dist/types/src/components/Form/fields/GeoPointField.d.ts +5 -0
  50. package/dist/types/src/components/Form/fields/GeoPointField.d.ts.map +1 -0
  51. package/dist/types/src/components/Form/fields/MarkdownField.d.ts +11 -0
  52. package/dist/types/src/components/Form/fields/MarkdownField.d.ts.map +1 -0
  53. package/dist/types/src/components/Form/fields/MarkdownField.stories.d.ts +85 -0
  54. package/dist/types/src/components/Form/fields/MarkdownField.stories.d.ts.map +1 -0
  55. package/dist/types/src/components/Form/fields/NumberField.d.ts +4 -0
  56. package/dist/types/src/components/Form/fields/NumberField.d.ts.map +1 -0
  57. package/dist/types/src/components/Form/fields/RefField.d.ts +22 -0
  58. package/dist/types/src/components/Form/fields/RefField.d.ts.map +1 -0
  59. package/dist/types/src/components/Form/fields/SelectField.d.ts +13 -0
  60. package/dist/types/src/components/Form/fields/SelectField.d.ts.map +1 -0
  61. package/dist/types/src/components/Form/fields/SelectOptionField.d.ts +5 -0
  62. package/dist/types/src/components/Form/fields/SelectOptionField.d.ts.map +1 -0
  63. package/dist/types/src/components/Form/fields/SelectOptionField.stories.d.ts +85 -0
  64. package/dist/types/src/components/Form/fields/SelectOptionField.stories.d.ts.map +1 -0
  65. package/dist/types/src/components/Form/fields/TextAreaField.d.ts +4 -0
  66. package/dist/types/src/components/Form/fields/TextAreaField.d.ts.map +1 -0
  67. package/dist/types/src/components/Form/fields/TextField.d.ts +4 -0
  68. package/dist/types/src/components/Form/fields/TextField.d.ts.map +1 -0
  69. package/dist/types/src/components/Form/fields/TupleField.d.ts +6 -0
  70. package/dist/types/src/components/Form/fields/TupleField.d.ts.map +1 -0
  71. package/dist/types/src/components/Form/fields/index.d.ts +13 -0
  72. package/dist/types/src/components/Form/fields/index.d.ts.map +1 -0
  73. package/dist/types/src/components/Form/index.d.ts +4 -4
  74. package/dist/types/src/components/Form/index.d.ts.map +1 -1
  75. package/dist/types/src/components/ObjectForm/ObjectForm.d.ts +8 -0
  76. package/dist/types/src/components/ObjectForm/ObjectForm.d.ts.map +1 -0
  77. package/dist/types/src/components/ObjectForm/index.d.ts +2 -0
  78. package/dist/types/src/components/ObjectForm/index.d.ts.map +1 -0
  79. package/dist/types/src/components/ObjectPicker/ObjectPicker.d.ts +37 -10
  80. package/dist/types/src/components/ObjectPicker/ObjectPicker.d.ts.map +1 -1
  81. package/dist/types/src/components/ObjectPicker/ObjectPicker.stories.d.ts +65 -59
  82. package/dist/types/src/components/ObjectPicker/ObjectPicker.stories.d.ts.map +1 -1
  83. package/dist/types/src/components/ObjectProperties/ObjectProperties.d.ts +9 -0
  84. package/dist/types/src/components/ObjectProperties/ObjectProperties.d.ts.map +1 -0
  85. package/dist/types/src/components/ObjectProperties/ObjectProperties.stories.d.ts +128 -0
  86. package/dist/types/src/components/ObjectProperties/ObjectProperties.stories.d.ts.map +1 -0
  87. package/dist/types/src/components/ObjectProperties/ObjectProperties.test.d.ts +2 -0
  88. package/dist/types/src/components/ObjectProperties/ObjectProperties.test.d.ts.map +1 -0
  89. package/dist/types/src/components/ObjectProperties/index.d.ts +2 -0
  90. package/dist/types/src/components/ObjectProperties/index.d.ts.map +1 -0
  91. package/dist/types/src/components/ObjectTree/ObjectTree.d.ts +9 -0
  92. package/dist/types/src/components/ObjectTree/ObjectTree.d.ts.map +1 -0
  93. package/dist/types/src/components/ObjectTree/ObjectTree.stories.d.ts +87 -0
  94. package/dist/types/src/components/ObjectTree/ObjectTree.stories.d.ts.map +1 -0
  95. package/dist/types/src/components/ObjectTree/index.d.ts +2 -0
  96. package/dist/types/src/components/ObjectTree/index.d.ts.map +1 -0
  97. package/dist/types/src/components/Settings/Settings.d.ts +51 -0
  98. package/dist/types/src/components/Settings/Settings.d.ts.map +1 -0
  99. package/dist/types/src/components/Settings/Settings.stories.d.ts +85 -0
  100. package/dist/types/src/components/Settings/Settings.stories.d.ts.map +1 -0
  101. package/dist/types/src/components/Settings/SettingsFieldSet.d.ts +43 -0
  102. package/dist/types/src/components/Settings/SettingsFieldSet.d.ts.map +1 -0
  103. package/dist/types/src/components/Settings/SettingsItem.d.ts +16 -0
  104. package/dist/types/src/components/Settings/SettingsItem.d.ts.map +1 -0
  105. package/dist/types/src/components/Settings/SettingsSelectField.d.ts +18 -0
  106. package/dist/types/src/components/Settings/SettingsSelectField.d.ts.map +1 -0
  107. package/dist/types/src/components/Settings/index.d.ts +4 -0
  108. package/dist/types/src/components/Settings/index.d.ts.map +1 -0
  109. package/dist/types/src/components/ViewEditor/ViewEditor.d.ts +16 -20
  110. package/dist/types/src/components/ViewEditor/ViewEditor.d.ts.map +1 -1
  111. package/dist/types/src/components/ViewEditor/ViewEditor.stories.d.ts +72 -66
  112. package/dist/types/src/components/ViewEditor/ViewEditor.stories.d.ts.map +1 -1
  113. package/dist/types/src/components/index.d.ts +4 -1
  114. package/dist/types/src/components/index.d.ts.map +1 -1
  115. package/dist/types/src/components/testing.d.ts +13 -6
  116. package/dist/types/src/components/testing.d.ts.map +1 -1
  117. package/dist/types/src/hooks/index.d.ts +2 -3
  118. package/dist/types/src/hooks/index.d.ts.map +1 -1
  119. package/dist/types/src/hooks/useFormHandler.d.ts +82 -0
  120. package/dist/types/src/hooks/useFormHandler.d.ts.map +1 -0
  121. package/dist/types/src/hooks/useKeyHandler.d.ts +7 -0
  122. package/dist/types/src/hooks/useKeyHandler.d.ts.map +1 -0
  123. package/dist/types/src/index.d.ts +0 -2
  124. package/dist/types/src/index.d.ts.map +1 -1
  125. package/dist/types/src/translations.d.ts +66 -60
  126. package/dist/types/src/translations.d.ts.map +1 -1
  127. package/dist/types/src/util/field-type.d.ts +19 -0
  128. package/dist/types/src/util/field-type.d.ts.map +1 -0
  129. package/dist/types/src/util/field.d.ts +3 -3
  130. package/dist/types/src/util/field.d.ts.map +1 -1
  131. package/dist/types/src/util/formatting.d.ts +2 -2
  132. package/dist/types/src/util/formatting.d.ts.map +1 -1
  133. package/dist/types/src/util/index.d.ts +2 -1
  134. package/dist/types/src/util/index.d.ts.map +1 -1
  135. package/dist/types/src/util/properties.d.ts +17 -0
  136. package/dist/types/src/util/properties.d.ts.map +1 -0
  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 +2 -2
  140. package/dist/types/src/util/refs.d.ts.map +1 -1
  141. package/dist/types/tsconfig.tsbuildinfo +1 -1
  142. package/package.json +61 -51
  143. package/src/components/FieldEditor/FieldEditor.stories.tsx +44 -25
  144. package/src/components/FieldEditor/FieldEditor.test.tsx +0 -1
  145. package/src/components/FieldEditor/FieldEditor.tsx +84 -84
  146. package/src/components/Form/AUDIT.md +17 -0
  147. package/src/components/Form/Form.stories.tsx +134 -252
  148. package/src/components/Form/Form.tsx +417 -107
  149. package/src/components/Form/FormField.tsx +332 -0
  150. package/src/components/Form/FormFieldComponent.stories.tsx +65 -0
  151. package/src/components/Form/FormFieldComponent.tsx +206 -0
  152. package/src/components/Form/FormFieldSet.tsx +173 -0
  153. package/src/components/Form/FormTooltipsContext.ts +25 -0
  154. package/src/components/Form/Layout/FormLayout.stories.tsx +339 -0
  155. package/src/components/Form/Layout/FormLayout.tsx +123 -0
  156. package/src/components/Form/Layout/annotation.ts +38 -0
  157. package/src/components/Form/Layout/index.ts +7 -0
  158. package/src/components/Form/Layout/parser.test.ts +120 -0
  159. package/src/components/Form/Layout/parser.ts +167 -0
  160. package/src/components/Form/README.md +3 -0
  161. package/src/components/Form/fields/ArrayField.tsx +185 -0
  162. package/src/components/Form/fields/BooleanField.tsx +22 -0
  163. package/src/components/Form/fields/DateField.tsx +95 -0
  164. package/src/components/Form/fields/GeoPointField.tsx +124 -0
  165. package/src/components/Form/fields/MarkdownField.stories.tsx +206 -0
  166. package/src/components/Form/fields/MarkdownField.tsx +168 -0
  167. package/src/components/Form/fields/NumberField.tsx +39 -0
  168. package/src/components/Form/fields/RefField.tsx +232 -0
  169. package/src/components/Form/fields/SelectField.tsx +77 -0
  170. package/src/components/Form/{custom/SelectOptionsInput.stories.tsx → fields/SelectOptionField.stories.tsx} +23 -18
  171. package/src/components/Form/{custom/SelectOptionsInput.tsx → fields/SelectOptionField.tsx} +31 -24
  172. package/src/components/Form/fields/TextAreaField.tsx +38 -0
  173. package/src/components/Form/fields/TextField.tsx +38 -0
  174. package/src/components/Form/{custom/TupleInput.tsx → fields/TupleField.tsx} +17 -9
  175. package/src/components/Form/fields/index.ts +16 -0
  176. package/src/components/Form/index.ts +5 -4
  177. package/src/components/ObjectForm/ObjectForm.tsx +115 -0
  178. package/src/components/{ControlSection → ObjectForm}/index.ts +1 -1
  179. package/src/components/ObjectPicker/ObjectPicker.stories.tsx +30 -25
  180. package/src/components/ObjectPicker/ObjectPicker.tsx +122 -81
  181. package/src/components/ObjectProperties/ObjectProperties.stories.tsx +350 -0
  182. package/src/components/ObjectProperties/ObjectProperties.test.tsx +108 -0
  183. package/src/components/ObjectProperties/ObjectProperties.tsx +147 -0
  184. package/src/components/ObjectProperties/index.ts +5 -0
  185. package/src/components/ObjectTree/ObjectTree.stories.tsx +134 -0
  186. package/src/components/ObjectTree/ObjectTree.tsx +190 -0
  187. package/src/components/ObjectTree/index.ts +5 -0
  188. package/src/components/Settings/AUDIT.md +51 -0
  189. package/src/components/Settings/Settings.stories.tsx +104 -0
  190. package/src/components/Settings/Settings.tsx +81 -0
  191. package/src/components/Settings/SettingsFieldSet.tsx +257 -0
  192. package/src/components/Settings/SettingsItem.tsx +58 -0
  193. package/src/components/Settings/SettingsSelectField.tsx +59 -0
  194. package/src/components/Settings/index.ts +7 -0
  195. package/src/components/ViewEditor/ViewEditor.stories.tsx +65 -73
  196. package/src/components/ViewEditor/ViewEditor.test.tsx +11 -11
  197. package/src/components/ViewEditor/ViewEditor.tsx +308 -214
  198. package/src/components/index.ts +4 -1
  199. package/src/components/testing.tsx +24 -11
  200. package/src/hooks/index.ts +2 -3
  201. package/src/hooks/useFormHandler.ts +380 -0
  202. package/src/hooks/useKeyHandler.tsx +37 -0
  203. package/src/index.ts +0 -4
  204. package/src/translations.ts +71 -65
  205. package/src/util/field-type.ts +60 -0
  206. package/src/util/field.ts +25 -25
  207. package/src/util/formatting.ts +22 -22
  208. package/src/util/index.ts +2 -1
  209. package/src/util/properties.test.ts +78 -0
  210. package/src/util/properties.ts +38 -0
  211. package/src/util/refs.ts +16 -18
  212. package/src/vite-env.d.ts +10 -0
  213. package/dist/types/src/components/ControlSection/ControlSection.d.ts +0 -23
  214. package/dist/types/src/components/ControlSection/ControlSection.d.ts.map +0 -1
  215. package/dist/types/src/components/ControlSection/ControlSection.stories.d.ts +0 -84
  216. package/dist/types/src/components/ControlSection/ControlSection.stories.d.ts.map +0 -1
  217. package/dist/types/src/components/ControlSection/index.d.ts +0 -2
  218. package/dist/types/src/components/ControlSection/index.d.ts.map +0 -1
  219. package/dist/types/src/components/Form/ArrayField.d.ts +0 -16
  220. package/dist/types/src/components/Form/ArrayField.d.ts.map +0 -1
  221. package/dist/types/src/components/Form/Defaults.d.ts +0 -14
  222. package/dist/types/src/components/Form/Defaults.d.ts.map +0 -1
  223. package/dist/types/src/components/Form/FormActions.d.ts +0 -9
  224. package/dist/types/src/components/Form/FormActions.d.ts.map +0 -1
  225. package/dist/types/src/components/Form/FormContent.d.ts +0 -63
  226. package/dist/types/src/components/Form/FormContent.d.ts.map +0 -1
  227. package/dist/types/src/components/Form/FormContext.d.ts +0 -23
  228. package/dist/types/src/components/Form/FormContext.d.ts.map +0 -1
  229. package/dist/types/src/components/Form/Input.d.ts +0 -29
  230. package/dist/types/src/components/Form/Input.d.ts.map +0 -1
  231. package/dist/types/src/components/Form/RefField.d.ts +0 -22
  232. package/dist/types/src/components/Form/RefField.d.ts.map +0 -1
  233. package/dist/types/src/components/Form/custom/GeoPointInput.d.ts +0 -4
  234. package/dist/types/src/components/Form/custom/GeoPointInput.d.ts.map +0 -1
  235. package/dist/types/src/components/Form/custom/SelectOptionsInput.d.ts +0 -4
  236. package/dist/types/src/components/Form/custom/SelectOptionsInput.d.ts.map +0 -1
  237. package/dist/types/src/components/Form/custom/SelectOptionsInput.stories.d.ts +0 -79
  238. package/dist/types/src/components/Form/custom/SelectOptionsInput.stories.d.ts.map +0 -1
  239. package/dist/types/src/components/Form/custom/TupleInput.d.ts +0 -6
  240. package/dist/types/src/components/Form/custom/TupleInput.d.ts.map +0 -1
  241. package/dist/types/src/components/Form/custom/index.d.ts +0 -4
  242. package/dist/types/src/components/Form/custom/index.d.ts.map +0 -1
  243. package/dist/types/src/components/Form/factory.d.ts +0 -8
  244. package/dist/types/src/components/Form/factory.d.ts.map +0 -1
  245. package/dist/types/src/hooks/useForm.d.ts +0 -67
  246. package/dist/types/src/hooks/useForm.d.ts.map +0 -1
  247. package/dist/types/src/hooks/useQueryRefOptions.d.ts +0 -29
  248. package/dist/types/src/hooks/useQueryRefOptions.d.ts.map +0 -1
  249. package/dist/types/src/hooks/useRefQueryLookupHandler.d.ts +0 -8
  250. package/dist/types/src/hooks/useRefQueryLookupHandler.d.ts.map +0 -1
  251. package/dist/types/src/util/schema.d.ts +0 -3
  252. package/dist/types/src/util/schema.d.ts.map +0 -1
  253. package/src/components/ControlSection/ControlSection.stories.tsx +0 -38
  254. package/src/components/ControlSection/ControlSection.tsx +0 -153
  255. package/src/components/Form/ArrayField.tsx +0 -125
  256. package/src/components/Form/Defaults.tsx +0 -205
  257. package/src/components/Form/FormActions.tsx +0 -54
  258. package/src/components/Form/FormContent.tsx +0 -322
  259. package/src/components/Form/FormContext.tsx +0 -110
  260. package/src/components/Form/Input.tsx +0 -54
  261. package/src/components/Form/RefField.tsx +0 -283
  262. package/src/components/Form/custom/GeoPointInput.tsx +0 -80
  263. package/src/components/Form/custom/index.ts +0 -7
  264. package/src/components/Form/factory.tsx +0 -31
  265. package/src/hooks/useForm.ts +0 -296
  266. package/src/hooks/useQueryRefOptions.ts +0 -62
  267. package/src/hooks/useRefQueryLookupHandler.ts +0 -39
  268. package/src/util/schema.ts +0 -12
@@ -0,0 +1,115 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import * as Schema from 'effect/Schema';
6
+ import React, { useCallback, useMemo } from 'react';
7
+
8
+ import { Obj, Ref, Tag, Type } from '@dxos/echo';
9
+ import { type JsonPath, splitJsonPath } from '@dxos/echo/internal';
10
+ import { invariant } from '@dxos/invariant';
11
+ import { URI } from '@dxos/keys';
12
+ import { HuePicker } from '@dxos/react-ui-pickers';
13
+ import { isNonNullable } from '@dxos/util';
14
+
15
+ import { translationKey } from '#translations';
16
+
17
+ import { Form, type FormFieldMap, omitId } from '../Form';
18
+
19
+ export type ObjectFormProps = {
20
+ type: Type.AnyEntity;
21
+ object: Obj.Unknown;
22
+ };
23
+
24
+ const createFieldMap: FormFieldMap = {
25
+ hue: ({ type, label, layout, getValue, onValueChange }) => {
26
+ const handleChange = useCallback((nextHue: string) => onValueChange(type, nextHue), [onValueChange, type]);
27
+ const handleReset = useCallback(() => onValueChange(type, undefined), [onValueChange, type]);
28
+ return (
29
+ <>
30
+ {layout !== 'inline' && <Form.Label label={label} />}
31
+ <HuePicker value={getValue()} onChange={handleChange} onReset={handleReset} />
32
+ </>
33
+ );
34
+ },
35
+ };
36
+
37
+ export const ObjectForm = ({ object, type }: ObjectFormProps) => {
38
+ const db = Obj.getDatabase(object);
39
+ const meta = Obj.getMeta(object);
40
+ const tags = (meta.tags ?? []).map((tag) => db?.makeRef(URI.make(tag))).filter(isNonNullable);
41
+ const values = useMemo(() => ({ tags, ...object }), [object, tags]);
42
+ const formSchema = useMemo(
43
+ () =>
44
+ omitId(
45
+ Schema.Struct({
46
+ tags: Schema.Array(Ref.Ref(Tag.Tag)).pipe(Schema.optional),
47
+ }).pipe(Schema.extend(Type.getSchema(type))),
48
+ ),
49
+ [type],
50
+ );
51
+
52
+ const handleCreate = useCallback((type: Type.AnyEntity, values: any) => {
53
+ invariant(db);
54
+ invariant(Type.isObject(type));
55
+ const newObject = db.add(Obj.make(type, values));
56
+ if (Obj.instanceOf(Tag.Tag, newObject)) {
57
+ Obj.update(object, (object) => {
58
+ Obj.getMeta(object).tags = [...(Obj.getMeta(object).tags ?? []), Obj.getURI(newObject)];
59
+ });
60
+ }
61
+ }, []);
62
+
63
+ // TODO(wittjosiah): Use FormRootProps type.
64
+ const handleChange = useCallback(
65
+ ({ tags, ...values }: any, { isValid, changed }: { isValid: boolean; changed: Record<JsonPath, boolean> }) => {
66
+ if (!isValid) {
67
+ return;
68
+ }
69
+
70
+ const changedPaths = Object.keys(changed).filter((path) => changed[path as JsonPath]) as JsonPath[];
71
+
72
+ // Handle tags separately using Obj.update.
73
+ const hasTagsChange = changedPaths.some((path) => splitJsonPath(path)[0] === 'tags');
74
+ if (hasTagsChange) {
75
+ Obj.update(object, (object) => {
76
+ Obj.getMeta(object).tags = tags?.map((tag: Ref.Ref<Tag.Tag>) => tag.uri) ?? [];
77
+ });
78
+ }
79
+
80
+ // Handle other property changes.
81
+ const nonTagPaths = changedPaths.filter((path) => splitJsonPath(path)[0] !== 'tags');
82
+ if (nonTagPaths.length > 0) {
83
+ Obj.update(object, () => {
84
+ for (const path of nonTagPaths) {
85
+ const parts = splitJsonPath(path);
86
+ const value = Obj.getValue(values, parts);
87
+ Obj.setValue(object, parts, value);
88
+ }
89
+ });
90
+ }
91
+ },
92
+ [object],
93
+ );
94
+
95
+ return (
96
+ <Form.Root
97
+ schema={formSchema}
98
+ defaultValues={values}
99
+ createTypename={Type.getTypename(Tag.Tag)}
100
+ createOptionIcon='ph--plus--regular'
101
+ createOptionLabel={['add-tag.label', { ns: translationKey }]}
102
+ createInitialValuePath='label'
103
+ createFieldMap={createFieldMap}
104
+ db={db}
105
+ onValuesChanged={handleChange}
106
+ onCreate={handleCreate}
107
+ >
108
+ <Form.Viewport>
109
+ <Form.Content>
110
+ <Form.FieldSet />
111
+ </Form.Content>
112
+ </Form.Viewport>
113
+ </Form.Root>
114
+ );
115
+ };
@@ -2,4 +2,4 @@
2
2
  // Copyright 2025 DXOS.org
3
3
  //
4
4
 
5
- export * from './ControlSection';
5
+ export * from './ObjectForm';
@@ -7,34 +7,35 @@ import * as Schema from 'effect/Schema';
7
7
  import React, { useCallback, useEffect, useMemo, useState } from 'react';
8
8
  import { expect, fn, userEvent, within } from 'storybook/test';
9
9
 
10
- import { Obj } from '@dxos/echo';
11
- import { faker } from '@dxos/random';
12
- import { Filter, useQuery } from '@dxos/react-client/echo';
13
- import { useClientProvider, withClientProvider } from '@dxos/react-client/testing';
10
+ import { Filter, Obj, Type } from '@dxos/echo';
11
+ import { random } from '@dxos/random';
12
+ import { useQuery } from '@dxos/react-client/echo';
13
+ import { useClientStory, withClientProvider } from '@dxos/react-client/testing';
14
14
  import { Button } from '@dxos/react-ui';
15
15
  import { withTheme } from '@dxos/react-ui/testing';
16
- import { DataType } from '@dxos/schema';
16
+ import { Person } from '@dxos/types';
17
+ import { osTranslations } from '@dxos/ui-theme';
17
18
 
18
- import { translations } from '../../translations';
19
+ import { translations } from '#translations';
19
20
 
20
21
  import { ObjectPicker } from './ObjectPicker';
21
22
 
22
- faker.seed(1);
23
+ random.seed(1);
23
24
 
24
25
  const createPerson = () =>
25
- Obj.make(DataType.Person, {
26
- fullName: faker.person.fullName(),
26
+ Obj.make(Person.Person, {
27
+ fullName: random.person.fullName(),
27
28
  });
28
29
 
29
30
  const omitId = Schema.omit<any, any, ['id']>('id');
30
- const personSchema = omitId(DataType.Person);
31
+ const personSchema = omitId(Type.getSchema(Person.Person));
31
32
 
32
33
  // Mock functions for testing
33
34
  const mockHandleSelect = fn();
34
35
  const mockHandleCreate = fn();
35
36
 
36
37
  const DefaultStory = () => {
37
- const { space } = useClientProvider();
38
+ const { space } = useClientStory();
38
39
  const [isOpen, setIsOpen] = useState(false);
39
40
 
40
41
  // Create sample Person objects
@@ -47,8 +48,8 @@ const DefaultStory = () => {
47
48
  }
48
49
  }, [space]);
49
50
 
50
- // Get all objects in the space (similar to BoardContainer)
51
- const allObjects = useQuery(space, Filter.everything());
51
+ // Get all objects in the space (similar to BoardArticle)
52
+ const allObjects = useQuery(space?.db, Filter.everything());
52
53
 
53
54
  // Map objects to options format expected by ObjectPicker
54
55
  const options = useMemo(
@@ -64,8 +65,10 @@ const DefaultStory = () => {
64
65
  const handleCreateCallback = useCallback(
65
66
  (values: any) => {
66
67
  console.log('[on create]', values);
67
- if (!space) return;
68
- const newPerson = space.db.add(Obj.make(DataType.Person, values));
68
+ if (!space) {
69
+ return;
70
+ }
71
+ const newPerson = space.db.add(Obj.make(Person.Person, values));
69
72
  mockHandleCreate(values);
70
73
  return newPerson;
71
74
  },
@@ -73,22 +76,22 @@ const DefaultStory = () => {
73
76
  );
74
77
 
75
78
  return (
76
- <div className='p-4'>
79
+ <div className='w-96'>
77
80
  <ObjectPicker.Root open={isOpen} onOpenChange={setIsOpen}>
78
81
  <ObjectPicker.Trigger asChild>
79
- <Button variant='primary' data-testid='trigger'>
82
+ <Button variant='primary' data-testid='trigger' classNames='w-full'>
80
83
  Select Person
81
84
  </Button>
82
85
  </ObjectPicker.Trigger>
83
86
  <ObjectPicker.Content
87
+ classNames='dx-card-popover-width'
84
88
  options={options}
85
- onSelect={mockHandleSelect}
86
89
  createSchema={personSchema}
87
- createOptionLabel={['create new person label', { ns: 'os' }]}
90
+ createOptionLabel={['create-new-person.label', { ns: osTranslations }]}
88
91
  createOptionIcon='ph--user-plus--regular'
89
92
  createInitialValuePath='fullName'
90
93
  onCreate={handleCreateCallback}
91
- classNames='popover-card-width'
94
+ onSelect={mockHandleSelect}
92
95
  />
93
96
  </ObjectPicker.Root>
94
97
  </div>
@@ -99,9 +102,9 @@ const meta = {
99
102
  title: 'ui/react-ui-form/ObjectPicker',
100
103
  render: DefaultStory,
101
104
  decorators: [
102
- withTheme,
105
+ withTheme(),
103
106
  withClientProvider({
104
- types: [DataType.Person],
107
+ types: [Person.Person],
105
108
  createIdentity: true,
106
109
  createSpace: true,
107
110
  }),
@@ -200,8 +203,10 @@ export const WithTest: Story = {
200
203
  await userEvent.click(saveButton);
201
204
 
202
205
  // Check that clicking the save button calls onCreate with expected values.
203
- await expect(mockHandleCreate).toHaveBeenCalledWith({
204
- fullName: unrelatedTerm,
205
- });
206
+ await expect(mockHandleCreate).toHaveBeenCalledWith(
207
+ expect.objectContaining({
208
+ fullName: unrelatedTerm,
209
+ }),
210
+ );
206
211
  },
207
212
  };
@@ -3,24 +3,40 @@
3
3
  //
4
4
 
5
5
  import type * as Schema from 'effect/Schema';
6
- import React, { type KeyboardEvent, type MouseEvent, forwardRef, useCallback, useMemo, useState } from 'react';
6
+ import React, { type KeyboardEvent, forwardRef, useCallback, useState } from 'react';
7
7
 
8
- import { Icon, Popover, type ThemedClassName, useTranslation } from '@dxos/react-ui';
9
- import { Combobox } from '@dxos/react-ui-searchlist';
8
+ import { type Palette, Popover, type ThemedClassName, useTranslation } from '@dxos/react-ui';
9
+ import { Combobox } from '@dxos/react-ui-list';
10
+ import { useSearchListResults } from '@dxos/react-ui-search';
11
+
12
+ import { translationKey } from '#translations';
10
13
 
11
- import { type QueryTag } from '../../hooks';
12
- import { translationKey } from '../../translations';
13
14
  import { Form } from '../Form';
15
+ import { type FormFieldMap } from '../Form/FormFieldComponent';
16
+
17
+ export type RefOption = {
18
+ id: string;
19
+ label: string;
20
+ hue?: Palette;
21
+ };
14
22
 
15
23
  export type ObjectPickerContentProps = ThemedClassName<{
16
- options: QueryTag[];
24
+ options: RefOption[];
17
25
  selectedIds?: string[];
18
- createSchema?: Schema.Schema.AnyNoContext;
19
26
  createOptionLabel?: [string, { ns: string }];
20
27
  createOptionIcon?: string;
28
+ createSchema?: Schema.Schema.AnyNoContext;
21
29
  createInitialValuePath?: string;
30
+ createFieldMap?: FormFieldMap;
31
+ /**
32
+ * Persist a newly-created object given the form values. May be async (e.g.
33
+ * to write to a database). The Promise is awaited before the inline create
34
+ * form is collapsed back to the picker list, so consumers can complete
35
+ * follow-up work (assign the new ref to a parent slot, close the popover)
36
+ * before the picker UI re-renders.
37
+ */
38
+ onCreate?: (values: any) => unknown | Promise<unknown>;
22
39
  onSelect: (id: string) => void;
23
- onCreate?: (values: any) => void;
24
40
  }>;
25
41
 
26
42
  const ObjectPickerContent = forwardRef<HTMLDivElement, ObjectPickerContentProps>(
@@ -32,46 +48,35 @@ const ObjectPickerContent = forwardRef<HTMLDivElement, ObjectPickerContentProps>
32
48
  createOptionLabel,
33
49
  createOptionIcon,
34
50
  createInitialValuePath,
51
+ createFieldMap,
35
52
  onSelect,
36
53
  onCreate,
37
54
  ...props
38
55
  },
39
- ref,
56
+ forwardedRef,
40
57
  ) => {
41
58
  const { t } = useTranslation(translationKey);
42
-
43
- const [searchString, setSearchString] = useState('');
44
59
  const [showForm, setShowForm] = useState(false);
60
+ const [formInitialValue, setFormInitialValue] = useState<string>('');
61
+
62
+ const { results, query, handleSearch } = useSearchListResults({
63
+ items: options,
64
+ });
45
65
 
46
66
  const handleFormSave = useCallback(
47
- (values: any) => {
48
- onCreate?.(values);
67
+ async (values: any) => {
68
+ await onCreate?.(values);
49
69
  setShowForm(false);
50
- setSearchString('');
70
+ setFormInitialValue('');
51
71
  },
52
72
  [onCreate],
53
73
  );
54
74
 
55
75
  const handleFormCancel = useCallback(() => {
56
76
  setShowForm(false);
57
- setSearchString('');
77
+ setFormInitialValue('');
58
78
  }, []);
59
79
 
60
- // TODO(thure): The following click and keydown handlers are necessary because `onSelect` is called after the
61
- // Popover is already closed. Augment/refactor CmdK, if possible, to facilitate stopping event default
62
- // and propagation.
63
-
64
- const handleClick = useCallback(
65
- (event: MouseEvent) => {
66
- if (createSchema && (event.target as HTMLElement).closest('[data-value="__create__"]')) {
67
- event.stopPropagation();
68
- event.preventDefault();
69
- setShowForm(true);
70
- }
71
- },
72
- [createSchema],
73
- );
74
-
75
80
  const handleKeyDown = useCallback(
76
81
  (event: KeyboardEvent) => {
77
82
  if (
@@ -83,78 +88,114 @@ const ObjectPickerContent = forwardRef<HTMLDivElement, ObjectPickerContentProps>
83
88
  ) {
84
89
  event.stopPropagation();
85
90
  event.preventDefault();
91
+ // Get the current query from the input element
92
+ const input = (event.currentTarget as HTMLElement).querySelector('input[type="text"]') as HTMLInputElement;
93
+ const currentQuery = input?.value || '';
94
+ setFormInitialValue(currentQuery);
86
95
  setShowForm(true);
87
96
  }
88
97
  },
89
98
  [createSchema],
90
99
  );
91
100
 
92
- const labelById = useMemo(
93
- () =>
94
- options.reduce((acc: Record<string, string>, option) => {
95
- acc[option.id.toLowerCase()] = option.label.toLowerCase();
96
- return acc;
97
- }, {}),
98
- [options],
99
- );
100
-
101
- return (
102
- <Combobox.Content
103
- {...props}
104
- ref={ref}
105
- filter={(value, search) => (value === '__create__' || labelById[value]?.includes(search.toLowerCase()) ? 1 : 0)}
106
- onClickCapture={handleClick}
107
- onKeyDownCapture={handleKeyDown}
108
- >
109
- {showForm && createSchema ? (
101
+ if (showForm && createSchema) {
102
+ return (
103
+ <Combobox.Content {...props} onKeyDownCapture={handleKeyDown} ref={forwardedRef}>
110
104
  <Popover.Viewport>
111
- <Form
105
+ <Form.Root
106
+ testId='create-referenced-object-form'
112
107
  schema={createSchema}
113
- values={createInitialValuePath ? { [createInitialValuePath]: searchString } : {}}
108
+ defaultValues={createInitialValuePath ? { [createInitialValuePath]: formInitialValue } : {}}
109
+ fieldMap={createFieldMap}
114
110
  onSave={handleFormSave}
115
111
  onCancel={handleFormCancel}
116
- testId='create-referenced-object-form'
117
- />
112
+ >
113
+ <Form.Viewport>
114
+ <Form.Content>
115
+ <Form.FieldSet />
116
+ <Form.Actions />
117
+ </Form.Content>
118
+ </Form.Viewport>
119
+ </Form.Root>
118
120
  </Popover.Viewport>
119
- ) : (
120
- <>
121
- <Combobox.Input
122
- placeholder={t('ref field combobox input placeholder')}
123
- value={searchString}
124
- onValueChange={setSearchString}
125
- autoFocus
121
+ <Combobox.Arrow />
122
+ </Combobox.Content>
123
+ );
124
+ }
125
+
126
+ return (
127
+ <Combobox.Content {...props} onKeyDownCapture={handleKeyDown} ref={forwardedRef}>
128
+ <Combobox.Input
129
+ placeholder={t('ref-field-combobox-input.placeholder')}
130
+ autoFocus
131
+ value={query}
132
+ onValueChange={handleSearch}
133
+ />
134
+ <Combobox.List>
135
+ {results.map((option) => (
136
+ <Combobox.Item
137
+ key={option.id}
138
+ value={option.id}
139
+ label={option.label}
140
+ checked={selectedIds?.includes(option.id)}
141
+ onSelect={() => onSelect(option.id)}
142
+ classNames='flex items-center gap-2'
143
+ />
144
+ ))}
145
+ {createSchema && onCreate && (
146
+ <CreateItem
147
+ query={query}
148
+ createOptionLabel={createOptionLabel}
149
+ createOptionIcon={createOptionIcon ?? 'ph--plus--regular'}
150
+ onCreateItemSelect={(currentQuery: string) => {
151
+ setFormInitialValue(currentQuery);
152
+ setShowForm(true);
153
+ }}
126
154
  />
127
- <Combobox.List>
128
- {options.map((option) => (
129
- <Combobox.Item
130
- key={option.id}
131
- value={option.id}
132
- onSelect={() => onSelect(option.id)}
133
- classNames='flex items-center gap-2'
134
- >
135
- <span className='grow'>{option.label}</span>
136
- {selectedIds?.includes(option.id) && <Icon icon='ph--check--regular' />}
137
- </Combobox.Item>
138
- ))}
139
- {searchString.length > 0 && createOptionLabel && createOptionIcon && createSchema && onCreate && (
140
- <Combobox.Item value='__create__' classNames='flex items-center gap-2'>
141
- <Icon icon={createOptionIcon} />
142
- {t(createOptionLabel[0], { ns: createOptionLabel[1].ns, text: searchString })}
143
- </Combobox.Item>
144
- )}
145
- </Combobox.List>
146
- </>
147
- )}
155
+ )}
156
+ </Combobox.List>
148
157
  <Combobox.Arrow />
149
158
  </Combobox.Content>
150
159
  );
151
160
  },
152
161
  );
153
162
 
154
- ObjectPickerContent.displayName = 'ObjectPickerContent';
163
+ const CreateItem = ({
164
+ query,
165
+ createOptionLabel,
166
+ createOptionIcon,
167
+ onCreateItemSelect,
168
+ }: {
169
+ query: string;
170
+ createOptionLabel?: [string, { ns: string }];
171
+ createOptionIcon: string;
172
+ onCreateItemSelect: (query: string) => void;
173
+ }) => {
174
+ const { t } = useTranslation(translationKey);
175
+
176
+ const label = createOptionLabel
177
+ ? t(createOptionLabel[0], { ns: createOptionLabel[1].ns, text: query })
178
+ : t('create-option.label');
179
+
180
+ return (
181
+ <Combobox.Item
182
+ value='__create__'
183
+ label={label}
184
+ icon={createOptionIcon}
185
+ classNames='flex items-center gap-2'
186
+ closeOnSelect={false}
187
+ onSelect={() => {
188
+ onCreateItemSelect(query);
189
+ }}
190
+ />
191
+ );
192
+ };
193
+
194
+ ObjectPickerContent.displayName = 'ObjectPicker.Content';
155
195
 
156
196
  export const ObjectPicker = {
157
197
  Root: Combobox.Root,
198
+ Portal: Combobox.Portal,
158
199
  Trigger: Combobox.Trigger,
159
200
  VirtualTrigger: Combobox.VirtualTrigger,
160
201
  Content: ObjectPickerContent,