@dxos/react-ui-form 0.8.4-main.fd6878d → 0.8.4-staging.60fe92afc8

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 (318) hide show
  1. package/LICENSE +102 -5
  2. package/README.md +0 -1
  3. package/dist/lib/browser/index.mjs +4054 -2217
  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 +87 -0
  7. package/dist/lib/browser/translations.mjs.map +7 -0
  8. package/dist/lib/node-esm/index.mjs +4054 -2217
  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 +89 -0
  12. package/dist/lib/node-esm/translations.mjs.map +7 -0
  13. package/dist/types/src/components/FieldEditor/FieldEditor.d.ts +8 -8
  14. package/dist/types/src/components/FieldEditor/FieldEditor.d.ts.map +1 -1
  15. package/dist/types/src/components/FieldEditor/FieldEditor.stories.d.ts +89 -5
  16. package/dist/types/src/components/FieldEditor/FieldEditor.stories.d.ts.map +1 -1
  17. package/dist/types/src/components/Form/Form.d.ts +135 -25
  18. package/dist/types/src/components/Form/Form.d.ts.map +1 -1
  19. package/dist/types/src/components/Form/Form.stories.d.ts +207 -56
  20. package/dist/types/src/components/Form/Form.stories.d.ts.map +1 -1
  21. package/dist/types/src/components/Form/FormField.d.ts +52 -0
  22. package/dist/types/src/components/Form/FormField.d.ts.map +1 -0
  23. package/dist/types/src/components/Form/FormFieldComponent.d.ts +98 -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 +26 -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 +50 -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 +123 -0
  34. package/dist/types/src/components/Form/Layout/FormLayout.stories.d.ts.map +1 -0
  35. package/dist/types/src/components/Form/Layout/FormLayout.stories.test.d.ts +2 -0
  36. package/dist/types/src/components/Form/Layout/FormLayout.stories.test.d.ts.map +1 -0
  37. package/dist/types/src/components/Form/Layout/FormLayout.test.d.ts +2 -0
  38. package/dist/types/src/components/Form/Layout/FormLayout.test.d.ts.map +1 -0
  39. package/dist/types/src/components/Form/Layout/annotation.d.ts +30 -0
  40. package/dist/types/src/components/Form/Layout/annotation.d.ts.map +1 -0
  41. package/dist/types/src/components/Form/Layout/index.d.ts +4 -0
  42. package/dist/types/src/components/Form/Layout/index.d.ts.map +1 -0
  43. package/dist/types/src/components/Form/Layout/parser.d.ts +31 -0
  44. package/dist/types/src/components/Form/Layout/parser.d.ts.map +1 -0
  45. package/dist/types/src/components/Form/Layout/parser.test.d.ts +2 -0
  46. package/dist/types/src/components/Form/Layout/parser.test.d.ts.map +1 -0
  47. package/dist/types/src/components/Form/fields/ArrayField.d.ts +18 -0
  48. package/dist/types/src/components/Form/fields/ArrayField.d.ts.map +1 -0
  49. package/dist/types/src/components/Form/fields/BooleanField.d.ts +4 -0
  50. package/dist/types/src/components/Form/fields/BooleanField.d.ts.map +1 -0
  51. package/dist/types/src/components/Form/fields/DateField.d.ts +4 -0
  52. package/dist/types/src/components/Form/fields/DateField.d.ts.map +1 -0
  53. package/dist/types/src/components/Form/fields/GeoPointField.d.ts +5 -0
  54. package/dist/types/src/components/Form/fields/GeoPointField.d.ts.map +1 -0
  55. package/dist/types/src/components/Form/fields/InlineRefField.d.ts +13 -0
  56. package/dist/types/src/components/Form/fields/InlineRefField.d.ts.map +1 -0
  57. package/dist/types/src/components/Form/fields/MarkdownField.d.ts +11 -0
  58. package/dist/types/src/components/Form/fields/MarkdownField.d.ts.map +1 -0
  59. package/dist/types/src/components/Form/fields/MarkdownField.stories.d.ts +89 -0
  60. package/dist/types/src/components/Form/fields/MarkdownField.stories.d.ts.map +1 -0
  61. package/dist/types/src/components/Form/fields/NumberField.d.ts +4 -0
  62. package/dist/types/src/components/Form/fields/NumberField.d.ts.map +1 -0
  63. package/dist/types/src/components/Form/fields/RefField.d.ts +34 -0
  64. package/dist/types/src/components/Form/fields/RefField.d.ts.map +1 -0
  65. package/dist/types/src/components/Form/fields/RefField.test.d.ts +2 -0
  66. package/dist/types/src/components/Form/fields/RefField.test.d.ts.map +1 -0
  67. package/dist/types/src/components/Form/fields/SelectField.d.ts +13 -0
  68. package/dist/types/src/components/Form/fields/SelectField.d.ts.map +1 -0
  69. package/dist/types/src/components/Form/fields/SelectOptionField.d.ts +5 -0
  70. package/dist/types/src/components/Form/fields/SelectOptionField.d.ts.map +1 -0
  71. package/dist/types/src/components/Form/fields/SelectOptionField.stories.d.ts +89 -0
  72. package/dist/types/src/components/Form/fields/SelectOptionField.stories.d.ts.map +1 -0
  73. package/dist/types/src/components/Form/fields/TextAreaField.d.ts +4 -0
  74. package/dist/types/src/components/Form/fields/TextAreaField.d.ts.map +1 -0
  75. package/dist/types/src/components/Form/fields/TextField.d.ts +4 -0
  76. package/dist/types/src/components/Form/fields/TextField.d.ts.map +1 -0
  77. package/dist/types/src/components/Form/fields/TupleField.d.ts +6 -0
  78. package/dist/types/src/components/Form/fields/TupleField.d.ts.map +1 -0
  79. package/dist/types/src/components/Form/fields/index.d.ts +14 -0
  80. package/dist/types/src/components/Form/fields/index.d.ts.map +1 -0
  81. package/dist/types/src/components/Form/index.d.ts +5 -4
  82. package/dist/types/src/components/Form/index.d.ts.map +1 -1
  83. package/dist/types/src/components/Form/meta-tags.d.ts +30 -0
  84. package/dist/types/src/components/Form/meta-tags.d.ts.map +1 -0
  85. package/dist/types/src/components/Form/meta-tags.test.d.ts +2 -0
  86. package/dist/types/src/components/Form/meta-tags.test.d.ts.map +1 -0
  87. package/dist/types/src/components/ObjectForm/ObjectForm.d.ts +8 -0
  88. package/dist/types/src/components/ObjectForm/ObjectForm.d.ts.map +1 -0
  89. package/dist/types/src/components/ObjectForm/ObjectForm.stories.d.ts +87 -0
  90. package/dist/types/src/components/ObjectForm/ObjectForm.stories.d.ts.map +1 -0
  91. package/dist/types/src/components/ObjectForm/index.d.ts +2 -0
  92. package/dist/types/src/components/ObjectForm/index.d.ts.map +1 -0
  93. package/dist/types/src/components/ObjectPicker/ObjectPicker.d.ts +68 -0
  94. package/dist/types/src/components/ObjectPicker/ObjectPicker.d.ts.map +1 -0
  95. package/dist/types/src/components/ObjectPicker/ObjectPicker.stories.d.ts +92 -0
  96. package/dist/types/src/components/ObjectPicker/ObjectPicker.stories.d.ts.map +1 -0
  97. package/dist/types/src/components/ObjectPicker/index.d.ts +2 -0
  98. package/dist/types/src/components/ObjectPicker/index.d.ts.map +1 -0
  99. package/dist/types/src/components/ObjectProperties/ObjectProperties.d.ts +9 -0
  100. package/dist/types/src/components/ObjectProperties/ObjectProperties.d.ts.map +1 -0
  101. package/dist/types/src/components/ObjectProperties/ObjectProperties.stories.d.ts +132 -0
  102. package/dist/types/src/components/ObjectProperties/ObjectProperties.stories.d.ts.map +1 -0
  103. package/dist/types/src/components/ObjectProperties/ObjectProperties.test.d.ts +2 -0
  104. package/dist/types/src/components/ObjectProperties/ObjectProperties.test.d.ts.map +1 -0
  105. package/dist/types/src/components/ObjectProperties/index.d.ts +2 -0
  106. package/dist/types/src/components/ObjectProperties/index.d.ts.map +1 -0
  107. package/dist/types/src/components/ObjectTree/ObjectTree.d.ts +9 -0
  108. package/dist/types/src/components/ObjectTree/ObjectTree.d.ts.map +1 -0
  109. package/dist/types/src/components/ObjectTree/ObjectTree.stories.d.ts +91 -0
  110. package/dist/types/src/components/ObjectTree/ObjectTree.stories.d.ts.map +1 -0
  111. package/dist/types/src/components/ObjectTree/index.d.ts +2 -0
  112. package/dist/types/src/components/ObjectTree/index.d.ts.map +1 -0
  113. package/dist/types/src/components/RefEditor/RefEditor.d.ts +73 -0
  114. package/dist/types/src/components/RefEditor/RefEditor.d.ts.map +1 -0
  115. package/dist/types/src/components/RefEditor/RefEditor.stories.d.ts +92 -0
  116. package/dist/types/src/components/RefEditor/RefEditor.stories.d.ts.map +1 -0
  117. package/dist/types/src/components/RefEditor/index.d.ts +3 -0
  118. package/dist/types/src/components/RefEditor/index.d.ts.map +1 -0
  119. package/dist/types/src/components/RefEditor/ref-editor-extension.d.ts +47 -0
  120. package/dist/types/src/components/RefEditor/ref-editor-extension.d.ts.map +1 -0
  121. package/dist/types/src/components/Settings/Settings.d.ts +51 -0
  122. package/dist/types/src/components/Settings/Settings.d.ts.map +1 -0
  123. package/dist/types/src/components/Settings/Settings.stories.d.ts +89 -0
  124. package/dist/types/src/components/Settings/Settings.stories.d.ts.map +1 -0
  125. package/dist/types/src/components/Settings/SettingsFieldSet.d.ts +43 -0
  126. package/dist/types/src/components/Settings/SettingsFieldSet.d.ts.map +1 -0
  127. package/dist/types/src/components/Settings/SettingsItem.d.ts +16 -0
  128. package/dist/types/src/components/Settings/SettingsItem.d.ts.map +1 -0
  129. package/dist/types/src/components/Settings/SettingsSelectField.d.ts +18 -0
  130. package/dist/types/src/components/Settings/SettingsSelectField.d.ts.map +1 -0
  131. package/dist/types/src/components/Settings/index.d.ts +4 -0
  132. package/dist/types/src/components/Settings/index.d.ts.map +1 -0
  133. package/dist/types/src/components/ViewEditor/ViewEditor.d.ts +21 -11
  134. package/dist/types/src/components/ViewEditor/ViewEditor.d.ts.map +1 -1
  135. package/dist/types/src/components/ViewEditor/ViewEditor.stories.d.ts +91 -9
  136. package/dist/types/src/components/ViewEditor/ViewEditor.stories.d.ts.map +1 -1
  137. package/dist/types/src/components/index.d.ts +6 -2
  138. package/dist/types/src/components/index.d.ts.map +1 -1
  139. package/dist/types/src/components/testing.d.ts +14 -4
  140. package/dist/types/src/components/testing.d.ts.map +1 -1
  141. package/dist/types/src/hooks/index.d.ts +2 -3
  142. package/dist/types/src/hooks/index.d.ts.map +1 -1
  143. package/dist/types/src/hooks/useFormHandler.d.ts +82 -0
  144. package/dist/types/src/hooks/useFormHandler.d.ts.map +1 -0
  145. package/dist/types/src/hooks/useKeyHandler.d.ts +7 -0
  146. package/dist/types/src/hooks/useKeyHandler.d.ts.map +1 -0
  147. package/dist/types/src/index.d.ts +0 -2
  148. package/dist/types/src/index.d.ts.map +1 -1
  149. package/dist/types/src/translations.d.ts +70 -55
  150. package/dist/types/src/translations.d.ts.map +1 -1
  151. package/dist/types/src/util/field-type.d.ts +19 -0
  152. package/dist/types/src/util/field-type.d.ts.map +1 -0
  153. package/dist/types/src/util/field.d.ts +3 -3
  154. package/dist/types/src/util/field.d.ts.map +1 -1
  155. package/dist/types/src/util/formatting.d.ts +2 -2
  156. package/dist/types/src/util/formatting.d.ts.map +1 -1
  157. package/dist/types/src/util/index.d.ts +2 -1
  158. package/dist/types/src/util/index.d.ts.map +1 -1
  159. package/dist/types/src/util/properties.d.ts +35 -0
  160. package/dist/types/src/util/properties.d.ts.map +1 -0
  161. package/dist/types/src/util/properties.test.d.ts +2 -0
  162. package/dist/types/src/util/properties.test.d.ts.map +1 -0
  163. package/dist/types/src/util/refs.d.ts +3 -3
  164. package/dist/types/src/util/refs.d.ts.map +1 -1
  165. package/dist/types/tsconfig.tsbuildinfo +1 -1
  166. package/package.json +64 -47
  167. package/src/components/FieldEditor/FieldEditor.stories.tsx +49 -31
  168. package/src/components/FieldEditor/FieldEditor.test.tsx +4 -2
  169. package/src/components/FieldEditor/FieldEditor.tsx +100 -97
  170. package/src/components/Form/AUDIT.md +17 -0
  171. package/src/components/Form/Form.stories.tsx +151 -221
  172. package/src/components/Form/Form.tsx +461 -95
  173. package/src/components/Form/FormField.tsx +345 -0
  174. package/src/components/Form/FormFieldComponent.stories.tsx +65 -0
  175. package/src/components/Form/FormFieldComponent.tsx +253 -0
  176. package/src/components/Form/FormFieldSet.tsx +213 -0
  177. package/src/components/Form/FormTooltipsContext.ts +25 -0
  178. package/src/components/Form/Layout/FormLayout.stories.test.tsx +51 -0
  179. package/src/components/Form/Layout/FormLayout.stories.tsx +422 -0
  180. package/src/components/Form/Layout/FormLayout.test.ts +59 -0
  181. package/src/components/Form/Layout/FormLayout.tsx +251 -0
  182. package/src/components/Form/Layout/annotation.ts +38 -0
  183. package/src/components/Form/Layout/index.ts +7 -0
  184. package/src/components/Form/Layout/parser.test.ts +120 -0
  185. package/src/components/Form/Layout/parser.ts +167 -0
  186. package/src/components/Form/README.md +3 -0
  187. package/src/components/Form/fields/ArrayField.tsx +173 -0
  188. package/src/components/Form/fields/BooleanField.tsx +22 -0
  189. package/src/components/Form/fields/DateField.tsx +101 -0
  190. package/src/components/Form/fields/GeoPointField.tsx +124 -0
  191. package/src/components/Form/fields/InlineRefField.tsx +138 -0
  192. package/src/components/Form/fields/MarkdownField.stories.tsx +206 -0
  193. package/src/components/Form/fields/MarkdownField.tsx +167 -0
  194. package/src/components/Form/fields/NumberField.tsx +76 -0
  195. package/src/components/Form/fields/RefField.test.ts +45 -0
  196. package/src/components/Form/fields/RefField.tsx +266 -0
  197. package/src/components/Form/fields/SelectField.tsx +77 -0
  198. package/src/components/Form/fields/SelectOptionField.stories.tsx +66 -0
  199. package/src/components/Form/{custom/SelectOptionsInput.tsx → fields/SelectOptionField.tsx} +39 -32
  200. package/src/components/Form/fields/TextAreaField.tsx +38 -0
  201. package/src/components/Form/fields/TextField.tsx +38 -0
  202. package/src/components/Form/fields/TupleField.tsx +55 -0
  203. package/src/components/Form/fields/index.ts +17 -0
  204. package/src/components/Form/index.ts +6 -4
  205. package/src/components/Form/meta-tags.test.ts +47 -0
  206. package/src/components/Form/meta-tags.ts +33 -0
  207. package/src/components/ObjectForm/ObjectForm.stories.tsx +62 -0
  208. package/src/components/ObjectForm/ObjectForm.tsx +111 -0
  209. package/src/components/{ControlSection → ObjectForm}/index.ts +1 -1
  210. package/src/components/ObjectPicker/ObjectPicker.stories.tsx +212 -0
  211. package/src/components/ObjectPicker/ObjectPicker.tsx +202 -0
  212. package/src/components/ObjectPicker/index.ts +5 -0
  213. package/src/components/ObjectProperties/ObjectProperties.stories.tsx +350 -0
  214. package/src/components/ObjectProperties/ObjectProperties.test.tsx +111 -0
  215. package/src/components/ObjectProperties/ObjectProperties.tsx +146 -0
  216. package/src/components/ObjectProperties/index.ts +5 -0
  217. package/src/components/ObjectTree/ObjectTree.stories.tsx +134 -0
  218. package/src/components/ObjectTree/ObjectTree.tsx +190 -0
  219. package/src/components/ObjectTree/index.ts +5 -0
  220. package/src/components/RefEditor/RefEditor.stories.tsx +148 -0
  221. package/src/components/RefEditor/RefEditor.tsx +275 -0
  222. package/src/components/RefEditor/index.ts +6 -0
  223. package/src/components/RefEditor/ref-editor-extension.ts +254 -0
  224. package/src/components/Settings/AUDIT.md +51 -0
  225. package/src/components/Settings/Settings.stories.tsx +104 -0
  226. package/src/components/Settings/Settings.tsx +81 -0
  227. package/src/components/Settings/SettingsFieldSet.tsx +257 -0
  228. package/src/components/Settings/SettingsItem.tsx +58 -0
  229. package/src/components/Settings/SettingsSelectField.tsx +59 -0
  230. package/src/components/Settings/index.ts +7 -0
  231. package/src/components/ViewEditor/ViewEditor.stories.tsx +132 -64
  232. package/src/components/ViewEditor/ViewEditor.test.tsx +23 -22
  233. package/src/components/ViewEditor/ViewEditor.tsx +378 -216
  234. package/src/components/index.ts +6 -3
  235. package/src/components/testing.tsx +31 -12
  236. package/src/hooks/index.ts +2 -3
  237. package/src/hooks/useFormHandler.ts +380 -0
  238. package/src/hooks/useKeyHandler.tsx +37 -0
  239. package/src/index.ts +0 -4
  240. package/src/translations.ts +76 -59
  241. package/src/util/field-type.ts +60 -0
  242. package/src/util/field.ts +33 -43
  243. package/src/util/formatting.ts +22 -22
  244. package/src/util/index.ts +2 -1
  245. package/src/util/properties.test.ts +153 -0
  246. package/src/util/properties.ts +100 -0
  247. package/src/util/refs.ts +20 -19
  248. package/src/vite-env.d.ts +10 -0
  249. package/dist/types/src/components/ControlSection/ControlSection.d.ts +0 -23
  250. package/dist/types/src/components/ControlSection/ControlSection.d.ts.map +0 -1
  251. package/dist/types/src/components/ControlSection/ControlSection.stories.d.ts +0 -8
  252. package/dist/types/src/components/ControlSection/ControlSection.stories.d.ts.map +0 -1
  253. package/dist/types/src/components/ControlSection/index.d.ts +0 -2
  254. package/dist/types/src/components/ControlSection/index.d.ts.map +0 -1
  255. package/dist/types/src/components/Form/ArrayField.d.ts +0 -16
  256. package/dist/types/src/components/Form/ArrayField.d.ts.map +0 -1
  257. package/dist/types/src/components/Form/Defaults.d.ts +0 -14
  258. package/dist/types/src/components/Form/Defaults.d.ts.map +0 -1
  259. package/dist/types/src/components/Form/FormActions.d.ts +0 -9
  260. package/dist/types/src/components/Form/FormActions.d.ts.map +0 -1
  261. package/dist/types/src/components/Form/FormContent.d.ts +0 -57
  262. package/dist/types/src/components/Form/FormContent.d.ts.map +0 -1
  263. package/dist/types/src/components/Form/FormContext.d.ts +0 -23
  264. package/dist/types/src/components/Form/FormContext.d.ts.map +0 -1
  265. package/dist/types/src/components/Form/Input.d.ts +0 -26
  266. package/dist/types/src/components/Form/Input.d.ts.map +0 -1
  267. package/dist/types/src/components/Form/RefField.d.ts +0 -12
  268. package/dist/types/src/components/Form/RefField.d.ts.map +0 -1
  269. package/dist/types/src/components/Form/custom/GeoPointInput.d.ts +0 -4
  270. package/dist/types/src/components/Form/custom/GeoPointInput.d.ts.map +0 -1
  271. package/dist/types/src/components/Form/custom/SelectOptionsInput.d.ts +0 -4
  272. package/dist/types/src/components/Form/custom/SelectOptionsInput.d.ts.map +0 -1
  273. package/dist/types/src/components/Form/custom/SelectOptionsInput.stories.d.ts +0 -7
  274. package/dist/types/src/components/Form/custom/SelectOptionsInput.stories.d.ts.map +0 -1
  275. package/dist/types/src/components/Form/custom/TupleInput.d.ts +0 -6
  276. package/dist/types/src/components/Form/custom/TupleInput.d.ts.map +0 -1
  277. package/dist/types/src/components/Form/custom/index.d.ts +0 -4
  278. package/dist/types/src/components/Form/custom/index.d.ts.map +0 -1
  279. package/dist/types/src/components/Form/factory.d.ts +0 -8
  280. package/dist/types/src/components/Form/factory.d.ts.map +0 -1
  281. package/dist/types/src/components/deprecated/Form.d.ts +0 -15
  282. package/dist/types/src/components/deprecated/Form.d.ts.map +0 -1
  283. package/dist/types/src/components/deprecated/Form.stories.d.ts +0 -13
  284. package/dist/types/src/components/deprecated/Form.stories.d.ts.map +0 -1
  285. package/dist/types/src/components/deprecated/FormInput.d.ts +0 -16
  286. package/dist/types/src/components/deprecated/FormInput.d.ts.map +0 -1
  287. package/dist/types/src/components/deprecated/index.d.ts +0 -2
  288. package/dist/types/src/components/deprecated/index.d.ts.map +0 -1
  289. package/dist/types/src/hooks/useForm.d.ts +0 -67
  290. package/dist/types/src/hooks/useForm.d.ts.map +0 -1
  291. package/dist/types/src/hooks/useQueryRefOptions.d.ts +0 -23
  292. package/dist/types/src/hooks/useQueryRefOptions.d.ts.map +0 -1
  293. package/dist/types/src/hooks/useRefQueryLookupHandler.d.ts +0 -8
  294. package/dist/types/src/hooks/useRefQueryLookupHandler.d.ts.map +0 -1
  295. package/dist/types/src/util/schema.d.ts +0 -3
  296. package/dist/types/src/util/schema.d.ts.map +0 -1
  297. package/src/components/ControlSection/ControlSection.stories.tsx +0 -42
  298. package/src/components/ControlSection/ControlSection.tsx +0 -139
  299. package/src/components/Form/ArrayField.tsx +0 -125
  300. package/src/components/Form/Defaults.tsx +0 -204
  301. package/src/components/Form/FormActions.tsx +0 -54
  302. package/src/components/Form/FormContent.tsx +0 -241
  303. package/src/components/Form/FormContext.tsx +0 -110
  304. package/src/components/Form/Input.tsx +0 -51
  305. package/src/components/Form/RefField.tsx +0 -214
  306. package/src/components/Form/custom/GeoPointInput.tsx +0 -72
  307. package/src/components/Form/custom/SelectOptionsInput.stories.tsx +0 -62
  308. package/src/components/Form/custom/TupleInput.tsx +0 -47
  309. package/src/components/Form/custom/index.ts +0 -7
  310. package/src/components/Form/factory.tsx +0 -31
  311. package/src/components/deprecated/Form.stories.tsx +0 -63
  312. package/src/components/deprecated/Form.tsx +0 -119
  313. package/src/components/deprecated/FormInput.tsx +0 -71
  314. package/src/components/deprecated/index.ts +0 -5
  315. package/src/hooks/useForm.ts +0 -293
  316. package/src/hooks/useQueryRefOptions.ts +0 -53
  317. package/src/hooks/useRefQueryLookupHandler.ts +0 -39
  318. package/src/util/schema.ts +0 -14
@@ -0,0 +1,202 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import type * as Schema from 'effect/Schema';
6
+ import React, { type KeyboardEvent, forwardRef, useCallback, useState } from 'react';
7
+
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';
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
+ };
22
+
23
+ export type ObjectPickerContentProps = ThemedClassName<{
24
+ options: RefOption[];
25
+ selectedIds?: string[];
26
+ createOptionLabel?: [string, { ns: string | readonly string[] }];
27
+ createOptionIcon?: string;
28
+ createSchema?: Schema.Schema.AnyNoContext;
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>;
39
+ onSelect: (id: string) => void;
40
+ }>;
41
+
42
+ const ObjectPickerContent = forwardRef<HTMLDivElement, ObjectPickerContentProps>(
43
+ (
44
+ {
45
+ options,
46
+ selectedIds,
47
+ createSchema,
48
+ createOptionLabel,
49
+ createOptionIcon,
50
+ createInitialValuePath,
51
+ createFieldMap,
52
+ onSelect,
53
+ onCreate,
54
+ ...props
55
+ },
56
+ forwardedRef,
57
+ ) => {
58
+ const { t } = useTranslation(translationKey);
59
+ const [showForm, setShowForm] = useState(false);
60
+ const [formInitialValue, setFormInitialValue] = useState<string>('');
61
+
62
+ const { results, query, handleSearch } = useSearchListResults({
63
+ items: options,
64
+ });
65
+
66
+ const handleFormSave = useCallback(
67
+ async (values: any) => {
68
+ await onCreate?.(values);
69
+ setShowForm(false);
70
+ setFormInitialValue('');
71
+ },
72
+ [onCreate],
73
+ );
74
+
75
+ const handleFormCancel = useCallback(() => {
76
+ setShowForm(false);
77
+ setFormInitialValue('');
78
+ }, []);
79
+
80
+ const handleKeyDown = useCallback(
81
+ (event: KeyboardEvent) => {
82
+ if (
83
+ event.key === 'Enter' &&
84
+ createSchema &&
85
+ (event.currentTarget as HTMLElement).querySelector(
86
+ '[role="option"][aria-selected="true"][data-value="__create__"]',
87
+ )
88
+ ) {
89
+ event.stopPropagation();
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);
95
+ setShowForm(true);
96
+ }
97
+ },
98
+ [createSchema],
99
+ );
100
+
101
+ if (showForm && createSchema) {
102
+ return (
103
+ <Combobox.Content {...props} onKeyDownCapture={handleKeyDown} ref={forwardedRef}>
104
+ <Popover.Viewport>
105
+ <Form.Root
106
+ testId='create-referenced-object-form'
107
+ schema={createSchema}
108
+ defaultValues={createInitialValuePath ? { [createInitialValuePath]: formInitialValue } : {}}
109
+ fieldMap={createFieldMap}
110
+ onSave={handleFormSave}
111
+ onCancel={handleFormCancel}
112
+ >
113
+ <Form.Viewport>
114
+ <Form.Content>
115
+ <Form.FieldSet />
116
+ <Form.Actions />
117
+ </Form.Content>
118
+ </Form.Viewport>
119
+ </Form.Root>
120
+ </Popover.Viewport>
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
+ }}
154
+ />
155
+ )}
156
+ </Combobox.List>
157
+ <Combobox.Arrow />
158
+ </Combobox.Content>
159
+ );
160
+ },
161
+ );
162
+
163
+ const CreateItem = ({
164
+ query,
165
+ createOptionLabel,
166
+ createOptionIcon,
167
+ onCreateItemSelect,
168
+ }: {
169
+ query: string;
170
+ createOptionLabel?: [string, { ns: string | readonly 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';
195
+
196
+ export const ObjectPicker = {
197
+ Root: Combobox.Root,
198
+ Portal: Combobox.Portal,
199
+ Trigger: Combobox.Trigger,
200
+ VirtualTrigger: Combobox.VirtualTrigger,
201
+ Content: ObjectPickerContent,
202
+ };
@@ -0,0 +1,5 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ export * from './ObjectPicker';
@@ -0,0 +1,350 @@
1
+ //
2
+ // Copyright 2023 DXOS.org
3
+ //
4
+
5
+ import { type Meta, type StoryObj } from '@storybook/react-vite';
6
+ import * as Schema from 'effect/Schema';
7
+ import React, { useEffect } from 'react';
8
+ import { expect, userEvent, within } from 'storybook/test';
9
+
10
+ import { DXN, Annotation, Filter, Obj, Ref, Tag, Type } from '@dxos/echo';
11
+ import { FormInputAnnotation, LabelAnnotation } from '@dxos/echo/Annotation';
12
+ import { useQuery } from '@dxos/react-client/echo';
13
+ import { useClientStory, withClientProvider } from '@dxos/react-client/testing';
14
+ import { Panel } from '@dxos/react-ui';
15
+ import { Loading, withLayout, withTheme } from '@dxos/react-ui/testing';
16
+ import { FactoryAnnotation, type FactoryFn } from '@dxos/schema';
17
+ import { Pipeline } from '@dxos/types';
18
+
19
+ import { translations } from '#translations';
20
+
21
+ import { OBJECT_PROPERTIES_DEBUG_SYMBOL } from '../testing';
22
+ import { ObjectProperties } from './ObjectProperties';
23
+
24
+ //
25
+ // Test-only schemas exercising a non-Tag ref-array. `Article.authors` is the
26
+ // case the generic create flow currently fails on: the picker creates an
27
+ // Author in the DB but never wires the new Ref into the array slot.
28
+ //
29
+
30
+ const Author = Schema.Struct({
31
+ name: Schema.String,
32
+ }).pipe(
33
+ LabelAnnotation.set(['name']),
34
+ Annotation.IconAnnotation.set({ icon: 'ph--user--regular', hue: 'blue' }),
35
+ Type.makeObject(DXN.make('org.dxos.test.author', '0.1.0')),
36
+ );
37
+ type Author = Type.InstanceType<typeof Author>;
38
+
39
+ const Article = Schema.Struct({
40
+ title: Schema.String.pipe(Schema.optional),
41
+ authors: Schema.Array(Ref.Ref(Author)),
42
+ }).pipe(
43
+ LabelAnnotation.set(['title']),
44
+ Annotation.IconAnnotation.set({ icon: 'ph--article--regular', hue: 'green' }),
45
+ Type.makeObject(DXN.make('org.dxos.test.article', '0.1.0')),
46
+ );
47
+ type Article = Type.InstanceType<typeof Article>;
48
+
49
+ //
50
+ // `Note` mirrors the `Subscription.Feed` shape: a required `Ref` field
51
+ // (`backing`) is hidden from the form via `FormInputAnnotation.set(false)`,
52
+ // so `Obj.make(Note, values)` from the picker would reject because the form
53
+ // values can't satisfy the schema. A `FactoryAnnotation` constructs the
54
+ // backing object and links it at create time. `cursor` is an optional hidden
55
+ // field included to exercise the same path for non-required hidden fields.
56
+ //
57
+ const NoteBacking = Schema.Struct({}).pipe(Type.makeObject(DXN.make('org.dxos.test.noteBacking', '0.1.0')));
58
+ type NoteBacking = Type.InstanceType<typeof NoteBacking>;
59
+
60
+ const Note = Schema.Struct({
61
+ title: Schema.String,
62
+ cursor: Schema.String.pipe(FormInputAnnotation.set(false), Schema.optional),
63
+ backing: Ref.Ref(NoteBacking).pipe(FormInputAnnotation.set(false)),
64
+ }).pipe(
65
+ LabelAnnotation.set(['title']),
66
+ Annotation.IconAnnotation.set({ icon: 'ph--note--regular', hue: 'amber' }),
67
+ FactoryAnnotation.set(((values: any) =>
68
+ Obj.make(Note, { ...values, backing: Ref.make(Obj.make(NoteBacking, {})) })) as FactoryFn),
69
+ Type.makeObject(DXN.make('org.dxos.test.note', '0.1.0')),
70
+ );
71
+ type Note = Type.InstanceType<typeof Note>;
72
+
73
+ const Notebook = Schema.Struct({
74
+ name: Schema.String.pipe(Schema.optional),
75
+ notes: Schema.Array(Ref.Ref(Note)),
76
+ }).pipe(
77
+ LabelAnnotation.set(['name']),
78
+ Annotation.IconAnnotation.set({ icon: 'ph--notebook--regular', hue: 'amber' }),
79
+ Type.makeObject(DXN.make('org.dxos.test.notebook', '0.1.0')),
80
+ );
81
+ type Notebook = Type.InstanceType<typeof Notebook>;
82
+
83
+ //
84
+ // Stories.
85
+ //
86
+
87
+ export type ObjectPropertiesDebug = {
88
+ /** Live database the story is rendering against. */
89
+ db: ReturnType<typeof useClientStory>['space'] extends infer S ? (S extends { db: infer D } ? D : never) : never;
90
+ /** Object whose properties are being edited. */
91
+ object: Obj.Unknown;
92
+ };
93
+
94
+ const PipelineStory = () => {
95
+ const { space } = useClientStory();
96
+ const [object] = useQuery(space?.db, Filter.type(Pipeline.Pipeline));
97
+
98
+ useEffect(() => {
99
+ if (typeof window !== 'undefined' && space?.db && object) {
100
+ (window as any)[OBJECT_PROPERTIES_DEBUG_SYMBOL] = { db: space.db, object } satisfies ObjectPropertiesDebug;
101
+ }
102
+ }, [space, object]);
103
+
104
+ if (!object) {
105
+ return <Loading />;
106
+ }
107
+ return (
108
+ <Panel.Root>
109
+ <Panel.Content asChild>
110
+ <ObjectProperties object={object} />
111
+ </Panel.Content>
112
+ </Panel.Root>
113
+ );
114
+ };
115
+
116
+ const ArticleStory = () => {
117
+ const { space } = useClientStory();
118
+ const [object] = useQuery(space?.db, Filter.type(Article));
119
+
120
+ useEffect(() => {
121
+ if (typeof window !== 'undefined' && space?.db && object) {
122
+ (window as any)[OBJECT_PROPERTIES_DEBUG_SYMBOL] = { db: space.db, object } satisfies ObjectPropertiesDebug;
123
+ }
124
+ }, [space, object]);
125
+
126
+ if (!object) {
127
+ return <Loading />;
128
+ }
129
+ return (
130
+ <Panel.Root>
131
+ <Panel.Content asChild>
132
+ <ObjectProperties object={object} />
133
+ </Panel.Content>
134
+ </Panel.Root>
135
+ );
136
+ };
137
+
138
+ const meta = {
139
+ title: 'ui/react-ui-form/ObjectProperties',
140
+ component: ObjectProperties as any,
141
+ decorators: [withTheme(), withLayout({ layout: 'column' })],
142
+ parameters: {
143
+ layout: 'fullscreen',
144
+ translations,
145
+ },
146
+ } satisfies Meta<typeof PipelineStory>;
147
+
148
+ export default meta;
149
+
150
+ type Story = StoryObj<typeof meta>;
151
+
152
+ const pipelineDecorators = [
153
+ withClientProvider({
154
+ createIdentity: true,
155
+ createSpace: true,
156
+ types: [Pipeline.Pipeline, Tag.Tag],
157
+ onCreateSpace: async ({ space }) => {
158
+ space.db.add(Pipeline.make());
159
+ space.db.add(Tag.make({ label: 'Tag 1' }));
160
+ space.db.add(Tag.make({ label: 'Tag 2' }));
161
+ space.db.add(Tag.make({ label: 'Tag 3' }));
162
+ },
163
+ }),
164
+ ];
165
+
166
+ const articleDecorators = [
167
+ withClientProvider({
168
+ createIdentity: true,
169
+ createSpace: true,
170
+ types: [Article, Author, Tag.Tag],
171
+ onCreateSpace: async ({ space }) => {
172
+ space.db.add(Obj.make(Article, { title: 'Untitled article', authors: [] }));
173
+ },
174
+ }),
175
+ ];
176
+
177
+ const NotebookStory = () => {
178
+ const { space } = useClientStory();
179
+ const [object] = useQuery(space?.db, Filter.type(Notebook));
180
+
181
+ useEffect(() => {
182
+ if (typeof window !== 'undefined' && space?.db && object) {
183
+ (window as any)[OBJECT_PROPERTIES_DEBUG_SYMBOL] = { db: space.db, object } satisfies ObjectPropertiesDebug;
184
+ }
185
+ }, [space, object]);
186
+
187
+ if (!object) {
188
+ return <Loading />;
189
+ }
190
+ return (
191
+ <Panel.Root>
192
+ <Panel.Content asChild>
193
+ <ObjectProperties object={object} />
194
+ </Panel.Content>
195
+ </Panel.Root>
196
+ );
197
+ };
198
+
199
+ const notebookDecorators = [
200
+ withClientProvider({
201
+ createIdentity: true,
202
+ createSpace: true,
203
+ types: [Notebook, Note, NoteBacking, Tag.Tag],
204
+ onCreateSpace: async ({ space }) => {
205
+ space.db.add(Obj.make(Notebook, { name: 'Untitled notebook', notes: [] }));
206
+ },
207
+ }),
208
+ ];
209
+
210
+ /**
211
+ * Default Pipeline form — manual exploration. Includes the Tags picker (which
212
+ * is the only ref-array path that currently works end-to-end, by virtue of
213
+ * `Tag.Tag` being special-cased in `ObjectProperties.handleCreate`).
214
+ */
215
+ export const Default: Story = {
216
+ render: PipelineStory,
217
+ decorators: pipelineDecorators,
218
+ };
219
+
220
+ /**
221
+ * Tag-creation play. Walks the inline create flow against the Tags picker and
222
+ * leaves the magazine in the post-Save state for assertions to inspect via
223
+ * `OBJECT_PROPERTIES_DEBUG_SYMBOL`.
224
+ *
225
+ * Expected end-state (currently passes):
226
+ * - new Tag.Tag exists in the DB with `label === "PinnedTag"`
227
+ * - `Obj.getMeta(object).tags` includes its DXN.
228
+ */
229
+ export const CreateTagPlay: Story = {
230
+ render: PipelineStory,
231
+ decorators: pipelineDecorators,
232
+ play: async ({ canvasElement }) => {
233
+ const canvas = within(canvasElement);
234
+ const body = within(document.body);
235
+
236
+ // Tags is `Schema.Array(Ref.Ref(Tag.Tag))` and starts empty, so it renders
237
+ // as an "Add item" button — same shape as any other ref-array. Click it
238
+ // to materialise an empty slot, then open the slot's picker.
239
+ const addButton = await canvas.findByRole('button', { name: /add/i }, { timeout: 10_000 });
240
+ await userEvent.click(addButton);
241
+ await new Promise((resolve) => setTimeout(resolve, 250));
242
+
243
+ const comboboxes = await canvas.findAllByRole('combobox', undefined, { timeout: 10_000 });
244
+ await expect(comboboxes.length).toBeGreaterThan(0);
245
+ await userEvent.click(comboboxes[0]);
246
+
247
+ const search = await body.findByPlaceholderText(/search/i, undefined, { timeout: 5000 });
248
+ await userEvent.type(search, 'PinnedTag');
249
+
250
+ // The Tag picker localises the create option as "Add tag" (no text suffix
251
+ // — the translation has no `{{text}}` placeholder), so match on /add/.
252
+ const createOption = await body.findByRole('option', { name: /add/i }, { timeout: 3000 });
253
+ await userEvent.click(createOption);
254
+
255
+ const form = await body.findByTestId('create-referenced-object-form', undefined, { timeout: 5000 });
256
+ const labelInput = await within(form).findByLabelText(/^label$/i);
257
+ await userEvent.clear(labelInput);
258
+ await userEvent.type(labelInput, 'PinnedTag');
259
+ await userEvent.click(await within(form).findByTestId('save-button'));
260
+
261
+ // Allow async state updates to flush before the test asserts.
262
+ await new Promise((resolve) => setTimeout(resolve, 250));
263
+ },
264
+ };
265
+
266
+ /**
267
+ * Ref-array creation play. Walks Add → Open → Create → Save against
268
+ * `Article.authors`. After the fix, the new Author should be persisted AND
269
+ * wired into `article.authors[0]`. Today it persists to the DB but the array
270
+ * slot stays empty.
271
+ */
272
+ export const CreateRefArrayPlay: Story = {
273
+ render: ArticleStory,
274
+ decorators: articleDecorators,
275
+ play: async ({ canvasElement }) => {
276
+ const canvas = within(canvasElement);
277
+ const body = within(document.body);
278
+
279
+ // ObjectProperties extends Article with the synthetic `_tags` meta-tags
280
+ // array (labelled "Tags"), so the form has two "Add item" buttons (Tags +
281
+ // Authors). Pick the last one — Authors comes after Tags in rendered order.
282
+ const addButtons = await canvas.findAllByRole('button', { name: /add/i }, { timeout: 10_000 });
283
+ await userEvent.click(addButtons[addButtons.length - 1]);
284
+ await new Promise((resolve) => setTimeout(resolve, 250));
285
+
286
+ // Open the empty slot's combobox.
287
+ const comboboxes = await canvas.findAllByRole('combobox', undefined, { timeout: 10_000 });
288
+ await expect(comboboxes.length).toBeGreaterThan(0);
289
+ await userEvent.click(comboboxes[0]);
290
+
291
+ const search = await body.findByPlaceholderText(/search/i, undefined, { timeout: 5000 });
292
+ await userEvent.type(search, 'Ada Lovelace');
293
+
294
+ // Author has no `createOptionLabel` → option falls back to "Create".
295
+ const createOption = await body.findByRole('option', { name: /create/i }, { timeout: 3000 });
296
+ await userEvent.click(createOption);
297
+
298
+ const form = await body.findByTestId('create-referenced-object-form', undefined, { timeout: 5000 });
299
+ const nameInput = await within(form).findByLabelText(/^name$/i);
300
+ await userEvent.clear(nameInput);
301
+ await userEvent.type(nameInput, 'Ada Lovelace');
302
+ await userEvent.click(await within(form).findByTestId('save-button'));
303
+
304
+ await new Promise((resolve) => setTimeout(resolve, 250));
305
+ },
306
+ };
307
+
308
+ /**
309
+ * Ref-array creation against a schema with a hidden required field. `Note`
310
+ * mirrors the shape of `Subscription.Feed`: the form omits `backing` (a
311
+ * required `Ref` annotated `FormInputAnnotation.set(false)`) but the schema
312
+ * requires it. Without `omitHiddenFormFields` + `FactoryAnnotation`, the
313
+ * form's validator would reject Save and the popover would never close.
314
+ *
315
+ * Expected end-state after the fix:
316
+ * - new Note exists in DB with a synthesised `backing` ref
317
+ * - `notebook.notes[0]` references the new Note
318
+ * - the create form is gone
319
+ */
320
+ export const CreateHiddenFieldPlay: Story = {
321
+ render: NotebookStory,
322
+ decorators: notebookDecorators,
323
+ play: async ({ canvasElement }) => {
324
+ const canvas = within(canvasElement);
325
+ const body = within(document.body);
326
+
327
+ // Two "Add item" buttons (Tags + Notes) — Notes comes second.
328
+ const addButtons = await canvas.findAllByRole('button', { name: /add/i }, { timeout: 10_000 });
329
+ await userEvent.click(addButtons[addButtons.length - 1]);
330
+ await new Promise((resolve) => setTimeout(resolve, 250));
331
+
332
+ const comboboxes = await canvas.findAllByRole('combobox', undefined, { timeout: 10_000 });
333
+ await expect(comboboxes.length).toBeGreaterThan(0);
334
+ await userEvent.click(comboboxes[0]);
335
+
336
+ const search = await body.findByPlaceholderText(/search/i, undefined, { timeout: 5000 });
337
+ await userEvent.type(search, 'Ideas');
338
+
339
+ const createOption = await body.findByRole('option', { name: /create/i }, { timeout: 3000 });
340
+ await userEvent.click(createOption);
341
+
342
+ const form = await body.findByTestId('create-referenced-object-form', undefined, { timeout: 5000 });
343
+ const titleInput = await within(form).findByLabelText(/^title$/i);
344
+ await userEvent.clear(titleInput);
345
+ await userEvent.type(titleInput, 'Ideas');
346
+ await userEvent.click(await within(form).findByTestId('save-button'));
347
+
348
+ await new Promise((resolve) => setTimeout(resolve, 250));
349
+ },
350
+ };
@@ -0,0 +1,111 @@
1
+ //
2
+ // Copyright 2026 DXOS.org
3
+ //
4
+
5
+ import { composeStories } from '@storybook/react';
6
+ import { act, cleanup } from '@testing-library/react';
7
+ import { afterEach, describe, expect, test } from 'vitest';
8
+
9
+ import { Filter, Obj, Tag } from '@dxos/echo';
10
+ import { DXN, EID } from '@dxos/keys';
11
+
12
+ import { OBJECT_PROPERTIES_DEBUG_SYMBOL } from '../testing';
13
+ import * as stories from './ObjectProperties.stories';
14
+ import { type ObjectPropertiesDebug } from './ObjectProperties.stories';
15
+
16
+ const { CreateTagPlay, CreateRefArrayPlay, CreateHiddenFieldPlay } = composeStories(stories);
17
+
18
+ const getDebug = (): ObjectPropertiesDebug => {
19
+ const debug = (window as any)[OBJECT_PROPERTIES_DEBUG_SYMBOL] as ObjectPropertiesDebug | undefined;
20
+ expect(debug).toBeDefined();
21
+ return debug!;
22
+ };
23
+
24
+ /**
25
+ * Refs may be stored in local (`echo:/<id>`) or qualified (`echo://<space>/<id>`) form —
26
+ * extract and compare object id.
27
+ */
28
+ const refTargetsObject = (uri: string, objectId: string): boolean => {
29
+ const parsed = EID.tryParse(uri);
30
+ return parsed !== undefined && EID.getEntityId(parsed) === objectId;
31
+ };
32
+
33
+ describe('ObjectProperties — inline create flow', () => {
34
+ afterEach(async () => {
35
+ // Flush pending React scheduler work before teardown to prevent
36
+ // "window is not defined" errors from setImmediate callbacks firing after happy-dom cleanup.
37
+ await act(async () => {});
38
+ cleanup();
39
+ delete (window as any)[OBJECT_PROPERTIES_DEBUG_SYMBOL];
40
+ });
41
+
42
+ test('Tag.Tag: Create + Save persists tag and assigns it to the object', { timeout: 30_000 }, async () => {
43
+ await CreateTagPlay.run();
44
+ const { db, object } = getDebug();
45
+
46
+ // The new Tag should exist in the DB.
47
+ const tags = (await db.query(Filter.type(Tag.Tag)).run()) as Tag.Tag[];
48
+ const created = tags.find((tag) => tag.label === 'PinnedTag');
49
+ expect(created, 'new Tag.Tag with label "PinnedTag" should be in the database').toBeDefined();
50
+
51
+ // And `object.meta.tags` should contain a ref that resolves to that tag.
52
+ const meta = Obj.getMeta(object);
53
+ const createdId = (created as any).id as string;
54
+ const matched = meta.tags.some((ref) => refTargetsObject(ref.uri, createdId));
55
+ expect(matched, "meta.tags should contain a ref targeting the new tag's id").toBe(true);
56
+ });
57
+
58
+ test('non-Tag ref-array: Create + Save assigns the new ref to the array slot', { timeout: 30_000 }, async () => {
59
+ await CreateRefArrayPlay.run();
60
+ const { db, object } = getDebug();
61
+
62
+ // The new Author should exist in the DB.
63
+ const authors = (await db.query(Filter.type(DXN.make('org.dxos.test.author'))).run()) as any[];
64
+ const created = authors.find((author: any) => author.name === 'Ada Lovelace');
65
+ expect(created, 'new Author with name "Ada Lovelace" should be in the database').toBeDefined();
66
+
67
+ // And the article's authors array should contain a Ref to it. This is the
68
+ // assertion that fails before the fix — handleCreate persists the object
69
+ // but never wires the new ref into the array slot.
70
+ const article = object as any;
71
+ expect(article.authors ?? [], 'article.authors should contain a single Ref').toHaveLength(1);
72
+ const createdId = (created as any).id as string;
73
+ expect(
74
+ refTargetsObject(article.authors[0]?.uri ?? '', createdId),
75
+ 'article.authors[0] should reference the newly-created Author',
76
+ ).toBe(true);
77
+ });
78
+
79
+ test(
80
+ 'hidden required field via FactoryAnnotation: Create + Save closes form and assigns ref',
81
+ { timeout: 30_000 },
82
+ async () => {
83
+ await CreateHiddenFieldPlay.run();
84
+ const { db, object } = getDebug();
85
+
86
+ // The new Note should exist in the DB with the synthesised backing ref.
87
+ const notes = (await db.query(Filter.type(DXN.make('org.dxos.test.note'))).run()) as any[];
88
+ const created = notes.find((note: any) => note.title === 'Ideas');
89
+ expect(created, 'new Note with title "Ideas" should be in the database').toBeDefined();
90
+ expect(
91
+ (created as any).backing,
92
+ 'FactoryAnnotation should have populated the hidden `backing` ref',
93
+ ).toBeDefined();
94
+
95
+ // The notebook's notes array should reference the new Note.
96
+ const notebook = object as any;
97
+ expect(notebook.notes ?? [], 'notebook.notes should contain a single Ref').toHaveLength(1);
98
+ const createdId = (created as any).id as string;
99
+ expect(
100
+ refTargetsObject(notebook.notes[0]?.uri ?? '', createdId),
101
+ 'notebook.notes[0] should reference the newly-created Note',
102
+ ).toBe(true);
103
+
104
+ // The picker's create form should be gone.
105
+ expect(
106
+ document.querySelector('[data-testid="create-referenced-object-form"]'),
107
+ 'create form should be dismissed after a successful save',
108
+ ).toBeNull();
109
+ },
110
+ );
111
+ });