@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.
- package/dist/lib/browser/index.mjs +1610 -1825
- package/dist/lib/browser/index.mjs.map +4 -4
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/node-esm/index.mjs +1610 -1825
- package/dist/lib/node-esm/index.mjs.map +4 -4
- package/dist/lib/node-esm/meta.json +1 -1
- package/dist/types/src/components/FieldEditor/FieldEditor.d.ts +3 -2
- package/dist/types/src/components/FieldEditor/FieldEditor.d.ts.map +1 -1
- package/dist/types/src/components/FieldEditor/FieldEditor.stories.d.ts +2 -2
- package/dist/types/src/components/FieldEditor/FieldEditor.stories.d.ts.map +1 -1
- package/dist/types/src/components/Form/Form-old.stories.d.ts +1 -42
- package/dist/types/src/components/Form/Form-old.stories.d.ts.map +1 -1
- package/dist/types/src/components/Form/Form.d.ts +9 -6
- package/dist/types/src/components/Form/Form.d.ts.map +1 -1
- package/dist/types/src/components/Form/Form.stories.d.ts +32 -32
- package/dist/types/src/components/Form/Form.stories.d.ts.map +1 -1
- package/dist/types/src/components/Form/FormField.d.ts +2 -2
- package/dist/types/src/components/Form/FormField.d.ts.map +1 -1
- package/dist/types/src/components/Form/FormFieldComponent.d.ts.map +1 -1
- package/dist/types/src/components/Form/FormFieldSet.d.ts +3 -3
- package/dist/types/src/components/Form/FormFieldSet.d.ts.map +1 -1
- package/dist/types/src/components/Form/fields/ArrayField.d.ts.map +1 -1
- package/dist/types/src/components/Form/fields/RefField.d.ts +8 -12
- package/dist/types/src/components/Form/fields/RefField.d.ts.map +1 -1
- package/dist/types/src/components/Form/fields/SelectOptionField.stories.d.ts +1 -1
- package/dist/types/src/components/ObjectPicker/ObjectPicker.d.ts +14 -9
- package/dist/types/src/components/ObjectPicker/ObjectPicker.d.ts.map +1 -1
- package/dist/types/src/components/ObjectPicker/ObjectPicker.stories.d.ts +1 -1
- package/dist/types/src/components/ObjectPicker/ObjectPicker.stories.d.ts.map +1 -1
- package/dist/types/src/components/Settings/Settings.d.ts +56 -0
- package/dist/types/src/components/Settings/Settings.d.ts.map +1 -0
- package/dist/types/src/components/{ControlSection/ControlSection.stories.d.ts → Settings/Settings.stories.d.ts} +3 -9
- package/dist/types/src/components/Settings/Settings.stories.d.ts.map +1 -0
- package/dist/types/src/components/Settings/index.d.ts +2 -0
- package/dist/types/src/components/Settings/index.d.ts.map +1 -0
- package/dist/types/src/components/ViewEditor/ViewEditor.d.ts +1 -1
- package/dist/types/src/components/ViewEditor/ViewEditor.d.ts.map +1 -1
- package/dist/types/src/components/ViewEditor/ViewEditor.stories.d.ts +1 -1
- package/dist/types/src/components/ViewEditor/ViewEditor.stories.d.ts.map +1 -1
- package/dist/types/src/components/index.d.ts +1 -1
- package/dist/types/src/components/index.d.ts.map +1 -1
- package/dist/types/src/components/testing.d.ts.map +1 -1
- package/dist/types/src/hooks/index.d.ts +0 -2
- package/dist/types/src/hooks/index.d.ts.map +1 -1
- package/dist/types/src/hooks/useFormHandler.d.ts.map +1 -1
- package/dist/types/src/translations.d.ts +2 -2
- package/dist/types/src/translations.d.ts.map +1 -1
- package/dist/types/src/util/echo.d.ts +6 -0
- package/dist/types/src/util/echo.d.ts.map +1 -0
- package/dist/types/src/util/index.d.ts +1 -0
- package/dist/types/src/util/index.d.ts.map +1 -1
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/package.json +49 -45
- package/src/components/FieldEditor/FieldEditor.stories.tsx +39 -18
- package/src/components/FieldEditor/FieldEditor.tsx +13 -6
- package/src/components/Form/Form-old.stories.tsx +42 -42
- package/src/components/Form/Form.stories.tsx +12 -17
- package/src/components/Form/Form.tsx +52 -27
- package/src/components/Form/FormField.tsx +23 -17
- package/src/components/Form/FormFieldComponent.stories.tsx +1 -1
- package/src/components/Form/FormFieldComponent.tsx +17 -16
- package/src/components/Form/FormFieldSet.tsx +8 -5
- package/src/components/Form/fields/ArrayField.tsx +11 -11
- package/src/components/Form/fields/RefField.tsx +82 -99
- package/src/components/Form/fields/SelectOptionField.stories.tsx +1 -1
- package/src/components/Form/fields/SelectOptionField.tsx +2 -2
- package/src/components/Form/fields/TupleField.tsx +1 -1
- package/src/components/ObjectPicker/ObjectPicker.stories.tsx +12 -5
- package/src/components/ObjectPicker/ObjectPicker.tsx +66 -57
- package/src/components/Settings/Settings.stories.tsx +56 -0
- package/src/components/Settings/Settings.tsx +234 -0
- package/src/components/{ControlSection → Settings}/index.ts +1 -1
- package/src/components/ViewEditor/ViewEditor.stories.tsx +18 -12
- package/src/components/ViewEditor/ViewEditor.test.tsx +1 -5
- package/src/components/ViewEditor/ViewEditor.tsx +77 -44
- package/src/components/index.ts +1 -1
- package/src/components/testing.tsx +3 -5
- package/src/hooks/index.ts +0 -2
- package/src/hooks/useFormHandler.ts +20 -6
- package/src/translations.ts +1 -1
- package/src/util/echo.ts +25 -0
- package/src/util/index.ts +1 -0
- package/dist/types/src/components/ControlSection/ControlSection.d.ts +0 -23
- package/dist/types/src/components/ControlSection/ControlSection.d.ts.map +0 -1
- package/dist/types/src/components/ControlSection/ControlSection.stories.d.ts.map +0 -1
- package/dist/types/src/components/ControlSection/index.d.ts +0 -2
- package/dist/types/src/components/ControlSection/index.d.ts.map +0 -1
- package/dist/types/src/hooks/useQueryRefOptions.d.ts +0 -30
- package/dist/types/src/hooks/useQueryRefOptions.d.ts.map +0 -1
- package/dist/types/src/hooks/useRefQueryOptions.d.ts +0 -8
- package/dist/types/src/hooks/useRefQueryOptions.d.ts.map +0 -1
- package/src/components/ControlSection/ControlSection.stories.tsx +0 -38
- package/src/components/ControlSection/ControlSection.tsx +0 -153
- package/src/hooks/useQueryRefOptions.ts +0 -70
- 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/
|
|
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
|
|
24
|
+
const isRefSnapshot = (val: any): val is { '/': string } => {
|
|
26
25
|
return typeof val === 'object' && typeof (val as any)?.['/'] === 'string';
|
|
27
26
|
};
|
|
28
27
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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
|
-
|
|
67
|
-
const
|
|
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 ||
|
|
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
|
-
|
|
85
|
-
|
|
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
|
-
(
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
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,
|
|
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
|
|
126
|
-
|
|
118
|
+
if (createSchema && onCreate) {
|
|
119
|
+
onCreate(createSchema, values);
|
|
120
|
+
}
|
|
127
121
|
},
|
|
128
|
-
[
|
|
122
|
+
[createSchema, onCreate],
|
|
129
123
|
);
|
|
130
124
|
|
|
131
|
-
const
|
|
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 (
|
|
136
|
-
|
|
137
|
-
handleUpdate(nextIds);
|
|
127
|
+
if (item?.id === id) {
|
|
128
|
+
handleUpdate(undefined);
|
|
138
129
|
} else {
|
|
139
|
-
|
|
140
|
-
handleUpdate([]);
|
|
141
|
-
} else {
|
|
142
|
-
handleUpdate([id]);
|
|
143
|
-
}
|
|
130
|
+
handleUpdate(id);
|
|
144
131
|
}
|
|
145
132
|
},
|
|
146
|
-
[
|
|
133
|
+
[item, handleUpdate],
|
|
147
134
|
);
|
|
148
135
|
|
|
149
|
-
if (!typename || ((readonly || layout === 'static') &&
|
|
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
|
-
|
|
145
|
+
!item ? (
|
|
159
146
|
<p className={mx(descriptionText, 'mbe-2')}>{t('empty readonly ref field label')}</p>
|
|
160
147
|
) : (
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
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
|
-
{
|
|
155
|
+
{item ? (
|
|
171
156
|
<div className='flex gap-2 is-full'>
|
|
172
|
-
{
|
|
173
|
-
<Input.
|
|
174
|
-
|
|
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.
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
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>
|
|
@@ -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/
|
|
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:
|
|
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/
|
|
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 {
|
|
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 } =
|
|
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:
|
|
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,
|
|
6
|
+
import React, { type KeyboardEvent, forwardRef, useCallback, useState } from 'react';
|
|
7
7
|
|
|
8
|
-
import {
|
|
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:
|
|
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 [
|
|
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
|
-
|
|
84
|
+
setFormInitialValue('');
|
|
50
85
|
},
|
|
51
86
|
[onCreate],
|
|
52
87
|
);
|
|
53
88
|
|
|
54
89
|
const handleFormCancel = useCallback(() => {
|
|
55
90
|
setShowForm(false);
|
|
56
|
-
|
|
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]:
|
|
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
|
-
{
|
|
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
|
-
{
|
|
145
|
-
<
|
|
146
|
-
|
|
147
|
-
{
|
|
148
|
-
|
|
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 = {};
|