@dxos/react-ui-form 0.8.4-main.c85a9c8dae → 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 (147) hide show
  1. package/LICENSE +102 -5
  2. package/dist/lib/browser/index.mjs +1028 -625
  3. package/dist/lib/browser/index.mjs.map +4 -4
  4. package/dist/lib/browser/meta.json +1 -1
  5. package/dist/lib/browser/translations.mjs +82 -0
  6. package/dist/lib/browser/translations.mjs.map +7 -0
  7. package/dist/lib/node-esm/index.mjs +1028 -625
  8. package/dist/lib/node-esm/index.mjs.map +4 -4
  9. package/dist/lib/node-esm/meta.json +1 -1
  10. package/dist/lib/node-esm/translations.mjs +84 -0
  11. package/dist/lib/node-esm/translations.mjs.map +7 -0
  12. package/dist/types/src/components/FieldEditor/FieldEditor.d.ts.map +1 -1
  13. package/dist/types/src/components/FieldEditor/FieldEditor.stories.d.ts +63 -61
  14. package/dist/types/src/components/FieldEditor/FieldEditor.stories.d.ts.map +1 -1
  15. package/dist/types/src/components/Form/Form.d.ts +40 -12
  16. package/dist/types/src/components/Form/Form.d.ts.map +1 -1
  17. package/dist/types/src/components/Form/Form.stories.d.ts +76 -74
  18. package/dist/types/src/components/Form/Form.stories.d.ts.map +1 -1
  19. package/dist/types/src/components/Form/FormField.d.ts +11 -5
  20. package/dist/types/src/components/Form/FormField.d.ts.map +1 -1
  21. package/dist/types/src/components/Form/FormFieldComponent.d.ts +8 -7
  22. package/dist/types/src/components/Form/FormFieldComponent.d.ts.map +1 -1
  23. package/dist/types/src/components/Form/FormFieldComponent.stories.d.ts.map +1 -1
  24. package/dist/types/src/components/Form/FormFieldSet.d.ts +5 -9
  25. package/dist/types/src/components/Form/FormFieldSet.d.ts.map +1 -1
  26. package/dist/types/src/components/Form/fields/ArrayField.d.ts +4 -4
  27. package/dist/types/src/components/Form/fields/ArrayField.d.ts.map +1 -1
  28. package/dist/types/src/components/Form/fields/BooleanField.d.ts.map +1 -1
  29. package/dist/types/src/components/Form/fields/GeoPointField.d.ts.map +1 -1
  30. package/dist/types/src/components/Form/fields/MarkdownField.d.ts +8 -1
  31. package/dist/types/src/components/Form/fields/MarkdownField.d.ts.map +1 -1
  32. package/dist/types/src/components/Form/fields/MarkdownField.stories.d.ts +84 -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.map +1 -1
  35. package/dist/types/src/components/Form/fields/RefField.d.ts +15 -7
  36. package/dist/types/src/components/Form/fields/RefField.d.ts.map +1 -1
  37. package/dist/types/src/components/Form/fields/SelectField.d.ts.map +1 -1
  38. package/dist/types/src/components/Form/fields/SelectOptionField.d.ts.map +1 -1
  39. package/dist/types/src/components/Form/fields/SelectOptionField.stories.d.ts +63 -61
  40. package/dist/types/src/components/Form/fields/SelectOptionField.stories.d.ts.map +1 -1
  41. package/dist/types/src/components/Form/fields/TextAreaField.d.ts.map +1 -1
  42. package/dist/types/src/components/Form/fields/TextField.d.ts +1 -1
  43. package/dist/types/src/components/Form/fields/TextField.d.ts.map +1 -1
  44. package/dist/types/src/components/Form/fields/TupleField.d.ts.map +1 -1
  45. package/dist/types/src/components/ObjectForm/ObjectForm.d.ts +9 -0
  46. package/dist/types/src/components/ObjectForm/ObjectForm.d.ts.map +1 -0
  47. package/dist/types/src/components/ObjectForm/index.d.ts +2 -0
  48. package/dist/types/src/components/ObjectForm/index.d.ts.map +1 -0
  49. package/dist/types/src/components/ObjectPicker/ObjectPicker.d.ts +26 -4
  50. package/dist/types/src/components/ObjectPicker/ObjectPicker.d.ts.map +1 -1
  51. package/dist/types/src/components/ObjectPicker/ObjectPicker.stories.d.ts +63 -61
  52. package/dist/types/src/components/ObjectPicker/ObjectPicker.stories.d.ts.map +1 -1
  53. package/dist/types/src/components/ObjectProperties/ObjectProperties.d.ts +9 -0
  54. package/dist/types/src/components/ObjectProperties/ObjectProperties.d.ts.map +1 -0
  55. package/dist/types/src/components/ObjectProperties/ObjectProperties.stories.d.ts +127 -0
  56. package/dist/types/src/components/ObjectProperties/ObjectProperties.stories.d.ts.map +1 -0
  57. package/dist/types/src/components/ObjectProperties/ObjectProperties.test.d.ts +2 -0
  58. package/dist/types/src/components/ObjectProperties/ObjectProperties.test.d.ts.map +1 -0
  59. package/dist/types/src/components/ObjectProperties/index.d.ts +2 -0
  60. package/dist/types/src/components/ObjectProperties/index.d.ts.map +1 -0
  61. package/dist/types/src/components/Settings/Settings.d.ts +28 -33
  62. package/dist/types/src/components/Settings/Settings.d.ts.map +1 -1
  63. package/dist/types/src/components/Settings/Settings.stories.d.ts +64 -61
  64. package/dist/types/src/components/Settings/Settings.stories.d.ts.map +1 -1
  65. package/dist/types/src/components/Settings/SettingsFieldSet.d.ts +43 -0
  66. package/dist/types/src/components/Settings/SettingsFieldSet.d.ts.map +1 -0
  67. package/dist/types/src/components/Settings/SettingsItem.d.ts +16 -0
  68. package/dist/types/src/components/Settings/SettingsItem.d.ts.map +1 -0
  69. package/dist/types/src/components/Settings/SettingsSelectField.d.ts +18 -0
  70. package/dist/types/src/components/Settings/SettingsSelectField.d.ts.map +1 -0
  71. package/dist/types/src/components/Settings/index.d.ts +2 -0
  72. package/dist/types/src/components/Settings/index.d.ts.map +1 -1
  73. package/dist/types/src/components/ViewEditor/ViewEditor.d.ts +3 -4
  74. package/dist/types/src/components/ViewEditor/ViewEditor.d.ts.map +1 -1
  75. package/dist/types/src/components/ViewEditor/ViewEditor.stories.d.ts +65 -63
  76. package/dist/types/src/components/ViewEditor/ViewEditor.stories.d.ts.map +1 -1
  77. package/dist/types/src/components/index.d.ts +2 -0
  78. package/dist/types/src/components/index.d.ts.map +1 -1
  79. package/dist/types/src/components/testing.d.ts +4 -4
  80. package/dist/types/src/components/testing.d.ts.map +1 -1
  81. package/dist/types/src/hooks/useFormHandler.d.ts.map +1 -1
  82. package/dist/types/src/hooks/useKeyHandler.d.ts +2 -1
  83. package/dist/types/src/hooks/useKeyHandler.d.ts.map +1 -1
  84. package/dist/types/src/index.d.ts +0 -2
  85. package/dist/types/src/index.d.ts.map +1 -1
  86. package/dist/types/src/translations.d.ts +63 -61
  87. package/dist/types/src/translations.d.ts.map +1 -1
  88. package/dist/types/src/util/field-type.d.ts +19 -0
  89. package/dist/types/src/util/field-type.d.ts.map +1 -0
  90. package/dist/types/src/util/field.d.ts.map +1 -1
  91. package/dist/types/src/util/formatting.d.ts.map +1 -1
  92. package/dist/types/src/util/index.d.ts +1 -0
  93. package/dist/types/src/util/index.d.ts.map +1 -1
  94. package/dist/types/src/util/properties.d.ts +7 -1
  95. package/dist/types/src/util/properties.d.ts.map +1 -1
  96. package/dist/types/src/util/properties.test.d.ts +2 -0
  97. package/dist/types/src/util/properties.test.d.ts.map +1 -0
  98. package/dist/types/src/util/refs.d.ts +1 -0
  99. package/dist/types/src/util/refs.d.ts.map +1 -1
  100. package/dist/types/tsconfig.tsbuildinfo +1 -1
  101. package/package.json +43 -41
  102. package/src/components/FieldEditor/FieldEditor.stories.tsx +2 -2
  103. package/src/components/FieldEditor/FieldEditor.test.tsx +0 -1
  104. package/src/components/FieldEditor/FieldEditor.tsx +4 -3
  105. package/src/components/Form/AUDIT.md +17 -0
  106. package/src/components/Form/Form.stories.tsx +25 -38
  107. package/src/components/Form/Form.tsx +77 -48
  108. package/src/components/Form/FormField.tsx +27 -15
  109. package/src/components/Form/FormFieldComponent.tsx +6 -4
  110. package/src/components/Form/FormFieldSet.tsx +98 -77
  111. package/src/components/Form/fields/ArrayField.tsx +14 -8
  112. package/src/components/Form/fields/GeoPointField.tsx +8 -7
  113. package/src/components/Form/fields/MarkdownField.stories.tsx +206 -0
  114. package/src/components/Form/fields/MarkdownField.tsx +135 -45
  115. package/src/components/Form/fields/RefField.tsx +79 -39
  116. package/src/components/Form/fields/SelectOptionField.stories.tsx +8 -6
  117. package/src/components/Form/fields/SelectOptionField.tsx +12 -12
  118. package/src/components/Form/fields/TextField.tsx +2 -2
  119. package/src/components/Form/fields/TupleField.tsx +1 -1
  120. package/src/components/ObjectForm/ObjectForm.tsx +114 -0
  121. package/src/components/ObjectForm/index.ts +5 -0
  122. package/src/components/ObjectPicker/ObjectPicker.stories.tsx +12 -10
  123. package/src/components/ObjectPicker/ObjectPicker.tsx +92 -65
  124. package/src/components/ObjectProperties/ObjectProperties.stories.tsx +362 -0
  125. package/src/components/ObjectProperties/ObjectProperties.test.tsx +101 -0
  126. package/src/components/ObjectProperties/ObjectProperties.tsx +148 -0
  127. package/src/components/ObjectProperties/index.ts +5 -0
  128. package/src/components/Settings/AUDIT.md +51 -0
  129. package/src/components/Settings/Settings.stories.tsx +69 -21
  130. package/src/components/Settings/Settings.tsx +33 -183
  131. package/src/components/Settings/SettingsFieldSet.tsx +257 -0
  132. package/src/components/Settings/SettingsItem.tsx +58 -0
  133. package/src/components/Settings/SettingsSelectField.tsx +59 -0
  134. package/src/components/Settings/index.ts +2 -0
  135. package/src/components/ViewEditor/ViewEditor.stories.tsx +14 -15
  136. package/src/components/ViewEditor/ViewEditor.test.tsx +0 -1
  137. package/src/components/ViewEditor/ViewEditor.tsx +40 -36
  138. package/src/components/index.ts +2 -0
  139. package/src/components/testing.tsx +19 -11
  140. package/src/hooks/useKeyHandler.tsx +7 -7
  141. package/src/index.ts +0 -4
  142. package/src/translations.ts +66 -63
  143. package/src/util/field-type.ts +60 -0
  144. package/src/util/index.ts +1 -0
  145. package/src/util/properties.test.ts +63 -0
  146. package/src/util/properties.ts +14 -3
  147. package/src/util/refs.ts +7 -3
@@ -9,9 +9,10 @@ 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, osTranslations, subtleHover } from '@dxos/ui-theme';
12
+ import { hues, osTranslations } from '@dxos/ui-theme';
13
+
14
+ import { translationKey } from '#translations';
13
15
 
14
- import { translationKey } from '../../../translations';
15
16
  import { type FormFieldComponentProps, FormFieldLabel } from '../FormFieldComponent';
16
17
 
17
18
  export const SelectOptionField = ({
@@ -119,7 +120,7 @@ export const SelectOptionField = ({
119
120
  return (
120
121
  <Input.Root validationValence={status}>
121
122
  <FormFieldLabel error={error} readonly={readonly} label={label} />
122
- <div role='none'>
123
+ <div>
123
124
  {options && (
124
125
  <List.Root
125
126
  items={options}
@@ -134,7 +135,7 @@ export const SelectOptionField = ({
134
135
  <List.Item
135
136
  role='button'
136
137
  item={item}
137
- classNames={[subtleHover, 'flex flex-col cursor-pointer rounded-xs']}
138
+ classNames='flex flex-col cursor-pointer rounded-xs dx-hover'
138
139
  aria-expanded={selected === item.id}
139
140
  >
140
141
  <div className='flex items-center'>
@@ -146,19 +147,18 @@ export const SelectOptionField = ({
146
147
  <IconButton
147
148
  iconOnly
148
149
  variant='ghost'
149
- label={t(selected === item.id ? 'collapse label' : 'expand label', { ns: osTranslations })}
150
+ label={t(selected === item.id ? 'collapse.label' : 'expand.label', { ns: osTranslations })}
150
151
  icon={selected === item.id ? 'ph--caret-down--regular' : 'ph--caret-right--regular'}
151
152
  onClick={() => handleClick(item.id)}
152
153
  />
153
154
  </div>
154
155
  {selected === item.id && (
155
- <div className='flex flex-col p-form-padding gap-1 dx-density-fine'>
156
- {/* 16px to match drag handle width. */}
157
- <Input.Label classNames='text-xs'>{t('select option label')}</Input.Label>
158
- <div className='grid grid-cols-[1fr_min-content_min-content] gap-1'>
156
+ <div className='flex flex-col p-form-padding gap-form-gap dx-density-fine'>
157
+ <Input.Label classNames='text-xs'>{t('select-option.label')}</Input.Label>
158
+ <div className='grid grid-cols-[1fr_min-content_min-content] gap-form-gap'>
159
159
  <Input.TextInput
160
160
  disabled={!!readonly}
161
- placeholder={t('select option label placeholder')}
161
+ placeholder={t('select-option-label.placeholder')}
162
162
  ref={selected === item.id ? inputRef : undefined}
163
163
  value={item.title}
164
164
  onChange={handleTitleChange(item.id)}
@@ -170,7 +170,7 @@ export const SelectOptionField = ({
170
170
  disabled={!!readonly}
171
171
  icon='ph--trash--fill'
172
172
  iconOnly
173
- label={t('select option delete')}
173
+ label={t('select-option-delete.button')}
174
174
  onClick={() => handleDelete(item.id)}
175
175
  />
176
176
  </div>
@@ -182,7 +182,7 @@ export const SelectOptionField = ({
182
182
  <IconButton
183
183
  variant='ghost'
184
184
  icon='ph--plus--regular'
185
- label={t('select option add')}
185
+ label={t('select-option-add.button')}
186
186
  onClick={handleAdd}
187
187
  disabled={!!readonly}
188
188
  classNames='w-full'
@@ -12,8 +12,8 @@ export const TextField = ({
12
12
  type,
13
13
  readonly,
14
14
  placeholder,
15
- onValueChange,
16
15
  onBlur,
16
+ onValueChange,
17
17
  ...props
18
18
  }: FormFieldComponentProps<string>) => {
19
19
  const handleChange = useCallback<NonNullable<TextInputProps['onChange']>>(
@@ -29,8 +29,8 @@ export const TextField = ({
29
29
  disabled={!!readonly}
30
30
  placeholder={placeholder}
31
31
  value={value}
32
- onChange={handleChange}
33
32
  onBlur={onBlur}
33
+ onChange={handleChange}
34
34
  />
35
35
  )}
36
36
  </FormFieldWrapper>
@@ -32,7 +32,7 @@ export const TupleField = ({
32
32
  return (
33
33
  <Input.Root validationValence={status}>
34
34
  <FormFieldLabel error={error} readonly={readonly} label={label} />
35
- <div className={mx('grid gap-2', gridCols[binding.length - 1])}>
35
+ <div className={mx('grid gap-form-gap', gridCols[binding.length - 1])}>
36
36
  {binding.map((prop) => (
37
37
  <Input.TextInput
38
38
  key={prop}
@@ -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
+ };
@@ -0,0 +1,5 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ export * from './ObjectForm';
@@ -7,24 +7,24 @@ 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 { Filter, useQuery } from '@dxos/react-client/echo';
10
+ import { Filter, Obj } from '@dxos/echo';
11
+ import { random } from '@dxos/random';
12
+ import { useQuery } from '@dxos/react-client/echo';
13
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
16
  import { Person } from '@dxos/types';
17
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
26
  Obj.make(Person.Person, {
27
- fullName: faker.person.fullName(),
27
+ fullName: random.person.fullName(),
28
28
  });
29
29
 
30
30
  const omitId = Schema.omit<any, any, ['id']>('id');
@@ -48,7 +48,7 @@ const DefaultStory = () => {
48
48
  }
49
49
  }, [space]);
50
50
 
51
- // Get all objects in the space (similar to BoardContainer)
51
+ // Get all objects in the space (similar to BoardArticle)
52
52
  const allObjects = useQuery(space?.db, Filter.everything());
53
53
 
54
54
  // Map objects to options format expected by ObjectPicker
@@ -65,7 +65,9 @@ const DefaultStory = () => {
65
65
  const handleCreateCallback = useCallback(
66
66
  (values: any) => {
67
67
  console.log('[on create]', values);
68
- if (!space) return;
68
+ if (!space) {
69
+ return;
70
+ }
69
71
  const newPerson = space.db.add(Obj.make(Person.Person, values));
70
72
  mockHandleCreate(values);
71
73
  return newPerson;
@@ -74,7 +76,7 @@ const DefaultStory = () => {
74
76
  );
75
77
 
76
78
  return (
77
- <div role='none' className='flex w-96'>
79
+ <div className='w-96'>
78
80
  <ObjectPicker.Root open={isOpen} onOpenChange={setIsOpen}>
79
81
  <ObjectPicker.Trigger asChild>
80
82
  <Button variant='primary' data-testid='trigger' classNames='w-full'>
@@ -85,7 +87,7 @@ const DefaultStory = () => {
85
87
  classNames='dx-card-popover-width'
86
88
  options={options}
87
89
  createSchema={personSchema}
88
- createOptionLabel={['create new person label', { ns: osTranslations }]}
90
+ createOptionLabel={['create-new-person.label', { ns: osTranslations }]}
89
91
  createOptionIcon='ph--user-plus--regular'
90
92
  createInitialValuePath='fullName'
91
93
  onCreate={handleCreateCallback}
@@ -6,10 +6,13 @@ import type * as Schema from 'effect/Schema';
6
6
  import React, { type KeyboardEvent, forwardRef, useCallback, useState } from 'react';
7
7
 
8
8
  import { type Palette, Popover, type ThemedClassName, useTranslation } from '@dxos/react-ui';
9
- import { Combobox, useSearchListInput, useSearchListResults } from '@dxos/react-ui-searchlist';
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 { translationKey } from '../../translations';
12
14
  import { Form } from '../Form';
15
+ import { type FormFieldMap } from '../Form/FormFieldComponent';
13
16
 
14
17
  export type RefOption = {
15
18
  id: string;
@@ -24,36 +27,18 @@ export type ObjectPickerContentProps = ThemedClassName<{
24
27
  createOptionIcon?: string;
25
28
  createSchema?: Schema.Schema.AnyNoContext;
26
29
  createInitialValuePath?: string;
27
- onCreate?: (values: any) => void;
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>;
28
39
  onSelect: (id: string) => void;
29
40
  }>;
30
41
 
31
- const CreateItem = ({
32
- createOptionLabel,
33
- createOptionIcon,
34
- onCreateItemSelect,
35
- }: {
36
- createOptionLabel: [string, { ns: string }];
37
- createOptionIcon: string;
38
- onCreateItemSelect: (query: string) => void;
39
- }) => {
40
- const { t } = useTranslation(translationKey);
41
- const { query } = useSearchListInput();
42
-
43
- return (
44
- <Combobox.Item
45
- value='__create__'
46
- label={t(createOptionLabel[0], { ns: createOptionLabel[1].ns, text: query })}
47
- icon={createOptionIcon}
48
- classNames='flex items-center gap-2'
49
- closeOnSelect={false}
50
- onSelect={() => {
51
- onCreateItemSelect(query);
52
- }}
53
- />
54
- );
55
- };
56
-
57
42
  const ObjectPickerContent = forwardRef<HTMLDivElement, ObjectPickerContentProps>(
58
43
  (
59
44
  {
@@ -63,23 +48,24 @@ const ObjectPickerContent = forwardRef<HTMLDivElement, ObjectPickerContentProps>
63
48
  createOptionLabel,
64
49
  createOptionIcon,
65
50
  createInitialValuePath,
51
+ createFieldMap,
66
52
  onSelect,
67
53
  onCreate,
68
54
  ...props
69
55
  },
70
- ref,
56
+ forwardedRef,
71
57
  ) => {
72
58
  const { t } = useTranslation(translationKey);
73
59
  const [showForm, setShowForm] = useState(false);
74
60
  const [formInitialValue, setFormInitialValue] = useState<string>('');
75
61
 
76
- const { results, handleSearch } = useSearchListResults({
62
+ const { results, query, handleSearch } = useSearchListResults({
77
63
  items: options,
78
64
  });
79
65
 
80
66
  const handleFormSave = useCallback(
81
- (values: any) => {
82
- onCreate?.(values);
67
+ async (values: any) => {
68
+ await onCreate?.(values);
83
69
  setShowForm(false);
84
70
  setFormInitialValue('');
85
71
  },
@@ -112,14 +98,15 @@ const ObjectPickerContent = forwardRef<HTMLDivElement, ObjectPickerContentProps>
112
98
  [createSchema],
113
99
  );
114
100
 
115
- return (
116
- <Combobox.Content {...props} ref={ref} onSearch={handleSearch} onKeyDownCapture={handleKeyDown}>
117
- {showForm && createSchema ? (
101
+ if (showForm && createSchema) {
102
+ return (
103
+ <Combobox.Content {...props} onKeyDownCapture={handleKeyDown} ref={forwardedRef}>
118
104
  <Popover.Viewport>
119
105
  <Form.Root
120
106
  testId='create-referenced-object-form'
121
107
  schema={createSchema}
122
- values={createInitialValuePath ? { [createInitialValuePath]: formInitialValue } : {}}
108
+ defaultValues={createInitialValuePath ? { [createInitialValuePath]: formInitialValue } : {}}
109
+ fieldMap={createFieldMap}
123
110
  onSave={handleFormSave}
124
111
  onCancel={handleFormCancel}
125
112
  >
@@ -131,40 +118,80 @@ const ObjectPickerContent = forwardRef<HTMLDivElement, ObjectPickerContentProps>
131
118
  </Form.Viewport>
132
119
  </Form.Root>
133
120
  </Popover.Viewport>
134
- ) : (
135
- <>
136
- <Combobox.Input placeholder={t('ref field combobox input placeholder')} autoFocus />
137
- <Combobox.List>
138
- {results.map((option) => (
139
- <Combobox.Item
140
- key={option.id}
141
- value={option.id}
142
- label={option.label}
143
- checked={selectedIds?.includes(option.id)}
144
- onSelect={() => onSelect(option.id)}
145
- classNames='flex items-center gap-2'
146
- />
147
- ))}
148
- {createOptionLabel && createOptionIcon && createSchema && onCreate && (
149
- <CreateItem
150
- createOptionLabel={createOptionLabel}
151
- createOptionIcon={createOptionIcon}
152
- onCreateItemSelect={(query: string) => {
153
- setFormInitialValue(query);
154
- setShowForm(true);
155
- }}
156
- />
157
- )}
158
- </Combobox.List>
159
- </>
160
- )}
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
+ }}
154
+ />
155
+ )}
156
+ </Combobox.List>
161
157
  <Combobox.Arrow />
162
158
  </Combobox.Content>
163
159
  );
164
160
  },
165
161
  );
166
162
 
167
- 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';
168
195
 
169
196
  export const ObjectPicker = {
170
197
  Root: Combobox.Root,