@dxos/react-ui-form 0.8.4-main.f9ba587 → 0.8.4-main.fcc0d83b33

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 (246) hide show
  1. package/README.md +0 -1
  2. package/dist/lib/browser/index.mjs +2787 -2137
  3. package/dist/lib/browser/index.mjs.map +4 -4
  4. package/dist/lib/browser/meta.json +1 -1
  5. package/dist/lib/browser/translations.mjs +82 -0
  6. package/dist/lib/browser/translations.mjs.map +7 -0
  7. package/dist/lib/node-esm/index.mjs +2787 -2137
  8. package/dist/lib/node-esm/index.mjs.map +4 -4
  9. package/dist/lib/node-esm/meta.json +1 -1
  10. package/dist/lib/node-esm/translations.mjs +84 -0
  11. package/dist/lib/node-esm/translations.mjs.map +7 -0
  12. package/dist/types/src/components/FieldEditor/FieldEditor.d.ts +9 -9
  13. package/dist/types/src/components/FieldEditor/FieldEditor.d.ts.map +1 -1
  14. package/dist/types/src/components/FieldEditor/FieldEditor.stories.d.ts +86 -7
  15. package/dist/types/src/components/FieldEditor/FieldEditor.stories.d.ts.map +1 -1
  16. package/dist/types/src/components/Form/Form.d.ts +109 -25
  17. package/dist/types/src/components/Form/Form.d.ts.map +1 -1
  18. package/dist/types/src/components/Form/Form.stories.d.ts +130 -56
  19. package/dist/types/src/components/Form/Form.stories.d.ts.map +1 -1
  20. package/dist/types/src/components/Form/FormField.d.ts +43 -0
  21. package/dist/types/src/components/Form/FormField.d.ts.map +1 -0
  22. package/dist/types/src/components/Form/FormFieldComponent.d.ts +79 -0
  23. package/dist/types/src/components/Form/FormFieldComponent.d.ts.map +1 -0
  24. package/dist/types/src/components/Form/FormFieldComponent.stories.d.ts +9 -0
  25. package/dist/types/src/components/Form/FormFieldComponent.stories.d.ts.map +1 -0
  26. package/dist/types/src/components/Form/FormFieldSet.d.ts +15 -0
  27. package/dist/types/src/components/Form/FormFieldSet.d.ts.map +1 -0
  28. package/dist/types/src/components/Form/fields/ArrayField.d.ts +18 -0
  29. package/dist/types/src/components/Form/fields/ArrayField.d.ts.map +1 -0
  30. package/dist/types/src/components/Form/fields/BooleanField.d.ts +4 -0
  31. package/dist/types/src/components/Form/fields/BooleanField.d.ts.map +1 -0
  32. package/dist/types/src/components/Form/fields/GeoPointField.d.ts +5 -0
  33. package/dist/types/src/components/Form/fields/GeoPointField.d.ts.map +1 -0
  34. package/dist/types/src/components/Form/fields/MarkdownField.d.ts +11 -0
  35. package/dist/types/src/components/Form/fields/MarkdownField.d.ts.map +1 -0
  36. package/dist/types/src/components/Form/fields/MarkdownField.stories.d.ts +83 -0
  37. package/dist/types/src/components/Form/fields/MarkdownField.stories.d.ts.map +1 -0
  38. package/dist/types/src/components/Form/fields/NumberField.d.ts +4 -0
  39. package/dist/types/src/components/Form/fields/NumberField.d.ts.map +1 -0
  40. package/dist/types/src/components/Form/fields/RefField.d.ts +22 -0
  41. package/dist/types/src/components/Form/fields/RefField.d.ts.map +1 -0
  42. package/dist/types/src/components/Form/fields/SelectField.d.ts +10 -0
  43. package/dist/types/src/components/Form/fields/SelectField.d.ts.map +1 -0
  44. package/dist/types/src/components/Form/fields/SelectOptionField.d.ts +5 -0
  45. package/dist/types/src/components/Form/fields/SelectOptionField.d.ts.map +1 -0
  46. package/dist/types/src/components/Form/fields/SelectOptionField.stories.d.ts +84 -0
  47. package/dist/types/src/components/Form/fields/SelectOptionField.stories.d.ts.map +1 -0
  48. package/dist/types/src/components/Form/fields/TextAreaField.d.ts +4 -0
  49. package/dist/types/src/components/Form/fields/TextAreaField.d.ts.map +1 -0
  50. package/dist/types/src/components/Form/fields/TextField.d.ts +4 -0
  51. package/dist/types/src/components/Form/fields/TextField.d.ts.map +1 -0
  52. package/dist/types/src/components/Form/fields/TupleField.d.ts +6 -0
  53. package/dist/types/src/components/Form/fields/TupleField.d.ts.map +1 -0
  54. package/dist/types/src/components/Form/fields/index.d.ts +12 -0
  55. package/dist/types/src/components/Form/fields/index.d.ts.map +1 -0
  56. package/dist/types/src/components/Form/index.d.ts +2 -4
  57. package/dist/types/src/components/Form/index.d.ts.map +1 -1
  58. package/dist/types/src/components/ObjectForm/ObjectForm.d.ts +9 -0
  59. package/dist/types/src/components/ObjectForm/ObjectForm.d.ts.map +1 -0
  60. package/dist/types/src/components/ObjectForm/index.d.ts +2 -0
  61. package/dist/types/src/components/ObjectForm/index.d.ts.map +1 -0
  62. package/dist/types/src/components/ObjectPicker/ObjectPicker.d.ts +68 -0
  63. package/dist/types/src/components/ObjectPicker/ObjectPicker.d.ts.map +1 -0
  64. package/dist/types/src/components/ObjectPicker/ObjectPicker.stories.d.ts +87 -0
  65. package/dist/types/src/components/ObjectPicker/ObjectPicker.stories.d.ts.map +1 -0
  66. package/dist/types/src/components/ObjectPicker/index.d.ts +2 -0
  67. package/dist/types/src/components/ObjectPicker/index.d.ts.map +1 -0
  68. package/dist/types/src/components/ObjectProperties/ObjectProperties.d.ts +9 -0
  69. package/dist/types/src/components/ObjectProperties/ObjectProperties.d.ts.map +1 -0
  70. package/dist/types/src/components/ObjectProperties/ObjectProperties.stories.d.ts +127 -0
  71. package/dist/types/src/components/ObjectProperties/ObjectProperties.stories.d.ts.map +1 -0
  72. package/dist/types/src/components/ObjectProperties/ObjectProperties.test.d.ts +2 -0
  73. package/dist/types/src/components/ObjectProperties/ObjectProperties.test.d.ts.map +1 -0
  74. package/dist/types/src/components/ObjectProperties/index.d.ts +2 -0
  75. package/dist/types/src/components/ObjectProperties/index.d.ts.map +1 -0
  76. package/dist/types/src/components/Settings/Settings.d.ts +51 -0
  77. package/dist/types/src/components/Settings/Settings.d.ts.map +1 -0
  78. package/dist/types/src/components/Settings/Settings.stories.d.ts +84 -0
  79. package/dist/types/src/components/Settings/Settings.stories.d.ts.map +1 -0
  80. package/dist/types/src/components/Settings/SettingsFieldSet.d.ts +43 -0
  81. package/dist/types/src/components/Settings/SettingsFieldSet.d.ts.map +1 -0
  82. package/dist/types/src/components/Settings/SettingsItem.d.ts +16 -0
  83. package/dist/types/src/components/Settings/SettingsItem.d.ts.map +1 -0
  84. package/dist/types/src/components/Settings/SettingsSelectField.d.ts +18 -0
  85. package/dist/types/src/components/Settings/SettingsSelectField.d.ts.map +1 -0
  86. package/dist/types/src/components/Settings/index.d.ts +4 -0
  87. package/dist/types/src/components/Settings/index.d.ts.map +1 -0
  88. package/dist/types/src/components/ViewEditor/ViewEditor.d.ts +22 -10
  89. package/dist/types/src/components/ViewEditor/ViewEditor.d.ts.map +1 -1
  90. package/dist/types/src/components/ViewEditor/ViewEditor.stories.d.ts +87 -9
  91. package/dist/types/src/components/ViewEditor/ViewEditor.stories.d.ts.map +1 -1
  92. package/dist/types/src/components/index.d.ts +4 -2
  93. package/dist/types/src/components/index.d.ts.map +1 -1
  94. package/dist/types/src/components/testing.d.ts +5 -4
  95. package/dist/types/src/components/testing.d.ts.map +1 -1
  96. package/dist/types/src/hooks/index.d.ts +2 -3
  97. package/dist/types/src/hooks/index.d.ts.map +1 -1
  98. package/dist/types/src/hooks/useFormHandler.d.ts +82 -0
  99. package/dist/types/src/hooks/useFormHandler.d.ts.map +1 -0
  100. package/dist/types/src/hooks/useKeyHandler.d.ts +7 -0
  101. package/dist/types/src/hooks/useKeyHandler.d.ts.map +1 -0
  102. package/dist/types/src/index.d.ts +0 -2
  103. package/dist/types/src/index.d.ts.map +1 -1
  104. package/dist/types/src/translations.d.ts +65 -54
  105. package/dist/types/src/translations.d.ts.map +1 -1
  106. package/dist/types/src/util/field-type.d.ts +19 -0
  107. package/dist/types/src/util/field-type.d.ts.map +1 -0
  108. package/dist/types/src/util/field.d.ts +3 -3
  109. package/dist/types/src/util/field.d.ts.map +1 -1
  110. package/dist/types/src/util/formatting.d.ts +2 -2
  111. package/dist/types/src/util/formatting.d.ts.map +1 -1
  112. package/dist/types/src/util/index.d.ts +2 -1
  113. package/dist/types/src/util/index.d.ts.map +1 -1
  114. package/dist/types/src/util/properties.d.ts +7 -0
  115. package/dist/types/src/util/properties.d.ts.map +1 -0
  116. package/dist/types/src/util/refs.d.ts +3 -3
  117. package/dist/types/src/util/refs.d.ts.map +1 -1
  118. package/dist/types/tsconfig.tsbuildinfo +1 -1
  119. package/package.json +57 -46
  120. package/src/components/FieldEditor/FieldEditor.stories.tsx +52 -31
  121. package/src/components/FieldEditor/FieldEditor.test.tsx +13 -13
  122. package/src/components/FieldEditor/FieldEditor.tsx +90 -95
  123. package/src/components/Form/AUDIT.md +17 -0
  124. package/src/components/Form/Form.stories.tsx +147 -223
  125. package/src/components/Form/Form.tsx +376 -95
  126. package/src/components/Form/FormField.tsx +306 -0
  127. package/src/components/Form/FormFieldComponent.stories.tsx +65 -0
  128. package/src/components/Form/FormFieldComponent.tsx +174 -0
  129. package/src/components/Form/FormFieldSet.tsx +144 -0
  130. package/src/components/Form/README.md +3 -0
  131. package/src/components/Form/fields/ArrayField.tsx +159 -0
  132. package/src/components/Form/fields/BooleanField.tsx +22 -0
  133. package/src/components/Form/fields/GeoPointField.tsx +121 -0
  134. package/src/components/Form/fields/MarkdownField.stories.tsx +152 -0
  135. package/src/components/Form/fields/MarkdownField.tsx +168 -0
  136. package/src/components/Form/fields/NumberField.tsx +39 -0
  137. package/src/components/Form/fields/RefField.tsx +229 -0
  138. package/src/components/Form/fields/SelectField.tsx +65 -0
  139. package/src/components/Form/fields/SelectOptionField.stories.tsx +66 -0
  140. package/src/components/Form/{custom/SelectOptionsInput.tsx → fields/SelectOptionField.tsx} +50 -45
  141. package/src/components/Form/fields/TextAreaField.tsx +38 -0
  142. package/src/components/Form/fields/TextField.tsx +38 -0
  143. package/src/components/Form/{custom/TupleInput.tsx → fields/TupleField.tsx} +17 -10
  144. package/src/components/Form/fields/index.ts +15 -0
  145. package/src/components/Form/index.ts +3 -4
  146. package/src/components/ObjectForm/ObjectForm.tsx +114 -0
  147. package/src/components/{ControlSection → ObjectForm}/index.ts +1 -1
  148. package/src/components/ObjectPicker/ObjectPicker.stories.tsx +212 -0
  149. package/src/components/ObjectPicker/ObjectPicker.tsx +194 -0
  150. package/src/components/ObjectPicker/index.ts +5 -0
  151. package/src/components/ObjectProperties/ObjectProperties.stories.tsx +362 -0
  152. package/src/components/ObjectProperties/ObjectProperties.test.tsx +101 -0
  153. package/src/components/ObjectProperties/ObjectProperties.tsx +141 -0
  154. package/src/components/ObjectProperties/index.ts +5 -0
  155. package/src/components/Settings/AUDIT.md +52 -0
  156. package/src/components/Settings/Settings.stories.tsx +104 -0
  157. package/src/components/Settings/Settings.tsx +82 -0
  158. package/src/components/Settings/SettingsFieldSet.tsx +257 -0
  159. package/src/components/Settings/SettingsItem.tsx +62 -0
  160. package/src/components/Settings/SettingsSelectField.tsx +59 -0
  161. package/src/components/Settings/index.ts +7 -0
  162. package/src/components/ViewEditor/ViewEditor.stories.tsx +145 -61
  163. package/src/components/ViewEditor/ViewEditor.test.tsx +22 -22
  164. package/src/components/ViewEditor/ViewEditor.tsx +355 -219
  165. package/src/components/index.ts +4 -3
  166. package/src/components/testing.tsx +31 -12
  167. package/src/hooks/index.ts +2 -3
  168. package/src/hooks/useFormHandler.ts +380 -0
  169. package/src/hooks/useKeyHandler.tsx +37 -0
  170. package/src/index.ts +0 -4
  171. package/src/translations.ts +70 -58
  172. package/src/util/field-type.ts +60 -0
  173. package/src/util/field.ts +33 -43
  174. package/src/util/formatting.ts +22 -22
  175. package/src/util/index.ts +2 -1
  176. package/src/util/properties.ts +18 -0
  177. package/src/util/refs.ts +17 -19
  178. package/dist/types/src/components/ControlSection/ControlSection.d.ts +0 -23
  179. package/dist/types/src/components/ControlSection/ControlSection.d.ts.map +0 -1
  180. package/dist/types/src/components/ControlSection/ControlSection.stories.d.ts +0 -8
  181. package/dist/types/src/components/ControlSection/ControlSection.stories.d.ts.map +0 -1
  182. package/dist/types/src/components/ControlSection/index.d.ts +0 -2
  183. package/dist/types/src/components/ControlSection/index.d.ts.map +0 -1
  184. package/dist/types/src/components/Form/ArrayField.d.ts +0 -16
  185. package/dist/types/src/components/Form/ArrayField.d.ts.map +0 -1
  186. package/dist/types/src/components/Form/Defaults.d.ts +0 -14
  187. package/dist/types/src/components/Form/Defaults.d.ts.map +0 -1
  188. package/dist/types/src/components/Form/FormActions.d.ts +0 -9
  189. package/dist/types/src/components/Form/FormActions.d.ts.map +0 -1
  190. package/dist/types/src/components/Form/FormContent.d.ts +0 -57
  191. package/dist/types/src/components/Form/FormContent.d.ts.map +0 -1
  192. package/dist/types/src/components/Form/FormContext.d.ts +0 -23
  193. package/dist/types/src/components/Form/FormContext.d.ts.map +0 -1
  194. package/dist/types/src/components/Form/Input.d.ts +0 -26
  195. package/dist/types/src/components/Form/Input.d.ts.map +0 -1
  196. package/dist/types/src/components/Form/RefField.d.ts +0 -12
  197. package/dist/types/src/components/Form/RefField.d.ts.map +0 -1
  198. package/dist/types/src/components/Form/custom/GeoPointInput.d.ts +0 -4
  199. package/dist/types/src/components/Form/custom/GeoPointInput.d.ts.map +0 -1
  200. package/dist/types/src/components/Form/custom/SelectOptionsInput.d.ts +0 -4
  201. package/dist/types/src/components/Form/custom/SelectOptionsInput.d.ts.map +0 -1
  202. package/dist/types/src/components/Form/custom/SelectOptionsInput.stories.d.ts +0 -7
  203. package/dist/types/src/components/Form/custom/SelectOptionsInput.stories.d.ts.map +0 -1
  204. package/dist/types/src/components/Form/custom/TupleInput.d.ts +0 -6
  205. package/dist/types/src/components/Form/custom/TupleInput.d.ts.map +0 -1
  206. package/dist/types/src/components/Form/custom/index.d.ts +0 -4
  207. package/dist/types/src/components/Form/custom/index.d.ts.map +0 -1
  208. package/dist/types/src/components/Form/factory.d.ts +0 -8
  209. package/dist/types/src/components/Form/factory.d.ts.map +0 -1
  210. package/dist/types/src/components/deprecated/Form.d.ts +0 -15
  211. package/dist/types/src/components/deprecated/Form.d.ts.map +0 -1
  212. package/dist/types/src/components/deprecated/Form.stories.d.ts +0 -13
  213. package/dist/types/src/components/deprecated/Form.stories.d.ts.map +0 -1
  214. package/dist/types/src/components/deprecated/FormInput.d.ts +0 -16
  215. package/dist/types/src/components/deprecated/FormInput.d.ts.map +0 -1
  216. package/dist/types/src/components/deprecated/index.d.ts +0 -2
  217. package/dist/types/src/components/deprecated/index.d.ts.map +0 -1
  218. package/dist/types/src/hooks/useForm.d.ts +0 -67
  219. package/dist/types/src/hooks/useForm.d.ts.map +0 -1
  220. package/dist/types/src/hooks/useQueryRefOptions.d.ts +0 -23
  221. package/dist/types/src/hooks/useQueryRefOptions.d.ts.map +0 -1
  222. package/dist/types/src/hooks/useRefQueryLookupHandler.d.ts +0 -8
  223. package/dist/types/src/hooks/useRefQueryLookupHandler.d.ts.map +0 -1
  224. package/dist/types/src/util/schema.d.ts +0 -3
  225. package/dist/types/src/util/schema.d.ts.map +0 -1
  226. package/src/components/ControlSection/ControlSection.stories.tsx +0 -41
  227. package/src/components/ControlSection/ControlSection.tsx +0 -139
  228. package/src/components/Form/ArrayField.tsx +0 -124
  229. package/src/components/Form/Defaults.tsx +0 -203
  230. package/src/components/Form/FormActions.tsx +0 -53
  231. package/src/components/Form/FormContent.tsx +0 -240
  232. package/src/components/Form/FormContext.tsx +0 -110
  233. package/src/components/Form/Input.tsx +0 -51
  234. package/src/components/Form/RefField.tsx +0 -213
  235. package/src/components/Form/custom/GeoPointInput.tsx +0 -72
  236. package/src/components/Form/custom/SelectOptionsInput.stories.tsx +0 -61
  237. package/src/components/Form/custom/index.ts +0 -7
  238. package/src/components/Form/factory.tsx +0 -31
  239. package/src/components/deprecated/Form.stories.tsx +0 -62
  240. package/src/components/deprecated/Form.tsx +0 -119
  241. package/src/components/deprecated/FormInput.tsx +0 -71
  242. package/src/components/deprecated/index.ts +0 -5
  243. package/src/hooks/useForm.ts +0 -291
  244. package/src/hooks/useQueryRefOptions.ts +0 -53
  245. package/src/hooks/useRefQueryLookupHandler.ts +0 -39
  246. package/src/util/schema.ts +0 -14
@@ -0,0 +1,229 @@
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, useSchema as defaultUseSchema } from '@dxos/echo-react';
10
+ import { ReferenceAnnotationId, type ReferenceAnnotationValue } from '@dxos/echo/internal';
11
+ import { findAnnotation } from '@dxos/effect';
12
+ import { DXN } 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.getDXN(result).toString();
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 === Type.getTypename(Obj.Unknown)
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
+ useSchema?: (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
+ placeholder,
72
+ layout,
73
+ getStatus,
74
+ getValue,
75
+ createOptionLabel,
76
+ createOptionIcon,
77
+ createInitialValuePath,
78
+ createFieldMap,
79
+ db,
80
+ useSchema = defaultUseSchema,
81
+ useResults = defaultUseResults,
82
+ getOptions = defaultGetOptions,
83
+ onCreate,
84
+ onValueChange,
85
+ } = props;
86
+ const { t } = useTranslation(translationKey);
87
+ const { status, error } = getStatus();
88
+
89
+ const typename = useMemo(
90
+ () => (type ? findAnnotation<ReferenceAnnotationValue>(type, ReferenceAnnotationId)?.typename : undefined),
91
+ [type],
92
+ );
93
+
94
+ const results = useResults(db, typename);
95
+ const options = useMemo(() => {
96
+ const parentLabel = type ? findAnnotation<boolean>(type, ParentLabelAnnotationId) === true : false;
97
+ return getOptions(results, { parentLabel });
98
+ }, [results, getOptions, type]);
99
+
100
+ const handleGetValue = useCallback(() => {
101
+ const formValue = getValue();
102
+ // Match form-value Refs against options by the bare object id (last DXN
103
+ // segment), not by full DXN string. Ref.make uses
104
+ // `DXN.fromLocalObjectId(id)` (`dxn:echo:@:<id>`), but
105
+ // `Entity.getDXN(obj)` on a registered object produces the space-scoped
106
+ // form (`dxn:echo:<spaceId>:<id>`). String-comparing the two never
107
+ // matches, so the just-created Ref's option lookup fails and the slot
108
+ // displays as empty even though the underlying form value IS set.
109
+ const dxnToObjectId = (dxn: string): string => dxn.split(':').pop() ?? dxn;
110
+
111
+ const unknownToRefOption = (value: unknown) => {
112
+ const isRef = Ref.isRef(value);
113
+ if (isRef || isRefSnapshot(value)) {
114
+ const dxnString = isRef ? value.dxn.toString() : value['/'];
115
+ const objectId = dxnToObjectId(dxnString);
116
+ const matchingOption = options.find((option) => dxnToObjectId(option.id) === objectId);
117
+ if (matchingOption) {
118
+ return matchingOption;
119
+ }
120
+ }
121
+
122
+ return undefined;
123
+ };
124
+
125
+ return unknownToRefOption(formValue);
126
+ }, [options, getValue]);
127
+
128
+ const item = handleGetValue();
129
+ const selectedIds = useMemo(() => (item ? [item.id] : []), [item]);
130
+ const createSchema = useSchema(db, typename);
131
+
132
+ // Lift the popover open state so we can dismiss it after a successful create.
133
+ const [open, setOpen] = useState(false);
134
+
135
+ const handleCreate = useCallback(
136
+ async (values: any) => {
137
+ if (!createSchema || !onCreate) {
138
+ return;
139
+ }
140
+ const newObject = await onCreate(createSchema, values);
141
+ if (newObject) {
142
+ // Wire the newly-created object into this slot's form value. ArrayField
143
+ // owns the array; this RefField represents a single slot, so writing a
144
+ // Ref via `onValueChange` populates that slot.
145
+ onValueChange(type, Ref.make(newObject));
146
+ }
147
+ setOpen(false);
148
+ },
149
+ [createSchema, onCreate, type, onValueChange],
150
+ );
151
+
152
+ const handleUpdate = useCallback(
153
+ (id: string | undefined) => {
154
+ const item = options.find((option) => option.id === id);
155
+ const ref = item ? Ref.fromDXN(DXN.parse(item.id)) : undefined;
156
+ onValueChange(type, ref);
157
+ },
158
+ [options, type, onValueChange],
159
+ );
160
+
161
+ const handleSelect = useCallback(
162
+ (id: string) => {
163
+ if (item?.id === id) {
164
+ handleUpdate(undefined);
165
+ } else {
166
+ handleUpdate(id);
167
+ }
168
+ },
169
+ [item, handleUpdate],
170
+ );
171
+
172
+ if (!typename || ((readonly || layout === 'static') && !item)) {
173
+ return null;
174
+ }
175
+
176
+ return (
177
+ <Input.Root validationValence={status}>
178
+ {layout !== 'inline' && <FormFieldLabel error={error} readonly={readonly} label={label} />}
179
+ <div>
180
+ {readonly ? (
181
+ !item ? (
182
+ <p className='text-description mb-2'>{t('empty-readonly-ref-field.label')}</p>
183
+ ) : (
184
+ <DxAnchor key={item.id} dxn={item.id} rootclassname='me-1'>
185
+ {item.label}
186
+ </DxAnchor>
187
+ )
188
+ ) : (
189
+ <ObjectPicker.Root open={open} onOpenChange={setOpen}>
190
+ <ObjectPicker.Trigger asChild classNames='p-0'>
191
+ {item ? (
192
+ <div role='none' className='flex gap-form-gap w-full'>
193
+ <Input.Root key={item.id}>
194
+ <Input.TextInput value={item.label} readOnly classNames='w-full' />
195
+ </Input.Root>
196
+ </div>
197
+ ) : (
198
+ <Button classNames='w-full text-start gap-form-gap'>
199
+ <div role='none' className='grow overflow-hidden'>
200
+ <span className='truncate text-description'>{placeholder ?? t('ref-field.placeholder')}</span>
201
+ </div>
202
+ <Icon size={3} icon='ph--caret-down--bold' />
203
+ </Button>
204
+ )}
205
+ </ObjectPicker.Trigger>
206
+ <ObjectPicker.Portal>
207
+ <ObjectPicker.Content
208
+ classNames='dx-card-popover-width'
209
+ options={options}
210
+ selectedIds={selectedIds}
211
+ // Strip hidden (`FormInputAnnotation.set(false)`) fields so the
212
+ // form's validator doesn't reject required-but-hidden fields
213
+ // such as backing-object refs supplied by a `FactoryAnnotation`.
214
+ createSchema={createSchema && omitHiddenFormFields(omitId(createSchema))}
215
+ createOptionLabel={createOptionLabel}
216
+ createOptionIcon={createOptionIcon}
217
+ createInitialValuePath={createInitialValuePath}
218
+ createFieldMap={createFieldMap}
219
+ onCreate={handleCreate}
220
+ onSelect={handleSelect}
221
+ />
222
+ </ObjectPicker.Portal>
223
+ </ObjectPicker.Root>
224
+ )}
225
+ </div>
226
+ {layout === 'full' && <Input.DescriptionAndValidation>{error}</Input.DescriptionAndValidation>}
227
+ </Input.Root>
228
+ );
229
+ };
@@ -0,0 +1,65 @@
1
+ //
2
+ // Copyright 2024 DXOS.org
3
+ //
4
+
5
+ import React, { useCallback } from 'react';
6
+
7
+ import { Input, Select, type SelectRootProps } from '@dxos/react-ui';
8
+
9
+ import { type FormFieldComponentProps, FormFieldLabel } from '../FormFieldComponent';
10
+
11
+ export type SelectFieldOptions = FormFieldComponentProps & {
12
+ options?: Array<{ value: string | number; label?: string }>;
13
+ };
14
+
15
+ export const SelectField = ({
16
+ type,
17
+ readonly,
18
+ label,
19
+ layout,
20
+ placeholder,
21
+ options,
22
+ getStatus,
23
+ getValue,
24
+ onValueChange,
25
+ }: SelectFieldOptions) => {
26
+ const { status, error } = getStatus();
27
+ const value = getValue() as string | undefined;
28
+
29
+ const handleValueChange = useCallback<NonNullable<SelectRootProps['onValueChange']>>(
30
+ (value) => onValueChange(type, value),
31
+ [type, onValueChange],
32
+ );
33
+
34
+ if ((readonly || layout === 'static') && value == null) {
35
+ return null;
36
+ }
37
+
38
+ return (
39
+ <Input.Root validationValence={status}>
40
+ {layout !== 'inline' && <FormFieldLabel error={error} readonly={readonly} label={label} />}
41
+ {layout === 'static' ? (
42
+ <p>{options?.find(({ value: optionValue }) => optionValue === value)?.label ?? String(value)}</p>
43
+ ) : (
44
+ <Select.Root value={value} onValueChange={handleValueChange}>
45
+ {/* TODO(burdon): Placeholder not working? */}
46
+ <Select.TriggerButton classNames='w-full' disabled={!!readonly} placeholder={placeholder} />
47
+ <Select.Portal>
48
+ <Select.Content>
49
+ <Select.Viewport>
50
+ {options?.map(({ value, label }) => (
51
+ // NOTE: Numeric values are converted to and from strings.
52
+ <Select.Option key={String(value)} value={String(value)}>
53
+ {label ?? String(value)}
54
+ </Select.Option>
55
+ ))}
56
+ </Select.Viewport>
57
+ <Select.Arrow />
58
+ </Select.Content>
59
+ </Select.Portal>
60
+ </Select.Root>
61
+ )}
62
+ {layout === 'full' && <Input.DescriptionAndValidation>{error}</Input.DescriptionAndValidation>}
63
+ </Input.Root>
64
+ );
65
+ };
@@ -0,0 +1,66 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import { type Meta, type StoryObj } from '@storybook/react-vite';
6
+ import * as Schema from 'effect/Schema';
7
+ import React, { useState } from 'react';
8
+
9
+ import { SelectOption } from '@dxos/echo/internal';
10
+ import { withLayout, withTheme } from '@dxos/react-ui/testing';
11
+
12
+ import { translations } from '#translations';
13
+
14
+ import { TestLayout } from '../../testing';
15
+ import { Form } from '../Form';
16
+ import { SelectOptionField } from './SelectOptionField';
17
+
18
+ const schema = Schema.Struct({
19
+ options: Schema.Array(SelectOption).pipe(Schema.mutable),
20
+ }).pipe(Schema.mutable);
21
+
22
+ const DefaultStory = () => {
23
+ const [values, setValues] = useState<{ options: SelectOption[] }>({
24
+ // TODO(Zaymon): Pick colors from our limited set.
25
+ options: [
26
+ { id: 'opt_1', title: 'First option', color: 'amber' },
27
+ { id: 'opt_2', title: 'Second option', color: 'emerald' },
28
+ { id: 'opt_3', title: 'Third option', color: 'rose' },
29
+ ],
30
+ });
31
+
32
+ return (
33
+ <TestLayout>
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>
47
+ </TestLayout>
48
+ );
49
+ };
50
+
51
+ const meta = {
52
+ title: 'ui/react-ui-form/SelectOptionField',
53
+ component: SelectOptionField as any,
54
+ render: DefaultStory,
55
+ decorators: [withTheme(), withLayout({ layout: 'fullscreen' })],
56
+ parameters: {
57
+ layout: 'fullscreen',
58
+ translations,
59
+ },
60
+ } satisfies Meta<typeof DefaultStory>;
61
+
62
+ export default meta;
63
+
64
+ type Story = StoryObj<typeof meta>;
65
+
66
+ export const Default: Story = {};
@@ -2,33 +2,40 @@
2
2
  // Copyright 2025 DXOS.org
3
3
  //
4
4
 
5
- import React, { type ChangeEvent, type KeyboardEvent, useCallback, useEffect, useState, useRef } from 'react';
5
+ import React, { type ChangeEvent, type KeyboardEvent, useCallback, useEffect, useRef, useState } from 'react';
6
6
 
7
- import { type SelectOption } from '@dxos/echo-schema';
7
+ import { type SelectOption } from '@dxos/echo/internal';
8
8
  import { PublicKey } from '@dxos/keys';
9
- import { type ChromaticPalette, Icon, IconButton, Input, Tag, Toolbar, useTranslation } from '@dxos/react-ui';
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, mx } from '@dxos/react-ui-theme';
12
+ import { hues, osTranslations, subtleHover } 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
- // TODO(ZaymonFC): Handle disabled.
18
- export const SelectOptionInput = ({ type, label, disabled, getStatus, getValue, onValueChange }: InputProps) => {
16
+ import { type FormFieldComponentProps, FormFieldLabel } from '../FormFieldComponent';
17
+
18
+ export const SelectOptionField = ({
19
+ type,
20
+ readonly,
21
+ label,
22
+ getStatus,
23
+ getValue,
24
+ onValueChange,
25
+ }: FormFieldComponentProps<SelectOption[] | undefined>) => {
26
+ const { t } = useTranslation(translationKey);
27
+ const { status, error } = getStatus();
19
28
  const [selected, setSelectedId] = useState<string | null>(null);
20
29
  const [isNewOption, setIsNewOption] = useState(false);
21
30
  const inputRef = useRef<HTMLInputElement>(null);
22
- const { t } = useTranslation(translationKey);
23
- const { status, error } = getStatus();
24
- const options = getValue<SelectOption[] | undefined>();
31
+ const options = getValue();
25
32
 
26
33
  // Initialization.
27
34
  useEffect(() => {
28
35
  if (options === undefined) {
29
36
  onValueChange(type, []);
30
37
  }
31
- }, [options, onValueChange, type]);
38
+ }, [options, type, onValueChange]);
32
39
 
33
40
  const randomHue = () => {
34
41
  const usedHues = new Set(options?.map((option) => option.color) ?? []);
@@ -89,8 +96,8 @@ export const SelectOptionInput = ({ type, label, disabled, getStatus, getValue,
89
96
  [options, type, onValueChange],
90
97
  );
91
98
 
92
- const handleKeyDown = useCallback((e: KeyboardEvent<HTMLInputElement>) => {
93
- if (e.key === 'Enter') {
99
+ const handleKeyDown = useCallback((ev: KeyboardEvent<HTMLInputElement>) => {
100
+ if (ev.key === 'Enter') {
94
101
  setSelectedId(null);
95
102
  }
96
103
  }, []);
@@ -112,10 +119,15 @@ export const SelectOptionInput = ({ type, label, disabled, getStatus, getValue,
112
119
 
113
120
  return (
114
121
  <Input.Root validationValence={status}>
115
- <InputHeader error={error} label={label} />
122
+ <FormFieldLabel error={error} readonly={readonly} label={label} />
116
123
  <div role='none'>
117
124
  {options && (
118
- <List.Root items={options} isItem={(item) => true} onMove={handleMove}>
125
+ <List.Root
126
+ items={options}
127
+ isItem={(_item) => true}
128
+ onMove={readonly ? undefined : handleMove}
129
+ readonly={!!readonly}
130
+ >
119
131
  {({ items }) => (
120
132
  <div role='list' className='w-full overflow-auto'>
121
133
  {items?.map((item) => (
@@ -123,45 +135,42 @@ export const SelectOptionInput = ({ type, label, disabled, getStatus, getValue,
123
135
  <List.Item
124
136
  role='button'
125
137
  item={item}
126
- classNames={mx('p-1 plb-2', 'flex flex-col', 'cursor-pointer', 'hover:bg-hoverOverlay')}
138
+ classNames={[subtleHover, 'flex flex-col cursor-pointer rounded-xs']}
127
139
  aria-expanded={selected === item.id}
128
140
  >
129
141
  <div className='flex items-center'>
130
- <List.ItemDragHandle />
142
+ <List.ItemDragHandle disabled={!!readonly} />
131
143
  <List.ItemTitle onClick={() => handleClick(item.id)} classNames='flex-1'>
132
144
  {/* TODO(ZaymonFC): Move spacer into Tag component. */}
133
145
  <Tag palette={item.color as ChromaticPalette}>{item.title || '\u200b'}</Tag>
134
146
  </List.ItemTitle>
135
- <Icon
147
+ <IconButton
148
+ iconOnly
149
+ variant='ghost'
150
+ label={t(selected === item.id ? 'collapse label' : 'expand label', { ns: osTranslations })}
136
151
  icon={selected === item.id ? 'ph--caret-down--regular' : 'ph--caret-right--regular'}
137
152
  onClick={() => handleClick(item.id)}
138
153
  />
139
154
  </div>
140
155
  {selected === item.id && (
141
- <div className='mis-4 mbs-2 flex flex-col gap-1'>
142
- {/* 16px to match drag handle width. */}
143
- <Input.Label classNames='text-xs'>{t('select option label')}</Input.Label>
144
- <div className='flex flex-row items-center gap-1'>
156
+ <div className='flex flex-col p-form-padding gap-form-gap dx-density-fine'>
157
+ <Input.Label classNames='text-xs'>{t('select-option.label')}</Input.Label>
158
+ <div className='grid grid-cols-[1fr_min-content_min-content] gap-form-gap'>
145
159
  <Input.TextInput
146
- placeholder={t('select option label placeholder')}
160
+ disabled={!!readonly}
161
+ placeholder={t('select-option-label.placeholder')}
147
162
  ref={selected === item.id ? inputRef : undefined}
148
163
  value={item.title}
149
164
  onChange={handleTitleChange(item.id)}
150
165
  onKeyDown={handleKeyDown}
151
166
  classNames='flex-1'
152
- data-no-submit
153
167
  />
154
- <Toolbar.Root classNames='p-0 m-0 !is-auto'>
155
- <HuePicker
156
- value={item.color}
157
- onChange={handleColorChange(item.id)}
158
- rootVariant='toolbar-button'
159
- />
160
- </Toolbar.Root>
168
+ <HuePicker disabled={!!readonly} value={item.color} onChange={handleColorChange(item.id)} />
161
169
  <IconButton
170
+ disabled={!!readonly}
162
171
  icon='ph--trash--fill'
163
172
  iconOnly
164
- label={t('select option delete')}
173
+ label={t('select-option-delete.button')}
165
174
  onClick={() => handleDelete(item.id)}
166
175
  />
167
176
  </div>
@@ -170,18 +179,14 @@ export const SelectOptionInput = ({ type, label, disabled, getStatus, getValue,
170
179
  </List.Item>
171
180
  </div>
172
181
  ))}
173
- <div role='listitem'>
174
- <div
175
- role='button'
176
- className={mx('p-1 plb-2', 'flex flex-col', 'cursor-pointer', 'hover:bg-hoverOverlay')}
177
- onClick={handleAdd}
178
- >
179
- <div className='flex items-center gap-1'>
180
- <Icon icon='ph--plus--regular' />
181
- <span className='text-sm'>{t('select option add')}</span>
182
- </div>
183
- </div>
184
- </div>
182
+ <IconButton
183
+ variant='ghost'
184
+ icon='ph--plus--regular'
185
+ label={t('select-option-add.button')}
186
+ onClick={handleAdd}
187
+ disabled={!!readonly}
188
+ classNames='w-full'
189
+ />
185
190
  </div>
186
191
  )}
187
192
  </List.Root>
@@ -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={3}
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,46 @@
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
+ readonly,
18
19
  label,
19
- disabled,
20
20
  getStatus,
21
21
  getValue,
22
22
  onValueChange,
23
23
  onBlur,
24
- }: InputProps & { binding: string[] }) => {
24
+ }: FormFieldComponentProps<Record<string, number>> & {
25
+ binding: string[];
26
+ }) => {
25
27
  const { status, error } = getStatus();
26
28
 
27
29
  // TODO(burdon): Generalize number/float/string, etc.
28
- const values: Record<string, number> = getValue<Record<string, number>>() ?? {};
30
+ const values: Record<string, number> = getValue() ?? {};
29
31
 
30
32
  return (
31
33
  <Input.Root validationValence={status}>
32
- <InputHeader error={error} label={label} />
33
- <div className={mx('grid gap-2', gridCols[binding.length - 1])}>
34
+ <FormFieldLabel error={error} readonly={readonly} label={label} />
35
+ <div className={mx('grid gap-form-gap', gridCols[binding.length - 1])}>
34
36
  {binding.map((prop) => (
35
37
  <Input.TextInput
36
38
  key={prop}
37
39
  type='number'
38
- disabled={disabled}
40
+ disabled={!!readonly}
39
41
  value={values[prop]}
40
- onChange={(event) => onValueChange(type, { ...values, [prop]: safeParseFloat(event.target.value, 0) })}
42
+ onChange={(event) => {
43
+ onValueChange(type, {
44
+ ...values,
45
+ [prop]: safeParseFloat(event.target.value, 0),
46
+ });
47
+ }}
41
48
  onBlur={onBlur}
42
49
  />
43
50
  ))}
@@ -0,0 +1,15 @@
1
+ //
2
+ // Copyright 2024 DXOS.org
3
+ //
4
+
5
+ export * from './ArrayField';
6
+ export * from './BooleanField';
7
+ export * from './GeoPointField';
8
+ export * from './MarkdownField';
9
+ export * from './NumberField';
10
+ export * from './RefField';
11
+ export * from './SelectField';
12
+ export * from './SelectOptionField';
13
+ export * from './TextField';
14
+ export * from './TextAreaField';
15
+ export * from './TupleField';