mirror of
https://github.com/langgenius/dify.git
synced 2026-05-10 05:56:31 +08:00
refactor(web): migrate drawer components to dify-ui and remove legacy drawer implementation (#35982)
Co-authored-by: CodingOnStar <hanxujiang@dify.com>
This commit is contained in:
parent
24ea21db25
commit
e48d7bb097
@ -305,9 +305,6 @@
|
||||
}
|
||||
},
|
||||
"web/app/components/app/configuration/config/agent/agent-tools/setting-built-in-tool.tsx": {
|
||||
"no-restricted-imports": {
|
||||
"count": 1
|
||||
},
|
||||
"react-hooks/exhaustive-deps": {
|
||||
"count": 1
|
||||
},
|
||||
@ -359,16 +356,6 @@
|
||||
"count": 2
|
||||
}
|
||||
},
|
||||
"web/app/components/app/configuration/configuration-view.tsx": {
|
||||
"no-restricted-imports": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"web/app/components/app/configuration/dataset-config/card-item/index.tsx": {
|
||||
"no-restricted-imports": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"web/app/components/app/configuration/dataset-config/index.tsx": {
|
||||
"ts/no-explicit-any": {
|
||||
"count": 1
|
||||
@ -475,9 +462,6 @@
|
||||
}
|
||||
},
|
||||
"web/app/components/app/log/list.tsx": {
|
||||
"no-restricted-imports": {
|
||||
"count": 1
|
||||
},
|
||||
"react/set-state-in-effect": {
|
||||
"count": 6
|
||||
},
|
||||
@ -519,9 +503,6 @@
|
||||
}
|
||||
},
|
||||
"web/app/components/app/workflow-log/list.tsx": {
|
||||
"no-restricted-imports": {
|
||||
"count": 1
|
||||
},
|
||||
"react/set-state-in-effect": {
|
||||
"count": 2
|
||||
}
|
||||
@ -933,11 +914,6 @@
|
||||
"count": 3
|
||||
}
|
||||
},
|
||||
"web/app/components/base/float-right-container/index.tsx": {
|
||||
"no-restricted-imports": {
|
||||
"count": 2
|
||||
}
|
||||
},
|
||||
"web/app/components/base/form/components/base/base-form.tsx": {
|
||||
"ts/no-explicit-any": {
|
||||
"count": 6
|
||||
@ -2092,9 +2068,6 @@
|
||||
}
|
||||
},
|
||||
"web/app/components/datasets/hit-testing/index.tsx": {
|
||||
"no-restricted-imports": {
|
||||
"count": 1
|
||||
},
|
||||
"react/unsupported-syntax": {
|
||||
"count": 1
|
||||
}
|
||||
@ -2540,18 +2513,10 @@
|
||||
}
|
||||
},
|
||||
"web/app/components/plugins/plugin-detail-panel/endpoint-modal.tsx": {
|
||||
"no-restricted-imports": {
|
||||
"count": 1
|
||||
},
|
||||
"ts/no-explicit-any": {
|
||||
"count": 7
|
||||
}
|
||||
},
|
||||
"web/app/components/plugins/plugin-detail-panel/index.tsx": {
|
||||
"no-restricted-imports": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"web/app/components/plugins/plugin-detail-panel/model-list.tsx": {
|
||||
"ts/no-explicit-any": {
|
||||
"count": 1
|
||||
@ -2573,9 +2538,6 @@
|
||||
}
|
||||
},
|
||||
"web/app/components/plugins/plugin-detail-panel/strategy-detail.tsx": {
|
||||
"no-restricted-imports": {
|
||||
"count": 1
|
||||
},
|
||||
"ts/no-explicit-any": {
|
||||
"count": 2
|
||||
}
|
||||
@ -2634,9 +2596,6 @@
|
||||
}
|
||||
},
|
||||
"web/app/components/plugins/plugin-detail-panel/trigger/event-detail-drawer.tsx": {
|
||||
"no-restricted-imports": {
|
||||
"count": 1
|
||||
},
|
||||
"ts/no-explicit-any": {
|
||||
"count": 5
|
||||
}
|
||||
@ -2874,11 +2833,6 @@
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"web/app/components/tools/mcp/detail/provider-detail.tsx": {
|
||||
"no-restricted-imports": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"web/app/components/tools/mcp/mcp-server-param-item.tsx": {
|
||||
"ts/no-explicit-any": {
|
||||
"count": 1
|
||||
@ -2894,11 +2848,6 @@
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"web/app/components/tools/provider/detail.tsx": {
|
||||
"no-restricted-imports": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"web/app/components/tools/provider/empty.tsx": {
|
||||
"ts/no-explicit-any": {
|
||||
"count": 1
|
||||
@ -3711,11 +3660,6 @@
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"web/app/components/workflow/nodes/knowledge-retrieval/components/dataset-item.tsx": {
|
||||
"no-restricted-imports": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"web/app/components/workflow/nodes/knowledge-retrieval/components/metadata/condition-list/condition-item.tsx": {
|
||||
"ts/no-explicit-any": {
|
||||
"count": 1
|
||||
|
||||
@ -120,19 +120,6 @@ vi.mock('@/utils/var', () => ({
|
||||
basePath: '',
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/base/drawer', () => ({
|
||||
default: ({ isOpen, children, onClose }: { isOpen: boolean, children: React.ReactNode, onClose: () => void }) => (
|
||||
isOpen
|
||||
? (
|
||||
<div data-testid="drawer">
|
||||
{children}
|
||||
<button data-testid="drawer-close" onClick={onClose}>Close Drawer</button>
|
||||
</div>
|
||||
)
|
||||
: null
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock('@langgenius/dify-ui/toast', () => ({
|
||||
default: { notify: vi.fn() },
|
||||
toast: {
|
||||
@ -525,10 +512,10 @@ describe('Tool Provider Detail Flow Integration', () => {
|
||||
render(<ProviderDetail collection={collection} onHide={mockOnHide} onRefreshData={mockOnRefreshData} />)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('drawer')).toBeInTheDocument()
|
||||
expect(screen.getByRole('dialog')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
fireEvent.click(screen.getByTestId('drawer-close'))
|
||||
fireEvent.click(screen.getByRole('button', { name: 'operation.close' }))
|
||||
expect(mockOnHide).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
@ -4,6 +4,14 @@ import type { Collection, Tool } from '@/app/components/tools/types'
|
||||
import type { ToolWithProvider } from '@/app/components/workflow/types'
|
||||
import { Button } from '@langgenius/dify-ui/button'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import {
|
||||
Drawer,
|
||||
DrawerBackdrop,
|
||||
DrawerContent,
|
||||
DrawerPopup,
|
||||
DrawerPortal,
|
||||
DrawerViewport,
|
||||
} from '@langgenius/dify-ui/drawer'
|
||||
import {
|
||||
RiArrowLeftLine,
|
||||
RiCloseLine,
|
||||
@ -12,7 +20,6 @@ import * as React from 'react'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import ActionButton from '@/app/components/base/action-button'
|
||||
import Drawer from '@/app/components/base/drawer'
|
||||
import Loading from '@/app/components/base/loading'
|
||||
import TabSlider from '@/app/components/base/tab-slider-plain'
|
||||
import Form from '@/app/components/header/account-setting/model-provider-page/model-modal/Form'
|
||||
@ -165,98 +172,105 @@ const SettingBuiltInTool: FC<Props> = ({
|
||||
|
||||
return (
|
||||
<Drawer
|
||||
isOpen
|
||||
clickOutsideNotOpen={false}
|
||||
onClose={onHide}
|
||||
footer={null}
|
||||
mask={false}
|
||||
positionCenter={false}
|
||||
panelClassName={cn('mt-[64px] mr-2 mb-2 w-[420px]! max-w-[420px]! justify-start rounded-2xl border-[0.5px] border-components-panel-border bg-components-panel-bg! p-0! shadow-xl')}
|
||||
open
|
||||
modal
|
||||
swipeDirection="right"
|
||||
onOpenChange={(open) => {
|
||||
if (!open)
|
||||
onHide()
|
||||
}}
|
||||
>
|
||||
<>
|
||||
{isLoading && <Loading type="app" />}
|
||||
{!isLoading && (
|
||||
<>
|
||||
{/* header */}
|
||||
<div className="relative border-b border-divider-subtle p-4 pb-3">
|
||||
<div className="absolute top-3 right-3">
|
||||
<ActionButton onClick={onHide}>
|
||||
<RiCloseLine className="h-4 w-4" />
|
||||
</ActionButton>
|
||||
</div>
|
||||
{showBackButton && (
|
||||
<div
|
||||
className="mb-2 flex cursor-pointer items-center gap-1 system-xs-semibold-uppercase text-text-accent-secondary"
|
||||
onClick={onHide}
|
||||
>
|
||||
<RiArrowLeftLine className="h-4 w-4" />
|
||||
{t('detailPanel.operation.back', { ns: 'plugin' })}
|
||||
</div>
|
||||
)}
|
||||
<div className="flex items-center gap-1">
|
||||
<Icon size="tiny" className="h-6 w-6" src={collection.icon} />
|
||||
<OrgInfo
|
||||
packageNameClassName="w-auto"
|
||||
orgName={collection.author}
|
||||
packageName={collection.name.split('/').pop() || ''}
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-1 system-md-semibold text-text-primary">{currTool?.label[language]}</div>
|
||||
{!!currTool?.description[language] && (
|
||||
<Description className="mt-3 mb-2 h-auto" text={currTool.description[language]} descriptionLineRows={2}></Description>
|
||||
)}
|
||||
{
|
||||
collection.allow_delete && collection.type === CollectionType.builtIn && (
|
||||
<PluginAuthInAgent
|
||||
pluginPayload={{
|
||||
provider: collection.name,
|
||||
category: AuthCategory.tool,
|
||||
providerType: collection.type,
|
||||
detail: collection as any,
|
||||
}}
|
||||
credentialId={credentialId}
|
||||
onAuthorizationItemClick={onAuthorizationItemClick}
|
||||
/>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
{/* form */}
|
||||
<div className="h-full">
|
||||
<div className="flex h-full flex-col">
|
||||
{(hasSetting && !readonly)
|
||||
? (
|
||||
<TabSlider
|
||||
className="mt-1 shrink-0 px-4"
|
||||
itemClassName="py-3"
|
||||
noBorderBottom
|
||||
value={currType}
|
||||
onChange={(value) => {
|
||||
setCurrType(value)
|
||||
}}
|
||||
options={[
|
||||
{ value: 'info', text: t('setBuiltInTools.parameters', { ns: 'tools' })! },
|
||||
{ value: 'setting', text: t('setBuiltInTools.setting', { ns: 'tools' })! },
|
||||
]}
|
||||
/>
|
||||
)
|
||||
: (
|
||||
<div className="p-4 pb-1 system-sm-semibold-uppercase text-text-primary">{t('setBuiltInTools.parameters', { ns: 'tools' })}</div>
|
||||
)}
|
||||
<div className="h-0 grow overflow-y-auto px-4">
|
||||
{isInfoActive ? infoUI : settingUI}
|
||||
{!readonly && !isInfoActive && (
|
||||
<div className="flex shrink-0 justify-end space-x-2 rounded-b-[10px] bg-components-panel-bg py-2">
|
||||
<Button className="flex h-8 items-center px-3! text-[13px]! font-medium" onClick={onHide}>{t('operation.cancel', { ns: 'common' })}</Button>
|
||||
<Button className="flex h-8 items-center px-3! text-[13px]! font-medium" variant="primary" disabled={!isValid} onClick={() => onSave?.(tempSetting)}>{t('operation.save', { ns: 'common' })}</Button>
|
||||
<DrawerPortal>
|
||||
<DrawerBackdrop className="bg-transparent" />
|
||||
<DrawerViewport>
|
||||
<DrawerPopup className={cn('justify-start bg-components-panel-bg! p-0! shadow-xl data-[swipe-direction=right]:top-16 data-[swipe-direction=right]:right-2 data-[swipe-direction=right]:bottom-2 data-[swipe-direction=right]:h-auto data-[swipe-direction=right]:w-[420px] data-[swipe-direction=right]:max-w-[420px] data-[swipe-direction=right]:rounded-2xl data-[swipe-direction=right]:border-[0.5px] data-[swipe-direction=right]:border-components-panel-border')}>
|
||||
<DrawerContent className="flex min-h-0 flex-1 flex-col p-0 pb-0">
|
||||
{isLoading && <Loading type="app" />}
|
||||
{!isLoading && (
|
||||
<>
|
||||
{/* header */}
|
||||
<div className="relative border-b border-divider-subtle p-4 pb-3">
|
||||
<div className="absolute top-3 right-3">
|
||||
<ActionButton onClick={onHide}>
|
||||
<RiCloseLine className="h-4 w-4" />
|
||||
</ActionButton>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<ReadmeEntrance pluginDetail={collection as any} className="mt-auto" />
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
{showBackButton && (
|
||||
<div
|
||||
className="mb-2 flex cursor-pointer items-center gap-1 system-xs-semibold-uppercase text-text-accent-secondary"
|
||||
onClick={onHide}
|
||||
>
|
||||
<RiArrowLeftLine className="h-4 w-4" />
|
||||
{t('detailPanel.operation.back', { ns: 'plugin' })}
|
||||
</div>
|
||||
)}
|
||||
<div className="flex items-center gap-1">
|
||||
<Icon size="tiny" className="h-6 w-6" src={collection.icon} />
|
||||
<OrgInfo
|
||||
packageNameClassName="w-auto"
|
||||
orgName={collection.author}
|
||||
packageName={collection.name.split('/').pop() || ''}
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-1 system-md-semibold text-text-primary">{currTool?.label[language]}</div>
|
||||
{!!currTool?.description[language] && (
|
||||
<Description className="mt-3 mb-2 h-auto" text={currTool.description[language]} descriptionLineRows={2}></Description>
|
||||
)}
|
||||
{
|
||||
collection.allow_delete && collection.type === CollectionType.builtIn && (
|
||||
<PluginAuthInAgent
|
||||
pluginPayload={{
|
||||
provider: collection.name,
|
||||
category: AuthCategory.tool,
|
||||
providerType: collection.type,
|
||||
detail: collection as any,
|
||||
}}
|
||||
credentialId={credentialId}
|
||||
onAuthorizationItemClick={onAuthorizationItemClick}
|
||||
/>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
{/* form */}
|
||||
<div className="h-full">
|
||||
<div className="flex h-full flex-col">
|
||||
{(hasSetting && !readonly)
|
||||
? (
|
||||
<TabSlider
|
||||
className="mt-1 shrink-0 px-4"
|
||||
itemClassName="py-3"
|
||||
noBorderBottom
|
||||
value={currType}
|
||||
onChange={(value) => {
|
||||
setCurrType(value)
|
||||
}}
|
||||
options={[
|
||||
{ value: 'info', text: t('setBuiltInTools.parameters', { ns: 'tools' })! },
|
||||
{ value: 'setting', text: t('setBuiltInTools.setting', { ns: 'tools' })! },
|
||||
]}
|
||||
/>
|
||||
)
|
||||
: (
|
||||
<div className="p-4 pb-1 system-sm-semibold-uppercase text-text-primary">{t('setBuiltInTools.parameters', { ns: 'tools' })}</div>
|
||||
)}
|
||||
<div className="h-0 grow overflow-y-auto px-4">
|
||||
{isInfoActive ? infoUI : settingUI}
|
||||
{!readonly && !isInfoActive && (
|
||||
<div className="flex shrink-0 justify-end space-x-2 rounded-b-[10px] bg-components-panel-bg py-2">
|
||||
<Button className="flex h-8 items-center px-3! text-[13px]! font-medium" onClick={onHide}>{t('operation.cancel', { ns: 'common' })}</Button>
|
||||
<Button className="flex h-8 items-center px-3! text-[13px]! font-medium" variant="primary" disabled={!isValid} onClick={() => onSave?.(tempSetting)}>{t('operation.save', { ns: 'common' })}</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<ReadmeEntrance pluginDetail={collection as any} className="mt-auto" />
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</DrawerContent>
|
||||
</DrawerPopup>
|
||||
</DrawerViewport>
|
||||
</DrawerPortal>
|
||||
</Drawer>
|
||||
)
|
||||
}
|
||||
|
||||
@ -12,6 +12,15 @@ import {
|
||||
AlertDialogTitle,
|
||||
} from '@langgenius/dify-ui/alert-dialog'
|
||||
import { Button } from '@langgenius/dify-ui/button'
|
||||
import {
|
||||
Drawer,
|
||||
DrawerBackdrop,
|
||||
DrawerCloseButton,
|
||||
DrawerContent,
|
||||
DrawerPopup,
|
||||
DrawerPortal,
|
||||
DrawerViewport,
|
||||
} from '@langgenius/dify-ui/drawer'
|
||||
import * as React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import AppPublisher from '@/app/components/app/app-publisher/features-wrapper'
|
||||
@ -21,7 +30,6 @@ import AgentSettingButton from '@/app/components/app/configuration/config/agent-
|
||||
import SelectDataSet from '@/app/components/app/configuration/dataset-config/select-dataset'
|
||||
import Debug from '@/app/components/app/configuration/debug'
|
||||
import Divider from '@/app/components/base/divider'
|
||||
import Drawer from '@/app/components/base/drawer'
|
||||
import { FeaturesProvider } from '@/app/components/base/features'
|
||||
import NewFeaturePanel from '@/app/components/base/features/new-feature-panel'
|
||||
import Loading from '@/app/components/base/loading'
|
||||
@ -192,19 +200,43 @@ const ConfigurationView: FC<ConfigurationViewModel> = ({
|
||||
)}
|
||||
|
||||
{isMobile && (
|
||||
<Drawer showClose isOpen={isShowDebugPanel} onClose={onHideDebugPanel} mask footer={null}>
|
||||
<Debug
|
||||
isAPIKeySet={contextValue.isAPIKeySet}
|
||||
onSetting={onOpenAccountSettings}
|
||||
inputs={contextValue.inputs}
|
||||
modelParameterParams={{
|
||||
setModel: onModelChange,
|
||||
onCompletionParamsChange,
|
||||
}}
|
||||
debugWithMultipleModel={!!debugWithMultipleModel}
|
||||
multipleModelConfigs={multipleModelConfigs}
|
||||
onMultipleModelConfigsChange={onMultipleModelConfigsChange}
|
||||
/>
|
||||
<Drawer
|
||||
open={isShowDebugPanel}
|
||||
modal
|
||||
swipeDirection="right"
|
||||
onOpenChange={(open) => {
|
||||
if (!open)
|
||||
onHideDebugPanel()
|
||||
}}
|
||||
>
|
||||
<DrawerPortal>
|
||||
<DrawerBackdrop className="bg-black/30" />
|
||||
<DrawerViewport>
|
||||
<DrawerPopup className="data-[swipe-direction=right]:w-full data-[swipe-direction=right]:max-w-sm">
|
||||
<DrawerContent className="flex min-h-0 flex-1 flex-col">
|
||||
<div className="mb-4 flex shrink-0 justify-end">
|
||||
<DrawerCloseButton
|
||||
aria-label={t('operation.close', { ns: 'common' })}
|
||||
className="h-6 w-6 rounded-md"
|
||||
data-testid="close-icon"
|
||||
/>
|
||||
</div>
|
||||
<Debug
|
||||
isAPIKeySet={contextValue.isAPIKeySet}
|
||||
onSetting={onOpenAccountSettings}
|
||||
inputs={contextValue.inputs}
|
||||
modelParameterParams={{
|
||||
setModel: onModelChange,
|
||||
onCompletionParamsChange,
|
||||
}}
|
||||
debugWithMultipleModel={!!debugWithMultipleModel}
|
||||
multipleModelConfigs={multipleModelConfigs}
|
||||
onMultipleModelConfigsChange={onMultipleModelConfigsChange}
|
||||
/>
|
||||
</DrawerContent>
|
||||
</DrawerPopup>
|
||||
</DrawerViewport>
|
||||
</DrawerPortal>
|
||||
</Drawer>
|
||||
)}
|
||||
|
||||
|
||||
@ -230,8 +230,12 @@ describe('dataset-config/card-item', () => {
|
||||
expect(screen.getByText('Mock settings modal'))!.toBeInTheDocument()
|
||||
|
||||
const overlay = [...document.querySelectorAll('[class]')]
|
||||
.find(element => element.className.toString().includes('bg-black/30'))
|
||||
.find(element =>
|
||||
element instanceof HTMLElement
|
||||
&& element.classList.contains('bg-background-overlay')
|
||||
&& !element.classList.contains('bg-transparent'),
|
||||
)
|
||||
|
||||
expect(overlay)!.toBeInTheDocument()
|
||||
expect(overlay).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
@ -2,6 +2,14 @@
|
||||
import type { FC } from 'react'
|
||||
import type { DataSet } from '@/models/datasets'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import {
|
||||
Drawer,
|
||||
DrawerBackdrop,
|
||||
DrawerContent,
|
||||
DrawerPopup,
|
||||
DrawerPortal,
|
||||
DrawerViewport,
|
||||
} from '@langgenius/dify-ui/drawer'
|
||||
import {
|
||||
RiDeleteBinLine,
|
||||
RiEditLine,
|
||||
@ -12,7 +20,6 @@ import { useTranslation } from 'react-i18next'
|
||||
import ActionButton, { ActionButtonState } from '@/app/components/base/action-button'
|
||||
import AppIcon from '@/app/components/base/app-icon'
|
||||
import Badge from '@/app/components/base/badge'
|
||||
import Drawer from '@/app/components/base/drawer'
|
||||
import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints'
|
||||
import { useKnowledge } from '@/hooks/use-knowledge'
|
||||
import SettingsModal from '../settings-modal'
|
||||
@ -112,14 +119,31 @@ const Item: FC<ItemProps> = ({
|
||||
/>
|
||||
)
|
||||
}
|
||||
<Drawer isOpen={showSettingsModal} onClose={() => setShowSettingsModal(false)} footer={null} mask={isMobile} panelClassName="mt-16 mx-2 sm:mr-2 mb-3 p-0! max-w-[640px]! rounded-xl">
|
||||
{showSettingsModal && (
|
||||
<SettingsModal
|
||||
currentDataset={config}
|
||||
onCancel={() => setShowSettingsModal(false)}
|
||||
onSave={handleSave}
|
||||
/>
|
||||
)}
|
||||
<Drawer
|
||||
open={showSettingsModal}
|
||||
modal
|
||||
swipeDirection="right"
|
||||
onOpenChange={(open) => {
|
||||
if (!open)
|
||||
setShowSettingsModal(false)
|
||||
}}
|
||||
>
|
||||
<DrawerPortal>
|
||||
<DrawerBackdrop className={cn(!isMobile && 'bg-transparent')} />
|
||||
<DrawerViewport>
|
||||
<DrawerPopup className="p-0! data-[swipe-direction=right]:top-16 data-[swipe-direction=right]:right-2 data-[swipe-direction=right]:bottom-3 data-[swipe-direction=right]:h-auto data-[swipe-direction=right]:w-full data-[swipe-direction=right]:max-w-[640px] data-[swipe-direction=right]:rounded-xl">
|
||||
<DrawerContent className="flex min-h-0 flex-1 flex-col p-0 pb-0">
|
||||
{showSettingsModal && (
|
||||
<SettingsModal
|
||||
currentDataset={config}
|
||||
onCancel={() => setShowSettingsModal(false)}
|
||||
onSave={handleSave}
|
||||
/>
|
||||
)}
|
||||
</DrawerContent>
|
||||
</DrawerPopup>
|
||||
</DrawerViewport>
|
||||
</DrawerPortal>
|
||||
</Drawer>
|
||||
</div>
|
||||
)
|
||||
|
||||
@ -84,19 +84,6 @@ vi.mock('@/app/components/app/store', () => ({
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/base/drawer', () => ({
|
||||
default: ({ children, isOpen, onClose }: { children: ReactNode, isOpen: boolean, onClose: () => void }) => (
|
||||
isOpen
|
||||
? (
|
||||
<div data-testid="drawer">
|
||||
<button onClick={onClose}>close-drawer</button>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
: null
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/base/loading', () => ({
|
||||
default: () => <div>loading</div>,
|
||||
}))
|
||||
@ -283,7 +270,7 @@ describe('ConversationList', () => {
|
||||
|
||||
await waitFor(() => {
|
||||
expect(onUrlUpdate).toHaveBeenCalled()
|
||||
expect(screen.getByTestId('drawer')).toBeInTheDocument()
|
||||
expect(screen.getByRole('dialog')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
const update = onUrlUpdate.mock.calls.at(-1)![0]
|
||||
@ -293,11 +280,26 @@ describe('ConversationList', () => {
|
||||
})
|
||||
|
||||
it('should close the drawer, refresh, and clear modal flags', async () => {
|
||||
mockChatConversationDetail = {
|
||||
id: 'conversation-1',
|
||||
created_at: 1710000000,
|
||||
model_config: {
|
||||
model: 'gpt-4o',
|
||||
configs: {
|
||||
introduction: 'Hello there',
|
||||
},
|
||||
user_input_form: [],
|
||||
},
|
||||
message: {
|
||||
inputs: {},
|
||||
},
|
||||
}
|
||||
|
||||
const { onUrlUpdate } = renderConversationList({
|
||||
searchParams: '?page=2&conversation_id=conversation-1',
|
||||
})
|
||||
|
||||
fireEvent.click(screen.getByText('close-drawer'))
|
||||
fireEvent.click(await screen.findByRole('button', { name: 'operation.close' }))
|
||||
|
||||
expect(mockOnRefresh).toHaveBeenCalledTimes(1)
|
||||
expect(mockSetShowPromptLogModal).toHaveBeenCalledWith(false)
|
||||
|
||||
@ -9,6 +9,14 @@ import {
|
||||
HandThumbUpIcon,
|
||||
} from '@heroicons/react/24/outline'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import {
|
||||
Drawer,
|
||||
DrawerBackdrop,
|
||||
DrawerContent,
|
||||
DrawerPopup,
|
||||
DrawerPortal,
|
||||
DrawerViewport,
|
||||
} from '@langgenius/dify-ui/drawer'
|
||||
import { toast } from '@langgenius/dify-ui/toast'
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip'
|
||||
import { RiCloseLine, RiEditFill } from '@remixicon/react'
|
||||
@ -28,7 +36,6 @@ import TextGeneration from '@/app/components/app/text-generate/item'
|
||||
import ActionButton from '@/app/components/base/action-button'
|
||||
import Chat from '@/app/components/base/chat/chat'
|
||||
import CopyIcon from '@/app/components/base/copy-icon'
|
||||
import Drawer from '@/app/components/base/drawer'
|
||||
import Loading from '@/app/components/base/loading'
|
||||
import MessageLogModal from '@/app/components/base/message-log-modal'
|
||||
import { WorkflowContextProvider } from '@/app/components/workflow/context'
|
||||
@ -429,7 +436,7 @@ function DetailPanel({ detail, onFeedback }: IDetailPanel) {
|
||||
<div className="flex grow flex-wrap items-center justify-end gap-y-1">
|
||||
{!isAdvanced && <ModelInfo model={detail.model_config.model} />}
|
||||
</div>
|
||||
<ActionButton size="l" onClick={onClose}>
|
||||
<ActionButton size="l" aria-label={t('operation.close', { ns: 'common' })} onClick={onClose}>
|
||||
<RiCloseLine className="h-4 w-4 text-text-tertiary" />
|
||||
</ActionButton>
|
||||
</div>
|
||||
@ -872,21 +879,32 @@ const ConversationList: FC<IConversationList> = ({ logs, appDetail, onRefresh })
|
||||
</tbody>
|
||||
</table>
|
||||
<Drawer
|
||||
isOpen={showDrawer}
|
||||
onClose={onCloseDrawer}
|
||||
mask={isMobile}
|
||||
footer={null}
|
||||
panelClassName="mt-16 mx-2 sm:mr-2 mb-4 p-0! max-w-[640px]! rounded-xl bg-components-panel-bg"
|
||||
>
|
||||
<DrawerContext.Provider value={{
|
||||
onClose: onCloseDrawer,
|
||||
appDetail,
|
||||
open={showDrawer}
|
||||
modal
|
||||
swipeDirection="right"
|
||||
onOpenChange={(open) => {
|
||||
if (!open)
|
||||
onCloseDrawer()
|
||||
}}
|
||||
>
|
||||
{isChatMode
|
||||
? <ChatConversationDetailComp appId={appDetail.id} conversationId={currentConversation?.id} />
|
||||
: <CompletionConversationDetailComp appId={appDetail.id} conversationId={currentConversation?.id} />}
|
||||
</DrawerContext.Provider>
|
||||
>
|
||||
<DrawerPortal>
|
||||
<DrawerBackdrop className={cn(!isMobile && 'bg-transparent')} />
|
||||
<DrawerViewport>
|
||||
<DrawerPopup className="bg-components-panel-bg p-0! data-[swipe-direction=right]:top-16 data-[swipe-direction=right]:right-2 data-[swipe-direction=right]:bottom-4 data-[swipe-direction=right]:h-auto data-[swipe-direction=right]:w-full data-[swipe-direction=right]:max-w-[640px] data-[swipe-direction=right]:rounded-xl">
|
||||
<DrawerContent className="flex min-h-0 flex-1 flex-col p-0 pb-0">
|
||||
<DrawerContext.Provider value={{
|
||||
onClose: onCloseDrawer,
|
||||
appDetail,
|
||||
}}
|
||||
>
|
||||
{isChatMode
|
||||
? <ChatConversationDetailComp appId={appDetail.id} conversationId={currentConversation?.id} />
|
||||
: <CompletionConversationDetailComp appId={appDetail.id} conversationId={currentConversation?.id} />}
|
||||
</DrawerContext.Provider>
|
||||
</DrawerContent>
|
||||
</DrawerPopup>
|
||||
</DrawerViewport>
|
||||
</DrawerPortal>
|
||||
</Drawer>
|
||||
</div>
|
||||
)
|
||||
|
||||
@ -4,10 +4,17 @@ import type { WorkflowAppLogDetail, WorkflowLogsResponse, WorkflowRunTriggeredFr
|
||||
import type { App } from '@/types/app'
|
||||
import { ArrowDownIcon } from '@heroicons/react/24/outline'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import {
|
||||
Drawer,
|
||||
DrawerBackdrop,
|
||||
DrawerContent,
|
||||
DrawerPopup,
|
||||
DrawerPortal,
|
||||
DrawerViewport,
|
||||
} from '@langgenius/dify-ui/drawer'
|
||||
import * as React from 'react'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Drawer from '@/app/components/base/drawer'
|
||||
import Loading from '@/app/components/base/loading'
|
||||
import Indicator from '@/app/components/header/indicator'
|
||||
import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints'
|
||||
@ -183,17 +190,28 @@ const WorkflowAppLogList: FC<ILogs> = ({ logs, appDetail, onRefresh }) => {
|
||||
</tbody>
|
||||
</table>
|
||||
<Drawer
|
||||
isOpen={showDrawer}
|
||||
onClose={onCloseDrawer}
|
||||
mask={isMobile}
|
||||
footer={null}
|
||||
panelClassName="mt-16 mx-2 sm:mr-2 mb-3 p-0! max-w-[600px]! rounded-xl border border-components-panel-border"
|
||||
open={showDrawer}
|
||||
modal
|
||||
swipeDirection="right"
|
||||
onOpenChange={(open) => {
|
||||
if (!open)
|
||||
onCloseDrawer()
|
||||
}}
|
||||
>
|
||||
<DetailPanel
|
||||
onClose={onCloseDrawer}
|
||||
runID={currentLog?.workflow_run.id || ''}
|
||||
canReplay={currentLog?.workflow_run.triggered_from === 'app-run' || currentLog?.workflow_run.triggered_from === 'debugging'}
|
||||
/>
|
||||
<DrawerPortal>
|
||||
<DrawerBackdrop className={cn(!isMobile && 'bg-transparent')} />
|
||||
<DrawerViewport>
|
||||
<DrawerPopup className="p-0! data-[swipe-direction=right]:top-16 data-[swipe-direction=right]:right-2 data-[swipe-direction=right]:bottom-3 data-[swipe-direction=right]:h-auto data-[swipe-direction=right]:w-full data-[swipe-direction=right]:max-w-[600px] data-[swipe-direction=right]:rounded-xl data-[swipe-direction=right]:border data-[swipe-direction=right]:border-components-panel-border">
|
||||
<DrawerContent className="flex min-h-0 flex-1 flex-col p-0 pb-0">
|
||||
<DetailPanel
|
||||
onClose={onCloseDrawer}
|
||||
runID={currentLog?.workflow_run.id || ''}
|
||||
canReplay={currentLog?.workflow_run.triggered_from === 'app-run' || currentLog?.workflow_run.triggered_from === 'debugging'}
|
||||
/>
|
||||
</DrawerContent>
|
||||
</DrawerPopup>
|
||||
</DrawerViewport>
|
||||
</DrawerPortal>
|
||||
</Drawer>
|
||||
</div>
|
||||
)
|
||||
|
||||
@ -1,676 +0,0 @@
|
||||
import type { IDrawerProps } from '../index'
|
||||
import { fireEvent, render, screen } from '@testing-library/react'
|
||||
import * as React from 'react'
|
||||
import Drawer from '../index'
|
||||
|
||||
// Capture dialog onClose for testing
|
||||
let capturedDialogOnClose: (() => void) | null = null
|
||||
|
||||
// Mock Base UI Dialog anatomy; behavior is covered at the legacy wrapper boundary here.
|
||||
vi.mock('@base-ui/react/dialog', () => ({
|
||||
Dialog: {
|
||||
Root: ({ children, open, onOpenChange }: {
|
||||
children: React.ReactNode
|
||||
open: boolean
|
||||
onOpenChange: (open: boolean) => void
|
||||
}) => {
|
||||
capturedDialogOnClose = () => onOpenChange(false)
|
||||
if (!open)
|
||||
return null
|
||||
return <>{children}</>
|
||||
},
|
||||
Portal: ({ children }: {
|
||||
children: React.ReactNode
|
||||
}) => <>{children}</>,
|
||||
Backdrop: ({ children, className }: {
|
||||
children?: React.ReactNode
|
||||
className: string
|
||||
}) => (
|
||||
<div
|
||||
data-testid="dialog-backdrop"
|
||||
className={className}
|
||||
onClick={() => capturedDialogOnClose?.()}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
),
|
||||
Popup: ({ children, className, ...props }: {
|
||||
children: React.ReactNode
|
||||
className: string
|
||||
}) => (
|
||||
<div
|
||||
data-testid="dialog"
|
||||
className={className}
|
||||
role="dialog"
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
),
|
||||
Title: ({ children, className, render, ...props }: {
|
||||
children: React.ReactNode
|
||||
className?: string
|
||||
render?: React.ReactElement
|
||||
}) => {
|
||||
const Component = render?.type ?? 'h2'
|
||||
return (
|
||||
<Component data-testid="dialog-title" className={className} {...props}>
|
||||
{children}
|
||||
</Component>
|
||||
)
|
||||
},
|
||||
},
|
||||
}))
|
||||
|
||||
// Mock XMarkIcon
|
||||
vi.mock('@heroicons/react/24/outline', () => ({
|
||||
XMarkIcon: ({ className, onClick }: { className: string, onClick?: () => void }) => (
|
||||
<svg data-testid="close-icon" className={className} onClick={onClick} />
|
||||
),
|
||||
}))
|
||||
|
||||
// Helper function to render Drawer with default props
|
||||
const defaultProps: IDrawerProps = {
|
||||
isOpen: true,
|
||||
onClose: vi.fn(),
|
||||
children: <div data-testid="drawer-content">Content</div>,
|
||||
}
|
||||
|
||||
const renderDrawer = (props: Partial<IDrawerProps> = {}) => {
|
||||
const mergedProps = { ...defaultProps, ...props }
|
||||
return render(<Drawer {...mergedProps} />)
|
||||
}
|
||||
|
||||
describe('Drawer', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
capturedDialogOnClose = null
|
||||
})
|
||||
|
||||
// Basic rendering tests
|
||||
describe('Rendering', () => {
|
||||
it('should render when isOpen is true', () => {
|
||||
// Arrange & Act
|
||||
renderDrawer({ isOpen: true })
|
||||
|
||||
// Assert
|
||||
expect(screen.getByRole('dialog')).toBeInTheDocument()
|
||||
expect(screen.getByTestId('drawer-content')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should not render when isOpen is false', () => {
|
||||
// Arrange & Act
|
||||
renderDrawer({ isOpen: false })
|
||||
|
||||
// Assert
|
||||
expect(screen.queryByRole('dialog')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render children content', () => {
|
||||
// Arrange
|
||||
const childContent = <p data-testid="custom-child">Custom Content</p>
|
||||
|
||||
// Act
|
||||
renderDrawer({ children: childContent })
|
||||
|
||||
// Assert
|
||||
expect(screen.getByTestId('custom-child')).toBeInTheDocument()
|
||||
expect(screen.getByText('Custom Content')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
// Title and description tests
|
||||
describe('Title and Description', () => {
|
||||
it('should render title when provided', () => {
|
||||
// Arrange & Act
|
||||
renderDrawer({ title: 'Test Title' })
|
||||
|
||||
// Assert
|
||||
expect(screen.getByText('Test Title')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should not render title when not provided', () => {
|
||||
// Arrange & Act
|
||||
renderDrawer({ title: '' })
|
||||
|
||||
// Assert
|
||||
const titles = screen.queryAllByTestId('dialog-title')
|
||||
const titleWithText = titles.find(el => el.textContent !== '')
|
||||
expect(titleWithText).toBeUndefined()
|
||||
})
|
||||
|
||||
it('should render description when provided', () => {
|
||||
// Arrange & Act
|
||||
renderDrawer({ description: 'Test Description' })
|
||||
|
||||
// Assert
|
||||
expect(screen.getByText('Test Description')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should not render description when not provided', () => {
|
||||
// Arrange & Act
|
||||
renderDrawer({ description: '' })
|
||||
|
||||
// Assert
|
||||
expect(screen.queryByText('Test Description')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render both title and description together', () => {
|
||||
// Arrange & Act
|
||||
renderDrawer({
|
||||
title: 'My Title',
|
||||
description: 'My Description',
|
||||
})
|
||||
|
||||
// Assert
|
||||
expect(screen.getByText('My Title')).toBeInTheDocument()
|
||||
expect(screen.getByText('My Description')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
// Close button tests
|
||||
describe('Close Button', () => {
|
||||
it('should render close icon when showClose is true', () => {
|
||||
// Arrange & Act
|
||||
renderDrawer({ showClose: true })
|
||||
|
||||
// Assert
|
||||
expect(screen.getByTestId('close-icon')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should not render close icon when showClose is false', () => {
|
||||
// Arrange & Act
|
||||
renderDrawer({ showClose: false })
|
||||
|
||||
// Assert
|
||||
expect(screen.queryByTestId('close-icon')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should not render close icon by default', () => {
|
||||
// Arrange & Act
|
||||
renderDrawer({})
|
||||
|
||||
// Assert
|
||||
expect(screen.queryByTestId('close-icon')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should call onClose when close icon is clicked', () => {
|
||||
// Arrange
|
||||
const onClose = vi.fn()
|
||||
renderDrawer({ showClose: true, onClose })
|
||||
|
||||
// Act
|
||||
fireEvent.click(screen.getByTestId('close-icon'))
|
||||
|
||||
// Assert
|
||||
expect(onClose).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
})
|
||||
|
||||
// Backdrop/Mask tests
|
||||
describe('Backdrop and Mask', () => {
|
||||
it('should render backdrop when noOverlay is false', () => {
|
||||
// Arrange & Act
|
||||
renderDrawer({ noOverlay: false })
|
||||
|
||||
// Assert
|
||||
expect(screen.getByTestId('dialog-backdrop')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should not render backdrop when noOverlay is true', () => {
|
||||
// Arrange & Act
|
||||
renderDrawer({ noOverlay: true })
|
||||
|
||||
// Assert
|
||||
expect(screen.queryByTestId('dialog-backdrop')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should apply mask background when mask is true', () => {
|
||||
// Arrange & Act
|
||||
renderDrawer({ mask: true })
|
||||
|
||||
// Assert
|
||||
const backdrop = screen.getByTestId('dialog-backdrop')
|
||||
expect(backdrop.className).toContain('bg-black/30')
|
||||
})
|
||||
|
||||
it('should not apply mask background when mask is false', () => {
|
||||
// Arrange & Act
|
||||
renderDrawer({ mask: false })
|
||||
|
||||
// Assert
|
||||
const backdrop = screen.getByTestId('dialog-backdrop')
|
||||
expect(backdrop.className).not.toContain('bg-black/30')
|
||||
})
|
||||
|
||||
it('should call onClose when backdrop is clicked and clickOutsideNotOpen is false', () => {
|
||||
// Arrange
|
||||
const onClose = vi.fn()
|
||||
renderDrawer({ onClose, clickOutsideNotOpen: false })
|
||||
|
||||
// Act
|
||||
fireEvent.click(screen.getByTestId('dialog-backdrop'))
|
||||
|
||||
// Assert
|
||||
expect(onClose).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('should not call onClose when backdrop is clicked and clickOutsideNotOpen is true', () => {
|
||||
// Arrange
|
||||
const onClose = vi.fn()
|
||||
renderDrawer({ onClose, clickOutsideNotOpen: true })
|
||||
|
||||
// Act
|
||||
fireEvent.click(screen.getByTestId('dialog-backdrop'))
|
||||
|
||||
// Assert
|
||||
expect(onClose).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
// Footer tests
|
||||
describe('Footer', () => {
|
||||
it('should render default footer with cancel and save buttons when footer is undefined', () => {
|
||||
// Arrange & Act
|
||||
renderDrawer({ footer: undefined })
|
||||
|
||||
// Assert
|
||||
expect(screen.getByText('common.operation.cancel')).toBeInTheDocument()
|
||||
expect(screen.getByText('common.operation.save')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should not render footer when footer is null', () => {
|
||||
// Arrange & Act
|
||||
renderDrawer({ footer: null })
|
||||
|
||||
// Assert
|
||||
expect(screen.queryByText('common.operation.cancel')).not.toBeInTheDocument()
|
||||
expect(screen.queryByText('common.operation.save')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render custom footer when provided', () => {
|
||||
// Arrange
|
||||
const customFooter = <div data-testid="custom-footer">Custom Footer</div>
|
||||
|
||||
// Act
|
||||
renderDrawer({ footer: customFooter })
|
||||
|
||||
// Assert
|
||||
expect(screen.getByTestId('custom-footer')).toBeInTheDocument()
|
||||
expect(screen.queryByText('common.operation.cancel')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should call onCancel when cancel button is clicked', () => {
|
||||
// Arrange
|
||||
const onCancel = vi.fn()
|
||||
renderDrawer({ onCancel })
|
||||
|
||||
// Act
|
||||
const cancelButton = screen.getByText('common.operation.cancel')
|
||||
fireEvent.click(cancelButton)
|
||||
|
||||
// Assert
|
||||
expect(onCancel).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('should call onOk when save button is clicked', () => {
|
||||
// Arrange
|
||||
const onOk = vi.fn()
|
||||
renderDrawer({ onOk })
|
||||
|
||||
// Act
|
||||
const saveButton = screen.getByText('common.operation.save')
|
||||
fireEvent.click(saveButton)
|
||||
|
||||
// Assert
|
||||
expect(onOk).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('should not throw when onCancel is not provided and cancel is clicked', () => {
|
||||
// Arrange
|
||||
renderDrawer({ onCancel: undefined })
|
||||
|
||||
// Act & Assert
|
||||
expect(() => {
|
||||
fireEvent.click(screen.getByText('common.operation.cancel'))
|
||||
}).not.toThrow()
|
||||
})
|
||||
|
||||
it('should not throw when onOk is not provided and save is clicked', () => {
|
||||
// Arrange
|
||||
renderDrawer({ onOk: undefined })
|
||||
|
||||
// Act & Assert
|
||||
expect(() => {
|
||||
fireEvent.click(screen.getByText('common.operation.save'))
|
||||
}).not.toThrow()
|
||||
})
|
||||
})
|
||||
|
||||
// Custom className tests
|
||||
describe('Custom ClassNames', () => {
|
||||
it('should apply custom dialogClassName', () => {
|
||||
// Arrange & Act
|
||||
const { container } = renderDrawer({ dialogClassName: 'custom-dialog-class' })
|
||||
|
||||
// Assert
|
||||
expect(container.querySelector('.custom-dialog-class')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should apply custom dialogBackdropClassName', () => {
|
||||
// Arrange & Act
|
||||
renderDrawer({ dialogBackdropClassName: 'custom-backdrop-class' })
|
||||
|
||||
// Assert
|
||||
expect(screen.getByTestId('dialog-backdrop').className).toContain('custom-backdrop-class')
|
||||
})
|
||||
|
||||
it('should apply custom containerClassName', () => {
|
||||
// Arrange & Act
|
||||
const { container } = renderDrawer({ containerClassName: 'custom-container-class' })
|
||||
|
||||
// Assert
|
||||
const containerDiv = container.querySelector('.custom-container-class')
|
||||
expect(containerDiv).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should apply custom panelClassName', () => {
|
||||
// Arrange & Act
|
||||
const { container } = renderDrawer({ panelClassName: 'custom-panel-class' })
|
||||
|
||||
// Assert
|
||||
const panelDiv = container.querySelector('.custom-panel-class')
|
||||
expect(panelDiv).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
// Position tests
|
||||
describe('Position', () => {
|
||||
it('should apply center position class when positionCenter is true', () => {
|
||||
// Arrange & Act
|
||||
const { container } = renderDrawer({ positionCenter: true })
|
||||
|
||||
// Assert
|
||||
const containerDiv = container.querySelector('.justify-center\\!')
|
||||
expect(containerDiv).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should use end position by default when positionCenter is false', () => {
|
||||
// Arrange & Act
|
||||
const { container } = renderDrawer({ positionCenter: false })
|
||||
|
||||
// Assert
|
||||
const containerDiv = container.querySelector('.justify-end')
|
||||
expect(containerDiv).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
// Unmount prop tests
|
||||
describe('Unmount Prop', () => {
|
||||
it('should pass unmount prop to Dialog component', () => {
|
||||
// Arrange & Act
|
||||
renderDrawer({ unmount: true })
|
||||
|
||||
// Assert
|
||||
expect(screen.getByTestId('dialog').getAttribute('data-unmount')).toBe('true')
|
||||
})
|
||||
|
||||
it('should default unmount to false', () => {
|
||||
// Arrange & Act
|
||||
renderDrawer({})
|
||||
|
||||
// Assert
|
||||
expect(screen.getByTestId('dialog').getAttribute('data-unmount')).toBe('false')
|
||||
})
|
||||
})
|
||||
|
||||
// Edge cases
|
||||
describe('Edge Cases', () => {
|
||||
it('should handle empty string title', () => {
|
||||
// Arrange & Act
|
||||
renderDrawer({ title: '' })
|
||||
|
||||
// Assert
|
||||
expect(screen.getByRole('dialog')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should handle empty string description', () => {
|
||||
// Arrange & Act
|
||||
renderDrawer({ description: '' })
|
||||
|
||||
// Assert
|
||||
expect(screen.getByRole('dialog')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should handle special characters in title', () => {
|
||||
// Arrange
|
||||
const specialTitle = '<script>alert("xss")</script>'
|
||||
|
||||
// Act
|
||||
renderDrawer({ title: specialTitle })
|
||||
|
||||
// Assert
|
||||
expect(screen.getByText(specialTitle)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should handle very long title', () => {
|
||||
// Arrange
|
||||
const longTitle = 'A'.repeat(500)
|
||||
|
||||
// Act
|
||||
renderDrawer({ title: longTitle })
|
||||
|
||||
// Assert
|
||||
expect(screen.getByText(longTitle)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should handle complex children with multiple elements', () => {
|
||||
// Arrange
|
||||
const complexChildren = (
|
||||
<div data-testid="complex-children">
|
||||
<h1>Heading</h1>
|
||||
<p>Paragraph</p>
|
||||
<input data-testid="input-element" />
|
||||
<button data-testid="button-element">Button</button>
|
||||
</div>
|
||||
)
|
||||
|
||||
// Act
|
||||
renderDrawer({ children: complexChildren })
|
||||
|
||||
// Assert
|
||||
expect(screen.getByTestId('complex-children')).toBeInTheDocument()
|
||||
expect(screen.getByText('Heading')).toBeInTheDocument()
|
||||
expect(screen.getByText('Paragraph')).toBeInTheDocument()
|
||||
expect(screen.getByTestId('input-element')).toBeInTheDocument()
|
||||
expect(screen.getByTestId('button-element')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should handle null children gracefully', () => {
|
||||
// Arrange & Act
|
||||
renderDrawer({ children: null as unknown as React.ReactNode })
|
||||
|
||||
// Assert
|
||||
expect(screen.getByRole('dialog')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should handle undefined footer without crashing', () => {
|
||||
// Arrange & Act
|
||||
renderDrawer({ footer: undefined })
|
||||
|
||||
// Assert
|
||||
expect(screen.getByRole('dialog')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should handle rapid open/close toggles', () => {
|
||||
// Arrange
|
||||
const onClose = vi.fn()
|
||||
const { rerender } = render(
|
||||
<Drawer {...defaultProps} isOpen={true} onClose={onClose}>
|
||||
<div>Content</div>
|
||||
</Drawer>,
|
||||
)
|
||||
|
||||
// Act - Toggle multiple times
|
||||
rerender(
|
||||
<Drawer {...defaultProps} isOpen={false} onClose={onClose}>
|
||||
<div>Content</div>
|
||||
</Drawer>,
|
||||
)
|
||||
rerender(
|
||||
<Drawer {...defaultProps} isOpen={true} onClose={onClose}>
|
||||
<div>Content</div>
|
||||
</Drawer>,
|
||||
)
|
||||
rerender(
|
||||
<Drawer {...defaultProps} isOpen={false} onClose={onClose}>
|
||||
<div>Content</div>
|
||||
</Drawer>,
|
||||
)
|
||||
|
||||
// Assert
|
||||
expect(screen.queryByRole('dialog')).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
// Combined prop scenarios
|
||||
describe('Combined Prop Scenarios', () => {
|
||||
it('should render with all optional props', () => {
|
||||
// Arrange & Act
|
||||
renderDrawer({
|
||||
title: 'Full Feature Title',
|
||||
description: 'Full Feature Description',
|
||||
dialogClassName: 'custom-dialog',
|
||||
dialogBackdropClassName: 'custom-backdrop',
|
||||
containerClassName: 'custom-container',
|
||||
panelClassName: 'custom-panel',
|
||||
showClose: true,
|
||||
mask: true,
|
||||
positionCenter: true,
|
||||
unmount: true,
|
||||
noOverlay: false,
|
||||
footer: <div data-testid="custom-full-footer">Footer</div>,
|
||||
})
|
||||
|
||||
// Assert
|
||||
expect(screen.getByRole('dialog')).toBeInTheDocument()
|
||||
expect(screen.getByText('Full Feature Title')).toBeInTheDocument()
|
||||
expect(screen.getByText('Full Feature Description')).toBeInTheDocument()
|
||||
expect(screen.getByTestId('close-icon')).toBeInTheDocument()
|
||||
expect(screen.getByTestId('custom-full-footer')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render minimal drawer with only required props', () => {
|
||||
// Arrange
|
||||
const minimalProps: IDrawerProps = {
|
||||
isOpen: true,
|
||||
onClose: vi.fn(),
|
||||
children: <div>Minimal Content</div>,
|
||||
}
|
||||
|
||||
// Act
|
||||
render(<Drawer {...minimalProps} />)
|
||||
|
||||
// Assert
|
||||
expect(screen.getByRole('dialog')).toBeInTheDocument()
|
||||
expect(screen.getByText('Minimal Content')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should handle showClose with title simultaneously', () => {
|
||||
// Arrange & Act
|
||||
renderDrawer({
|
||||
title: 'Title with Close',
|
||||
showClose: true,
|
||||
})
|
||||
|
||||
// Assert
|
||||
expect(screen.getByText('Title with Close')).toBeInTheDocument()
|
||||
expect(screen.getByTestId('close-icon')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should handle noOverlay with clickOutsideNotOpen', () => {
|
||||
// Arrange
|
||||
const onClose = vi.fn()
|
||||
|
||||
// Act
|
||||
renderDrawer({
|
||||
noOverlay: true,
|
||||
clickOutsideNotOpen: true,
|
||||
onClose,
|
||||
})
|
||||
|
||||
// Assert - backdrop should not exist
|
||||
expect(screen.queryByTestId('dialog-backdrop')).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
// Dialog onClose callback tests (e.g., Escape key)
|
||||
describe('Dialog onClose Callback', () => {
|
||||
it('should call onClose when Dialog triggers close and clickOutsideNotOpen is false', () => {
|
||||
// Arrange
|
||||
const onClose = vi.fn()
|
||||
renderDrawer({ onClose, clickOutsideNotOpen: false })
|
||||
|
||||
// Act - Simulate Dialog's onClose (e.g., pressing Escape)
|
||||
capturedDialogOnClose?.()
|
||||
|
||||
// Assert
|
||||
expect(onClose).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('should not call onClose when Dialog triggers close and clickOutsideNotOpen is true', () => {
|
||||
// Arrange
|
||||
const onClose = vi.fn()
|
||||
renderDrawer({ onClose, clickOutsideNotOpen: true })
|
||||
|
||||
// Act - Simulate Dialog's onClose (e.g., pressing Escape)
|
||||
capturedDialogOnClose?.()
|
||||
|
||||
// Assert
|
||||
expect(onClose).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should call onClose by default when Dialog triggers close', () => {
|
||||
// Arrange
|
||||
const onClose = vi.fn()
|
||||
renderDrawer({ onClose })
|
||||
|
||||
// Act
|
||||
capturedDialogOnClose?.()
|
||||
|
||||
// Assert
|
||||
expect(onClose).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
})
|
||||
|
||||
// Event handler interaction tests
|
||||
describe('Event Handler Interactions', () => {
|
||||
it('should handle multiple consecutive close icon clicks', () => {
|
||||
// Arrange
|
||||
const onClose = vi.fn()
|
||||
renderDrawer({ showClose: true, onClose })
|
||||
|
||||
// Act
|
||||
const closeIcon = screen.getByTestId('close-icon')
|
||||
fireEvent.click(closeIcon)
|
||||
fireEvent.click(closeIcon)
|
||||
fireEvent.click(closeIcon)
|
||||
|
||||
// Assert
|
||||
expect(onClose).toHaveBeenCalledTimes(3)
|
||||
})
|
||||
|
||||
it('should handle onCancel and onOk being the same function', () => {
|
||||
// Arrange
|
||||
const handler = vi.fn()
|
||||
renderDrawer({ onCancel: handler, onOk: handler })
|
||||
|
||||
// Act
|
||||
fireEvent.click(screen.getByText('common.operation.cancel'))
|
||||
fireEvent.click(screen.getByText('common.operation.save'))
|
||||
|
||||
// Assert
|
||||
expect(handler).toHaveBeenCalledTimes(2)
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -1,114 +0,0 @@
|
||||
import type { Meta, StoryObj } from '@storybook/nextjs-vite'
|
||||
import { useState } from 'react'
|
||||
import { fn } from 'storybook/test'
|
||||
import Drawer from '.'
|
||||
|
||||
const meta = {
|
||||
title: 'Base/Feedback/Drawer',
|
||||
component: Drawer,
|
||||
parameters: {
|
||||
layout: 'fullscreen',
|
||||
docs: {
|
||||
description: {
|
||||
component: 'Sliding panel built on Base UI dialog primitives. Supports optional mask, custom footer, and close behaviour.',
|
||||
},
|
||||
},
|
||||
},
|
||||
tags: ['autodocs'],
|
||||
} satisfies Meta<typeof Drawer>
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof meta>
|
||||
|
||||
const DrawerDemo = (props: React.ComponentProps<typeof Drawer>) => {
|
||||
const [open, setOpen] = useState(false)
|
||||
|
||||
return (
|
||||
<div className="flex h-[400px] items-center justify-center bg-background-default-subtle">
|
||||
<button
|
||||
type="button"
|
||||
className="rounded-md bg-primary-600 px-4 py-2 text-sm font-medium text-white shadow-sm hover:bg-primary-700"
|
||||
onClick={() => setOpen(true)}
|
||||
>
|
||||
Open drawer
|
||||
</button>
|
||||
|
||||
<Drawer
|
||||
{...props}
|
||||
isOpen={open}
|
||||
onClose={() => setOpen(false)}
|
||||
title={props.title ?? 'Edit configuration'}
|
||||
description={props.description ?? 'Adjust settings in the side panel and save.'}
|
||||
footer={props.footer ?? undefined}
|
||||
>
|
||||
<div className="mt-4 space-y-3 text-sm text-text-secondary">
|
||||
<p>
|
||||
This example renders arbitrary content inside the drawer body. Use it for contextual forms, settings, or informational panels.
|
||||
</p>
|
||||
<div className="rounded-lg border border-divider-subtle bg-components-panel-bg p-3 text-xs">
|
||||
Content area
|
||||
</div>
|
||||
</div>
|
||||
</Drawer>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const Playground: Story = {
|
||||
render: args => <DrawerDemo {...args} />,
|
||||
args: {
|
||||
children: null,
|
||||
isOpen: false,
|
||||
onClose: fn(),
|
||||
},
|
||||
parameters: {
|
||||
docs: {
|
||||
source: {
|
||||
language: 'tsx',
|
||||
code: `
|
||||
const [open, setOpen] = useState(false)
|
||||
|
||||
<Drawer
|
||||
isOpen={open}
|
||||
onClose={() => setOpen(false)}
|
||||
title="Edit configuration"
|
||||
description="Adjust settings in the side panel and save."
|
||||
>
|
||||
...
|
||||
</Drawer>
|
||||
`.trim(),
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export const CustomFooter: Story = {
|
||||
render: args => (
|
||||
<DrawerDemo
|
||||
{...args}
|
||||
footer={(
|
||||
<div className="mt-6 flex justify-end gap-2">
|
||||
<button className="rounded-md border border-divider-subtle px-3 py-1.5 text-sm text-text-secondary" onClick={() => args.onCancel?.()}>Discard</button>
|
||||
<button className="rounded-md bg-primary-600 px-3 py-1.5 text-sm text-white">Save changes</button>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
),
|
||||
args: {
|
||||
children: null,
|
||||
isOpen: false,
|
||||
onClose: fn(),
|
||||
},
|
||||
parameters: {
|
||||
docs: {
|
||||
source: {
|
||||
language: 'tsx',
|
||||
code: `
|
||||
<Drawer footer={<CustomFooter />}>
|
||||
...
|
||||
</Drawer>
|
||||
`.trim(),
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@ -1,128 +0,0 @@
|
||||
'use client'
|
||||
// eslint-disable-next-line no-restricted-imports -- Temporary legacy drawer exception: remove this direct Base UI wrapper after callers migrate to dify-ui drawer primitives.
|
||||
import { Dialog as BaseDialog } from '@base-ui/react/dialog'
|
||||
import { Button } from '@langgenius/dify-ui/button'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
export type IDrawerProps = {
|
||||
title?: string
|
||||
description?: string
|
||||
dialogClassName?: string
|
||||
dialogBackdropClassName?: string
|
||||
containerClassName?: string
|
||||
panelClassName?: string
|
||||
children: React.ReactNode
|
||||
footer?: React.ReactNode
|
||||
mask?: boolean
|
||||
positionCenter?: boolean
|
||||
isOpen: boolean
|
||||
showClose?: boolean
|
||||
clickOutsideNotOpen?: boolean
|
||||
onClose: () => void
|
||||
onCancel?: () => void
|
||||
onOk?: () => void
|
||||
unmount?: boolean
|
||||
noOverlay?: boolean
|
||||
}
|
||||
|
||||
export default function Drawer({
|
||||
title = '',
|
||||
description = '',
|
||||
dialogClassName = '',
|
||||
dialogBackdropClassName = '',
|
||||
containerClassName = '',
|
||||
panelClassName = '',
|
||||
children,
|
||||
footer,
|
||||
mask = true,
|
||||
positionCenter,
|
||||
showClose = false,
|
||||
isOpen,
|
||||
clickOutsideNotOpen,
|
||||
onClose,
|
||||
onCancel,
|
||||
onOk,
|
||||
unmount = false,
|
||||
noOverlay = false,
|
||||
}: IDrawerProps) {
|
||||
const { t } = useTranslation()
|
||||
return (
|
||||
<BaseDialog.Root
|
||||
open={isOpen}
|
||||
disablePointerDismissal={clickOutsideNotOpen}
|
||||
onOpenChange={(open) => {
|
||||
if (!open && !clickOutsideNotOpen)
|
||||
onClose()
|
||||
}}
|
||||
>
|
||||
<BaseDialog.Portal>
|
||||
<div className={cn('fixed inset-0 z-30 overflow-y-auto', dialogClassName)}>
|
||||
<div className={cn('flex h-screen w-screen justify-end', positionCenter && 'justify-center!', containerClassName)}>
|
||||
{!noOverlay && (
|
||||
<BaseDialog.Backdrop
|
||||
className={cn('fixed inset-0 z-40', mask && 'bg-black/30', dialogBackdropClassName)}
|
||||
/>
|
||||
)}
|
||||
<BaseDialog.Popup
|
||||
data-unmount={unmount}
|
||||
className={cn('relative z-50 flex w-full max-w-sm flex-col justify-between overflow-hidden bg-components-panel-bg p-6 text-left align-middle shadow-xl', panelClassName)}
|
||||
>
|
||||
<>
|
||||
<div className="flex justify-between">
|
||||
{title && (
|
||||
<BaseDialog.Title
|
||||
render={<h3 />}
|
||||
className="text-lg leading-6 font-medium text-text-primary"
|
||||
>
|
||||
{title}
|
||||
</BaseDialog.Title>
|
||||
)}
|
||||
{showClose && (
|
||||
<div className="mb-4 flex cursor-pointer items-center">
|
||||
<span
|
||||
className="i-heroicons-x-mark h-4 w-4 text-text-tertiary"
|
||||
onClick={onClose}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' || e.key === ' ')
|
||||
onClose()
|
||||
}}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
aria-label={t('operation.close', { ns: 'common' })}
|
||||
data-testid="close-icon"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{description && <div className="mt-2 text-xs font-normal text-text-tertiary">{description}</div>}
|
||||
{children}
|
||||
</>
|
||||
{footer || (footer === null
|
||||
? null
|
||||
: (
|
||||
<div className="mt-10 flex flex-row justify-end">
|
||||
<Button
|
||||
className="mr-2"
|
||||
onClick={() => {
|
||||
onCancel?.()
|
||||
}}
|
||||
>
|
||||
{t('operation.cancel', { ns: 'common' })}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
onOk?.()
|
||||
}}
|
||||
>
|
||||
{t('operation.save', { ns: 'common' })}
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
</BaseDialog.Popup>
|
||||
</div>
|
||||
</div>
|
||||
</BaseDialog.Portal>
|
||||
</BaseDialog.Root>
|
||||
)
|
||||
}
|
||||
@ -32,7 +32,6 @@ describe('FloatRightContainer', () => {
|
||||
isMobile={true}
|
||||
isOpen={false}
|
||||
onClose={vi.fn()}
|
||||
unmount={true}
|
||||
>
|
||||
<div>Closed mobile content</div>
|
||||
</FloatRightContainer>,
|
||||
@ -99,53 +98,12 @@ describe('FloatRightContainer', () => {
|
||||
expect(onClose).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('should call onClose when close is done using escape key', async () => {
|
||||
const onClose = vi.fn()
|
||||
render(
|
||||
<FloatRightContainer
|
||||
isMobile={true}
|
||||
isOpen={true}
|
||||
onClose={onClose}
|
||||
showClose={true}
|
||||
>
|
||||
<div>Closable content</div>
|
||||
</FloatRightContainer>,
|
||||
)
|
||||
|
||||
const closeIcon = screen.getByTestId('close-icon')
|
||||
closeIcon.focus()
|
||||
await userEvent.keyboard('{Enter}')
|
||||
|
||||
expect(onClose).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('should call onClose when close is done using space key', async () => {
|
||||
const onClose = vi.fn()
|
||||
render(
|
||||
<FloatRightContainer
|
||||
isMobile={true}
|
||||
isOpen={true}
|
||||
onClose={onClose}
|
||||
showClose={true}
|
||||
>
|
||||
<div>Closable content</div>
|
||||
</FloatRightContainer>,
|
||||
)
|
||||
|
||||
const closeIcon = screen.getByTestId('close-icon')
|
||||
closeIcon.focus()
|
||||
await userEvent.keyboard(' ')
|
||||
|
||||
expect(onClose).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('should apply drawer className props in mobile drawer mode', async () => {
|
||||
it('should apply panel className in mobile drawer mode', async () => {
|
||||
render(
|
||||
<FloatRightContainer
|
||||
isMobile={true}
|
||||
isOpen={true}
|
||||
onClose={vi.fn()}
|
||||
dialogClassName="custom-dialog-class"
|
||||
panelClassName="custom-panel-class"
|
||||
>
|
||||
<div>Class forwarding content</div>
|
||||
@ -153,7 +111,6 @@ describe('FloatRightContainer', () => {
|
||||
)
|
||||
|
||||
const dialog = await screen.findByRole('dialog')
|
||||
expect(document.querySelector('.custom-dialog-class')).toBeInTheDocument()
|
||||
|
||||
const panel = document.querySelector('.custom-panel-class')
|
||||
expect(panel).toBeInTheDocument()
|
||||
|
||||
@ -49,7 +49,6 @@ const ContainerDemo = () => {
|
||||
isOpen={open}
|
||||
onClose={() => setOpen(false)}
|
||||
title="Responsive panel"
|
||||
description="Switch the toggle to see drawer vs inline behaviour."
|
||||
mask
|
||||
>
|
||||
<div className="rounded-xl border border-divider-subtle bg-components-panel-bg p-4 text-xs text-text-secondary">
|
||||
|
||||
@ -1,17 +1,79 @@
|
||||
'use client'
|
||||
import type { IDrawerProps } from '@/app/components/base/drawer'
|
||||
import Drawer from '@/app/components/base/drawer'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import {
|
||||
Drawer,
|
||||
DrawerBackdrop,
|
||||
DrawerCloseButton,
|
||||
DrawerContent,
|
||||
DrawerPopup,
|
||||
DrawerPortal,
|
||||
DrawerTitle,
|
||||
DrawerViewport,
|
||||
} from '@langgenius/dify-ui/drawer'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
type IFloatRightContainerProps = {
|
||||
isMobile: boolean
|
||||
isOpen: boolean
|
||||
onClose: () => void
|
||||
children?: React.ReactNode
|
||||
} & IDrawerProps
|
||||
showClose?: boolean
|
||||
panelClassName?: string
|
||||
title?: string
|
||||
mask?: boolean
|
||||
}
|
||||
|
||||
const FloatRightContainer = ({
|
||||
isMobile,
|
||||
children,
|
||||
isOpen,
|
||||
onClose,
|
||||
showClose = false,
|
||||
panelClassName,
|
||||
title,
|
||||
mask = true,
|
||||
}: IFloatRightContainerProps) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const FloatRightContainer = ({ isMobile, children, isOpen, ...drawerProps }: IFloatRightContainerProps) => {
|
||||
return (
|
||||
<>
|
||||
{isMobile && (
|
||||
<Drawer isOpen={isOpen} {...drawerProps}>{children}</Drawer>
|
||||
<Drawer
|
||||
open={isOpen}
|
||||
modal
|
||||
swipeDirection="right"
|
||||
onOpenChange={(open) => {
|
||||
if (!open)
|
||||
onClose()
|
||||
}}
|
||||
>
|
||||
<DrawerPortal>
|
||||
<DrawerBackdrop className={cn(!mask && 'bg-transparent')} />
|
||||
<DrawerViewport>
|
||||
<DrawerPopup className={cn('data-[swipe-direction=right]:w-full data-[swipe-direction=right]:max-w-sm', panelClassName)}>
|
||||
<DrawerContent className="flex min-h-0 flex-1 flex-col">
|
||||
{(title || showClose) && (
|
||||
<div className="mb-4 flex shrink-0 items-center justify-between">
|
||||
{title && (
|
||||
<DrawerTitle className="text-lg leading-6 font-medium text-text-primary">
|
||||
{title}
|
||||
</DrawerTitle>
|
||||
)}
|
||||
{showClose && (
|
||||
<DrawerCloseButton
|
||||
aria-label={t('operation.close', { ns: 'common' })}
|
||||
className="h-6 w-6 rounded-md"
|
||||
data-testid="close-icon"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{children}
|
||||
</DrawerContent>
|
||||
</DrawerPopup>
|
||||
</DrawerViewport>
|
||||
</DrawerPortal>
|
||||
</Drawer>
|
||||
)}
|
||||
{(!isMobile && isOpen) && (
|
||||
<>{children}</>
|
||||
|
||||
@ -54,7 +54,7 @@ export const PreviewPanel: FC<PreviewPanelProps> = ({
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<FloatRightContainer isMobile={isMobile} isOpen={true} onClose={noop} footer={null}>
|
||||
<FloatRightContainer isMobile={isMobile} isOpen={true} onClose={noop}>
|
||||
<PreviewContainer
|
||||
header={(
|
||||
<PreviewHeader title={t('stepTwo.preview', { ns: 'datasetCreation' })}>
|
||||
|
||||
@ -292,7 +292,7 @@ const DocumentDetail: FC<DocumentDetailProps> = ({ datasetId, documentId }) => {
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<FloatRightContainer showClose isOpen={showMetadata} onClose={() => setShowMetadata(false)} isMobile={isMobile} panelClassName="justify-start!" footer={null}>
|
||||
<FloatRightContainer showClose isOpen={showMetadata} onClose={() => setShowMetadata(false)} isMobile={isMobile} panelClassName="justify-start!">
|
||||
<Metadata
|
||||
className="mt-3 mr-2"
|
||||
datasetId={datasetId}
|
||||
|
||||
@ -10,12 +10,19 @@ import type {
|
||||
} from '@/models/datasets'
|
||||
import type { RetrievalConfig } from '@/types/app'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import {
|
||||
Drawer,
|
||||
DrawerBackdrop,
|
||||
DrawerContent,
|
||||
DrawerPopup,
|
||||
DrawerPortal,
|
||||
DrawerViewport,
|
||||
} from '@langgenius/dify-ui/drawer'
|
||||
import { useBoolean } from 'ahooks'
|
||||
import * as React from 'react'
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useContext } from 'use-context-selector'
|
||||
import Drawer from '@/app/components/base/drawer'
|
||||
import FloatRightContainer from '@/app/components/base/float-right-container'
|
||||
import Loading from '@/app/components/base/loading'
|
||||
import Pagination from '@/app/components/base/pagination'
|
||||
@ -158,7 +165,6 @@ const HitTestingPage: FC<Props> = ({ datasetId }: Props) => {
|
||||
isMobile={isMobile}
|
||||
isOpen={isShowRightPanel}
|
||||
onClose={hideRightPanel}
|
||||
footer={null}
|
||||
>
|
||||
<div className="flex min-w-0 flex-1 flex-col pt-3">
|
||||
{isRetrievalLoading
|
||||
@ -181,23 +187,33 @@ const HitTestingPage: FC<Props> = ({ datasetId }: Props) => {
|
||||
</div>
|
||||
</FloatRightContainer>
|
||||
<Drawer
|
||||
unmount={true}
|
||||
isOpen={isShowModifyRetrievalModal}
|
||||
onClose={() => setIsShowModifyRetrievalModal(false)}
|
||||
footer={null}
|
||||
mask={isMobile}
|
||||
panelClassName="mt-16 mx-2 sm:mr-2 mb-3 p-0! max-w-[640px]! rounded-xl"
|
||||
>
|
||||
<ModifyRetrievalModal
|
||||
indexMethod={currentDataset?.indexing_technique || ''}
|
||||
value={retrievalConfig}
|
||||
isShow={isShowModifyRetrievalModal}
|
||||
onHide={() => setIsShowModifyRetrievalModal(false)}
|
||||
onSave={(value) => {
|
||||
setRetrievalConfig(value)
|
||||
open={isShowModifyRetrievalModal}
|
||||
modal
|
||||
swipeDirection="right"
|
||||
onOpenChange={(open) => {
|
||||
if (!open)
|
||||
setIsShowModifyRetrievalModal(false)
|
||||
}}
|
||||
/>
|
||||
}}
|
||||
>
|
||||
<DrawerPortal>
|
||||
<DrawerBackdrop className={cn(!isMobile && 'bg-transparent')} />
|
||||
<DrawerViewport>
|
||||
<DrawerPopup className="p-0! data-[swipe-direction=right]:top-16 data-[swipe-direction=right]:right-2 data-[swipe-direction=right]:bottom-3 data-[swipe-direction=right]:h-auto data-[swipe-direction=right]:w-full data-[swipe-direction=right]:max-w-[640px] data-[swipe-direction=right]:rounded-xl">
|
||||
<DrawerContent className="flex min-h-0 flex-1 flex-col p-0 pb-0">
|
||||
<ModifyRetrievalModal
|
||||
indexMethod={currentDataset?.indexing_technique || ''}
|
||||
value={retrievalConfig}
|
||||
isShow={isShowModifyRetrievalModal}
|
||||
onHide={() => setIsShowModifyRetrievalModal(false)}
|
||||
onSave={(value) => {
|
||||
setRetrievalConfig(value)
|
||||
setIsShowModifyRetrievalModal(false)
|
||||
}}
|
||||
/>
|
||||
</DrawerContent>
|
||||
</DrawerPopup>
|
||||
</DrawerViewport>
|
||||
</DrawerPortal>
|
||||
</Drawer>
|
||||
</div>
|
||||
)
|
||||
|
||||
@ -4,12 +4,19 @@ import type { FormSchema } from '../../base/form/types'
|
||||
import type { PluginDetail } from '../types'
|
||||
import { Button } from '@langgenius/dify-ui/button'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import {
|
||||
Drawer,
|
||||
DrawerBackdrop,
|
||||
DrawerContent,
|
||||
DrawerPopup,
|
||||
DrawerPortal,
|
||||
DrawerViewport,
|
||||
} from '@langgenius/dify-ui/drawer'
|
||||
import { toast } from '@langgenius/dify-ui/toast'
|
||||
import { RiArrowRightUpLine, RiCloseLine } from '@remixicon/react'
|
||||
import * as React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import ActionButton from '@/app/components/base/action-button'
|
||||
import Drawer from '@/app/components/base/drawer'
|
||||
import Form from '@/app/components/header/account-setting/model-provider-page/model-modal/Form'
|
||||
import { useRenderI18nObject } from '@/hooks/use-i18n'
|
||||
import { ReadmeEntrance } from '../readme-panel/entrance'
|
||||
@ -75,60 +82,67 @@ const EndpointModal: FC<Props> = ({
|
||||
|
||||
return (
|
||||
<Drawer
|
||||
isOpen
|
||||
clickOutsideNotOpen={false}
|
||||
onClose={onCancel}
|
||||
footer={null}
|
||||
mask
|
||||
positionCenter={false}
|
||||
panelClassName={cn('mt-[64px] mr-2 mb-2 w-[420px]! max-w-[420px]! justify-start rounded-2xl border-[0.5px] border-components-panel-border bg-components-panel-bg! p-0! shadow-xl')}
|
||||
open
|
||||
modal
|
||||
swipeDirection="right"
|
||||
onOpenChange={(open) => {
|
||||
if (!open)
|
||||
onCancel()
|
||||
}}
|
||||
>
|
||||
<>
|
||||
<div className="p-4 pb-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="system-xl-semibold text-text-primary">{t('detailPanel.endpointModalTitle', { ns: 'plugin' })}</div>
|
||||
<ActionButton onClick={onCancel}>
|
||||
<RiCloseLine className="h-4 w-4" />
|
||||
</ActionButton>
|
||||
</div>
|
||||
<div className="mt-0.5 system-xs-regular text-text-tertiary">{t('detailPanel.endpointModalDesc', { ns: 'plugin' })}</div>
|
||||
<ReadmeEntrance pluginDetail={pluginDetail} className="px-0 pt-3" />
|
||||
</div>
|
||||
<div className="grow overflow-y-auto">
|
||||
<div className="px-4 py-2">
|
||||
<Form
|
||||
value={tempCredential}
|
||||
onChange={(v) => {
|
||||
setTempCredential(v)
|
||||
}}
|
||||
formSchemas={formSchemas as any}
|
||||
isEditMode={true}
|
||||
showOnVariableMap={{}}
|
||||
validating={false}
|
||||
inputClassName="bg-components-input-bg-normal hover:bg-components-input-bg-hover"
|
||||
fieldMoreInfo={item => item.url
|
||||
? (
|
||||
<a
|
||||
href={item.url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="inline-flex items-center body-xs-regular text-text-accent-secondary"
|
||||
>
|
||||
{t('howToGet', { ns: 'tools' })}
|
||||
<RiArrowRightUpLine className="ml-1 h-3 w-3" />
|
||||
</a>
|
||||
)
|
||||
: null}
|
||||
/>
|
||||
</div>
|
||||
<div className={cn('flex justify-end p-4 pt-0')}>
|
||||
<div className="flex gap-2">
|
||||
<Button onClick={onCancel}>{t('operation.cancel', { ns: 'common' })}</Button>
|
||||
<Button variant="primary" onClick={handleSave}>{t('operation.save', { ns: 'common' })}</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
<DrawerPortal>
|
||||
<DrawerBackdrop className="bg-black/30" />
|
||||
<DrawerViewport>
|
||||
<DrawerPopup className={cn('justify-start bg-components-panel-bg! p-0! shadow-xl data-[swipe-direction=right]:top-16 data-[swipe-direction=right]:right-2 data-[swipe-direction=right]:bottom-2 data-[swipe-direction=right]:h-auto data-[swipe-direction=right]:w-[420px] data-[swipe-direction=right]:max-w-[420px] data-[swipe-direction=right]:rounded-2xl data-[swipe-direction=right]:border-[0.5px] data-[swipe-direction=right]:border-components-panel-border')}>
|
||||
<DrawerContent className="flex min-h-0 flex-1 flex-col p-0 pb-0">
|
||||
<div className="p-4 pb-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="system-xl-semibold text-text-primary">{t('detailPanel.endpointModalTitle', { ns: 'plugin' })}</div>
|
||||
<ActionButton onClick={onCancel}>
|
||||
<RiCloseLine className="h-4 w-4" />
|
||||
</ActionButton>
|
||||
</div>
|
||||
<div className="mt-0.5 system-xs-regular text-text-tertiary">{t('detailPanel.endpointModalDesc', { ns: 'plugin' })}</div>
|
||||
<ReadmeEntrance pluginDetail={pluginDetail} className="px-0 pt-3" />
|
||||
</div>
|
||||
<div className="grow overflow-y-auto">
|
||||
<div className="px-4 py-2">
|
||||
<Form
|
||||
value={tempCredential}
|
||||
onChange={(v) => {
|
||||
setTempCredential(v)
|
||||
}}
|
||||
formSchemas={formSchemas as any}
|
||||
isEditMode={true}
|
||||
showOnVariableMap={{}}
|
||||
validating={false}
|
||||
inputClassName="bg-components-input-bg-normal hover:bg-components-input-bg-hover"
|
||||
fieldMoreInfo={item => item.url
|
||||
? (
|
||||
<a
|
||||
href={item.url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="inline-flex items-center body-xs-regular text-text-accent-secondary"
|
||||
>
|
||||
{t('howToGet', { ns: 'tools' })}
|
||||
<RiArrowRightUpLine className="ml-1 h-3 w-3" />
|
||||
</a>
|
||||
)
|
||||
: null}
|
||||
/>
|
||||
</div>
|
||||
<div className={cn('flex justify-end p-4 pt-0')}>
|
||||
<div className="flex gap-2">
|
||||
<Button onClick={onCancel}>{t('operation.cancel', { ns: 'common' })}</Button>
|
||||
<Button variant="primary" onClick={handleSave}>{t('operation.save', { ns: 'common' })}</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</DrawerContent>
|
||||
</DrawerPopup>
|
||||
</DrawerViewport>
|
||||
</DrawerPortal>
|
||||
</Drawer>
|
||||
)
|
||||
}
|
||||
|
||||
@ -2,8 +2,15 @@
|
||||
import type { FC } from 'react'
|
||||
import type { PluginDetail } from '@/app/components/plugins/types'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import {
|
||||
Drawer,
|
||||
DrawerBackdrop,
|
||||
DrawerContent,
|
||||
DrawerPopup,
|
||||
DrawerPortal,
|
||||
DrawerViewport,
|
||||
} from '@langgenius/dify-ui/drawer'
|
||||
import { useCallback, useEffect } from 'react'
|
||||
import Drawer from '@/app/components/base/drawer'
|
||||
import { PluginCategoryEnum } from '@/app/components/plugins/types'
|
||||
import { ReadmeEntrance } from '../readme-panel/entrance'
|
||||
import ActionList from './action-list'
|
||||
@ -53,37 +60,46 @@ const PluginDetailPanel: FC<Props> = ({
|
||||
|
||||
return (
|
||||
<Drawer
|
||||
isOpen={!!detail}
|
||||
clickOutsideNotOpen={false}
|
||||
onClose={onHide}
|
||||
footer={null}
|
||||
mask={false}
|
||||
positionCenter={false}
|
||||
panelClassName={cn('mt-[64px] mr-2 mb-2 w-[420px]! max-w-[420px]! justify-start rounded-2xl border-[0.5px] border-components-panel-border bg-components-panel-bg! p-0! shadow-xl')}
|
||||
open={!!detail}
|
||||
modal
|
||||
swipeDirection="right"
|
||||
onOpenChange={(open) => {
|
||||
if (!open)
|
||||
onHide()
|
||||
}}
|
||||
>
|
||||
{detail && (
|
||||
<>
|
||||
<DetailHeader detail={detail} onUpdate={handleUpdate} onHide={onHide} />
|
||||
<div className="grow overflow-y-auto">
|
||||
<div className="flex min-h-full flex-col">
|
||||
<div className="flex-1">
|
||||
{detail.declaration.category === PluginCategoryEnum.trigger && (
|
||||
<>
|
||||
<SubscriptionList pluginDetail={detail} />
|
||||
<TriggerEventsList />
|
||||
</>
|
||||
)}
|
||||
{!!detail.declaration.tool && <ActionList detail={detail} />}
|
||||
{!!detail.declaration.agent_strategy && <AgentStrategyList detail={detail} />}
|
||||
{!!detail.declaration.endpoint && <EndpointList detail={detail} />}
|
||||
{!!detail.declaration.model && <ModelList detail={detail} />}
|
||||
{!!detail.declaration.datasource && <DatasourceActionList detail={detail} />}
|
||||
</div>
|
||||
<ReadmeEntrance pluginDetail={detail} className="mt-auto" />
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
<DrawerPortal>
|
||||
<DrawerBackdrop className="bg-transparent" />
|
||||
<DrawerViewport>
|
||||
<DrawerPopup className={cn('justify-start bg-components-panel-bg! p-0! shadow-xl data-[swipe-direction=right]:top-16 data-[swipe-direction=right]:right-2 data-[swipe-direction=right]:bottom-2 data-[swipe-direction=right]:h-auto data-[swipe-direction=right]:w-[420px] data-[swipe-direction=right]:max-w-[420px] data-[swipe-direction=right]:rounded-2xl data-[swipe-direction=right]:border-[0.5px] data-[swipe-direction=right]:border-components-panel-border')}>
|
||||
<DrawerContent className="flex min-h-0 flex-1 flex-col p-0 pb-0">
|
||||
{detail && (
|
||||
<>
|
||||
<DetailHeader detail={detail} onUpdate={handleUpdate} onHide={onHide} />
|
||||
<div className="grow overflow-y-auto">
|
||||
<div className="flex min-h-full flex-col">
|
||||
<div className="flex-1">
|
||||
{detail.declaration.category === PluginCategoryEnum.trigger && (
|
||||
<>
|
||||
<SubscriptionList pluginDetail={detail} />
|
||||
<TriggerEventsList />
|
||||
</>
|
||||
)}
|
||||
{!!detail.declaration.tool && <ActionList detail={detail} />}
|
||||
{!!detail.declaration.agent_strategy && <AgentStrategyList detail={detail} />}
|
||||
{!!detail.declaration.endpoint && <EndpointList detail={detail} />}
|
||||
{!!detail.declaration.model && <ModelList detail={detail} />}
|
||||
{!!detail.declaration.datasource && <DatasourceActionList detail={detail} />}
|
||||
</div>
|
||||
<ReadmeEntrance pluginDetail={detail} className="mt-auto" />
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</DrawerContent>
|
||||
</DrawerPopup>
|
||||
</DrawerViewport>
|
||||
</DrawerPortal>
|
||||
</Drawer>
|
||||
)
|
||||
}
|
||||
|
||||
@ -5,6 +5,14 @@ import type {
|
||||
} from '@/app/components/plugins/types'
|
||||
import type { Locale } from '@/i18n-config'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import {
|
||||
Drawer,
|
||||
DrawerBackdrop,
|
||||
DrawerContent,
|
||||
DrawerPopup,
|
||||
DrawerPortal,
|
||||
DrawerViewport,
|
||||
} from '@langgenius/dify-ui/drawer'
|
||||
import {
|
||||
RiArrowLeftLine,
|
||||
RiCloseLine,
|
||||
@ -14,7 +22,6 @@ import { useMemo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import ActionButton from '@/app/components/base/action-button'
|
||||
import Divider from '@/app/components/base/divider'
|
||||
import Drawer from '@/app/components/base/drawer'
|
||||
import Icon from '@/app/components/plugins/card/base/card-icon'
|
||||
import Description from '@/app/components/plugins/card/base/description'
|
||||
import { API_PREFIX } from '@/config'
|
||||
@ -75,92 +82,99 @@ const StrategyDetail: FC<Props> = ({
|
||||
|
||||
return (
|
||||
<Drawer
|
||||
isOpen
|
||||
clickOutsideNotOpen={false}
|
||||
onClose={onHide}
|
||||
footer={null}
|
||||
mask={false}
|
||||
positionCenter={false}
|
||||
panelClassName={cn('mt-[64px] mr-2 mb-2 w-[420px]! max-w-[420px]! justify-start rounded-2xl border-[0.5px] border-components-panel-border bg-components-panel-bg! p-0! shadow-xl')}
|
||||
open
|
||||
modal
|
||||
swipeDirection="right"
|
||||
onOpenChange={(open) => {
|
||||
if (!open)
|
||||
onHide()
|
||||
}}
|
||||
>
|
||||
<>
|
||||
{/* header */}
|
||||
<div className="relative border-b border-divider-subtle p-4 pb-3">
|
||||
<div className="absolute top-3 right-3">
|
||||
<ActionButton onClick={onHide}>
|
||||
<RiCloseLine className="h-4 w-4" />
|
||||
</ActionButton>
|
||||
</div>
|
||||
<div
|
||||
className="mb-2 flex cursor-pointer items-center gap-1 system-xs-semibold-uppercase text-text-accent-secondary"
|
||||
onClick={onHide}
|
||||
>
|
||||
<RiArrowLeftLine className="h-4 w-4" />
|
||||
BACK
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<Icon size="tiny" className="h-6 w-6" src={`${API_PREFIX}/workspaces/current/plugin/icon?tenant_id=${provider.tenant_id}&filename=${provider.icon}`} />
|
||||
<div className="">{getValueFromI18nObject(provider.label)}</div>
|
||||
</div>
|
||||
<div className="mt-1 system-md-semibold text-text-primary">{getValueFromI18nObject(detail.identity.label)}</div>
|
||||
<Description className="mt-3" text={getValueFromI18nObject(detail.description)} descriptionLineRows={2}></Description>
|
||||
</div>
|
||||
{/* form */}
|
||||
<div className="h-full">
|
||||
<div className="flex h-full flex-col overflow-y-auto">
|
||||
<div className="p-4 pb-1 system-sm-semibold-uppercase text-text-primary">{t('setBuiltInTools.parameters', { ns: 'tools' })}</div>
|
||||
<div className="px-4">
|
||||
{detail.parameters.length > 0 && (
|
||||
<div className="space-y-1 py-2">
|
||||
{detail.parameters.map((item: any, index) => (
|
||||
<div key={index} className="py-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="code-sm-semibold text-text-secondary">{getValueFromI18nObject(item.label)}</div>
|
||||
<div className="system-xs-regular text-text-tertiary">
|
||||
{getType(item.type)}
|
||||
</div>
|
||||
{item.required && (
|
||||
<div className="system-xs-medium text-text-warning-secondary">{t('setBuiltInTools.required', { ns: 'tools' })}</div>
|
||||
)}
|
||||
<DrawerPortal>
|
||||
<DrawerBackdrop className="bg-transparent" />
|
||||
<DrawerViewport>
|
||||
<DrawerPopup className={cn('justify-start bg-components-panel-bg! p-0! shadow-xl data-[swipe-direction=right]:top-16 data-[swipe-direction=right]:right-2 data-[swipe-direction=right]:bottom-2 data-[swipe-direction=right]:h-auto data-[swipe-direction=right]:w-[420px] data-[swipe-direction=right]:max-w-[420px] data-[swipe-direction=right]:rounded-2xl data-[swipe-direction=right]:border-[0.5px] data-[swipe-direction=right]:border-components-panel-border')}>
|
||||
<DrawerContent className="flex min-h-0 flex-1 flex-col p-0 pb-0">
|
||||
{/* header */}
|
||||
<div className="relative border-b border-divider-subtle p-4 pb-3">
|
||||
<div className="absolute top-3 right-3">
|
||||
<ActionButton onClick={onHide}>
|
||||
<RiCloseLine className="h-4 w-4" />
|
||||
</ActionButton>
|
||||
</div>
|
||||
<div
|
||||
className="mb-2 flex cursor-pointer items-center gap-1 system-xs-semibold-uppercase text-text-accent-secondary"
|
||||
onClick={onHide}
|
||||
>
|
||||
<RiArrowLeftLine className="h-4 w-4" />
|
||||
BACK
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<Icon size="tiny" className="h-6 w-6" src={`${API_PREFIX}/workspaces/current/plugin/icon?tenant_id=${provider.tenant_id}&filename=${provider.icon}`} />
|
||||
<div className="">{getValueFromI18nObject(provider.label)}</div>
|
||||
</div>
|
||||
<div className="mt-1 system-md-semibold text-text-primary">{getValueFromI18nObject(detail.identity.label)}</div>
|
||||
<Description className="mt-3" text={getValueFromI18nObject(detail.description)} descriptionLineRows={2}></Description>
|
||||
</div>
|
||||
{/* form */}
|
||||
<div className="h-full">
|
||||
<div className="flex h-full flex-col overflow-y-auto">
|
||||
<div className="p-4 pb-1 system-sm-semibold-uppercase text-text-primary">{t('setBuiltInTools.parameters', { ns: 'tools' })}</div>
|
||||
<div className="px-4">
|
||||
{detail.parameters.length > 0 && (
|
||||
<div className="space-y-1 py-2">
|
||||
{detail.parameters.map((item: any, index) => (
|
||||
<div key={index} className="py-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="code-sm-semibold text-text-secondary">{getValueFromI18nObject(item.label)}</div>
|
||||
<div className="system-xs-regular text-text-tertiary">
|
||||
{getType(item.type)}
|
||||
</div>
|
||||
{item.required && (
|
||||
<div className="system-xs-medium text-text-warning-secondary">{t('setBuiltInTools.required', { ns: 'tools' })}</div>
|
||||
)}
|
||||
</div>
|
||||
{item.human_description && (
|
||||
<div className="mt-0.5 system-xs-regular text-text-tertiary">
|
||||
{getValueFromI18nObject(item.human_description)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
{item.human_description && (
|
||||
<div className="mt-0.5 system-xs-regular text-text-tertiary">
|
||||
{getValueFromI18nObject(item.human_description)}
|
||||
)}
|
||||
</div>
|
||||
{detail.output_schema && (
|
||||
<>
|
||||
<div className="px-4">
|
||||
<Divider className="mt-2!" />
|
||||
</div>
|
||||
<div className="p-4 pb-1 system-sm-semibold-uppercase text-text-primary">OUTPUT</div>
|
||||
{outputSchema.length > 0 && (
|
||||
<div className="space-y-1 px-4 py-2">
|
||||
{outputSchema.map((outputItem, index) => (
|
||||
<div key={index} className="py-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="code-sm-semibold text-text-secondary">{outputItem.name}</div>
|
||||
<div className="system-xs-regular text-text-tertiary">{outputItem.type}</div>
|
||||
</div>
|
||||
{outputItem.description && (
|
||||
<div className="mt-0.5 system-xs-regular text-text-tertiary">
|
||||
{outputItem.description}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{detail.output_schema && (
|
||||
<>
|
||||
<div className="px-4">
|
||||
<Divider className="mt-2!" />
|
||||
</div>
|
||||
<div className="p-4 pb-1 system-sm-semibold-uppercase text-text-primary">OUTPUT</div>
|
||||
{outputSchema.length > 0 && (
|
||||
<div className="space-y-1 px-4 py-2">
|
||||
{outputSchema.map((outputItem, index) => (
|
||||
<div key={index} className="py-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="code-sm-semibold text-text-secondary">{outputItem.name}</div>
|
||||
<div className="system-xs-regular text-text-tertiary">{outputItem.type}</div>
|
||||
</div>
|
||||
{outputItem.description && (
|
||||
<div className="mt-0.5 system-xs-regular text-text-tertiary">
|
||||
{outputItem.description}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
</div>
|
||||
</DrawerContent>
|
||||
</DrawerPopup>
|
||||
</DrawerViewport>
|
||||
</DrawerPortal>
|
||||
</Drawer>
|
||||
)
|
||||
}
|
||||
|
||||
@ -4,6 +4,14 @@ import type { FC } from 'react'
|
||||
import type { TriggerEvent } from '@/app/components/plugins/types'
|
||||
import type { TriggerProviderApiEntity } from '@/app/components/workflow/block-selector/types'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import {
|
||||
Drawer,
|
||||
DrawerBackdrop,
|
||||
DrawerContent,
|
||||
DrawerPopup,
|
||||
DrawerPortal,
|
||||
DrawerViewport,
|
||||
} from '@langgenius/dify-ui/drawer'
|
||||
import {
|
||||
RiArrowLeftLine,
|
||||
RiCloseLine,
|
||||
@ -11,7 +19,6 @@ import {
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import ActionButton from '@/app/components/base/action-button'
|
||||
import Divider from '@/app/components/base/divider'
|
||||
import Drawer from '@/app/components/base/drawer'
|
||||
import { useLanguage } from '@/app/components/header/account-setting/model-provider-page/hooks'
|
||||
import Icon from '@/app/components/plugins/card/base/card-icon'
|
||||
import Description from '@/app/components/plugins/card/base/description'
|
||||
@ -82,78 +89,87 @@ export const EventDetailDrawer: FC<EventDetailDrawerProps> = (props) => {
|
||||
|
||||
return (
|
||||
<Drawer
|
||||
isOpen
|
||||
clickOutsideNotOpen={false}
|
||||
onClose={onClose}
|
||||
footer={null}
|
||||
mask={false}
|
||||
positionCenter={false}
|
||||
panelClassName={cn('mt-[64px] mr-2 mb-2 w-[420px]! max-w-[420px]! justify-start rounded-2xl border-[0.5px] border-components-panel-border bg-components-panel-bg! p-0! shadow-xl')}
|
||||
open
|
||||
modal
|
||||
swipeDirection="right"
|
||||
onOpenChange={(open) => {
|
||||
if (!open)
|
||||
onClose()
|
||||
}}
|
||||
>
|
||||
<div className="relative border-b border-divider-subtle p-4 pb-3">
|
||||
<div className="absolute top-3 right-3">
|
||||
<ActionButton onClick={onClose}>
|
||||
<RiCloseLine className="h-4 w-4" />
|
||||
</ActionButton>
|
||||
</div>
|
||||
<div
|
||||
className="mb-2 flex cursor-pointer items-center gap-1 system-xs-semibold-uppercase text-text-accent-secondary"
|
||||
onClick={onClose}
|
||||
>
|
||||
<RiArrowLeftLine className="h-4 w-4" />
|
||||
{t('detailPanel.operation.back', { ns: 'plugin' })}
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<Icon size="tiny" className="h-6 w-6" src={providerInfo.icon!} />
|
||||
<OrgInfo
|
||||
packageNameClassName="w-auto"
|
||||
orgName={providerInfo.author}
|
||||
packageName={providerInfo.name.split('/').pop() || ''}
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-1 system-md-semibold text-text-primary">{eventInfo?.identity?.label[language]}</div>
|
||||
<Description className="mt-3 mb-2 h-auto" text={eventInfo.description[language]!} descriptionLineRows={2}></Description>
|
||||
</div>
|
||||
<div className="flex h-full flex-col gap-2 overflow-y-auto px-4 pt-4 pb-2">
|
||||
<div className="system-sm-semibold-uppercase text-text-secondary">{t('setBuiltInTools.parameters', { ns: 'tools' })}</div>
|
||||
{parametersSchemas.length > 0
|
||||
? (
|
||||
parametersSchemas.map((item, index) => (
|
||||
<div key={index} className="py-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="code-sm-semibold text-text-secondary">{item.label[language]}</div>
|
||||
<div className="system-xs-regular text-text-tertiary">
|
||||
{getType(item.type, t)}
|
||||
</div>
|
||||
{item.required && (
|
||||
<div className="system-xs-medium text-text-warning-secondary">{t('setBuiltInTools.required', { ns: 'tools' })}</div>
|
||||
)}
|
||||
</div>
|
||||
{item.description && (
|
||||
<div className="mt-0.5 system-xs-regular text-text-tertiary">
|
||||
{item.description?.[language]}
|
||||
</div>
|
||||
)}
|
||||
<DrawerPortal>
|
||||
<DrawerBackdrop className="bg-transparent" />
|
||||
<DrawerViewport>
|
||||
<DrawerPopup className={cn('justify-start bg-components-panel-bg! p-0! shadow-xl data-[swipe-direction=right]:top-16 data-[swipe-direction=right]:right-2 data-[swipe-direction=right]:bottom-2 data-[swipe-direction=right]:h-auto data-[swipe-direction=right]:w-[420px] data-[swipe-direction=right]:max-w-[420px] data-[swipe-direction=right]:rounded-2xl data-[swipe-direction=right]:border-[0.5px] data-[swipe-direction=right]:border-components-panel-border')}>
|
||||
<DrawerContent className="flex min-h-0 flex-1 flex-col p-0 pb-0">
|
||||
<div className="relative border-b border-divider-subtle p-4 pb-3">
|
||||
<div className="absolute top-3 right-3">
|
||||
<ActionButton onClick={onClose}>
|
||||
<RiCloseLine className="h-4 w-4" />
|
||||
</ActionButton>
|
||||
</div>
|
||||
))
|
||||
)
|
||||
: <div className="system-xs-regular text-text-tertiary">{t('events.item.noParameters', { ns: 'pluginTrigger' })}</div>}
|
||||
<Divider className="mt-1 mb-2 h-px" />
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="system-sm-semibold-uppercase text-text-secondary">{t('events.output', { ns: 'pluginTrigger' })}</div>
|
||||
<div className="relative left-[-7px]">
|
||||
{outputFields.map(item => (
|
||||
<Field
|
||||
key={item.name}
|
||||
name={item.name}
|
||||
payload={item.field}
|
||||
required={item.required}
|
||||
rootClassName="code-sm-semibold text-text-secondary"
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="mb-2 flex cursor-pointer items-center gap-1 system-xs-semibold-uppercase text-text-accent-secondary"
|
||||
onClick={onClose}
|
||||
>
|
||||
<RiArrowLeftLine className="h-4 w-4" />
|
||||
{t('detailPanel.operation.back', { ns: 'plugin' })}
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<Icon size="tiny" className="h-6 w-6" src={providerInfo.icon!} />
|
||||
<OrgInfo
|
||||
packageNameClassName="w-auto"
|
||||
orgName={providerInfo.author}
|
||||
packageName={providerInfo.name.split('/').pop() || ''}
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-1 system-md-semibold text-text-primary">{eventInfo?.identity?.label[language]}</div>
|
||||
<Description className="mt-3 mb-2 h-auto" text={eventInfo.description[language]!} descriptionLineRows={2}></Description>
|
||||
</div>
|
||||
<div className="flex h-full flex-col gap-2 overflow-y-auto px-4 pt-4 pb-2">
|
||||
<div className="system-sm-semibold-uppercase text-text-secondary">{t('setBuiltInTools.parameters', { ns: 'tools' })}</div>
|
||||
{parametersSchemas.length > 0
|
||||
? (
|
||||
parametersSchemas.map((item, index) => (
|
||||
<div key={index} className="py-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="code-sm-semibold text-text-secondary">{item.label[language]}</div>
|
||||
<div className="system-xs-regular text-text-tertiary">
|
||||
{getType(item.type, t)}
|
||||
</div>
|
||||
{item.required && (
|
||||
<div className="system-xs-medium text-text-warning-secondary">{t('setBuiltInTools.required', { ns: 'tools' })}</div>
|
||||
)}
|
||||
</div>
|
||||
{item.description && (
|
||||
<div className="mt-0.5 system-xs-regular text-text-tertiary">
|
||||
{item.description?.[language]}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))
|
||||
)
|
||||
: <div className="system-xs-regular text-text-tertiary">{t('events.item.noParameters', { ns: 'pluginTrigger' })}</div>}
|
||||
<Divider className="mt-1 mb-2 h-px" />
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="system-sm-semibold-uppercase text-text-secondary">{t('events.output', { ns: 'pluginTrigger' })}</div>
|
||||
<div className="relative left-[-7px]">
|
||||
{outputFields.map(item => (
|
||||
<Field
|
||||
key={item.name}
|
||||
name={item.name}
|
||||
payload={item.field}
|
||||
required={item.required}
|
||||
rootClassName="code-sm-semibold text-text-secondary"
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</DrawerContent>
|
||||
</DrawerPopup>
|
||||
</DrawerViewport>
|
||||
</DrawerPortal>
|
||||
</Drawer>
|
||||
)
|
||||
}
|
||||
|
||||
@ -6,15 +6,6 @@ import * as React from 'react'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import MCPDetailPanel from '../provider-detail'
|
||||
|
||||
// Mock the drawer component
|
||||
vi.mock('@/app/components/base/drawer', () => ({
|
||||
default: ({ children, isOpen }: { children: ReactNode, isOpen: boolean }) => {
|
||||
if (!isOpen)
|
||||
return null
|
||||
return <div data-testid="drawer">{children}</div>
|
||||
},
|
||||
}))
|
||||
|
||||
// Mock the content component to expose onUpdate callback
|
||||
vi.mock('../content', () => ({
|
||||
default: ({ detail, onUpdate }: { detail: ToolWithProvider, onUpdate: (isDelete?: boolean) => void }) => (
|
||||
@ -71,7 +62,7 @@ describe('MCPDetailPanel', () => {
|
||||
<MCPDetailPanel {...defaultProps} detail={detail} />,
|
||||
{ wrapper: createWrapper() },
|
||||
)
|
||||
expect(screen.getByTestId('drawer')).toBeInTheDocument()
|
||||
expect(screen.getByRole('dialog')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render content when detail is provided', () => {
|
||||
|
||||
@ -2,8 +2,15 @@
|
||||
import type { FC } from 'react'
|
||||
import type { ToolWithProvider } from '../../../workflow/types'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import {
|
||||
Drawer,
|
||||
DrawerBackdrop,
|
||||
DrawerContent,
|
||||
DrawerPopup,
|
||||
DrawerPortal,
|
||||
DrawerViewport,
|
||||
} from '@langgenius/dify-ui/drawer'
|
||||
import * as React from 'react'
|
||||
import Drawer from '@/app/components/base/drawer'
|
||||
import MCPDetailContent from './content'
|
||||
|
||||
type Props = {
|
||||
@ -32,23 +39,32 @@ const MCPDetailPanel: FC<Props> = ({
|
||||
|
||||
return (
|
||||
<Drawer
|
||||
isOpen={!!detail}
|
||||
clickOutsideNotOpen={false}
|
||||
onClose={onHide}
|
||||
footer={null}
|
||||
mask={false}
|
||||
positionCenter={false}
|
||||
panelClassName={cn('mt-[64px] mr-2 mb-2 w-[420px]! max-w-[420px]! justify-start rounded-2xl border-[0.5px] border-components-panel-border bg-components-panel-bg! p-0! shadow-xl')}
|
||||
open={!!detail}
|
||||
modal
|
||||
swipeDirection="right"
|
||||
onOpenChange={(open) => {
|
||||
if (!open)
|
||||
onHide()
|
||||
}}
|
||||
>
|
||||
{detail && (
|
||||
<MCPDetailContent
|
||||
detail={detail}
|
||||
onHide={onHide}
|
||||
onUpdate={handleUpdate}
|
||||
isTriggerAuthorize={isTriggerAuthorize}
|
||||
onFirstCreate={onFirstCreate}
|
||||
/>
|
||||
)}
|
||||
<DrawerPortal>
|
||||
<DrawerBackdrop className="bg-transparent" />
|
||||
<DrawerViewport>
|
||||
<DrawerPopup className={cn('justify-start bg-components-panel-bg! p-0! shadow-xl data-[swipe-direction=right]:top-16 data-[swipe-direction=right]:right-2 data-[swipe-direction=right]:bottom-2 data-[swipe-direction=right]:h-auto data-[swipe-direction=right]:w-[420px] data-[swipe-direction=right]:max-w-[420px] data-[swipe-direction=right]:rounded-2xl data-[swipe-direction=right]:border-[0.5px] data-[swipe-direction=right]:border-components-panel-border')}>
|
||||
<DrawerContent className="flex min-h-0 flex-1 flex-col p-0 pb-0">
|
||||
{detail && (
|
||||
<MCPDetailContent
|
||||
detail={detail}
|
||||
onHide={onHide}
|
||||
onUpdate={handleUpdate}
|
||||
isTriggerAuthorize={isTriggerAuthorize}
|
||||
onFirstCreate={onFirstCreate}
|
||||
/>
|
||||
)}
|
||||
</DrawerContent>
|
||||
</DrawerPopup>
|
||||
</DrawerViewport>
|
||||
</DrawerPortal>
|
||||
</Drawer>
|
||||
)
|
||||
}
|
||||
|
||||
@ -74,11 +74,6 @@ vi.mock('@/utils/var', () => ({
|
||||
basePath: '',
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/base/drawer', () => ({
|
||||
default: ({ children, isOpen }: { children: React.ReactNode, isOpen: boolean }) =>
|
||||
isOpen ? <div data-testid="drawer">{children}</div> : null,
|
||||
}))
|
||||
|
||||
const mockToastSuccess = vi.hoisted(() => vi.fn())
|
||||
const mockToastError = vi.hoisted(() => vi.fn())
|
||||
vi.mock('@langgenius/dify-ui/toast', () => ({
|
||||
|
||||
@ -12,6 +12,14 @@ import {
|
||||
} from '@langgenius/dify-ui/alert-dialog'
|
||||
import { Button } from '@langgenius/dify-ui/button'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import {
|
||||
Drawer,
|
||||
DrawerBackdrop,
|
||||
DrawerContent,
|
||||
DrawerPopup,
|
||||
DrawerPortal,
|
||||
DrawerViewport,
|
||||
} from '@langgenius/dify-ui/drawer'
|
||||
import { toast } from '@langgenius/dify-ui/toast'
|
||||
import {
|
||||
RiCloseLine,
|
||||
@ -20,7 +28,6 @@ import * as React from 'react'
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import ActionButton from '@/app/components/base/action-button'
|
||||
import Drawer from '@/app/components/base/drawer'
|
||||
import { LinkExternal02, Settings01 } from '@/app/components/base/icons/src/vender/line/general'
|
||||
import Loading from '@/app/components/base/loading'
|
||||
import { ConfigurationMethodEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
|
||||
@ -230,204 +237,213 @@ const ProviderDetail = ({
|
||||
|
||||
return (
|
||||
<Drawer
|
||||
isOpen={!!collection}
|
||||
clickOutsideNotOpen={false}
|
||||
onClose={onHide}
|
||||
footer={null}
|
||||
mask={false}
|
||||
positionCenter={false}
|
||||
panelClassName={cn('mt-[64px] mr-2 mb-2 w-[420px]! max-w-[420px]! justify-start rounded-2xl border-[0.5px] border-components-panel-border bg-components-panel-bg! p-0! shadow-xl')}
|
||||
open={!!collection}
|
||||
modal
|
||||
swipeDirection="right"
|
||||
onOpenChange={(open) => {
|
||||
if (!open)
|
||||
onHide()
|
||||
}}
|
||||
>
|
||||
<div className="flex h-full flex-col p-4">
|
||||
<div className="shrink-0">
|
||||
<div className="mb-3 flex">
|
||||
<Icon src={collection.icon} />
|
||||
<div className="ml-3 w-0 grow">
|
||||
<div className="flex h-5 items-center">
|
||||
<Title title={collection.label[language]!} />
|
||||
</div>
|
||||
<div className="mt-0.5 mb-1 flex h-4 items-center justify-between">
|
||||
<OrgInfo
|
||||
packageNameClassName="w-auto"
|
||||
orgName={collection.author}
|
||||
packageName={collection.name}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-1">
|
||||
<ActionButton onClick={onHide}>
|
||||
<RiCloseLine className="h-4 w-4" />
|
||||
</ActionButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{!!collection.description[language] && (
|
||||
<Description text={collection.description[language]} descriptionLineRows={2}></Description>
|
||||
)}
|
||||
<div className="flex gap-1 border-b-[0.5px] border-divider-subtle">
|
||||
{collection.type === CollectionType.custom && !isDetailLoading && (
|
||||
<Button
|
||||
className={cn('my-3 w-full shrink-0')}
|
||||
onClick={() => setIsShowEditCustomCollectionModal(true)}
|
||||
>
|
||||
<Settings01 className="mr-1 h-4 w-4 text-text-tertiary" />
|
||||
<div className="system-sm-medium text-text-secondary">{t('createTool.editAction', { ns: 'tools' })}</div>
|
||||
</Button>
|
||||
)}
|
||||
{collection.type === CollectionType.workflow && !isDetailLoading && customCollection && (
|
||||
<>
|
||||
<Button
|
||||
variant="primary"
|
||||
className={cn('my-3 w-[183px] shrink-0')}
|
||||
>
|
||||
<a className="flex items-center" href={`${basePath}/app/${(customCollection as WorkflowToolProviderResponse).workflow_app_id}/workflow`} rel="noreferrer" target="_blank">
|
||||
<div className="system-sm-medium">{t('openInStudio', { ns: 'tools' })}</div>
|
||||
<LinkExternal02 className="ml-1 h-4 w-4" />
|
||||
</a>
|
||||
</Button>
|
||||
<Button
|
||||
className={cn('my-3 w-[183px] shrink-0')}
|
||||
onClick={() => setWorkflowToolDrawerOpen(true)}
|
||||
disabled={!isCurrentWorkspaceManager}
|
||||
>
|
||||
<div className="system-sm-medium text-text-secondary">{t('createTool.editAction', { ns: 'tools' })}</div>
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex min-h-0 flex-1 flex-col pt-3">
|
||||
{isDetailLoading && <div className="flex h-[200px]"><Loading type="app" /></div>}
|
||||
{!isDetailLoading && (
|
||||
<>
|
||||
<div className="shrink-0">
|
||||
{(collection.type === CollectionType.builtIn || collection.type === CollectionType.model) && isAuthed && (
|
||||
<div className="mb-1 flex h-6 items-center justify-between system-sm-semibold-uppercase text-text-secondary">
|
||||
{t('detailPanel.actionNum', { ns: 'plugin', num: toolList.length, action: toolList.length > 1 ? 'actions' : 'action' })}
|
||||
{needAuth && (
|
||||
<DrawerPortal>
|
||||
<DrawerBackdrop className="bg-transparent" />
|
||||
<DrawerViewport>
|
||||
<DrawerPopup className={cn('justify-start bg-components-panel-bg! p-0! shadow-xl data-[swipe-direction=right]:top-16 data-[swipe-direction=right]:right-2 data-[swipe-direction=right]:bottom-2 data-[swipe-direction=right]:h-auto data-[swipe-direction=right]:w-[420px] data-[swipe-direction=right]:max-w-[420px] data-[swipe-direction=right]:rounded-2xl data-[swipe-direction=right]:border-[0.5px] data-[swipe-direction=right]:border-components-panel-border')}>
|
||||
<DrawerContent className="flex min-h-0 flex-1 flex-col p-0 pb-0">
|
||||
<div className="flex h-full flex-col p-4">
|
||||
<div className="shrink-0">
|
||||
<div className="mb-3 flex">
|
||||
<Icon src={collection.icon} />
|
||||
<div className="ml-3 w-0 grow">
|
||||
<div className="flex h-5 items-center">
|
||||
<Title title={collection.label[language]!} />
|
||||
</div>
|
||||
<div className="mt-0.5 mb-1 flex h-4 items-center justify-between">
|
||||
<OrgInfo
|
||||
packageNameClassName="w-auto"
|
||||
orgName={collection.author}
|
||||
packageName={collection.name}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-1">
|
||||
<ActionButton aria-label={t('operation.close', { ns: 'common' })} onClick={onHide}>
|
||||
<RiCloseLine className="h-4 w-4" />
|
||||
</ActionButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{!!collection.description[language] && (
|
||||
<Description text={collection.description[language]} descriptionLineRows={2}></Description>
|
||||
)}
|
||||
<div className="flex gap-1 border-b-[0.5px] border-divider-subtle">
|
||||
{collection.type === CollectionType.custom && !isDetailLoading && (
|
||||
<Button
|
||||
className={cn('my-3 w-full shrink-0')}
|
||||
onClick={() => setIsShowEditCustomCollectionModal(true)}
|
||||
>
|
||||
<Settings01 className="mr-1 h-4 w-4 text-text-tertiary" />
|
||||
<div className="system-sm-medium text-text-secondary">{t('createTool.editAction', { ns: 'tools' })}</div>
|
||||
</Button>
|
||||
)}
|
||||
{collection.type === CollectionType.workflow && !isDetailLoading && customCollection && (
|
||||
<>
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="small"
|
||||
onClick={() => {
|
||||
if (collection.type === CollectionType.builtIn || collection.type === CollectionType.model)
|
||||
showSettingAuthModal()
|
||||
}}
|
||||
variant="primary"
|
||||
className={cn('my-3 w-[183px] shrink-0')}
|
||||
>
|
||||
<a className="flex items-center" href={`${basePath}/app/${(customCollection as WorkflowToolProviderResponse).workflow_app_id}/workflow`} rel="noreferrer" target="_blank">
|
||||
<div className="system-sm-medium">{t('openInStudio', { ns: 'tools' })}</div>
|
||||
<LinkExternal02 className="ml-1 h-4 w-4" />
|
||||
</a>
|
||||
</Button>
|
||||
<Button
|
||||
className={cn('my-3 w-[183px] shrink-0')}
|
||||
onClick={() => setWorkflowToolDrawerOpen(true)}
|
||||
disabled={!isCurrentWorkspaceManager}
|
||||
>
|
||||
<Indicator className="mr-2" color="green" />
|
||||
{t('auth.authorized', { ns: 'tools' })}
|
||||
<div className="system-sm-medium text-text-secondary">{t('createTool.editAction', { ns: 'tools' })}</div>
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{(collection.type === CollectionType.builtIn || collection.type === CollectionType.model) && needAuth && !isAuthed && (
|
||||
<>
|
||||
<div className="system-sm-semibold-uppercase text-text-secondary">
|
||||
<span className="">{t('includeToolNum', { ns: 'tools', num: toolList.length, action: toolList.length > 1 ? 'actions' : 'action' }).toLocaleUpperCase()}</span>
|
||||
<span className="px-1">·</span>
|
||||
<span className="text-util-colors-orange-orange-600">{t('auth.setup', { ns: 'tools' }).toLocaleUpperCase()}</span>
|
||||
</div>
|
||||
<Button
|
||||
variant="primary"
|
||||
className={cn('my-3 w-full shrink-0')}
|
||||
onClick={() => {
|
||||
if (collection.type === CollectionType.builtIn || collection.type === CollectionType.model)
|
||||
showSettingAuthModal()
|
||||
}}
|
||||
disabled={!isCurrentWorkspaceManager}
|
||||
>
|
||||
{t('auth.unauthorized', { ns: 'tools' })}
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
{(collection.type === CollectionType.custom) && (
|
||||
<div className="system-sm-semibold-uppercase text-text-secondary">
|
||||
<span className="">{t('includeToolNum', { ns: 'tools', num: toolList.length, action: toolList.length > 1 ? 'actions' : 'action' }).toLocaleUpperCase()}</span>
|
||||
</div>
|
||||
)}
|
||||
{(collection.type === CollectionType.workflow) && (
|
||||
<div className="system-sm-semibold-uppercase text-text-secondary">
|
||||
<span className="">{t('createTool.toolInput.title', { ns: 'tools' }).toLocaleUpperCase()}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="mt-1 flex-1 overflow-y-auto py-2">
|
||||
{collection.type !== CollectionType.workflow && toolList.map(tool => (
|
||||
<ToolItem
|
||||
key={tool.name}
|
||||
disabled={false}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex min-h-0 flex-1 flex-col pt-3">
|
||||
{isDetailLoading && <div className="flex h-[200px]"><Loading type="app" /></div>}
|
||||
{!isDetailLoading && (
|
||||
<>
|
||||
<div className="shrink-0">
|
||||
{(collection.type === CollectionType.builtIn || collection.type === CollectionType.model) && isAuthed && (
|
||||
<div className="mb-1 flex h-6 items-center justify-between system-sm-semibold-uppercase text-text-secondary">
|
||||
{t('detailPanel.actionNum', { ns: 'plugin', num: toolList.length, action: toolList.length > 1 ? 'actions' : 'action' })}
|
||||
{needAuth && (
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="small"
|
||||
onClick={() => {
|
||||
if (collection.type === CollectionType.builtIn || collection.type === CollectionType.model)
|
||||
showSettingAuthModal()
|
||||
}}
|
||||
disabled={!isCurrentWorkspaceManager}
|
||||
>
|
||||
<Indicator className="mr-2" color="green" />
|
||||
{t('auth.authorized', { ns: 'tools' })}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{(collection.type === CollectionType.builtIn || collection.type === CollectionType.model) && needAuth && !isAuthed && (
|
||||
<>
|
||||
<div className="system-sm-semibold-uppercase text-text-secondary">
|
||||
<span className="">{t('includeToolNum', { ns: 'tools', num: toolList.length, action: toolList.length > 1 ? 'actions' : 'action' }).toLocaleUpperCase()}</span>
|
||||
<span className="px-1">·</span>
|
||||
<span className="text-util-colors-orange-orange-600">{t('auth.setup', { ns: 'tools' }).toLocaleUpperCase()}</span>
|
||||
</div>
|
||||
<Button
|
||||
variant="primary"
|
||||
className={cn('my-3 w-full shrink-0')}
|
||||
onClick={() => {
|
||||
if (collection.type === CollectionType.builtIn || collection.type === CollectionType.model)
|
||||
showSettingAuthModal()
|
||||
}}
|
||||
disabled={!isCurrentWorkspaceManager}
|
||||
>
|
||||
{t('auth.unauthorized', { ns: 'tools' })}
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
{(collection.type === CollectionType.custom) && (
|
||||
<div className="system-sm-semibold-uppercase text-text-secondary">
|
||||
<span className="">{t('includeToolNum', { ns: 'tools', num: toolList.length, action: toolList.length > 1 ? 'actions' : 'action' }).toLocaleUpperCase()}</span>
|
||||
</div>
|
||||
)}
|
||||
{(collection.type === CollectionType.workflow) && (
|
||||
<div className="system-sm-semibold-uppercase text-text-secondary">
|
||||
<span className="">{t('createTool.toolInput.title', { ns: 'tools' }).toLocaleUpperCase()}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="mt-1 flex-1 overflow-y-auto py-2">
|
||||
{collection.type !== CollectionType.workflow && toolList.map(tool => (
|
||||
<ToolItem
|
||||
key={tool.name}
|
||||
disabled={false}
|
||||
collection={collection}
|
||||
tool={tool}
|
||||
isBuiltIn={isBuiltIn}
|
||||
isModel={isModel}
|
||||
/>
|
||||
))}
|
||||
{collection.type === CollectionType.workflow && (customCollection as WorkflowToolProviderResponse)?.tool?.parameters.map(item => (
|
||||
<div key={item.name} className="mb-1 py-1">
|
||||
<div className="mb-1 flex items-center gap-2">
|
||||
<span className="code-sm-semibold text-text-secondary">{item.name}</span>
|
||||
<span className="system-xs-regular text-text-tertiary">{item.type}</span>
|
||||
<span className="system-xs-medium text-text-warning-secondary">{item.required ? t('createTool.toolInput.required', { ns: 'tools' }) : ''}</span>
|
||||
</div>
|
||||
<div className="system-xs-regular text-text-tertiary">{item.llm_description}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
{showSettingAuth && (
|
||||
<ConfigCredential
|
||||
collection={collection}
|
||||
tool={tool}
|
||||
isBuiltIn={isBuiltIn}
|
||||
isModel={isModel}
|
||||
onCancel={() => setShowSettingAuth(false)}
|
||||
onSaved={async (value) => {
|
||||
await updateBuiltInToolCredential(collection.name, value)
|
||||
toast.success(t('api.actionSuccess', { ns: 'common' }))
|
||||
await onRefreshData()
|
||||
setShowSettingAuth(false)
|
||||
}}
|
||||
onRemove={async () => {
|
||||
await removeBuiltInToolCredential(collection.name)
|
||||
toast.success(t('api.actionSuccess', { ns: 'common' }))
|
||||
await onRefreshData()
|
||||
setShowSettingAuth(false)
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
{collection.type === CollectionType.workflow && (customCollection as WorkflowToolProviderResponse)?.tool?.parameters.map(item => (
|
||||
<div key={item.name} className="mb-1 py-1">
|
||||
<div className="mb-1 flex items-center gap-2">
|
||||
<span className="code-sm-semibold text-text-secondary">{item.name}</span>
|
||||
<span className="system-xs-regular text-text-tertiary">{item.type}</span>
|
||||
<span className="system-xs-medium text-text-warning-secondary">{item.required ? t('createTool.toolInput.required', { ns: 'tools' }) : ''}</span>
|
||||
)}
|
||||
{isShowEditCollectionToolModal && (
|
||||
<EditCustomToolModal
|
||||
payload={customCollection}
|
||||
onHide={() => setIsShowEditCustomCollectionModal(false)}
|
||||
onEdit={doUpdateCustomToolCollection}
|
||||
onRemove={onClickCustomToolDelete}
|
||||
/>
|
||||
)}
|
||||
{workflowToolDrawerOpen && (
|
||||
<WorkflowToolDrawer
|
||||
payload={customCollection as unknown as WorkflowToolDrawerPayload}
|
||||
onHide={() => setWorkflowToolDrawerOpen(false)}
|
||||
onRemove={onClickWorkflowToolDelete}
|
||||
onSave={updateWorkflowToolProvider}
|
||||
/>
|
||||
)}
|
||||
<AlertDialog open={showConfirmDelete} onOpenChange={open => !open && setShowConfirmDelete(false)}>
|
||||
<AlertDialogContent>
|
||||
<div className="flex flex-col gap-2 px-6 pt-6 pb-4">
|
||||
<AlertDialogTitle className="w-full truncate title-2xl-semi-bold text-text-primary">
|
||||
{t('createTool.deleteToolConfirmTitle', { ns: 'tools' })}
|
||||
</AlertDialogTitle>
|
||||
<AlertDialogDescription className="w-full system-md-regular wrap-break-word whitespace-pre-wrap text-text-tertiary">
|
||||
{t('createTool.deleteToolConfirmContent', { ns: 'tools' })}
|
||||
</AlertDialogDescription>
|
||||
</div>
|
||||
<div className="system-xs-regular text-text-tertiary">{item.llm_description}</div>
|
||||
</div>
|
||||
))}
|
||||
<AlertDialogActions>
|
||||
<AlertDialogCancelButton>{t('operation.cancel', { ns: 'common' })}</AlertDialogCancelButton>
|
||||
<AlertDialogConfirmButton onClick={handleConfirmDelete}>
|
||||
{t('operation.confirm', { ns: 'common' })}
|
||||
</AlertDialogConfirmButton>
|
||||
</AlertDialogActions>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
{showSettingAuth && (
|
||||
<ConfigCredential
|
||||
collection={collection}
|
||||
onCancel={() => setShowSettingAuth(false)}
|
||||
onSaved={async (value) => {
|
||||
await updateBuiltInToolCredential(collection.name, value)
|
||||
toast.success(t('api.actionSuccess', { ns: 'common' }))
|
||||
await onRefreshData()
|
||||
setShowSettingAuth(false)
|
||||
}}
|
||||
onRemove={async () => {
|
||||
await removeBuiltInToolCredential(collection.name)
|
||||
toast.success(t('api.actionSuccess', { ns: 'common' }))
|
||||
await onRefreshData()
|
||||
setShowSettingAuth(false)
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{isShowEditCollectionToolModal && (
|
||||
<EditCustomToolModal
|
||||
payload={customCollection}
|
||||
onHide={() => setIsShowEditCustomCollectionModal(false)}
|
||||
onEdit={doUpdateCustomToolCollection}
|
||||
onRemove={onClickCustomToolDelete}
|
||||
/>
|
||||
)}
|
||||
{workflowToolDrawerOpen && (
|
||||
<WorkflowToolDrawer
|
||||
payload={customCollection as unknown as WorkflowToolDrawerPayload}
|
||||
onHide={() => setWorkflowToolDrawerOpen(false)}
|
||||
onRemove={onClickWorkflowToolDelete}
|
||||
onSave={updateWorkflowToolProvider}
|
||||
/>
|
||||
)}
|
||||
<AlertDialog open={showConfirmDelete} onOpenChange={open => !open && setShowConfirmDelete(false)}>
|
||||
<AlertDialogContent>
|
||||
<div className="flex flex-col gap-2 px-6 pt-6 pb-4">
|
||||
<AlertDialogTitle className="w-full truncate title-2xl-semi-bold text-text-primary">
|
||||
{t('createTool.deleteToolConfirmTitle', { ns: 'tools' })}
|
||||
</AlertDialogTitle>
|
||||
<AlertDialogDescription className="w-full system-md-regular wrap-break-word whitespace-pre-wrap text-text-tertiary">
|
||||
{t('createTool.deleteToolConfirmContent', { ns: 'tools' })}
|
||||
</AlertDialogDescription>
|
||||
</div>
|
||||
<AlertDialogActions>
|
||||
<AlertDialogCancelButton>{t('operation.cancel', { ns: 'common' })}</AlertDialogCancelButton>
|
||||
<AlertDialogConfirmButton onClick={handleConfirmDelete}>
|
||||
{t('operation.confirm', { ns: 'common' })}
|
||||
</AlertDialogConfirmButton>
|
||||
</AlertDialogActions>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</div>
|
||||
</DrawerContent>
|
||||
</DrawerPopup>
|
||||
</DrawerViewport>
|
||||
</DrawerPortal>
|
||||
</Drawer>
|
||||
)
|
||||
}
|
||||
|
||||
@ -1,6 +1,15 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import type { DataSet } from '@/models/datasets'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import {
|
||||
Drawer,
|
||||
DrawerBackdrop,
|
||||
DrawerContent,
|
||||
DrawerPopup,
|
||||
DrawerPortal,
|
||||
DrawerViewport,
|
||||
} from '@langgenius/dify-ui/drawer'
|
||||
import {
|
||||
RiDeleteBinLine,
|
||||
RiEditLine,
|
||||
@ -13,7 +22,6 @@ import SettingsModal from '@/app/components/app/configuration/dataset-config/set
|
||||
import ActionButton, { ActionButtonState } from '@/app/components/base/action-button'
|
||||
import AppIcon from '@/app/components/base/app-icon'
|
||||
import Badge from '@/app/components/base/badge'
|
||||
import Drawer from '@/app/components/base/drawer'
|
||||
import { ModelFeatureEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
|
||||
import FeatureIcon from '@/app/components/header/account-setting/model-provider-page/model-selector/feature-icon'
|
||||
import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints'
|
||||
@ -131,12 +139,29 @@ const DatasetItem: FC<Props> = ({
|
||||
}
|
||||
|
||||
{isShowSettingsModal && (
|
||||
<Drawer isOpen={isShowSettingsModal} onClose={hideSettingsModal} footer={null} mask={isMobile} panelClassName="mt-16 mx-2 sm:mr-2 mb-3 p-0! max-w-[640px]! rounded-xl">
|
||||
<SettingsModal
|
||||
currentDataset={payload}
|
||||
onCancel={hideSettingsModal}
|
||||
onSave={handleSave}
|
||||
/>
|
||||
<Drawer
|
||||
open={isShowSettingsModal}
|
||||
modal
|
||||
swipeDirection="right"
|
||||
onOpenChange={(open) => {
|
||||
if (!open)
|
||||
hideSettingsModal()
|
||||
}}
|
||||
>
|
||||
<DrawerPortal>
|
||||
<DrawerBackdrop className={cn(!isMobile && 'bg-transparent')} />
|
||||
<DrawerViewport>
|
||||
<DrawerPopup className="p-0! data-[swipe-direction=right]:top-16 data-[swipe-direction=right]:right-2 data-[swipe-direction=right]:bottom-3 data-[swipe-direction=right]:h-auto data-[swipe-direction=right]:w-full data-[swipe-direction=right]:max-w-[640px] data-[swipe-direction=right]:rounded-xl">
|
||||
<DrawerContent className="flex min-h-0 flex-1 flex-col p-0 pb-0">
|
||||
<SettingsModal
|
||||
currentDataset={payload}
|
||||
onCancel={hideSettingsModal}
|
||||
onSave={handleSave}
|
||||
/>
|
||||
</DrawerContent>
|
||||
</DrawerPopup>
|
||||
</DrawerViewport>
|
||||
</DrawerPortal>
|
||||
</Drawer>
|
||||
)}
|
||||
</div>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user