@dxos/react-ui-form 0.8.4-main.f5c0578 → 0.8.4-main.fcfe5033a5
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 +2753 -2121
- package/dist/lib/browser/index.mjs.map +4 -4
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/node-esm/index.mjs +2753 -2121
- 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 +8 -8
- package/dist/types/src/components/FieldEditor/FieldEditor.d.ts.map +1 -1
- package/dist/types/src/components/FieldEditor/FieldEditor.stories.d.ts +83 -5
- package/dist/types/src/components/FieldEditor/FieldEditor.stories.d.ts.map +1 -1
- package/dist/types/src/components/Form/Form.d.ts +86 -25
- package/dist/types/src/components/Form/Form.d.ts.map +1 -1
- package/dist/types/src/components/Form/Form.stories.d.ts +129 -56
- package/dist/types/src/components/Form/Form.stories.d.ts.map +1 -1
- package/dist/types/src/components/Form/FormField.d.ts +43 -0
- package/dist/types/src/components/Form/FormField.d.ts.map +1 -0
- package/dist/types/src/components/Form/FormFieldComponent.d.ts +77 -0
- package/dist/types/src/components/Form/FormFieldComponent.d.ts.map +1 -0
- package/dist/types/src/components/Form/FormFieldComponent.stories.d.ts +9 -0
- package/dist/types/src/components/Form/FormFieldComponent.stories.d.ts.map +1 -0
- package/dist/types/src/components/Form/FormFieldSet.d.ts +18 -0
- package/dist/types/src/components/Form/FormFieldSet.d.ts.map +1 -0
- package/dist/types/src/components/Form/fields/ArrayField.d.ts +18 -0
- package/dist/types/src/components/Form/fields/ArrayField.d.ts.map +1 -0
- package/dist/types/src/components/Form/fields/BooleanField.d.ts +4 -0
- package/dist/types/src/components/Form/fields/BooleanField.d.ts.map +1 -0
- package/dist/types/src/components/Form/fields/GeoPointField.d.ts +5 -0
- package/dist/types/src/components/Form/fields/GeoPointField.d.ts.map +1 -0
- package/dist/types/src/components/Form/fields/MarkdownField.d.ts +4 -0
- package/dist/types/src/components/Form/fields/MarkdownField.d.ts.map +1 -0
- package/dist/types/src/components/Form/fields/NumberField.d.ts +4 -0
- package/dist/types/src/components/Form/fields/NumberField.d.ts.map +1 -0
- package/dist/types/src/components/Form/fields/RefField.d.ts +16 -0
- package/dist/types/src/components/Form/fields/RefField.d.ts.map +1 -0
- package/dist/types/src/components/Form/fields/SelectField.d.ts +10 -0
- package/dist/types/src/components/Form/fields/SelectField.d.ts.map +1 -0
- package/dist/types/src/components/Form/fields/SelectOptionField.d.ts +5 -0
- package/dist/types/src/components/Form/fields/SelectOptionField.d.ts.map +1 -0
- package/dist/types/src/components/Form/fields/SelectOptionField.stories.d.ts +83 -0
- package/dist/types/src/components/Form/fields/SelectOptionField.stories.d.ts.map +1 -0
- package/dist/types/src/components/Form/fields/TextAreaField.d.ts +4 -0
- package/dist/types/src/components/Form/fields/TextAreaField.d.ts.map +1 -0
- package/dist/types/src/components/Form/fields/TextField.d.ts +4 -0
- package/dist/types/src/components/Form/fields/TextField.d.ts.map +1 -0
- package/dist/types/src/components/Form/fields/TupleField.d.ts +6 -0
- package/dist/types/src/components/Form/fields/TupleField.d.ts.map +1 -0
- package/dist/types/src/components/Form/fields/index.d.ts +12 -0
- package/dist/types/src/components/Form/fields/index.d.ts.map +1 -0
- package/dist/types/src/components/Form/index.d.ts +2 -4
- package/dist/types/src/components/Form/index.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 +54 -0
- package/dist/types/src/components/ObjectPicker/ObjectPicker.d.ts.map +1 -0
- package/dist/types/src/components/ObjectPicker/ObjectPicker.stories.d.ts +86 -0
- package/dist/types/src/components/ObjectPicker/ObjectPicker.stories.d.ts.map +1 -0
- package/dist/types/src/components/ObjectPicker/index.d.ts +2 -0
- package/dist/types/src/components/ObjectPicker/index.d.ts.map +1 -0
- 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 +43 -0
- package/dist/types/src/components/Settings/Settings.d.ts.map +1 -0
- package/dist/types/src/components/Settings/Settings.stories.d.ts +83 -0
- package/dist/types/src/components/Settings/Settings.stories.d.ts.map +1 -0
- package/dist/types/src/components/Settings/SettingsFieldSet.d.ts +38 -0
- package/dist/types/src/components/Settings/SettingsFieldSet.d.ts.map +1 -0
- package/dist/types/src/components/Settings/SettingsItem.d.ts +17 -0
- package/dist/types/src/components/Settings/SettingsItem.d.ts.map +1 -0
- package/dist/types/src/components/Settings/index.d.ts +3 -0
- package/dist/types/src/components/Settings/index.d.ts.map +1 -0
- package/dist/types/src/components/ViewEditor/ViewEditor.d.ts +22 -10
- package/dist/types/src/components/ViewEditor/ViewEditor.d.ts.map +1 -1
- package/dist/types/src/components/ViewEditor/ViewEditor.stories.d.ts +85 -8
- package/dist/types/src/components/ViewEditor/ViewEditor.stories.d.ts.map +1 -1
- package/dist/types/src/components/index.d.ts +4 -2
- 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/index.d.ts +2 -3
- package/dist/types/src/hooks/index.d.ts.map +1 -1
- package/dist/types/src/hooks/useFormHandler.d.ts +82 -0
- package/dist/types/src/hooks/useFormHandler.d.ts.map +1 -0
- package/dist/types/src/hooks/useKeyHandler.d.ts +7 -0
- package/dist/types/src/hooks/useKeyHandler.d.ts.map +1 -0
- package/dist/types/src/translations.d.ts +64 -55
- 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 +3 -3
- package/dist/types/src/util/field.d.ts.map +1 -1
- package/dist/types/src/util/formatting.d.ts +2 -2
- package/dist/types/src/util/formatting.d.ts.map +1 -1
- package/dist/types/src/util/index.d.ts +2 -1
- package/dist/types/src/util/index.d.ts.map +1 -1
- package/dist/types/src/util/properties.d.ts +7 -0
- package/dist/types/src/util/properties.d.ts.map +1 -0
- package/dist/types/src/util/refs.d.ts +3 -3
- package/dist/types/src/util/refs.d.ts.map +1 -1
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/package.json +45 -43
- package/src/components/FieldEditor/FieldEditor.stories.tsx +48 -31
- package/src/components/FieldEditor/FieldEditor.test.tsx +0 -1
- package/src/components/FieldEditor/FieldEditor.tsx +84 -90
- package/src/components/Form/AUDIT.md +17 -0
- package/src/components/Form/Form.stories.tsx +144 -222
- package/src/components/Form/Form.tsx +359 -96
- package/src/components/Form/FormField.tsx +304 -0
- package/src/components/Form/FormFieldComponent.stories.tsx +65 -0
- package/src/components/Form/FormFieldComponent.tsx +171 -0
- package/src/components/Form/FormFieldSet.tsx +144 -0
- package/src/components/Form/README.md +3 -0
- package/src/components/Form/fields/ArrayField.tsx +154 -0
- package/src/components/Form/fields/BooleanField.tsx +22 -0
- package/src/components/Form/fields/GeoPointField.tsx +120 -0
- package/src/components/Form/fields/MarkdownField.tsx +78 -0
- package/src/components/Form/fields/NumberField.tsx +39 -0
- package/src/components/Form/fields/RefField.tsx +198 -0
- package/src/components/Form/fields/SelectField.tsx +65 -0
- package/src/components/Form/fields/SelectOptionField.stories.tsx +65 -0
- package/src/components/Form/{custom/SelectOptionsInput.tsx → fields/SelectOptionField.tsx} +36 -31
- package/src/components/Form/fields/TextAreaField.tsx +38 -0
- package/src/components/Form/fields/TextField.tsx +38 -0
- package/src/components/Form/{custom/TupleInput.tsx → fields/TupleField.tsx} +17 -10
- package/src/components/Form/fields/index.ts +15 -0
- package/src/components/Form/index.ts +3 -4
- package/src/components/ObjectForm/ObjectForm.tsx +113 -0
- package/src/components/{ControlSection → ObjectForm}/index.ts +1 -1
- package/src/components/ObjectPicker/ObjectPicker.stories.tsx +209 -0
- package/src/components/ObjectPicker/ObjectPicker.tsx +186 -0
- package/src/components/ObjectPicker/index.ts +5 -0
- 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 +103 -0
- package/src/components/Settings/Settings.tsx +81 -0
- package/src/components/Settings/SettingsFieldSet.tsx +211 -0
- package/src/components/Settings/SettingsItem.tsx +62 -0
- package/src/components/Settings/index.ts +6 -0
- package/src/components/ViewEditor/ViewEditor.stories.tsx +139 -63
- package/src/components/ViewEditor/ViewEditor.test.tsx +12 -15
- package/src/components/ViewEditor/ViewEditor.tsx +346 -211
- package/src/components/index.ts +4 -3
- package/src/components/testing.tsx +28 -12
- package/src/hooks/index.ts +2 -3
- package/src/hooks/useFormHandler.ts +380 -0
- package/src/hooks/useKeyHandler.tsx +37 -0
- package/src/translations.ts +67 -59
- package/src/util/field-type.ts +57 -0
- package/src/util/field.ts +33 -43
- package/src/util/formatting.ts +22 -22
- package/src/util/index.ts +2 -1
- package/src/util/properties.ts +18 -0
- package/src/util/refs.ts +17 -19
- 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 -8
- 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/ArrayField.d.ts +0 -16
- package/dist/types/src/components/Form/ArrayField.d.ts.map +0 -1
- package/dist/types/src/components/Form/Defaults.d.ts +0 -14
- package/dist/types/src/components/Form/Defaults.d.ts.map +0 -1
- package/dist/types/src/components/Form/FormActions.d.ts +0 -9
- package/dist/types/src/components/Form/FormActions.d.ts.map +0 -1
- package/dist/types/src/components/Form/FormContent.d.ts +0 -57
- package/dist/types/src/components/Form/FormContent.d.ts.map +0 -1
- package/dist/types/src/components/Form/FormContext.d.ts +0 -23
- package/dist/types/src/components/Form/FormContext.d.ts.map +0 -1
- package/dist/types/src/components/Form/Input.d.ts +0 -26
- package/dist/types/src/components/Form/Input.d.ts.map +0 -1
- package/dist/types/src/components/Form/RefField.d.ts +0 -12
- package/dist/types/src/components/Form/RefField.d.ts.map +0 -1
- package/dist/types/src/components/Form/custom/GeoPointInput.d.ts +0 -4
- package/dist/types/src/components/Form/custom/GeoPointInput.d.ts.map +0 -1
- package/dist/types/src/components/Form/custom/SelectOptionsInput.d.ts +0 -4
- package/dist/types/src/components/Form/custom/SelectOptionsInput.d.ts.map +0 -1
- package/dist/types/src/components/Form/custom/SelectOptionsInput.stories.d.ts +0 -7
- package/dist/types/src/components/Form/custom/SelectOptionsInput.stories.d.ts.map +0 -1
- package/dist/types/src/components/Form/custom/TupleInput.d.ts +0 -6
- package/dist/types/src/components/Form/custom/TupleInput.d.ts.map +0 -1
- package/dist/types/src/components/Form/custom/index.d.ts +0 -4
- package/dist/types/src/components/Form/custom/index.d.ts.map +0 -1
- package/dist/types/src/components/Form/factory.d.ts +0 -8
- package/dist/types/src/components/Form/factory.d.ts.map +0 -1
- package/dist/types/src/components/deprecated/Form.d.ts +0 -15
- package/dist/types/src/components/deprecated/Form.d.ts.map +0 -1
- package/dist/types/src/components/deprecated/Form.stories.d.ts +0 -13
- package/dist/types/src/components/deprecated/Form.stories.d.ts.map +0 -1
- package/dist/types/src/components/deprecated/FormInput.d.ts +0 -16
- package/dist/types/src/components/deprecated/FormInput.d.ts.map +0 -1
- package/dist/types/src/components/deprecated/index.d.ts +0 -2
- package/dist/types/src/components/deprecated/index.d.ts.map +0 -1
- package/dist/types/src/hooks/useForm.d.ts +0 -67
- package/dist/types/src/hooks/useForm.d.ts.map +0 -1
- package/dist/types/src/hooks/useQueryRefOptions.d.ts +0 -23
- package/dist/types/src/hooks/useQueryRefOptions.d.ts.map +0 -1
- package/dist/types/src/hooks/useRefQueryLookupHandler.d.ts +0 -8
- package/dist/types/src/hooks/useRefQueryLookupHandler.d.ts.map +0 -1
- package/dist/types/src/util/schema.d.ts +0 -3
- package/dist/types/src/util/schema.d.ts.map +0 -1
- package/src/components/ControlSection/ControlSection.stories.tsx +0 -42
- package/src/components/ControlSection/ControlSection.tsx +0 -139
- package/src/components/Form/ArrayField.tsx +0 -125
- package/src/components/Form/Defaults.tsx +0 -204
- package/src/components/Form/FormActions.tsx +0 -54
- package/src/components/Form/FormContent.tsx +0 -241
- package/src/components/Form/FormContext.tsx +0 -110
- package/src/components/Form/Input.tsx +0 -51
- package/src/components/Form/RefField.tsx +0 -214
- package/src/components/Form/custom/GeoPointInput.tsx +0 -72
- package/src/components/Form/custom/SelectOptionsInput.stories.tsx +0 -62
- package/src/components/Form/custom/index.ts +0 -7
- package/src/components/Form/factory.tsx +0 -31
- package/src/components/deprecated/Form.stories.tsx +0 -63
- package/src/components/deprecated/Form.tsx +0 -119
- package/src/components/deprecated/FormInput.tsx +0 -71
- package/src/components/deprecated/index.ts +0 -5
- package/src/hooks/useForm.ts +0 -293
- package/src/hooks/useQueryRefOptions.ts +0 -53
- package/src/hooks/useRefQueryLookupHandler.ts +0 -39
- package/src/util/schema.ts +0 -14
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2025 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { type Meta, type StoryObj } from '@storybook/react-vite';
|
|
6
|
+
import * as Schema from 'effect/Schema';
|
|
7
|
+
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
|
8
|
+
import { expect, fn, userEvent, within } from 'storybook/test';
|
|
9
|
+
|
|
10
|
+
import { Obj } from '@dxos/echo';
|
|
11
|
+
import { random } from '@dxos/random';
|
|
12
|
+
import { Filter, useQuery } from '@dxos/react-client/echo';
|
|
13
|
+
import { useClientStory, withClientProvider } from '@dxos/react-client/testing';
|
|
14
|
+
import { Button } from '@dxos/react-ui';
|
|
15
|
+
import { withTheme } from '@dxos/react-ui/testing';
|
|
16
|
+
import { Person } from '@dxos/types';
|
|
17
|
+
import { osTranslations } from '@dxos/ui-theme';
|
|
18
|
+
|
|
19
|
+
import { translations } from '../../translations';
|
|
20
|
+
import { ObjectPicker } from './ObjectPicker';
|
|
21
|
+
|
|
22
|
+
random.seed(1);
|
|
23
|
+
|
|
24
|
+
const createPerson = () =>
|
|
25
|
+
Obj.make(Person.Person, {
|
|
26
|
+
fullName: random.person.fullName(),
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
const omitId = Schema.omit<any, any, ['id']>('id');
|
|
30
|
+
const personSchema = omitId(Person.Person);
|
|
31
|
+
|
|
32
|
+
// Mock functions for testing
|
|
33
|
+
const mockHandleSelect = fn();
|
|
34
|
+
const mockHandleCreate = fn();
|
|
35
|
+
|
|
36
|
+
const DefaultStory = () => {
|
|
37
|
+
const { space } = useClientStory();
|
|
38
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
39
|
+
|
|
40
|
+
// Create sample Person objects
|
|
41
|
+
useEffect(() => {
|
|
42
|
+
if (space) {
|
|
43
|
+
// Create some sample Person objects for testing
|
|
44
|
+
Array.from({ length: 10 }).forEach(() => {
|
|
45
|
+
space.db.add(createPerson());
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
}, [space]);
|
|
49
|
+
|
|
50
|
+
// Get all objects in the space (similar to BoardContainer)
|
|
51
|
+
const allObjects = useQuery(space?.db, Filter.everything());
|
|
52
|
+
|
|
53
|
+
// Map objects to options format expected by ObjectPicker
|
|
54
|
+
const options = useMemo(
|
|
55
|
+
() =>
|
|
56
|
+
allObjects.map((obj) => ({
|
|
57
|
+
id: obj.id,
|
|
58
|
+
label: obj.fullName || obj.name || obj.title || obj.id,
|
|
59
|
+
hue: 'neutral' as const,
|
|
60
|
+
})),
|
|
61
|
+
[allObjects],
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
const handleCreateCallback = useCallback(
|
|
65
|
+
(values: any) => {
|
|
66
|
+
console.log('[on create]', values);
|
|
67
|
+
if (!space) return;
|
|
68
|
+
const newPerson = space.db.add(Obj.make(Person.Person, values));
|
|
69
|
+
mockHandleCreate(values);
|
|
70
|
+
return newPerson;
|
|
71
|
+
},
|
|
72
|
+
[space],
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
return (
|
|
76
|
+
<div role='none' className='w-96'>
|
|
77
|
+
<ObjectPicker.Root open={isOpen} onOpenChange={setIsOpen}>
|
|
78
|
+
<ObjectPicker.Trigger asChild>
|
|
79
|
+
<Button variant='primary' data-testid='trigger' classNames='w-full'>
|
|
80
|
+
Select Person
|
|
81
|
+
</Button>
|
|
82
|
+
</ObjectPicker.Trigger>
|
|
83
|
+
<ObjectPicker.Content
|
|
84
|
+
classNames='dx-card-popover-width'
|
|
85
|
+
options={options}
|
|
86
|
+
createSchema={personSchema}
|
|
87
|
+
createOptionLabel={['create-new-person.label', { ns: osTranslations }]}
|
|
88
|
+
createOptionIcon='ph--user-plus--regular'
|
|
89
|
+
createInitialValuePath='fullName'
|
|
90
|
+
onCreate={handleCreateCallback}
|
|
91
|
+
onSelect={mockHandleSelect}
|
|
92
|
+
/>
|
|
93
|
+
</ObjectPicker.Root>
|
|
94
|
+
</div>
|
|
95
|
+
);
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
const meta = {
|
|
99
|
+
title: 'ui/react-ui-form/ObjectPicker',
|
|
100
|
+
render: DefaultStory,
|
|
101
|
+
decorators: [
|
|
102
|
+
withTheme(),
|
|
103
|
+
withClientProvider({
|
|
104
|
+
types: [Person.Person],
|
|
105
|
+
createIdentity: true,
|
|
106
|
+
createSpace: true,
|
|
107
|
+
}),
|
|
108
|
+
],
|
|
109
|
+
parameters: {
|
|
110
|
+
layout: 'centered',
|
|
111
|
+
controls: { disable: true },
|
|
112
|
+
translations,
|
|
113
|
+
},
|
|
114
|
+
} satisfies Meta<typeof DefaultStory>;
|
|
115
|
+
|
|
116
|
+
export default meta;
|
|
117
|
+
|
|
118
|
+
type Story = StoryObj<typeof meta>;
|
|
119
|
+
|
|
120
|
+
export const Default: Story = {};
|
|
121
|
+
|
|
122
|
+
export const WithTest: Story = {
|
|
123
|
+
play: async ({ canvasElement }) => {
|
|
124
|
+
const canvas = within(canvasElement);
|
|
125
|
+
const body = within(document.body);
|
|
126
|
+
|
|
127
|
+
// Find and click the trigger button.
|
|
128
|
+
const triggerButton = await canvas.findByTestId('trigger', undefined, { timeout: 5000 });
|
|
129
|
+
await expect(triggerButton).toBeVisible();
|
|
130
|
+
await userEvent.click(triggerButton);
|
|
131
|
+
|
|
132
|
+
// Check that the listbox renders when the trigger is clicked.
|
|
133
|
+
const listbox = await body.findByRole('listbox', undefined, { timeout: 5000 });
|
|
134
|
+
await expect(listbox).toBeVisible();
|
|
135
|
+
|
|
136
|
+
// Find the search input.
|
|
137
|
+
const searchInput = await body.findByPlaceholderText('Search…');
|
|
138
|
+
await expect(searchInput).toBeVisible();
|
|
139
|
+
|
|
140
|
+
// Test searching filters the results - get initial options count.
|
|
141
|
+
const initialOptions = await body.findAllByRole('option');
|
|
142
|
+
const initialCount = initialOptions.length;
|
|
143
|
+
await expect(initialCount).toBeGreaterThan(0);
|
|
144
|
+
|
|
145
|
+
// Type a search term that should filter results.
|
|
146
|
+
await userEvent.type(searchInput, 'John');
|
|
147
|
+
|
|
148
|
+
// Wait a moment for filtering.
|
|
149
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
150
|
+
|
|
151
|
+
// Check that results are filtered (should be fewer options)
|
|
152
|
+
const _filteredOptions = await body.findAllByRole('option');
|
|
153
|
+
// Note: We expect either fewer results or the same if no John exists
|
|
154
|
+
|
|
155
|
+
// Clear search and test selecting a result
|
|
156
|
+
await userEvent.clear(searchInput);
|
|
157
|
+
|
|
158
|
+
// Get the first option and click it
|
|
159
|
+
const updatedOptions = await body.findAllByRole('option');
|
|
160
|
+
const firstOption = updatedOptions[0];
|
|
161
|
+
await expect(firstOption).toBeVisible();
|
|
162
|
+
|
|
163
|
+
const _firstOptionText = firstOption.textContent;
|
|
164
|
+
await userEvent.click(firstOption);
|
|
165
|
+
|
|
166
|
+
// Check that clicking a result fires onSelect with that id
|
|
167
|
+
// The onSelect should have been called with the selected option's id
|
|
168
|
+
await expect(mockHandleSelect).toHaveBeenCalled();
|
|
169
|
+
|
|
170
|
+
// Re-open the picker to test create functionality.
|
|
171
|
+
await userEvent.click(triggerButton);
|
|
172
|
+
|
|
173
|
+
// Wait for listbox to appear again.
|
|
174
|
+
const newListbox = await body.findByRole('listbox', undefined, { timeout: 5000 });
|
|
175
|
+
await expect(newListbox).toBeVisible();
|
|
176
|
+
|
|
177
|
+
const newSearchInput = await body.findByPlaceholderText('Search…');
|
|
178
|
+
|
|
179
|
+
// Test searching an unrelated term provides the __create__ option.
|
|
180
|
+
const unrelatedTerm = 'ZxyUnrelatedName123';
|
|
181
|
+
await userEvent.type(newSearchInput, unrelatedTerm);
|
|
182
|
+
|
|
183
|
+
// Wait for the create option to appear.
|
|
184
|
+
await new Promise((resolve) => setTimeout(resolve, 200));
|
|
185
|
+
|
|
186
|
+
// Look for the create option.
|
|
187
|
+
const createOption = await body.findByRole('option', undefined, { timeout: 3000 });
|
|
188
|
+
await expect(createOption).toBeVisible();
|
|
189
|
+
|
|
190
|
+
// Click the create option - this should change state to the form field.
|
|
191
|
+
await userEvent.click(createOption);
|
|
192
|
+
|
|
193
|
+
// Check that clicking that option changes state to the form field.
|
|
194
|
+
const form = await body.findByTestId('create-referenced-object-form', undefined, { timeout: 5000 });
|
|
195
|
+
await expect(form).toBeVisible();
|
|
196
|
+
|
|
197
|
+
// Find and click the save button.
|
|
198
|
+
const saveButton = await within(form).findByTestId('save-button');
|
|
199
|
+
await expect(saveButton).toBeVisible();
|
|
200
|
+
await userEvent.click(saveButton);
|
|
201
|
+
|
|
202
|
+
// Check that clicking the save button calls onCreate with expected values.
|
|
203
|
+
await expect(mockHandleCreate).toHaveBeenCalledWith(
|
|
204
|
+
expect.objectContaining({
|
|
205
|
+
fullName: unrelatedTerm,
|
|
206
|
+
}),
|
|
207
|
+
);
|
|
208
|
+
},
|
|
209
|
+
};
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2025 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import type * as Schema from 'effect/Schema';
|
|
6
|
+
import React, { type KeyboardEvent, forwardRef, useCallback, useState } from 'react';
|
|
7
|
+
|
|
8
|
+
import { type Palette, Popover, type ThemedClassName, useTranslation } from '@dxos/react-ui';
|
|
9
|
+
import { Combobox, useSearchListInput, useSearchListResults } from '@dxos/react-ui-search';
|
|
10
|
+
|
|
11
|
+
import { translationKey } from '../../translations';
|
|
12
|
+
import { Form } from '../Form';
|
|
13
|
+
import { type FormFieldMap } from '../Form/FormFieldComponent';
|
|
14
|
+
|
|
15
|
+
export type RefOption = {
|
|
16
|
+
id: string;
|
|
17
|
+
label: string;
|
|
18
|
+
hue?: Palette;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export type ObjectPickerContentProps = ThemedClassName<{
|
|
22
|
+
options: RefOption[];
|
|
23
|
+
selectedIds?: string[];
|
|
24
|
+
createOptionLabel?: [string, { ns: string }];
|
|
25
|
+
createOptionIcon?: string;
|
|
26
|
+
createSchema?: Schema.Schema.AnyNoContext;
|
|
27
|
+
createInitialValuePath?: string;
|
|
28
|
+
createFieldMap?: FormFieldMap;
|
|
29
|
+
onCreate?: (values: any) => void;
|
|
30
|
+
onSelect: (id: string) => void;
|
|
31
|
+
}>;
|
|
32
|
+
|
|
33
|
+
const ObjectPickerContent = forwardRef<HTMLDivElement, ObjectPickerContentProps>(
|
|
34
|
+
(
|
|
35
|
+
{
|
|
36
|
+
options,
|
|
37
|
+
selectedIds,
|
|
38
|
+
createSchema,
|
|
39
|
+
createOptionLabel,
|
|
40
|
+
createOptionIcon,
|
|
41
|
+
createInitialValuePath,
|
|
42
|
+
createFieldMap,
|
|
43
|
+
onSelect,
|
|
44
|
+
onCreate,
|
|
45
|
+
...props
|
|
46
|
+
},
|
|
47
|
+
forwardedRef,
|
|
48
|
+
) => {
|
|
49
|
+
const { t } = useTranslation(translationKey);
|
|
50
|
+
const [showForm, setShowForm] = useState(false);
|
|
51
|
+
const [formInitialValue, setFormInitialValue] = useState<string>('');
|
|
52
|
+
|
|
53
|
+
const { results, handleSearch } = useSearchListResults({
|
|
54
|
+
items: options,
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
const handleFormSave = useCallback(
|
|
58
|
+
(values: any) => {
|
|
59
|
+
onCreate?.(values);
|
|
60
|
+
setShowForm(false);
|
|
61
|
+
setFormInitialValue('');
|
|
62
|
+
},
|
|
63
|
+
[onCreate],
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
const handleFormCancel = useCallback(() => {
|
|
67
|
+
setShowForm(false);
|
|
68
|
+
setFormInitialValue('');
|
|
69
|
+
}, []);
|
|
70
|
+
|
|
71
|
+
const handleKeyDown = useCallback(
|
|
72
|
+
(event: KeyboardEvent) => {
|
|
73
|
+
if (
|
|
74
|
+
event.key === 'Enter' &&
|
|
75
|
+
createSchema &&
|
|
76
|
+
(event.currentTarget as HTMLElement).querySelector(
|
|
77
|
+
'[role="option"][aria-selected="true"][data-value="__create__"]',
|
|
78
|
+
)
|
|
79
|
+
) {
|
|
80
|
+
event.stopPropagation();
|
|
81
|
+
event.preventDefault();
|
|
82
|
+
// Get the current query from the input element
|
|
83
|
+
const input = (event.currentTarget as HTMLElement).querySelector('input[type="text"]') as HTMLInputElement;
|
|
84
|
+
const currentQuery = input?.value || '';
|
|
85
|
+
setFormInitialValue(currentQuery);
|
|
86
|
+
setShowForm(true);
|
|
87
|
+
}
|
|
88
|
+
},
|
|
89
|
+
[createSchema],
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
if (showForm && createSchema) {
|
|
93
|
+
return (
|
|
94
|
+
<Combobox.Content {...props} onSearch={handleSearch} onKeyDownCapture={handleKeyDown} ref={forwardedRef}>
|
|
95
|
+
<Popover.Viewport>
|
|
96
|
+
<Form.Root
|
|
97
|
+
testId='create-referenced-object-form'
|
|
98
|
+
schema={createSchema}
|
|
99
|
+
defaultValues={createInitialValuePath ? { [createInitialValuePath]: formInitialValue } : {}}
|
|
100
|
+
fieldMap={createFieldMap}
|
|
101
|
+
onSave={handleFormSave}
|
|
102
|
+
onCancel={handleFormCancel}
|
|
103
|
+
>
|
|
104
|
+
<Form.Viewport>
|
|
105
|
+
<Form.Content>
|
|
106
|
+
<Form.FieldSet />
|
|
107
|
+
<Form.Actions />
|
|
108
|
+
</Form.Content>
|
|
109
|
+
</Form.Viewport>
|
|
110
|
+
</Form.Root>
|
|
111
|
+
</Popover.Viewport>
|
|
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>
|
|
142
|
+
<Combobox.Arrow />
|
|
143
|
+
</Combobox.Content>
|
|
144
|
+
);
|
|
145
|
+
},
|
|
146
|
+
);
|
|
147
|
+
|
|
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';
|
|
179
|
+
|
|
180
|
+
export const ObjectPicker = {
|
|
181
|
+
Root: Combobox.Root,
|
|
182
|
+
Portal: Combobox.Portal,
|
|
183
|
+
Trigger: Combobox.Trigger,
|
|
184
|
+
VirtualTrigger: Combobox.VirtualTrigger,
|
|
185
|
+
Content: ObjectPickerContent,
|
|
186
|
+
};
|
|
@@ -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
|
+
});
|