@dxos/react-ui-form 0.8.4-main.937b3ca → 0.8.4-main.9be5663bfe
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/README.md +0 -1
- package/dist/lib/browser/index.mjs +1003 -668
- package/dist/lib/browser/index.mjs.map +4 -4
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/node-esm/index.mjs +1003 -668
- 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 -3
- package/dist/types/src/components/FieldEditor/FieldEditor.d.ts.map +1 -1
- package/dist/types/src/components/FieldEditor/FieldEditor.stories.d.ts +62 -61
- package/dist/types/src/components/FieldEditor/FieldEditor.stories.d.ts.map +1 -1
- package/dist/types/src/components/Form/Form.d.ts +23 -18
- package/dist/types/src/components/Form/Form.d.ts.map +1 -1
- package/dist/types/src/components/Form/Form.stories.d.ts +68 -67
- package/dist/types/src/components/Form/Form.stories.d.ts.map +1 -1
- package/dist/types/src/components/Form/FormField.d.ts +7 -1
- package/dist/types/src/components/Form/FormField.d.ts.map +1 -1
- package/dist/types/src/components/Form/FormFieldComponent.d.ts +0 -1
- package/dist/types/src/components/Form/FormFieldComponent.d.ts.map +1 -1
- package/dist/types/src/components/Form/FormFieldComponent.stories.d.ts +2 -2
- package/dist/types/src/components/Form/FormFieldComponent.stories.d.ts.map +1 -1
- package/dist/types/src/components/Form/FormFieldSet.d.ts +9 -10
- 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 -6
- package/dist/types/src/components/Form/fields/RefField.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 +62 -61
- package/dist/types/src/components/Form/fields/SelectOptionField.stories.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 +10 -2
- package/dist/types/src/components/ObjectPicker/ObjectPicker.d.ts.map +1 -1
- package/dist/types/src/components/ObjectPicker/ObjectPicker.stories.d.ts +62 -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 +83 -0
- package/dist/types/src/components/ObjectProperties/ObjectProperties.stories.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 +35 -0
- package/dist/types/src/components/Settings/Settings.d.ts.map +1 -0
- package/dist/types/src/components/Settings/Settings.stories.d.ts +82 -0
- 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 +5 -5
- package/dist/types/src/components/ViewEditor/ViewEditor.d.ts.map +1 -1
- package/dist/types/src/components/ViewEditor/ViewEditor.stories.d.ts +66 -64
- package/dist/types/src/components/ViewEditor/ViewEditor.stories.d.ts.map +1 -1
- package/dist/types/src/components/index.d.ts +3 -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/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/translations.d.ts +62 -61
- package/dist/types/src/translations.d.ts.map +1 -1
- 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 +37 -38
- package/src/components/FieldEditor/FieldEditor.stories.tsx +7 -8
- package/src/components/FieldEditor/FieldEditor.test.tsx +0 -1
- package/src/components/FieldEditor/FieldEditor.tsx +3 -4
- package/src/components/Form/AUDIT.md +17 -0
- package/src/components/Form/Form.stories.tsx +25 -26
- package/src/components/Form/Form.tsx +101 -66
- package/src/components/Form/FormField.tsx +32 -14
- package/src/components/Form/FormFieldComponent.stories.tsx +5 -5
- package/src/components/Form/FormFieldComponent.tsx +17 -20
- package/src/components/Form/FormFieldSet.tsx +126 -105
- package/src/components/Form/fields/ArrayField.tsx +11 -8
- package/src/components/Form/fields/GeoPointField.tsx +6 -6
- package/src/components/Form/fields/MarkdownField.tsx +1 -1
- package/src/components/Form/fields/RefField.tsx +37 -20
- package/src/components/Form/fields/SelectField.tsx +1 -1
- package/src/components/Form/fields/SelectOptionField.stories.tsx +2 -3
- package/src/components/Form/fields/SelectOptionField.tsx +9 -10
- package/src/components/Form/fields/TupleField.tsx +1 -1
- package/src/components/ObjectForm/ObjectForm.tsx +113 -0
- package/src/components/{ControlSection → ObjectForm}/index.ts +1 -1
- package/src/components/ObjectPicker/ObjectPicker.stories.tsx +13 -18
- package/src/components/ObjectPicker/ObjectPicker.tsx +71 -60
- package/src/components/ObjectProperties/ObjectProperties.stories.tsx +63 -0
- package/src/components/ObjectProperties/ObjectProperties.tsx +129 -0
- package/src/components/ObjectProperties/index.ts +5 -0
- package/src/components/Settings/Settings.stories.tsx +55 -0
- package/src/components/Settings/Settings.tsx +129 -0
- package/src/components/Settings/index.ts +5 -0
- package/src/components/ViewEditor/ViewEditor.stories.tsx +36 -33
- package/src/components/ViewEditor/ViewEditor.test.tsx +3 -7
- package/src/components/ViewEditor/ViewEditor.tsx +117 -85
- package/src/components/index.ts +3 -1
- package/src/components/testing.tsx +20 -8
- package/src/hooks/useFormHandler.ts +42 -18
- package/src/hooks/useKeyHandler.tsx +7 -7
- package/src/translations.ts +63 -63
- package/src/util/refs.ts +7 -3
- 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 +0 -87
- 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/components/Form/Form-old.stories.d.ts +0 -131
- package/dist/types/src/components/Form/Form-old.stories.d.ts.map +0 -1
- package/src/components/ControlSection/ControlSection.stories.tsx +0 -38
- package/src/components/ControlSection/ControlSection.tsx +0 -153
- package/src/components/Form/Form-old.stories.tsx +0 -314
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2025 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import * as Schema from 'effect/Schema';
|
|
6
|
+
import React, { useCallback, useMemo } from 'react';
|
|
7
|
+
|
|
8
|
+
import { DXN, Obj, Ref, Tag, Type } from '@dxos/echo';
|
|
9
|
+
import { type JsonPath, splitJsonPath } from '@dxos/echo/internal';
|
|
10
|
+
import { invariant } from '@dxos/invariant';
|
|
11
|
+
import { HuePicker } from '@dxos/react-ui-pickers';
|
|
12
|
+
import { isNonNullable } from '@dxos/util';
|
|
13
|
+
|
|
14
|
+
import { translationKey } from '../../translations';
|
|
15
|
+
import { Form, type FormFieldMap, omitId } from '../Form';
|
|
16
|
+
|
|
17
|
+
export type ObjectFormProps = {
|
|
18
|
+
schema: Schema.Schema.AnyNoContext;
|
|
19
|
+
object: Obj.Unknown;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const createFieldMap: FormFieldMap = {
|
|
23
|
+
hue: ({ type, label, layout, getValue, onValueChange }) => {
|
|
24
|
+
const handleChange = useCallback((nextHue: string) => onValueChange(type, nextHue), [onValueChange, type]);
|
|
25
|
+
const handleReset = useCallback(() => onValueChange(type, undefined), [onValueChange, type]);
|
|
26
|
+
return (
|
|
27
|
+
<>
|
|
28
|
+
{layout !== 'inline' && <Form.Label label={label} />}
|
|
29
|
+
<HuePicker value={getValue()} onChange={handleChange} onReset={handleReset} />
|
|
30
|
+
</>
|
|
31
|
+
);
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export const ObjectForm = ({ object, schema }: ObjectFormProps) => {
|
|
36
|
+
const db = Obj.getDatabase(object);
|
|
37
|
+
const meta = Obj.getMeta(object);
|
|
38
|
+
const tags = (meta.tags ?? []).map((tag) => db?.makeRef(DXN.parse(tag))).filter(isNonNullable);
|
|
39
|
+
const values = useMemo(() => ({ tags, ...object }), [object, tags]);
|
|
40
|
+
const formSchema = useMemo(
|
|
41
|
+
() =>
|
|
42
|
+
omitId(
|
|
43
|
+
Schema.Struct({
|
|
44
|
+
tags: Schema.Array(Ref.Ref(Tag.Tag)).pipe(Schema.optional),
|
|
45
|
+
}).pipe(Schema.extend(schema)),
|
|
46
|
+
),
|
|
47
|
+
[schema],
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
const handleCreate = useCallback((schema: Type.AnyEntity, values: any) => {
|
|
51
|
+
invariant(db);
|
|
52
|
+
invariant(Type.isObjectSchema(schema));
|
|
53
|
+
const newObject = db.add(Obj.make(schema, values));
|
|
54
|
+
if (Obj.instanceOf(Tag.Tag, newObject)) {
|
|
55
|
+
Obj.change(object, (object) => {
|
|
56
|
+
Obj.getMeta(object).tags = [...(Obj.getMeta(object).tags ?? []), Obj.getDXN(newObject).toString()];
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
}, []);
|
|
60
|
+
|
|
61
|
+
// TODO(wittjosiah): Use FormRootProps type.
|
|
62
|
+
const handleChange = useCallback(
|
|
63
|
+
({ tags, ...values }: any, { isValid, changed }: { isValid: boolean; changed: Record<JsonPath, boolean> }) => {
|
|
64
|
+
if (!isValid) {
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const changedPaths = Object.keys(changed).filter((path) => changed[path as JsonPath]) as JsonPath[];
|
|
69
|
+
|
|
70
|
+
// Handle tags separately using Obj.change.
|
|
71
|
+
const hasTagsChange = changedPaths.some((path) => splitJsonPath(path)[0] === 'tags');
|
|
72
|
+
if (hasTagsChange) {
|
|
73
|
+
Obj.change(object, (object) => {
|
|
74
|
+
Obj.getMeta(object).tags = tags?.map((tag: Ref.Ref<Tag.Tag>) => tag.dxn.toString()) ?? [];
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Handle other property changes.
|
|
79
|
+
const nonTagPaths = changedPaths.filter((path) => splitJsonPath(path)[0] !== 'tags');
|
|
80
|
+
if (nonTagPaths.length > 0) {
|
|
81
|
+
Obj.change(object, () => {
|
|
82
|
+
for (const path of nonTagPaths) {
|
|
83
|
+
const parts = splitJsonPath(path);
|
|
84
|
+
const value = Obj.getValue(values, parts);
|
|
85
|
+
Obj.setValue(object, parts, value);
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
},
|
|
90
|
+
[object],
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
return (
|
|
94
|
+
<Form.Root
|
|
95
|
+
schema={formSchema}
|
|
96
|
+
defaultValues={values}
|
|
97
|
+
createTypename={Type.getTypename(Tag.Tag)}
|
|
98
|
+
createOptionIcon='ph--plus--regular'
|
|
99
|
+
createOptionLabel={['add-tag.label', { ns: translationKey }]}
|
|
100
|
+
createInitialValuePath='label'
|
|
101
|
+
createFieldMap={createFieldMap}
|
|
102
|
+
db={db}
|
|
103
|
+
onValuesChanged={handleChange}
|
|
104
|
+
onCreate={handleCreate}
|
|
105
|
+
>
|
|
106
|
+
<Form.Viewport>
|
|
107
|
+
<Form.Content>
|
|
108
|
+
<Form.FieldSet />
|
|
109
|
+
</Form.Content>
|
|
110
|
+
</Form.Viewport>
|
|
111
|
+
</Form.Root>
|
|
112
|
+
);
|
|
113
|
+
};
|
|
@@ -8,7 +8,7 @@ import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
|
|
8
8
|
import { expect, fn, userEvent, within } from 'storybook/test';
|
|
9
9
|
|
|
10
10
|
import { Obj } from '@dxos/echo';
|
|
11
|
-
import {
|
|
11
|
+
import { random } from '@dxos/random';
|
|
12
12
|
import { Filter, useQuery } from '@dxos/react-client/echo';
|
|
13
13
|
import { useClientStory, withClientProvider } from '@dxos/react-client/testing';
|
|
14
14
|
import { Button } from '@dxos/react-ui';
|
|
@@ -17,14 +17,13 @@ import { Person } from '@dxos/types';
|
|
|
17
17
|
import { osTranslations } from '@dxos/ui-theme';
|
|
18
18
|
|
|
19
19
|
import { translations } from '../../translations';
|
|
20
|
-
|
|
21
20
|
import { ObjectPicker } from './ObjectPicker';
|
|
22
21
|
|
|
23
|
-
|
|
22
|
+
random.seed(1);
|
|
24
23
|
|
|
25
24
|
const createPerson = () =>
|
|
26
25
|
Obj.make(Person.Person, {
|
|
27
|
-
fullName:
|
|
26
|
+
fullName: random.person.fullName(),
|
|
28
27
|
});
|
|
29
28
|
|
|
30
29
|
const omitId = Schema.omit<any, any, ['id']>('id');
|
|
@@ -74,18 +73,18 @@ const DefaultStory = () => {
|
|
|
74
73
|
);
|
|
75
74
|
|
|
76
75
|
return (
|
|
77
|
-
<div role='none' className='
|
|
76
|
+
<div role='none' className='w-96'>
|
|
78
77
|
<ObjectPicker.Root open={isOpen} onOpenChange={setIsOpen}>
|
|
79
78
|
<ObjectPicker.Trigger asChild>
|
|
80
|
-
<Button variant='primary' data-testid='trigger' classNames='
|
|
79
|
+
<Button variant='primary' data-testid='trigger' classNames='w-full'>
|
|
81
80
|
Select Person
|
|
82
81
|
</Button>
|
|
83
82
|
</ObjectPicker.Trigger>
|
|
84
83
|
<ObjectPicker.Content
|
|
85
|
-
classNames='
|
|
84
|
+
classNames='dx-card-popover-width'
|
|
86
85
|
options={options}
|
|
87
86
|
createSchema={personSchema}
|
|
88
|
-
createOptionLabel={['create
|
|
87
|
+
createOptionLabel={['create-new-person.label', { ns: osTranslations }]}
|
|
89
88
|
createOptionIcon='ph--user-plus--regular'
|
|
90
89
|
createInitialValuePath='fullName'
|
|
91
90
|
onCreate={handleCreateCallback}
|
|
@@ -100,7 +99,7 @@ const meta = {
|
|
|
100
99
|
title: 'ui/react-ui-form/ObjectPicker',
|
|
101
100
|
render: DefaultStory,
|
|
102
101
|
decorators: [
|
|
103
|
-
withTheme,
|
|
102
|
+
withTheme(),
|
|
104
103
|
withClientProvider({
|
|
105
104
|
types: [Person.Person],
|
|
106
105
|
createIdentity: true,
|
|
@@ -201,14 +200,10 @@ export const WithTest: Story = {
|
|
|
201
200
|
await userEvent.click(saveButton);
|
|
202
201
|
|
|
203
202
|
// Check that clicking the save button calls onCreate with expected values.
|
|
204
|
-
await expect(mockHandleCreate).toHaveBeenCalledWith(
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
identities: [],
|
|
210
|
-
urls: [],
|
|
211
|
-
fields: [],
|
|
212
|
-
});
|
|
203
|
+
await expect(mockHandleCreate).toHaveBeenCalledWith(
|
|
204
|
+
expect.objectContaining({
|
|
205
|
+
fullName: unrelatedTerm,
|
|
206
|
+
}),
|
|
207
|
+
);
|
|
213
208
|
},
|
|
214
209
|
};
|
|
@@ -6,10 +6,11 @@ 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-
|
|
9
|
+
import { Combobox, useSearchListInput, useSearchListResults } from '@dxos/react-ui-search';
|
|
10
10
|
|
|
11
11
|
import { translationKey } from '../../translations';
|
|
12
12
|
import { Form } from '../Form';
|
|
13
|
+
import { type FormFieldMap } from '../Form/FormFieldComponent';
|
|
13
14
|
|
|
14
15
|
export type RefOption = {
|
|
15
16
|
id: string;
|
|
@@ -24,36 +25,11 @@ export type ObjectPickerContentProps = ThemedClassName<{
|
|
|
24
25
|
createOptionIcon?: string;
|
|
25
26
|
createSchema?: Schema.Schema.AnyNoContext;
|
|
26
27
|
createInitialValuePath?: string;
|
|
28
|
+
createFieldMap?: FormFieldMap;
|
|
27
29
|
onCreate?: (values: any) => void;
|
|
28
30
|
onSelect: (id: string) => void;
|
|
29
31
|
}>;
|
|
30
32
|
|
|
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
33
|
const ObjectPickerContent = forwardRef<HTMLDivElement, ObjectPickerContentProps>(
|
|
58
34
|
(
|
|
59
35
|
{
|
|
@@ -63,11 +39,12 @@ const ObjectPickerContent = forwardRef<HTMLDivElement, ObjectPickerContentProps>
|
|
|
63
39
|
createOptionLabel,
|
|
64
40
|
createOptionIcon,
|
|
65
41
|
createInitialValuePath,
|
|
42
|
+
createFieldMap,
|
|
66
43
|
onSelect,
|
|
67
44
|
onCreate,
|
|
68
45
|
...props
|
|
69
46
|
},
|
|
70
|
-
|
|
47
|
+
forwardedRef,
|
|
71
48
|
) => {
|
|
72
49
|
const { t } = useTranslation(translationKey);
|
|
73
50
|
const [showForm, setShowForm] = useState(false);
|
|
@@ -112,14 +89,15 @@ const ObjectPickerContent = forwardRef<HTMLDivElement, ObjectPickerContentProps>
|
|
|
112
89
|
[createSchema],
|
|
113
90
|
);
|
|
114
91
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
{
|
|
92
|
+
if (showForm && createSchema) {
|
|
93
|
+
return (
|
|
94
|
+
<Combobox.Content {...props} onSearch={handleSearch} onKeyDownCapture={handleKeyDown} ref={forwardedRef}>
|
|
118
95
|
<Popover.Viewport>
|
|
119
96
|
<Form.Root
|
|
120
97
|
testId='create-referenced-object-form'
|
|
121
98
|
schema={createSchema}
|
|
122
|
-
|
|
99
|
+
defaultValues={createInitialValuePath ? { [createInitialValuePath]: formInitialValue } : {}}
|
|
100
|
+
fieldMap={createFieldMap}
|
|
123
101
|
onSave={handleFormSave}
|
|
124
102
|
onCancel={handleFormCancel}
|
|
125
103
|
>
|
|
@@ -131,40 +109,73 @@ const ObjectPickerContent = forwardRef<HTMLDivElement, ObjectPickerContentProps>
|
|
|
131
109
|
</Form.Viewport>
|
|
132
110
|
</Form.Root>
|
|
133
111
|
</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
|
-
|
|
112
|
+
<Combobox.Arrow />
|
|
113
|
+
</Combobox.Content>
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return (
|
|
118
|
+
<Combobox.Content {...props} onSearch={handleSearch} onKeyDownCapture={handleKeyDown} ref={forwardedRef}>
|
|
119
|
+
<Combobox.Input placeholder={t('ref-field-combobox-input.placeholder')} autoFocus />
|
|
120
|
+
<Combobox.List>
|
|
121
|
+
{results.map((option) => (
|
|
122
|
+
<Combobox.Item
|
|
123
|
+
key={option.id}
|
|
124
|
+
value={option.id}
|
|
125
|
+
label={option.label}
|
|
126
|
+
checked={selectedIds?.includes(option.id)}
|
|
127
|
+
onSelect={() => onSelect(option.id)}
|
|
128
|
+
classNames='flex items-center gap-2'
|
|
129
|
+
/>
|
|
130
|
+
))}
|
|
131
|
+
{createSchema && onCreate && (
|
|
132
|
+
<CreateItem
|
|
133
|
+
createOptionLabel={createOptionLabel}
|
|
134
|
+
createOptionIcon={createOptionIcon ?? 'ph--plus--regular'}
|
|
135
|
+
onCreateItemSelect={(query: string) => {
|
|
136
|
+
setFormInitialValue(query);
|
|
137
|
+
setShowForm(true);
|
|
138
|
+
}}
|
|
139
|
+
/>
|
|
140
|
+
)}
|
|
141
|
+
</Combobox.List>
|
|
161
142
|
<Combobox.Arrow />
|
|
162
143
|
</Combobox.Content>
|
|
163
144
|
);
|
|
164
145
|
},
|
|
165
146
|
);
|
|
166
147
|
|
|
167
|
-
|
|
148
|
+
const CreateItem = ({
|
|
149
|
+
createOptionLabel,
|
|
150
|
+
createOptionIcon,
|
|
151
|
+
onCreateItemSelect,
|
|
152
|
+
}: {
|
|
153
|
+
createOptionLabel?: [string, { ns: string }];
|
|
154
|
+
createOptionIcon: string;
|
|
155
|
+
onCreateItemSelect: (query: string) => void;
|
|
156
|
+
}) => {
|
|
157
|
+
const { t } = useTranslation(translationKey);
|
|
158
|
+
const { query } = useSearchListInput();
|
|
159
|
+
|
|
160
|
+
const label = createOptionLabel
|
|
161
|
+
? t(createOptionLabel[0], { ns: createOptionLabel[1].ns, text: query })
|
|
162
|
+
: t('create-option.label');
|
|
163
|
+
|
|
164
|
+
return (
|
|
165
|
+
<Combobox.Item
|
|
166
|
+
value='__create__'
|
|
167
|
+
label={label}
|
|
168
|
+
icon={createOptionIcon}
|
|
169
|
+
classNames='flex items-center gap-2'
|
|
170
|
+
closeOnSelect={false}
|
|
171
|
+
onSelect={() => {
|
|
172
|
+
onCreateItemSelect(query);
|
|
173
|
+
}}
|
|
174
|
+
/>
|
|
175
|
+
);
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
ObjectPickerContent.displayName = 'ObjectPicker.Content';
|
|
168
179
|
|
|
169
180
|
export const ObjectPicker = {
|
|
170
181
|
Root: Combobox.Root,
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2023 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { type Meta, type StoryObj } from '@storybook/react-vite';
|
|
6
|
+
import React from 'react';
|
|
7
|
+
|
|
8
|
+
import { Filter, Tag } from '@dxos/echo';
|
|
9
|
+
import { useQuery } from '@dxos/react-client/echo';
|
|
10
|
+
import { useClientStory, withClientProvider } from '@dxos/react-client/testing';
|
|
11
|
+
import { Panel } from '@dxos/react-ui';
|
|
12
|
+
import { Loading, withLayout, withTheme } from '@dxos/react-ui/testing';
|
|
13
|
+
import { Pipeline } from '@dxos/types';
|
|
14
|
+
|
|
15
|
+
import { translations } from '../../translations';
|
|
16
|
+
import { ObjectProperties } from './ObjectProperties';
|
|
17
|
+
|
|
18
|
+
const DefaultStory = () => {
|
|
19
|
+
const { space } = useClientStory();
|
|
20
|
+
const [object] = useQuery(space?.db, Filter.type(Pipeline.Pipeline));
|
|
21
|
+
if (!object) {
|
|
22
|
+
return <Loading />;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return (
|
|
26
|
+
<Panel.Root>
|
|
27
|
+
<Panel.Content asChild>
|
|
28
|
+
<ObjectProperties object={object} />
|
|
29
|
+
</Panel.Content>
|
|
30
|
+
</Panel.Root>
|
|
31
|
+
);
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const meta = {
|
|
35
|
+
title: 'ui/react-ui-form/ObjectProperties',
|
|
36
|
+
component: ObjectProperties as any,
|
|
37
|
+
render: DefaultStory,
|
|
38
|
+
decorators: [
|
|
39
|
+
withTheme(),
|
|
40
|
+
withLayout({ layout: 'column' }),
|
|
41
|
+
withClientProvider({
|
|
42
|
+
createIdentity: true,
|
|
43
|
+
createSpace: true,
|
|
44
|
+
types: [Pipeline.Pipeline, Tag.Tag],
|
|
45
|
+
onCreateSpace: async ({ space }) => {
|
|
46
|
+
space.db.add(Pipeline.make());
|
|
47
|
+
space.db.add(Tag.make({ label: 'Tag 1' }));
|
|
48
|
+
space.db.add(Tag.make({ label: 'Tag 2' }));
|
|
49
|
+
space.db.add(Tag.make({ label: 'Tag 3' }));
|
|
50
|
+
},
|
|
51
|
+
}),
|
|
52
|
+
],
|
|
53
|
+
parameters: {
|
|
54
|
+
layout: 'fullscreen',
|
|
55
|
+
translations,
|
|
56
|
+
},
|
|
57
|
+
} satisfies Meta<typeof DefaultStory>;
|
|
58
|
+
|
|
59
|
+
export default meta;
|
|
60
|
+
|
|
61
|
+
type Story = StoryObj<typeof meta>;
|
|
62
|
+
|
|
63
|
+
export const Default: Story = {};
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2024 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import * as Function from 'effect/Function';
|
|
6
|
+
import * as Option from 'effect/Option';
|
|
7
|
+
import * as Schema from 'effect/Schema';
|
|
8
|
+
import React, { type PropsWithChildren, useCallback, useMemo } from 'react';
|
|
9
|
+
|
|
10
|
+
import { DXN, Obj, Ref, Tag, Type } from '@dxos/echo';
|
|
11
|
+
import { type JsonPath, splitJsonPath } from '@dxos/echo/internal';
|
|
12
|
+
import { invariant } from '@dxos/invariant';
|
|
13
|
+
import { HuePicker } from '@dxos/react-ui-pickers';
|
|
14
|
+
import { composable, composableProps } from '@dxos/ui-theme';
|
|
15
|
+
import { isNonNullable } from '@dxos/util';
|
|
16
|
+
|
|
17
|
+
import { translationKey } from '../../translations';
|
|
18
|
+
import { Form, type FormFieldMap, omitId } from '../Form';
|
|
19
|
+
|
|
20
|
+
export type ObjectPropertiesProps = PropsWithChildren<{ object: Obj.Unknown }>;
|
|
21
|
+
|
|
22
|
+
// TODO(wittjosiah): Reconcile w/ ObjectForm.
|
|
23
|
+
export const ObjectProperties = composable<HTMLDivElement, ObjectPropertiesProps>(
|
|
24
|
+
({ children, object, ...props }, forwardedRef) => {
|
|
25
|
+
const db = Obj.getDatabase(object);
|
|
26
|
+
const meta = Obj.getMeta(object);
|
|
27
|
+
const tags = (meta.tags ?? []).map((tag) => db?.makeRef(DXN.parse(tag))).filter(isNonNullable);
|
|
28
|
+
const values = useMemo(() => ({ tags, ...object }), [object, tags]);
|
|
29
|
+
|
|
30
|
+
const formSchema = useMemo(() => {
|
|
31
|
+
return Function.pipe(
|
|
32
|
+
Obj.getSchema(object),
|
|
33
|
+
Option.fromNullable,
|
|
34
|
+
Option.map((schema) => omitId(BaseSchema.pipe(Schema.extend(schema)))),
|
|
35
|
+
Option.getOrUndefined,
|
|
36
|
+
);
|
|
37
|
+
}, [object]);
|
|
38
|
+
|
|
39
|
+
const handleCreate = useCallback((schema: Type.AnyEntity, values: any) => {
|
|
40
|
+
invariant(db);
|
|
41
|
+
invariant(Type.isObjectSchema(schema));
|
|
42
|
+
const newObject = db.add(Obj.make(schema, values));
|
|
43
|
+
if (Obj.instanceOf(Tag.Tag, newObject)) {
|
|
44
|
+
Obj.change(object, (object) => {
|
|
45
|
+
Obj.getMeta(object).tags = [...(Obj.getMeta(object).tags ?? []), Obj.getDXN(newObject).toString()];
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
}, []);
|
|
49
|
+
|
|
50
|
+
// TODO(wittjosiah): Use FormRootProps type.
|
|
51
|
+
const handleChange = useCallback(
|
|
52
|
+
(
|
|
53
|
+
{ tags, ...values }: Schema.Schema.Type<typeof formSchema>,
|
|
54
|
+
{ isValid, changed }: { isValid: boolean; changed: Record<JsonPath, boolean> },
|
|
55
|
+
) => {
|
|
56
|
+
if (!isValid) {
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const changedPaths = Object.keys(changed).filter((path) => changed[path as JsonPath]) as JsonPath[];
|
|
61
|
+
|
|
62
|
+
// Handle tags separately using Obj.change.
|
|
63
|
+
const hasTagsChange = changedPaths.some((path) => splitJsonPath(path)[0] === 'tags');
|
|
64
|
+
if (hasTagsChange) {
|
|
65
|
+
Obj.change(object, (object) => {
|
|
66
|
+
Obj.getMeta(object).tags = tags?.map((tag: Ref.Ref<Tag.Tag>) => tag.dxn.toString()) ?? [];
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Handle other property changes.
|
|
71
|
+
const nonTagPaths = changedPaths.filter((path) => splitJsonPath(path)[0] !== 'tags');
|
|
72
|
+
if (nonTagPaths.length > 0) {
|
|
73
|
+
Obj.change(object, () => {
|
|
74
|
+
for (const path of nonTagPaths) {
|
|
75
|
+
const parts = splitJsonPath(path);
|
|
76
|
+
const value = Obj.getValue(values as any, parts);
|
|
77
|
+
Obj.setValue(object, parts, value);
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
},
|
|
82
|
+
[object],
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
if (!formSchema) {
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return (
|
|
90
|
+
<Form.Root
|
|
91
|
+
schema={formSchema}
|
|
92
|
+
defaultValues={values as any}
|
|
93
|
+
createTypename={Type.getTypename(Tag.Tag)}
|
|
94
|
+
createOptionIcon='ph--plus--regular'
|
|
95
|
+
createOptionLabel={['add-tag.label', { ns: translationKey }]}
|
|
96
|
+
createInitialValuePath='label'
|
|
97
|
+
createFieldMap={createFieldMap}
|
|
98
|
+
db={db}
|
|
99
|
+
onValuesChanged={handleChange}
|
|
100
|
+
onCreate={handleCreate}
|
|
101
|
+
>
|
|
102
|
+
<Form.Viewport {...composableProps(props)} ref={forwardedRef}>
|
|
103
|
+
<Form.Content>
|
|
104
|
+
<Form.FieldSet />
|
|
105
|
+
{children}
|
|
106
|
+
</Form.Content>
|
|
107
|
+
</Form.Viewport>
|
|
108
|
+
</Form.Root>
|
|
109
|
+
);
|
|
110
|
+
},
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
const createFieldMap: FormFieldMap = {
|
|
114
|
+
hue: ({ type, label, layout, getValue, onValueChange }) => {
|
|
115
|
+
const handleChange = useCallback((nextHue: string) => onValueChange(type, nextHue), [onValueChange, type]);
|
|
116
|
+
const handleReset = useCallback(() => onValueChange(type, undefined), [onValueChange, type]);
|
|
117
|
+
return (
|
|
118
|
+
<>
|
|
119
|
+
{layout !== 'inline' && <Form.Label label={label} />}
|
|
120
|
+
<HuePicker value={getValue()} onChange={handleChange} onReset={handleReset} />
|
|
121
|
+
</>
|
|
122
|
+
);
|
|
123
|
+
},
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
// TODO(wittjosiah): Would be nice to control order when extending so this isn't always first/last.
|
|
127
|
+
const BaseSchema = Schema.Struct({
|
|
128
|
+
tags: Schema.Array(Ref.Ref(Tag.Tag)).pipe(Schema.optional),
|
|
129
|
+
});
|
|
@@ -0,0 +1,55 @@
|
|
|
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 { random } 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
|
+
import { Settings } from './Settings';
|
|
14
|
+
|
|
15
|
+
random.seed(132);
|
|
16
|
+
|
|
17
|
+
const DefaultStory = () => {
|
|
18
|
+
return (
|
|
19
|
+
<Settings.Viewport>
|
|
20
|
+
<Settings.Section title='Settings' description={random.lorem.paragraphs(1)}>
|
|
21
|
+
<Settings.Item title={random.lorem.sentence(2)} description={random.lorem.paragraphs(1)}>
|
|
22
|
+
<Input.TextInput placeholder='Input' />
|
|
23
|
+
</Settings.Item>
|
|
24
|
+
<Settings.Item title={random.lorem.sentence(2)} description={random.lorem.paragraphs(2)}>
|
|
25
|
+
<Input.Switch />
|
|
26
|
+
</Settings.Item>
|
|
27
|
+
<Settings.Item title={random.lorem.sentence(3)} description={random.lorem.paragraphs(2)}>
|
|
28
|
+
<Button>Test</Button>
|
|
29
|
+
</Settings.Item>
|
|
30
|
+
</Settings.Section>
|
|
31
|
+
<Settings.Section title='Panel Example'>
|
|
32
|
+
<Settings.Panel>
|
|
33
|
+
<h3 className='text-lg mb-2'>Members</h3>
|
|
34
|
+
<p className='text-description'>Content inside a panel.</p>
|
|
35
|
+
</Settings.Panel>
|
|
36
|
+
</Settings.Section>
|
|
37
|
+
</Settings.Viewport>
|
|
38
|
+
);
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const meta = {
|
|
42
|
+
title: 'ui/react-ui-form/Settings',
|
|
43
|
+
render: DefaultStory,
|
|
44
|
+
decorators: [withTheme(), withLayout({ layout: 'fullscreen' })],
|
|
45
|
+
parameters: {
|
|
46
|
+
layout: 'fullscreen',
|
|
47
|
+
translations,
|
|
48
|
+
},
|
|
49
|
+
} satisfies Meta;
|
|
50
|
+
|
|
51
|
+
export default meta;
|
|
52
|
+
|
|
53
|
+
type Story = StoryObj<typeof meta>;
|
|
54
|
+
|
|
55
|
+
export const Default: Story = {};
|