@dxos/react-ui-form 0.8.4-main.3c1ae3b → 0.8.4-main.3eb6e50203

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 (95) hide show
  1. package/dist/lib/browser/index.mjs +1610 -1825
  2. package/dist/lib/browser/index.mjs.map +4 -4
  3. package/dist/lib/browser/meta.json +1 -1
  4. package/dist/lib/node-esm/index.mjs +1610 -1825
  5. package/dist/lib/node-esm/index.mjs.map +4 -4
  6. package/dist/lib/node-esm/meta.json +1 -1
  7. package/dist/types/src/components/FieldEditor/FieldEditor.d.ts +3 -2
  8. package/dist/types/src/components/FieldEditor/FieldEditor.d.ts.map +1 -1
  9. package/dist/types/src/components/FieldEditor/FieldEditor.stories.d.ts +2 -2
  10. package/dist/types/src/components/FieldEditor/FieldEditor.stories.d.ts.map +1 -1
  11. package/dist/types/src/components/Form/Form-old.stories.d.ts +1 -42
  12. package/dist/types/src/components/Form/Form-old.stories.d.ts.map +1 -1
  13. package/dist/types/src/components/Form/Form.d.ts +9 -6
  14. package/dist/types/src/components/Form/Form.d.ts.map +1 -1
  15. package/dist/types/src/components/Form/Form.stories.d.ts +32 -32
  16. package/dist/types/src/components/Form/Form.stories.d.ts.map +1 -1
  17. package/dist/types/src/components/Form/FormField.d.ts +2 -2
  18. package/dist/types/src/components/Form/FormField.d.ts.map +1 -1
  19. package/dist/types/src/components/Form/FormFieldComponent.d.ts.map +1 -1
  20. package/dist/types/src/components/Form/FormFieldSet.d.ts +3 -3
  21. package/dist/types/src/components/Form/FormFieldSet.d.ts.map +1 -1
  22. package/dist/types/src/components/Form/fields/ArrayField.d.ts.map +1 -1
  23. package/dist/types/src/components/Form/fields/RefField.d.ts +8 -12
  24. package/dist/types/src/components/Form/fields/RefField.d.ts.map +1 -1
  25. package/dist/types/src/components/Form/fields/SelectOptionField.stories.d.ts +1 -1
  26. package/dist/types/src/components/ObjectPicker/ObjectPicker.d.ts +14 -9
  27. package/dist/types/src/components/ObjectPicker/ObjectPicker.d.ts.map +1 -1
  28. package/dist/types/src/components/ObjectPicker/ObjectPicker.stories.d.ts +1 -1
  29. package/dist/types/src/components/ObjectPicker/ObjectPicker.stories.d.ts.map +1 -1
  30. package/dist/types/src/components/Settings/Settings.d.ts +56 -0
  31. package/dist/types/src/components/Settings/Settings.d.ts.map +1 -0
  32. package/dist/types/src/components/{ControlSection/ControlSection.stories.d.ts → Settings/Settings.stories.d.ts} +3 -9
  33. package/dist/types/src/components/Settings/Settings.stories.d.ts.map +1 -0
  34. package/dist/types/src/components/Settings/index.d.ts +2 -0
  35. package/dist/types/src/components/Settings/index.d.ts.map +1 -0
  36. package/dist/types/src/components/ViewEditor/ViewEditor.d.ts +1 -1
  37. package/dist/types/src/components/ViewEditor/ViewEditor.d.ts.map +1 -1
  38. package/dist/types/src/components/ViewEditor/ViewEditor.stories.d.ts +1 -1
  39. package/dist/types/src/components/ViewEditor/ViewEditor.stories.d.ts.map +1 -1
  40. package/dist/types/src/components/index.d.ts +1 -1
  41. package/dist/types/src/components/index.d.ts.map +1 -1
  42. package/dist/types/src/components/testing.d.ts.map +1 -1
  43. package/dist/types/src/hooks/index.d.ts +0 -2
  44. package/dist/types/src/hooks/index.d.ts.map +1 -1
  45. package/dist/types/src/hooks/useFormHandler.d.ts.map +1 -1
  46. package/dist/types/src/translations.d.ts +2 -2
  47. package/dist/types/src/translations.d.ts.map +1 -1
  48. package/dist/types/src/util/echo.d.ts +6 -0
  49. package/dist/types/src/util/echo.d.ts.map +1 -0
  50. package/dist/types/src/util/index.d.ts +1 -0
  51. package/dist/types/src/util/index.d.ts.map +1 -1
  52. package/dist/types/tsconfig.tsbuildinfo +1 -1
  53. package/package.json +49 -45
  54. package/src/components/FieldEditor/FieldEditor.stories.tsx +39 -18
  55. package/src/components/FieldEditor/FieldEditor.tsx +13 -6
  56. package/src/components/Form/Form-old.stories.tsx +42 -42
  57. package/src/components/Form/Form.stories.tsx +12 -17
  58. package/src/components/Form/Form.tsx +52 -27
  59. package/src/components/Form/FormField.tsx +23 -17
  60. package/src/components/Form/FormFieldComponent.stories.tsx +1 -1
  61. package/src/components/Form/FormFieldComponent.tsx +17 -16
  62. package/src/components/Form/FormFieldSet.tsx +8 -5
  63. package/src/components/Form/fields/ArrayField.tsx +11 -11
  64. package/src/components/Form/fields/RefField.tsx +82 -99
  65. package/src/components/Form/fields/SelectOptionField.stories.tsx +1 -1
  66. package/src/components/Form/fields/SelectOptionField.tsx +2 -2
  67. package/src/components/Form/fields/TupleField.tsx +1 -1
  68. package/src/components/ObjectPicker/ObjectPicker.stories.tsx +12 -5
  69. package/src/components/ObjectPicker/ObjectPicker.tsx +66 -57
  70. package/src/components/Settings/Settings.stories.tsx +56 -0
  71. package/src/components/Settings/Settings.tsx +234 -0
  72. package/src/components/{ControlSection → Settings}/index.ts +1 -1
  73. package/src/components/ViewEditor/ViewEditor.stories.tsx +18 -12
  74. package/src/components/ViewEditor/ViewEditor.test.tsx +1 -5
  75. package/src/components/ViewEditor/ViewEditor.tsx +77 -44
  76. package/src/components/index.ts +1 -1
  77. package/src/components/testing.tsx +3 -5
  78. package/src/hooks/index.ts +0 -2
  79. package/src/hooks/useFormHandler.ts +20 -6
  80. package/src/translations.ts +1 -1
  81. package/src/util/echo.ts +25 -0
  82. package/src/util/index.ts +1 -0
  83. package/dist/types/src/components/ControlSection/ControlSection.d.ts +0 -23
  84. package/dist/types/src/components/ControlSection/ControlSection.d.ts.map +0 -1
  85. package/dist/types/src/components/ControlSection/ControlSection.stories.d.ts.map +0 -1
  86. package/dist/types/src/components/ControlSection/index.d.ts +0 -2
  87. package/dist/types/src/components/ControlSection/index.d.ts.map +0 -1
  88. package/dist/types/src/hooks/useQueryRefOptions.d.ts +0 -30
  89. package/dist/types/src/hooks/useQueryRefOptions.d.ts.map +0 -1
  90. package/dist/types/src/hooks/useRefQueryOptions.d.ts +0 -8
  91. package/dist/types/src/hooks/useRefQueryOptions.d.ts.map +0 -1
  92. package/src/components/ControlSection/ControlSection.stories.tsx +0 -38
  93. package/src/components/ControlSection/ControlSection.tsx +0 -153
  94. package/src/hooks/useQueryRefOptions.ts +0 -70
  95. package/src/hooks/useRefQueryOptions.ts +0 -37
@@ -4,38 +4,53 @@
4
4
 
5
5
  import '@dxos/lit-ui/dx-tag-picker.pcss';
6
6
 
7
- import type * as Schema from 'effect/Schema';
8
7
  import React, { useCallback, useMemo } from 'react';
9
8
 
10
- import { Ref } from '@dxos/echo';
9
+ import { type Database, Entity, Filter, Ref, Type } from '@dxos/echo';
11
10
  import { ReferenceAnnotationId, type ReferenceAnnotationValue } from '@dxos/echo/internal';
11
+ import { useQuery, useSchema as useSchema$ } from '@dxos/echo-react';
12
12
  import { findAnnotation } from '@dxos/effect';
13
13
  import { DXN } from '@dxos/keys';
14
14
  import { DxAnchor } from '@dxos/lit-ui/react';
15
15
  import { Button, Icon, Input, useTranslation } from '@dxos/react-ui';
16
- import { descriptionText, mx } from '@dxos/react-ui-theme';
17
- import { isNonNullable } from '@dxos/util';
16
+ import { descriptionText, mx } from '@dxos/ui-theme';
18
17
 
19
- import { type QueryRefOptions, useQueryRefOptions } from '../../../hooks';
20
18
  import { translationKey } from '../../../translations';
21
- import { ObjectPicker } from '../../ObjectPicker';
19
+ import { ObjectPicker, type ObjectPickerContentProps, type RefOption } from '../../ObjectPicker';
20
+ import { omitId } from '../Form';
22
21
  import { type FormFieldComponentProps, FormFieldLabel } from '../FormFieldComponent';
23
22
 
24
23
  // TODO(burdon): Factor out.
25
- const isRefSnapShot = (val: any): val is { '/': string } => {
24
+ const isRefSnapshot = (val: any): val is { '/': string } => {
26
25
  return typeof val === 'object' && typeof (val as any)?.['/'] === 'string';
27
26
  };
28
27
 
29
- export type RefFieldProps = FormFieldComponentProps & {
30
- // TODO(wittjosiah): Remove this. Array is handled upstream.
31
- array?: boolean;
32
- createOptionLabel?: [string, { ns: string }];
33
- createOptionIcon?: string;
34
- createSchema?: Schema.Schema.AnyNoContext;
35
- createInitialValuePath?: string;
36
- onCreate?: (values: any) => void;
37
- onQueryRefOptions?: QueryRefOptions;
38
- };
28
+ const defaultGetOptions: NonNullable<RefFieldProps['getOptions']> = (results) =>
29
+ results.map((result) => {
30
+ const id = Entity.getDXN(result).toString();
31
+ const label = Entity.getLabel(result);
32
+ return { id, label: label ?? id };
33
+ });
34
+
35
+ const defaultResultsHook: NonNullable<RefFieldProps['resultsHook']> = (db, typename) =>
36
+ useQuery(
37
+ db,
38
+ typename
39
+ ? // For Type.Ref(Type.Obj) we want to show all objects.
40
+ typename === Type.getTypename(Type.Obj)
41
+ ? Filter.everything()
42
+ : Filter.typename(typename)
43
+ : Filter.nothing(),
44
+ );
45
+
46
+ export type RefFieldProps = FormFieldComponentProps &
47
+ Pick<ObjectPickerContentProps, 'createOptionLabel' | 'createOptionIcon' | 'createInitialValuePath'> & {
48
+ db?: Database.Database;
49
+ resultsHook?: (db?: Database.Database, typename?: string) => Entity.Any[];
50
+ schemaHook?: (db?: Database.Database, typename?: string) => Type.Entity.Any;
51
+ getOptions?: (objects: Entity.Any[]) => RefOption[];
52
+ onCreate?: (schema: Type.Entity.Any, values: any) => void;
53
+ };
39
54
 
40
55
  export const RefField = (props: RefFieldProps) => {
41
56
  const {
@@ -46,13 +61,14 @@ export const RefField = (props: RefFieldProps) => {
46
61
  layout,
47
62
  getStatus,
48
63
  getValue,
49
- array,
50
64
  createOptionLabel,
51
65
  createOptionIcon,
52
- createSchema,
53
66
  createInitialValuePath,
67
+ db,
68
+ resultsHook: useResults = defaultResultsHook,
69
+ schemaHook: useSchema = useSchema$,
70
+ getOptions = defaultGetOptions,
54
71
  onCreate,
55
- onQueryRefOptions,
56
72
  onValueChange,
57
73
  } = props;
58
74
  const { t } = useTranslation(translationKey);
@@ -63,15 +79,15 @@ export const RefField = (props: RefFieldProps) => {
63
79
  [type],
64
80
  );
65
81
 
66
- // TODO(burdon): Query items on demand.
67
- const { options, update: updateOptions } = useQueryRefOptions({ typename, onQueryRefOptions });
82
+ const results = useResults(db, typename);
83
+ const options = useMemo(() => getOptions(results), [results, getOptions]);
68
84
 
69
85
  const handleGetValue = useCallback(() => {
70
86
  const formValue = getValue();
71
87
 
72
88
  const unknownToRefOption = (value: unknown) => {
73
89
  const isRef = Ref.isRef(value);
74
- if (isRef || isRefSnapShot(value)) {
90
+ if (isRef || isRefSnapshot(value)) {
75
91
  const dxnString = isRef ? value.dxn.toString() : value['/'];
76
92
  const matchingOption = options.find((option) => option.id === dxnString);
77
93
  if (matchingOption) {
@@ -81,72 +97,43 @@ export const RefField = (props: RefFieldProps) => {
81
97
  return undefined;
82
98
  };
83
99
 
84
- if (array && Array.isArray(formValue)) {
85
- return formValue.map(unknownToRefOption).filter(isNonNullable) ?? [];
86
- }
87
-
88
- const option = unknownToRefOption(formValue);
89
- if (option) {
90
- return [option];
91
- }
92
-
93
- return [];
94
- }, [options, array, getValue]);
100
+ return unknownToRefOption(formValue);
101
+ }, [options, getValue]);
95
102
 
96
103
  const handleUpdate = useCallback(
97
- (ids: string[]) => {
98
- if (ids.length === 0) {
99
- onValueChange(type, undefined);
100
- return;
101
- }
102
-
103
- const refs = ids
104
- .map((id) => {
105
- const item = options.find((option) => option.id === id);
106
- if (item) {
107
- const dxn = DXN.parse(item.id);
108
- return Ref.fromDXN(dxn);
109
- }
110
- return null;
111
- })
112
- .filter(isNonNullable);
113
-
114
- if (array) {
115
- onValueChange(type, refs);
116
- } else {
117
- onValueChange(type, refs[0]);
118
- }
104
+ (id: string | undefined) => {
105
+ const item = options.find((option) => option.id === id);
106
+ const ref = item ? Ref.fromDXN(DXN.parse(item.id)) : undefined;
107
+ onValueChange(type, ref);
119
108
  },
120
- [options, type, array, onValueChange],
109
+ [options, type, onValueChange],
121
110
  );
122
111
 
112
+ const item = handleGetValue();
113
+ const selectedIds = useMemo(() => (item ? [item.id] : []), [item]);
114
+ const createSchema = useSchema(db, typename);
115
+
123
116
  const handleCreate = useCallback(
124
117
  (values: any) => {
125
- onCreate?.(values);
126
- updateOptions();
118
+ if (createSchema && onCreate) {
119
+ onCreate(createSchema, values);
120
+ }
127
121
  },
128
- [onCreate, updateOptions],
122
+ [createSchema, onCreate],
129
123
  );
130
124
 
131
- const items = handleGetValue();
132
- const selectedIds = useMemo(() => items.map((i: any) => i.id), [items]);
133
- const toggleSelect = useCallback(
125
+ const handleSelect = useCallback(
134
126
  (id: string) => {
135
- if (array) {
136
- const nextIds = selectedIds.includes(id) ? selectedIds.filter((x) => x !== id) : [...selectedIds, id];
137
- handleUpdate(nextIds);
127
+ if (item?.id === id) {
128
+ handleUpdate(undefined);
138
129
  } else {
139
- if (selectedIds[0] === id) {
140
- handleUpdate([]);
141
- } else {
142
- handleUpdate([id]);
143
- }
130
+ handleUpdate(id);
144
131
  }
145
132
  },
146
- [array, selectedIds, handleUpdate],
133
+ [item, handleUpdate],
147
134
  );
148
135
 
149
- if (!typename || ((readonly || layout === 'static') && items.length < 1)) {
136
+ if (!typename || ((readonly || layout === 'static') && !item)) {
150
137
  return null;
151
138
  }
152
139
 
@@ -155,48 +142,44 @@ export const RefField = (props: RefFieldProps) => {
155
142
  {layout !== 'inline' && <FormFieldLabel error={error} readonly={readonly} label={label} />}
156
143
  <div>
157
144
  {readonly ? (
158
- items.length < 1 ? (
145
+ !item ? (
159
146
  <p className={mx(descriptionText, 'mbe-2')}>{t('empty readonly ref field label')}</p>
160
147
  ) : (
161
- items.map((item) => (
162
- <DxAnchor key={item.id} refid={item.id} rootclassname='mie-1'>
163
- {item.label}
164
- </DxAnchor>
165
- ))
148
+ <DxAnchor key={item.id} dxn={item.id} rootclassname='mie-1'>
149
+ {item.label}
150
+ </DxAnchor>
166
151
  )
167
152
  ) : (
168
153
  <ObjectPicker.Root>
169
154
  <ObjectPicker.Trigger asChild classNames='p-0'>
170
- {items?.length === 1 ? (
155
+ {item ? (
171
156
  <div className='flex gap-2 is-full'>
172
- {items?.map((item) => (
173
- <Input.Root key={item.id}>
174
- <Input.TextInput value={item.label} readOnly classNames='is-full' />
175
- </Input.Root>
176
- ))}
157
+ <Input.Root key={item.id}>
158
+ <Input.TextInput value={item.label} readOnly classNames='is-full' />
159
+ </Input.Root>
177
160
  </div>
178
161
  ) : (
179
162
  <Button classNames='is-full text-start gap-2'>
180
163
  <div role='none' className='grow overflow-hidden'>
181
- <span className='flex truncate text-description'>
182
- {placeholder ?? t('ref field placeholder', { count: array ? 99 : 1 })}
183
- </span>
164
+ <span className='flex truncate text-description'>{placeholder ?? t('ref field placeholder')}</span>
184
165
  </div>
185
166
  <Icon size={3} icon='ph--caret-down--bold' />
186
167
  </Button>
187
168
  )}
188
169
  </ObjectPicker.Trigger>
189
- <ObjectPicker.Content
190
- classNames='popover-card-width'
191
- options={options}
192
- selectedIds={selectedIds}
193
- createSchema={createSchema}
194
- createOptionLabel={createOptionLabel}
195
- createOptionIcon={createOptionIcon}
196
- createInitialValuePath={createInitialValuePath}
197
- onCreate={handleCreate}
198
- onSelect={toggleSelect}
199
- />
170
+ <ObjectPicker.Portal>
171
+ <ObjectPicker.Content
172
+ classNames='popover-card-width'
173
+ options={options}
174
+ selectedIds={selectedIds}
175
+ createSchema={createSchema && omitId(createSchema)}
176
+ createOptionLabel={createOptionLabel}
177
+ createOptionIcon={createOptionIcon}
178
+ createInitialValuePath={createInitialValuePath}
179
+ onCreate={handleCreate}
180
+ onSelect={handleSelect}
181
+ />
182
+ </ObjectPicker.Portal>
200
183
  </ObjectPicker.Root>
201
184
  )}
202
185
  </div>
@@ -50,7 +50,7 @@ const meta = {
50
50
  title: 'ui/react-ui-form/SelectOptionField',
51
51
  component: SelectOptionField as any,
52
52
  render: DefaultStory,
53
- decorators: [withTheme],
53
+ decorators: [withTheme()],
54
54
  parameters: {
55
55
  layout: 'fullscreen',
56
56
  translations,
@@ -9,7 +9,7 @@ import { PublicKey } from '@dxos/keys';
9
9
  import { type ChromaticPalette, IconButton, Input, Tag, useTranslation } from '@dxos/react-ui';
10
10
  import { List } from '@dxos/react-ui-list';
11
11
  import { HuePicker } from '@dxos/react-ui-pickers';
12
- import { hues, subtleHover } from '@dxos/react-ui-theme';
12
+ import { hues, osTranslations, subtleHover } from '@dxos/ui-theme';
13
13
 
14
14
  import { translationKey } from '../../../translations';
15
15
  import { type FormFieldComponentProps, FormFieldLabel } from '../FormFieldComponent';
@@ -146,7 +146,7 @@ export const SelectOptionField = ({
146
146
  <IconButton
147
147
  iconOnly
148
148
  variant='ghost'
149
- label={t(selected === item.id ? 'collapse label' : 'expand label', { ns: 'os' })}
149
+ label={t(selected === item.id ? 'collapse label' : 'expand label', { ns: osTranslations })}
150
150
  icon={selected === item.id ? 'ph--caret-down--regular' : 'ph--caret-right--regular'}
151
151
  onClick={() => handleClick(item.id)}
152
152
  />
@@ -5,7 +5,7 @@
5
5
  import React from 'react';
6
6
 
7
7
  import { Input } from '@dxos/react-ui';
8
- import { mx } from '@dxos/react-ui-theme';
8
+ import { mx } from '@dxos/ui-theme';
9
9
  import { safeParseFloat } from '@dxos/util';
10
10
 
11
11
  import { type FormFieldComponentProps, FormFieldLabel } from '../FormFieldComponent';
@@ -10,10 +10,11 @@ import { expect, fn, userEvent, within } from 'storybook/test';
10
10
  import { Obj } from '@dxos/echo';
11
11
  import { faker } 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
16
  import { Person } from '@dxos/types';
17
+ import { osTranslations } from '@dxos/ui-theme';
17
18
 
18
19
  import { translations } from '../../translations';
19
20
 
@@ -34,7 +35,7 @@ const mockHandleSelect = fn();
34
35
  const mockHandleCreate = fn();
35
36
 
36
37
  const DefaultStory = () => {
37
- const { space } = useClientProvider();
38
+ const { space } = useClientStory();
38
39
  const [isOpen, setIsOpen] = useState(false);
39
40
 
40
41
  // Create sample Person objects
@@ -48,7 +49,7 @@ const DefaultStory = () => {
48
49
  }, [space]);
49
50
 
50
51
  // Get all objects in the space (similar to BoardContainer)
51
- const allObjects = useQuery(space, Filter.everything());
52
+ const allObjects = useQuery(space?.db, Filter.everything());
52
53
 
53
54
  // Map objects to options format expected by ObjectPicker
54
55
  const options = useMemo(
@@ -84,7 +85,7 @@ const DefaultStory = () => {
84
85
  classNames='popover-card-width'
85
86
  options={options}
86
87
  createSchema={personSchema}
87
- createOptionLabel={['create new person label', { ns: 'os' }]}
88
+ createOptionLabel={['create new person label', { ns: osTranslations }]}
88
89
  createOptionIcon='ph--user-plus--regular'
89
90
  createInitialValuePath='fullName'
90
91
  onCreate={handleCreateCallback}
@@ -99,7 +100,7 @@ const meta = {
99
100
  title: 'ui/react-ui-form/ObjectPicker',
100
101
  render: DefaultStory,
101
102
  decorators: [
102
- withTheme,
103
+ withTheme(),
103
104
  withClientProvider({
104
105
  types: [Person.Person],
105
106
  createIdentity: true,
@@ -202,6 +203,12 @@ export const WithTest: Story = {
202
203
  // Check that clicking the save button calls onCreate with expected values.
203
204
  await expect(mockHandleCreate).toHaveBeenCalledWith({
204
205
  fullName: unrelatedTerm,
206
+ addresses: [],
207
+ phoneNumbers: [],
208
+ emails: [],
209
+ identities: [],
210
+ urls: [],
211
+ fields: [],
205
212
  });
206
213
  },
207
214
  };
@@ -3,26 +3,57 @@
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-searchlist';
10
10
 
11
- import { type QueryTag } from '../../hooks';
12
11
  import { translationKey } from '../../translations';
13
12
  import { Form } from '../Form';
14
13
 
14
+ export type RefOption = {
15
+ id: string;
16
+ label: string;
17
+ hue?: Palette;
18
+ };
19
+
15
20
  export type ObjectPickerContentProps = ThemedClassName<{
16
- options: QueryTag[];
21
+ options: RefOption[];
17
22
  selectedIds?: string[];
18
- createSchema?: Schema.Schema.AnyNoContext;
19
23
  createOptionLabel?: [string, { ns: string }];
20
24
  createOptionIcon?: string;
25
+ createSchema?: Schema.Schema.AnyNoContext;
21
26
  createInitialValuePath?: string;
22
- onSelect: (id: string) => void;
23
27
  onCreate?: (values: any) => void;
28
+ onSelect: (id: string) => void;
24
29
  }>;
25
30
 
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
+
26
57
  const ObjectPickerContent = forwardRef<HTMLDivElement, ObjectPickerContentProps>(
27
58
  (
28
59
  {
@@ -40,36 +71,26 @@ const ObjectPickerContent = forwardRef<HTMLDivElement, ObjectPickerContentProps>
40
71
  ) => {
41
72
  const { t } = useTranslation(translationKey);
42
73
  const [showForm, setShowForm] = useState(false);
43
- const [searchString, setSearchString] = useState('');
74
+ const [formInitialValue, setFormInitialValue] = useState<string>('');
75
+
76
+ const { results, handleSearch } = useSearchListResults({
77
+ items: options,
78
+ });
44
79
 
45
80
  const handleFormSave = useCallback(
46
81
  (values: any) => {
47
82
  onCreate?.(values);
48
83
  setShowForm(false);
49
- setSearchString('');
84
+ setFormInitialValue('');
50
85
  },
51
86
  [onCreate],
52
87
  );
53
88
 
54
89
  const handleFormCancel = useCallback(() => {
55
90
  setShowForm(false);
56
- setSearchString('');
91
+ setFormInitialValue('');
57
92
  }, []);
58
93
 
59
- // TODO(thure): The following click and keydown handlers are necessary because `onSelect` is called after the Popover is already closed.
60
- // Augment/refactor CmdK, if possible, to facilitate stopping event defaultand propagation.
61
-
62
- const handleClick = useCallback(
63
- (event: MouseEvent) => {
64
- if (createSchema && (event.target as HTMLElement).closest('[data-value="__create__"]')) {
65
- event.stopPropagation();
66
- event.preventDefault();
67
- setShowForm(true);
68
- }
69
- },
70
- [createSchema],
71
- );
72
-
73
94
  const handleKeyDown = useCallback(
74
95
  (event: KeyboardEvent) => {
75
96
  if (
@@ -81,35 +102,24 @@ const ObjectPickerContent = forwardRef<HTMLDivElement, ObjectPickerContentProps>
81
102
  ) {
82
103
  event.stopPropagation();
83
104
  event.preventDefault();
105
+ // Get the current query from the input element
106
+ const input = (event.currentTarget as HTMLElement).querySelector('input[type="text"]') as HTMLInputElement;
107
+ const currentQuery = input?.value || '';
108
+ setFormInitialValue(currentQuery);
84
109
  setShowForm(true);
85
110
  }
86
111
  },
87
112
  [createSchema],
88
113
  );
89
114
 
90
- const labelById = useMemo(
91
- () =>
92
- options.reduce((acc: Record<string, string>, option) => {
93
- acc[option.id.toLowerCase()] = option.label.toLowerCase();
94
- return acc;
95
- }, {}),
96
- [options],
97
- );
98
-
99
115
  return (
100
- <Combobox.Content
101
- {...props}
102
- ref={ref}
103
- filter={(value, search) => (value === '__create__' || labelById[value]?.includes(search.toLowerCase()) ? 1 : 0)}
104
- onClickCapture={handleClick}
105
- onKeyDownCapture={handleKeyDown}
106
- >
116
+ <Combobox.Content {...props} ref={ref} onSearch={handleSearch} onKeyDownCapture={handleKeyDown}>
107
117
  {showForm && createSchema ? (
108
118
  <Popover.Viewport>
109
119
  <Form.Root
110
120
  testId='create-referenced-object-form'
111
121
  schema={createSchema}
112
- values={createInitialValuePath ? { [createInitialValuePath]: searchString } : {}}
122
+ values={createInitialValuePath ? { [createInitialValuePath]: formInitialValue } : {}}
113
123
  onSave={handleFormSave}
114
124
  onCancel={handleFormCancel}
115
125
  >
@@ -123,29 +133,27 @@ const ObjectPickerContent = forwardRef<HTMLDivElement, ObjectPickerContentProps>
123
133
  </Popover.Viewport>
124
134
  ) : (
125
135
  <>
126
- <Combobox.Input
127
- placeholder={t('ref field combobox input placeholder')}
128
- value={searchString}
129
- onValueChange={setSearchString}
130
- autoFocus
131
- />
136
+ <Combobox.Input placeholder={t('ref field combobox input placeholder')} autoFocus />
132
137
  <Combobox.List>
133
- {options.map((option) => (
138
+ {results.map((option) => (
134
139
  <Combobox.Item
135
140
  key={option.id}
136
141
  value={option.id}
142
+ label={option.label}
143
+ checked={selectedIds?.includes(option.id)}
137
144
  onSelect={() => onSelect(option.id)}
138
145
  classNames='flex items-center gap-2'
139
- >
140
- <span className='grow'>{option.label}</span>
141
- {selectedIds?.includes(option.id) && <Icon icon='ph--check--regular' />}
142
- </Combobox.Item>
146
+ />
143
147
  ))}
144
- {searchString.length > 0 && createOptionLabel && createOptionIcon && createSchema && onCreate && (
145
- <Combobox.Item value='__create__' classNames='flex items-center gap-2'>
146
- <Icon icon={createOptionIcon} />
147
- {t(createOptionLabel[0], { ns: createOptionLabel[1].ns, text: searchString })}
148
- </Combobox.Item>
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
+ />
149
157
  )}
150
158
  </Combobox.List>
151
159
  </>
@@ -160,6 +168,7 @@ ObjectPickerContent.displayName = 'ObjectPickerContent';
160
168
 
161
169
  export const ObjectPicker = {
162
170
  Root: Combobox.Root,
171
+ Portal: Combobox.Portal,
163
172
  Trigger: Combobox.Trigger,
164
173
  VirtualTrigger: Combobox.VirtualTrigger,
165
174
  Content: ObjectPickerContent,
@@ -0,0 +1,56 @@
1
+ //
2
+ // Copyright 2024 DXOS.org
3
+ //
4
+
5
+ import { type Meta, type StoryObj } from '@storybook/react-vite';
6
+ import React from 'react';
7
+
8
+ import { faker } from '@dxos/random';
9
+ import { Button, Input } from '@dxos/react-ui';
10
+ import { withLayout, withTheme } from '@dxos/react-ui/testing';
11
+
12
+ import { translations } from '../../translations';
13
+
14
+ import { Settings } from './Settings';
15
+
16
+ faker.seed(132);
17
+
18
+ const DefaultStory = () => {
19
+ return (
20
+ <Settings.Root>
21
+ <Settings.Section title='Settings' description={faker.lorem.paragraphs(1)}>
22
+ <Settings.Group>
23
+ <Settings.ItemInput title={faker.lorem.sentence(2)} description={faker.lorem.paragraphs(1)}>
24
+ <Input.Root>
25
+ <Input.TextInput placeholder='Input' />
26
+ </Input.Root>
27
+ </Settings.ItemInput>
28
+ <Settings.ItemInput title={faker.lorem.sentence(2)} description={faker.lorem.paragraphs(2)}>
29
+ <Input.Root>
30
+ <Input.Switch />
31
+ </Input.Root>
32
+ </Settings.ItemInput>
33
+ <Settings.ItemInput title={faker.lorem.sentence(3)} description={faker.lorem.paragraphs(2)}>
34
+ <Button>Test</Button>
35
+ </Settings.ItemInput>
36
+ </Settings.Group>
37
+ </Settings.Section>
38
+ </Settings.Root>
39
+ );
40
+ };
41
+
42
+ const meta = {
43
+ title: 'ui/react-ui-form/Settings',
44
+ render: DefaultStory,
45
+ decorators: [withTheme(), withLayout({ layout: 'fullscreen' })],
46
+ parameters: {
47
+ layout: 'fullscreen',
48
+ translations,
49
+ },
50
+ } satisfies Meta;
51
+
52
+ export default meta;
53
+
54
+ type Story = StoryObj<typeof meta>;
55
+
56
+ export const Default: Story = {};