@dxos/react-ui-form 0.8.4-main.406dc2a → 0.8.4-main.422d1c7879

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 (219) hide show
  1. package/README.md +0 -1
  2. package/dist/lib/browser/index.mjs +2856 -2309
  3. package/dist/lib/browser/index.mjs.map +4 -4
  4. package/dist/lib/browser/meta.json +1 -1
  5. package/dist/lib/node-esm/index.mjs +2856 -2309
  6. package/dist/lib/node-esm/index.mjs.map +4 -4
  7. package/dist/lib/node-esm/meta.json +1 -1
  8. package/dist/types/src/components/FieldEditor/FieldEditor.d.ts +8 -7
  9. package/dist/types/src/components/FieldEditor/FieldEditor.d.ts.map +1 -1
  10. package/dist/types/src/components/FieldEditor/FieldEditor.stories.d.ts +68 -60
  11. package/dist/types/src/components/FieldEditor/FieldEditor.stories.d.ts.map +1 -1
  12. package/dist/types/src/components/Form/Form.d.ts +93 -26
  13. package/dist/types/src/components/Form/Form.d.ts.map +1 -1
  14. package/dist/types/src/components/Form/Form.stories.d.ts +114 -137
  15. package/dist/types/src/components/Form/Form.stories.d.ts.map +1 -1
  16. package/dist/types/src/components/Form/FormField.d.ts +43 -0
  17. package/dist/types/src/components/Form/FormField.d.ts.map +1 -0
  18. package/dist/types/src/components/Form/FormFieldComponent.d.ts +80 -0
  19. package/dist/types/src/components/Form/FormFieldComponent.d.ts.map +1 -0
  20. package/dist/types/src/components/Form/FormFieldComponent.stories.d.ts +9 -0
  21. package/dist/types/src/components/Form/FormFieldComponent.stories.d.ts.map +1 -0
  22. package/dist/types/src/components/Form/FormFieldSet.d.ts +18 -0
  23. package/dist/types/src/components/Form/FormFieldSet.d.ts.map +1 -0
  24. package/dist/types/src/components/Form/fields/ArrayField.d.ts +18 -0
  25. package/dist/types/src/components/Form/fields/ArrayField.d.ts.map +1 -0
  26. package/dist/types/src/components/Form/fields/BooleanField.d.ts +4 -0
  27. package/dist/types/src/components/Form/fields/BooleanField.d.ts.map +1 -0
  28. package/dist/types/src/components/Form/fields/GeoPointField.d.ts +5 -0
  29. package/dist/types/src/components/Form/fields/GeoPointField.d.ts.map +1 -0
  30. package/dist/types/src/components/Form/fields/MarkdownField.d.ts +11 -0
  31. package/dist/types/src/components/Form/fields/MarkdownField.d.ts.map +1 -0
  32. package/dist/types/src/components/Form/fields/MarkdownField.stories.d.ts +83 -0
  33. package/dist/types/src/components/Form/fields/MarkdownField.stories.d.ts.map +1 -0
  34. package/dist/types/src/components/Form/fields/NumberField.d.ts +4 -0
  35. package/dist/types/src/components/Form/fields/NumberField.d.ts.map +1 -0
  36. package/dist/types/src/components/Form/fields/RefField.d.ts +22 -0
  37. package/dist/types/src/components/Form/fields/RefField.d.ts.map +1 -0
  38. package/dist/types/src/components/Form/fields/SelectField.d.ts +10 -0
  39. package/dist/types/src/components/Form/fields/SelectField.d.ts.map +1 -0
  40. package/dist/types/src/components/Form/fields/SelectOptionField.d.ts +5 -0
  41. package/dist/types/src/components/Form/fields/SelectOptionField.d.ts.map +1 -0
  42. package/dist/types/src/components/Form/fields/SelectOptionField.stories.d.ts +84 -0
  43. package/dist/types/src/components/Form/fields/SelectOptionField.stories.d.ts.map +1 -0
  44. package/dist/types/src/components/Form/fields/TextAreaField.d.ts +4 -0
  45. package/dist/types/src/components/Form/fields/TextAreaField.d.ts.map +1 -0
  46. package/dist/types/src/components/Form/fields/TextField.d.ts +4 -0
  47. package/dist/types/src/components/Form/fields/TextField.d.ts.map +1 -0
  48. package/dist/types/src/components/Form/fields/TupleField.d.ts +6 -0
  49. package/dist/types/src/components/Form/fields/TupleField.d.ts.map +1 -0
  50. package/dist/types/src/components/Form/fields/index.d.ts +12 -0
  51. package/dist/types/src/components/Form/fields/index.d.ts.map +1 -0
  52. package/dist/types/src/components/Form/index.d.ts +2 -4
  53. package/dist/types/src/components/Form/index.d.ts.map +1 -1
  54. package/dist/types/src/components/ObjectForm/ObjectForm.d.ts +9 -0
  55. package/dist/types/src/components/ObjectForm/ObjectForm.d.ts.map +1 -0
  56. package/dist/types/src/components/ObjectForm/index.d.ts +2 -0
  57. package/dist/types/src/components/ObjectForm/index.d.ts.map +1 -0
  58. package/dist/types/src/components/ObjectPicker/ObjectPicker.d.ts +37 -10
  59. package/dist/types/src/components/ObjectPicker/ObjectPicker.d.ts.map +1 -1
  60. package/dist/types/src/components/ObjectPicker/ObjectPicker.stories.d.ts +64 -59
  61. package/dist/types/src/components/ObjectPicker/ObjectPicker.stories.d.ts.map +1 -1
  62. package/dist/types/src/components/ObjectProperties/ObjectProperties.d.ts +9 -0
  63. package/dist/types/src/components/ObjectProperties/ObjectProperties.d.ts.map +1 -0
  64. package/dist/types/src/components/ObjectProperties/ObjectProperties.stories.d.ts +127 -0
  65. package/dist/types/src/components/ObjectProperties/ObjectProperties.stories.d.ts.map +1 -0
  66. package/dist/types/src/components/ObjectProperties/ObjectProperties.test.d.ts +2 -0
  67. package/dist/types/src/components/ObjectProperties/ObjectProperties.test.d.ts.map +1 -0
  68. package/dist/types/src/components/ObjectProperties/index.d.ts +2 -0
  69. package/dist/types/src/components/ObjectProperties/index.d.ts.map +1 -0
  70. package/dist/types/src/components/Settings/Settings.d.ts +43 -0
  71. package/dist/types/src/components/Settings/Settings.d.ts.map +1 -0
  72. package/dist/types/src/components/Settings/Settings.stories.d.ts +84 -0
  73. package/dist/types/src/components/Settings/Settings.stories.d.ts.map +1 -0
  74. package/dist/types/src/components/Settings/SettingsFieldSet.d.ts +38 -0
  75. package/dist/types/src/components/Settings/SettingsFieldSet.d.ts.map +1 -0
  76. package/dist/types/src/components/Settings/SettingsItem.d.ts +17 -0
  77. package/dist/types/src/components/Settings/SettingsItem.d.ts.map +1 -0
  78. package/dist/types/src/components/Settings/index.d.ts +3 -0
  79. package/dist/types/src/components/Settings/index.d.ts.map +1 -0
  80. package/dist/types/src/components/ViewEditor/ViewEditor.d.ts +13 -15
  81. package/dist/types/src/components/ViewEditor/ViewEditor.d.ts.map +1 -1
  82. package/dist/types/src/components/ViewEditor/ViewEditor.stories.d.ts +70 -64
  83. package/dist/types/src/components/ViewEditor/ViewEditor.stories.d.ts.map +1 -1
  84. package/dist/types/src/components/index.d.ts +3 -1
  85. package/dist/types/src/components/index.d.ts.map +1 -1
  86. package/dist/types/src/components/testing.d.ts +4 -6
  87. package/dist/types/src/components/testing.d.ts.map +1 -1
  88. package/dist/types/src/hooks/index.d.ts +2 -3
  89. package/dist/types/src/hooks/index.d.ts.map +1 -1
  90. package/dist/types/src/hooks/useFormHandler.d.ts +82 -0
  91. package/dist/types/src/hooks/useFormHandler.d.ts.map +1 -0
  92. package/dist/types/src/hooks/useKeyHandler.d.ts +7 -0
  93. package/dist/types/src/hooks/useKeyHandler.d.ts.map +1 -0
  94. package/dist/types/src/translations.d.ts +65 -60
  95. package/dist/types/src/translations.d.ts.map +1 -1
  96. package/dist/types/src/util/field-type.d.ts +19 -0
  97. package/dist/types/src/util/field-type.d.ts.map +1 -0
  98. package/dist/types/src/util/field.d.ts +3 -3
  99. package/dist/types/src/util/field.d.ts.map +1 -1
  100. package/dist/types/src/util/formatting.d.ts +2 -2
  101. package/dist/types/src/util/formatting.d.ts.map +1 -1
  102. package/dist/types/src/util/index.d.ts +2 -1
  103. package/dist/types/src/util/index.d.ts.map +1 -1
  104. package/dist/types/src/util/properties.d.ts +7 -0
  105. package/dist/types/src/util/properties.d.ts.map +1 -0
  106. package/dist/types/src/util/refs.d.ts +2 -2
  107. package/dist/types/src/util/refs.d.ts.map +1 -1
  108. package/dist/types/tsconfig.tsbuildinfo +1 -1
  109. package/package.json +47 -50
  110. package/src/components/FieldEditor/FieldEditor.stories.tsx +43 -25
  111. package/src/components/FieldEditor/FieldEditor.test.tsx +0 -1
  112. package/src/components/FieldEditor/FieldEditor.tsx +74 -78
  113. package/src/components/Form/AUDIT.md +17 -0
  114. package/src/components/Form/Form.stories.tsx +137 -250
  115. package/src/components/Form/Form.tsx +375 -107
  116. package/src/components/Form/FormField.tsx +305 -0
  117. package/src/components/Form/FormFieldComponent.stories.tsx +65 -0
  118. package/src/components/Form/FormFieldComponent.tsx +174 -0
  119. package/src/components/Form/FormFieldSet.tsx +144 -0
  120. package/src/components/Form/README.md +3 -0
  121. package/src/components/Form/fields/ArrayField.tsx +158 -0
  122. package/src/components/Form/fields/BooleanField.tsx +22 -0
  123. package/src/components/Form/fields/GeoPointField.tsx +120 -0
  124. package/src/components/Form/fields/MarkdownField.stories.tsx +151 -0
  125. package/src/components/Form/fields/MarkdownField.tsx +167 -0
  126. package/src/components/Form/fields/NumberField.tsx +39 -0
  127. package/src/components/Form/fields/RefField.tsx +228 -0
  128. package/src/components/Form/fields/SelectField.tsx +65 -0
  129. package/src/components/Form/{custom/SelectOptionsInput.stories.tsx → fields/SelectOptionField.stories.tsx} +21 -17
  130. package/src/components/Form/{custom/SelectOptionsInput.tsx → fields/SelectOptionField.tsx} +28 -23
  131. package/src/components/Form/fields/TextAreaField.tsx +38 -0
  132. package/src/components/Form/fields/TextField.tsx +38 -0
  133. package/src/components/Form/{custom/TupleInput.tsx → fields/TupleField.tsx} +16 -9
  134. package/src/components/Form/fields/index.ts +15 -0
  135. package/src/components/Form/index.ts +3 -4
  136. package/src/components/ObjectForm/ObjectForm.tsx +113 -0
  137. package/src/components/{ControlSection → ObjectForm}/index.ts +1 -1
  138. package/src/components/ObjectPicker/ObjectPicker.stories.tsx +23 -21
  139. package/src/components/ObjectPicker/ObjectPicker.tsx +112 -80
  140. package/src/components/ObjectProperties/ObjectProperties.stories.tsx +361 -0
  141. package/src/components/ObjectProperties/ObjectProperties.test.tsx +101 -0
  142. package/src/components/ObjectProperties/ObjectProperties.tsx +140 -0
  143. package/src/components/ObjectProperties/index.ts +5 -0
  144. package/src/components/Settings/Settings.stories.tsx +103 -0
  145. package/src/components/Settings/Settings.tsx +81 -0
  146. package/src/components/Settings/SettingsFieldSet.tsx +211 -0
  147. package/src/components/Settings/SettingsItem.tsx +62 -0
  148. package/src/components/Settings/index.ts +6 -0
  149. package/src/components/ViewEditor/ViewEditor.stories.tsx +53 -50
  150. package/src/components/ViewEditor/ViewEditor.test.tsx +6 -6
  151. package/src/components/ViewEditor/ViewEditor.tsx +296 -206
  152. package/src/components/index.ts +3 -1
  153. package/src/components/testing.tsx +24 -11
  154. package/src/hooks/index.ts +2 -3
  155. package/src/hooks/useFormHandler.ts +380 -0
  156. package/src/hooks/useKeyHandler.tsx +37 -0
  157. package/src/translations.ts +70 -65
  158. package/src/util/field-type.ts +57 -0
  159. package/src/util/field.ts +25 -25
  160. package/src/util/formatting.ts +22 -22
  161. package/src/util/index.ts +2 -1
  162. package/src/util/properties.ts +18 -0
  163. package/src/util/refs.ts +16 -18
  164. package/dist/types/src/components/ControlSection/ControlSection.d.ts +0 -23
  165. package/dist/types/src/components/ControlSection/ControlSection.d.ts.map +0 -1
  166. package/dist/types/src/components/ControlSection/ControlSection.stories.d.ts +0 -84
  167. package/dist/types/src/components/ControlSection/ControlSection.stories.d.ts.map +0 -1
  168. package/dist/types/src/components/ControlSection/index.d.ts +0 -2
  169. package/dist/types/src/components/ControlSection/index.d.ts.map +0 -1
  170. package/dist/types/src/components/Form/ArrayField.d.ts +0 -16
  171. package/dist/types/src/components/Form/ArrayField.d.ts.map +0 -1
  172. package/dist/types/src/components/Form/Defaults.d.ts +0 -14
  173. package/dist/types/src/components/Form/Defaults.d.ts.map +0 -1
  174. package/dist/types/src/components/Form/FormActions.d.ts +0 -9
  175. package/dist/types/src/components/Form/FormActions.d.ts.map +0 -1
  176. package/dist/types/src/components/Form/FormContent.d.ts +0 -63
  177. package/dist/types/src/components/Form/FormContent.d.ts.map +0 -1
  178. package/dist/types/src/components/Form/FormContext.d.ts +0 -23
  179. package/dist/types/src/components/Form/FormContext.d.ts.map +0 -1
  180. package/dist/types/src/components/Form/Input.d.ts +0 -29
  181. package/dist/types/src/components/Form/Input.d.ts.map +0 -1
  182. package/dist/types/src/components/Form/RefField.d.ts +0 -22
  183. package/dist/types/src/components/Form/RefField.d.ts.map +0 -1
  184. package/dist/types/src/components/Form/custom/GeoPointInput.d.ts +0 -4
  185. package/dist/types/src/components/Form/custom/GeoPointInput.d.ts.map +0 -1
  186. package/dist/types/src/components/Form/custom/SelectOptionsInput.d.ts +0 -4
  187. package/dist/types/src/components/Form/custom/SelectOptionsInput.d.ts.map +0 -1
  188. package/dist/types/src/components/Form/custom/SelectOptionsInput.stories.d.ts +0 -79
  189. package/dist/types/src/components/Form/custom/SelectOptionsInput.stories.d.ts.map +0 -1
  190. package/dist/types/src/components/Form/custom/TupleInput.d.ts +0 -6
  191. package/dist/types/src/components/Form/custom/TupleInput.d.ts.map +0 -1
  192. package/dist/types/src/components/Form/custom/index.d.ts +0 -4
  193. package/dist/types/src/components/Form/custom/index.d.ts.map +0 -1
  194. package/dist/types/src/components/Form/factory.d.ts +0 -8
  195. package/dist/types/src/components/Form/factory.d.ts.map +0 -1
  196. package/dist/types/src/hooks/useForm.d.ts +0 -67
  197. package/dist/types/src/hooks/useForm.d.ts.map +0 -1
  198. package/dist/types/src/hooks/useQueryRefOptions.d.ts +0 -29
  199. package/dist/types/src/hooks/useQueryRefOptions.d.ts.map +0 -1
  200. package/dist/types/src/hooks/useRefQueryLookupHandler.d.ts +0 -8
  201. package/dist/types/src/hooks/useRefQueryLookupHandler.d.ts.map +0 -1
  202. package/dist/types/src/util/schema.d.ts +0 -3
  203. package/dist/types/src/util/schema.d.ts.map +0 -1
  204. package/src/components/ControlSection/ControlSection.stories.tsx +0 -38
  205. package/src/components/ControlSection/ControlSection.tsx +0 -153
  206. package/src/components/Form/ArrayField.tsx +0 -125
  207. package/src/components/Form/Defaults.tsx +0 -205
  208. package/src/components/Form/FormActions.tsx +0 -54
  209. package/src/components/Form/FormContent.tsx +0 -322
  210. package/src/components/Form/FormContext.tsx +0 -110
  211. package/src/components/Form/Input.tsx +0 -54
  212. package/src/components/Form/RefField.tsx +0 -283
  213. package/src/components/Form/custom/GeoPointInput.tsx +0 -80
  214. package/src/components/Form/custom/index.ts +0 -7
  215. package/src/components/Form/factory.tsx +0 -31
  216. package/src/hooks/useForm.ts +0 -296
  217. package/src/hooks/useQueryRefOptions.ts +0 -62
  218. package/src/hooks/useRefQueryLookupHandler.ts +0 -39
  219. package/src/util/schema.ts +0 -12
@@ -0,0 +1,228 @@
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
+ import { ObjectPicker, type ObjectPickerContentProps, type RefOption } from '../../ObjectPicker';
19
+ import { omitHiddenFormFields, omitId } from '../Form';
20
+ import { type FormFieldComponentProps, FormFieldLabel } from '../FormFieldComponent';
21
+
22
+ // TODO(burdon): Factor out.
23
+ const isRefSnapshot = (val: any): val is { '/': string } => {
24
+ return typeof val === 'object' && typeof (val as any)?.['/'] === 'string';
25
+ };
26
+
27
+ const defaultGetOptions: NonNullable<RefFieldProps['getOptions']> = (results, { parentLabel } = {}) =>
28
+ results.map((result) => {
29
+ const id = Entity.getDXN(result).toString();
30
+ const parent = parentLabel ? Obj.getParent(result as Obj.Unknown) : undefined;
31
+ const label = parent ? Entity.getLabel(parent) : Entity.getLabel(result);
32
+ return { id, label: label ?? id };
33
+ });
34
+
35
+ const defaultUseResults: NonNullable<RefFieldProps['useResults']> = (db, typename) =>
36
+ useQuery(
37
+ db,
38
+ typename
39
+ ? // For Ref.Ref(Obj.Unknown) we want to show all objects.
40
+ typename === Type.getTypename(Obj.Unknown)
41
+ ? Filter.everything()
42
+ : Filter.typename(typename)
43
+ : Filter.nothing(),
44
+ );
45
+
46
+ export type RefFieldProps = FormFieldComponentProps &
47
+ Pick<
48
+ ObjectPickerContentProps,
49
+ 'createOptionLabel' | 'createOptionIcon' | 'createInitialValuePath' | 'createFieldMap'
50
+ > & {
51
+ db?: Database.Database;
52
+ // TODO(burdon): Replace hooks with callbacks.
53
+ useSchema?: (db?: Database.Database, typename?: string) => Type.AnyEntity;
54
+ useResults?: (db?: Database.Database, typename?: string) => Entity.Any[];
55
+ getOptions?: (objects: Entity.Any[], options?: { parentLabel?: boolean }) => RefOption[];
56
+ /**
57
+ * Persist a newly-created object. Called after the user fills out the
58
+ * inline create form and clicks Save. Should add the object to the
59
+ * database and return it (sync or async). The returned object is then
60
+ * wired into this slot's form value as a Ref.
61
+ */
62
+ onCreate?: (schema: Type.AnyEntity, values: any) => Obj.Unknown | Promise<Obj.Unknown> | undefined | void;
63
+ };
64
+
65
+ export const RefField = (props: RefFieldProps) => {
66
+ const {
67
+ type,
68
+ readonly,
69
+ label,
70
+ placeholder,
71
+ layout,
72
+ getStatus,
73
+ getValue,
74
+ createOptionLabel,
75
+ createOptionIcon,
76
+ createInitialValuePath,
77
+ createFieldMap,
78
+ db,
79
+ useSchema = defaultUseSchema,
80
+ useResults = defaultUseResults,
81
+ getOptions = defaultGetOptions,
82
+ onCreate,
83
+ onValueChange,
84
+ } = props;
85
+ const { t } = useTranslation(translationKey);
86
+ const { status, error } = getStatus();
87
+
88
+ const typename = useMemo(
89
+ () => (type ? findAnnotation<ReferenceAnnotationValue>(type, ReferenceAnnotationId)?.typename : undefined),
90
+ [type],
91
+ );
92
+
93
+ const results = useResults(db, typename);
94
+ const options = useMemo(() => {
95
+ const parentLabel = type ? findAnnotation<boolean>(type, ParentLabelAnnotationId) === true : false;
96
+ return getOptions(results, { parentLabel });
97
+ }, [results, getOptions, type]);
98
+
99
+ const handleGetValue = useCallback(() => {
100
+ const formValue = getValue();
101
+ // Match form-value Refs against options by the bare object id (last DXN
102
+ // segment), not by full DXN string. Ref.make uses
103
+ // `DXN.fromLocalObjectId(id)` (`dxn:echo:@:<id>`), but
104
+ // `Entity.getDXN(obj)` on a registered object produces the space-scoped
105
+ // form (`dxn:echo:<spaceId>:<id>`). String-comparing the two never
106
+ // matches, so the just-created Ref's option lookup fails and the slot
107
+ // displays as empty even though the underlying form value IS set.
108
+ const dxnToObjectId = (dxn: string): string => dxn.split(':').pop() ?? dxn;
109
+
110
+ const unknownToRefOption = (value: unknown) => {
111
+ const isRef = Ref.isRef(value);
112
+ if (isRef || isRefSnapshot(value)) {
113
+ const dxnString = isRef ? value.dxn.toString() : value['/'];
114
+ const objectId = dxnToObjectId(dxnString);
115
+ const matchingOption = options.find((option) => dxnToObjectId(option.id) === objectId);
116
+ if (matchingOption) {
117
+ return matchingOption;
118
+ }
119
+ }
120
+
121
+ return undefined;
122
+ };
123
+
124
+ return unknownToRefOption(formValue);
125
+ }, [options, getValue]);
126
+
127
+ const item = handleGetValue();
128
+ const selectedIds = useMemo(() => (item ? [item.id] : []), [item]);
129
+ const createSchema = useSchema(db, typename);
130
+
131
+ // Lift the popover open state so we can dismiss it after a successful create.
132
+ const [open, setOpen] = useState(false);
133
+
134
+ const handleCreate = useCallback(
135
+ async (values: any) => {
136
+ if (!createSchema || !onCreate) {
137
+ return;
138
+ }
139
+ const newObject = await onCreate(createSchema, values);
140
+ if (newObject) {
141
+ // Wire the newly-created object into this slot's form value. ArrayField
142
+ // owns the array; this RefField represents a single slot, so writing a
143
+ // Ref via `onValueChange` populates that slot.
144
+ onValueChange(type, Ref.make(newObject));
145
+ }
146
+ setOpen(false);
147
+ },
148
+ [createSchema, onCreate, type, onValueChange],
149
+ );
150
+
151
+ const handleUpdate = useCallback(
152
+ (id: string | undefined) => {
153
+ const item = options.find((option) => option.id === id);
154
+ const ref = item ? Ref.fromDXN(DXN.parse(item.id)) : undefined;
155
+ onValueChange(type, ref);
156
+ },
157
+ [options, type, onValueChange],
158
+ );
159
+
160
+ const handleSelect = useCallback(
161
+ (id: string) => {
162
+ if (item?.id === id) {
163
+ handleUpdate(undefined);
164
+ } else {
165
+ handleUpdate(id);
166
+ }
167
+ },
168
+ [item, handleUpdate],
169
+ );
170
+
171
+ if (!typename || ((readonly || layout === 'static') && !item)) {
172
+ return null;
173
+ }
174
+
175
+ return (
176
+ <Input.Root validationValence={status}>
177
+ {layout !== 'inline' && <FormFieldLabel error={error} readonly={readonly} label={label} />}
178
+ <div>
179
+ {readonly ? (
180
+ !item ? (
181
+ <p className='text-description mb-2'>{t('empty-readonly-ref-field.label')}</p>
182
+ ) : (
183
+ <DxAnchor key={item.id} dxn={item.id} rootclassname='me-1'>
184
+ {item.label}
185
+ </DxAnchor>
186
+ )
187
+ ) : (
188
+ <ObjectPicker.Root open={open} onOpenChange={setOpen}>
189
+ <ObjectPicker.Trigger asChild classNames='p-0'>
190
+ {item ? (
191
+ <div role='none' className='flex gap-form-gap w-full'>
192
+ <Input.Root key={item.id}>
193
+ <Input.TextInput value={item.label} readOnly classNames='w-full' />
194
+ </Input.Root>
195
+ </div>
196
+ ) : (
197
+ <Button classNames='w-full text-start gap-form-gap'>
198
+ <div role='none' className='grow overflow-hidden'>
199
+ <span className='truncate text-description'>{placeholder ?? t('ref-field.placeholder')}</span>
200
+ </div>
201
+ <Icon size={3} icon='ph--caret-down--bold' />
202
+ </Button>
203
+ )}
204
+ </ObjectPicker.Trigger>
205
+ <ObjectPicker.Portal>
206
+ <ObjectPicker.Content
207
+ classNames='dx-card-popover-width'
208
+ options={options}
209
+ selectedIds={selectedIds}
210
+ // Strip hidden (`FormInputAnnotation.set(false)`) fields so the
211
+ // form's validator doesn't reject required-but-hidden fields
212
+ // such as backing-object refs supplied by a `FactoryAnnotation`.
213
+ createSchema={createSchema && omitHiddenFormFields(omitId(createSchema))}
214
+ createOptionLabel={createOptionLabel}
215
+ createOptionIcon={createOptionIcon}
216
+ createInitialValuePath={createInitialValuePath}
217
+ createFieldMap={createFieldMap}
218
+ onCreate={handleCreate}
219
+ onSelect={handleSelect}
220
+ />
221
+ </ObjectPicker.Portal>
222
+ </ObjectPicker.Root>
223
+ )}
224
+ </div>
225
+ {layout === 'full' && <Input.DescriptionAndValidation>{error}</Input.DescriptionAndValidation>}
226
+ </Input.Root>
227
+ );
228
+ };
@@ -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
+ };
@@ -6,17 +6,16 @@ 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
12
  import { translations } from '../../../translations';
13
- import { TestLayout, TestPanel } from '../../testing';
13
+ import { TestLayout } from '../../testing';
14
14
  import { Form } from '../Form';
15
-
16
- import { SelectOptionInput } from './SelectOptionsInput';
15
+ import { SelectOptionField } from './SelectOptionField';
17
16
 
18
17
  const schema = Schema.Struct({
19
- options: Schema.Array(SelectOptionSchema).pipe(Schema.mutable),
18
+ options: Schema.Array(SelectOption).pipe(Schema.mutable),
20
19
  }).pipe(Schema.mutable);
21
20
 
22
21
  const DefaultStory = () => {
@@ -31,23 +30,28 @@ const DefaultStory = () => {
31
30
 
32
31
  return (
33
32
  <TestLayout>
34
- <TestPanel>
35
- <Form
36
- schema={schema}
37
- values={values}
38
- onSave={(vals) => setValues(vals)}
39
- Custom={{ options: SelectOptionInput }}
40
- />
41
- </TestPanel>
33
+ <Form.Root
34
+ schema={schema}
35
+ values={values}
36
+ fieldMap={{ options: SelectOptionField }}
37
+ onSave={(values) => setValues(values)}
38
+ >
39
+ <Form.Viewport>
40
+ <Form.Content>
41
+ <Form.FieldSet />
42
+ <Form.Actions />
43
+ </Form.Content>
44
+ </Form.Viewport>
45
+ </Form.Root>
42
46
  </TestLayout>
43
47
  );
44
48
  };
45
49
 
46
50
  const meta = {
47
- title: 'ui/react-ui-form/SelectOptionsInput',
48
- component: SelectOptionInput as any,
51
+ title: 'ui/react-ui-form/SelectOptionField',
52
+ component: SelectOptionField as any,
49
53
  render: DefaultStory,
50
- decorators: [withTheme],
54
+ decorators: [withTheme(), withLayout({ layout: 'fullscreen' })],
51
55
  parameters: {
52
56
  layout: 'fullscreen',
53
57
  translations,
@@ -9,25 +9,32 @@ 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, subtleHover } from '@dxos/ui-theme';
13
13
 
14
14
  import { translationKey } from '../../../translations';
15
- import { InputHeader, type InputProps } from '../Input';
16
-
17
- export const SelectOptionInput = ({ type, label, readonly, getStatus, getValue, onValueChange }: InputProps) => {
15
+ import { type FormFieldComponentProps, FormFieldLabel } from '../FormFieldComponent';
16
+
17
+ export const SelectOptionField = ({
18
+ type,
19
+ readonly,
20
+ label,
21
+ getStatus,
22
+ getValue,
23
+ onValueChange,
24
+ }: FormFieldComponentProps<SelectOption[] | undefined>) => {
25
+ const { t } = useTranslation(translationKey);
26
+ const { status, error } = getStatus();
18
27
  const [selected, setSelectedId] = useState<string | null>(null);
19
28
  const [isNewOption, setIsNewOption] = useState(false);
20
29
  const inputRef = useRef<HTMLInputElement>(null);
21
- const { t } = useTranslation(translationKey);
22
- const { status, error } = getStatus();
23
- const options = getValue<SelectOption[] | undefined>();
30
+ const options = getValue();
24
31
 
25
32
  // Initialization.
26
33
  useEffect(() => {
27
34
  if (options === undefined) {
28
35
  onValueChange(type, []);
29
36
  }
30
- }, [options, onValueChange, type]);
37
+ }, [options, type, onValueChange]);
31
38
 
32
39
  const randomHue = () => {
33
40
  const usedHues = new Set(options?.map((option) => option.color) ?? []);
@@ -88,8 +95,8 @@ export const SelectOptionInput = ({ type, label, readonly, getStatus, getValue,
88
95
  [options, type, onValueChange],
89
96
  );
90
97
 
91
- const handleKeyDown = useCallback((e: KeyboardEvent<HTMLInputElement>) => {
92
- if (e.key === 'Enter') {
98
+ const handleKeyDown = useCallback((ev: KeyboardEvent<HTMLInputElement>) => {
99
+ if (ev.key === 'Enter') {
93
100
  setSelectedId(null);
94
101
  }
95
102
  }, []);
@@ -111,12 +118,12 @@ export const SelectOptionInput = ({ type, label, readonly, getStatus, getValue,
111
118
 
112
119
  return (
113
120
  <Input.Root validationValence={status}>
114
- <InputHeader error={error} label={label} />
121
+ <FormFieldLabel error={error} readonly={readonly} label={label} />
115
122
  <div role='none'>
116
123
  {options && (
117
124
  <List.Root
118
125
  items={options}
119
- isItem={(item) => true}
126
+ isItem={(_item) => true}
120
127
  onMove={readonly ? undefined : handleMove}
121
128
  readonly={!!readonly}
122
129
  >
@@ -127,7 +134,7 @@ export const SelectOptionInput = ({ type, label, readonly, getStatus, getValue,
127
134
  <List.Item
128
135
  role='button'
129
136
  item={item}
130
- classNames={[subtleHover, 'flex flex-col cursor-pointer rounded-sm']}
137
+ classNames={[subtleHover, 'flex flex-col cursor-pointer rounded-xs']}
131
138
  aria-expanded={selected === item.id}
132
139
  >
133
140
  <div className='flex items-center'>
@@ -139,32 +146,30 @@ export const SelectOptionInput = ({ type, label, readonly, getStatus, getValue,
139
146
  <IconButton
140
147
  iconOnly
141
148
  variant='ghost'
142
- label={t(selected === item.id ? 'collapse label' : 'expand label', { ns: 'os' })}
149
+ label={t(selected === item.id ? 'collapse label' : 'expand label', { ns: osTranslations })}
143
150
  icon={selected === item.id ? 'ph--caret-down--regular' : 'ph--caret-right--regular'}
144
151
  onClick={() => handleClick(item.id)}
145
152
  />
146
153
  </div>
147
154
  {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'>
155
+ <div className='flex flex-col p-form-padding gap-form-gap dx-density-fine'>
156
+ <Input.Label classNames='text-xs'>{t('select-option.label')}</Input.Label>
157
+ <div className='grid grid-cols-[1fr_min-content_min-content] gap-form-gap'>
152
158
  <Input.TextInput
153
159
  disabled={!!readonly}
154
- placeholder={t('select option label placeholder')}
160
+ placeholder={t('select-option-label.placeholder')}
155
161
  ref={selected === item.id ? inputRef : undefined}
156
162
  value={item.title}
157
163
  onChange={handleTitleChange(item.id)}
158
164
  onKeyDown={handleKeyDown}
159
165
  classNames='flex-1'
160
- data-no-submit
161
166
  />
162
167
  <HuePicker disabled={!!readonly} value={item.color} onChange={handleColorChange(item.id)} />
163
168
  <IconButton
164
169
  disabled={!!readonly}
165
170
  icon='ph--trash--fill'
166
171
  iconOnly
167
- label={t('select option delete')}
172
+ label={t('select-option-delete.button')}
168
173
  onClick={() => handleDelete(item.id)}
169
174
  />
170
175
  </div>
@@ -176,10 +181,10 @@ export const SelectOptionInput = ({ type, label, readonly, getStatus, getValue,
176
181
  <IconButton
177
182
  variant='ghost'
178
183
  icon='ph--plus--regular'
179
- label={t('select option add')}
184
+ label={t('select-option-add.button')}
180
185
  onClick={handleAdd}
181
186
  disabled={!!readonly}
182
- classNames='is-full'
187
+ classNames='w-full'
183
188
  />
184
189
  </div>
185
190
  )}
@@ -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
- label,
19
18
  readonly,
19
+ label,
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
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';
@@ -2,8 +2,7 @@
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';