@dxos/react-ui-form 0.8.4-main.bc674ce → 0.8.4-main.bcb3aa67d6
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 +620 -583
- package/dist/lib/browser/index.mjs.map +4 -4
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/node-esm/index.mjs +620 -583
- 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 +61 -61
- package/dist/types/src/components/FieldEditor/FieldEditor.stories.d.ts.map +1 -1
- package/dist/types/src/components/Form/Form.d.ts +18 -15
- package/dist/types/src/components/Form/Form.d.ts.map +1 -1
- package/dist/types/src/components/Form/Form.stories.d.ts +67 -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 +7 -5
- 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 +61 -61
- 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 +61 -61
- package/dist/types/src/components/ObjectPicker/ObjectPicker.stories.d.ts.map +1 -1
- package/dist/types/src/components/Settings/Settings.d.ts +39 -0
- package/dist/types/src/components/Settings/Settings.d.ts.map +1 -0
- package/dist/types/src/components/Settings/Settings.stories.d.ts +81 -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 +65 -64
- package/dist/types/src/components/ViewEditor/ViewEditor.stories.d.ts.map +1 -1
- package/dist/types/src/components/index.d.ts +1 -1
- package/dist/types/src/components/index.d.ts.map +1 -1
- package/dist/types/src/components/testing.d.ts.map +1 -1
- package/dist/types/src/hooks/useFormHandler.d.ts.map +1 -1
- package/dist/types/src/translations.d.ts +61 -61
- package/dist/types/src/translations.d.ts.map +1 -1
- package/dist/types/src/util/index.d.ts +0 -1
- package/dist/types/src/util/index.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 +36 -38
- package/src/components/FieldEditor/FieldEditor.stories.tsx +4 -6
- package/src/components/FieldEditor/FieldEditor.tsx +3 -4
- package/src/components/Form/AUDIT.md +17 -0
- package/src/components/Form/Form.stories.tsx +23 -24
- package/src/components/Form/Form.tsx +58 -65
- package/src/components/Form/FormField.tsx +27 -8
- package/src/components/Form/FormFieldComponent.stories.tsx +5 -5
- package/src/components/Form/FormFieldComponent.tsx +17 -20
- package/src/components/Form/FormFieldSet.tsx +108 -104
- 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 +35 -17
- package/src/components/Form/fields/SelectField.tsx +1 -1
- package/src/components/Form/fields/SelectOptionField.stories.tsx +2 -2
- package/src/components/Form/fields/SelectOptionField.tsx +9 -10
- package/src/components/Form/fields/TupleField.tsx +1 -1
- package/src/components/ObjectPicker/ObjectPicker.stories.tsx +10 -14
- package/src/components/ObjectPicker/ObjectPicker.tsx +71 -60
- package/src/components/Settings/Settings.stories.tsx +56 -0
- package/src/components/Settings/Settings.tsx +129 -0
- package/src/components/{ControlSection → Settings}/index.ts +1 -1
- package/src/components/ViewEditor/ViewEditor.stories.tsx +22 -20
- package/src/components/ViewEditor/ViewEditor.test.tsx +3 -6
- package/src/components/ViewEditor/ViewEditor.tsx +99 -70
- package/src/components/index.ts +1 -1
- package/src/components/testing.tsx +20 -8
- package/src/hooks/useFormHandler.ts +42 -24
- package/src/translations.ts +62 -63
- package/src/util/index.ts +0 -1
- 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/dist/types/src/util/echo.d.ts +0 -6
- package/dist/types/src/util/echo.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
- package/src/util/echo.ts +0 -25
|
@@ -6,14 +6,15 @@ import '@dxos/lit-ui/dx-tag-picker.pcss';
|
|
|
6
6
|
|
|
7
7
|
import React, { useCallback, useMemo } from 'react';
|
|
8
8
|
|
|
9
|
-
import { type Database, Entity, Filter, Ref,
|
|
9
|
+
import { type Database, Entity, Filter, Obj, Ref, Type } from '@dxos/echo';
|
|
10
10
|
import { ReferenceAnnotationId, type ReferenceAnnotationValue } from '@dxos/echo/internal';
|
|
11
11
|
import { useQuery, useSchema as useSchema$ } from '@dxos/echo-react';
|
|
12
12
|
import { findAnnotation } from '@dxos/effect';
|
|
13
13
|
import { DXN } from '@dxos/keys';
|
|
14
14
|
import { DxAnchor } from '@dxos/lit-ui/react';
|
|
15
15
|
import { Button, Icon, Input, useTranslation } from '@dxos/react-ui';
|
|
16
|
-
import {
|
|
16
|
+
import { ParentLabelAnnotationId } from '@dxos/schema';
|
|
17
|
+
import { mx } from '@dxos/ui-theme';
|
|
17
18
|
|
|
18
19
|
import { translationKey } from '../../../translations';
|
|
19
20
|
import { ObjectPicker, type ObjectPickerContentProps, type RefOption } from '../../ObjectPicker';
|
|
@@ -25,23 +26,35 @@ const isRefSnapshot = (val: any): val is { '/': string } => {
|
|
|
25
26
|
return typeof val === 'object' && typeof (val as any)?.['/'] === 'string';
|
|
26
27
|
};
|
|
27
28
|
|
|
28
|
-
const defaultGetOptions: NonNullable<RefFieldProps['getOptions']> = (results) =>
|
|
29
|
+
const defaultGetOptions: NonNullable<RefFieldProps['getOptions']> = (results, { parentLabel } = {}) =>
|
|
29
30
|
results.map((result) => {
|
|
30
31
|
const id = Entity.getDXN(result).toString();
|
|
31
|
-
const
|
|
32
|
+
const parent = parentLabel ? Obj.getParent(result as Obj.Unknown) : undefined;
|
|
33
|
+
const label = parent ? Entity.getLabel(parent) : Entity.getLabel(result);
|
|
32
34
|
return { id, label: label ?? id };
|
|
33
35
|
});
|
|
34
36
|
|
|
35
37
|
const defaultResultsHook: NonNullable<RefFieldProps['resultsHook']> = (db, typename) =>
|
|
36
|
-
useQuery(
|
|
38
|
+
useQuery(
|
|
39
|
+
db,
|
|
40
|
+
typename
|
|
41
|
+
? // For Ref.Ref(Obj.Unknown) we want to show all objects.
|
|
42
|
+
typename === Type.getTypename(Obj.Unknown)
|
|
43
|
+
? Filter.everything()
|
|
44
|
+
: Filter.typename(typename)
|
|
45
|
+
: Filter.nothing(),
|
|
46
|
+
);
|
|
37
47
|
|
|
38
48
|
export type RefFieldProps = FormFieldComponentProps &
|
|
39
|
-
Pick<
|
|
49
|
+
Pick<
|
|
50
|
+
ObjectPickerContentProps,
|
|
51
|
+
'createOptionLabel' | 'createOptionIcon' | 'createInitialValuePath' | 'createFieldMap'
|
|
52
|
+
> & {
|
|
40
53
|
db?: Database.Database;
|
|
41
54
|
resultsHook?: (db?: Database.Database, typename?: string) => Entity.Any[];
|
|
42
|
-
schemaHook?: (db?: Database.Database, typename?: string) => Type.
|
|
43
|
-
getOptions?: (objects: Entity.Any[]) => RefOption[];
|
|
44
|
-
onCreate?: (schema: Type.
|
|
55
|
+
schemaHook?: (db?: Database.Database, typename?: string) => Type.AnyEntity;
|
|
56
|
+
getOptions?: (objects: Entity.Any[], options?: { parentLabel?: boolean }) => RefOption[];
|
|
57
|
+
onCreate?: (schema: Type.AnyEntity, values: any) => void;
|
|
45
58
|
};
|
|
46
59
|
|
|
47
60
|
export const RefField = (props: RefFieldProps) => {
|
|
@@ -56,6 +69,7 @@ export const RefField = (props: RefFieldProps) => {
|
|
|
56
69
|
createOptionLabel,
|
|
57
70
|
createOptionIcon,
|
|
58
71
|
createInitialValuePath,
|
|
72
|
+
createFieldMap,
|
|
59
73
|
db,
|
|
60
74
|
resultsHook: useResults = defaultResultsHook,
|
|
61
75
|
schemaHook: useSchema = useSchema$,
|
|
@@ -72,7 +86,10 @@ export const RefField = (props: RefFieldProps) => {
|
|
|
72
86
|
);
|
|
73
87
|
|
|
74
88
|
const results = useResults(db, typename);
|
|
75
|
-
const options = useMemo(() =>
|
|
89
|
+
const options = useMemo(() => {
|
|
90
|
+
const parentLabel = type ? findAnnotation<boolean>(type, ParentLabelAnnotationId) === true : false;
|
|
91
|
+
return getOptions(results, { parentLabel });
|
|
92
|
+
}, [results, getOptions, type]);
|
|
76
93
|
|
|
77
94
|
const handleGetValue = useCallback(() => {
|
|
78
95
|
const formValue = getValue();
|
|
@@ -135,9 +152,9 @@ export const RefField = (props: RefFieldProps) => {
|
|
|
135
152
|
<div>
|
|
136
153
|
{readonly ? (
|
|
137
154
|
!item ? (
|
|
138
|
-
<p className={mx(
|
|
155
|
+
<p className={mx('text-description', 'mb-2')}>{t('empty-readonly-ref-field.label')}</p>
|
|
139
156
|
) : (
|
|
140
|
-
<DxAnchor key={item.id}
|
|
157
|
+
<DxAnchor key={item.id} dxn={item.id} rootclassname='me-1'>
|
|
141
158
|
{item.label}
|
|
142
159
|
</DxAnchor>
|
|
143
160
|
)
|
|
@@ -145,15 +162,15 @@ export const RefField = (props: RefFieldProps) => {
|
|
|
145
162
|
<ObjectPicker.Root>
|
|
146
163
|
<ObjectPicker.Trigger asChild classNames='p-0'>
|
|
147
164
|
{item ? (
|
|
148
|
-
<div className='flex gap-
|
|
165
|
+
<div className='flex gap-form-gap w-full'>
|
|
149
166
|
<Input.Root key={item.id}>
|
|
150
|
-
<Input.TextInput value={item.label} readOnly classNames='
|
|
167
|
+
<Input.TextInput value={item.label} readOnly classNames='w-full' />
|
|
151
168
|
</Input.Root>
|
|
152
169
|
</div>
|
|
153
170
|
) : (
|
|
154
|
-
<Button classNames='
|
|
171
|
+
<Button classNames='w-full text-start gap-form-gap'>
|
|
155
172
|
<div role='none' className='grow overflow-hidden'>
|
|
156
|
-
<span className='flex truncate text-description'>{placeholder ?? t('ref
|
|
173
|
+
<span className='flex truncate text-description'>{placeholder ?? t('ref-field.placeholder')}</span>
|
|
157
174
|
</div>
|
|
158
175
|
<Icon size={3} icon='ph--caret-down--bold' />
|
|
159
176
|
</Button>
|
|
@@ -161,13 +178,14 @@ export const RefField = (props: RefFieldProps) => {
|
|
|
161
178
|
</ObjectPicker.Trigger>
|
|
162
179
|
<ObjectPicker.Portal>
|
|
163
180
|
<ObjectPicker.Content
|
|
164
|
-
classNames='
|
|
181
|
+
classNames='dx-card-popover-width'
|
|
165
182
|
options={options}
|
|
166
183
|
selectedIds={selectedIds}
|
|
167
184
|
createSchema={createSchema && omitId(createSchema)}
|
|
168
185
|
createOptionLabel={createOptionLabel}
|
|
169
186
|
createOptionIcon={createOptionIcon}
|
|
170
187
|
createInitialValuePath={createInitialValuePath}
|
|
188
|
+
createFieldMap={createFieldMap}
|
|
171
189
|
onCreate={handleCreate}
|
|
172
190
|
onSelect={handleSelect}
|
|
173
191
|
/>
|
|
@@ -43,7 +43,7 @@ export const SelectField = ({
|
|
|
43
43
|
) : (
|
|
44
44
|
<Select.Root value={value} onValueChange={handleValueChange}>
|
|
45
45
|
{/* TODO(burdon): Placeholder not working? */}
|
|
46
|
-
<Select.TriggerButton classNames='
|
|
46
|
+
<Select.TriggerButton classNames='w-full' disabled={!!readonly} placeholder={placeholder} />
|
|
47
47
|
<Select.Portal>
|
|
48
48
|
<Select.Content>
|
|
49
49
|
<Select.Viewport>
|
|
@@ -7,7 +7,7 @@ import * as Schema from 'effect/Schema';
|
|
|
7
7
|
import React, { useState } from 'react';
|
|
8
8
|
|
|
9
9
|
import { SelectOption } from '@dxos/echo/internal';
|
|
10
|
-
import { withTheme } from '@dxos/react-ui/testing';
|
|
10
|
+
import { withLayout, withTheme } from '@dxos/react-ui/testing';
|
|
11
11
|
|
|
12
12
|
import { translations } from '../../../translations';
|
|
13
13
|
import { TestLayout } from '../../testing';
|
|
@@ -50,7 +50,7 @@ const meta = {
|
|
|
50
50
|
title: 'ui/react-ui-form/SelectOptionField',
|
|
51
51
|
component: SelectOptionField as any,
|
|
52
52
|
render: DefaultStory,
|
|
53
|
-
decorators: [withTheme],
|
|
53
|
+
decorators: [withTheme(), withLayout({ layout: 'fullscreen' })],
|
|
54
54
|
parameters: {
|
|
55
55
|
layout: 'fullscreen',
|
|
56
56
|
translations,
|
|
@@ -128,13 +128,13 @@ export const SelectOptionField = ({
|
|
|
128
128
|
readonly={!!readonly}
|
|
129
129
|
>
|
|
130
130
|
{({ items }) => (
|
|
131
|
-
<div role='list' className='
|
|
131
|
+
<div role='list' className='w-full overflow-auto'>
|
|
132
132
|
{items?.map((item) => (
|
|
133
133
|
<div key={item.id}>
|
|
134
134
|
<List.Item
|
|
135
135
|
role='button'
|
|
136
136
|
item={item}
|
|
137
|
-
classNames={[subtleHover, 'flex flex-col cursor-pointer rounded-
|
|
137
|
+
classNames={[subtleHover, 'flex flex-col cursor-pointer rounded-xs']}
|
|
138
138
|
aria-expanded={selected === item.id}
|
|
139
139
|
>
|
|
140
140
|
<div className='flex items-center'>
|
|
@@ -152,13 +152,12 @@ export const SelectOptionField = ({
|
|
|
152
152
|
/>
|
|
153
153
|
</div>
|
|
154
154
|
{selected === item.id && (
|
|
155
|
-
<div className='
|
|
156
|
-
{
|
|
157
|
-
<
|
|
158
|
-
<div className='grid grid-cols-[1fr_min-content_min-content] gap-1'>
|
|
155
|
+
<div className='flex flex-col p-form-padding gap-form-gap dx-density-fine'>
|
|
156
|
+
<Input.Label classNames='text-xs'>{t('select-option.label')}</Input.Label>
|
|
157
|
+
<div className='grid grid-cols-[1fr_min-content_min-content] gap-form-gap'>
|
|
159
158
|
<Input.TextInput
|
|
160
159
|
disabled={!!readonly}
|
|
161
|
-
placeholder={t('select
|
|
160
|
+
placeholder={t('select-option-label.placeholder')}
|
|
162
161
|
ref={selected === item.id ? inputRef : undefined}
|
|
163
162
|
value={item.title}
|
|
164
163
|
onChange={handleTitleChange(item.id)}
|
|
@@ -170,7 +169,7 @@ export const SelectOptionField = ({
|
|
|
170
169
|
disabled={!!readonly}
|
|
171
170
|
icon='ph--trash--fill'
|
|
172
171
|
iconOnly
|
|
173
|
-
label={t('select
|
|
172
|
+
label={t('select-option-delete.button')}
|
|
174
173
|
onClick={() => handleDelete(item.id)}
|
|
175
174
|
/>
|
|
176
175
|
</div>
|
|
@@ -182,10 +181,10 @@ export const SelectOptionField = ({
|
|
|
182
181
|
<IconButton
|
|
183
182
|
variant='ghost'
|
|
184
183
|
icon='ph--plus--regular'
|
|
185
|
-
label={t('select
|
|
184
|
+
label={t('select-option-add.button')}
|
|
186
185
|
onClick={handleAdd}
|
|
187
186
|
disabled={!!readonly}
|
|
188
|
-
classNames='
|
|
187
|
+
classNames='w-full'
|
|
189
188
|
/>
|
|
190
189
|
</div>
|
|
191
190
|
)}
|
|
@@ -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}
|
|
@@ -74,18 +74,18 @@ const DefaultStory = () => {
|
|
|
74
74
|
);
|
|
75
75
|
|
|
76
76
|
return (
|
|
77
|
-
<div role='none' className='
|
|
77
|
+
<div role='none' className='w-96'>
|
|
78
78
|
<ObjectPicker.Root open={isOpen} onOpenChange={setIsOpen}>
|
|
79
79
|
<ObjectPicker.Trigger asChild>
|
|
80
|
-
<Button variant='primary' data-testid='trigger' classNames='
|
|
80
|
+
<Button variant='primary' data-testid='trigger' classNames='w-full'>
|
|
81
81
|
Select Person
|
|
82
82
|
</Button>
|
|
83
83
|
</ObjectPicker.Trigger>
|
|
84
84
|
<ObjectPicker.Content
|
|
85
|
-
classNames='
|
|
85
|
+
classNames='dx-card-popover-width'
|
|
86
86
|
options={options}
|
|
87
87
|
createSchema={personSchema}
|
|
88
|
-
createOptionLabel={['create
|
|
88
|
+
createOptionLabel={['create-new-person.label', { ns: osTranslations }]}
|
|
89
89
|
createOptionIcon='ph--user-plus--regular'
|
|
90
90
|
createInitialValuePath='fullName'
|
|
91
91
|
onCreate={handleCreateCallback}
|
|
@@ -100,7 +100,7 @@ const meta = {
|
|
|
100
100
|
title: 'ui/react-ui-form/ObjectPicker',
|
|
101
101
|
render: DefaultStory,
|
|
102
102
|
decorators: [
|
|
103
|
-
withTheme,
|
|
103
|
+
withTheme(),
|
|
104
104
|
withClientProvider({
|
|
105
105
|
types: [Person.Person],
|
|
106
106
|
createIdentity: true,
|
|
@@ -201,14 +201,10 @@ export const WithTest: Story = {
|
|
|
201
201
|
await userEvent.click(saveButton);
|
|
202
202
|
|
|
203
203
|
// 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
|
-
});
|
|
204
|
+
await expect(mockHandleCreate).toHaveBeenCalledWith(
|
|
205
|
+
expect.objectContaining({
|
|
206
|
+
fullName: unrelatedTerm,
|
|
207
|
+
}),
|
|
208
|
+
);
|
|
213
209
|
},
|
|
214
210
|
};
|
|
@@ -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,56 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2024 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { type Meta, type StoryObj } from '@storybook/react-vite';
|
|
6
|
+
import React from 'react';
|
|
7
|
+
|
|
8
|
+
import { faker } from '@dxos/random';
|
|
9
|
+
import { Button, Input } from '@dxos/react-ui';
|
|
10
|
+
import { withLayout, withTheme } from '@dxos/react-ui/testing';
|
|
11
|
+
|
|
12
|
+
import { translations } from '../../translations';
|
|
13
|
+
|
|
14
|
+
import { Settings } from './Settings';
|
|
15
|
+
|
|
16
|
+
faker.seed(132);
|
|
17
|
+
|
|
18
|
+
const DefaultStory = () => {
|
|
19
|
+
return (
|
|
20
|
+
<Settings.Viewport>
|
|
21
|
+
<Settings.Section title='Settings' description={faker.lorem.paragraphs(1)}>
|
|
22
|
+
<Settings.Item title={faker.lorem.sentence(2)} description={faker.lorem.paragraphs(1)}>
|
|
23
|
+
<Input.TextInput placeholder='Input' />
|
|
24
|
+
</Settings.Item>
|
|
25
|
+
<Settings.Item title={faker.lorem.sentence(2)} description={faker.lorem.paragraphs(2)}>
|
|
26
|
+
<Input.Switch />
|
|
27
|
+
</Settings.Item>
|
|
28
|
+
<Settings.Item title={faker.lorem.sentence(3)} description={faker.lorem.paragraphs(2)}>
|
|
29
|
+
<Button>Test</Button>
|
|
30
|
+
</Settings.Item>
|
|
31
|
+
</Settings.Section>
|
|
32
|
+
<Settings.Section title='Panel Example'>
|
|
33
|
+
<Settings.Panel>
|
|
34
|
+
<h3 className='text-lg mb-2'>Members</h3>
|
|
35
|
+
<p className='text-description'>Content inside a panel.</p>
|
|
36
|
+
</Settings.Panel>
|
|
37
|
+
</Settings.Section>
|
|
38
|
+
</Settings.Viewport>
|
|
39
|
+
);
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const meta = {
|
|
43
|
+
title: 'ui/react-ui-form/Settings',
|
|
44
|
+
render: DefaultStory,
|
|
45
|
+
decorators: [withTheme(), withLayout({ layout: 'fullscreen' })],
|
|
46
|
+
parameters: {
|
|
47
|
+
layout: 'fullscreen',
|
|
48
|
+
translations,
|
|
49
|
+
},
|
|
50
|
+
} satisfies Meta;
|
|
51
|
+
|
|
52
|
+
export default meta;
|
|
53
|
+
|
|
54
|
+
type Story = StoryObj<typeof meta>;
|
|
55
|
+
|
|
56
|
+
export const Default: Story = {};
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2025 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import React, { type PropsWithChildren } from 'react';
|
|
6
|
+
|
|
7
|
+
import { Input, type Label, ScrollArea, type ThemedClassName, toLocalizedString, useTranslation } from '@dxos/react-ui';
|
|
8
|
+
import { composable, composableProps, mx } from '@dxos/ui-theme';
|
|
9
|
+
|
|
10
|
+
import { translationKey } from '../../translations';
|
|
11
|
+
|
|
12
|
+
const SETTINGS_ROOT_NAME = 'Settings.Root';
|
|
13
|
+
const SETTINGS_VIEWPORT_NAME = 'Settings.Viewport';
|
|
14
|
+
const SETTINGS_SECTION_NAME = 'Settings.Section';
|
|
15
|
+
const SETTINGS_PANEL_NAME = 'Settings.Panel';
|
|
16
|
+
const SETTINGS_ITEM_NAME = 'Settings.Item';
|
|
17
|
+
|
|
18
|
+
//
|
|
19
|
+
// Root (headless — no styling, no scroll).
|
|
20
|
+
//
|
|
21
|
+
|
|
22
|
+
type SettingsRootProps = ThemedClassName<PropsWithChildren>;
|
|
23
|
+
|
|
24
|
+
const SettingsRoot = ({ children, classNames }: SettingsRootProps) => {
|
|
25
|
+
return <div className={mx('flex flex-col gap-2', classNames)}>{children}</div>;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
SettingsRoot.displayName = SETTINGS_ROOT_NAME;
|
|
29
|
+
|
|
30
|
+
//
|
|
31
|
+
// Viewport (scroll area + padding + document layout).
|
|
32
|
+
//
|
|
33
|
+
|
|
34
|
+
const SettingsViewport = composable<HTMLDivElement>(({ children, ...props }, forwardedRef) => {
|
|
35
|
+
return (
|
|
36
|
+
<ScrollArea.Root {...composableProps(props)} orientation='vertical' centered thin ref={forwardedRef}>
|
|
37
|
+
<ScrollArea.Viewport classNames='px-4 pointer-coarse:px-2'>
|
|
38
|
+
<div role='none' className='dx-document flex flex-col gap-2 py-4'>
|
|
39
|
+
{children}
|
|
40
|
+
</div>
|
|
41
|
+
</ScrollArea.Viewport>
|
|
42
|
+
</ScrollArea.Root>
|
|
43
|
+
);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
SettingsViewport.displayName = SETTINGS_VIEWPORT_NAME;
|
|
47
|
+
|
|
48
|
+
//
|
|
49
|
+
// Section (title + optional description + children with spacing).
|
|
50
|
+
//
|
|
51
|
+
|
|
52
|
+
type SettingsSectionProps = PropsWithChildren<{
|
|
53
|
+
title: Label;
|
|
54
|
+
description?: Label;
|
|
55
|
+
}>;
|
|
56
|
+
|
|
57
|
+
const SettingsSection = ({ title, description, children }: SettingsSectionProps) => {
|
|
58
|
+
const { t } = useTranslation(translationKey);
|
|
59
|
+
return (
|
|
60
|
+
<>
|
|
61
|
+
<h2 className='px-trim-md mt-trim-md text-xl'>{toLocalizedString(title, t)}</h2>
|
|
62
|
+
{description && <p className='px-trim-md text-description'>{toLocalizedString(description, t)}</p>}
|
|
63
|
+
<div className='w-full pt-trim-md space-y-trim-md'>{children}</div>
|
|
64
|
+
</>
|
|
65
|
+
);
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
SettingsSection.displayName = SETTINGS_SECTION_NAME;
|
|
69
|
+
|
|
70
|
+
//
|
|
71
|
+
// Panel (bordered card container).
|
|
72
|
+
//
|
|
73
|
+
|
|
74
|
+
type SettingsPanelProps = ThemedClassName<PropsWithChildren>;
|
|
75
|
+
|
|
76
|
+
const SettingsPanel = ({ classNames, children }: SettingsPanelProps) => {
|
|
77
|
+
return (
|
|
78
|
+
<div role='none' className={mx('flex flex-col gap-3 p-trim-md border border-separator rounded-sm', classNames)}>
|
|
79
|
+
{children}
|
|
80
|
+
</div>
|
|
81
|
+
);
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
SettingsPanel.displayName = SETTINGS_PANEL_NAME;
|
|
85
|
+
|
|
86
|
+
//
|
|
87
|
+
// Item (label + optional description + control, proper label semantics).
|
|
88
|
+
//
|
|
89
|
+
|
|
90
|
+
type SettingsItemProps = PropsWithChildren<{
|
|
91
|
+
title: Label;
|
|
92
|
+
description?: Label;
|
|
93
|
+
}>;
|
|
94
|
+
|
|
95
|
+
const SettingsItem = ({ title, description = '', children }: SettingsItemProps) => {
|
|
96
|
+
const { t } = useTranslation(translationKey);
|
|
97
|
+
|
|
98
|
+
return (
|
|
99
|
+
<Input.Root>
|
|
100
|
+
<SettingsPanel classNames='grid grid-cols-1 md:grid-cols-[1fr_1fr] gap-x-trim-lg gap-y-0'>
|
|
101
|
+
<Input.Label classNames='pb-trim-md text-base-surface-text text-lg md:col-span-2'>
|
|
102
|
+
{toLocalizedString(title, t)}
|
|
103
|
+
</Input.Label>
|
|
104
|
+
<Input.DescriptionAndValidation>
|
|
105
|
+
<Input.Description classNames='text-base text-description'>
|
|
106
|
+
{toLocalizedString(description, t)}
|
|
107
|
+
</Input.Description>
|
|
108
|
+
</Input.DescriptionAndValidation>
|
|
109
|
+
<div role='none' className='text-end py-1'>
|
|
110
|
+
{children}
|
|
111
|
+
</div>
|
|
112
|
+
</SettingsPanel>
|
|
113
|
+
</Input.Root>
|
|
114
|
+
);
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
SettingsItem.displayName = SETTINGS_ITEM_NAME;
|
|
118
|
+
|
|
119
|
+
//
|
|
120
|
+
// Settings
|
|
121
|
+
//
|
|
122
|
+
|
|
123
|
+
export const Settings = {
|
|
124
|
+
Root: SettingsRoot,
|
|
125
|
+
Viewport: SettingsViewport,
|
|
126
|
+
Section: SettingsSection,
|
|
127
|
+
Panel: SettingsPanel,
|
|
128
|
+
Item: SettingsItem,
|
|
129
|
+
};
|