@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.
- package/LICENSE +102 -5
- package/dist/lib/browser/index.mjs +1028 -625
- package/dist/lib/browser/index.mjs.map +4 -4
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/browser/translations.mjs +82 -0
- package/dist/lib/browser/translations.mjs.map +7 -0
- package/dist/lib/node-esm/index.mjs +1028 -625
- package/dist/lib/node-esm/index.mjs.map +4 -4
- package/dist/lib/node-esm/meta.json +1 -1
- package/dist/lib/node-esm/translations.mjs +84 -0
- package/dist/lib/node-esm/translations.mjs.map +7 -0
- package/dist/types/src/components/FieldEditor/FieldEditor.d.ts.map +1 -1
- package/dist/types/src/components/FieldEditor/FieldEditor.stories.d.ts +63 -61
- package/dist/types/src/components/FieldEditor/FieldEditor.stories.d.ts.map +1 -1
- package/dist/types/src/components/Form/Form.d.ts +40 -12
- package/dist/types/src/components/Form/Form.d.ts.map +1 -1
- package/dist/types/src/components/Form/Form.stories.d.ts +76 -74
- package/dist/types/src/components/Form/Form.stories.d.ts.map +1 -1
- package/dist/types/src/components/Form/FormField.d.ts +11 -5
- package/dist/types/src/components/Form/FormField.d.ts.map +1 -1
- package/dist/types/src/components/Form/FormFieldComponent.d.ts +8 -7
- package/dist/types/src/components/Form/FormFieldComponent.d.ts.map +1 -1
- package/dist/types/src/components/Form/FormFieldComponent.stories.d.ts.map +1 -1
- package/dist/types/src/components/Form/FormFieldSet.d.ts +5 -9
- package/dist/types/src/components/Form/FormFieldSet.d.ts.map +1 -1
- package/dist/types/src/components/Form/fields/ArrayField.d.ts +4 -4
- package/dist/types/src/components/Form/fields/ArrayField.d.ts.map +1 -1
- package/dist/types/src/components/Form/fields/BooleanField.d.ts.map +1 -1
- package/dist/types/src/components/Form/fields/GeoPointField.d.ts.map +1 -1
- package/dist/types/src/components/Form/fields/MarkdownField.d.ts +8 -1
- package/dist/types/src/components/Form/fields/MarkdownField.d.ts.map +1 -1
- package/dist/types/src/components/Form/fields/MarkdownField.stories.d.ts +84 -0
- package/dist/types/src/components/Form/fields/MarkdownField.stories.d.ts.map +1 -0
- package/dist/types/src/components/Form/fields/NumberField.d.ts.map +1 -1
- package/dist/types/src/components/Form/fields/RefField.d.ts +15 -7
- package/dist/types/src/components/Form/fields/RefField.d.ts.map +1 -1
- package/dist/types/src/components/Form/fields/SelectField.d.ts.map +1 -1
- package/dist/types/src/components/Form/fields/SelectOptionField.d.ts.map +1 -1
- package/dist/types/src/components/Form/fields/SelectOptionField.stories.d.ts +63 -61
- package/dist/types/src/components/Form/fields/SelectOptionField.stories.d.ts.map +1 -1
- package/dist/types/src/components/Form/fields/TextAreaField.d.ts.map +1 -1
- package/dist/types/src/components/Form/fields/TextField.d.ts +1 -1
- package/dist/types/src/components/Form/fields/TextField.d.ts.map +1 -1
- package/dist/types/src/components/Form/fields/TupleField.d.ts.map +1 -1
- package/dist/types/src/components/ObjectForm/ObjectForm.d.ts +9 -0
- package/dist/types/src/components/ObjectForm/ObjectForm.d.ts.map +1 -0
- package/dist/types/src/components/ObjectForm/index.d.ts +2 -0
- package/dist/types/src/components/ObjectForm/index.d.ts.map +1 -0
- package/dist/types/src/components/ObjectPicker/ObjectPicker.d.ts +26 -4
- package/dist/types/src/components/ObjectPicker/ObjectPicker.d.ts.map +1 -1
- package/dist/types/src/components/ObjectPicker/ObjectPicker.stories.d.ts +63 -61
- package/dist/types/src/components/ObjectPicker/ObjectPicker.stories.d.ts.map +1 -1
- package/dist/types/src/components/ObjectProperties/ObjectProperties.d.ts +9 -0
- package/dist/types/src/components/ObjectProperties/ObjectProperties.d.ts.map +1 -0
- package/dist/types/src/components/ObjectProperties/ObjectProperties.stories.d.ts +127 -0
- package/dist/types/src/components/ObjectProperties/ObjectProperties.stories.d.ts.map +1 -0
- package/dist/types/src/components/ObjectProperties/ObjectProperties.test.d.ts +2 -0
- package/dist/types/src/components/ObjectProperties/ObjectProperties.test.d.ts.map +1 -0
- package/dist/types/src/components/ObjectProperties/index.d.ts +2 -0
- package/dist/types/src/components/ObjectProperties/index.d.ts.map +1 -0
- package/dist/types/src/components/Settings/Settings.d.ts +28 -33
- package/dist/types/src/components/Settings/Settings.d.ts.map +1 -1
- package/dist/types/src/components/Settings/Settings.stories.d.ts +64 -61
- package/dist/types/src/components/Settings/Settings.stories.d.ts.map +1 -1
- package/dist/types/src/components/Settings/SettingsFieldSet.d.ts +43 -0
- package/dist/types/src/components/Settings/SettingsFieldSet.d.ts.map +1 -0
- package/dist/types/src/components/Settings/SettingsItem.d.ts +16 -0
- package/dist/types/src/components/Settings/SettingsItem.d.ts.map +1 -0
- package/dist/types/src/components/Settings/SettingsSelectField.d.ts +18 -0
- package/dist/types/src/components/Settings/SettingsSelectField.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 -1
- package/dist/types/src/components/ViewEditor/ViewEditor.d.ts +3 -4
- package/dist/types/src/components/ViewEditor/ViewEditor.d.ts.map +1 -1
- package/dist/types/src/components/ViewEditor/ViewEditor.stories.d.ts +65 -63
- package/dist/types/src/components/ViewEditor/ViewEditor.stories.d.ts.map +1 -1
- package/dist/types/src/components/index.d.ts +2 -0
- package/dist/types/src/components/index.d.ts.map +1 -1
- package/dist/types/src/components/testing.d.ts +4 -4
- package/dist/types/src/components/testing.d.ts.map +1 -1
- package/dist/types/src/hooks/useFormHandler.d.ts.map +1 -1
- package/dist/types/src/hooks/useKeyHandler.d.ts +2 -1
- package/dist/types/src/hooks/useKeyHandler.d.ts.map +1 -1
- package/dist/types/src/index.d.ts +0 -2
- package/dist/types/src/index.d.ts.map +1 -1
- package/dist/types/src/translations.d.ts +63 -61
- package/dist/types/src/translations.d.ts.map +1 -1
- package/dist/types/src/util/field-type.d.ts +19 -0
- package/dist/types/src/util/field-type.d.ts.map +1 -0
- package/dist/types/src/util/field.d.ts.map +1 -1
- package/dist/types/src/util/formatting.d.ts.map +1 -1
- package/dist/types/src/util/index.d.ts +1 -0
- package/dist/types/src/util/index.d.ts.map +1 -1
- package/dist/types/src/util/properties.d.ts +7 -1
- package/dist/types/src/util/properties.d.ts.map +1 -1
- package/dist/types/src/util/properties.test.d.ts +2 -0
- package/dist/types/src/util/properties.test.d.ts.map +1 -0
- package/dist/types/src/util/refs.d.ts +1 -0
- package/dist/types/src/util/refs.d.ts.map +1 -1
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/package.json +43 -41
- package/src/components/FieldEditor/FieldEditor.stories.tsx +2 -2
- package/src/components/FieldEditor/FieldEditor.test.tsx +0 -1
- package/src/components/FieldEditor/FieldEditor.tsx +4 -3
- package/src/components/Form/AUDIT.md +17 -0
- package/src/components/Form/Form.stories.tsx +25 -38
- package/src/components/Form/Form.tsx +77 -48
- package/src/components/Form/FormField.tsx +27 -15
- package/src/components/Form/FormFieldComponent.tsx +6 -4
- package/src/components/Form/FormFieldSet.tsx +98 -77
- package/src/components/Form/fields/ArrayField.tsx +14 -8
- package/src/components/Form/fields/GeoPointField.tsx +8 -7
- package/src/components/Form/fields/MarkdownField.stories.tsx +206 -0
- package/src/components/Form/fields/MarkdownField.tsx +135 -45
- package/src/components/Form/fields/RefField.tsx +79 -39
- package/src/components/Form/fields/SelectOptionField.stories.tsx +8 -6
- package/src/components/Form/fields/SelectOptionField.tsx +12 -12
- package/src/components/Form/fields/TextField.tsx +2 -2
- package/src/components/Form/fields/TupleField.tsx +1 -1
- package/src/components/ObjectForm/ObjectForm.tsx +114 -0
- package/src/components/ObjectForm/index.ts +5 -0
- package/src/components/ObjectPicker/ObjectPicker.stories.tsx +12 -10
- package/src/components/ObjectPicker/ObjectPicker.tsx +92 -65
- package/src/components/ObjectProperties/ObjectProperties.stories.tsx +362 -0
- package/src/components/ObjectProperties/ObjectProperties.test.tsx +101 -0
- package/src/components/ObjectProperties/ObjectProperties.tsx +148 -0
- package/src/components/ObjectProperties/index.ts +5 -0
- package/src/components/Settings/AUDIT.md +51 -0
- package/src/components/Settings/Settings.stories.tsx +69 -21
- package/src/components/Settings/Settings.tsx +33 -183
- package/src/components/Settings/SettingsFieldSet.tsx +257 -0
- package/src/components/Settings/SettingsItem.tsx +58 -0
- package/src/components/Settings/SettingsSelectField.tsx +59 -0
- package/src/components/Settings/index.ts +2 -0
- package/src/components/ViewEditor/ViewEditor.stories.tsx +14 -15
- package/src/components/ViewEditor/ViewEditor.test.tsx +0 -1
- package/src/components/ViewEditor/ViewEditor.tsx +40 -36
- package/src/components/index.ts +2 -0
- package/src/components/testing.tsx +19 -11
- package/src/hooks/useKeyHandler.tsx +7 -7
- package/src/index.ts +0 -4
- package/src/translations.ts +66 -63
- package/src/util/field-type.ts +60 -0
- package/src/util/index.ts +1 -0
- package/src/util/properties.test.ts +63 -0
- package/src/util/properties.ts +14 -3
- 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
|
|
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
|
|
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=
|
|
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
|
|
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-
|
|
156
|
-
{
|
|
157
|
-
<
|
|
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
|
|
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
|
|
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
|
|
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-
|
|
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
|
+
};
|
|
@@ -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 {
|
|
12
|
-
import {
|
|
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 '
|
|
19
|
+
import { translations } from '#translations';
|
|
20
20
|
|
|
21
21
|
import { ObjectPicker } from './ObjectPicker';
|
|
22
22
|
|
|
23
|
-
|
|
23
|
+
random.seed(1);
|
|
24
24
|
|
|
25
25
|
const createPerson = () =>
|
|
26
26
|
Obj.make(Person.Person, {
|
|
27
|
-
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
|
|
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)
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
116
|
-
|
|
117
|
-
{
|
|
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
|
-
|
|
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
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
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
|
-
|
|
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,
|