From e48d7bb097159629923730bc5da7496d4d438ca7 Mon Sep 17 00:00:00 2001 From: Coding On Star <447357187@qq.com> Date: Sun, 10 May 2026 00:24:45 +0800 Subject: [PATCH] refactor(web): migrate drawer components to dify-ui and remove legacy drawer implementation (#35982) Co-authored-by: CodingOnStar --- eslint-suppressions.json | 56 -- .../tools/tool-provider-detail-flow.test.tsx | 17 +- .../agent-tools/setting-built-in-tool.tsx | 196 ++--- .../app/configuration/configuration-view.tsx | 60 +- .../card-item/__tests__/index.spec.tsx | 8 +- .../dataset-config/card-item/index.tsx | 42 +- .../app/log/__tests__/list.spec.tsx | 32 +- web/app/components/app/log/list.tsx | 50 +- web/app/components/app/workflow-log/list.tsx | 40 +- .../base/drawer/__tests__/index.spec.tsx | 676 ------------------ .../components/base/drawer/index.stories.tsx | 114 --- web/app/components/base/drawer/index.tsx | 128 ---- .../__tests__/index.spec.tsx | 45 +- .../float-right-container/index.stories.tsx | 1 - .../base/float-right-container/index.tsx | 72 +- .../step-two/components/preview-panel.tsx | 2 +- .../datasets/documents/detail/index.tsx | 2 +- .../components/datasets/hit-testing/index.tsx | 52 +- .../plugin-detail-panel/endpoint-modal.tsx | 122 ++-- .../plugins/plugin-detail-panel/index.tsx | 78 +- .../plugin-detail-panel/strategy-detail.tsx | 178 ++--- .../trigger/event-detail-drawer.tsx | 158 ++-- .../detail/__tests__/provider-detail.spec.tsx | 11 +- .../tools/mcp/detail/provider-detail.tsx | 50 +- .../tools/provider/__tests__/detail.spec.tsx | 5 - web/app/components/tools/provider/detail.tsx | 396 +++++----- .../components/dataset-item.tsx | 39 +- 27 files changed, 946 insertions(+), 1684 deletions(-) delete mode 100644 web/app/components/base/drawer/__tests__/index.spec.tsx delete mode 100644 web/app/components/base/drawer/index.stories.tsx delete mode 100644 web/app/components/base/drawer/index.tsx 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' })}
- -
-
-
-
{ - 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 - ? ( - - {t('howToGet', { ns: 'tools' })} - - - ) - : null} - /> -
-
-
- - -
-
-
- + + + + + +
+
+
{t('detailPanel.endpointModalTitle', { ns: 'plugin' })}
+ + + +
+
{t('detailPanel.endpointModalDesc', { ns: 'plugin' })}
+ +
+
+
+ { + 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 + ? ( + + {t('howToGet', { ns: 'tools' })} + + + ) + : null} + /> +
+
+
+ + +
+
+
+
+
+
+
) } 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() + }} > -
-
-
- -
-
- - </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> ) } 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<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>