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:
Coding On Star 2026-05-10 00:24:45 +08:00 committed by GitHub
parent 24ea21db25
commit e48d7bb097
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
27 changed files with 946 additions and 1684 deletions

View File

@ -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

View File

@ -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()
})
})

View File

@ -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>
)
}

View File

@ -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>
)}

View File

@ -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()
})
})

View File

@ -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>
)

View File

@ -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)

View File

@ -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>
)

View File

@ -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>
)

View File

@ -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)
})
})
})

View File

@ -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(),
},
},
},
}

View File

@ -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>
)
}

View File

@ -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()

View File

@ -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">

View File

@ -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}</>

View File

@ -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' })}>

View File

@ -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}

View File

@ -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>
)

View File

@ -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>
)
}

View File

@ -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>
)
}

View File

@ -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>
)
}

View File

@ -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>
)
}

View File

@ -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', () => {

View File

@ -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>
)
}

View File

@ -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', () => ({

View File

@ -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>
)
}

View File

@ -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>