@dxos/react-ui-form 0.8.4-main.c4373fc → 0.8.4-main.d05539e30a

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