@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,232 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import React, { useCallback, useMemo, useState } from 'react';
6
+
7
+ import '@dxos/lit-ui/dx-tag-picker.pcss';
8
+ import { type Database, Entity, Filter, Obj, Ref, Type } from '@dxos/echo';
9
+ import { useQuery, useType as defaultUseType } from '@dxos/echo-react';
10
+ import { ANY_OBJECT_TYPENAME, ReferenceAnnotationId, type ReferenceAnnotationValue } from '@dxos/echo/internal';
11
+ import { findAnnotation } from '@dxos/effect';
12
+ import { URI } from '@dxos/keys';
13
+ import { DxAnchor } from '@dxos/lit-ui/react';
14
+ import { Button, Icon, Input, useTranslation } from '@dxos/react-ui';
15
+ import { ParentLabelAnnotationId } from '@dxos/schema';
16
+
17
+ import { translationKey } from '#translations';
18
+
19
+ import { ObjectPicker, type ObjectPickerContentProps, type RefOption } from '../../ObjectPicker';
20
+ import { omitHiddenFormFields, omitId } from '../Form';
21
+ import { type FormFieldComponentProps, FormFieldLabel } from '../FormFieldComponent';
22
+
23
+ // TODO(burdon): Factor out.
24
+ const isRefSnapshot = (val: any): val is { '/': string } => {
25
+ return typeof val === 'object' && typeof (val as any)?.['/'] === 'string';
26
+ };
27
+
28
+ const defaultGetOptions: NonNullable<RefFieldProps['getOptions']> = (results, { parentLabel } = {}) =>
29
+ results.map((result) => {
30
+ const id = Entity.getURI(result);
31
+ const parent = parentLabel ? Obj.getParent(result as Obj.Unknown) : undefined;
32
+ const label = parent ? Entity.getLabel(parent) : Entity.getLabel(result);
33
+ return { id, label: label ?? id };
34
+ });
35
+
36
+ const defaultUseResults: NonNullable<RefFieldProps['useResults']> = (db, typename) =>
37
+ useQuery(
38
+ db,
39
+ typename
40
+ ? // For Ref.Ref(Obj.Unknown) we want to show all objects.
41
+ typename === ANY_OBJECT_TYPENAME
42
+ ? Filter.everything()
43
+ : Filter.typename(typename)
44
+ : Filter.nothing(),
45
+ );
46
+
47
+ export type RefFieldProps = FormFieldComponentProps &
48
+ Pick<
49
+ ObjectPickerContentProps,
50
+ 'createOptionLabel' | 'createOptionIcon' | 'createInitialValuePath' | 'createFieldMap'
51
+ > & {
52
+ db?: Database.Database;
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[];
56
+ getOptions?: (objects: Entity.Any[], options?: { parentLabel?: boolean }) => RefOption[];
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;
64
+ };
65
+
66
+ export const RefField = (props: RefFieldProps) => {
67
+ const {
68
+ type,
69
+ readonly,
70
+ label,
71
+ jsonPath,
72
+ placeholder,
73
+ layout,
74
+ getStatus,
75
+ getValue,
76
+ createOptionLabel,
77
+ createOptionIcon,
78
+ createInitialValuePath,
79
+ createFieldMap,
80
+ db,
81
+ useType = defaultUseType,
82
+ useResults = defaultUseResults,
83
+ getOptions = defaultGetOptions,
84
+ onCreate,
85
+ onValueChange,
86
+ } = props;
87
+ const { t } = useTranslation(translationKey);
88
+ const { status, error } = getStatus();
89
+
90
+ const typename = useMemo(
91
+ () => (type ? findAnnotation<ReferenceAnnotationValue>(type, ReferenceAnnotationId)?.typename : undefined),
92
+ [type],
93
+ );
94
+
95
+ const results = useResults(db, typename);
96
+ const options = useMemo(() => {
97
+ const parentLabel = type ? findAnnotation<boolean>(type, ParentLabelAnnotationId) === true : false;
98
+ return getOptions(results, { parentLabel });
99
+ }, [results, getOptions, type]);
100
+
101
+ const handleGetValue = useCallback(() => {
102
+ const formValue = getValue();
103
+ // Match form-value Refs against options by the bare object id (last DXN
104
+ // segment), not by full DXN string. Ref.make uses
105
+ // `DXN.fromLocalObjectId(id)` (`dxn:echo:@:<id>`), but
106
+ // `Entity.getURI(obj)` on a registered object produces the space-scoped
107
+ // form (`dxn:echo:<spaceId>:<id>`). String-comparing the two never
108
+ // matches, so the just-created Ref's option lookup fails and the slot
109
+ // displays as empty even though the underlying form value IS set.
110
+ const dxnToObjectId = (dxn: string): string => dxn.split(':').pop() ?? dxn;
111
+
112
+ const unknownToRefOption = (value: unknown) => {
113
+ const isRef = Ref.isRef(value);
114
+ if (isRef || isRefSnapshot(value)) {
115
+ const dxnString = isRef ? value.uri : value['/'];
116
+ const objectId = dxnToObjectId(dxnString);
117
+ const matchingOption = options.find((option) => dxnToObjectId(option.id) === objectId);
118
+ if (matchingOption) {
119
+ return matchingOption;
120
+ }
121
+ }
122
+
123
+ return undefined;
124
+ };
125
+
126
+ return unknownToRefOption(formValue);
127
+ }, [options, getValue]);
128
+
129
+ const item = handleGetValue();
130
+ const selectedIds = useMemo(() => (item ? [item.id] : []), [item]);
131
+ const createSchema = useType(db, typename);
132
+
133
+ // Lift the popover open state so we can dismiss it after a successful create.
134
+ const [open, setOpen] = useState(false);
135
+
136
+ const handleCreate = useCallback(
137
+ async (values: any) => {
138
+ if (!createSchema || !onCreate) {
139
+ return;
140
+ }
141
+ const newObject = await onCreate(createSchema, values);
142
+ if (newObject) {
143
+ // Wire the newly-created object into this slot's form value. ArrayField
144
+ // owns the array; this RefField represents a single slot, so writing a
145
+ // Ref via `onValueChange` populates that slot.
146
+ onValueChange(type, Ref.make(newObject));
147
+ }
148
+ setOpen(false);
149
+ },
150
+ [createSchema, onCreate, type, onValueChange],
151
+ );
152
+
153
+ const handleUpdate = useCallback(
154
+ (id: string | undefined) => {
155
+ const item = options.find((option) => option.id === id);
156
+ const ref = item ? Ref.fromURI(URI.make(item.id)) : undefined;
157
+ onValueChange(type, ref);
158
+ },
159
+ [options, type, onValueChange],
160
+ );
161
+
162
+ const handleSelect = useCallback(
163
+ (id: string) => {
164
+ if (item?.id === id) {
165
+ handleUpdate(undefined);
166
+ } else {
167
+ handleUpdate(id);
168
+ }
169
+ },
170
+ [item, handleUpdate],
171
+ );
172
+
173
+ if (!typename || ((readonly || layout === 'static') && !item)) {
174
+ return null;
175
+ }
176
+
177
+ return (
178
+ <Input.Root validationValence={status}>
179
+ {layout !== 'inline' && <FormFieldLabel error={error} readonly={readonly} label={label} path={jsonPath} />}
180
+ <div>
181
+ {readonly ? (
182
+ !item ? (
183
+ <p className='text-description mb-2'>{t('empty-readonly-ref-field.label')}</p>
184
+ ) : (
185
+ <DxAnchor key={item.id} dxn={item.id} rootclassname='me-1'>
186
+ {item.label}
187
+ </DxAnchor>
188
+ )
189
+ ) : (
190
+ <ObjectPicker.Root open={open} onOpenChange={setOpen}>
191
+ <ObjectPicker.Trigger asChild classNames='p-0'>
192
+ {item ? (
193
+ <div className='flex gap-form-gap w-full'>
194
+ <Input.Root key={item.id}>
195
+ <Input.TextInput value={item.label} readOnly classNames='w-full' />
196
+ </Input.Root>
197
+ </div>
198
+ ) : (
199
+ <Button classNames='w-full text-start gap-form-gap'>
200
+ <div className='grow overflow-hidden'>
201
+ <span className='truncate text-description'>
202
+ {placeholder || label || t('ref-field.placeholder')}
203
+ </span>
204
+ </div>
205
+ <Icon size={3} icon='ph--caret-down--bold' />
206
+ </Button>
207
+ )}
208
+ </ObjectPicker.Trigger>
209
+ <ObjectPicker.Portal>
210
+ <ObjectPicker.Content
211
+ classNames='dx-card-popover-width'
212
+ options={options}
213
+ selectedIds={selectedIds}
214
+ // Strip hidden (`FormInputAnnotation.set(false)`) fields so the
215
+ // form's validator doesn't reject required-but-hidden fields
216
+ // such as backing-object refs supplied by a `FactoryAnnotation`.
217
+ createSchema={createSchema && omitHiddenFormFields(omitId(Type.getSchema(createSchema)))}
218
+ createOptionLabel={createOptionLabel}
219
+ createOptionIcon={createOptionIcon}
220
+ createInitialValuePath={createInitialValuePath}
221
+ createFieldMap={createFieldMap}
222
+ onCreate={handleCreate}
223
+ onSelect={handleSelect}
224
+ />
225
+ </ObjectPicker.Portal>
226
+ </ObjectPicker.Root>
227
+ )}
228
+ </div>
229
+ {layout === 'full' && <Input.DescriptionAndValidation>{error}</Input.DescriptionAndValidation>}
230
+ </Input.Root>
231
+ );
232
+ };
@@ -0,0 +1,77 @@
1
+ //
2
+ // Copyright 2024 DXOS.org
3
+ //
4
+
5
+ import React, { useCallback } from 'react';
6
+
7
+ import { Icon, Input, Select, type SelectRootProps } from '@dxos/react-ui';
8
+ import { getStyles } from '@dxos/ui-theme';
9
+
10
+ import { type FormFieldComponentProps, FormFieldLabel } from '../FormFieldComponent';
11
+
12
+ export type SelectFieldOptions = FormFieldComponentProps & {
13
+ options?: Array<{ value: string | number; label?: string; secondaryLabel?: string; icon?: string; iconHue?: string }>;
14
+ };
15
+
16
+ export const SelectField = ({
17
+ type,
18
+ readonly,
19
+ label,
20
+ jsonPath,
21
+ layout,
22
+ placeholder,
23
+ options,
24
+ getStatus,
25
+ getValue,
26
+ onValueChange,
27
+ }: SelectFieldOptions) => {
28
+ const { status, error } = getStatus();
29
+ const value = getValue() as string | undefined;
30
+
31
+ const handleValueChange = useCallback<NonNullable<SelectRootProps['onValueChange']>>(
32
+ (value) => onValueChange(type, value),
33
+ [type, onValueChange],
34
+ );
35
+
36
+ if ((readonly || layout === 'static') && value == null) {
37
+ return null;
38
+ }
39
+
40
+ return (
41
+ <Input.Root validationValence={status}>
42
+ {layout !== 'inline' && <FormFieldLabel error={error} readonly={readonly} label={label} path={jsonPath} />}
43
+ {layout === 'static' ? (
44
+ <p>{options?.find(({ value: optionValue }) => optionValue === value)?.label ?? String(value)}</p>
45
+ ) : (
46
+ <Select.Root value={value} onValueChange={handleValueChange}>
47
+ {/* TODO(burdon): Placeholder not working? */}
48
+ <Select.TriggerButton classNames='w-full' disabled={!!readonly} placeholder={placeholder} />
49
+ <Select.Portal>
50
+ <Select.Content>
51
+ <Select.Viewport>
52
+ {options?.map(({ value, label, secondaryLabel, icon, iconHue }) => (
53
+ // NOTE: Numeric values are converted to and from strings.
54
+ <Select.Option key={String(value)} value={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>
60
+ </Select.Option>
61
+ ))}
62
+ </Select.Viewport>
63
+ <Select.Arrow />
64
+ </Select.Content>
65
+ </Select.Portal>
66
+ </Select.Root>
67
+ )}
68
+ {layout === 'full' && <Input.DescriptionAndValidation>{error}</Input.DescriptionAndValidation>}
69
+ </Input.Root>
70
+ );
71
+ };
72
+
73
+ const getIconHueStyles = (iconHue?: string): string | undefined => {
74
+ const styles = iconHue ? getStyles(iconHue) : undefined;
75
+
76
+ return styles?.foreground;
77
+ };
@@ -6,17 +6,17 @@ import { type Meta, type StoryObj } from '@storybook/react-vite';
6
6
  import * as Schema from 'effect/Schema';
7
7
  import React, { useState } from 'react';
8
8
 
9
- import { type SelectOption, SelectOptionSchema } from '@dxos/echo/internal';
10
- import { withTheme } from '@dxos/react-ui/testing';
9
+ import { SelectOption } from '@dxos/echo/internal';
10
+ import { withLayout, withTheme } from '@dxos/react-ui/testing';
11
11
 
12
- import { translations } from '../../../translations';
13
- import { TestLayout, TestPanel } from '../../testing';
14
- import { Form } from '../Form';
12
+ import { translations } from '#translations';
15
13
 
16
- import { SelectOptionInput } from './SelectOptionsInput';
14
+ import { TestLayout } from '../../testing';
15
+ import { Form } from '../Form';
16
+ import { SelectOptionField } from './SelectOptionField';
17
17
 
18
18
  const schema = Schema.Struct({
19
- options: Schema.Array(SelectOptionSchema).pipe(Schema.mutable),
19
+ options: Schema.Array(SelectOption).pipe(Schema.mutable),
20
20
  }).pipe(Schema.mutable);
21
21
 
22
22
  const DefaultStory = () => {
@@ -31,23 +31,28 @@ const DefaultStory = () => {
31
31
 
32
32
  return (
33
33
  <TestLayout>
34
- <TestPanel>
35
- <Form
36
- schema={schema}
37
- values={values}
38
- onSave={(vals) => setValues(vals)}
39
- Custom={{ options: SelectOptionInput }}
40
- />
41
- </TestPanel>
34
+ <Form.Root
35
+ schema={schema}
36
+ values={values}
37
+ fieldMap={{ options: SelectOptionField }}
38
+ onSave={(values) => setValues(values)}
39
+ >
40
+ <Form.Viewport>
41
+ <Form.Content>
42
+ <Form.FieldSet />
43
+ <Form.Actions />
44
+ </Form.Content>
45
+ </Form.Viewport>
46
+ </Form.Root>
42
47
  </TestLayout>
43
48
  );
44
49
  };
45
50
 
46
51
  const meta = {
47
- title: 'ui/react-ui-form/SelectOptionsInput',
48
- component: SelectOptionInput as any,
52
+ title: 'ui/react-ui-form/SelectOptionField',
53
+ component: SelectOptionField as any,
49
54
  render: DefaultStory,
50
- decorators: [withTheme],
55
+ decorators: [withTheme(), withLayout({ layout: 'fullscreen' })],
51
56
  parameters: {
52
57
  layout: 'fullscreen',
53
58
  translations,
@@ -9,25 +9,34 @@ 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, subtleHover } from '@dxos/react-ui-theme';
12
+ import { hues, osTranslations } from '@dxos/ui-theme';
13
13
 
14
- import { translationKey } from '../../../translations';
15
- import { InputHeader, type InputProps } from '../Input';
14
+ import { translationKey } from '#translations';
16
15
 
17
- export const SelectOptionInput = ({ type, label, readonly, getStatus, getValue, onValueChange }: InputProps) => {
16
+ import { type FormFieldComponentProps, FormFieldLabel } from '../FormFieldComponent';
17
+
18
+ export const SelectOptionField = ({
19
+ type,
20
+ readonly,
21
+ label,
22
+ jsonPath,
23
+ getStatus,
24
+ getValue,
25
+ onValueChange,
26
+ }: FormFieldComponentProps<SelectOption[] | undefined>) => {
27
+ const { t } = useTranslation(translationKey);
28
+ const { status, error } = getStatus();
18
29
  const [selected, setSelectedId] = useState<string | null>(null);
19
30
  const [isNewOption, setIsNewOption] = useState(false);
20
31
  const inputRef = useRef<HTMLInputElement>(null);
21
- const { t } = useTranslation(translationKey);
22
- const { status, error } = getStatus();
23
- const options = getValue<SelectOption[] | undefined>();
32
+ const options = getValue();
24
33
 
25
34
  // Initialization.
26
35
  useEffect(() => {
27
36
  if (options === undefined) {
28
37
  onValueChange(type, []);
29
38
  }
30
- }, [options, onValueChange, type]);
39
+ }, [options, type, onValueChange]);
31
40
 
32
41
  const randomHue = () => {
33
42
  const usedHues = new Set(options?.map((option) => option.color) ?? []);
@@ -88,8 +97,8 @@ export const SelectOptionInput = ({ type, label, readonly, getStatus, getValue,
88
97
  [options, type, onValueChange],
89
98
  );
90
99
 
91
- const handleKeyDown = useCallback((e: KeyboardEvent<HTMLInputElement>) => {
92
- if (e.key === 'Enter') {
100
+ const handleKeyDown = useCallback((ev: KeyboardEvent<HTMLInputElement>) => {
101
+ if (ev.key === 'Enter') {
93
102
  setSelectedId(null);
94
103
  }
95
104
  }, []);
@@ -111,12 +120,12 @@ export const SelectOptionInput = ({ type, label, readonly, getStatus, getValue,
111
120
 
112
121
  return (
113
122
  <Input.Root validationValence={status}>
114
- <InputHeader error={error} label={label} />
115
- <div role='none'>
123
+ <FormFieldLabel error={error} readonly={readonly} label={label} path={jsonPath} />
124
+ <div>
116
125
  {options && (
117
126
  <List.Root
118
127
  items={options}
119
- isItem={(item) => true}
128
+ isItem={(_item) => true}
120
129
  onMove={readonly ? undefined : handleMove}
121
130
  readonly={!!readonly}
122
131
  >
@@ -127,7 +136,7 @@ export const SelectOptionInput = ({ type, label, readonly, getStatus, getValue,
127
136
  <List.Item
128
137
  role='button'
129
138
  item={item}
130
- classNames={[subtleHover, 'flex flex-col cursor-pointer rounded-sm']}
139
+ classNames='flex flex-col cursor-pointer rounded-xs dx-hover'
131
140
  aria-expanded={selected === item.id}
132
141
  >
133
142
  <div className='flex items-center'>
@@ -139,32 +148,30 @@ export const SelectOptionInput = ({ type, label, readonly, getStatus, getValue,
139
148
  <IconButton
140
149
  iconOnly
141
150
  variant='ghost'
142
- label={t(selected === item.id ? 'collapse label' : 'expand label', { ns: 'os' })}
151
+ label={t(selected === item.id ? 'collapse.label' : 'expand.label', { ns: osTranslations })}
143
152
  icon={selected === item.id ? 'ph--caret-down--regular' : 'ph--caret-right--regular'}
144
153
  onClick={() => handleClick(item.id)}
145
154
  />
146
155
  </div>
147
156
  {selected === item.id && (
148
- <div className='pli-cardSpacingInline plb-cardSpacingBlock flex flex-col gap-1 density-fine'>
149
- {/* 16px to match drag handle width. */}
150
- <Input.Label classNames='text-xs'>{t('select option label')}</Input.Label>
151
- <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'>
152
160
  <Input.TextInput
153
161
  disabled={!!readonly}
154
- placeholder={t('select option label placeholder')}
162
+ placeholder={t('select-option-label.placeholder')}
155
163
  ref={selected === item.id ? inputRef : undefined}
156
164
  value={item.title}
157
165
  onChange={handleTitleChange(item.id)}
158
166
  onKeyDown={handleKeyDown}
159
167
  classNames='flex-1'
160
- data-no-submit
161
168
  />
162
169
  <HuePicker disabled={!!readonly} value={item.color} onChange={handleColorChange(item.id)} />
163
170
  <IconButton
164
171
  disabled={!!readonly}
165
172
  icon='ph--trash--fill'
166
173
  iconOnly
167
- label={t('select option delete')}
174
+ label={t('select-option-delete.button')}
168
175
  onClick={() => handleDelete(item.id)}
169
176
  />
170
177
  </div>
@@ -176,10 +183,10 @@ export const SelectOptionInput = ({ type, label, readonly, getStatus, getValue,
176
183
  <IconButton
177
184
  variant='ghost'
178
185
  icon='ph--plus--regular'
179
- label={t('select option add')}
186
+ label={t('select-option-add.button')}
180
187
  onClick={handleAdd}
181
188
  disabled={!!readonly}
182
- classNames='is-full'
189
+ classNames='w-full'
183
190
  />
184
191
  </div>
185
192
  )}
@@ -0,0 +1,38 @@
1
+ //
2
+ // Copyright 2024 DXOS.org
3
+ //
4
+
5
+ import React, { useCallback } from 'react';
6
+
7
+ import { Input, type TextAreaProps } from '@dxos/react-ui';
8
+
9
+ import { type FormFieldComponentProps, FormFieldWrapper } from '../FormFieldComponent';
10
+
11
+ export const TextAreaField = ({
12
+ type,
13
+ readonly,
14
+ placeholder,
15
+ onValueChange,
16
+ onBlur,
17
+ ...props
18
+ }: FormFieldComponentProps<string>) => {
19
+ const handleChange = useCallback<NonNullable<TextAreaProps['onChange']>>(
20
+ (event) => onValueChange(type, event.target.value),
21
+ [type, onValueChange],
22
+ );
23
+
24
+ return (
25
+ <FormFieldWrapper<string> readonly={readonly} {...props}>
26
+ {({ value = '' }) => (
27
+ <Input.TextArea
28
+ rows={5}
29
+ disabled={!!readonly}
30
+ placeholder={placeholder}
31
+ value={value}
32
+ onChange={handleChange}
33
+ onBlur={onBlur}
34
+ />
35
+ )}
36
+ </FormFieldWrapper>
37
+ );
38
+ };
@@ -0,0 +1,38 @@
1
+ //
2
+ // Copyright 2024 DXOS.org
3
+ //
4
+
5
+ import React, { useCallback } from 'react';
6
+
7
+ import { Input, type TextInputProps } from '@dxos/react-ui';
8
+
9
+ import { type FormFieldComponentProps, FormFieldWrapper } from '../FormFieldComponent';
10
+
11
+ export const TextField = ({
12
+ type,
13
+ readonly,
14
+ placeholder,
15
+ onBlur,
16
+ onValueChange,
17
+ ...props
18
+ }: FormFieldComponentProps<string>) => {
19
+ const handleChange = useCallback<NonNullable<TextInputProps['onChange']>>(
20
+ (event) => onValueChange(type, event.target.value),
21
+ [type, onValueChange],
22
+ );
23
+
24
+ return (
25
+ <FormFieldWrapper<string> readonly={readonly} {...props}>
26
+ {({ value = '' }) => (
27
+ <Input.TextInput
28
+ noAutoFill
29
+ disabled={!!readonly}
30
+ placeholder={placeholder}
31
+ value={value}
32
+ onBlur={onBlur}
33
+ onChange={handleChange}
34
+ />
35
+ )}
36
+ </FormFieldWrapper>
37
+ );
38
+ };
@@ -5,39 +5,47 @@
5
5
  import React from 'react';
6
6
 
7
7
  import { Input } from '@dxos/react-ui';
8
- import { mx } from '@dxos/react-ui-theme';
8
+ import { mx } from '@dxos/ui-theme';
9
9
  import { safeParseFloat } from '@dxos/util';
10
10
 
11
- import { InputHeader, type InputProps } from '../Input';
11
+ import { type FormFieldComponentProps, FormFieldLabel } from '../FormFieldComponent';
12
12
 
13
13
  const gridCols = ['grid-cols-1', 'grid-cols-2', 'grid-cols-3', 'grid-cols-4'];
14
14
 
15
- export const TupleInput = ({
15
+ export const TupleField = ({
16
16
  binding,
17
17
  type,
18
- label,
19
18
  readonly,
19
+ label,
20
+ jsonPath,
20
21
  getStatus,
21
22
  getValue,
22
23
  onValueChange,
23
24
  onBlur,
24
- }: InputProps & { binding: string[] }) => {
25
+ }: FormFieldComponentProps<Record<string, number>> & {
26
+ binding: string[];
27
+ }) => {
25
28
  const { status, error } = getStatus();
26
29
 
27
30
  // TODO(burdon): Generalize number/float/string, etc.
28
- const values: Record<string, number> = getValue<Record<string, number>>() ?? {};
31
+ const values: Record<string, number> = getValue() ?? {};
29
32
 
30
33
  return (
31
34
  <Input.Root validationValence={status}>
32
- <InputHeader error={error} label={label} />
33
- <div className={mx('grid gap-2', gridCols[binding.length - 1])}>
35
+ <FormFieldLabel error={error} readonly={readonly} label={label} path={jsonPath} />
36
+ <div className={mx('grid gap-form-gap', gridCols[binding.length - 1])}>
34
37
  {binding.map((prop) => (
35
38
  <Input.TextInput
36
39
  key={prop}
37
40
  type='number'
38
41
  disabled={!!readonly}
39
42
  value={values[prop]}
40
- onChange={(event) => onValueChange(type, { ...values, [prop]: safeParseFloat(event.target.value, 0) })}
43
+ onChange={(event) => {
44
+ onValueChange(type, {
45
+ ...values,
46
+ [prop]: safeParseFloat(event.target.value, 0),
47
+ });
48
+ }}
41
49
  onBlur={onBlur}
42
50
  />
43
51
  ))}
@@ -0,0 +1,16 @@
1
+ //
2
+ // Copyright 2024 DXOS.org
3
+ //
4
+
5
+ export * from './ArrayField';
6
+ export * from './BooleanField';
7
+ export * from './DateField';
8
+ export * from './GeoPointField';
9
+ export * from './MarkdownField';
10
+ export * from './NumberField';
11
+ export * from './RefField';
12
+ export * from './SelectField';
13
+ export * from './SelectOptionField';
14
+ export * from './TextField';
15
+ export * from './TextAreaField';
16
+ export * from './TupleField';
@@ -2,8 +2,9 @@
2
2
  // Copyright 2024 DXOS.org
3
3
  //
4
4
 
5
- export * from './custom';
6
- export * from './Defaults';
5
+ export * from './fields';
6
+
7
7
  export * from './Form';
8
- export * from './FormContext';
9
- export * from './Input';
8
+ export * from './FormFieldComponent';
9
+ export * from './FormTooltipsContext';
10
+ export * from './Layout';