@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,113 @@
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
+ import { Form, type FormFieldMap, omitId } from '../Form';
16
+
17
+ export type ObjectFormProps = {
18
+ schema: Schema.Schema.AnyNoContext;
19
+ object: Obj.Unknown;
20
+ };
21
+
22
+ const createFieldMap: FormFieldMap = {
23
+ hue: ({ type, label, layout, getValue, onValueChange }) => {
24
+ const handleChange = useCallback((nextHue: string) => onValueChange(type, nextHue), [onValueChange, type]);
25
+ const handleReset = useCallback(() => onValueChange(type, undefined), [onValueChange, type]);
26
+ return (
27
+ <>
28
+ {layout !== 'inline' && <Form.Label label={label} />}
29
+ <HuePicker value={getValue()} onChange={handleChange} onReset={handleReset} />
30
+ </>
31
+ );
32
+ },
33
+ };
34
+
35
+ export const ObjectForm = ({ object, schema }: ObjectFormProps) => {
36
+ const db = Obj.getDatabase(object);
37
+ const meta = Obj.getMeta(object);
38
+ const tags = (meta.tags ?? []).map((tag) => db?.makeRef(DXN.parse(tag))).filter(isNonNullable);
39
+ const values = useMemo(() => ({ tags, ...object }), [object, tags]);
40
+ const formSchema = useMemo(
41
+ () =>
42
+ omitId(
43
+ Schema.Struct({
44
+ tags: Schema.Array(Ref.Ref(Tag.Tag)).pipe(Schema.optional),
45
+ }).pipe(Schema.extend(schema)),
46
+ ),
47
+ [schema],
48
+ );
49
+
50
+ const handleCreate = useCallback((schema: Type.AnyEntity, values: any) => {
51
+ invariant(db);
52
+ invariant(Type.isObjectSchema(schema));
53
+ const newObject = db.add(Obj.make(schema, values));
54
+ if (Obj.instanceOf(Tag.Tag, newObject)) {
55
+ Obj.change(object, (object) => {
56
+ Obj.getMeta(object).tags = [...(Obj.getMeta(object).tags ?? []), Obj.getDXN(newObject).toString()];
57
+ });
58
+ }
59
+ }, []);
60
+
61
+ // TODO(wittjosiah): Use FormRootProps type.
62
+ const handleChange = useCallback(
63
+ ({ tags, ...values }: any, { isValid, changed }: { isValid: boolean; changed: Record<JsonPath, boolean> }) => {
64
+ if (!isValid) {
65
+ return;
66
+ }
67
+
68
+ const changedPaths = Object.keys(changed).filter((path) => changed[path as JsonPath]) as JsonPath[];
69
+
70
+ // Handle tags separately using Obj.change.
71
+ const hasTagsChange = changedPaths.some((path) => splitJsonPath(path)[0] === 'tags');
72
+ if (hasTagsChange) {
73
+ Obj.change(object, (object) => {
74
+ Obj.getMeta(object).tags = tags?.map((tag: Ref.Ref<Tag.Tag>) => tag.dxn.toString()) ?? [];
75
+ });
76
+ }
77
+
78
+ // Handle other property changes.
79
+ const nonTagPaths = changedPaths.filter((path) => splitJsonPath(path)[0] !== 'tags');
80
+ if (nonTagPaths.length > 0) {
81
+ Obj.change(object, () => {
82
+ for (const path of nonTagPaths) {
83
+ const parts = splitJsonPath(path);
84
+ const value = Obj.getValue(values, parts);
85
+ Obj.setValue(object, parts, value);
86
+ }
87
+ });
88
+ }
89
+ },
90
+ [object],
91
+ );
92
+
93
+ return (
94
+ <Form.Root
95
+ schema={formSchema}
96
+ defaultValues={values}
97
+ createTypename={Type.getTypename(Tag.Tag)}
98
+ createOptionIcon='ph--plus--regular'
99
+ createOptionLabel={['add-tag.label', { ns: translationKey }]}
100
+ createInitialValuePath='label'
101
+ createFieldMap={createFieldMap}
102
+ db={db}
103
+ onValuesChanged={handleChange}
104
+ onCreate={handleCreate}
105
+ >
106
+ <Form.Viewport>
107
+ <Form.Content>
108
+ <Form.FieldSet />
109
+ </Form.Content>
110
+ </Form.Viewport>
111
+ </Form.Root>
112
+ );
113
+ };
@@ -2,4 +2,4 @@
2
2
  // Copyright 2025 DXOS.org
3
3
  //
4
4
 
5
- export * from './ControlSection';
5
+ export * from './ObjectForm';
@@ -8,33 +8,33 @@ import React, { useCallback, useEffect, useMemo, useState } from 'react';
8
8
  import { expect, fn, userEvent, within } from 'storybook/test';
9
9
 
10
10
  import { Obj } from '@dxos/echo';
11
- import { faker } from '@dxos/random';
11
+ import { random } from '@dxos/random';
12
12
  import { Filter, useQuery } from '@dxos/react-client/echo';
13
- import { useClientProvider, withClientProvider } from '@dxos/react-client/testing';
13
+ import { useClientStory, withClientProvider } from '@dxos/react-client/testing';
14
14
  import { Button } from '@dxos/react-ui';
15
15
  import { withTheme } from '@dxos/react-ui/testing';
16
- import { DataType } from '@dxos/schema';
16
+ import { Person } from '@dxos/types';
17
+ import { osTranslations } from '@dxos/ui-theme';
17
18
 
18
19
  import { translations } from '../../translations';
19
-
20
20
  import { ObjectPicker } from './ObjectPicker';
21
21
 
22
- faker.seed(1);
22
+ random.seed(1);
23
23
 
24
24
  const createPerson = () =>
25
- Obj.make(DataType.Person, {
26
- fullName: faker.person.fullName(),
25
+ Obj.make(Person.Person, {
26
+ fullName: random.person.fullName(),
27
27
  });
28
28
 
29
29
  const omitId = Schema.omit<any, any, ['id']>('id');
30
- const personSchema = omitId(DataType.Person);
30
+ const personSchema = omitId(Person.Person);
31
31
 
32
32
  // Mock functions for testing
33
33
  const mockHandleSelect = fn();
34
34
  const mockHandleCreate = fn();
35
35
 
36
36
  const DefaultStory = () => {
37
- const { space } = useClientProvider();
37
+ const { space } = useClientStory();
38
38
  const [isOpen, setIsOpen] = useState(false);
39
39
 
40
40
  // Create sample Person objects
@@ -48,7 +48,7 @@ const DefaultStory = () => {
48
48
  }, [space]);
49
49
 
50
50
  // Get all objects in the space (similar to BoardContainer)
51
- const allObjects = useQuery(space, Filter.everything());
51
+ const allObjects = useQuery(space?.db, Filter.everything());
52
52
 
53
53
  // Map objects to options format expected by ObjectPicker
54
54
  const options = useMemo(
@@ -65,7 +65,7 @@ const DefaultStory = () => {
65
65
  (values: any) => {
66
66
  console.log('[on create]', values);
67
67
  if (!space) return;
68
- const newPerson = space.db.add(Obj.make(DataType.Person, values));
68
+ const newPerson = space.db.add(Obj.make(Person.Person, values));
69
69
  mockHandleCreate(values);
70
70
  return newPerson;
71
71
  },
@@ -73,22 +73,22 @@ const DefaultStory = () => {
73
73
  );
74
74
 
75
75
  return (
76
- <div className='p-4'>
76
+ <div role='none' className='w-96'>
77
77
  <ObjectPicker.Root open={isOpen} onOpenChange={setIsOpen}>
78
78
  <ObjectPicker.Trigger asChild>
79
- <Button variant='primary' data-testid='trigger'>
79
+ <Button variant='primary' data-testid='trigger' classNames='w-full'>
80
80
  Select Person
81
81
  </Button>
82
82
  </ObjectPicker.Trigger>
83
83
  <ObjectPicker.Content
84
+ classNames='dx-card-popover-width'
84
85
  options={options}
85
- onSelect={mockHandleSelect}
86
86
  createSchema={personSchema}
87
- createOptionLabel={['create new person label', { ns: 'os' }]}
87
+ createOptionLabel={['create-new-person.label', { ns: osTranslations }]}
88
88
  createOptionIcon='ph--user-plus--regular'
89
89
  createInitialValuePath='fullName'
90
90
  onCreate={handleCreateCallback}
91
- classNames='popover-card-width'
91
+ onSelect={mockHandleSelect}
92
92
  />
93
93
  </ObjectPicker.Root>
94
94
  </div>
@@ -99,9 +99,9 @@ const meta = {
99
99
  title: 'ui/react-ui-form/ObjectPicker',
100
100
  render: DefaultStory,
101
101
  decorators: [
102
- withTheme,
102
+ withTheme(),
103
103
  withClientProvider({
104
- types: [DataType.Person],
104
+ types: [Person.Person],
105
105
  createIdentity: true,
106
106
  createSpace: true,
107
107
  }),
@@ -200,8 +200,10 @@ export const WithTest: Story = {
200
200
  await userEvent.click(saveButton);
201
201
 
202
202
  // Check that clicking the save button calls onCreate with expected values.
203
- await expect(mockHandleCreate).toHaveBeenCalledWith({
204
- fullName: unrelatedTerm,
205
- });
203
+ await expect(mockHandleCreate).toHaveBeenCalledWith(
204
+ expect.objectContaining({
205
+ fullName: unrelatedTerm,
206
+ }),
207
+ );
206
208
  },
207
209
  };
@@ -3,24 +3,38 @@
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 { Combobox } from '@dxos/react-ui-searchlist';
8
+ import { type Palette, Popover, type ThemedClassName, useTranslation } from '@dxos/react-ui';
9
+ import { Combobox, useSearchListInput, useSearchListResults } from '@dxos/react-ui-search';
10
10
 
11
- import { type QueryTag } from '../../hooks';
12
11
  import { translationKey } from '../../translations';
13
12
  import { Form } from '../Form';
13
+ import { type FormFieldMap } from '../Form/FormFieldComponent';
14
+
15
+ export type RefOption = {
16
+ id: string;
17
+ label: string;
18
+ hue?: Palette;
19
+ };
14
20
 
15
21
  export type ObjectPickerContentProps = ThemedClassName<{
16
- options: QueryTag[];
22
+ options: RefOption[];
17
23
  selectedIds?: string[];
18
- createSchema?: Schema.Schema.AnyNoContext;
19
24
  createOptionLabel?: [string, { ns: string }];
20
25
  createOptionIcon?: string;
26
+ createSchema?: Schema.Schema.AnyNoContext;
21
27
  createInitialValuePath?: string;
28
+ createFieldMap?: FormFieldMap;
29
+ /**
30
+ * Persist a newly-created object given the form values. May be async (e.g.
31
+ * to write to a database). The Promise is awaited before the inline create
32
+ * form is collapsed back to the picker list, so consumers can complete
33
+ * follow-up work (assign the new ref to a parent slot, close the popover)
34
+ * before the picker UI re-renders.
35
+ */
36
+ onCreate?: (values: any) => unknown | Promise<unknown>;
22
37
  onSelect: (id: string) => void;
23
- onCreate?: (values: any) => void;
24
38
  }>;
25
39
 
26
40
  const ObjectPickerContent = forwardRef<HTMLDivElement, ObjectPickerContentProps>(
@@ -32,46 +46,35 @@ const ObjectPickerContent = forwardRef<HTMLDivElement, ObjectPickerContentProps>
32
46
  createOptionLabel,
33
47
  createOptionIcon,
34
48
  createInitialValuePath,
49
+ createFieldMap,
35
50
  onSelect,
36
51
  onCreate,
37
52
  ...props
38
53
  },
39
- ref,
54
+ forwardedRef,
40
55
  ) => {
41
56
  const { t } = useTranslation(translationKey);
42
-
43
- const [searchString, setSearchString] = useState('');
44
57
  const [showForm, setShowForm] = useState(false);
58
+ const [formInitialValue, setFormInitialValue] = useState<string>('');
59
+
60
+ const { results, handleSearch } = useSearchListResults({
61
+ items: options,
62
+ });
45
63
 
46
64
  const handleFormSave = useCallback(
47
- (values: any) => {
48
- onCreate?.(values);
65
+ async (values: any) => {
66
+ await onCreate?.(values);
49
67
  setShowForm(false);
50
- setSearchString('');
68
+ setFormInitialValue('');
51
69
  },
52
70
  [onCreate],
53
71
  );
54
72
 
55
73
  const handleFormCancel = useCallback(() => {
56
74
  setShowForm(false);
57
- setSearchString('');
75
+ setFormInitialValue('');
58
76
  }, []);
59
77
 
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
78
  const handleKeyDown = useCallback(
76
79
  (event: KeyboardEvent) => {
77
80
  if (
@@ -83,78 +86,107 @@ const ObjectPickerContent = forwardRef<HTMLDivElement, ObjectPickerContentProps>
83
86
  ) {
84
87
  event.stopPropagation();
85
88
  event.preventDefault();
89
+ // Get the current query from the input element
90
+ const input = (event.currentTarget as HTMLElement).querySelector('input[type="text"]') as HTMLInputElement;
91
+ const currentQuery = input?.value || '';
92
+ setFormInitialValue(currentQuery);
86
93
  setShowForm(true);
87
94
  }
88
95
  },
89
96
  [createSchema],
90
97
  );
91
98
 
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
- <Combobox.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 ? (
99
+ if (showForm && createSchema) {
100
+ return (
101
+ <Combobox.Content {...props} onSearch={handleSearch} onKeyDownCapture={handleKeyDown} ref={forwardedRef}>
110
102
  <Popover.Viewport>
111
- <Form
103
+ <Form.Root
104
+ testId='create-referenced-object-form'
112
105
  schema={createSchema}
113
- values={createInitialValuePath ? { [createInitialValuePath]: searchString } : {}}
106
+ defaultValues={createInitialValuePath ? { [createInitialValuePath]: formInitialValue } : {}}
107
+ fieldMap={createFieldMap}
114
108
  onSave={handleFormSave}
115
109
  onCancel={handleFormCancel}
116
- testId='create-referenced-object-form'
117
- />
110
+ >
111
+ <Form.Viewport>
112
+ <Form.Content>
113
+ <Form.FieldSet />
114
+ <Form.Actions />
115
+ </Form.Content>
116
+ </Form.Viewport>
117
+ </Form.Root>
118
118
  </Popover.Viewport>
119
- ) : (
120
- <>
121
- <Combobox.Input
122
- placeholder={t('ref field combobox input placeholder')}
123
- value={searchString}
124
- onValueChange={setSearchString}
125
- autoFocus
119
+ <Combobox.Arrow />
120
+ </Combobox.Content>
121
+ );
122
+ }
123
+
124
+ return (
125
+ <Combobox.Content {...props} onSearch={handleSearch} onKeyDownCapture={handleKeyDown} ref={forwardedRef}>
126
+ <Combobox.Input placeholder={t('ref-field-combobox-input.placeholder')} autoFocus />
127
+ <Combobox.List>
128
+ {results.map((option) => (
129
+ <Combobox.Item
130
+ key={option.id}
131
+ value={option.id}
132
+ label={option.label}
133
+ checked={selectedIds?.includes(option.id)}
134
+ onSelect={() => onSelect(option.id)}
135
+ classNames='flex items-center gap-2'
136
+ />
137
+ ))}
138
+ {createSchema && onCreate && (
139
+ <CreateItem
140
+ createOptionLabel={createOptionLabel}
141
+ createOptionIcon={createOptionIcon ?? 'ph--plus--regular'}
142
+ onCreateItemSelect={(query: string) => {
143
+ setFormInitialValue(query);
144
+ setShowForm(true);
145
+ }}
126
146
  />
127
- <Combobox.List>
128
- {options.map((option) => (
129
- <Combobox.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
- </Combobox.Item>
138
- ))}
139
- {searchString.length > 0 && createOptionLabel && createOptionIcon && createSchema && onCreate && (
140
- <Combobox.Item value='__create__' classNames='flex items-center gap-2'>
141
- <Icon icon={createOptionIcon} />
142
- {t(createOptionLabel[0], { ns: createOptionLabel[1].ns, text: searchString })}
143
- </Combobox.Item>
144
- )}
145
- </Combobox.List>
146
- </>
147
- )}
147
+ )}
148
+ </Combobox.List>
148
149
  <Combobox.Arrow />
149
150
  </Combobox.Content>
150
151
  );
151
152
  },
152
153
  );
153
154
 
154
- ObjectPickerContent.displayName = 'ObjectPickerContent';
155
+ const CreateItem = ({
156
+ createOptionLabel,
157
+ createOptionIcon,
158
+ onCreateItemSelect,
159
+ }: {
160
+ createOptionLabel?: [string, { ns: string }];
161
+ createOptionIcon: string;
162
+ onCreateItemSelect: (query: string) => void;
163
+ }) => {
164
+ const { t } = useTranslation(translationKey);
165
+ const { query } = useSearchListInput();
166
+
167
+ const label = createOptionLabel
168
+ ? t(createOptionLabel[0], { ns: createOptionLabel[1].ns, text: query })
169
+ : t('create-option.label');
170
+
171
+ return (
172
+ <Combobox.Item
173
+ value='__create__'
174
+ label={label}
175
+ icon={createOptionIcon}
176
+ classNames='flex items-center gap-2'
177
+ closeOnSelect={false}
178
+ onSelect={() => {
179
+ onCreateItemSelect(query);
180
+ }}
181
+ />
182
+ );
183
+ };
184
+
185
+ ObjectPickerContent.displayName = 'ObjectPicker.Content';
155
186
 
156
187
  export const ObjectPicker = {
157
188
  Root: Combobox.Root,
189
+ Portal: Combobox.Portal,
158
190
  Trigger: Combobox.Trigger,
159
191
  VirtualTrigger: Combobox.VirtualTrigger,
160
192
  Content: ObjectPickerContent,