refactor: replace deprecated Confirm component with new ConfirmDialog across multiple files

This commit is contained in:
CodingOnStar
2026-04-13 17:27:24 +08:00
parent 11c518478e
commit ff4521504d
63 changed files with 548 additions and 1155 deletions

View File

@ -10,7 +10,7 @@
* - Access mode icons
*/
import type { App } from '@/types/app'
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
import { fireEvent, render, screen, waitFor, within } from '@testing-library/react'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import AppCard from '@/app/components/apps/app-card'
import { AccessMode } from '@/models/access-control'
@ -22,26 +22,7 @@ let mockSystemFeatures = {
branding: { enabled: false },
webapp_auth: { enabled: false },
}
const toastMocks = vi.hoisted(() => ({
mockNotify: vi.fn(),
dismiss: vi.fn(),
update: vi.fn(),
promise: vi.fn(),
}))
const mockRouterPush = vi.fn()
vi.mock('@/app/components/base/ui/toast', () => ({
toast: {
success: (message: string, options?: Record<string, unknown>) => toastMocks.mockNotify({ type: 'success', message, ...options }),
error: (message: string, options?: Record<string, unknown>) => toastMocks.mockNotify({ type: 'error', message, ...options }),
warning: (message: string, options?: Record<string, unknown>) => toastMocks.mockNotify({ type: 'warning', message, ...options }),
info: (message: string, options?: Record<string, unknown>) => toastMocks.mockNotify({ type: 'info', message, ...options }),
dismiss: toastMocks.dismiss,
update: toastMocks.update,
promise: toastMocks.promise,
},
}))
const mockOnPlanInfoChanged = vi.fn()
const mockDeleteAppMutation = vi.fn().mockResolvedValue(undefined)
let mockDeleteMutationPending = false
@ -207,20 +188,6 @@ vi.mock('@/app/components/app/switch-app-modal', () => ({
},
}))
vi.mock('@/app/components/base/confirm', () => ({
default: ({ isShow, onConfirm, onCancel, title }: Record<string, unknown>) => {
if (!isShow)
return null
return (
<div data-testid="confirm-delete-modal">
<span>{title as string}</span>
<button data-testid="confirm-delete" onClick={onConfirm as () => void}>Delete</button>
<button data-testid="cancel-delete" onClick={onCancel as () => void}>Cancel</button>
</div>
)
},
}))
vi.mock('@/app/components/workflow/dsl-export-confirm-modal', () => ({
default: ({ onConfirm, onClose }: Record<string, unknown>) => (
<div data-testid="dsl-export-confirm-modal">
@ -342,14 +309,15 @@ describe('App Card Operations Flow', () => {
fireEvent.click(deleteBtn)
})
const confirmBtn = screen.queryByTestId('confirm-delete')
if (confirmBtn) {
fireEvent.click(confirmBtn)
const dialog = await screen.findByRole('alertdialog')
fireEvent.change(within(dialog).getByRole('textbox', { name: 'deleteAppConfirmInputLabel' }), {
target: { value: 'Deletable App' },
})
fireEvent.click(within(dialog).getByRole('button', { name: 'common.operation.confirm' }))
await waitFor(() => {
expect(mockDeleteAppMutation).toHaveBeenCalledWith('app-to-delete')
})
}
await waitFor(() => {
expect(mockDeleteAppMutation).toHaveBeenCalledWith('app-to-delete')
})
}
})
})

View File

@ -7,7 +7,7 @@ import type { Collection } from '@/app/components/tools/types'
* Verifies that different provider types render correctly and
* handle auth/edit/delete flows.
*/
import { cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react'
import { cleanup, fireEvent, render, screen, waitFor, within } from '@testing-library/react'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { CollectionType } from '@/app/components/tools/types'
@ -133,36 +133,6 @@ vi.mock('@/app/components/base/drawer', () => ({
),
}))
vi.mock('@/app/components/base/confirm', () => ({
default: ({ title, isShow, onConfirm, onCancel }: {
title: string
content: string
isShow: boolean
onConfirm: () => void
onCancel: () => void
}) => (
isShow
? (
<div data-testid="confirm-dialog">
<span>{title}</span>
<button data-testid="confirm-ok" onClick={onConfirm}>Confirm</button>
<button data-testid="confirm-cancel" onClick={onCancel}>Cancel</button>
</div>
)
: null
),
}))
vi.mock('@/app/components/base/ui/toast', () => ({
default: { notify: vi.fn() },
toast: {
success: vi.fn(),
error: vi.fn(),
warning: vi.fn(),
info: vi.fn(),
},
}))
vi.mock('@/app/components/base/icons/src/vender/line/general', () => ({
LinkExternal02: () => <span data-testid="link-icon" />,
Settings01: () => <span data-testid="settings-icon" />,
@ -464,12 +434,10 @@ describe('Tool Provider Detail Flow Integration', () => {
})
fireEvent.click(screen.getByTestId('custom-modal-remove'))
await waitFor(() => {
expect(screen.getByTestId('confirm-dialog')).toBeInTheDocument()
expect(screen.getByText('Delete Tool')).toBeInTheDocument()
})
const dialog = await screen.findByRole('alertdialog')
expect(within(dialog).getByText('Delete Tool')).toBeInTheDocument()
fireEvent.click(screen.getByTestId('confirm-ok'))
fireEvent.click(within(dialog).getAllByRole('button').at(-1)!)
await waitFor(() => {
expect(mockRemoveCustomCollection).toHaveBeenCalledWith('test_collection')
expect(mockOnRefreshData).toHaveBeenCalled()
@ -526,11 +494,9 @@ describe('Tool Provider Detail Flow Integration', () => {
})
fireEvent.click(screen.getByTestId('wf-modal-remove'))
await waitFor(() => {
expect(screen.getByTestId('confirm-dialog')).toBeInTheDocument()
})
const dialog = await screen.findByRole('alertdialog')
fireEvent.click(screen.getByTestId('confirm-ok'))
fireEvent.click(within(dialog).getAllByRole('button').at(-1)!)
await waitFor(() => {
expect(mockDeleteWorkflowTool).toHaveBeenCalledWith('test-collection')
expect(mockOnRefreshData).toHaveBeenCalled()

View File

@ -6,7 +6,6 @@ import * as React from 'react'
import { useCallback, useState } from 'react'
import { useTranslation } from 'react-i18next'
import Button from '@/app/components/base/button'
import Confirm from '@/app/components/base/confirm'
import Divider from '@/app/components/base/divider'
import { LinkExternal02 } from '@/app/components/base/icons/src/vender/line/general'
import { Lock01 } from '@/app/components/base/icons/src/vender/solid/security'
@ -14,6 +13,7 @@ import {
PortalToFollowElem,
PortalToFollowElemContent,
} from '@/app/components/base/portal-to-follow-elem'
import Confirm from '@/app/components/base/ui/confirm-dialog'
import { toast } from '@/app/components/base/ui/toast'
import { addTracingConfig, removeTracingConfig, updateTracingConfig } from '@/service/apps'
import { docURL } from './config'

View File

@ -1,5 +1,5 @@
import type { App, AppSSO } from '@/types/app'
import { act, render, screen, waitFor } from '@testing-library/react'
import { act, render, screen, waitFor, within } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import * as React from 'react'
import { AppModeEnum } from '@/types/app'
@ -36,24 +36,6 @@ vi.mock('@/app/components/app/duplicate-modal', () => ({
),
}))
vi.mock('@/app/components/base/confirm', () => ({
default: ({ isShow, title, onConfirm, onCancel }: {
isShow: boolean
title: string
onConfirm: () => void
onCancel: () => void
}) => (
isShow
? (
<div data-testid="confirm-modal" data-title={title}>
<button type="button" onClick={onConfirm}>Confirm</button>
<button type="button" onClick={onCancel}>Cancel</button>
</div>
)
: null
),
}))
vi.mock('@/app/components/workflow/update-dsl-modal', () => ({
default: ({ onCancel, onBackup }: { onCancel: () => void, onBackup: () => void }) => (
<div data-testid="import-dsl-modal">
@ -113,7 +95,7 @@ describe('AppInfoModals', () => {
render(<AppInfoModals {...defaultProps} activeModal={null} />)
})
expect(screen.queryByTestId('switch-modal')).not.toBeInTheDocument()
expect(screen.queryByTestId('confirm-modal')).not.toBeInTheDocument()
expect(screen.queryByRole('alertdialog')).not.toBeInTheDocument()
})
it('should render SwitchAppModal when activeModal is switch', async () => {
@ -148,9 +130,9 @@ describe('AppInfoModals', () => {
render(<AppInfoModals {...defaultProps} activeModal="delete" />)
})
await waitFor(() => {
const confirm = screen.getByTestId('confirm-modal')
expect(confirm).toBeInTheDocument()
expect(confirm).toHaveAttribute('data-title', 'app.deleteAppConfirmTitle')
const dialog = screen.getByRole('alertdialog')
expect(dialog).toBeInTheDocument()
expect(within(dialog).getByText('app.deleteAppConfirmTitle')).toBeInTheDocument()
})
})
@ -168,9 +150,9 @@ describe('AppInfoModals', () => {
render(<AppInfoModals {...defaultProps} activeModal="exportWarning" />)
})
await waitFor(() => {
const confirm = screen.getByTestId('confirm-modal')
expect(confirm).toBeInTheDocument()
expect(confirm).toHaveAttribute('data-title', 'workflow.sidebar.exportWarning')
const dialog = screen.getByRole('alertdialog')
expect(dialog).toBeInTheDocument()
expect(within(dialog).getByText('workflow.sidebar.exportWarning')).toBeInTheDocument()
})
})
@ -202,8 +184,8 @@ describe('AppInfoModals', () => {
render(<AppInfoModals {...defaultProps} activeModal="delete" />)
})
await waitFor(() => expect(screen.getByText('Cancel')).toBeInTheDocument())
await user.click(screen.getByText('Cancel'))
const dialog = await screen.findByRole('alertdialog')
await user.click(within(dialog).getByRole('button', { name: 'common.operation.cancel' }))
expect(defaultProps.closeModal).toHaveBeenCalledTimes(1)
})
@ -214,8 +196,9 @@ describe('AppInfoModals', () => {
render(<AppInfoModals {...defaultProps} activeModal="delete" />)
})
await waitFor(() => expect(screen.getByText('Confirm')).toBeInTheDocument())
await user.click(screen.getByText('Confirm'))
await user.type(screen.getByRole('textbox'), 'Test App')
const dialog = screen.getByRole('alertdialog')
await user.click(within(dialog).getAllByRole('button').at(-1)!)
expect(defaultProps.onConfirmDelete).toHaveBeenCalledTimes(1)
})
@ -226,8 +209,8 @@ describe('AppInfoModals', () => {
render(<AppInfoModals {...defaultProps} activeModal="exportWarning" />)
})
await waitFor(() => expect(screen.getByText('Confirm')).toBeInTheDocument())
await user.click(screen.getByText('Confirm'))
const dialog = await screen.findByRole('alertdialog')
await user.click(within(dialog).getAllByRole('button').at(-1)!)
expect(defaultProps.handleConfirmExport).toHaveBeenCalledTimes(1)
})

View File

@ -11,7 +11,7 @@ import dynamic from '@/next/dynamic'
const SwitchAppModal = dynamic(() => import('@/app/components/app/switch-app-modal'), { ssr: false })
const CreateAppModal = dynamic(() => import('@/app/components/explore/create-app-modal'), { ssr: false })
const DuplicateAppModal = dynamic(() => import('@/app/components/app/duplicate-modal'), { ssr: false })
const Confirm = dynamic(() => import('@/app/components/base/confirm'), { ssr: false })
const Confirm = dynamic(() => import('@/app/components/base/ui/confirm-dialog'), { ssr: false })
const UpdateDSLModal = dynamic(() => import('@/app/components/workflow/update-dsl-modal'), { ssr: false })
const DSLExportConfirmModal = dynamic(() => import('@/app/components/workflow/dsl-export-confirm-modal'), { ssr: false })

View File

@ -1,7 +1,8 @@
import type { DataSet } from '@/models/datasets'
import { render, screen, waitFor } from '@testing-library/react'
import { render, screen, waitFor, within } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import * as React from 'react'
import * as toastModule from '@/app/components/base/ui/toast'
import {
ChunkingMode,
DatasetPermission,
@ -112,10 +113,6 @@ vi.mock('@/service/datasets', () => ({
deleteDataset: (...args: unknown[]) => mockDeleteDataset(...args),
}))
vi.mock('@/app/components/base/ui/toast', () => ({
toast: (...args: unknown[]) => mockToast(...args),
}))
vi.mock('@/app/components/datasets/rename-modal', () => ({
default: ({
show,
@ -137,33 +134,6 @@ vi.mock('@/app/components/datasets/rename-modal', () => ({
},
}))
vi.mock('@/app/components/base/confirm', () => ({
default: ({
isShow,
onConfirm,
onCancel,
title,
content,
}: {
isShow: boolean
onConfirm: () => void
onCancel: () => void
title: string
content: string
}) => {
if (!isShow)
return null
return (
<div data-testid="confirm-dialog">
<span>{title}</span>
<span>{content}</span>
<button type="button" onClick={onConfirm}>confirm</button>
<button type="button" onClick={onCancel}>cancel</button>
</div>
)
},
}))
vi.mock('@/app/components/base/portal-to-follow-elem', () => ({
PortalToFollowElem: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
PortalToFollowElemTrigger: ({ children, onClick }: { children: React.ReactNode, onClick?: () => void }) => (
@ -172,6 +142,11 @@ vi.mock('@/app/components/base/portal-to-follow-elem', () => ({
PortalToFollowElemContent: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
}))
vi.spyOn(toastModule, 'toast').mockImplementation((...args) => {
mockToast(...args)
return 'toast-id'
})
describe('Dropdown callback coverage', () => {
beforeEach(() => {
vi.clearAllMocks()
@ -220,14 +195,11 @@ describe('Dropdown callback coverage', () => {
await user.click(screen.getByTestId('portal-trigger'))
await user.click(screen.getByText('common.operation.delete'))
await waitFor(() => {
expect(screen.getByTestId('confirm-dialog')).toBeInTheDocument()
})
await user.click(screen.getByText('cancel'))
const dialog = await screen.findByRole('alertdialog')
await user.click(within(dialog).getByRole('button', { name: 'common.operation.cancel' }))
await waitFor(() => {
expect(screen.queryByTestId('confirm-dialog')).not.toBeInTheDocument()
expect(screen.queryByRole('alertdialog')).not.toBeInTheDocument()
})
})
@ -273,6 +245,6 @@ describe('Dropdown callback coverage', () => {
await waitFor(() => {
expect(mockToast).toHaveBeenCalledWith('check failed', { type: 'error' })
})
expect(screen.queryByTestId('confirm-dialog')).not.toBeInTheDocument()
expect(screen.queryByRole('alertdialog')).not.toBeInTheDocument()
})
})

View File

@ -14,8 +14,8 @@ import { useExportPipelineDSL } from '@/service/use-pipeline'
import { cn } from '@/utils/classnames'
import { downloadBlob } from '@/utils/download'
import ActionButton from '../../base/action-button'
import Confirm from '../../base/confirm'
import { PortalToFollowElem, PortalToFollowElemContent, PortalToFollowElemTrigger } from '../../base/portal-to-follow-elem'
import Confirm from '../../base/ui/confirm-dialog'
import RenameDatasetModal from '../../datasets/rename-modal'
import Menu from './menu'

View File

@ -3,8 +3,8 @@ import { RiDeleteBinLine } from '@remixicon/react'
import { useBoolean } from 'ahooks'
import * as React from 'react'
import { useTranslation } from 'react-i18next'
import Confirm from '@/app/components/base/confirm'
import Divider from '@/app/components/base/divider'
import Confirm from '@/app/components/base/ui/confirm-dialog'
import { cn } from '@/utils/classnames'
const i18nPrefix = 'batchAction'

View File

@ -3,7 +3,7 @@
import type { FC } from 'react'
import * as React from 'react'
import { useTranslation } from 'react-i18next'
import Confirm from '@/app/components/base/confirm'
import Confirm from '@/app/components/base/ui/confirm-dialog'
type Props = {
isShow: boolean

View File

@ -3,9 +3,9 @@ import type { FC } from 'react'
import * as React from 'react'
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import Confirm from '@/app/components/base/confirm'
import Drawer from '@/app/components/base/drawer-plus'
import { MessageCheckRemove } from '@/app/components/base/icons/src/vender/line/communication'
import Confirm from '@/app/components/base/ui/confirm-dialog'
import { toast } from '@/app/components/base/ui/toast'
import AnnotationFull from '@/app/components/billing/annotation-full'
import { useProviderContext } from '@/context/provider-context'

View File

@ -2,7 +2,7 @@
import type { FC } from 'react'
import * as React from 'react'
import { useTranslation } from 'react-i18next'
import Confirm from '@/app/components/base/confirm'
import Confirm from '@/app/components/base/ui/confirm-dialog'
type Props = {
isShow: boolean

View File

@ -5,11 +5,11 @@ import * as React from 'react'
import { useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import Badge from '@/app/components/base/badge'
import Confirm from '@/app/components/base/confirm'
import Drawer from '@/app/components/base/drawer-plus'
import { MessageCheckRemove } from '@/app/components/base/icons/src/vender/line/communication'
import Pagination from '@/app/components/base/pagination'
import TabSlider from '@/app/components/base/tab-slider-plain'
import Confirm from '@/app/components/base/ui/confirm-dialog'
import { APP_PAGE_LIMIT } from '@/config'
import useTimestamp from '@/hooks/use-timestamp'
import { fetchHitHistoryList } from '@/service/annotation'

View File

@ -6,9 +6,9 @@ import * as React from 'react'
import { useCallback, useState } from 'react'
import { useTranslation } from 'react-i18next'
import AppPublisher from '@/app/components/app/app-publisher'
import Confirm from '@/app/components/base/confirm'
import { useFeatures, useFeaturesStore } from '@/app/components/base/features/hooks'
import { FILE_EXTS } from '@/app/components/base/prompt-editor/constants'
import Confirm from '@/app/components/base/ui/confirm-dialog'
import { SupportUploadFileTypes } from '@/app/components/workflow/types'
import { Resolution } from '@/types/app'

View File

@ -11,8 +11,8 @@ import { useCallback, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { ReactSortable } from 'react-sortablejs'
import { useContext } from 'use-context-selector'
import Confirm from '@/app/components/base/confirm'
import Tooltip from '@/app/components/base/tooltip'
import Confirm from '@/app/components/base/ui/confirm-dialog'
import { toast } from '@/app/components/base/ui/toast'
import { InputVarType } from '@/app/components/workflow/types'
import ConfigContext from '@/context/debug-configuration'

View File

@ -20,10 +20,10 @@ import * as React from 'react'
import { useCallback, useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import Button from '@/app/components/base/button'
import Confirm from '@/app/components/base/confirm'
import { Generator } from '@/app/components/base/icons/src/vender/other'
import Loading from '@/app/components/base/loading'
import Modal from '@/app/components/base/modal'
import Confirm from '@/app/components/base/ui/confirm-dialog'
import { toast } from '@/app/components/base/ui/toast'
import { ModelTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'

View File

@ -11,10 +11,10 @@ import * as React from 'react'
import { useCallback, useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import Button from '@/app/components/base/button'
import Confirm from '@/app/components/base/confirm'
import { Generator } from '@/app/components/base/icons/src/vender/other'
import Loading from '@/app/components/base/loading'
import Modal from '@/app/components/base/modal'
import Confirm from '@/app/components/base/ui/confirm-dialog'
import { toast } from '@/app/components/base/ui/toast'
import { ModelTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
import { useModelListAndDefaultModelAndCurrentProviderAndModel } from '@/app/components/header/account-setting/model-provider-page/hooks'

View File

@ -9,10 +9,10 @@ import { useStore as useAppStore } from '@/app/components/app/store'
import AppIcon from '@/app/components/base/app-icon'
import Button from '@/app/components/base/button'
import Checkbox from '@/app/components/base/checkbox'
import Confirm from '@/app/components/base/confirm'
import { AlertTriangle } from '@/app/components/base/icons/src/vender/solid/alertsAndFeedback'
import Input from '@/app/components/base/input'
import Modal from '@/app/components/base/modal'
import Confirm from '@/app/components/base/ui/confirm-dialog'
import { toast } from '@/app/components/base/ui/toast'
import AppsFull from '@/app/components/billing/apps-full-in-dialog'
import { NEED_REFRESH_APP_LIST_KEY } from '@/config'

View File

@ -5,7 +5,7 @@ import ActionButton from '@/app/components/base/action-button'
import AppIcon from '@/app/components/base/app-icon'
import InputsFormContent from '@/app/components/base/chat/chat-with-history/inputs-form/content'
import RenameModal from '@/app/components/base/chat/chat-with-history/sidebar/rename-modal'
import Confirm from '@/app/components/base/confirm'
import Confirm from '@/app/components/base/ui/confirm-dialog'
import { useChatWithHistoryContext } from './context'
import MobileOperationDropdown from './header/mobile-operation-dropdown'
import Operation from './header/operation'

View File

@ -10,8 +10,8 @@ import ActionButton, { ActionButtonState } from '@/app/components/base/action-bu
import AppIcon from '@/app/components/base/app-icon'
import ViewFormDropdown from '@/app/components/base/chat/chat-with-history/inputs-form/view-form-dropdown'
import RenameModal from '@/app/components/base/chat/chat-with-history/sidebar/rename-modal'
import Confirm from '@/app/components/base/confirm'
import Tooltip from '@/app/components/base/tooltip'
import Confirm from '@/app/components/base/ui/confirm-dialog'
import { cn } from '@/utils/classnames'
import {
useChatWithHistoryContext,

View File

@ -106,22 +106,6 @@ vi.mock('@/app/components/base/modal', () => ({
},
}))
// Mock Confirm
vi.mock('@/app/components/base/confirm', () => ({
default: ({ onCancel, onConfirm, title, content, isShow }: { onCancel: () => void, onConfirm: () => void, title: string, content?: React.ReactNode, isShow: boolean }) => {
if (!isShow)
return null
return (
<div data-testid="confirm-dialog">
<div data-testid="confirm-title">{title}</div>
<button data-testid="confirm-cancel" onClick={onCancel}>Cancel</button>
<div data-testid="confirm-content">{content}</div>
<button data-testid="confirm-confirm" onClick={onConfirm}>Confirm</button>
</div>
)
},
}))
describe('Sidebar Index', () => {
const mockContextValue = {
isInstalledApp: false,
@ -475,8 +459,9 @@ describe('Sidebar Index', () => {
render(<Sidebar />)
await user.click(screen.getByTestId('delete-1'))
expect(screen.getByTestId('confirm-dialog')).toBeInTheDocument()
expect(screen.getByTestId('confirm-title')).toBeInTheDocument()
const dialog = screen.getByRole('alertdialog')
expect(dialog).toBeInTheDocument()
expect(within(dialog).getByText('share.chat.deleteConversation.title')).toBeInTheDocument()
})
it('should call handleDeleteConversation when confirm is clicked', async () => {
@ -490,7 +475,8 @@ describe('Sidebar Index', () => {
render(<Sidebar />)
await user.click(screen.getByTestId('delete-1'))
await user.click(screen.getByTestId('confirm-confirm'))
const dialog = screen.getByRole('alertdialog')
await user.click(within(dialog).getAllByRole('button').at(-1)!)
expect(handleDeleteConversation).toHaveBeenCalledWith('1', expect.objectContaining({
onSuccess: expect.any(Function),
@ -502,11 +488,12 @@ describe('Sidebar Index', () => {
render(<Sidebar />)
await user.click(screen.getByTestId('delete-1'))
expect(screen.getByTestId('confirm-dialog')).toBeInTheDocument()
const dialog = screen.getByRole('alertdialog')
expect(dialog).toBeInTheDocument()
await user.click(screen.getByTestId('confirm-cancel'))
await user.click(within(dialog).getByRole('button', { name: 'common.operation.cancel' }))
await waitFor(() => {
expect(screen.queryByTestId('confirm-dialog')).not.toBeInTheDocument()
expect(screen.queryByRole('alertdialog')).not.toBeInTheDocument()
})
})
@ -525,7 +512,8 @@ describe('Sidebar Index', () => {
render(<Sidebar />)
await user.click(screen.getByTestId('delete-1'))
await user.click(screen.getByTestId('confirm-confirm'))
const dialog = screen.getByRole('alertdialog')
await user.click(within(dialog).getAllByRole('button').at(-1)!)
expect(handleDeleteConversation).toHaveBeenCalledWith('1', expect.any(Object))
})
@ -837,7 +825,8 @@ describe('Sidebar Index', () => {
// Delete it
await user.click(screen.getByTestId('delete-1'))
await user.click(screen.getByTestId('confirm-confirm'))
const dialog = await screen.findByRole('alertdialog')
await user.click(within(dialog).getByRole('button', { name: 'common.operation.confirm' }))
expect(handleDeleteConversation).toHaveBeenCalled()
})
@ -901,8 +890,9 @@ describe('Sidebar Index', () => {
try {
render(<Sidebar />)
await user.click(screen.getByTestId('delete-1'))
expect(screen.getByTestId('confirm-dialog')).toBeInTheDocument()
expect(screen.getByTestId('confirm-content')).toBeEmptyDOMElement()
const dialog = screen.getByRole('alertdialog')
expect(dialog).toBeInTheDocument()
expect(dialog).not.toHaveTextContent('share.chat.deleteConversation.content')
}
finally {
useTranslationSpy.mockRestore()

View File

@ -14,8 +14,8 @@ import AppIcon from '@/app/components/base/app-icon'
import Button from '@/app/components/base/button'
import List from '@/app/components/base/chat/chat-with-history/sidebar/list'
import RenameModal from '@/app/components/base/chat/chat-with-history/sidebar/rename-modal'
import Confirm from '@/app/components/base/confirm'
import DifyLogo from '@/app/components/base/logo/dify-logo'
import Confirm from '@/app/components/base/ui/confirm-dialog'
import MenuDropdown from '@/app/components/share/text-generation/menu-dropdown'
import { useGlobalPublicStore } from '@/context/global-public-context'
import { cn } from '@/utils/classnames'

View File

@ -1,117 +0,0 @@
import { act, fireEvent, render, screen } from '@testing-library/react'
import Confirm from '..'
vi.mock('react-dom', async () => {
const actual = await vi.importActual<typeof import('react-dom')>('react-dom')
return {
...actual,
createPortal: (children: React.ReactNode) => children,
}
})
const onCancel = vi.fn()
const onConfirm = vi.fn()
describe('Confirm Component', () => {
beforeEach(() => {
vi.clearAllMocks()
})
describe('Rendering', () => {
it('renders confirm correctly', () => {
render(<Confirm isShow={true} title="test title" onCancel={onCancel} onConfirm={onConfirm} />)
expect(screen.getByText('test title')).toBeInTheDocument()
})
it('does not render on isShow false', () => {
const { container } = render(<Confirm isShow={false} title="test title" onCancel={onCancel} onConfirm={onConfirm} />)
expect(container.firstChild).toBeNull()
})
it('hides after delay when isShow changes to false', () => {
vi.useFakeTimers()
const { rerender } = render(<Confirm isShow={true} title="test title" onCancel={onCancel} onConfirm={onConfirm} />)
expect(screen.getByText('test title')).toBeInTheDocument()
rerender(<Confirm isShow={false} title="test title" onCancel={onCancel} onConfirm={onConfirm} />)
act(() => {
vi.advanceTimersByTime(200)
})
expect(screen.queryByText('test title')).not.toBeInTheDocument()
vi.useRealTimers()
})
it('renders content when provided', () => {
render(<Confirm isShow={true} title="title" content="some description" onCancel={onCancel} onConfirm={onConfirm} />)
expect(screen.getByText('some description')).toBeInTheDocument()
})
})
describe('Props', () => {
it('showCancel prop works', () => {
render(<Confirm isShow={true} title="test title" onCancel={onCancel} onConfirm={onConfirm} showCancel={false} />)
expect(screen.getByRole('button', { name: 'common.operation.confirm' })).toBeInTheDocument()
expect(screen.queryByRole('button', { name: 'common.operation.cancel' })).not.toBeInTheDocument()
})
it('showConfirm prop works', () => {
render(<Confirm isShow={true} title="test title" onCancel={onCancel} onConfirm={onConfirm} showConfirm={false} />)
expect(screen.getByRole('button', { name: 'common.operation.cancel' })).toBeInTheDocument()
expect(screen.queryByRole('button', { name: 'common.operation.confirm' })).not.toBeInTheDocument()
})
it('renders custom confirm and cancel text', () => {
render(<Confirm isShow={true} title="title" confirmText="Yes" cancelText="No" onCancel={onCancel} onConfirm={onConfirm} />)
expect(screen.getByRole('button', { name: 'Yes' })).toBeInTheDocument()
expect(screen.getByRole('button', { name: 'No' })).toBeInTheDocument()
})
it('disables confirm button when isDisabled is true', () => {
render(<Confirm isShow={true} title="title" isDisabled={true} onCancel={onCancel} onConfirm={onConfirm} />)
expect(screen.getByRole('button', { name: 'common.operation.confirm' })).toBeDisabled()
})
})
describe('User Interactions', () => {
it('clickAway is handled properly', () => {
render(<Confirm isShow={true} title="test title" onCancel={onCancel} onConfirm={onConfirm} />)
const overlay = screen.getByTestId('confirm-overlay') as HTMLElement
expect(overlay).toBeTruthy()
fireEvent.mouseDown(overlay)
expect(onCancel).toHaveBeenCalledTimes(1)
})
it('overlay click stops propagation', () => {
render(<Confirm isShow={true} title="test title" onCancel={onCancel} onConfirm={onConfirm} />)
const overlay = screen.getByTestId('confirm-overlay') as HTMLElement
const clickEvent = new MouseEvent('click', { bubbles: true, cancelable: true })
const preventDefaultSpy = vi.spyOn(clickEvent, 'preventDefault')
const stopPropagationSpy = vi.spyOn(clickEvent, 'stopPropagation')
overlay.dispatchEvent(clickEvent)
expect(preventDefaultSpy).toHaveBeenCalled()
expect(stopPropagationSpy).toHaveBeenCalled()
})
it('does not close on click away when maskClosable is false', () => {
render(<Confirm isShow={true} title="test title" maskClosable={false} onCancel={onCancel} onConfirm={onConfirm} />)
const overlay = screen.getByTestId('confirm-overlay') as HTMLElement
fireEvent.mouseDown(overlay)
expect(onCancel).not.toHaveBeenCalled()
})
it('escape keyboard event works', () => {
render(<Confirm isShow={true} title="test title" onCancel={onCancel} onConfirm={onConfirm} />)
fireEvent.keyDown(document, { key: 'Escape' })
expect(onCancel).toHaveBeenCalledTimes(1)
expect(onConfirm).not.toHaveBeenCalled()
})
it('Enter keyboard event works', () => {
render(<Confirm isShow={true} title="test title" onCancel={onCancel} onConfirm={onConfirm} />)
fireEvent.keyDown(document, { key: 'Enter' })
expect(onConfirm).toHaveBeenCalledTimes(1)
expect(onCancel).not.toHaveBeenCalled()
})
})
})

View File

@ -1,216 +0,0 @@
import type { Meta, StoryObj } from '@storybook/nextjs-vite'
import { useState } from 'react'
import Confirm from '.'
import Button from '../button'
const meta = {
title: 'Base/Feedback/Confirm',
component: Confirm,
parameters: {
layout: 'centered',
docs: {
description: {
component: 'Confirmation dialog component that supports warning and info types, with customizable button text and behavior.',
},
},
},
tags: ['autodocs'],
argTypes: {
type: {
control: 'select',
options: ['info', 'warning'],
description: 'Dialog type',
},
isShow: {
control: 'boolean',
description: 'Whether to show the dialog',
},
title: {
control: 'text',
description: 'Dialog title',
},
content: {
control: 'text',
description: 'Dialog content',
},
confirmText: {
control: 'text',
description: 'Confirm button text',
},
cancelText: {
control: 'text',
description: 'Cancel button text',
},
isLoading: {
control: 'boolean',
description: 'Confirm button loading state',
},
isDisabled: {
control: 'boolean',
description: 'Confirm button disabled state',
},
showConfirm: {
control: 'boolean',
description: 'Whether to show confirm button',
},
showCancel: {
control: 'boolean',
description: 'Whether to show cancel button',
},
maskClosable: {
control: 'boolean',
description: 'Whether clicking mask closes dialog',
},
},
args: {
onConfirm: () => {
console.log('✅ User clicked confirm')
},
onCancel: () => {
console.log('❌ User clicked cancel')
},
},
} satisfies Meta<typeof Confirm>
export default meta
type Story = StoryObj<typeof meta>
// Interactive demo wrapper
const ConfirmDemo = (args: any) => {
const [isShow, setIsShow] = useState(false)
return (
<div>
<Button variant="primary" onClick={() => setIsShow(true)}>
Open Dialog
</Button>
<Confirm
{...args}
isShow={isShow}
onConfirm={() => {
console.log('✅ User clicked confirm')
setIsShow(false)
}}
onCancel={() => {
console.log('❌ User clicked cancel')
setIsShow(false)
}}
/>
</div>
)
}
// Basic warning dialog - Delete action
export const WarningDialog: Story = {
render: args => <ConfirmDemo {...args} />,
args: {
type: 'warning',
title: 'Delete Confirmation',
content: 'Are you sure you want to delete this project? This action cannot be undone.',
isShow: false,
},
}
// Info dialog
export const InfoDialog: Story = {
render: args => <ConfirmDemo {...args} />,
args: {
type: 'info',
title: 'Notice',
content: 'Your changes have been saved. Do you want to proceed to the next step?',
isShow: false,
},
}
// Custom button text
export const CustomButtonText: Story = {
render: args => <ConfirmDemo {...args} />,
args: {
type: 'warning',
title: 'Exit Editor',
content: 'You have unsaved changes. Are you sure you want to exit?',
confirmText: 'Discard Changes',
cancelText: 'Continue Editing',
isShow: false,
},
}
// Loading state
export const LoadingState: Story = {
render: args => <ConfirmDemo {...args} />,
args: {
type: 'warning',
title: 'Deleting...',
content: 'Please wait while we delete the file...',
isLoading: true,
isShow: false,
},
}
// Disabled state
export const DisabledState: Story = {
render: args => <ConfirmDemo {...args} />,
args: {
type: 'info',
title: 'Verification Required',
content: 'Please complete email verification before proceeding.',
isDisabled: true,
isShow: false,
},
}
// Alert style - Confirm button only
export const AlertStyle: Story = {
render: args => <ConfirmDemo {...args} />,
args: {
type: 'info',
title: 'Success',
content: 'Your settings have been updated!',
showCancel: false,
confirmText: 'Got it',
isShow: false,
},
}
// Dangerous action - Long content
export const DangerousAction: Story = {
render: args => <ConfirmDemo {...args} />,
args: {
type: 'warning',
title: 'Permanently Delete Account',
content: 'This action will permanently delete your account and all associated data, including: all projects and files, collaboration history, and personal settings. This action cannot be reversed!',
confirmText: 'Delete My Account',
cancelText: 'Keep My Account',
isShow: false,
},
}
// Non-closable mask
export const NotMaskClosable: Story = {
render: args => <ConfirmDemo {...args} />,
args: {
type: 'warning',
title: 'Important Action',
content: 'This action requires your explicit choice. Clicking outside will not close this dialog.',
maskClosable: false,
isShow: false,
},
}
// Full feature demo - Playground
export const Playground: Story = {
render: args => <ConfirmDemo {...args} />,
args: {
type: 'warning',
title: 'This is a title',
content: 'This is the dialog content text...',
confirmText: undefined,
cancelText: undefined,
isLoading: false,
isDisabled: false,
showConfirm: true,
showCancel: true,
maskClosable: true,
isShow: false,
},
}

View File

@ -1,164 +0,0 @@
/**
* @deprecated Use `@/app/components/base/ui/alert-dialog` instead.
* See issue #32767 for migration details.
*/
import * as React from 'react'
import { useEffect, useRef, useState } from 'react'
import { createPortal } from 'react-dom'
import { useTranslation } from 'react-i18next'
import Button from '../button'
import Tooltip from '../tooltip'
/** @deprecated Use `@/app/components/base/ui/alert-dialog` instead. */
export type IConfirm = {
className?: string
isShow: boolean
type?: 'info' | 'warning' | 'danger'
title: string
content?: React.ReactNode
confirmText?: string | null
onConfirm: () => void
cancelText?: string
onCancel: () => void
isLoading?: boolean
isDisabled?: boolean
showConfirm?: boolean
showCancel?: boolean
maskClosable?: boolean
confirmInputLabel?: string
confirmInputPlaceholder?: string
confirmInputValue?: string
onConfirmInputChange?: (value: string) => void
confirmInputMatchValue?: string
}
function Confirm({
isShow,
type = 'warning',
title,
content,
confirmText,
cancelText,
onConfirm,
onCancel,
showConfirm = true,
showCancel = true,
isLoading = false,
isDisabled = false,
maskClosable = true,
confirmInputLabel,
confirmInputPlaceholder,
confirmInputValue = '',
onConfirmInputChange,
confirmInputMatchValue,
}: IConfirm) {
const { t } = useTranslation()
const dialogRef = useRef<HTMLDivElement>(null)
const titleRef = useRef<HTMLDivElement>(null)
const [isVisible, setIsVisible] = useState(isShow)
const [isTitleTruncated, setIsTitleTruncated] = useState(false)
const confirmTxt = confirmText || `${t('operation.confirm', { ns: 'common' })}`
const cancelTxt = cancelText || `${t('operation.cancel', { ns: 'common' })}`
const isConfirmDisabled = isDisabled || (confirmInputMatchValue ? confirmInputValue !== confirmInputMatchValue : false)
useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => {
if (event.key === 'Escape')
onCancel()
if (event.key === 'Enter' && isShow && !isConfirmDisabled) {
event.preventDefault()
onConfirm()
}
}
document.addEventListener('keydown', handleKeyDown)
return () => {
document.removeEventListener('keydown', handleKeyDown)
}
}, [onCancel, onConfirm, isShow, isConfirmDisabled])
const handleClickOutside = (event: MouseEvent) => {
if (maskClosable && dialogRef.current && !dialogRef.current.contains(event.target as Node))
onCancel()
}
useEffect(() => {
document.addEventListener('mousedown', handleClickOutside)
return () => {
document.removeEventListener('mousedown', handleClickOutside)
}
}, [maskClosable])
useEffect(() => {
if (isShow) {
setIsVisible(true)
}
else {
const timer = setTimeout(() => setIsVisible(false), 200)
return () => clearTimeout(timer)
}
}, [isShow])
useEffect(() => {
if (titleRef.current) {
const isOverflowing = titleRef.current.scrollWidth > titleRef.current.clientWidth
setIsTitleTruncated(isOverflowing)
}
}, [title, isVisible])
if (!isVisible)
return null
return createPortal(
<div
className="fixed inset-0 z-10000000 flex items-center justify-center bg-background-overlay"
onClick={(e) => {
e.preventDefault()
e.stopPropagation()
}}
data-testid="confirm-overlay"
>
<div ref={dialogRef} className="relative w-full max-w-[480px] overflow-hidden">
<div className="shadows-shadow-lg flex max-w-full flex-col items-start rounded-2xl border-[0.5px] border-solid border-components-panel-border bg-components-panel-bg">
<div className="flex flex-col items-start gap-2 self-stretch pb-4 pl-6 pr-6 pt-6">
<Tooltip
popupContent={title}
disabled={!isTitleTruncated}
portalContentClassName="z-10000001!"
asChild={false}
triggerClassName="w-full"
>
<div ref={titleRef} className="title-2xl-semi-bold w-full truncate text-text-primary">
{title}
</div>
</Tooltip>
<div className="w-full whitespace-pre-wrap wrap-break-word text-text-tertiary system-md-regular">{content}</div>
{confirmInputLabel && (
<div className="mt-2">
<label className="mb-1 block text-text-secondary system-sm-regular">
{confirmInputLabel}
</label>
<input
type="text"
className="border-components-input-border bg-components-input-bg focus:border-components-input-border-focus focus:ring-components-input-border-focus h-9 w-full rounded-lg border px-3 text-sm text-text-primary placeholder:text-text-quaternary focus:outline-hidden focus:ring-1"
placeholder={confirmInputPlaceholder}
value={confirmInputValue}
onChange={e => onConfirmInputChange?.(e.target.value)}
/>
</div>
)}
</div>
<div className="flex items-start justify-end gap-2 self-stretch p-6">
{showCancel && <Button onClick={onCancel}>{cancelTxt}</Button>}
{showConfirm && <Button variant="primary" destructive={type !== 'info'} loading={isLoading} disabled={isConfirmDisabled} onClick={onConfirm}>{confirmTxt}</Button>}
</div>
</div>
</div>
</div>,
document.body,
)
}
export default React.memo(Confirm)

View File

@ -3,8 +3,8 @@ import type { Tag } from '@/app/components/base/tag-management/constant'
import { useDebounceFn } from 'ahooks'
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import Confirm from '@/app/components/base/confirm'
import Tooltip from '@/app/components/base/tooltip'
import Confirm from '@/app/components/base/ui/confirm-dialog'
import { toast } from '@/app/components/base/ui/toast'
import { deleteTag, updateTag } from '@/service/tag'
import { cn } from '@/utils/classnames'

View File

@ -16,8 +16,8 @@ type AlertDialogContentProps = {
children: React.ReactNode
className?: string
overlayClassName?: string
popupProps?: Omit<React.ComponentPropsWithoutRef<typeof BaseAlertDialog.Popup>, 'children' | 'className'>
backdropProps?: Omit<React.ComponentPropsWithoutRef<typeof BaseAlertDialog.Backdrop>, 'className'>
popupProps?: Omit<React.ComponentPropsWithoutRef<typeof BaseAlertDialog.Popup>, 'children' | 'className'> & React.ComponentPropsWithoutRef<'div'>
backdropProps?: Omit<React.ComponentPropsWithoutRef<typeof BaseAlertDialog.Backdrop>, 'className'> & React.ComponentPropsWithoutRef<'div'>
}
export function AlertDialogContent({

View File

@ -0,0 +1,149 @@
import { fireEvent, render, screen } from '@testing-library/react'
import { describe, expect, it, vi } from 'vitest'
import Confirm from '..'
const onCancel = vi.fn()
const onConfirm = vi.fn()
const getOverlay = () => {
const overlay = document.querySelector('.confirm-dialog-overlay')
if (!overlay)
throw new Error('Expected confirm dialog overlay to be rendered')
return overlay
}
describe('ConfirmDialog wrapper', () => {
beforeEach(() => {
vi.clearAllMocks()
})
// Rendering scenarios
describe('Rendering', () => {
it('should render confirm correctly when isShow is true', async () => {
render(<Confirm isShow title="test title" onCancel={onCancel} onConfirm={onConfirm} />)
expect(await screen.findByRole('alertdialog')).toBeInTheDocument()
expect(screen.getByText('test title')).toBeInTheDocument()
})
it('should not render when isShow is false', () => {
render(<Confirm isShow={false} title="test title" onCancel={onCancel} onConfirm={onConfirm} />)
expect(screen.queryByRole('alertdialog')).not.toBeInTheDocument()
})
it('should render content when provided', async () => {
render(<Confirm isShow title="title" content="some description" onCancel={onCancel} onConfirm={onConfirm} />)
expect(await screen.findByText('some description')).toBeInTheDocument()
})
})
// Prop-driven rendering and state
describe('Props', () => {
it('should hide cancel button when showCancel is false', async () => {
render(<Confirm isShow title="test title" onCancel={onCancel} onConfirm={onConfirm} showCancel={false} />)
expect(await screen.findByRole('button', { name: 'common.operation.confirm' })).toBeInTheDocument()
expect(screen.queryByRole('button', { name: 'common.operation.cancel' })).not.toBeInTheDocument()
})
it('should hide confirm button when showConfirm is false', async () => {
render(<Confirm isShow title="test title" onCancel={onCancel} onConfirm={onConfirm} showConfirm={false} />)
expect(await screen.findByRole('button', { name: 'common.operation.cancel' })).toBeInTheDocument()
expect(screen.queryByRole('button', { name: 'common.operation.confirm' })).not.toBeInTheDocument()
})
it('should render custom confirm and cancel text', async () => {
render(<Confirm isShow title="title" confirmText="Yes" cancelText="No" onCancel={onCancel} onConfirm={onConfirm} />)
expect(await screen.findByRole('button', { name: 'Yes' })).toBeInTheDocument()
expect(screen.getByRole('button', { name: 'No' })).toBeInTheDocument()
})
it('should disable confirm button when isDisabled is true', async () => {
render(<Confirm isShow title="title" isDisabled={true} onCancel={onCancel} onConfirm={onConfirm} />)
expect(await screen.findByRole('button', { name: 'common.operation.confirm' })).toBeDisabled()
})
it('should keep cancel button enabled when confirm button is loading', async () => {
render(<Confirm isShow title="title" isLoading={true} onCancel={onCancel} onConfirm={onConfirm} />)
expect(await screen.findByRole('button', { name: 'common.operation.cancel' })).toBeEnabled()
expect(screen.getByRole('button', { name: /common\.operation\.confirm/i })).toBeDisabled()
})
it('should disable confirm button until confirm input matches expected value', async () => {
render(
<Confirm
isShow
title="title"
confirmInputLabel="Type DELETE to continue"
confirmInputPlaceholder="DELETE"
confirmInputValue="DEL"
confirmInputMatchValue="DELETE"
onCancel={onCancel}
onConfirm={onConfirm}
/>,
)
expect(await screen.findByRole('button', { name: 'common.operation.confirm' })).toBeDisabled()
expect(screen.getByLabelText('Type DELETE to continue')).toHaveValue('DEL')
})
})
// User interactions
describe('User Interactions', () => {
it('should call onCancel when clicking the backdrop', async () => {
render(<Confirm isShow title="test title" onCancel={onCancel} onConfirm={onConfirm} />)
const overlay = getOverlay()
fireEvent.mouseDown(overlay)
expect(onCancel).toHaveBeenCalledTimes(1)
})
it('should stop propagation on backdrop click', async () => {
render(<Confirm isShow title="test title" onCancel={onCancel} onConfirm={onConfirm} />)
const overlay = getOverlay()
const clickEvent = new MouseEvent('click', { bubbles: true, cancelable: true })
const preventDefaultSpy = vi.spyOn(clickEvent, 'preventDefault')
const stopPropagationSpy = vi.spyOn(clickEvent, 'stopPropagation')
overlay.dispatchEvent(clickEvent)
expect(preventDefaultSpy).toHaveBeenCalled()
expect(stopPropagationSpy).toHaveBeenCalled()
})
it('should not close on click away when maskClosable is false', async () => {
render(<Confirm isShow title="test title" maskClosable={false} onCancel={onCancel} onConfirm={onConfirm} />)
const overlay = getOverlay()
fireEvent.mouseDown(overlay)
expect(onCancel).not.toHaveBeenCalled()
})
it('should call onCancel when Escape key is pressed', () => {
render(<Confirm isShow title="test title" onCancel={onCancel} onConfirm={onConfirm} />)
fireEvent.keyDown(document, { key: 'Escape' })
expect(onCancel).toHaveBeenCalledTimes(1)
expect(onConfirm).not.toHaveBeenCalled()
})
it('should call onConfirm when Enter key is pressed', () => {
render(<Confirm isShow title="test title" onCancel={onCancel} onConfirm={onConfirm} />)
fireEvent.keyDown(document, { key: 'Enter' })
expect(onConfirm).toHaveBeenCalledTimes(1)
expect(onCancel).not.toHaveBeenCalled()
})
})
})

View File

@ -0,0 +1,154 @@
'use client'
import * as React from 'react'
import { useEffect, useId } from 'react'
import { useTranslation } from 'react-i18next'
import Input from '@/app/components/base/input'
import {
AlertDialog,
AlertDialogActions,
AlertDialogCancelButton,
AlertDialogConfirmButton,
AlertDialogContent,
AlertDialogDescription,
AlertDialogTitle,
} from '@/app/components/base/ui/alert-dialog'
export type IConfirm = {
className?: string
isShow: boolean
type?: 'info' | 'warning' | 'danger'
title: string
content?: React.ReactNode
confirmText?: string | null
onConfirm: () => void
cancelText?: string
onCancel: () => void
isLoading?: boolean
isDisabled?: boolean
showConfirm?: boolean
showCancel?: boolean
maskClosable?: boolean
confirmInputLabel?: string
confirmInputPlaceholder?: string
confirmInputValue?: string
onConfirmInputChange?: (value: string) => void
confirmInputMatchValue?: string
}
function Confirm({
className,
isShow,
type = 'warning',
title,
content,
confirmText,
cancelText,
onConfirm,
onCancel,
showConfirm = true,
showCancel = true,
isLoading = false,
isDisabled = false,
maskClosable = true,
confirmInputLabel,
confirmInputPlaceholder,
confirmInputValue = '',
onConfirmInputChange,
confirmInputMatchValue,
}: IConfirm) {
const { t } = useTranslation()
const confirmInputId = useId()
const confirmTxt = confirmText || t('operation.confirm', { ns: 'common' })
const cancelTxt = cancelText || t('operation.cancel', { ns: 'common' })
const isConfirmDisabled = isDisabled || (confirmInputMatchValue ? confirmInputValue !== confirmInputMatchValue : false)
useEffect(() => {
if (!isShow)
return
const handleKeyDown = (event: KeyboardEvent) => {
if (event.key === 'Enter' && !isConfirmDisabled) {
event.preventDefault()
onConfirm()
}
}
document.addEventListener('keydown', handleKeyDown)
return () => {
document.removeEventListener('keydown', handleKeyDown)
}
}, [isConfirmDisabled, isShow, onConfirm])
return (
<AlertDialog
open={isShow}
onOpenChange={(open) => {
if (!open)
onCancel()
}}
>
<AlertDialogContent
className={className}
overlayClassName="confirm-dialog-overlay"
backdropProps={{
onClick: (event) => {
event.preventDefault()
event.stopPropagation()
},
onMouseDown: () => {
if (maskClosable)
onCancel()
},
}}
>
<div className="flex flex-col gap-2 px-6 pt-6 pb-4">
<AlertDialogTitle title={title} className="w-full truncate title-2xl-semi-bold text-text-primary">
{title}
</AlertDialogTitle>
{content !== undefined && content !== null && (
<AlertDialogDescription className="w-full system-md-regular wrap-break-word whitespace-pre-wrap text-text-tertiary">
{content}
</AlertDialogDescription>
)}
{confirmInputLabel && (
<div className="mt-2">
<label htmlFor={confirmInputId} className="mb-2 block system-sm-regular text-text-secondary">
{confirmInputLabel}
</label>
<Input
id={confirmInputId}
value={confirmInputValue}
placeholder={confirmInputPlaceholder}
onChange={event => onConfirmInputChange?.(event.target.value)}
/>
</div>
)}
</div>
{(showCancel || showConfirm) && (
<AlertDialogActions>
{showCancel && (
<AlertDialogCancelButton>
{cancelTxt}
</AlertDialogCancelButton>
)}
{showConfirm && (
<AlertDialogConfirmButton
variant="primary"
destructive={type !== 'info'}
loading={isLoading}
disabled={isConfirmDisabled}
onClick={onConfirm}
>
{confirmTxt}
</AlertDialogConfirmButton>
)}
</AlertDialogActions>
)}
</AlertDialogContent>
</AlertDialog>
)
}
export default React.memo(Confirm)

View File

@ -1,6 +1,7 @@
import type { PipelineTemplate } from '@/models/pipeline'
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
import { fireEvent, render, screen, waitFor, within } from '@testing-library/react'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import * as toastModule from '@/app/components/base/ui/toast'
import { ChunkingMode } from '@/models/datasets'
import TemplateCard from '../index'
@ -14,21 +15,17 @@ vi.mock('@/app/components/base/amplitude', () => ({
trackEvent: vi.fn(),
}))
const { mockToastSuccess, mockToastError } = vi.hoisted(() => ({
mockToastSuccess: vi.fn(),
mockToastError: vi.fn(),
}))
const mockToastSuccess = vi.fn()
const mockToastError = vi.fn()
vi.mock('@/app/components/base/ui/toast', async (importOriginal) => {
const actual = await importOriginal<typeof import('@/app/components/base/ui/toast')>()
return {
...actual,
toast: {
...actual.toast,
success: mockToastSuccess,
error: mockToastError,
},
}
vi.spyOn(toastModule.toast, 'success').mockImplementation((...args) => {
mockToastSuccess(...args)
return 'toast-id'
})
vi.spyOn(toastModule.toast, 'error').mockImplementation((...args) => {
mockToastError(...args)
return 'toast-id'
})
// Mock download utilities
@ -37,33 +34,6 @@ vi.mock('@/utils/download', () => ({
downloadUrl: vi.fn(),
}))
// Capture Confirm callbacks
let _capturedOnConfirm: (() => void) | undefined
let _capturedOnCancel: (() => void) | undefined
vi.mock('@/app/components/base/confirm', () => ({
default: ({ isShow, onConfirm, onCancel, title, content }: {
isShow: boolean
onConfirm: () => void
onCancel: () => void
title: string
content: string
}) => {
_capturedOnConfirm = onConfirm
_capturedOnCancel = onCancel
return isShow
? (
<div data-testid="confirm-dialog">
<div data-testid="confirm-title">{title}</div>
<div data-testid="confirm-content">{content}</div>
<button data-testid="confirm-cancel" onClick={onCancel}>Cancel</button>
<button data-testid="confirm-submit" onClick={onConfirm}>Confirm</button>
</div>
)
: null
},
}))
// Capture Actions callbacks
let _capturedHandleDelete: (() => void) | undefined
let _capturedHandleExportDSL: (() => void) | undefined
@ -187,8 +157,6 @@ describe('TemplateCard', () => {
mockToastSuccess.mockReset()
mockToastError.mockReset()
mockIsExporting = false
_capturedOnConfirm = undefined
_capturedOnCancel = undefined
_capturedHandleDelete = undefined
_capturedHandleExportDSL = undefined
_capturedOpenEditModal = undefined
@ -507,7 +475,7 @@ describe('TemplateCard', () => {
fireEvent.click(deleteButton)
await waitFor(() => {
expect(screen.getByTestId('confirm-dialog')).toBeInTheDocument()
expect(screen.getByRole('alertdialog')).toBeInTheDocument()
})
})
@ -517,14 +485,14 @@ describe('TemplateCard', () => {
fireEvent.click(deleteButton)
await waitFor(() => {
expect(screen.getByTestId('confirm-dialog')).toBeInTheDocument()
expect(screen.getByRole('alertdialog')).toBeInTheDocument()
})
const cancelButton = screen.getByTestId('confirm-cancel')
const cancelButton = within(screen.getByRole('alertdialog')).getByRole('button', { name: 'common.operation.cancel' })
fireEvent.click(cancelButton)
await waitFor(() => {
expect(screen.queryByTestId('confirm-dialog')).not.toBeInTheDocument()
expect(screen.queryByRole('alertdialog')).not.toBeInTheDocument()
})
})
@ -539,10 +507,10 @@ describe('TemplateCard', () => {
fireEvent.click(deleteButton)
await waitFor(() => {
expect(screen.getByTestId('confirm-dialog')).toBeInTheDocument()
expect(screen.getByRole('alertdialog')).toBeInTheDocument()
})
const confirmButton = screen.getByTestId('confirm-submit')
const confirmButton = within(screen.getByRole('alertdialog')).getAllByRole('button').at(-1)!
fireEvent.click(confirmButton)
await waitFor(() => {
@ -561,10 +529,10 @@ describe('TemplateCard', () => {
fireEvent.click(deleteButton)
await waitFor(() => {
expect(screen.getByTestId('confirm-dialog')).toBeInTheDocument()
expect(screen.getByRole('alertdialog')).toBeInTheDocument()
})
const confirmButton = screen.getByTestId('confirm-submit')
const confirmButton = within(screen.getByRole('alertdialog')).getAllByRole('button').at(-1)!
fireEvent.click(confirmButton)
await waitFor(() => {
@ -583,14 +551,14 @@ describe('TemplateCard', () => {
fireEvent.click(deleteButton)
await waitFor(() => {
expect(screen.getByTestId('confirm-dialog')).toBeInTheDocument()
expect(screen.getByRole('alertdialog')).toBeInTheDocument()
})
const confirmButton = screen.getByTestId('confirm-submit')
const confirmButton = within(screen.getByRole('alertdialog')).getAllByRole('button').at(-1)!
fireEvent.click(confirmButton)
await waitFor(() => {
expect(screen.queryByTestId('confirm-dialog')).not.toBeInTheDocument()
expect(screen.queryByRole('alertdialog')).not.toBeInTheDocument()
})
})
})

View File

@ -3,8 +3,8 @@ import * as React from 'react'
import { useCallback, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { trackEvent } from '@/app/components/base/amplitude'
import Confirm from '@/app/components/base/confirm'
import Modal from '@/app/components/base/modal'
import Confirm from '@/app/components/base/ui/confirm-dialog'
import { toast } from '@/app/components/base/ui/toast'
import { usePluginDependencies } from '@/app/components/workflow/plugin-dependency/hooks'
import { useRouter } from '@/next/navigation'

View File

@ -7,12 +7,12 @@ import { noop } from 'es-toolkit/function'
import * as React from 'react'
import { useCallback, useState } from 'react'
import { useTranslation } from 'react-i18next'
import Confirm from '@/app/components/base/confirm'
import Divider from '@/app/components/base/divider'
import { SearchLinesSparkle } from '@/app/components/base/icons/src/vender/knowledge'
import CustomPopover from '@/app/components/base/popover'
import Switch from '@/app/components/base/switch'
import Tooltip from '@/app/components/base/tooltip'
import Confirm from '@/app/components/base/ui/confirm-dialog'
import { toast } from '@/app/components/base/ui/toast'
import { IS_CE_EDITION } from '@/config'
import { DataSourceType, DocumentActionType } from '@/models/datasets'

View File

@ -4,9 +4,9 @@ import { useBoolean } from 'ahooks'
import * as React from 'react'
import { useTranslation } from 'react-i18next'
import Button from '@/app/components/base/button'
import Confirm from '@/app/components/base/confirm'
import Divider from '@/app/components/base/divider'
import { SearchLinesSparkle } from '@/app/components/base/icons/src/vender/knowledge'
import Confirm from '@/app/components/base/ui/confirm-dialog'
import { IS_CE_EDITION } from '@/config'
import { cn } from '@/utils/classnames'

View File

@ -5,10 +5,10 @@ import * as React from 'react'
import { useCallback, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import Badge from '@/app/components/base/badge'
import Confirm from '@/app/components/base/confirm'
import Divider from '@/app/components/base/divider'
import Switch from '@/app/components/base/switch'
import Tooltip from '@/app/components/base/tooltip'
import Confirm from '@/app/components/base/ui/confirm-dialog'
import ImageList from '@/app/components/datasets/common/image-list'
import { ChunkingMode } from '@/models/datasets'
import { cn } from '@/utils/classnames'

View File

@ -5,9 +5,9 @@ import { memo, useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import ActionButton from '@/app/components/base/action-button'
import Button from '@/app/components/base/button'
import Confirm from '@/app/components/base/confirm'
import { PortalToFollowElem, PortalToFollowElemContent } from '@/app/components/base/portal-to-follow-elem'
import Tooltip from '@/app/components/base/tooltip'
import Confirm from '@/app/components/base/ui/confirm-dialog'
import { toast } from '@/app/components/base/ui/toast'
import { createExternalAPI } from '@/service/datasets'
import Form from './Form'

View File

@ -8,8 +8,8 @@ import * as React from 'react'
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import ActionButton from '@/app/components/base/action-button'
import Confirm from '@/app/components/base/confirm'
import { ApiConnectionMod } from '@/app/components/base/icons/src/vender/solid/development'
import Confirm from '@/app/components/base/ui/confirm-dialog'
import { useExternalKnowledgeApi } from '@/context/external-knowledge-api-context'
import { useModalContext } from '@/context/modal-context'
import { checkUsageExternalAPI, deleteExternalAPI, fetchExternalAPI, updateExternalAPI } from '@/service/datasets'

View File

@ -19,28 +19,6 @@ vi.mock('../../../../rename-modal', () => ({
),
}))
// Mock Confirm component since it uses createPortal which can cause issues in tests
vi.mock('@/app/components/base/confirm', () => ({
default: ({ isShow, title, content, onConfirm, onCancel }: {
isShow: boolean
title: string
content?: React.ReactNode
onConfirm: () => void
onCancel: () => void
}) => (
isShow
? (
<div data-testid="confirm-modal">
<div data-testid="confirm-title">{title}</div>
<div data-testid="confirm-content">{content}</div>
<button onClick={onCancel} role="button" aria-label="cancel">Cancel</button>
<button onClick={onConfirm} role="button" aria-label="confirm">Confirm</button>
</div>
)
: null
),
}))
describe('DatasetCardModals', () => {
const mockDataset: DataSet = {
id: 'dataset-1',

View File

@ -1,7 +1,7 @@
import type { DataSet } from '@/models/datasets'
import * as React from 'react'
import { useTranslation } from 'react-i18next'
import Confirm from '@/app/components/base/confirm'
import Confirm from '@/app/components/base/ui/confirm-dialog'
import RenameDatasetModal from '../../../rename-modal'
type ModalState = {

View File

@ -7,12 +7,12 @@ import * as React from 'react'
import { useCallback, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import Button from '@/app/components/base/button'
import Confirm from '@/app/components/base/confirm'
import Drawer from '@/app/components/base/drawer'
import Input from '@/app/components/base/input'
import Modal from '@/app/components/base/modal'
import Switch from '@/app/components/base/switch'
import Tooltip from '@/app/components/base/tooltip'
import Confirm from '@/app/components/base/ui/confirm-dialog'
import { toast } from '@/app/components/base/ui/toast'
import CreateModal from '@/app/components/datasets/metadata/metadata-dataset/create-metadata-modal'
import { cn } from '@/utils/classnames'

View File

@ -8,10 +8,10 @@ import {
import { useTranslation } from 'react-i18next'
import ActionButton from '@/app/components/base/action-button'
import Button from '@/app/components/base/button'
import Confirm from '@/app/components/base/confirm'
import CopyFeedback from '@/app/components/base/copy-feedback'
import Loading from '@/app/components/base/loading'
import Modal from '@/app/components/base/modal'
import Confirm from '@/app/components/base/ui/confirm-dialog'
import { useAppContext } from '@/context/app-context'
import useTimestamp from '@/hooks/use-timestamp'
import {

View File

@ -105,7 +105,7 @@ describe('Item Component', () => {
// Act
fireEvent.click(screen.getByText('common.operation.delete'))
const dialog = screen.getByTestId('confirm-overlay')
const dialog = screen.getByRole('alertdialog')
const confirmButton = within(dialog).getByText('common.operation.delete')
fireEvent.click(confirmButton)
@ -123,7 +123,7 @@ describe('Item Component', () => {
// Act
fireEvent.click(screen.getByText('common.operation.delete'))
const dialog = screen.getByTestId('confirm-overlay')
const dialog = screen.getByRole('alertdialog')
const confirmButton = within(dialog).getByText('common.operation.delete')
fireEvent.click(confirmButton)

View File

@ -7,7 +7,7 @@ import {
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import Button from '@/app/components/base/button'
import Confirm from '@/app/components/base/confirm'
import Confirm from '@/app/components/base/ui/confirm-dialog'
import { useModalContext } from '@/context/modal-context'
import { deleteApiBasedExtension } from '@/service/common'

View File

@ -8,7 +8,7 @@ import {
useRef,
} from 'react'
import { useTranslation } from 'react-i18next'
import Confirm from '@/app/components/base/confirm'
import Confirm from '@/app/components/base/ui/confirm-dialog'
import {
ApiKeyModal,
usePluginAuthAction,

View File

@ -20,12 +20,12 @@ import {
} from 'react'
import { useTranslation } from 'react-i18next'
import Button from '@/app/components/base/button'
import Confirm from '@/app/components/base/confirm'
import {
PortalToFollowElem,
PortalToFollowElemContent,
PortalToFollowElemTrigger,
} from '@/app/components/base/portal-to-follow-elem'
import Confirm from '@/app/components/base/ui/confirm-dialog'
import { cn } from '@/utils/classnames'
import { useAuth } from '../hooks'
import AuthorizedItem from './authorized-item'

View File

@ -2,9 +2,9 @@ import type { Credential, CustomConfigurationModelFixedFields, ModelItem, ModelL
import { memo, useCallback, useEffect, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import Button from '@/app/components/base/button'
import Confirm from '@/app/components/base/confirm'
import Loading from '@/app/components/base/loading'
import Modal from '@/app/components/base/modal'
import Confirm from '@/app/components/base/ui/confirm-dialog'
import { toast } from '@/app/components/base/ui/toast'
import { SwitchCredentialInLoadBalancing } from '@/app/components/header/account-setting/model-provider-page/model-auth'
import { useGetModelCredential, useUpdateModelLoadBalancingConfig } from '@/service/use-models'

View File

@ -13,12 +13,12 @@ import {
} from 'react'
import { useTranslation } from 'react-i18next'
import Button from '@/app/components/base/button'
import Confirm from '@/app/components/base/confirm'
import {
PortalToFollowElem,
PortalToFollowElemContent,
PortalToFollowElemTrigger,
} from '@/app/components/base/portal-to-follow-elem'
import Confirm from '@/app/components/base/ui/confirm-dialog'
import { toast } from '@/app/components/base/ui/toast'
import Indicator from '@/app/components/header/indicator'
import { cn } from '@/utils/classnames'

View File

@ -6,10 +6,10 @@ import * as React from 'react'
import { useEffect, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import ActionButton from '@/app/components/base/action-button'
import Confirm from '@/app/components/base/confirm'
import { CopyCheck } from '@/app/components/base/icons/src/vender/line/files'
import Switch from '@/app/components/base/switch'
import Tooltip from '@/app/components/base/tooltip'
import Confirm from '@/app/components/base/ui/confirm-dialog'
import { toast } from '@/app/components/base/ui/toast'
import Indicator from '@/app/components/header/indicator'
import { addDefaultValue, toolCredentialToFormSchemas } from '@/app/components/tools/utils/to-form-schema'

View File

@ -1,6 +1,7 @@
import type { MetaData, PluginCategoryEnum } from '../../types'
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
import { fireEvent, render, screen, waitFor, within } from '@testing-library/react'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import * as toastModule from '@/app/components/base/ui/toast'
// ==================== Imports (after mocks) ====================
@ -16,30 +17,19 @@ const {
mockCheckForUpdates,
mockSetShowUpdatePluginModal,
mockInvalidateInstalledPluginList,
mockToastNotify,
} = vi.hoisted(() => ({
mockUninstallPlugin: vi.fn(),
mockFetchReleases: vi.fn(),
mockCheckForUpdates: vi.fn(),
mockSetShowUpdatePluginModal: vi.fn(),
mockInvalidateInstalledPluginList: vi.fn(),
mockToastNotify: vi.fn(),
}))
const mockToastNotify = vi.fn()
vi.mock('@/app/components/base/ui/toast', () => ({
toast: Object.assign(
(message: string, options?: { type?: string }) => mockToastNotify({ type: options?.type, message }),
{
success: (message: string) => mockToastNotify({ type: 'success', message }),
error: (message: string) => mockToastNotify({ type: 'error', message }),
warning: (message: string) => mockToastNotify({ type: 'warning', message }),
info: (message: string) => mockToastNotify({ type: 'info', message }),
dismiss: vi.fn(),
update: vi.fn(),
promise: vi.fn(),
},
),
}))
vi.spyOn(toastModule, 'toast').mockImplementation((message, options) => {
mockToastNotify({ type: options?.type, message })
return 'toast-id'
})
// Mock uninstall plugin service
vi.mock('@/service/plugins', () => ({
@ -92,30 +82,6 @@ vi.mock('../../../base/tooltip', () => ({
),
}))
// Mock Confirm - uses createPortal which has issues in test environment
vi.mock('../../../base/confirm', () => ({
default: ({ isShow, title, content, onCancel, onConfirm, isLoading, isDisabled }: {
isShow: boolean
title: string
content: React.ReactNode
onCancel: () => void
onConfirm: () => void
isLoading: boolean
isDisabled: boolean
}) => {
if (!isShow)
return null
return (
<div data-testid="confirm-modal" data-loading={isLoading} data-disabled={isDisabled}>
<div data-testid="confirm-title">{title}</div>
<div data-testid="confirm-content">{content}</div>
<button data-testid="confirm-cancel" onClick={onCancel}>Cancel</button>
<button data-testid="confirm-ok" onClick={onConfirm} disabled={isDisabled}>Confirm</button>
</div>
)
},
}))
// ==================== Test Utilities ====================
type ActionProps = {
@ -151,6 +117,11 @@ const createActionProps = (overrides: Partial<ActionProps> = {}): ActionProps =>
...overrides,
})
const getConfirmDialog = () => screen.getByRole('alertdialog')
const getConfirmButtons = () => within(getConfirmDialog()).getAllByRole('button')
const getConfirmCancelButton = () => getConfirmButtons()[0]
const getConfirmConfirmButton = () => getConfirmButtons().at(-1)!
// ==================== Tests ====================
// Helper to find action buttons (real ActionButton component uses type="button")
@ -277,8 +248,8 @@ describe('Action Component', () => {
fireEvent.click(getActionButtons()[0])
// Assert
expect(screen.getByTestId('confirm-modal')).toBeInTheDocument()
expect(screen.getByTestId('confirm-title')).toHaveTextContent('plugin.action.delete')
expect(getConfirmDialog()).toBeInTheDocument()
expect(screen.getByText('plugin.action.delete')).toBeInTheDocument()
})
it('should display plugin name in delete confirm content', () => {
@ -298,7 +269,7 @@ describe('Action Component', () => {
expect(screen.getByText('my-awesome-plugin')).toBeInTheDocument()
})
it('should hide confirm modal when cancel is clicked', () => {
it('should hide confirm modal when cancel is clicked', async () => {
// Arrange
const props = createActionProps({
isShowDelete: true,
@ -309,12 +280,14 @@ describe('Action Component', () => {
// Act
render(<Action {...props} />)
fireEvent.click(getActionButtons()[0])
expect(screen.getByTestId('confirm-modal')).toBeInTheDocument()
expect(getConfirmDialog()).toBeInTheDocument()
fireEvent.click(screen.getByTestId('confirm-cancel'))
fireEvent.click(getConfirmCancelButton())
// Assert
expect(screen.queryByTestId('confirm-modal')).not.toBeInTheDocument()
await waitFor(() => {
expect(screen.queryByRole('alertdialog')).not.toBeInTheDocument()
})
})
it('should call uninstallPlugin when confirm is clicked', async () => {
@ -329,7 +302,7 @@ describe('Action Component', () => {
// Act
render(<Action {...props} />)
fireEvent.click(getActionButtons()[0])
fireEvent.click(screen.getByTestId('confirm-ok'))
fireEvent.click(getConfirmConfirmButton())
// Assert
await waitFor(() => {
@ -351,7 +324,7 @@ describe('Action Component', () => {
// Act
render(<Action {...props} />)
fireEvent.click(getActionButtons()[0])
fireEvent.click(screen.getByTestId('confirm-ok'))
fireEvent.click(getConfirmConfirmButton())
// Assert
await waitFor(() => {
@ -373,7 +346,7 @@ describe('Action Component', () => {
// Act
render(<Action {...props} />)
fireEvent.click(getActionButtons()[0])
fireEvent.click(screen.getByTestId('confirm-ok'))
fireEvent.click(getConfirmConfirmButton())
// Assert
await waitFor(() => {
@ -395,7 +368,7 @@ describe('Action Component', () => {
// Act
render(<Action {...props} />)
fireEvent.click(getActionButtons()[0])
fireEvent.click(screen.getByTestId('confirm-ok'))
fireEvent.click(getConfirmConfirmButton())
// Assert
await waitFor(() => {
@ -422,17 +395,17 @@ describe('Action Component', () => {
// Act
render(<Action {...props} />)
fireEvent.click(getActionButtons()[0])
fireEvent.click(screen.getByTestId('confirm-ok'))
fireEvent.click(getConfirmConfirmButton())
// Assert - Loading state
await waitFor(() => {
expect(screen.getByTestId('confirm-modal')).toHaveAttribute('data-loading', 'true')
expect(getConfirmConfirmButton()).toHaveAttribute('aria-busy', 'true')
})
// Resolve and check modal closes
resolveUninstall!({ success: true })
await waitFor(() => {
expect(screen.queryByTestId('confirm-modal')).not.toBeInTheDocument()
expect(screen.queryByRole('alertdialog')).not.toBeInTheDocument()
})
})
})
@ -699,7 +672,7 @@ describe('Action Component', () => {
// Act - First render and delete
const { rerender } = render(<Action {...props} />)
fireEvent.click(getActionButtons()[0])
fireEvent.click(screen.getByTestId('confirm-ok'))
fireEvent.click(getConfirmConfirmButton())
await waitFor(() => {
expect(mockUninstallPlugin).toHaveBeenCalledWith('stable-install-id')
@ -709,7 +682,7 @@ describe('Action Component', () => {
mockUninstallPlugin.mockClear()
rerender(<Action {...props} />)
fireEvent.click(getActionButtons()[0])
fireEvent.click(screen.getByTestId('confirm-ok'))
fireEvent.click(getConfirmConfirmButton())
await waitFor(() => {
expect(mockUninstallPlugin).toHaveBeenCalledWith('stable-install-id')
@ -735,7 +708,7 @@ describe('Action Component', () => {
// Act
const { rerender } = render(<Action {...props1} />)
fireEvent.click(getActionButtons()[0])
fireEvent.click(screen.getByTestId('confirm-ok'))
fireEvent.click(getConfirmConfirmButton())
await waitFor(() => {
expect(mockUninstallPlugin).toHaveBeenCalledWith('install-1')
@ -744,7 +717,7 @@ describe('Action Component', () => {
mockUninstallPlugin.mockClear()
rerender(<Action {...props2} />)
fireEvent.click(getActionButtons()[0])
fireEvent.click(screen.getByTestId('confirm-ok'))
fireEvent.click(getConfirmConfirmButton())
await waitFor(() => {
expect(mockUninstallPlugin).toHaveBeenCalledWith('install-2')
@ -772,7 +745,7 @@ describe('Action Component', () => {
// Act
const { rerender } = render(<Action {...props1} />)
fireEvent.click(getActionButtons()[0])
fireEvent.click(screen.getByTestId('confirm-ok'))
fireEvent.click(getConfirmConfirmButton())
await waitFor(() => {
expect(onDelete1).toHaveBeenCalled()
@ -781,7 +754,7 @@ describe('Action Component', () => {
rerender(<Action {...props2} />)
fireEvent.click(getActionButtons()[0])
fireEvent.click(screen.getByTestId('confirm-ok'))
fireEvent.click(getConfirmConfirmButton())
await waitFor(() => {
expect(onDelete2).toHaveBeenCalled()
@ -847,17 +820,17 @@ describe('Action Component', () => {
// Act
render(<Action {...props} />)
fireEvent.click(getActionButtons()[0])
fireEvent.click(screen.getByTestId('confirm-ok'))
fireEvent.click(getConfirmConfirmButton())
// The confirm button should be disabled during deletion
expect(screen.getByTestId('confirm-modal')).toHaveAttribute('data-loading', 'true')
expect(screen.getByTestId('confirm-modal')).toHaveAttribute('data-disabled', 'true')
expect(getConfirmConfirmButton()).toHaveAttribute('aria-busy', 'true')
expect(getConfirmConfirmButton()).toBeDisabled()
// Resolve the deletion
resolveFirst!({ success: true })
await waitFor(() => {
expect(screen.queryByTestId('confirm-modal')).not.toBeInTheDocument()
expect(screen.queryByRole('alertdialog')).not.toBeInTheDocument()
})
})

View File

@ -12,8 +12,8 @@ import { useModalContext } from '@/context/modal-context'
import { uninstallPlugin } from '@/service/plugins'
import { useInvalidateInstalledPluginList } from '@/service/use-plugins'
import ActionButton from '../../base/action-button'
import Confirm from '../../base/confirm'
import Tooltip from '../../base/tooltip'
import Confirm from '../../base/ui/confirm-dialog'
import { checkForUpdates, fetchReleases } from '../install-plugin/hooks'
import PluginInfo from '../plugin-page/plugin-info'
import { PluginSource } from '../types'

View File

@ -1,5 +1,6 @@
import { fireEvent, render, screen } from '@testing-library/react'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { fireEvent, render, screen, within } from '@testing-library/react'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import * as toastModule from '@/app/components/base/ui/toast'
import Conversion from '../conversion'
@ -23,23 +24,8 @@ vi.mock('@/service/knowledge/use-dataset', () => ({
vi.mock('@/service/use-base', () => ({
useInvalid: () => mockInvalidDatasetDetail,
}))
const { mockToast } = vi.hoisted(() => {
const mockToast = Object.assign(vi.fn(), {
success: vi.fn(),
error: vi.fn(),
warning: vi.fn(),
info: vi.fn(),
dismiss: vi.fn(),
update: vi.fn(),
promise: vi.fn(),
})
return { mockToast }
})
vi.mock('@/app/components/base/ui/toast', () => ({
toast: mockToast,
}))
const mockToastSuccess = vi.fn()
const mockToastError = vi.fn()
vi.mock('@/app/components/base/button', () => ({
default: ({ children, onClick, ...props }: Record<string, unknown>) => (
@ -47,29 +33,6 @@ vi.mock('@/app/components/base/button', () => ({
),
}))
vi.mock('@/app/components/base/confirm', () => ({
default: ({
isShow,
onConfirm,
onCancel,
title,
}: {
isShow: boolean
onConfirm: () => void
onCancel: () => void
title: string
}) =>
isShow
? (
<div data-testid="confirm-modal">
<span>{title}</span>
<button data-testid="confirm-btn" onClick={onConfirm}>Confirm</button>
<button data-testid="cancel-btn" onClick={onCancel}>Cancel</button>
</div>
)
: null,
}))
vi.mock('../screenshot', () => ({
default: () => <div data-testid="screenshot" />,
}))
@ -77,10 +40,14 @@ vi.mock('../screenshot', () => ({
describe('Conversion', () => {
beforeEach(() => {
vi.clearAllMocks()
})
afterEach(() => {
vi.restoreAllMocks()
vi.spyOn(toastModule.toast, 'success').mockImplementation((...args) => {
mockToastSuccess(...args)
return 'toast-id'
})
vi.spyOn(toastModule.toast, 'error').mockImplementation((...args) => {
mockToastError(...args)
return 'toast-id'
})
})
it('should render conversion title and description', () => {
@ -112,29 +79,31 @@ describe('Conversion', () => {
it('should show confirm modal when convert button clicked', () => {
render(<Conversion />)
expect(screen.queryByTestId('confirm-modal')).not.toBeInTheDocument()
expect(screen.queryByRole('alertdialog')).not.toBeInTheDocument()
fireEvent.click(screen.getByText('datasetPipeline.operations.convert'))
expect(screen.getByTestId('confirm-modal')).toBeInTheDocument()
expect(screen.getByText('datasetPipeline.conversion.confirm.title')).toBeInTheDocument()
const dialog = screen.getByRole('alertdialog')
expect(dialog).toBeInTheDocument()
expect(within(dialog).getByText('datasetPipeline.conversion.confirm.title')).toBeInTheDocument()
})
it('should hide confirm modal when cancel is clicked', () => {
render(<Conversion />)
fireEvent.click(screen.getByText('datasetPipeline.operations.convert'))
expect(screen.getByTestId('confirm-modal')).toBeInTheDocument()
const dialog = screen.getByRole('alertdialog')
expect(dialog).toBeInTheDocument()
fireEvent.click(screen.getByTestId('cancel-btn'))
expect(screen.queryByTestId('confirm-modal')).not.toBeInTheDocument()
fireEvent.click(within(dialog).getByRole('button', { name: 'common.operation.cancel' }))
expect(screen.queryByRole('alertdialog')).not.toBeInTheDocument()
})
it('should call convert when confirm is clicked', () => {
render(<Conversion />)
fireEvent.click(screen.getByText('datasetPipeline.operations.convert'))
fireEvent.click(screen.getByTestId('confirm-btn'))
fireEvent.click(within(screen.getByRole('alertdialog')).getAllByRole('button').at(-1)!)
expect(mockConvert).toHaveBeenCalledWith('ds-123', expect.objectContaining({
onSuccess: expect.any(Function),
@ -150,9 +119,9 @@ describe('Conversion', () => {
render(<Conversion />)
fireEvent.click(screen.getByText('datasetPipeline.operations.convert'))
fireEvent.click(screen.getByTestId('confirm-btn'))
fireEvent.click(within(screen.getByRole('alertdialog')).getAllByRole('button').at(-1)!)
expect(mockToast.success).toHaveBeenCalledWith('datasetPipeline.conversion.successMessage')
expect(mockToastSuccess).toHaveBeenCalledWith('datasetPipeline.conversion.successMessage')
expect(mockInvalidDatasetDetail).toHaveBeenCalled()
})
@ -164,9 +133,9 @@ describe('Conversion', () => {
render(<Conversion />)
fireEvent.click(screen.getByText('datasetPipeline.operations.convert'))
fireEvent.click(screen.getByTestId('confirm-btn'))
fireEvent.click(within(screen.getByRole('alertdialog')).getAllByRole('button').at(-1)!)
expect(mockToast.error).toHaveBeenCalledWith('datasetPipeline.conversion.errorMessage')
expect(mockToastError).toHaveBeenCalledWith('datasetPipeline.conversion.errorMessage')
})
it('should handle conversion error', async () => {
@ -177,8 +146,8 @@ describe('Conversion', () => {
render(<Conversion />)
fireEvent.click(screen.getByText('datasetPipeline.operations.convert'))
fireEvent.click(screen.getByTestId('confirm-btn'))
fireEvent.click(within(screen.getByRole('alertdialog')).getAllByRole('button').at(-1)!)
expect(mockToast.error).toHaveBeenCalledWith('datasetPipeline.conversion.errorMessage')
expect(mockToastError).toHaveBeenCalledWith('datasetPipeline.conversion.errorMessage')
})
})

View File

@ -2,7 +2,7 @@ import * as React from 'react'
import { useCallback, useState } from 'react'
import { useTranslation } from 'react-i18next'
import Button from '@/app/components/base/button'
import Confirm from '@/app/components/base/confirm'
import Confirm from '@/app/components/base/ui/confirm-dialog'
import { toast } from '@/app/components/base/ui/toast'
import { useParams } from '@/next/navigation'
import { datasetDetailQueryKeyPrefix } from '@/service/knowledge/use-dataset'

View File

@ -5,24 +5,6 @@ import Popup from '../popup'
const mockPublishWorkflow = vi.fn().mockResolvedValue({ created_at: '2024-01-01T00:00:00Z' })
const mockPublishAsCustomizedPipeline = vi.fn().mockResolvedValue({})
const toastMocks = vi.hoisted(() => ({
call: vi.fn(),
dismiss: vi.fn(),
update: vi.fn(),
promise: vi.fn(),
}))
vi.mock('@/app/components/base/ui/toast', () => ({
toast: Object.assign(toastMocks.call, {
success: vi.fn((message: string, options?: Record<string, unknown>) => toastMocks.call({ type: 'success', message, ...options })),
error: vi.fn((message: string, options?: Record<string, unknown>) => toastMocks.call({ type: 'error', message, ...options })),
warning: vi.fn((message: string, options?: Record<string, unknown>) => toastMocks.call({ type: 'warning', message, ...options })),
info: vi.fn((message: string, options?: Record<string, unknown>) => toastMocks.call({ type: 'info', message, ...options })),
dismiss: toastMocks.dismiss,
update: toastMocks.update,
promise: toastMocks.promise,
}),
}))
const mockPush = vi.fn()
const mockHandleCheckBeforePublish = vi.fn().mockResolvedValue(true)
const mockSetPublishedAt = vi.fn()
@ -87,24 +69,6 @@ vi.mock('@/app/components/base/button', () => ({
),
}))
vi.mock('@/app/components/base/confirm', () => ({
default: ({ isShow, onConfirm, onCancel, title }: {
isShow: boolean
onConfirm: () => void
onCancel: () => void
title: string
}) =>
isShow
? (
<div data-testid="confirm-modal">
<span>{title}</span>
<button data-testid="publish-confirm" onClick={onConfirm}>OK</button>
<button data-testid="publish-cancel" onClick={onCancel}>Cancel</button>
</div>
)
: null,
}))
vi.mock('@/app/components/base/divider', () => ({
default: () => <hr />,
}))

View File

@ -6,10 +6,10 @@ import { memo, useCallback, useState } from 'react'
import { Trans, useTranslation } from 'react-i18next'
import { trackEvent } from '@/app/components/base/amplitude'
import Button from '@/app/components/base/button'
import Confirm from '@/app/components/base/confirm'
import Divider from '@/app/components/base/divider'
import { SparklesSoft } from '@/app/components/base/icons/src/public/common'
import PremiumBadge from '@/app/components/base/premium-badge'
import Confirm from '@/app/components/base/ui/confirm-dialog'
import { toast } from '@/app/components/base/ui/toast'
import { useChecklistBeforePublish } from '@/app/components/workflow/hooks'
import ShortcutsName from '@/app/components/workflow/shortcuts-name'

View File

@ -1,7 +1,7 @@
import type { ReactNode } from 'react'
import type { ToolWithProvider } from '@/app/components/workflow/types'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
import { fireEvent, render, screen, waitFor, within } from '@testing-library/react'
import * as React from 'react'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import MCPCard from '../provider-card'
@ -49,31 +49,6 @@ vi.mock('../modal', () => ({
},
}))
// Mock the Confirm dialog
type ConfirmDialogProps = {
isShow: boolean
onConfirm: () => void
onCancel: () => void
isLoading: boolean
}
vi.mock('@/app/components/base/confirm', () => ({
default: ({ isShow, onConfirm, onCancel, isLoading }: ConfirmDialogProps) => {
if (!isShow)
return null
return (
<div data-testid="confirm-dialog">
<button data-testid="confirm-delete-btn" onClick={onConfirm} disabled={isLoading}>
{isLoading ? 'Deleting...' : 'Confirm Delete'}
</button>
<button data-testid="cancel-delete-btn" onClick={onCancel}>
Cancel
</button>
</div>
)
},
}))
// Mock the OperationDropdown
type OperationDropdownProps = {
onEdit: () => void
@ -450,7 +425,7 @@ describe('MCPCard', () => {
// Confirm dialog should be shown
await waitFor(() => {
expect(screen.getByTestId('confirm-dialog')).toBeInTheDocument()
expect(screen.getByRole('alertdialog')).toBeInTheDocument()
})
})
@ -462,15 +437,15 @@ describe('MCPCard', () => {
fireEvent.click(removeBtn)
await waitFor(() => {
expect(screen.getByTestId('confirm-dialog')).toBeInTheDocument()
expect(screen.getByRole('alertdialog')).toBeInTheDocument()
})
// Cancel
const cancelBtn = screen.getByTestId('cancel-delete-btn')
const cancelBtn = within(screen.getByRole('alertdialog')).getByRole('button', { name: 'common.operation.cancel' })
fireEvent.click(cancelBtn)
await waitFor(() => {
expect(screen.queryByTestId('confirm-dialog')).not.toBeInTheDocument()
expect(screen.queryByRole('alertdialog')).not.toBeInTheDocument()
})
})
@ -483,11 +458,11 @@ describe('MCPCard', () => {
fireEvent.click(removeBtn)
await waitFor(() => {
expect(screen.getByTestId('confirm-dialog')).toBeInTheDocument()
expect(screen.getByRole('alertdialog')).toBeInTheDocument()
})
// Confirm delete
const confirmBtn = screen.getByTestId('confirm-delete-btn')
const confirmBtn = within(screen.getByRole('alertdialog')).getAllByRole('button').at(-1)!
fireEvent.click(confirmBtn)
await waitFor(() => {
@ -506,11 +481,11 @@ describe('MCPCard', () => {
fireEvent.click(removeBtn)
await waitFor(() => {
expect(screen.getByTestId('confirm-dialog')).toBeInTheDocument()
expect(screen.getByRole('alertdialog')).toBeInTheDocument()
})
// Confirm delete
const confirmBtn = screen.getByTestId('confirm-delete-btn')
const confirmBtn = within(screen.getByRole('alertdialog')).getAllByRole('button').at(-1)!
fireEvent.click(confirmBtn)
await waitFor(() => {

View File

@ -1,7 +1,7 @@
import type { ReactNode } from 'react'
import type { ToolWithProvider } from '@/app/components/workflow/types'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
import { fireEvent, render, screen, waitFor, within } from '@testing-library/react'
import * as React from 'react'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import MCPDetailContent from '../content'
@ -84,20 +84,6 @@ vi.mock('../../modal', () => ({
},
}))
// Mock Confirm dialog
vi.mock('@/app/components/base/confirm', () => ({
default: ({ isShow, onConfirm, onCancel, title }: { isShow: boolean, onConfirm: () => void, onCancel: () => void, title: string }) => {
if (!isShow)
return null
return (
<div data-testid="confirm-dialog" data-title={title}>
<button data-testid="confirm-btn" onClick={onConfirm}>Confirm</button>
<button data-testid="cancel-btn" onClick={onCancel}>Cancel</button>
</div>
)
},
}))
// Mock OperationDropdown
vi.mock('../operation-dropdown', () => ({
default: ({ onEdit, onRemove }: { onEdit: () => void, onRemove: () => void }) => (
@ -494,7 +480,7 @@ describe('MCPDetailContent', () => {
fireEvent.click(updateBtn)
await waitFor(() => {
expect(screen.getByTestId('confirm-dialog')).toBeInTheDocument()
expect(screen.getByRole('alertdialog')).toBeInTheDocument()
})
})
@ -514,11 +500,11 @@ describe('MCPDetailContent', () => {
fireEvent.click(updateBtn)
await waitFor(() => {
expect(screen.getByTestId('confirm-dialog')).toBeInTheDocument()
expect(screen.getByRole('alertdialog')).toBeInTheDocument()
})
// Confirm the update
const confirmBtn = screen.getByTestId('confirm-btn')
const confirmBtn = within(screen.getByRole('alertdialog')).getAllByRole('button').at(-1)!
fireEvent.click(confirmBtn)
await waitFor(() => {
@ -636,7 +622,7 @@ describe('MCPDetailContent', () => {
fireEvent.click(removeBtn)
await waitFor(() => {
expect(screen.getByTestId('confirm-dialog')).toBeInTheDocument()
expect(screen.getByRole('alertdialog')).toBeInTheDocument()
})
})
@ -648,15 +634,15 @@ describe('MCPDetailContent', () => {
fireEvent.click(removeBtn)
await waitFor(() => {
expect(screen.getByTestId('confirm-dialog')).toBeInTheDocument()
expect(screen.getByRole('alertdialog')).toBeInTheDocument()
})
// Cancel
const cancelBtn = screen.getByTestId('cancel-btn')
const cancelBtn = within(screen.getByRole('alertdialog')).getByRole('button', { name: 'common.operation.cancel' })
fireEvent.click(cancelBtn)
await waitFor(() => {
expect(screen.queryByTestId('confirm-dialog')).not.toBeInTheDocument()
expect(screen.queryByRole('alertdialog')).not.toBeInTheDocument()
})
})
@ -669,11 +655,11 @@ describe('MCPDetailContent', () => {
fireEvent.click(removeBtn)
await waitFor(() => {
expect(screen.getByTestId('confirm-dialog')).toBeInTheDocument()
expect(screen.getByRole('alertdialog')).toBeInTheDocument()
})
// Confirm delete
const confirmBtn = screen.getByTestId('confirm-btn')
const confirmBtn = within(screen.getByRole('alertdialog')).getAllByRole('button').at(-1)!
fireEvent.click(confirmBtn)
await waitFor(() => {
@ -692,11 +678,11 @@ describe('MCPDetailContent', () => {
fireEvent.click(removeBtn)
await waitFor(() => {
expect(screen.getByTestId('confirm-dialog')).toBeInTheDocument()
expect(screen.getByRole('alertdialog')).toBeInTheDocument()
})
// Confirm delete
const confirmBtn = screen.getByTestId('confirm-btn')
const confirmBtn = within(screen.getByRole('alertdialog')).getAllByRole('button').at(-1)!
fireEvent.click(confirmBtn)
await waitFor(() => {
@ -840,15 +826,15 @@ describe('MCPDetailContent', () => {
fireEvent.click(updateBtn)
await waitFor(() => {
expect(screen.getByTestId('confirm-dialog')).toBeInTheDocument()
expect(screen.getByRole('alertdialog')).toBeInTheDocument()
})
// Cancel the update
const cancelBtn = screen.getByTestId('cancel-btn')
const cancelBtn = within(screen.getByRole('alertdialog')).getByRole('button', { name: 'common.operation.cancel' })
fireEvent.click(cancelBtn)
await waitFor(() => {
expect(screen.queryByTestId('confirm-dialog')).not.toBeInTheDocument()
expect(screen.queryByRole('alertdialog')).not.toBeInTheDocument()
})
})
})

View File

@ -13,8 +13,8 @@ import { useCallback, useEffect } from 'react'
import { useTranslation } from 'react-i18next'
import ActionButton from '@/app/components/base/action-button'
import Button from '@/app/components/base/button'
import Confirm from '@/app/components/base/confirm'
import Tooltip from '@/app/components/base/tooltip'
import Confirm from '@/app/components/base/ui/confirm-dialog'
import Indicator from '@/app/components/header/indicator'
import Icon from '@/app/components/plugins/card/base/card-icon'
import { useAppContext } from '@/context/app-context'

View File

@ -7,12 +7,12 @@ import { RiEditLine, RiLoopLeftLine } from '@remixicon/react'
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import Button from '@/app/components/base/button'
import Confirm from '@/app/components/base/confirm'
import CopyFeedback from '@/app/components/base/copy-feedback'
import Divider from '@/app/components/base/divider'
import { Mcp } from '@/app/components/base/icons/src/vender/other'
import Switch from '@/app/components/base/switch'
import Tooltip from '@/app/components/base/tooltip'
import Confirm from '@/app/components/base/ui/confirm-dialog'
import Indicator from '@/app/components/header/indicator'
import MCPServerModal from '@/app/components/tools/mcp/mcp-server-modal'
import { useDocLink } from '@/context/i18n'

View File

@ -4,7 +4,7 @@ import { RiHammerFill } from '@remixicon/react'
import { useBoolean } from 'ahooks'
import { useCallback, useState } from 'react'
import { useTranslation } from 'react-i18next'
import Confirm from '@/app/components/base/confirm'
import Confirm from '@/app/components/base/ui/confirm-dialog'
import Indicator from '@/app/components/header/indicator'
import Icon from '@/app/components/plugins/card/base/card-icon'
import { useAppContext } from '@/context/app-context'

View File

@ -1,5 +1,5 @@
import type { Collection } from '../../types'
import { act, cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react'
import { act, cleanup, fireEvent, render, screen, waitFor, within } from '@testing-library/react'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { AuthType, CollectionType } from '../../types'
import ProviderDetail from '../detail'
@ -79,28 +79,6 @@ vi.mock('@/app/components/base/drawer', () => ({
isOpen ? <div data-testid="drawer">{children}</div> : null,
}))
vi.mock('@/app/components/base/confirm', () => ({
default: ({ isShow, onConfirm, onCancel, title }: { isShow: boolean, onConfirm: () => void, onCancel: () => void, title: string }) =>
isShow
? (
<div data-testid="confirm-dialog">
<span>{title}</span>
<button data-testid="confirm-btn" onClick={onConfirm}>Confirm</button>
<button data-testid="cancel-btn" onClick={onCancel}>Cancel</button>
</div>
)
: null,
}))
const mockToastSuccess = vi.hoisted(() => vi.fn())
const mockToastError = vi.hoisted(() => vi.fn())
vi.mock('@/app/components/base/ui/toast', () => ({
toast: {
success: mockToastSuccess,
error: mockToastError,
},
}))
vi.mock('@/app/components/header/indicator', () => ({
default: () => <span data-testid="indicator" />,
}))
@ -552,9 +530,9 @@ describe('ProviderDetail', () => {
})
fireEvent.click(screen.getByText('tools.createTool.editAction'))
fireEvent.click(screen.getByTestId('edit-remove'))
expect(screen.getByTestId('confirm-dialog')).toBeInTheDocument()
expect(screen.getByRole('alertdialog')).toBeInTheDocument()
await act(async () => {
fireEvent.click(screen.getByTestId('confirm-btn'))
fireEvent.click(within(screen.getByRole('alertdialog')).getAllByRole('button').at(-1)!)
})
await waitFor(() => {
expect(mockRemoveCustomCollection).toHaveBeenCalledWith('test-collection')
@ -626,9 +604,9 @@ describe('ProviderDetail', () => {
})
fireEvent.click(screen.getByText('tools.createTool.editAction'))
fireEvent.click(screen.getByTestId('wf-remove'))
expect(screen.getByTestId('confirm-dialog')).toBeInTheDocument()
expect(screen.getByRole('alertdialog')).toBeInTheDocument()
await act(async () => {
fireEvent.click(screen.getByTestId('confirm-btn'))
fireEvent.click(within(screen.getByRole('alertdialog')).getAllByRole('button').at(-1)!)
})
await waitFor(() => {
expect(mockDeleteWorkflowTool).toHaveBeenCalledWith('test-id')
@ -710,9 +688,9 @@ describe('ProviderDetail', () => {
})
fireEvent.click(screen.getByText('tools.createTool.editAction'))
fireEvent.click(screen.getByTestId('edit-remove'))
expect(screen.getByTestId('confirm-dialog')).toBeInTheDocument()
fireEvent.click(screen.getByTestId('cancel-btn'))
expect(screen.queryByTestId('confirm-dialog')).not.toBeInTheDocument()
expect(screen.getByRole('alertdialog')).toBeInTheDocument()
fireEvent.click(within(screen.getByRole('alertdialog')).getByRole('button', { name: 'common.operation.cancel' }))
expect(screen.queryByRole('alertdialog')).not.toBeInTheDocument()
})
})
})

View File

@ -9,10 +9,10 @@ import { useCallback, useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import ActionButton from '@/app/components/base/action-button'
import Button from '@/app/components/base/button'
import Confirm from '@/app/components/base/confirm'
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 Confirm from '@/app/components/base/ui/confirm-dialog'
import { toast } from '@/app/components/base/ui/toast'
import { ConfigurationMethodEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
import Indicator from '@/app/components/header/indicator'

View File

@ -103,7 +103,7 @@ import { WorkflowHistoryProvider } from './workflow-history-store'
import 'reactflow/dist/style.css'
import './style.css'
const Confirm = dynamic(() => import('@/app/components/base/confirm'), {
const Confirm = dynamic(() => import('@/app/components/base/ui/confirm-dialog'), {
ssr: false,
})

View File

@ -2,7 +2,7 @@
import type { FC } from 'react'
import * as React from 'react'
import { useTranslation } from 'react-i18next'
import Confirm from '@/app/components/base/confirm'
import Confirm from '@/app/components/base/ui/confirm-dialog'
type Props = {
isShow: boolean

View File

@ -132,7 +132,7 @@
},
"app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/provider-config-modal.tsx": {
"no-restricted-imports": {
"count": 2
"count": 1
},
"tailwindcss/enforce-consistent-class-order": {
"count": 3
@ -353,7 +353,7 @@
},
"app/components/app-sidebar/dataset-info/dropdown.tsx": {
"no-restricted-imports": {
"count": 2
"count": 1
},
"ts/no-explicit-any": {
"count": 1
@ -415,9 +415,6 @@
}
},
"app/components/app/annotation/batch-action.tsx": {
"no-restricted-imports": {
"count": 1
},
"tailwindcss/enforce-consistent-class-order": {
"count": 4
}
@ -452,11 +449,6 @@
"count": 2
}
},
"app/components/app/annotation/clear-all-annotations-confirm-modal/index.tsx": {
"no-restricted-imports": {
"count": 1
}
},
"app/components/app/annotation/edit-annotation-modal/edit-item/index.tsx": {
"erasable-syntax-only/enums": {
"count": 1
@ -472,9 +464,6 @@
}
},
"app/components/app/annotation/edit-annotation-modal/index.tsx": {
"no-restricted-imports": {
"count": 1
},
"tailwindcss/enforce-consistent-class-order": {
"count": 2
}
@ -517,11 +506,6 @@
"count": 8
}
},
"app/components/app/annotation/remove-annotation-confirm-modal/index.tsx": {
"no-restricted-imports": {
"count": 1
}
},
"app/components/app/annotation/type.ts": {
"erasable-syntax-only/enums": {
"count": 2
@ -531,9 +515,6 @@
"erasable-syntax-only/enums": {
"count": 1
},
"no-restricted-imports": {
"count": 1
},
"react/set-state-in-effect": {
"count": 5
},
@ -571,9 +552,6 @@
}
},
"app/components/app/app-publisher/features-wrapper.tsx": {
"no-restricted-imports": {
"count": 1
},
"ts/no-explicit-any": {
"count": 4
}
@ -711,7 +689,7 @@
},
"app/components/app/configuration/config-var/index.tsx": {
"no-restricted-imports": {
"count": 2
"count": 1
},
"tailwindcss/enforce-consistent-class-order": {
"count": 1
@ -839,7 +817,7 @@
},
"app/components/app/configuration/config/automatic/get-automatic-res.tsx": {
"no-restricted-imports": {
"count": 2
"count": 1
},
"react/set-state-in-effect": {
"count": 4
@ -910,7 +888,7 @@
},
"app/components/app/configuration/config/code-generator/get-code-generator-res.tsx": {
"no-restricted-imports": {
"count": 2
"count": 1
},
"react/set-state-in-effect": {
"count": 4
@ -1297,7 +1275,7 @@
},
"app/components/app/switch-app-modal/index.tsx": {
"no-restricted-imports": {
"count": 2
"count": 1
},
"react/set-state-in-effect": {
"count": 1
@ -1653,9 +1631,6 @@
}
},
"app/components/base/chat/chat-with-history/header-in-mobile.tsx": {
"no-restricted-imports": {
"count": 1
},
"tailwindcss/enforce-consistent-class-order": {
"count": 4
},
@ -1665,7 +1640,7 @@
},
"app/components/base/chat/chat-with-history/header/index.tsx": {
"no-restricted-imports": {
"count": 2
"count": 1
},
"tailwindcss/enforce-consistent-class-order": {
"count": 1
@ -1719,9 +1694,6 @@
}
},
"app/components/base/chat/chat-with-history/sidebar/index.tsx": {
"no-restricted-imports": {
"count": 1
},
"tailwindcss/enforce-consistent-class-order": {
"count": 1
}
@ -1980,22 +1952,6 @@
"count": 3
}
},
"app/components/base/confirm/index.stories.tsx": {
"no-console": {
"count": 4
},
"ts/no-explicit-any": {
"count": 1
}
},
"app/components/base/confirm/index.tsx": {
"react/set-state-in-effect": {
"count": 2
},
"tailwindcss/enforce-consistent-class-order": {
"count": 6
}
},
"app/components/base/content-dialog/index.stories.tsx": {
"react/set-state-in-effect": {
"count": 1
@ -3730,7 +3686,7 @@
},
"app/components/base/tag-management/tag-item-editor.tsx": {
"no-restricted-imports": {
"count": 2
"count": 1
},
"tailwindcss/enforce-consistent-class-order": {
"count": 2
@ -4209,7 +4165,7 @@
},
"app/components/datasets/create-from-pipeline/list/template-card/index.tsx": {
"no-restricted-imports": {
"count": 2
"count": 1
}
},
"app/components/datasets/create-from-pipeline/list/template-card/operations.tsx": {
@ -4525,7 +4481,7 @@
},
"app/components/datasets/documents/components/operations.tsx": {
"no-restricted-imports": {
"count": 3
"count": 2
},
"tailwindcss/enforce-consistent-class-order": {
"count": 1
@ -4792,9 +4748,6 @@
}
},
"app/components/datasets/documents/detail/completed/common/batch-action.tsx": {
"no-restricted-imports": {
"count": 1
},
"tailwindcss/enforce-consistent-class-order": {
"count": 2
}
@ -4893,7 +4846,7 @@
},
"app/components/datasets/documents/detail/completed/segment-card/index.tsx": {
"no-restricted-imports": {
"count": 2
"count": 1
},
"tailwindcss/enforce-consistent-class-order": {
"count": 3
@ -5034,7 +4987,7 @@
},
"app/components/datasets/external-api/external-api-modal/index.tsx": {
"no-restricted-imports": {
"count": 3
"count": 2
},
"react/set-state-in-effect": {
"count": 1
@ -5049,9 +5002,6 @@
}
},
"app/components/datasets/external-api/external-knowledge-api-card/index.tsx": {
"no-restricted-imports": {
"count": 1
},
"tailwindcss/enforce-consistent-class-order": {
"count": 1
}
@ -5227,11 +5177,6 @@
"count": 9
}
},
"app/components/datasets/list/dataset-card/components/dataset-card-modals.tsx": {
"no-restricted-imports": {
"count": 1
}
},
"app/components/datasets/list/dataset-card/components/description.tsx": {
"tailwindcss/enforce-consistent-class-order": {
"count": 1
@ -5344,7 +5289,7 @@
},
"app/components/datasets/metadata/metadata-dataset/dataset-metadata-drawer.tsx": {
"no-restricted-imports": {
"count": 3
"count": 2
},
"tailwindcss/enforce-consistent-class-order": {
"count": 4
@ -5535,7 +5480,7 @@
},
"app/components/develop/secret-key/secret-key-modal.tsx": {
"no-restricted-imports": {
"count": 2
"count": 1
},
"tailwindcss/enforce-consistent-class-order": {
"count": 2
@ -5800,11 +5745,6 @@
"count": 2
}
},
"app/components/header/account-setting/api-based-extension-page/item.tsx": {
"no-restricted-imports": {
"count": 1
}
},
"app/components/header/account-setting/api-based-extension-page/modal.tsx": {
"no-restricted-imports": {
"count": 1
@ -5827,9 +5767,6 @@
}
},
"app/components/header/account-setting/data-source-page-new/card.tsx": {
"no-restricted-imports": {
"count": 1
},
"tailwindcss/enforce-consistent-class-order": {
"count": 3
},
@ -6047,7 +5984,7 @@
},
"app/components/header/account-setting/model-provider-page/model-auth/authorized/index.tsx": {
"no-restricted-imports": {
"count": 3
"count": 2
},
"tailwindcss/enforce-consistent-class-order": {
"count": 2
@ -6282,7 +6219,7 @@
},
"app/components/header/account-setting/model-provider-page/provider-added-card/model-load-balancing-modal.tsx": {
"no-restricted-imports": {
"count": 2
"count": 1
},
"react/set-state-in-effect": {
"count": 1
@ -6660,7 +6597,7 @@
},
"app/components/plugins/plugin-auth/authorized/index.tsx": {
"no-restricted-imports": {
"count": 3
"count": 2
},
"tailwindcss/enforce-consistent-class-order": {
"count": 2
@ -6807,7 +6744,7 @@
},
"app/components/plugins/plugin-detail-panel/endpoint-card.tsx": {
"no-restricted-imports": {
"count": 2
"count": 1
},
"tailwindcss/enforce-consistent-class-order": {
"count": 5
@ -7077,7 +7014,7 @@
},
"app/components/plugins/plugin-item/action.tsx": {
"no-restricted-imports": {
"count": 2
"count": 1
}
},
"app/components/plugins/plugin-item/index.tsx": {
@ -7307,9 +7244,6 @@
}
},
"app/components/rag-pipeline/components/conversion.tsx": {
"no-restricted-imports": {
"count": 1
},
"tailwindcss/enforce-consistent-class-order": {
"count": 1
}
@ -7472,9 +7406,6 @@
}
},
"app/components/rag-pipeline/components/rag-pipeline-header/publisher/popup.tsx": {
"no-restricted-imports": {
"count": 1
},
"tailwindcss/enforce-consistent-class-order": {
"count": 7
}
@ -7735,7 +7666,7 @@
},
"app/components/tools/mcp/detail/content.tsx": {
"no-restricted-imports": {
"count": 2
"count": 1
},
"tailwindcss/enforce-consistent-class-order": {
"count": 9
@ -7793,7 +7724,7 @@
},
"app/components/tools/mcp/mcp-service-card.tsx": {
"no-restricted-imports": {
"count": 2
"count": 1
},
"tailwindcss/enforce-consistent-class-order": {
"count": 7
@ -7808,9 +7739,6 @@
}
},
"app/components/tools/mcp/provider-card.tsx": {
"no-restricted-imports": {
"count": 1
},
"tailwindcss/enforce-consistent-class-order": {
"count": 7
},
@ -7845,9 +7773,6 @@
}
},
"app/components/tools/provider/detail.tsx": {
"no-restricted-imports": {
"count": 1
},
"tailwindcss/enforce-consistent-class-order": {
"count": 3
}
@ -8776,11 +8701,6 @@
"count": 1
}
},
"app/components/workflow/nodes/_base/components/remove-effect-var-confirm.tsx": {
"no-restricted-imports": {
"count": 1
}
},
"app/components/workflow/nodes/_base/components/retry/retry-on-node.tsx": {
"tailwindcss/enforce-consistent-class-order": {
"count": 1
@ -11686,9 +11606,6 @@
}
},
"hooks/use-pay.tsx": {
"no-restricted-imports": {
"count": 2
},
"react/set-state-in-effect": {
"count": 4
}

View File

@ -1,9 +1,9 @@
'use client'
import type { IConfirm } from '@/app/components/base/confirm'
import type { IConfirm } from '@/app/components/base/ui/confirm-dialog'
import { useCallback, useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import Confirm from '@/app/components/base/confirm'
import Confirm from '@/app/components/base/ui/confirm-dialog'
import { useRouter, useSearchParams } from '@/next/navigation'
import { useNotionBinding } from '@/service/use-common'