diff --git a/eslint-suppressions.json b/eslint-suppressions.json
index e49483f63c..23e2da9ee0 100644
--- a/eslint-suppressions.json
+++ b/eslint-suppressions.json
@@ -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
diff --git a/web/__tests__/tools/tool-provider-detail-flow.test.tsx b/web/__tests__/tools/tool-provider-detail-flow.test.tsx
index c0dd6da1c5..5e3e94d4ff 100644
--- a/web/__tests__/tools/tool-provider-detail-flow.test.tsx
+++ b/web/__tests__/tools/tool-provider-detail-flow.test.tsx
@@ -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
- ? (
-
- {children}
-
-
- )
- : null
- ),
-}))
-
vi.mock('@langgenius/dify-ui/toast', () => ({
default: { notify: vi.fn() },
toast: {
@@ -525,10 +512,10 @@ describe('Tool Provider Detail Flow Integration', () => {
render()
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()
})
})
diff --git a/web/app/components/app/configuration/config/agent/agent-tools/setting-built-in-tool.tsx b/web/app/components/app/configuration/config/agent/agent-tools/setting-built-in-tool.tsx
index 71922d5a7e..806fdd5e93 100644
--- a/web/app/components/app/configuration/config/agent/agent-tools/setting-built-in-tool.tsx
+++ b/web/app/components/app/configuration/config/agent/agent-tools/setting-built-in-tool.tsx
@@ -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 = ({
return (
{
+ if (!open)
+ onHide()
+ }}
>
- <>
- {isLoading && }
- {!isLoading && (
- <>
- {/* header */}
-
-
- {showBackButton && (
-
-
- {t('detailPanel.operation.back', { ns: 'plugin' })}
-
- )}
-
-
-
-
-
{currTool?.label[language]}
- {!!currTool?.description[language] && (
-
- )}
- {
- collection.allow_delete && collection.type === CollectionType.builtIn && (
-
- )
- }
-
- {/* form */}
-
-
- {(hasSetting && !readonly)
- ? (
-
{
- setCurrType(value)
- }}
- options={[
- { value: 'info', text: t('setBuiltInTools.parameters', { ns: 'tools' })! },
- { value: 'setting', text: t('setBuiltInTools.setting', { ns: 'tools' })! },
- ]}
- />
- )
- : (
- {t('setBuiltInTools.parameters', { ns: 'tools' })}
- )}
-
- {isInfoActive ? infoUI : settingUI}
- {!readonly && !isInfoActive && (
-
-
-
+
+
+
+
+
+ {isLoading && }
+ {!isLoading && (
+ <>
+ {/* header */}
+
-
-
-
- >
- )}
- >
+ {showBackButton && (
+
+
+ {t('detailPanel.operation.back', { ns: 'plugin' })}
+
+ )}
+
+
+
+
+ {currTool?.label[language]}
+ {!!currTool?.description[language] && (
+
+ )}
+ {
+ collection.allow_delete && collection.type === CollectionType.builtIn && (
+
+ )
+ }
+
+ {/* form */}
+
+
+ {(hasSetting && !readonly)
+ ? (
+
{
+ setCurrType(value)
+ }}
+ options={[
+ { value: 'info', text: t('setBuiltInTools.parameters', { ns: 'tools' })! },
+ { value: 'setting', text: t('setBuiltInTools.setting', { ns: 'tools' })! },
+ ]}
+ />
+ )
+ : (
+ {t('setBuiltInTools.parameters', { ns: 'tools' })}
+ )}
+
+ {isInfoActive ? infoUI : settingUI}
+ {!readonly && !isInfoActive && (
+
+
+
+
+ )}
+
+
+
+
+ >
+ )}
+
+
+
+
)
}
diff --git a/web/app/components/app/configuration/configuration-view.tsx b/web/app/components/app/configuration/configuration-view.tsx
index 1e2b0bf81a..04cb3ffeda 100644
--- a/web/app/components/app/configuration/configuration-view.tsx
+++ b/web/app/components/app/configuration/configuration-view.tsx
@@ -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
= ({
)}
{isMobile && (
-
-
+ {
+ if (!open)
+ onHideDebugPanel()
+ }}
+ >
+
+
+
+
+
+
+
+
+
+
+
+
+
)}
diff --git a/web/app/components/app/configuration/dataset-config/card-item/__tests__/index.spec.tsx b/web/app/components/app/configuration/dataset-config/card-item/__tests__/index.spec.tsx
index 1d14f7dbd2..cffed4b846 100644
--- a/web/app/components/app/configuration/dataset-config/card-item/__tests__/index.spec.tsx
+++ b/web/app/components/app/configuration/dataset-config/card-item/__tests__/index.spec.tsx
@@ -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()
})
})
diff --git a/web/app/components/app/configuration/dataset-config/card-item/index.tsx b/web/app/components/app/configuration/dataset-config/card-item/index.tsx
index 8eb856feb3..62dd17f4b1 100644
--- a/web/app/components/app/configuration/dataset-config/card-item/index.tsx
+++ b/web/app/components/app/configuration/dataset-config/card-item/index.tsx
@@ -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 = ({
/>
)
}
- setShowSettingsModal(false)} footer={null} mask={isMobile} panelClassName="mt-16 mx-2 sm:mr-2 mb-3 p-0! max-w-[640px]! rounded-xl">
- {showSettingsModal && (
- setShowSettingsModal(false)}
- onSave={handleSave}
- />
- )}
+ {
+ if (!open)
+ setShowSettingsModal(false)
+ }}
+ >
+
+
+
+
+
+ {showSettingsModal && (
+ setShowSettingsModal(false)}
+ onSave={handleSave}
+ />
+ )}
+
+
+
+
)
diff --git a/web/app/components/app/log/__tests__/list.spec.tsx b/web/app/components/app/log/__tests__/list.spec.tsx
index fe589b599a..dbd350f16b 100644
--- a/web/app/components/app/log/__tests__/list.spec.tsx
+++ b/web/app/components/app/log/__tests__/list.spec.tsx
@@ -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
- ? (
-
-
- {children}
-
- )
- : null
- ),
-}))
-
vi.mock('@/app/components/base/loading', () => ({
default: () => loading
,
}))
@@ -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)
diff --git a/web/app/components/app/log/list.tsx b/web/app/components/app/log/list.tsx
index 1633d53ccc..2e078b7b93 100644
--- a/web/app/components/app/log/list.tsx
+++ b/web/app/components/app/log/list.tsx
@@ -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) {
{!isAdvanced && }
-
+
@@ -872,21 +879,32 @@ const ConversationList: FC = ({ logs, appDetail, onRefresh })
- {
+ if (!open)
+ onCloseDrawer()
}}
- >
- {isChatMode
- ?
- : }
-
+ >
+
+
+
+
+
+
+ {isChatMode
+ ?
+ : }
+
+
+
+
+
)
diff --git a/web/app/components/app/workflow-log/list.tsx b/web/app/components/app/workflow-log/list.tsx
index e514962d9b..78d9a329e6 100644
--- a/web/app/components/app/workflow-log/list.tsx
+++ b/web/app/components/app/workflow-log/list.tsx
@@ -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 = ({ logs, appDetail, onRefresh }) => {
{
+ if (!open)
+ onCloseDrawer()
+ }}
>
-
+
+
+
+
+
+
+
+
+
+
)
diff --git a/web/app/components/base/drawer/__tests__/index.spec.tsx b/web/app/components/base/drawer/__tests__/index.spec.tsx
deleted file mode 100644
index 1f9c1c258f..0000000000
--- a/web/app/components/base/drawer/__tests__/index.spec.tsx
+++ /dev/null
@@ -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
- }) => (
- capturedDialogOnClose?.()}
- >
- {children}
-
- ),
- Popup: ({ children, className, ...props }: {
- children: React.ReactNode
- className: string
- }) => (
-
- {children}
-
- ),
- Title: ({ children, className, render, ...props }: {
- children: React.ReactNode
- className?: string
- render?: React.ReactElement
- }) => {
- const Component = render?.type ?? 'h2'
- return (
-
- {children}
-
- )
- },
- },
-}))
-
-// Mock XMarkIcon
-vi.mock('@heroicons/react/24/outline', () => ({
- XMarkIcon: ({ className, onClick }: { className: string, onClick?: () => void }) => (
-
- ),
-}))
-
-// Helper function to render Drawer with default props
-const defaultProps: IDrawerProps = {
- isOpen: true,
- onClose: vi.fn(),
- children: Content
,
-}
-
-const renderDrawer = (props: Partial = {}) => {
- const mergedProps = { ...defaultProps, ...props }
- return render()
-}
-
-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 = Custom Content
-
- // 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 = Custom Footer
-
- // 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 = ''
-
- // 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 = (
-
-
Heading
-
Paragraph
-
-
-
- )
-
- // 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(
-
- Content
- ,
- )
-
- // Act - Toggle multiple times
- rerender(
-
- Content
- ,
- )
- rerender(
-
- Content
- ,
- )
- rerender(
-
- Content
- ,
- )
-
- // 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: Footer
,
- })
-
- // 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: Minimal Content
,
- }
-
- // Act
- render()
-
- // 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)
- })
- })
-})
diff --git a/web/app/components/base/drawer/index.stories.tsx b/web/app/components/base/drawer/index.stories.tsx
deleted file mode 100644
index 57ab35281c..0000000000
--- a/web/app/components/base/drawer/index.stories.tsx
+++ /dev/null
@@ -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
-
-export default meta
-type Story = StoryObj
-
-const DrawerDemo = (props: React.ComponentProps) => {
- const [open, setOpen] = useState(false)
-
- return (
-
-
-
-
setOpen(false)}
- title={props.title ?? 'Edit configuration'}
- description={props.description ?? 'Adjust settings in the side panel and save.'}
- footer={props.footer ?? undefined}
- >
-
-
- This example renders arbitrary content inside the drawer body. Use it for contextual forms, settings, or informational panels.
-
-
- Content area
-
-
-
-
- )
-}
-
-export const Playground: Story = {
- render: args => ,
- args: {
- children: null,
- isOpen: false,
- onClose: fn(),
- },
- parameters: {
- docs: {
- source: {
- language: 'tsx',
- code: `
-const [open, setOpen] = useState(false)
-
- setOpen(false)}
- title="Edit configuration"
- description="Adjust settings in the side panel and save."
->
- ...
-
- `.trim(),
- },
- },
- },
-}
-
-export const CustomFooter: Story = {
- render: args => (
-
-
-
-
- )}
- />
- ),
- args: {
- children: null,
- isOpen: false,
- onClose: fn(),
- },
- parameters: {
- docs: {
- source: {
- language: 'tsx',
- code: `
-}>
- ...
-
- `.trim(),
- },
- },
- },
-}
diff --git a/web/app/components/base/drawer/index.tsx b/web/app/components/base/drawer/index.tsx
deleted file mode 100644
index d6c0d58e7c..0000000000
--- a/web/app/components/base/drawer/index.tsx
+++ /dev/null
@@ -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 (
- {
- if (!open && !clickOutsideNotOpen)
- onClose()
- }}
- >
-
-
-
- {!noOverlay && (
-
- )}
-
- <>
-
- {title && (
-
}
- className="text-lg leading-6 font-medium text-text-primary"
- >
- {title}
-
- )}
- {showClose && (
-
- {
- if (e.key === 'Enter' || e.key === ' ')
- onClose()
- }}
- role="button"
- tabIndex={0}
- aria-label={t('operation.close', { ns: 'common' })}
- data-testid="close-icon"
- />
-
- )}
-
- {description && {description}
}
- {children}
- >
- {footer || (footer === null
- ? null
- : (
-
-
-
-
- ))}
-
-
-
-
-
- )
-}
diff --git a/web/app/components/base/float-right-container/__tests__/index.spec.tsx b/web/app/components/base/float-right-container/__tests__/index.spec.tsx
index 236a30dd20..4466b2cadc 100644
--- a/web/app/components/base/float-right-container/__tests__/index.spec.tsx
+++ b/web/app/components/base/float-right-container/__tests__/index.spec.tsx
@@ -32,7 +32,6 @@ describe('FloatRightContainer', () => {
isMobile={true}
isOpen={false}
onClose={vi.fn()}
- unmount={true}
>
Closed mobile content
,
@@ -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(
-
- Closable content
- ,
- )
-
- 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(
-
- Closable content
- ,
- )
-
- 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(
Class forwarding content
@@ -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()
diff --git a/web/app/components/base/float-right-container/index.stories.tsx b/web/app/components/base/float-right-container/index.stories.tsx
index 5887afd1e3..ec26bb7be0 100644
--- a/web/app/components/base/float-right-container/index.stories.tsx
+++ b/web/app/components/base/float-right-container/index.stories.tsx
@@ -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
>
diff --git a/web/app/components/base/float-right-container/index.tsx b/web/app/components/base/float-right-container/index.tsx
index 7435fd9643..db3b73da95 100644
--- a/web/app/components/base/float-right-container/index.tsx
+++ b/web/app/components/base/float-right-container/index.tsx
@@ -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 && (
-
{children}
+
{
+ if (!open)
+ onClose()
+ }}
+ >
+
+
+
+
+
+ {(title || showClose) && (
+
+ {title && (
+
+ {title}
+
+ )}
+ {showClose && (
+
+ )}
+
+ )}
+ {children}
+
+
+
+
+
)}
{(!isMobile && isOpen) && (
<>{children}>
diff --git a/web/app/components/datasets/create/step-two/components/preview-panel.tsx b/web/app/components/datasets/create/step-two/components/preview-panel.tsx
index 254a28619c..9f10863288 100644
--- a/web/app/components/datasets/create/step-two/components/preview-panel.tsx
+++ b/web/app/components/datasets/create/step-two/components/preview-panel.tsx
@@ -54,7 +54,7 @@ export const PreviewPanel: FC
= ({
const { t } = useTranslation()
return (
-
+
diff --git a/web/app/components/datasets/documents/detail/index.tsx b/web/app/components/datasets/documents/detail/index.tsx
index caae703f6b..43fc99851d 100644
--- a/web/app/components/datasets/documents/detail/index.tsx
+++ b/web/app/components/datasets/documents/detail/index.tsx
@@ -292,7 +292,7 @@ const DocumentDetail: FC = ({ datasetId, documentId }) => {
)}
)}
- setShowMetadata(false)} isMobile={isMobile} panelClassName="justify-start!" footer={null}>
+ setShowMetadata(false)} isMobile={isMobile} panelClassName="justify-start!">
= ({ datasetId }: Props) => {
isMobile={isMobile}
isOpen={isShowRightPanel}
onClose={hideRightPanel}
- footer={null}
>
{isRetrievalLoading
@@ -181,23 +187,33 @@ const HitTestingPage: FC
= ({ datasetId }: Props) => {
setIsShowModifyRetrievalModal(false)}
- footer={null}
- mask={isMobile}
- panelClassName="mt-16 mx-2 sm:mr-2 mb-3 p-0! max-w-[640px]! rounded-xl"
- >
- setIsShowModifyRetrievalModal(false)}
- onSave={(value) => {
- setRetrievalConfig(value)
+ open={isShowModifyRetrievalModal}
+ modal
+ swipeDirection="right"
+ onOpenChange={(open) => {
+ if (!open)
setIsShowModifyRetrievalModal(false)
- }}
- />
+ }}
+ >
+
+
+
+
+
+ setIsShowModifyRetrievalModal(false)}
+ onSave={(value) => {
+ setRetrievalConfig(value)
+ setIsShowModifyRetrievalModal(false)
+ }}
+ />
+
+
+
+
)
diff --git a/web/app/components/plugins/plugin-detail-panel/endpoint-modal.tsx b/web/app/components/plugins/plugin-detail-panel/endpoint-modal.tsx
index 0fe0fff6df..5bac8c827b 100644
--- a/web/app/components/plugins/plugin-detail-panel/endpoint-modal.tsx
+++ b/web/app/components/plugins/plugin-detail-panel/endpoint-modal.tsx
@@ -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 = ({
return (
{
+ if (!open)
+ onCancel()
+ }}
>
- <>
-
-
-
{t('detailPanel.endpointModalTitle', { ns: 'plugin' })}
-
-
-
-
-
{t('detailPanel.endpointModalDesc', { ns: 'plugin' })}
-
-
-
-
-
-
-
-
-
-
-
- >
+
+
+
+
+
+
+
+
{t('detailPanel.endpointModalTitle', { ns: 'plugin' })}
+
+
+
+
+
{t('detailPanel.endpointModalDesc', { ns: 'plugin' })}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
)
}
diff --git a/web/app/components/plugins/plugin-detail-panel/index.tsx b/web/app/components/plugins/plugin-detail-panel/index.tsx
index 3041d2e2a6..877b15c51e 100644
--- a/web/app/components/plugins/plugin-detail-panel/index.tsx
+++ b/web/app/components/plugins/plugin-detail-panel/index.tsx
@@ -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 = ({
return (
{
+ if (!open)
+ onHide()
+ }}
>
- {detail && (
- <>
-
-
-
-
- {detail.declaration.category === PluginCategoryEnum.trigger && (
- <>
-
-
- >
- )}
- {!!detail.declaration.tool &&
}
- {!!detail.declaration.agent_strategy &&
}
- {!!detail.declaration.endpoint &&
}
- {!!detail.declaration.model &&
}
- {!!detail.declaration.datasource &&
}
-
-
-
-
- >
- )}
+
+
+
+
+
+ {detail && (
+ <>
+
+
+
+
+ {detail.declaration.category === PluginCategoryEnum.trigger && (
+ <>
+
+
+ >
+ )}
+ {!!detail.declaration.tool &&
}
+ {!!detail.declaration.agent_strategy &&
}
+ {!!detail.declaration.endpoint &&
}
+ {!!detail.declaration.model &&
}
+ {!!detail.declaration.datasource &&
}
+
+
+
+
+ >
+ )}
+
+
+
+
)
}
diff --git a/web/app/components/plugins/plugin-detail-panel/strategy-detail.tsx b/web/app/components/plugins/plugin-detail-panel/strategy-detail.tsx
index 8fda455b26..824697566b 100644
--- a/web/app/components/plugins/plugin-detail-panel/strategy-detail.tsx
+++ b/web/app/components/plugins/plugin-detail-panel/strategy-detail.tsx
@@ -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 = ({
return (
{
+ if (!open)
+ onHide()
+ }}
>
- <>
- {/* header */}
-
-
-
-
- BACK
-
-
-
-
{getValueFromI18nObject(provider.label)}
-
-
{getValueFromI18nObject(detail.identity.label)}
-
-
- {/* form */}
-
-
-
{t('setBuiltInTools.parameters', { ns: 'tools' })}
-
- {detail.parameters.length > 0 && (
-
- {detail.parameters.map((item: any, index) => (
-
-
-
{getValueFromI18nObject(item.label)}
-
- {getType(item.type)}
-
- {item.required && (
-
{t('setBuiltInTools.required', { ns: 'tools' })}
- )}
+
+
+
+
+
+ {/* header */}
+
+
+
+
+ BACK
+
+
+
+
{getValueFromI18nObject(provider.label)}
+
+
{getValueFromI18nObject(detail.identity.label)}
+
+
+ {/* form */}
+
+
+
{t('setBuiltInTools.parameters', { ns: 'tools' })}
+
+ {detail.parameters.length > 0 && (
+
+ {detail.parameters.map((item: any, index) => (
+
+
+
{getValueFromI18nObject(item.label)}
+
+ {getType(item.type)}
+
+ {item.required && (
+
{t('setBuiltInTools.required', { ns: 'tools' })}
+ )}
+
+ {item.human_description && (
+
+ {getValueFromI18nObject(item.human_description)}
+
+ )}
+
+ ))}
- {item.human_description && (
-
- {getValueFromI18nObject(item.human_description)}
+ )}
+
+ {detail.output_schema && (
+ <>
+
+
OUTPUT
+ {outputSchema.length > 0 && (
+
+ {outputSchema.map((outputItem, index) => (
+
+
+
{outputItem.name}
+
{outputItem.type}
+
+ {outputItem.description && (
+
+ {outputItem.description}
+
+ )}
+
+ ))}
)}
-
- ))}
+ >
+ )}
- )}
-
- {detail.output_schema && (
- <>
-
- OUTPUT
- {outputSchema.length > 0 && (
-
- {outputSchema.map((outputItem, index) => (
-
-
-
{outputItem.name}
-
{outputItem.type}
-
- {outputItem.description && (
-
- {outputItem.description}
-
- )}
-
- ))}
-
- )}
- >
- )}
-
-
- >
+
+
+
+
+
)
}
diff --git a/web/app/components/plugins/plugin-detail-panel/trigger/event-detail-drawer.tsx b/web/app/components/plugins/plugin-detail-panel/trigger/event-detail-drawer.tsx
index eb56a0178c..3de2b30f1c 100644
--- a/web/app/components/plugins/plugin-detail-panel/trigger/event-detail-drawer.tsx
+++ b/web/app/components/plugins/plugin-detail-panel/trigger/event-detail-drawer.tsx
@@ -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
= (props) => {
return (
{
+ if (!open)
+ onClose()
+ }}
>
-
-
-
-
- {t('detailPanel.operation.back', { ns: 'plugin' })}
-
-
-
-
-
-
{eventInfo?.identity?.label[language]}
-
-
-
-
{t('setBuiltInTools.parameters', { ns: 'tools' })}
- {parametersSchemas.length > 0
- ? (
- parametersSchemas.map((item, index) => (
-
-
-
{item.label[language]}
-
- {getType(item.type, t)}
-
- {item.required && (
-
{t('setBuiltInTools.required', { ns: 'tools' })}
- )}
-
- {item.description && (
-
- {item.description?.[language]}
-
- )}
+
+
+
+
+
+
+
- ))
- )
- :
{t('events.item.noParameters', { ns: 'pluginTrigger' })}
}
-
-
-
{t('events.output', { ns: 'pluginTrigger' })}
-
- {outputFields.map(item => (
-
- ))}
-
-
-
+
+
+ {t('detailPanel.operation.back', { ns: 'plugin' })}
+
+
+
+
+
+ {eventInfo?.identity?.label[language]}
+
+
+
+
{t('setBuiltInTools.parameters', { ns: 'tools' })}
+ {parametersSchemas.length > 0
+ ? (
+ parametersSchemas.map((item, index) => (
+
+
+
{item.label[language]}
+
+ {getType(item.type, t)}
+
+ {item.required && (
+
{t('setBuiltInTools.required', { ns: 'tools' })}
+ )}
+
+ {item.description && (
+
+ {item.description?.[language]}
+
+ )}
+
+ ))
+ )
+ :
{t('events.item.noParameters', { ns: 'pluginTrigger' })}
}
+
+
+
{t('events.output', { ns: 'pluginTrigger' })}
+
+ {outputFields.map(item => (
+
+ ))}
+
+
+
+
+
+
+
)
}
diff --git a/web/app/components/tools/mcp/detail/__tests__/provider-detail.spec.tsx b/web/app/components/tools/mcp/detail/__tests__/provider-detail.spec.tsx
index 05380916b2..4d69d89516 100644
--- a/web/app/components/tools/mcp/detail/__tests__/provider-detail.spec.tsx
+++ b/web/app/components/tools/mcp/detail/__tests__/provider-detail.spec.tsx
@@ -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
{children}
- },
-}))
-
// 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', () => {
,
{ wrapper: createWrapper() },
)
- expect(screen.getByTestId('drawer')).toBeInTheDocument()
+ expect(screen.getByRole('dialog')).toBeInTheDocument()
})
it('should render content when detail is provided', () => {
diff --git a/web/app/components/tools/mcp/detail/provider-detail.tsx b/web/app/components/tools/mcp/detail/provider-detail.tsx
index 7b2d351637..e704e6d262 100644
--- a/web/app/components/tools/mcp/detail/provider-detail.tsx
+++ b/web/app/components/tools/mcp/detail/provider-detail.tsx
@@ -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
= ({
return (
{
+ if (!open)
+ onHide()
+ }}
>
- {detail && (
-
- )}
+
+
+
+
+
+ {detail && (
+
+ )}
+
+
+
+
)
}
diff --git a/web/app/components/tools/provider/__tests__/detail.spec.tsx b/web/app/components/tools/provider/__tests__/detail.spec.tsx
index edb052fa41..5a26589e11 100644
--- a/web/app/components/tools/provider/__tests__/detail.spec.tsx
+++ b/web/app/components/tools/provider/__tests__/detail.spec.tsx
@@ -74,11 +74,6 @@ vi.mock('@/utils/var', () => ({
basePath: '',
}))
-vi.mock('@/app/components/base/drawer', () => ({
- default: ({ children, isOpen }: { children: React.ReactNode, isOpen: boolean }) =>
- isOpen ? {children}
: null,
-}))
-
const mockToastSuccess = vi.hoisted(() => vi.fn())
const mockToastError = vi.hoisted(() => vi.fn())
vi.mock('@langgenius/dify-ui/toast', () => ({
diff --git a/web/app/components/tools/provider/detail.tsx b/web/app/components/tools/provider/detail.tsx
index 9080ee2c7d..359f6af88c 100644
--- a/web/app/components/tools/provider/detail.tsx
+++ b/web/app/components/tools/provider/detail.tsx
@@ -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 (
{
+ if (!open)
+ onHide()
+ }}
>
-
-
- {!!collection.description[language] && (
-
- )}
-
- {collection.type === CollectionType.custom && !isDetailLoading && (
-
- )}
- {collection.type === CollectionType.workflow && !isDetailLoading && customCollection && (
- <>
-
-
- >
- )}
-
-
- {isDetailLoading &&
}
- {!isDetailLoading && (
- <>
-
- {(collection.type === CollectionType.builtIn || collection.type === CollectionType.model) && isAuthed && (
-
- {t('detailPanel.actionNum', { ns: 'plugin', num: toolList.length, action: toolList.length > 1 ? 'actions' : 'action' })}
- {needAuth && (
+
+
+
+
+
+
+
+ {!!collection.description[language] && (
+
+ )}
+
+ {collection.type === CollectionType.custom && !isDetailLoading && (
+
+ )}
+ {collection.type === CollectionType.workflow && !isDetailLoading && customCollection && (
+ <>
+
- )}
-
- )}
- {(collection.type === CollectionType.builtIn || collection.type === CollectionType.model) && needAuth && !isAuthed && (
- <>
-
- {t('includeToolNum', { ns: 'tools', num: toolList.length, action: toolList.length > 1 ? 'actions' : 'action' }).toLocaleUpperCase()}
- ·
- {t('auth.setup', { ns: 'tools' }).toLocaleUpperCase()}
-
-
- >
- )}
- {(collection.type === CollectionType.custom) && (
-
- {t('includeToolNum', { ns: 'tools', num: toolList.length, action: toolList.length > 1 ? 'actions' : 'action' }).toLocaleUpperCase()}
-
- )}
- {(collection.type === CollectionType.workflow) && (
-
- {t('createTool.toolInput.title', { ns: 'tools' }).toLocaleUpperCase()}
-
- )}
-
-
- {collection.type !== CollectionType.workflow && toolList.map(tool => (
-
+ )}
+
+
+ {isDetailLoading &&
}
+ {!isDetailLoading && (
+ <>
+
+ {(collection.type === CollectionType.builtIn || collection.type === CollectionType.model) && isAuthed && (
+
+ {t('detailPanel.actionNum', { ns: 'plugin', num: toolList.length, action: toolList.length > 1 ? 'actions' : 'action' })}
+ {needAuth && (
+
+ )}
+
+ )}
+ {(collection.type === CollectionType.builtIn || collection.type === CollectionType.model) && needAuth && !isAuthed && (
+ <>
+
+ {t('includeToolNum', { ns: 'tools', num: toolList.length, action: toolList.length > 1 ? 'actions' : 'action' }).toLocaleUpperCase()}
+ ·
+ {t('auth.setup', { ns: 'tools' }).toLocaleUpperCase()}
+
+
+ >
+ )}
+ {(collection.type === CollectionType.custom) && (
+
+ {t('includeToolNum', { ns: 'tools', num: toolList.length, action: toolList.length > 1 ? 'actions' : 'action' }).toLocaleUpperCase()}
+
+ )}
+ {(collection.type === CollectionType.workflow) && (
+
+ {t('createTool.toolInput.title', { ns: 'tools' }).toLocaleUpperCase()}
+
+ )}
+
+
+ {collection.type !== CollectionType.workflow && toolList.map(tool => (
+
+ ))}
+ {collection.type === CollectionType.workflow && (customCollection as WorkflowToolProviderResponse)?.tool?.parameters.map(item => (
+
+
+ {item.name}
+ {item.type}
+ {item.required ? t('createTool.toolInput.required', { ns: 'tools' }) : ''}
+
+
{item.llm_description}
+
+ ))}
+
+ >
+ )}
+
+ {showSettingAuth && (
+ 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 => (
-
-
-
{item.name}
-
{item.type}
-
{item.required ? t('createTool.toolInput.required', { ns: 'tools' }) : ''}
+ )}
+ {isShowEditCollectionToolModal && (
+
setIsShowEditCustomCollectionModal(false)}
+ onEdit={doUpdateCustomToolCollection}
+ onRemove={onClickCustomToolDelete}
+ />
+ )}
+ {workflowToolDrawerOpen && (
+ setWorkflowToolDrawerOpen(false)}
+ onRemove={onClickWorkflowToolDelete}
+ onSave={updateWorkflowToolProvider}
+ />
+ )}
+ !open && setShowConfirmDelete(false)}>
+
+
+
+ {t('createTool.deleteToolConfirmTitle', { ns: 'tools' })}
+
+
+ {t('createTool.deleteToolConfirmContent', { ns: 'tools' })}
+
- {item.llm_description}
-
- ))}
+
+ {t('operation.cancel', { ns: 'common' })}
+
+ {t('operation.confirm', { ns: 'common' })}
+
+
+
+
- >
- )}
-
- {showSettingAuth && (
-
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 && (
- setIsShowEditCustomCollectionModal(false)}
- onEdit={doUpdateCustomToolCollection}
- onRemove={onClickCustomToolDelete}
- />
- )}
- {workflowToolDrawerOpen && (
- setWorkflowToolDrawerOpen(false)}
- onRemove={onClickWorkflowToolDelete}
- onSave={updateWorkflowToolProvider}
- />
- )}
- !open && setShowConfirmDelete(false)}>
-
-
-
- {t('createTool.deleteToolConfirmTitle', { ns: 'tools' })}
-
-
- {t('createTool.deleteToolConfirmContent', { ns: 'tools' })}
-
-
-
- {t('operation.cancel', { ns: 'common' })}
-
- {t('operation.confirm', { ns: 'common' })}
-
-
-
-
-
+
+
+
+
)
}
diff --git a/web/app/components/workflow/nodes/knowledge-retrieval/components/dataset-item.tsx b/web/app/components/workflow/nodes/knowledge-retrieval/components/dataset-item.tsx
index 90e9caae9d..8182a5ab87 100644
--- a/web/app/components/workflow/nodes/knowledge-retrieval/components/dataset-item.tsx
+++ b/web/app/components/workflow/nodes/knowledge-retrieval/components/dataset-item.tsx
@@ -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
= ({
}
{isShowSettingsModal && (
-
-
+ {
+ if (!open)
+ hideSettingsModal()
+ }}
+ >
+
+
+
+
+
+
+
+
+
+
)}