diff --git a/web/__tests__/apps/app-card-operations-flow.test.tsx b/web/__tests__/apps/app-card-operations-flow.test.tsx index 765c7045e5..8e45367db4 100644 --- a/web/__tests__/apps/app-card-operations-flow.test.tsx +++ b/web/__tests__/apps/app-card-operations-flow.test.tsx @@ -207,20 +207,6 @@ vi.mock('@/app/components/app/switch-app-modal', () => ({ }, })) -vi.mock('@/app/components/base/confirm', () => ({ - default: ({ isShow, onConfirm, onCancel, title }: Record) => { - if (!isShow) - return null - return ( -
- {title as string} - - -
- ) - }, -})) - vi.mock('@/app/components/workflow/dsl-export-confirm-modal', () => ({ default: ({ onConfirm, onClose }: Record) => (
@@ -342,14 +328,16 @@ describe('App Card Operations Flow', () => { fireEvent.click(deleteBtn) }) - const confirmBtn = screen.queryByTestId('confirm-delete') - if (confirmBtn) { - fireEvent.click(confirmBtn) + await waitFor(() => { + expect(screen.getByText('app.deleteAppConfirmTitle')).toBeInTheDocument() + }) - await waitFor(() => { - expect(mockDeleteAppMutation).toHaveBeenCalledWith('app-to-delete') - }) - } + fireEvent.change(screen.getByRole('textbox'), { target: { value: 'Deletable App' } }) + fireEvent.click(screen.getByRole('button', { name: 'common.operation.confirm' })) + + await waitFor(() => { + expect(mockDeleteAppMutation).toHaveBeenCalledWith('app-to-delete') + }) } }) }) diff --git a/web/__tests__/tools/tool-provider-detail-flow.test.tsx b/web/__tests__/tools/tool-provider-detail-flow.test.tsx index 3d66467695..ce5ffe531e 100644 --- a/web/__tests__/tools/tool-provider-detail-flow.test.tsx +++ b/web/__tests__/tools/tool-provider-detail-flow.test.tsx @@ -133,26 +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 - ? ( -
- {title} - - -
- ) - : null - ), -})) - vi.mock('@/app/components/base/ui/toast', () => ({ default: { notify: vi.fn() }, toast: { @@ -242,6 +222,8 @@ vi.mock('@/app/components/tools/provider/tool-item', () => ({ const { default: ProviderDetail } = await import('@/app/components/tools/provider/detail') +const getDeleteConfirmButton = () => screen.getByRole('button', { name: /operation\.confirm$/ }) + const makeCollection = (overrides: Partial = {}): Collection => ({ id: 'test-collection', name: 'test_collection', @@ -465,11 +447,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() }) - fireEvent.click(screen.getByTestId('confirm-ok')) + fireEvent.click(getDeleteConfirmButton()) await waitFor(() => { expect(mockRemoveCustomCollection).toHaveBeenCalledWith('test_collection') expect(mockOnRefreshData).toHaveBeenCalled() @@ -527,10 +508,10 @@ describe('Tool Provider Detail Flow Integration', () => { fireEvent.click(screen.getByTestId('wf-modal-remove')) await waitFor(() => { - expect(screen.getByTestId('confirm-dialog')).toBeInTheDocument() + expect(screen.getByText('Delete Tool')).toBeInTheDocument() }) - fireEvent.click(screen.getByTestId('confirm-ok')) + fireEvent.click(getDeleteConfirmButton()) await waitFor(() => { expect(mockDeleteWorkflowTool).toHaveBeenCalledWith('test-collection') expect(mockOnRefreshData).toHaveBeenCalled() diff --git a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/provider-config-modal.tsx b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/provider-config-modal.tsx index caf6562a3e..c63d482c8f 100644 --- a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/provider-config-modal.tsx +++ b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/provider-config-modal.tsx @@ -5,7 +5,6 @@ import { useBoolean } from 'ahooks' 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 { LinkExternal02 } from '@/app/components/base/icons/src/vender/line/general' import { Lock01 } from '@/app/components/base/icons/src/vender/solid/security' @@ -14,6 +13,15 @@ import { PortalToFollowElemContent, } from '@/app/components/base/portal-to-follow-elem' import { Button } from '@/app/components/base/ui/button' +import { + AlertDialog, + AlertDialogActions, + AlertDialogCancelButton, + AlertDialogConfirmButton, + AlertDialogContent, + AlertDialogDescription, + AlertDialogTitle, +} from '@/app/components/base/ui/alert-dialog' import { toast } from '@/app/components/base/ui/toast' import { addTracingConfig, removeTracingConfig, updateTracingConfig } from '@/service/apps' import { docURL } from './config' @@ -679,14 +687,24 @@ const ProviderConfigModal: FC = ({ ) : ( - + !open && hideRemoveConfirm()}> + +
+ + {t(`${I18N_PREFIX}.removeConfirmTitle`, { ns: 'app', key: t(`tracing.${type}.title`, { ns: 'app' }) })!} + + + {t(`${I18N_PREFIX}.removeConfirmContent`, { ns: 'app' })} + +
+ + {t('operation.cancel', { ns: 'common' })} + + {t('operation.confirm', { ns: 'common' })} + + +
+
)} ) diff --git a/web/app/components/app-sidebar/app-info/__tests__/app-info-modals.spec.tsx b/web/app/components/app-sidebar/app-info/__tests__/app-info-modals.spec.tsx index 2f98089e40..707b0da267 100644 --- a/web/app/components/app-sidebar/app-info/__tests__/app-info-modals.spec.tsx +++ b/web/app/components/app-sidebar/app-info/__tests__/app-info-modals.spec.tsx @@ -1,5 +1,5 @@ import type { App, AppSSO } from '@/types/app' -import { act, render, screen, waitFor } from '@testing-library/react' +import { act, fireEvent, render, screen, waitFor } 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 - ? ( -
- - -
- ) - : null - ), -})) - vi.mock('@/app/components/workflow/update-dsl-modal', () => ({ default: ({ onCancel, onBackup }: { onCancel: () => void, onBackup: () => void }) => (
@@ -113,7 +95,7 @@ describe('AppInfoModals', () => { render() }) expect(screen.queryByTestId('switch-modal')).not.toBeInTheDocument() - expect(screen.queryByTestId('confirm-modal')).not.toBeInTheDocument() + expect(screen.queryByText('app.deleteAppConfirmTitle')).not.toBeInTheDocument() }) it('should render SwitchAppModal when activeModal is switch', async () => { @@ -143,14 +125,13 @@ describe('AppInfoModals', () => { }) }) - it('should render Confirm for delete when activeModal is delete', async () => { + it('should render delete alert dialog when activeModal is delete', async () => { await act(async () => { render() }) await waitFor(() => { - const confirm = screen.getByTestId('confirm-modal') - expect(confirm).toBeInTheDocument() - expect(confirm).toHaveAttribute('data-title', 'app.deleteAppConfirmTitle') + expect(screen.getByText('app.deleteAppConfirmTitle')).toBeInTheDocument() + expect(screen.getByRole('textbox')).toBeInTheDocument() }) }) @@ -163,14 +144,12 @@ describe('AppInfoModals', () => { }) }) - it('should render export warning Confirm when activeModal is exportWarning', async () => { + it('should render export warning alert dialog when activeModal is exportWarning', async () => { await act(async () => { render() }) await waitFor(() => { - const confirm = screen.getByTestId('confirm-modal') - expect(confirm).toBeInTheDocument() - expect(confirm).toHaveAttribute('data-title', 'workflow.sidebar.exportWarning') + expect(screen.getByText('workflow.sidebar.exportWarning')).toBeInTheDocument() }) }) @@ -202,20 +181,47 @@ describe('AppInfoModals', () => { render() }) - await waitFor(() => expect(screen.getByText('Cancel')).toBeInTheDocument()) - await user.click(screen.getByText('Cancel')) + await waitFor(() => expect(screen.getByRole('button', { name: 'common.operation.cancel' })).toBeInTheDocument()) + await user.click(screen.getByRole('button', { name: 'common.operation.cancel' })) expect(defaultProps.closeModal).toHaveBeenCalledTimes(1) }) + it('should clear the delete confirmation input when delete modal is cancelled', async () => { + const user = userEvent.setup() + await act(async () => { + render() + }) + + const input = await screen.findByRole('textbox') + await user.type(input, 'wrong-name') + await user.click(screen.getByRole('button', { name: 'common.operation.cancel' })) + + expect(defaultProps.closeModal).toHaveBeenCalledTimes(1) + expect(input).toHaveValue('') + }) + + it('should not confirm delete when the form is submitted with unmatched input', async () => { + await act(async () => { + render() + }) + + const form = document.querySelector('form') + expect(form).toBeTruthy() + + fireEvent.submit(form!) + + expect(defaultProps.onConfirmDelete).not.toHaveBeenCalled() + }) + it('should call onConfirmDelete when confirm on delete modal', async () => { const user = userEvent.setup() await act(async () => { render() }) - await waitFor(() => expect(screen.getByText('Confirm')).toBeInTheDocument()) - await user.click(screen.getByText('Confirm')) + await user.type(screen.getByRole('textbox'), 'Test App') + await user.click(screen.getByRole('button', { name: 'common.operation.confirm' })) expect(defaultProps.onConfirmDelete).toHaveBeenCalledTimes(1) }) @@ -226,8 +232,8 @@ describe('AppInfoModals', () => { render() }) - await waitFor(() => expect(screen.getByText('Confirm')).toBeInTheDocument()) - await user.click(screen.getByText('Confirm')) + await waitFor(() => expect(screen.getByRole('button', { name: 'common.operation.confirm' })).toBeInTheDocument()) + await user.click(screen.getByRole('button', { name: 'common.operation.confirm' })) expect(defaultProps.handleConfirmExport).toHaveBeenCalledTimes(1) }) diff --git a/web/app/components/app-sidebar/app-info/app-info-modals.tsx b/web/app/components/app-sidebar/app-info/app-info-modals.tsx index 6b76be87bb..fa84ecc0b6 100644 --- a/web/app/components/app-sidebar/app-info/app-info-modals.tsx +++ b/web/app/components/app-sidebar/app-info/app-info-modals.tsx @@ -6,12 +6,21 @@ import type { App, AppSSO } from '@/types/app' import * as React from 'react' import { useState } 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' 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 UpdateDSLModal = dynamic(() => import('@/app/components/workflow/update-dsl-modal'), { ssr: false }) const DSLExportConfirmModal = dynamic(() => import('@/app/components/workflow/dsl-export-confirm-modal'), { ssr: false }) @@ -44,6 +53,12 @@ const AppInfoModals = ({ }: AppInfoModalsProps) => { const { t } = useTranslation() const [confirmDeleteInput, setConfirmDeleteInput] = useState('') + const isDeleteConfirmDisabled = confirmDeleteInput !== appDetail.name + + const handleDeleteDialogClose = () => { + setConfirmDeleteInput('') + closeModal() + } return ( <> @@ -85,39 +100,73 @@ const AppInfoModals = ({ onHide={closeModal} /> )} - {activeModal === 'delete' && ( - { - setConfirmDeleteInput('') - closeModal() - }} - /> - )} + !open && handleDeleteDialogClose()}> + +
{ + e.preventDefault() + if (isDeleteConfirmDisabled) + return + onConfirmDelete() + }} + > +
+ + {t('deleteAppConfirmTitle', { ns: 'app' })} + + + {t('deleteAppConfirmContent', { ns: 'app' })} + +
+ + setConfirmDeleteInput(e.target.value)} + /> +
+
+ + + {t('operation.cancel', { ns: 'common' })} + + + {t('operation.confirm', { ns: 'common' })} + + +
+
+
{activeModal === 'importDSL' && ( )} - {activeModal === 'exportWarning' && ( - - )} + !open && closeModal()}> + +
+ + {t('sidebar.exportWarning', { ns: 'workflow' })} + + + {t('sidebar.exportWarningDesc', { ns: 'workflow' })} + +
+ + {t('operation.cancel', { ns: 'common' })} + + {t('operation.confirm', { ns: 'common' })} + + +
+
{secretEnvList.length > 0 && ( ({ }, })) -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 ( -
- {title} - {content} - - -
- ) - }, -})) - vi.mock('@/app/components/base/portal-to-follow-elem', () => ({ PortalToFollowElem: ({ children }: { children: React.ReactNode }) =>
{children}
, PortalToFollowElemTrigger: ({ children, onClick }: { children: React.ReactNode, onClick?: () => void }) => ( @@ -221,13 +194,13 @@ describe('Dropdown callback coverage', () => { await user.click(screen.getByText('common.operation.delete')) await waitFor(() => { - expect(screen.getByTestId('confirm-dialog')).toBeInTheDocument() + expect(screen.getByText('dataset.deleteDatasetConfirmTitle')).toBeInTheDocument() }) - await user.click(screen.getByText('cancel')) + await user.click(screen.getByRole('button', { name: 'common.operation.cancel' })) await waitFor(() => { - expect(screen.queryByTestId('confirm-dialog')).not.toBeInTheDocument() + expect(screen.queryByText('dataset.deleteDatasetConfirmTitle')).not.toBeInTheDocument() }) }) @@ -273,6 +246,6 @@ describe('Dropdown callback coverage', () => { await waitFor(() => { expect(mockToast).toHaveBeenCalledWith('check failed', { type: 'error' }) }) - expect(screen.queryByTestId('confirm-dialog')).not.toBeInTheDocument() + expect(screen.queryByText('dataset.deleteDatasetConfirmTitle')).not.toBeInTheDocument() }) }) diff --git a/web/app/components/app-sidebar/dataset-info/dropdown.tsx b/web/app/components/app-sidebar/dataset-info/dropdown.tsx index 6c70f96b34..33092865f8 100644 --- a/web/app/components/app-sidebar/dataset-info/dropdown.tsx +++ b/web/app/components/app-sidebar/dataset-info/dropdown.tsx @@ -14,8 +14,16 @@ 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 { + AlertDialog, + AlertDialogActions, + AlertDialogCancelButton, + AlertDialogConfirmButton, + AlertDialogContent, + AlertDialogDescription, + AlertDialogTitle, +} from '../../base/ui/alert-dialog' import RenameDatasetModal from '../../datasets/rename-modal' import Menu from './menu' @@ -135,15 +143,26 @@ const DropDown = ({ onSuccess={refreshDataset} /> )} - {showConfirmDelete && ( - setShowConfirmDelete(false)} - /> - )} + !open && setShowConfirmDelete(false)}> + +
+ + {t('deleteDatasetConfirmTitle', { ns: 'dataset' })} + + + {confirmMessage} + +
+ + + {t('operation.cancel', { ns: 'common' })} + + + {t('operation.confirm', { ns: 'common' })} + + +
+
) } diff --git a/web/app/components/app/annotation/__tests__/batch-action.spec.tsx b/web/app/components/app/annotation/__tests__/batch-action.spec.tsx index 95dddd4b23..102465e116 100644 --- a/web/app/components/app/annotation/__tests__/batch-action.spec.tsx +++ b/web/app/components/app/annotation/__tests__/batch-action.spec.tsx @@ -1,4 +1,4 @@ -import { act, fireEvent, render, screen, waitFor } from '@testing-library/react' +import { fireEvent, render, screen, waitFor, within } from '@testing-library/react' import * as React from 'react' import BatchAction from '../batch-action' @@ -29,14 +29,28 @@ describe('BatchAction', () => { render() fireEvent.click(screen.getByRole('button', { name: 'common.operation.delete' })) - await screen.findByText('appAnnotation.list.delete.title') + const dialog = await screen.findByRole('alertdialog') + expect(within(dialog).getByText('appAnnotation.list.delete.title')).toBeInTheDocument() - await act(async () => { - fireEvent.click(screen.getAllByRole('button', { name: 'common.operation.delete' })[1]) - }) + fireEvent.click(within(dialog).getByRole('button', { name: 'common.operation.delete' })) await waitFor(() => { expect(onBatchDelete).toHaveBeenCalledTimes(1) }) }) + + it('should hide delete confirmation when cancel is clicked', async () => { + const onBatchDelete = vi.fn() + render() + + fireEvent.click(screen.getByRole('button', { name: 'common.operation.delete' })) + const dialog = await screen.findByRole('alertdialog') + + fireEvent.click(within(dialog).getByRole('button', { name: 'common.operation.cancel' })) + + await waitFor(() => { + expect(screen.queryByRole('alertdialog')).not.toBeInTheDocument() + }) + expect(onBatchDelete).not.toHaveBeenCalled() + }) }) diff --git a/web/app/components/app/annotation/batch-action.tsx b/web/app/components/app/annotation/batch-action.tsx index 76aab79079..e59e57b392 100644 --- a/web/app/components/app/annotation/batch-action.tsx +++ b/web/app/components/app/annotation/batch-action.tsx @@ -3,8 +3,15 @@ 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 { + AlertDialog, + AlertDialogActions, + AlertDialogCancelButton, + AlertDialogConfirmButton, + AlertDialogContent, + AlertDialogTitle, +} from '@/app/components/base/ui/alert-dialog' import { cn } from '@/utils/classnames' const i18nPrefix = 'batchAction' @@ -41,38 +48,42 @@ const BatchAction: FC = ({ return (
-
+
{selectedIds.length} - {t(`${i18nPrefix}.selected`, { ns: 'appAnnotation' })} + {t(`${i18nPrefix}.selected`, { ns: 'appAnnotation' })}
-
-
- { - isShowDeleteConfirm && ( - - ) - } + !open && hideDeleteConfirm()}> + +
+ + {t('list.delete.title', { ns: 'appAnnotation' })} + +
+ + + {t('operation.cancel', { ns: 'common' })} + + + {t('operation.delete', { ns: 'common' })} + + +
+
) } diff --git a/web/app/components/app/annotation/clear-all-annotations-confirm-modal/index.tsx b/web/app/components/app/annotation/clear-all-annotations-confirm-modal/index.tsx index 3d5fd566d2..b960e1d59c 100644 --- a/web/app/components/app/annotation/clear-all-annotations-confirm-modal/index.tsx +++ b/web/app/components/app/annotation/clear-all-annotations-confirm-modal/index.tsx @@ -3,7 +3,14 @@ import type { FC } from 'react' import * as React from 'react' import { useTranslation } from 'react-i18next' -import Confirm from '@/app/components/base/confirm' +import { + AlertDialog, + AlertDialogActions, + AlertDialogCancelButton, + AlertDialogConfirmButton, + AlertDialogContent, + AlertDialogTitle, +} from '@/app/components/base/ui/alert-dialog' type Props = { isShow: boolean @@ -17,15 +24,26 @@ const ClearAllAnnotationsConfirmModal: FC = ({ onConfirm, }) => { const { t } = useTranslation() + const title = t('table.header.clearAllConfirm', { ns: 'appAnnotation' }) return ( - + !open && onHide()}> + +
+ + {title} + +
+ + + {t('operation.cancel', { ns: 'common' })} + + + {t('operation.confirm', { ns: 'common' })} + + +
+
) } diff --git a/web/app/components/app/annotation/edit-annotation-modal/__tests__/index.spec.tsx b/web/app/components/app/annotation/edit-annotation-modal/__tests__/index.spec.tsx index 271331e49f..53a6f7356a 100644 --- a/web/app/components/app/annotation/edit-annotation-modal/__tests__/index.spec.tsx +++ b/web/app/components/app/annotation/edit-annotation-modal/__tests__/index.spec.tsx @@ -332,6 +332,24 @@ describe('EditAnnotationModal', () => { // Assert expect(mockOnRemove).toHaveBeenCalled() }) + + it('should hide confirm modal when removal is cancelled', async () => { + const props = { + ...defaultProps, + annotationId: 'test-annotation-id', + } + const user = userEvent.setup() + + render() + await user.click(screen.getByText('appAnnotation.editModal.removeThisCache')) + expect(screen.getByText('appDebug.feature.annotation.removeConfirm')).toBeInTheDocument() + + await user.click(screen.getByRole('button', { name: 'common.operation.cancel' })) + + await waitFor(() => { + expect(screen.queryByText('appDebug.feature.annotation.removeConfirm')).not.toBeInTheDocument() + }) + }) }) // Edge Cases (REQUIRED) diff --git a/web/app/components/app/annotation/edit-annotation-modal/index.tsx b/web/app/components/app/annotation/edit-annotation-modal/index.tsx index 2fd232f26c..53bf813388 100644 --- a/web/app/components/app/annotation/edit-annotation-modal/index.tsx +++ b/web/app/components/app/annotation/edit-annotation-modal/index.tsx @@ -3,9 +3,16 @@ 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 { + AlertDialog, + AlertDialogActions, + AlertDialogCancelButton, + AlertDialogConfirmButton, + AlertDialogContent, + AlertDialogTitle, +} from '@/app/components/base/ui/alert-dialog' import { toast } from '@/app/components/base/ui/toast' import AnnotationFull from '@/app/components/billing/annotation-full' import { useProviderContext } from '@/context/provider-context' @@ -106,16 +113,33 @@ const EditAnnotationModal: FC = ({ readonly={isAdd && isAnnotationFull} onSave={editedContent => handleSave(EditItemType.Answer, editedContent)} /> - setShowModal(false)} - onConfirm={() => { - onRemove() - setShowModal(false) - onHide() - }} - title={t('feature.annotation.removeConfirm', { ns: 'appDebug' })} - /> + !open && setShowModal(false)}> + +
+ + {t('feature.annotation.removeConfirm', { ns: 'appDebug' })} + +
+ + + {t('operation.cancel', { ns: 'common' })} + + { + onRemove() + setShowModal(false) + onHide() + }} + > + {t('operation.confirm', { ns: 'common' })} + + +
+
)} diff --git a/web/app/components/app/annotation/remove-annotation-confirm-modal/index.tsx b/web/app/components/app/annotation/remove-annotation-confirm-modal/index.tsx index f52e6c74c5..70e28b048c 100644 --- a/web/app/components/app/annotation/remove-annotation-confirm-modal/index.tsx +++ b/web/app/components/app/annotation/remove-annotation-confirm-modal/index.tsx @@ -2,7 +2,14 @@ import type { FC } from 'react' import * as React from 'react' import { useTranslation } from 'react-i18next' -import Confirm from '@/app/components/base/confirm' +import { + AlertDialog, + AlertDialogActions, + AlertDialogCancelButton, + AlertDialogConfirmButton, + AlertDialogContent, + AlertDialogTitle, +} from '@/app/components/base/ui/alert-dialog' type Props = { isShow: boolean @@ -16,14 +23,26 @@ const RemoveAnnotationConfirmModal: FC = ({ onRemove, }) => { const { t } = useTranslation() + const title = t('feature.annotation.removeConfirm', { ns: 'appDebug' }) return ( - + !open && onHide()}> + +
+ + {title} + +
+ + + {t('operation.cancel', { ns: 'common' })} + + + {t('operation.confirm', { ns: 'common' })} + + +
+
) } export default React.memo(RemoveAnnotationConfirmModal) diff --git a/web/app/components/app/annotation/view-annotation-modal/__tests__/index.spec.tsx b/web/app/components/app/annotation/view-annotation-modal/__tests__/index.spec.tsx index 4490772716..d05fa27ccd 100644 --- a/web/app/components/app/annotation/view-annotation-modal/__tests__/index.spec.tsx +++ b/web/app/components/app/annotation/view-annotation-modal/__tests__/index.spec.tsx @@ -157,4 +157,17 @@ describe('ViewAnnotationModal', () => { expect(props.onHide).toHaveBeenCalledTimes(1) }) }) + + it('should close the remove confirmation when cancelled', async () => { + renderComponent() + + fireEvent.click(screen.getByText('appAnnotation.editModal.removeThisCache')) + expect(await screen.findByText('appDebug.feature.annotation.removeConfirm')).toBeInTheDocument() + + fireEvent.click(screen.getByRole('button', { name: 'common.operation.cancel' })) + + await waitFor(() => { + expect(screen.queryByText('appDebug.feature.annotation.removeConfirm')).not.toBeInTheDocument() + }) + }) }) diff --git a/web/app/components/app/annotation/view-annotation-modal/index.tsx b/web/app/components/app/annotation/view-annotation-modal/index.tsx index 644ff808fd..d9ebe093f5 100644 --- a/web/app/components/app/annotation/view-annotation-modal/index.tsx +++ b/web/app/components/app/annotation/view-annotation-modal/index.tsx @@ -5,11 +5,18 @@ 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 { + AlertDialog, + AlertDialogActions, + AlertDialogCancelButton, + AlertDialogConfirmButton, + AlertDialogContent, + AlertDialogTitle, +} from '@/app/components/base/ui/alert-dialog' import { APP_PAGE_LIMIT } from '@/config' import useTimestamp from '@/hooks/use-timestamp' import { fetchHitHistoryList } from '@/service/annotation' @@ -212,16 +219,33 @@ const ViewAnnotationModal: FC = ({
{activeTab === TabType.annotation ? annotationTab : hitHistoryTab}
- setShowModal(false)} - onConfirm={async () => { - await onRemove() - setShowModal(false) - onHide() - }} - title={t('feature.annotation.removeConfirm', { ns: 'appDebug' })} - /> + !open && setShowModal(false)}> + +
+ + {t('feature.annotation.removeConfirm', { ns: 'appDebug' })} + +
+ + + {t('operation.cancel', { ns: 'common' })} + + { + await onRemove() + setShowModal(false) + onHide() + }} + > + {t('operation.confirm', { ns: 'common' })} + + +
+
)} foot={id diff --git a/web/app/components/app/app-publisher/__tests__/features-wrapper.spec.tsx b/web/app/components/app/app-publisher/__tests__/features-wrapper.spec.tsx index 739b0002ee..e9e54d1106 100644 --- a/web/app/components/app/app-publisher/__tests__/features-wrapper.spec.tsx +++ b/web/app/components/app/app-publisher/__tests__/features-wrapper.spec.tsx @@ -1,5 +1,5 @@ /* eslint-disable ts/no-explicit-any */ -import { fireEvent, render, screen, waitFor } from '@testing-library/react' +import { fireEvent, render, screen, waitFor, within } from '@testing-library/react' import FeaturesWrappedAppPublisher from '../features-wrapper' const mockSetFeatures = vi.fn() @@ -137,4 +137,23 @@ describe('FeaturesWrappedAppPublisher', () => { })) }) }) + + it('should close restore confirmation without restoring when cancelled', async () => { + render( + , + ) + + fireEvent.click(screen.getByText('restore-through-wrapper')) + const dialog = screen.getByRole('alertdialog') + + fireEvent.click(within(dialog).getByRole('button', { name: 'operation.cancel' })) + + await waitFor(() => { + expect(screen.queryByRole('alertdialog')).not.toBeInTheDocument() + }) + expect(publishedConfig.modelConfig.resetAppConfig).not.toHaveBeenCalled() + expect(mockSetFeatures).not.toHaveBeenCalled() + }) }) diff --git a/web/app/components/app/app-publisher/features-wrapper.tsx b/web/app/components/app/app-publisher/features-wrapper.tsx index 381e9a553e..6a5c9582a7 100644 --- a/web/app/components/app/app-publisher/features-wrapper.tsx +++ b/web/app/components/app/app-publisher/features-wrapper.tsx @@ -6,9 +6,17 @@ 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 { + AlertDialog, + AlertDialogActions, + AlertDialogCancelButton, + AlertDialogConfirmButton, + AlertDialogContent, + AlertDialogDescription, + AlertDialogTitle, +} from '@/app/components/base/ui/alert-dialog' import { SupportUploadFileTypes } from '@/app/components/workflow/types' import { Resolution } from '@/types/app' @@ -74,15 +82,24 @@ const FeaturesWrappedAppPublisher = (props: Props) => { onRestore: () => setRestoreConfirmOpen(true), }} /> - {restoreConfirmOpen && ( - setRestoreConfirmOpen(false)} - /> - )} + !open && setRestoreConfirmOpen(false)}> + +
+ + {t('resetConfig.title', { ns: 'appDebug' })} + + + {t('resetConfig.message', { ns: 'appDebug' })} + +
+ + {t('operation.cancel', { ns: 'common' })} + + {t('operation.confirm', { ns: 'common' })} + + +
+
) } diff --git a/web/app/components/app/configuration/config-var/index.tsx b/web/app/components/app/configuration/config-var/index.tsx index 17f5e2efe5..1c25d06f10 100644 --- a/web/app/components/app/configuration/config-var/index.tsx +++ b/web/app/components/app/configuration/config-var/index.tsx @@ -11,8 +11,16 @@ 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 { + AlertDialog, + AlertDialogActions, + AlertDialogCancelButton, + AlertDialogConfirmButton, + AlertDialogContent, + AlertDialogDescription, + AlertDialogTitle, +} from '@/app/components/base/ui/alert-dialog' import { toast } from '@/app/components/base/ui/toast' import { InputVarType } from '@/app/components/workflow/types' import ConfigContext from '@/context/debug-configuration' @@ -264,7 +272,7 @@ const ConfigVar: FC = ({ promptVariables, readonly, onPromptVar > {!hasVar && (
-
{t('notSetVar', { ns: 'appDebug' })}
+
{t('notSetVar', { ns: 'appDebug' })}
)} {hasVar && ( @@ -313,18 +321,29 @@ const ConfigVar: FC = ({ promptVariables, readonly, onPromptVar /> )} - {isShowDeleteContextVarModal && ( - { - didRemoveVar(removeIndex as number) - hideDeleteContextVarModal() - }} - onCancel={hideDeleteContextVarModal} - /> - )} + !open && hideDeleteContextVarModal()}> + +
+ + {t('feature.dataSet.queryVariable.deleteContextVarTitle', { ns: 'appDebug', varName: promptVariables[removeIndex as number]?.name })} + + + {t('feature.dataSet.queryVariable.deleteContextVarTip', { ns: 'appDebug' })} + +
+ + {t('operation.cancel', { ns: 'common' })} + { + didRemoveVar(removeIndex as number) + hideDeleteContextVarModal() + }} + > + {t('operation.confirm', { ns: 'common' })} + + +
+
) diff --git a/web/app/components/app/configuration/config/automatic/__tests__/get-automatic-res.spec.tsx b/web/app/components/app/configuration/config/automatic/__tests__/get-automatic-res.spec.tsx index ab5ff97e58..154a7a5a14 100644 --- a/web/app/components/app/configuration/config/automatic/__tests__/get-automatic-res.spec.tsx +++ b/web/app/components/app/configuration/config/automatic/__tests__/get-automatic-res.spec.tsx @@ -1,4 +1,4 @@ -import { fireEvent, render, screen, waitFor } from '@testing-library/react' +import { fireEvent, render, screen, waitFor, within } from '@testing-library/react' import { AppModeEnum } from '@/types/app' import GetAutomaticRes from '../get-automatic-res' @@ -237,6 +237,42 @@ describe('GetAutomaticRes', () => { })) }) + it('should close overwrite confirmation without applying the generated result when cancelled', async () => { + mockGenerateBasicAppFirstTimeRule.mockResolvedValue({ + prompt: 'generated prompt', + variables: ['city'], + opening_statement: 'hello there', + }) + + render( + , + ) + + fireEvent.click(screen.getByText('set-basic-instruction')) + fireEvent.click(screen.getByText('generate.generate')) + + await waitFor(() => { + expect(screen.getByTestId('result-panel')).toHaveTextContent('generated prompt') + }) + + fireEvent.click(screen.getByText('apply-result')) + const dialog = await screen.findByRole('alertdialog') + + fireEvent.click(within(dialog).getByRole('button', { name: 'operation.cancel' })) + + await waitFor(() => { + expect(screen.queryByRole('alertdialog')).not.toBeInTheDocument() + }) + expect(mockOnFinished).not.toHaveBeenCalled() + }) + it('should request workflow generation and surface service errors', async () => { mockGenerateRule.mockResolvedValue({ error: 'generation failed', diff --git a/web/app/components/app/configuration/config/automatic/get-automatic-res.tsx b/web/app/components/app/configuration/config/automatic/get-automatic-res.tsx index fcdee7db11..d9200af773 100644 --- a/web/app/components/app/configuration/config/automatic/get-automatic-res.tsx +++ b/web/app/components/app/configuration/config/automatic/get-automatic-res.tsx @@ -19,12 +19,19 @@ import { useBoolean, useSessionStorageState } from 'ahooks' import * as React from 'react' import { useCallback, useEffect, useState } from 'react' import { useTranslation } from 'react-i18next' -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 { + AlertDialog, + AlertDialogActions, + AlertDialogCancelButton, + AlertDialogConfirmButton, + AlertDialogContent, + AlertDialogDescription, + AlertDialogTitle, +} from '@/app/components/base/ui/alert-dialog' import { Button } from '@/app/components/base/ui/button' - import { toast } from '@/app/components/base/ui/toast' import { ModelTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations' @@ -387,18 +394,29 @@ const GetAutomaticRes: FC = ({ )} {isLoading && renderLoading} {isShowAutoPromptResPlaceholder() && } - {isShowConfirmOverwrite && ( - { - hideShowConfirmOverwrite() - onFinished(current!) - }} - onCancel={hideShowConfirmOverwrite} - /> - )} + !open && hideShowConfirmOverwrite()}> + +
+ + {t('generate.overwriteTitle', { ns: 'appDebug' })} + + + {t('generate.overwriteMessage', { ns: 'appDebug' })} + +
+ + {t('operation.cancel', { ns: 'common' })} + { + hideShowConfirmOverwrite() + onFinished(current!) + }} + > + {t('operation.confirm', { ns: 'common' })} + + +
+
) diff --git a/web/app/components/app/configuration/config/code-generator/__tests__/get-code-generator-res.spec.tsx b/web/app/components/app/configuration/config/code-generator/__tests__/get-code-generator-res.spec.tsx index 098f84b06e..25039bae96 100644 --- a/web/app/components/app/configuration/config/code-generator/__tests__/get-code-generator-res.spec.tsx +++ b/web/app/components/app/configuration/config/code-generator/__tests__/get-code-generator-res.spec.tsx @@ -1,4 +1,4 @@ -import { fireEvent, render, screen, waitFor } from '@testing-library/react' +import { fireEvent, render, screen, waitFor, within } from '@testing-library/react' import { CodeLanguage } from '@/app/components/workflow/nodes/code/types' import { AppModeEnum } from '@/types/app' import GetCodeGeneratorResModal from '../get-code-generator-res' @@ -223,6 +223,43 @@ describe('GetCodeGeneratorResModal', () => { })) }) + it('should close overwrite confirmation without applying the generated code when cancelled', async () => { + mockGenerateRule.mockResolvedValue({ + code: 'print("hello")', + }) + + render( + , + ) + + fireEvent.click(screen.getByText('set-code-instruction')) + fireEvent.click(screen.getByText('set-code-output')) + fireEvent.click(screen.getByText('codegen.generate')) + + await waitFor(() => { + expect(screen.getByTestId('code-result-panel')).toHaveTextContent('print("hello")') + }) + + fireEvent.click(screen.getByText('apply-code-result')) + const dialog = await screen.findByRole('alertdialog') + + fireEvent.click(within(dialog).getByRole('button', { name: 'operation.cancel' })) + + await waitFor(() => { + expect(screen.queryByRole('alertdialog')).not.toBeInTheDocument() + }) + expect(mockOnFinished).not.toHaveBeenCalled() + }) + it('should surface service errors without creating a result version', async () => { mockGenerateRule.mockResolvedValue({ error: 'generation failed', diff --git a/web/app/components/app/configuration/config/code-generator/get-code-generator-res.tsx b/web/app/components/app/configuration/config/code-generator/get-code-generator-res.tsx index 666dab3425..eb1ee7e10c 100644 --- a/web/app/components/app/configuration/config/code-generator/get-code-generator-res.tsx +++ b/web/app/components/app/configuration/config/code-generator/get-code-generator-res.tsx @@ -10,10 +10,18 @@ import { import * as React from 'react' import { useCallback, useEffect, useState } from 'react' import { useTranslation } from 'react-i18next' -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 { + AlertDialog, + AlertDialogActions, + AlertDialogCancelButton, + AlertDialogConfirmButton, + AlertDialogContent, + AlertDialogDescription, + AlertDialogTitle, +} from '@/app/components/base/ui/alert-dialog' import { Button } from '@/app/components/base/ui/button' import { toast } from '@/app/components/base/ui/toast' import { ModelTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations' @@ -263,18 +271,29 @@ export const GetCodeGeneratorResModal: FC = ( )} - {isShowConfirmOverwrite && ( - { - hideShowConfirmOverwrite() - onFinished(current!) - }} - onCancel={hideShowConfirmOverwrite} - /> - )} + !open && hideShowConfirmOverwrite()}> + +
+ + {t('codegen.overwriteConfirmTitle', { ns: 'appDebug' })} + + + {t('codegen.overwriteConfirmMessage', { ns: 'appDebug' })} + +
+ + {t('operation.cancel', { ns: 'common' })} + { + hideShowConfirmOverwrite() + onFinished(current!) + }} + > + {t('operation.confirm', { ns: 'common' })} + + +
+
) } diff --git a/web/app/components/app/configuration/configuration-view.tsx b/web/app/components/app/configuration/configuration-view.tsx index 049ccf4171..35ab462ad1 100644 --- a/web/app/components/app/configuration/configuration-view.tsx +++ b/web/app/components/app/configuration/configuration-view.tsx @@ -153,28 +153,26 @@ const ConfigurationView: FC = ({ - {showUseGPT4Confirm && ( - !open && setShowUseGPT4Confirm(false)}> - -
- - {t('trailUseGPT4Info.title', { ns: 'appDebug' })} - - - {t('trailUseGPT4Info.description', { ns: 'appDebug' })} - -
- - - {t('operation.cancel', { ns: 'common' })} - - - {t('operation.confirm', { ns: 'common' })} - - -
-
- )} + !open && setShowUseGPT4Confirm(false)}> + +
+ + {t('trailUseGPT4Info.title', { ns: 'appDebug' })} + + + {t('trailUseGPT4Info.description', { ns: 'appDebug' })} + +
+ + + {t('operation.cancel', { ns: 'common' })} + + + {t('operation.confirm', { ns: 'common' })} + + +
+
{isShowSelectDataSet && ( { const t = (key: string) => key @@ -100,4 +100,31 @@ describe('app-card-sections', () => { expect(screen.getByText('overview.appInfo.customize.entry')).toBeInTheDocument() expect(AppModeEnum.CHAT).toBe('chat') }) + + it('should invoke regenerate dialog callbacks from the url section', () => { + const onRegenerate = vi.fn() + const onHideRegenerateConfirm = vi.fn() + + render( + , + ) + + const dialog = screen.getByRole('alertdialog') + + fireEvent.click(within(dialog).getByRole('button', { name: /operation\.cancel/i })) + expect(onHideRegenerateConfirm).toHaveBeenCalled() + + fireEvent.click(within(dialog).getByRole('button', { name: /operation\.confirm/i })) + expect(onRegenerate).toHaveBeenCalledTimes(1) + }) }) diff --git a/web/app/components/app/overview/app-card-sections.tsx b/web/app/components/app/overview/app-card-sections.tsx index 659335227c..93067d02b7 100644 --- a/web/app/components/app/overview/app-card-sections.tsx +++ b/web/app/components/app/overview/app-card-sections.tsx @@ -180,28 +180,26 @@ export const AppCardUrlSection = ({ {isApp && } {isApp && } - {showConfirmDelete && ( - !open && onHideRegenerateConfirm()}> - -
- - {t('overview.appInfo.regenerate', { ns: 'appOverview' })} - - - {t('overview.appInfo.regenerateNotice', { ns: 'appOverview' })} - -
- - - {t('operation.cancel', { ns: 'common' })} - - - {t('operation.confirm', { ns: 'common' })} - - -
-
- )} + !open && onHideRegenerateConfirm()}> + +
+ + {t('overview.appInfo.regenerate', { ns: 'appOverview' })} + + + {t('overview.appInfo.regenerateNotice', { ns: 'appOverview' })} + +
+ + + {t('operation.cancel', { ns: 'common' })} + + + {t('operation.confirm', { ns: 'common' })} + + +
+
{isApp && isCurrentWorkspaceManager && (
{ await user.click(screen.getByRole('button', { name: 'common.operation.cancel' })) expect(screen.queryByRole('button', { name: 'common.operation.confirm' })).not.toBeInTheDocument() + expect(screen.getByRole('checkbox')).not.toBeChecked() }) it('should toggle remove-original from the checkbox control itself', async () => { diff --git a/web/app/components/app/switch-app-modal/index.tsx b/web/app/components/app/switch-app-modal/index.tsx index 16016b8096..aa7f1974ec 100644 --- a/web/app/components/app/switch-app-modal/index.tsx +++ b/web/app/components/app/switch-app-modal/index.tsx @@ -8,11 +8,19 @@ import { useTranslation } from 'react-i18next' import { useStore as useAppStore } from '@/app/components/app/store' import AppIcon from '@/app/components/base/app-icon' 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 { Button } from '@/app/components/base/ui/button' +import { + AlertDialog, + AlertDialogActions, + AlertDialogCancelButton, + AlertDialogConfirmButton, + AlertDialogContent, + AlertDialogDescription, + AlertDialogTitle, +} from '@/app/components/base/ui/alert-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' @@ -91,6 +99,14 @@ const SwitchAppModal = ({ show, appDetail, inAppDetail = false, onSuccess, onClo setShowConfirmDelete(true) }, [removeOriginal]) + const handleConfirmDeleteOpenChange = (open: boolean) => { + if (open) + return + + setShowConfirmDelete(false) + setRemoveOriginal(false) + } + return ( <>
- {showConfirmDelete && ( - setShowConfirmDelete(false)} - onCancel={() => { - setShowConfirmDelete(false) - setRemoveOriginal(false) - }} - /> - )} + + +
+ + {t('deleteAppConfirmTitle', { ns: 'app' })} + + + {t('deleteAppConfirmContent', { ns: 'app' })} + +
+ + + {t('operation.cancel', { ns: 'common' })} + + setShowConfirmDelete(false)}> + {t('operation.confirm', { ns: 'common' })} + + +
+
) } diff --git a/web/app/components/base/chat/chat-with-history/header-in-mobile.tsx b/web/app/components/base/chat/chat-with-history/header-in-mobile.tsx index 4f6792fa1b..d101a41449 100644 --- a/web/app/components/base/chat/chat-with-history/header-in-mobile.tsx +++ b/web/app/components/base/chat/chat-with-history/header-in-mobile.tsx @@ -5,7 +5,15 @@ 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 { + AlertDialog, + AlertDialogActions, + AlertDialogCancelButton, + AlertDialogConfirmButton, + AlertDialogContent, + AlertDialogDescription, + AlertDialogTitle, +} from '@/app/components/base/ui/alert-dialog' import { useChatWithHistoryContext } from './context' import MobileOperationDropdown from './header/mobile-operation-dropdown' import Operation from './header/operation' @@ -78,7 +86,7 @@ const HeaderInMobile = () => { imageUrl={appData?.site.icon_url} background={appData?.site.icon_background} /> -
+
{appData?.site.title}
@@ -121,7 +129,7 @@ const HeaderInMobile = () => {
e.stopPropagation()}>
-
{t('chat.chatSettingsTitle', { ns: 'share' })}
+
{t('chat.chatSettingsTitle', { ns: 'share' })}
@@ -129,15 +137,24 @@ const HeaderInMobile = () => {
)} - {!!showConfirm && ( - - )} + !open && handleCancelConfirm()}> + +
+ + {t('chat.deleteConversation.title', { ns: 'share' })} + + + {t('chat.deleteConversation.content', { ns: 'share' }) || ''} + +
+ + {t('operation.cancel', { ns: 'common' })} + + {t('operation.confirm', { ns: 'common' })} + + +
+
{showRename && ( { />
{!currentConversationId && ( -
{appData?.site.title}
+
{appData?.site.title}
)} {currentConversationId && currentConversationItem && isSidebarCollapsed && ( <> @@ -141,15 +149,24 @@ const Header = () => { )}
- {!!showConfirm && ( - - )} + !open && handleCancelConfirm()}> + +
+ + {t('chat.deleteConversation.title', { ns: 'share' })} + + + {t('chat.deleteConversation.content', { ns: 'share' }) || ''} + +
+ + {t('operation.cancel', { ns: 'common' })} + + {t('operation.confirm', { ns: 'common' })} + + +
+
{showRename && ( ({ }, })) -// 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 ( -
-
{title}
- -
{content}
- -
- ) - }, -})) - describe('Sidebar Index', () => { const mockContextValue = { isInstalledApp: false, @@ -475,8 +459,7 @@ describe('Sidebar Index', () => { render() await user.click(screen.getByTestId('delete-1')) - expect(screen.getByTestId('confirm-dialog')).toBeInTheDocument() - expect(screen.getByTestId('confirm-title')).toBeInTheDocument() + expect(screen.getByText('share.chat.deleteConversation.title')).toBeInTheDocument() }) it('should call handleDeleteConversation when confirm is clicked', async () => { @@ -490,7 +473,7 @@ describe('Sidebar Index', () => { render() await user.click(screen.getByTestId('delete-1')) - await user.click(screen.getByTestId('confirm-confirm')) + await user.click(screen.getByRole('button', { name: 'common.operation.confirm' })) expect(handleDeleteConversation).toHaveBeenCalledWith('1', expect.objectContaining({ onSuccess: expect.any(Function), @@ -502,11 +485,11 @@ describe('Sidebar Index', () => { render() await user.click(screen.getByTestId('delete-1')) - expect(screen.getByTestId('confirm-dialog')).toBeInTheDocument() + expect(screen.getByText('share.chat.deleteConversation.title')).toBeInTheDocument() - await user.click(screen.getByTestId('confirm-cancel')) + await user.click(screen.getByRole('button', { name: 'common.operation.cancel' })) await waitFor(() => { - expect(screen.queryByTestId('confirm-dialog')).not.toBeInTheDocument() + expect(screen.queryByText('share.chat.deleteConversation.title')).not.toBeInTheDocument() }) }) @@ -525,7 +508,7 @@ describe('Sidebar Index', () => { render() await user.click(screen.getByTestId('delete-1')) - await user.click(screen.getByTestId('confirm-confirm')) + await user.click(screen.getByRole('button', { name: 'common.operation.confirm' })) expect(handleDeleteConversation).toHaveBeenCalledWith('1', expect.any(Object)) }) @@ -837,7 +820,7 @@ describe('Sidebar Index', () => { // Delete it await user.click(screen.getByTestId('delete-1')) - await user.click(screen.getByTestId('confirm-confirm')) + await user.click(screen.getByRole('button', { name: 'common.operation.confirm' })) expect(handleDeleteConversation).toHaveBeenCalled() }) @@ -901,8 +884,8 @@ describe('Sidebar Index', () => { try { render() await user.click(screen.getByTestId('delete-1')) - expect(screen.getByTestId('confirm-dialog')).toBeInTheDocument() - expect(screen.getByTestId('confirm-content')).toBeEmptyDOMElement() + expect(screen.getByText('share.chat.deleteConversation.title')).toBeInTheDocument() + expect(screen.queryByText('share.chat.deleteConversation.content')).not.toBeInTheDocument() } finally { useTranslationSpy.mockRestore() diff --git a/web/app/components/base/chat/chat-with-history/sidebar/index.tsx b/web/app/components/base/chat/chat-with-history/sidebar/index.tsx index 7759b98847..919ddba551 100644 --- a/web/app/components/base/chat/chat-with-history/sidebar/index.tsx +++ b/web/app/components/base/chat/chat-with-history/sidebar/index.tsx @@ -13,9 +13,17 @@ import ActionButton from '@/app/components/base/action-button' import AppIcon from '@/app/components/base/app-icon' 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 { Button } from '@/app/components/base/ui/button' +import { + AlertDialog, + AlertDialogActions, + AlertDialogCancelButton, + AlertDialogConfirmButton, + AlertDialogContent, + AlertDialogDescription, + AlertDialogTitle, +} from '@/app/components/base/ui/alert-dialog' import MenuDropdown from '@/app/components/share/text-generation/menu-dropdown' import { useGlobalPublicStore } from '@/context/global-public-context' import { cn } from '@/utils/classnames' @@ -167,15 +175,24 @@ const Sidebar = ({ isPanel, panelVisible }: Props) => { )} - {!!showConfirm && ( - - )} + !open && handleCancelConfirm()}> + +
+ + {t('chat.deleteConversation.title', { ns: 'share' })} + + + {deleteConversationContent} + +
+ + {t('operation.cancel', { ns: 'common' })} + + {t('operation.confirm', { ns: 'common' })} + + +
+
{showRename && ( { - const actual = await vi.importActual('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() - expect(screen.getByText('test title')).toBeInTheDocument() - }) - - it('does not render on isShow false', () => { - const { container } = render() - expect(container.firstChild).toBeNull() - }) - - it('hides after delay when isShow changes to false', () => { - vi.useFakeTimers() - const { rerender } = render() - expect(screen.getByText('test title')).toBeInTheDocument() - - rerender() - act(() => { - vi.advanceTimersByTime(200) - }) - expect(screen.queryByText('test title')).not.toBeInTheDocument() - vi.useRealTimers() - }) - - it('renders content when provided', () => { - render() - expect(screen.getByText('some description')).toBeInTheDocument() - }) - }) - - describe('Props', () => { - it('showCancel prop works', () => { - render() - expect(screen.getByRole('button', { name: 'common.operation.confirm' })).toBeInTheDocument() - expect(screen.queryByRole('button', { name: 'common.operation.cancel' })).not.toBeInTheDocument() - }) - - it('showConfirm prop works', () => { - render() - 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() - expect(screen.getByRole('button', { name: 'Yes' })).toBeInTheDocument() - expect(screen.getByRole('button', { name: 'No' })).toBeInTheDocument() - }) - - it('disables confirm button when isDisabled is true', () => { - render() - expect(screen.getByRole('button', { name: 'common.operation.confirm' })).toBeDisabled() - }) - }) - - describe('User Interactions', () => { - it('clickAway is handled properly', () => { - render() - const overlay = screen.getByTestId('confirm-overlay') as HTMLElement - expect(overlay).toBeTruthy() - fireEvent.mouseDown(overlay) - expect(onCancel).toHaveBeenCalledTimes(1) - }) - - it('overlay click stops propagation', () => { - render() - 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() - const overlay = screen.getByTestId('confirm-overlay') as HTMLElement - fireEvent.mouseDown(overlay) - expect(onCancel).not.toHaveBeenCalled() - }) - - it('escape keyboard event works', () => { - render() - fireEvent.keyDown(document, { key: 'Escape' }) - expect(onCancel).toHaveBeenCalledTimes(1) - expect(onConfirm).not.toHaveBeenCalled() - }) - - it('Enter keyboard event works', () => { - render() - fireEvent.keyDown(document, { key: 'Enter' }) - expect(onConfirm).toHaveBeenCalledTimes(1) - expect(onCancel).not.toHaveBeenCalled() - }) - }) -}) diff --git a/web/app/components/base/confirm/index.stories.tsx b/web/app/components/base/confirm/index.stories.tsx deleted file mode 100644 index 9f2998eb55..0000000000 --- a/web/app/components/base/confirm/index.stories.tsx +++ /dev/null @@ -1,216 +0,0 @@ -import type { Meta, StoryObj } from '@storybook/nextjs-vite' -import { useState } from 'react' -import { Button } from '@/app/components/base/ui/button' -import Confirm from '.' - -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 - -export default meta -type Story = StoryObj - -// Interactive demo wrapper -const ConfirmDemo = (args: any) => { - const [isShow, setIsShow] = useState(false) - - return ( -
- - { - console.log('✅ User clicked confirm') - setIsShow(false) - }} - onCancel={() => { - console.log('❌ User clicked cancel') - setIsShow(false) - }} - /> -
- ) -} - -// Basic warning dialog - Delete action -export const WarningDialog: Story = { - render: 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 => , - 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 => , - 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 => , - 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 => , - 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 => , - 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 => , - 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 => , - 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 => , - 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, - }, -} diff --git a/web/app/components/base/confirm/index.tsx b/web/app/components/base/confirm/index.tsx deleted file mode 100644 index f23df5cc42..0000000000 --- a/web/app/components/base/confirm/index.tsx +++ /dev/null @@ -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 '@/app/components/base/ui/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(null) - const titleRef = useRef(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( -
{ - e.preventDefault() - e.stopPropagation() - }} - data-testid="confirm-overlay" - > -
-
-
- -
- {title} -
-
-
{content}
- {confirmInputLabel && ( -
- - onConfirmInputChange?.(e.target.value)} - /> -
- )} -
-
- {showCancel && } - {showConfirm && } -
-
-
-
, - document.body, - ) -} - -export default React.memo(Confirm) diff --git a/web/app/components/base/tag-management/tag-item-editor.tsx b/web/app/components/base/tag-management/tag-item-editor.tsx index 2de769e375..c7baedf6c9 100644 --- a/web/app/components/base/tag-management/tag-item-editor.tsx +++ b/web/app/components/base/tag-management/tag-item-editor.tsx @@ -3,8 +3,16 @@ 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 { + AlertDialog, + AlertDialogActions, + AlertDialogCancelButton, + AlertDialogConfirmButton, + AlertDialogContent, + AlertDialogDescription, + AlertDialogTitle, +} from '@/app/components/base/ui/alert-dialog' import { toast } from '@/app/components/base/ui/toast' import { deleteTag, updateTag } from '@/service/tag' import { cn } from '@/utils/classnames' @@ -118,16 +126,34 @@ const TagItemEditor: FC = ({ tag }) => { )} {isEditing && ( setName(e.target.value)} onKeyDown={e => e.key === 'Enter' && editTag(tag.id, name)} onBlur={() => editTag(tag.id, name)} />)} - { - handleRemove() - setShowRemoveModal(false) - }} - onCancel={() => setShowRemoveModal(false)} - /> + !open && setShowRemoveModal(false)}> + +
+ + {`${t('tag.delete', { ns: 'common' })} "${tag.name}"`} + + + {t('tag.deleteTip', { ns: 'common' })} + +
+ + + {t('operation.cancel', { ns: 'common' })} + + { + handleRemove() + setShowRemoveModal(false) + }} + > + {t('operation.confirm', { ns: 'common' })} + + +
+
) } diff --git a/web/app/components/datasets/create-from-pipeline/list/template-card/__tests__/index.spec.tsx b/web/app/components/datasets/create-from-pipeline/list/template-card/__tests__/index.spec.tsx index 4ce4ecdb87..43c2023437 100644 --- a/web/app/components/datasets/create-from-pipeline/list/template-card/__tests__/index.spec.tsx +++ b/web/app/components/datasets/create-from-pipeline/list/template-card/__tests__/index.spec.tsx @@ -37,33 +37,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 - ? ( -
-
{title}
-
{content}
- - -
- ) - : null - }, -})) - // Capture Actions callbacks let _capturedHandleDelete: (() => void) | undefined let _capturedHandleExportDSL: (() => void) | undefined @@ -182,13 +155,14 @@ describe('TemplateCard', () => { type: 'customized' as const, } + const getDeleteConfirmButton = () => screen.getByRole('button', { name: 'common.operation.confirm' }) + const getDeleteCancelButton = () => screen.getByRole('button', { name: 'common.operation.cancel' }) + beforeEach(() => { vi.clearAllMocks() mockToastSuccess.mockReset() mockToastError.mockReset() mockIsExporting = false - _capturedOnConfirm = undefined - _capturedOnCancel = undefined _capturedHandleDelete = undefined _capturedHandleExportDSL = undefined _capturedOpenEditModal = undefined @@ -507,7 +481,7 @@ describe('TemplateCard', () => { fireEvent.click(deleteButton) await waitFor(() => { - expect(screen.getByTestId('confirm-dialog')).toBeInTheDocument() + expect(screen.getByText('datasetPipeline.deletePipeline.title')).toBeInTheDocument() }) }) @@ -517,14 +491,13 @@ describe('TemplateCard', () => { fireEvent.click(deleteButton) await waitFor(() => { - expect(screen.getByTestId('confirm-dialog')).toBeInTheDocument() + expect(screen.getByText('datasetPipeline.deletePipeline.title')).toBeInTheDocument() }) - const cancelButton = screen.getByTestId('confirm-cancel') - fireEvent.click(cancelButton) + fireEvent.click(getDeleteCancelButton()) await waitFor(() => { - expect(screen.queryByTestId('confirm-dialog')).not.toBeInTheDocument() + expect(screen.queryByText('datasetPipeline.deletePipeline.title')).not.toBeInTheDocument() }) }) @@ -539,11 +512,10 @@ describe('TemplateCard', () => { fireEvent.click(deleteButton) await waitFor(() => { - expect(screen.getByTestId('confirm-dialog')).toBeInTheDocument() + expect(screen.getByText('datasetPipeline.deletePipeline.title')).toBeInTheDocument() }) - const confirmButton = screen.getByTestId('confirm-submit') - fireEvent.click(confirmButton) + fireEvent.click(getDeleteConfirmButton()) await waitFor(() => { expect(mockDeletePipeline).toHaveBeenCalledWith('pipeline-1', expect.any(Object)) @@ -561,11 +533,10 @@ describe('TemplateCard', () => { fireEvent.click(deleteButton) await waitFor(() => { - expect(screen.getByTestId('confirm-dialog')).toBeInTheDocument() + expect(screen.getByText('datasetPipeline.deletePipeline.title')).toBeInTheDocument() }) - const confirmButton = screen.getByTestId('confirm-submit') - fireEvent.click(confirmButton) + fireEvent.click(getDeleteConfirmButton()) await waitFor(() => { expect(mockInvalidCustomizedTemplateList).toHaveBeenCalled() @@ -583,14 +554,13 @@ describe('TemplateCard', () => { fireEvent.click(deleteButton) await waitFor(() => { - expect(screen.getByTestId('confirm-dialog')).toBeInTheDocument() + expect(screen.getByText('datasetPipeline.deletePipeline.title')).toBeInTheDocument() }) - const confirmButton = screen.getByTestId('confirm-submit') - fireEvent.click(confirmButton) + fireEvent.click(getDeleteConfirmButton()) await waitFor(() => { - expect(screen.queryByTestId('confirm-dialog')).not.toBeInTheDocument() + expect(screen.queryByText('datasetPipeline.deletePipeline.title')).not.toBeInTheDocument() }) }) }) diff --git a/web/app/components/datasets/create-from-pipeline/list/template-card/index.tsx b/web/app/components/datasets/create-from-pipeline/list/template-card/index.tsx index d7881708d6..5fa3511e7c 100644 --- a/web/app/components/datasets/create-from-pipeline/list/template-card/index.tsx +++ b/web/app/components/datasets/create-from-pipeline/list/template-card/index.tsx @@ -3,8 +3,16 @@ 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 { + AlertDialog, + AlertDialogActions, + AlertDialogCancelButton, + AlertDialogConfirmButton, + AlertDialogContent, + AlertDialogDescription, + AlertDialogTitle, +} from '@/app/components/base/ui/alert-dialog' import { toast } from '@/app/components/base/ui/toast' import { usePluginDependencies } from '@/app/components/workflow/plugin-dependency/hooks' import { useRouter } from '@/next/navigation' @@ -156,15 +164,24 @@ const TemplateCard = ({ /> )} - {showDeleteConfirm && ( - - )} + !open && onCancelDelete()}> + +
+ + {t('deletePipeline.title', { ns: 'datasetPipeline' })} + + + {t('deletePipeline.content', { ns: 'datasetPipeline' })} + +
+ + {t('operation.cancel', { ns: 'common' })} + + {t('operation.confirm', { ns: 'common' })} + + +
+
{showDetailModal && ( ) : handleSwitch(v ? 'enable' : 'disable')} size="md" />} - + )} {embeddingAvailable && ( @@ -280,8 +288,24 @@ const Operations = ({ embeddingAvailable, datasetId, detail, selectedIds, onSele /> )} - {showModal - && ( onOperate('delete')} onCancel={() => setShowModal(false)} />)} + !open && setShowModal(false)}> + +
+ + {t('list.delete.title', { ns: 'datasetDocuments' })} + + + {t('list.delete.content', { ns: 'datasetDocuments' })} + +
+ + {t('operation.cancel', { ns: 'common' })} + onOperate('delete')}> + {t('operation.sure', { ns: 'common' })} + + +
+
{isShowRenameModal && currDocument && ()} diff --git a/web/app/components/datasets/documents/detail/completed/common/__tests__/batch-action.spec.tsx b/web/app/components/datasets/documents/detail/completed/common/__tests__/batch-action.spec.tsx index eda7d3845c..f310ae4007 100644 --- a/web/app/components/datasets/documents/detail/completed/common/__tests__/batch-action.spec.tsx +++ b/web/app/components/datasets/documents/detail/completed/common/__tests__/batch-action.spec.tsx @@ -1,4 +1,4 @@ -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 BatchAction from '../batch-action' @@ -105,6 +105,21 @@ describe('BatchAction', () => { expect(mockOnBatchDelete).toHaveBeenCalledTimes(1) }) }) + + it('should close delete confirmation when cancel is clicked', async () => { + const mockOnBatchDelete = vi.fn() + render() + + fireEvent.click(screen.getByText(/batchAction\.delete/i)) + const dialog = await screen.findByRole('alertdialog') + + fireEvent.click(within(dialog).getByRole('button', { name: 'common.operation.cancel' })) + + await waitFor(() => { + expect(screen.queryByRole('alertdialog')).not.toBeInTheDocument() + }) + expect(mockOnBatchDelete).not.toHaveBeenCalled() + }) }) // Optional props tests diff --git a/web/app/components/datasets/documents/detail/completed/common/batch-action.tsx b/web/app/components/datasets/documents/detail/completed/common/batch-action.tsx index 3e5f395dfe..5ecf95b59c 100644 --- a/web/app/components/datasets/documents/detail/completed/common/batch-action.tsx +++ b/web/app/components/datasets/documents/detail/completed/common/batch-action.tsx @@ -3,10 +3,18 @@ import { RiArchive2Line, RiCheckboxCircleLine, RiCloseCircleLine, RiDeleteBinLin 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 { SearchLinesSparkle } from '@/app/components/base/icons/src/vender/knowledge' import { Button } from '@/app/components/base/ui/button' +import { + AlertDialog, + AlertDialogActions, + AlertDialogCancelButton, + AlertDialogConfirmButton, + AlertDialogContent, + AlertDialogDescription, + AlertDialogTitle, +} from '@/app/components/base/ui/alert-dialog' import { IS_CE_EDITION } from '@/config' import { cn } from '@/utils/classnames' @@ -147,20 +155,24 @@ const BatchAction: FC = ({ {t(`${i18nPrefix}.cancel`, { ns: 'dataset' })} - { - isShowDeleteConfirm && ( - - ) - } + !open && hideDeleteConfirm()}> + +
+ + {t('list.delete.title', { ns: 'datasetDocuments' })} + + + {t('list.delete.content', { ns: 'datasetDocuments' })} + +
+ + {t('operation.cancel', { ns: 'common' })} + + {t('operation.sure', { ns: 'common' })} + + +
+
) } diff --git a/web/app/components/datasets/documents/detail/completed/segment-card/index.tsx b/web/app/components/datasets/documents/detail/completed/segment-card/index.tsx index d43b45a6b5..df35b9b5f3 100644 --- a/web/app/components/datasets/documents/detail/completed/segment-card/index.tsx +++ b/web/app/components/datasets/documents/detail/completed/segment-card/index.tsx @@ -5,10 +5,17 @@ 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 { + AlertDialog, + AlertDialogActions, + AlertDialogCancelButton, + AlertDialogConfirmButton, + AlertDialogContent, + AlertDialogTitle, +} from '@/app/components/base/ui/alert-dialog' import ImageList from '@/app/components/datasets/common/image-list' import { ChunkingMode } from '@/models/datasets' import { cn } from '@/utils/classnames' @@ -137,7 +144,7 @@ const SegmentCard: FC = ({ data-testid="segment-card" className={cn( 'chunk-card group/card w-full rounded-xl px-3', - isFullDocMode ? '' : 'pb-2 pt-2.5 hover:bg-dataset-chunk-detail-card-hover-bg', + isFullDocMode ? '' : 'pt-2.5 pb-2 hover:bg-dataset-chunk-detail-card-hover-bg', focused.segmentContent ? 'bg-dataset-chunk-detail-card-hover-bg' : '', className, )} @@ -170,7 +177,7 @@ const SegmentCard: FC = ({
{embeddingAvailable && ( - ) } diff --git a/web/app/components/datasets/external-api/external-api-modal/index.tsx b/web/app/components/datasets/external-api/external-api-modal/index.tsx index 1d60e67634..0cd6593232 100644 --- a/web/app/components/datasets/external-api/external-api-modal/index.tsx +++ b/web/app/components/datasets/external-api/external-api-modal/index.tsx @@ -4,9 +4,17 @@ import { RiBook2Line, RiCloseLine, RiInformation2Line, RiLock2Fill } from '@remi import { memo, useEffect, useState } from 'react' import { useTranslation } from 'react-i18next' import ActionButton from '@/app/components/base/action-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 { + AlertDialog, + AlertDialogActions, + AlertDialogCancelButton, + AlertDialogConfirmButton, + AlertDialogContent, + AlertDialogDescription, + AlertDialogTitle, +} from '@/app/components/base/ui/alert-dialog' import { Button } from '@/app/components/base/ui/button' import { toast } from '@/app/components/base/ui/toast' import { createExternalAPI } from '@/service/datasets' @@ -176,7 +184,27 @@ const AddExternalAPIModal: FC = ({ data, onSave, onCan {t('externalAPIForm.encrypted.end', { ns: 'dataset' })}
- {showConfirm && (datasetBindings?.length ?? 0) > 0 && ( setShowConfirm(false)} onConfirm={handleSave} />)} + 0} + onOpenChange={open => !open && setShowConfirm(false)} + > + +
+ + Warning + + + {`${t('editExternalAPIConfirmWarningContent.front', { ns: 'dataset' })} ${datasetBindings?.length} ${t('editExternalAPIConfirmWarningContent.end', { ns: 'dataset' })}`} + +
+ + {t('operation.cancel', { ns: 'common' })} + + {t('operation.confirm', { ns: 'common' })} + + +
+
diff --git a/web/app/components/datasets/external-api/external-knowledge-api-card/index.tsx b/web/app/components/datasets/external-api/external-knowledge-api-card/index.tsx index 224b090b6e..23993db7dd 100644 --- a/web/app/components/datasets/external-api/external-knowledge-api-card/index.tsx +++ b/web/app/components/datasets/external-api/external-knowledge-api-card/index.tsx @@ -8,8 +8,16 @@ 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 { + AlertDialog, + AlertDialogActions, + AlertDialogCancelButton, + AlertDialogConfirmButton, + AlertDialogContent, + AlertDialogDescription, + AlertDialogTitle, +} from '@/app/components/base/ui/alert-dialog' import { useExternalKnowledgeApi } from '@/context/external-knowledge-api-context' import { useModalContext } from '@/context/modal-context' import { checkUsageExternalAPI, deleteExternalAPI, fetchExternalAPI, updateExternalAPI } from '@/service/datasets' @@ -115,7 +123,7 @@ const ExternalKnowledgeAPICard: React.FC = ({ api
{api.name}
-
{api.settings.endpoint}
+
{api.settings.endpoint}
@@ -131,20 +139,26 @@ const ExternalKnowledgeAPICard: React.FC = ({ api
- {showConfirm && ( - 0 - ? `${t('deleteExternalAPIConfirmWarningContent.content.front', { ns: 'dataset' })} ${usageCount} ${t('deleteExternalAPIConfirmWarningContent.content.end', { ns: 'dataset' })}` - : t('deleteExternalAPIConfirmWarningContent.noConnectionContent', { ns: 'dataset' }) - } - type="warning" - onConfirm={handleConfirmDelete} - onCancel={() => setShowConfirm(false)} - /> - )} + !open && setShowConfirm(false)}> + +
+ + {`${t('deleteExternalAPIConfirmWarningContent.title.front', { ns: 'dataset' })} ${api.name}${t('deleteExternalAPIConfirmWarningContent.title.end', { ns: 'dataset' })}`} + + + {usageCount > 0 + ? `${t('deleteExternalAPIConfirmWarningContent.content.front', { ns: 'dataset' })} ${usageCount} ${t('deleteExternalAPIConfirmWarningContent.content.end', { ns: 'dataset' })}` + : t('deleteExternalAPIConfirmWarningContent.noConnectionContent', { ns: 'dataset' })} + +
+ + {t('operation.cancel', { ns: 'common' })} + + {t('operation.confirm', { ns: 'common' })} + + +
+
) } diff --git a/web/app/components/datasets/list/dataset-card/components/__tests__/dataset-card-modals.spec.tsx b/web/app/components/datasets/list/dataset-card/components/__tests__/dataset-card-modals.spec.tsx index e3e4a70936..8cc10ae5ae 100644 --- a/web/app/components/datasets/list/dataset-card/components/__tests__/dataset-card-modals.spec.tsx +++ b/web/app/components/datasets/list/dataset-card/components/__tests__/dataset-card-modals.spec.tsx @@ -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 - ? ( -
-
{title}
-
{content}
- - -
- ) - : null - ), -})) - describe('DatasetCardModals', () => { const mockDataset: DataSet = { id: 'dataset-1', diff --git a/web/app/components/datasets/list/dataset-card/components/dataset-card-modals.tsx b/web/app/components/datasets/list/dataset-card/components/dataset-card-modals.tsx index 8162bc94c4..79b0d73799 100644 --- a/web/app/components/datasets/list/dataset-card/components/dataset-card-modals.tsx +++ b/web/app/components/datasets/list/dataset-card/components/dataset-card-modals.tsx @@ -1,7 +1,15 @@ import type { DataSet } from '@/models/datasets' import * as React from 'react' import { useTranslation } from 'react-i18next' -import Confirm from '@/app/components/base/confirm' +import { + AlertDialog, + AlertDialogActions, + AlertDialogCancelButton, + AlertDialogConfirmButton, + AlertDialogContent, + AlertDialogDescription, + AlertDialogTitle, +} from '@/app/components/base/ui/alert-dialog' import RenameDatasetModal from '../../../rename-modal' type ModalState = { @@ -39,15 +47,26 @@ const DatasetCardModals = ({ onSuccess={onSuccess} /> )} - {modalState.showConfirmDelete && ( - - )} + !open && onCloseConfirm()}> + +
+ + {t('deleteDatasetConfirmTitle', { ns: 'dataset' })} + + + {modalState.confirmMessage} + +
+ + + {t('operation.cancel', { ns: 'common' })} + + + {t('operation.confirm', { ns: 'common' })} + + +
+
) } diff --git a/web/app/components/datasets/metadata/metadata-dataset/dataset-metadata-drawer.tsx b/web/app/components/datasets/metadata/metadata-dataset/dataset-metadata-drawer.tsx index 24b2fdc363..f2abf609a2 100644 --- a/web/app/components/datasets/metadata/metadata-dataset/dataset-metadata-drawer.tsx +++ b/web/app/components/datasets/metadata/metadata-dataset/dataset-metadata-drawer.tsx @@ -6,13 +6,21 @@ import { useBoolean, useHover } from 'ahooks' import * as React from 'react' import { useCallback, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' -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 { Button } from '@/app/components/base/ui/button' +import { + AlertDialog, + AlertDialogActions, + AlertDialogCancelButton, + AlertDialogConfirmButton, + AlertDialogContent, + AlertDialogDescription, + AlertDialogTitle, +} from '@/app/components/base/ui/alert-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' @@ -95,16 +103,24 @@ const Item: FC = ({ - {isShowDeleteConfirm && ( - - )} + !open && hideDeleteConfirm()}> + +
+ + {t('metadata.datasetMetadata.deleteTitle', { ns: 'dataset' })} + + + {t('metadata.datasetMetadata.deleteContent', { ns: 'dataset', name: payload.name })} + +
+ + {t('operation.cancel', { ns: 'common' })} + + {t('operation.confirm', { ns: 'common' })} + + +
+
) diff --git a/web/app/components/develop/secret-key/__tests__/secret-key-modal.spec.tsx b/web/app/components/develop/secret-key/__tests__/secret-key-modal.spec.tsx index 9b15e75b9d..333ef45162 100644 --- a/web/app/components/develop/secret-key/__tests__/secret-key-modal.spec.tsx +++ b/web/app/components/develop/secret-key/__tests__/secret-key-modal.spec.tsx @@ -1,4 +1,4 @@ -import { act, render, screen, waitFor } from '@testing-library/react' +import { act, fireEvent, render, screen, waitFor } from '@testing-library/react' import userEvent from '@testing-library/user-event' import { afterEach } from 'vitest' import SecretKeyModal from '../secret-key-modal' @@ -477,6 +477,32 @@ describe('SecretKeyModal', () => { expect(mockDelAppApikey).not.toHaveBeenCalled() }) + + it('should close confirm dialog when Escape is pressed', async () => { + const user = userEvent.setup({ advanceTimers: vi.advanceTimersByTime }) + await renderModal() + + const actionButtons = document.body.querySelectorAll('button.action-btn') + const deleteButton = actionButtons[1] + await act(async () => { + await user.click(deleteButton!) + vi.runAllTimers() + }) + + await waitFor(() => { + expect(screen.getByText('appApi.actionMsg.deleteConfirmTitle')).toBeInTheDocument() + }) + await flushTransitions() + + await act(async () => { + fireEvent.keyDown(document, { key: 'Escape', code: 'Escape' }) + vi.runAllTimers() + }) + + await waitFor(() => { + expect(screen.queryByText('appApi.actionMsg.deleteConfirmTitle')).not.toBeInTheDocument() + }) + }) }) describe('delete key for dataset', () => { diff --git a/web/app/components/develop/secret-key/secret-key-modal.tsx b/web/app/components/develop/secret-key/secret-key-modal.tsx index ec83251ec6..dbd27606be 100644 --- a/web/app/components/develop/secret-key/secret-key-modal.tsx +++ b/web/app/components/develop/secret-key/secret-key-modal.tsx @@ -7,11 +7,19 @@ import { } from 'react' import { useTranslation } from 'react-i18next' import ActionButton from '@/app/components/base/action-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 { Button } from '@/app/components/base/ui/button' +import { + AlertDialog, + AlertDialogActions, + AlertDialogCancelButton, + AlertDialogConfirmButton, + AlertDialogContent, + AlertDialogDescription, + AlertDialogTitle, +} from '@/app/components/base/ui/alert-dialog' import { useAppContext } from '@/context/app-context' import useTimestamp from '@/hooks/use-timestamp' import { @@ -87,6 +95,14 @@ const SecretKeyModal = ({ return `${token.slice(0, 3)}...${token.slice(-20)}` } + const handleDeleteConfirmOpenChange = (open: boolean) => { + if (open) + return + + setDelKeyId('') + setShowConfirmDelete(false) + } + return (
@@ -135,18 +151,29 @@ const SecretKeyModal = ({
setVisible(false)} newKey={newKey} /> - {showConfirmDelete && ( - { - setDelKeyId('') - setShowConfirmDelete(false) - }} - /> - )} + + +
+ + {t('actionMsg.deleteConfirmTitle', { ns: 'appApi' })} + + + {t('actionMsg.deleteConfirmTips', { ns: 'appApi' })} + +
+ + + {t('operation.cancel', { ns: 'common' })} + + + {t('operation.confirm', { ns: 'common' })} + + +
+
) } diff --git a/web/app/components/header/account-setting/api-based-extension-page/__tests__/item.spec.tsx b/web/app/components/header/account-setting/api-based-extension-page/__tests__/item.spec.tsx index 265c262cbf..cf5c46a509 100644 --- a/web/app/components/header/account-setting/api-based-extension-page/__tests__/item.spec.tsx +++ b/web/app/components/header/account-setting/api-based-extension-page/__tests__/item.spec.tsx @@ -105,8 +105,10 @@ describe('Item Component', () => { // Act fireEvent.click(screen.getByText('common.operation.delete')) - const dialog = screen.getByTestId('confirm-overlay') - const confirmButton = within(dialog).getByText('common.operation.delete') + const dialog = screen.getByRole('alertdialog', { + name: /common\.operation\.delete.*Test Extension.*\?/i, + }) + const confirmButton = within(dialog).getByRole('button', { name: 'common.operation.delete' }) fireEvent.click(confirmButton) // Assert @@ -123,8 +125,10 @@ describe('Item Component', () => { // Act fireEvent.click(screen.getByText('common.operation.delete')) - const dialog = screen.getByTestId('confirm-overlay') - const confirmButton = within(dialog).getByText('common.operation.delete') + const dialog = screen.getByRole('alertdialog', { + name: /common\.operation\.delete.*Test Extension.*\?/i, + }) + const confirmButton = within(dialog).getByRole('button', { name: 'common.operation.delete' }) fireEvent.click(confirmButton) // Assert @@ -133,14 +137,16 @@ describe('Item Component', () => { }) }) - it('should close delete confirmation when clicking cancel button', () => { + it('should close delete confirmation when clicking cancel button', async () => { // Act render() fireEvent.click(screen.getByText('common.operation.delete')) fireEvent.click(screen.getByText('common.operation.cancel')) // Assert - expect(screen.queryByText(/common\.operation\.delete.*Test Extension.*\?/i)).not.toBeInTheDocument() + await waitFor(() => { + expect(screen.queryByText(/common\.operation\.delete.*Test Extension.*\?/i)).not.toBeInTheDocument() + }) }) it('should not call delete API when canceling deletion', () => { diff --git a/web/app/components/header/account-setting/api-based-extension-page/item.tsx b/web/app/components/header/account-setting/api-based-extension-page/item.tsx index 0624224a45..5ef349f5e0 100644 --- a/web/app/components/header/account-setting/api-based-extension-page/item.tsx +++ b/web/app/components/header/account-setting/api-based-extension-page/item.tsx @@ -6,7 +6,14 @@ import { } from '@remixicon/react' import { useState } from 'react' import { useTranslation } from 'react-i18next' -import Confirm from '@/app/components/base/confirm' +import { + AlertDialog, + AlertDialogActions, + AlertDialogCancelButton, + AlertDialogConfirmButton, + AlertDialogContent, + AlertDialogTitle, +} from '@/app/components/base/ui/alert-dialog' import { Button } from '@/app/components/base/ui/button' import { useModalContext } from '@/context/modal-context' import { deleteApiBasedExtension } from '@/service/common' @@ -57,18 +64,21 @@ const Item: FC = ({ {t('operation.delete', { ns: 'common' })} - { - showDeleteConfirm - && ( - setShowDeleteConfirm(false)} - title={`${t('operation.delete', { ns: 'common' })} “${data.name}”?`} - onConfirm={handleDeleteApiBasedExtension} - confirmText={t('operation.delete', { ns: 'common' }) || ''} - /> - ) - } + !open && setShowDeleteConfirm(false)}> + +
+ + {`${t('operation.delete', { ns: 'common' })} \u201C${data.name}\u201D?`} + +
+ + {t('operation.cancel', { ns: 'common' })} + + {t('operation.delete', { ns: 'common' }) || ''} + + +
+
) } diff --git a/web/app/components/header/account-setting/data-source-page-new/card.tsx b/web/app/components/header/account-setting/data-source-page-new/card.tsx index a68c7d99cc..0f7a11f61f 100644 --- a/web/app/components/header/account-setting/data-source-page-new/card.tsx +++ b/web/app/components/header/account-setting/data-source-page-new/card.tsx @@ -8,7 +8,14 @@ import { useRef, } from 'react' import { useTranslation } from 'react-i18next' -import Confirm from '@/app/components/base/confirm' +import { + AlertDialog, + AlertDialogActions, + AlertDialogCancelButton, + AlertDialogConfirmButton, + AlertDialogContent, + AlertDialogTitle, +} from '@/app/components/base/ui/alert-dialog' import { ApiKeyModal, usePluginAuthAction, @@ -123,7 +130,7 @@ const Card = ({
{renderI18nObject(label)}
-
+
{author}
/
{name} @@ -135,7 +142,7 @@ const Card = ({ onUpdate={handleAuthUpdate} />
-
+
{t('auth.connectedWorkspace', { ns: 'plugin' })}
@@ -157,23 +164,27 @@ const Card = ({ { !credentials_list.length && (
-
+
{t('auth.emptyAuth', { ns: 'plugin' })}
) } - { - deleteCredentialId && ( - - ) - } + !open && closeConfirm()}> + +
+ + {t('list.delete.title', { ns: 'datasetDocuments' })} + +
+ + {t('operation.cancel', { ns: 'common' })} + + {t('operation.confirm', { ns: 'common' })} + + +
+
{ !!editValues && ( { fireEvent.click(screen.getByRole('button', { name: /common.operation.confirm/i })) expect(mockHandleConfirmDelete).toHaveBeenCalled() }) + + it('should close confirm dialog when deletion is cancelled', () => { + mockDeleteCredentialId = 'cred-1' + + render( + , + ) + + const dialog = screen.getByRole('alertdialog') + fireEvent.click(within(dialog).getByRole('button', { name: /operation.cancel/i })) + + expect(mockCloseConfirmDelete).toHaveBeenCalledTimes(1) + }) + + it('should disable the confirm button while deletion is in progress', () => { + mockDeleteCredentialId = 'cred-1' + mockDoingAction = true + + render( + , + ) + + const dialog = screen.getByRole('alertdialog') + expect(within(dialog).getByRole('button', { name: /common.operation.confirm/i })).toBeDisabled() + }) }) diff --git a/web/app/components/header/account-setting/model-provider-page/model-auth/authorized/index.tsx b/web/app/components/header/account-setting/model-provider-page/model-auth/authorized/index.tsx index 09e64c654f..d1a2bc1f89 100644 --- a/web/app/components/header/account-setting/model-provider-page/model-auth/authorized/index.tsx +++ b/web/app/components/header/account-setting/model-provider-page/model-auth/authorized/index.tsx @@ -19,12 +19,19 @@ import { useState, } from 'react' import { useTranslation } from 'react-i18next' -import Confirm from '@/app/components/base/confirm' import { PortalToFollowElem, PortalToFollowElemContent, PortalToFollowElemTrigger, } from '@/app/components/base/portal-to-follow-elem' +import { + AlertDialog, + AlertDialogActions, + AlertDialogCancelButton, + AlertDialogConfirmButton, + AlertDialogContent, + AlertDialogTitle, +} from '@/app/components/base/ui/alert-dialog' import { Button } from '@/app/components/base/ui/button' import { cn } from '@/utils/classnames' import { useAuth } from '../hooks' @@ -240,17 +247,21 @@ const Authorized = ({
- { - deleteCredentialId && ( - - ) - } + !open && closeConfirmDelete()}> + +
+ + {t('modelProvider.confirmDelete', { ns: 'common' })} + +
+ + {t('operation.cancel', { ns: 'common' })} + + {t('operation.confirm', { ns: 'common' })} + + +
+
) } diff --git a/web/app/components/header/account-setting/model-provider-page/provider-added-card/model-load-balancing-modal.tsx b/web/app/components/header/account-setting/model-provider-page/provider-added-card/model-load-balancing-modal.tsx index cb4ffd0392..4c8c718ac2 100644 --- a/web/app/components/header/account-setting/model-provider-page/provider-added-card/model-load-balancing-modal.tsx +++ b/web/app/components/header/account-setting/model-provider-page/provider-added-card/model-load-balancing-modal.tsx @@ -1,9 +1,16 @@ import type { Credential, CustomConfigurationModelFixedFields, ModelItem, ModelLoadBalancingConfig, ModelLoadBalancingConfigEntry, ModelProvider } from '../declarations' import { memo, useCallback, useEffect, useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' -import Confirm from '@/app/components/base/confirm' import Loading from '@/app/components/base/loading' import Modal from '@/app/components/base/modal' +import { + AlertDialog, + AlertDialogActions, + AlertDialogCancelButton, + AlertDialogConfirmButton, + AlertDialogContent, + AlertDialogTitle, +} from '@/app/components/base/ui/alert-dialog' import { Button } from '@/app/components/base/ui/button' import { toast } from '@/app/components/base/ui/toast' import { SwitchCredentialInLoadBalancing } from '@/app/components/header/account-setting/model-provider-page/model-auth' @@ -266,7 +273,21 @@ const ModelLoadBalancingModal = ({ provider, configurateMethod, currentCustomCon )} - {deleteModel && ()} + !open && closeConfirmDelete()}> + +
+ + {t('modelProvider.confirmDelete', { ns: 'common' })} + +
+ + {t('operation.cancel', { ns: 'common' })} + + {t('operation.confirm', { ns: 'common' })} + + +
+
) } diff --git a/web/app/components/plugins/plugin-auth/authorized/index.tsx b/web/app/components/plugins/plugin-auth/authorized/index.tsx index cb2e8aaca1..dc905d9f8c 100644 --- a/web/app/components/plugins/plugin-auth/authorized/index.tsx +++ b/web/app/components/plugins/plugin-auth/authorized/index.tsx @@ -12,12 +12,19 @@ import { useState, } from 'react' import { useTranslation } from 'react-i18next' -import Confirm from '@/app/components/base/confirm' import { PortalToFollowElem, PortalToFollowElemContent, PortalToFollowElemTrigger, } from '@/app/components/base/portal-to-follow-elem' +import { + AlertDialog, + AlertDialogActions, + AlertDialogCancelButton, + AlertDialogConfirmButton, + AlertDialogContent, + AlertDialogTitle, +} from '@/app/components/base/ui/alert-dialog' import { Button } from '@/app/components/base/ui/button' import { toast } from '@/app/components/base/ui/toast' import Indicator from '@/app/components/header/indicator' @@ -318,17 +325,21 @@ const Authorized = ({
- { - deleteCredentialId && ( - - ) - } + !open && closeConfirm()}> + +
+ + {t('list.delete.title', { ns: 'datasetDocuments' })} + +
+ + {t('operation.cancel', { ns: 'common' })} + + {t('operation.confirm', { ns: 'common' })} + + +
+
{ !!editValues && ( { beforeEach(() => { vi.clearAllMocks() - vi.useFakeTimers() // Reset failure flags failureFlags.enable = false failureFlags.disable = false @@ -152,6 +151,12 @@ describe('EndpointCard', () => { vi.useRealTimers() }) + const waitForAlertDialogToClose = async () => { + await waitFor(() => { + expect(screen.queryByRole('alertdialog')).not.toBeInTheDocument() + }) + } + describe('Rendering', () => { it('should render endpoint name', () => { render() @@ -243,6 +248,7 @@ describe('EndpointCard', () => { describe('Copy Functionality', () => { it('should reset copy state after timeout', async () => { + vi.useFakeTimers() render() const allButtons = screen.getAllByRole('button') @@ -276,19 +282,19 @@ describe('EndpointCard', () => { expect(mockHandleChange).toHaveBeenCalled() }) - it('should hide disable confirm and revert state when cancel clicked', () => { + it('should hide disable confirm and revert state when cancel clicked', async () => { render() fireEvent.click(screen.getByRole('switch')) expect(screen.getByText('plugin.detailPanel.endpointDisableTip')).toBeInTheDocument() fireEvent.click(screen.getByRole('button', { name: 'common.operation.cancel' })) + await waitForAlertDialogToClose() - // Confirm should be hidden - expect(screen.queryByText('plugin.detailPanel.endpointDisableTip')).not.toBeInTheDocument() + expect(screen.getByRole('switch')).toHaveAttribute('aria-checked', 'true') }) - it('should hide delete confirm when cancel clicked', () => { + it('should hide delete confirm when cancel clicked', async () => { render() const allButtons = screen.getAllByRole('button') @@ -296,8 +302,7 @@ describe('EndpointCard', () => { expect(screen.getByText('plugin.detailPanel.endpointDeleteTip')).toBeInTheDocument() fireEvent.click(screen.getByRole('button', { name: 'common.operation.cancel' })) - - expect(screen.queryByText('plugin.detailPanel.endpointDeleteTip')).not.toBeInTheDocument() + await waitForAlertDialogToClose() }) it('should hide edit modal when cancel clicked', () => { diff --git a/web/app/components/plugins/plugin-detail-panel/endpoint-card.tsx b/web/app/components/plugins/plugin-detail-panel/endpoint-card.tsx index af7412d60e..679801a56a 100644 --- a/web/app/components/plugins/plugin-detail-panel/endpoint-card.tsx +++ b/web/app/components/plugins/plugin-detail-panel/endpoint-card.tsx @@ -6,10 +6,17 @@ 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 { + AlertDialog, + AlertDialogActions, + AlertDialogCancelButton, + AlertDialogConfirmButton, + AlertDialogContent, + AlertDialogTitle, +} from '@/app/components/base/ui/alert-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' @@ -122,6 +129,14 @@ const EndpointCard = ({ setIsCopied(true) } + const handleDisableConfirmOpenChange = (open: boolean) => { + if (open) + return + + hideDisableConfirm() + setActive(true) + } + useEffect(() => { if (isCopied) { const timer = setTimeout(() => { @@ -139,7 +154,7 @@ const EndpointCard = ({
-
+
{data.name}
@@ -154,8 +169,8 @@ const EndpointCard = ({
{data.declaration.endpoints.filter(endpoint => !endpoint.hidden).map((endpoint, index) => (
-
{endpoint.method}
-
+
{endpoint.method}
+
{`${data.url}${endpoint.path}`}
handleCopy(`${data.url}${endpoint.path}`)}> @@ -168,13 +183,13 @@ const EndpointCard = ({
{active && ( -
+
{t('detailPanel.serviceOk', { ns: 'plugin' })}
)} {!active && ( -
+
{t('detailPanel.disabled', { ns: 'plugin' })}
@@ -186,27 +201,47 @@ const EndpointCard = ({ size="sm" />
- {isShowDisableConfirm && ( - {t('detailPanel.endpointDisableContent', { ns: 'plugin', name: data.name })}
} - onCancel={() => { - hideDisableConfirm() - setActive(true) - }} - onConfirm={() => disableEndpoint(endpointID)} - /> - )} - {isShowDeleteConfirm && ( - {t('detailPanel.endpointDeleteContent', { ns: 'plugin', name: data.name })}
} - onCancel={hideDeleteConfirm} - onConfirm={() => deleteEndpoint(endpointID)} - /> - )} + + +
+ + {t('detailPanel.endpointDisableTip', { ns: 'plugin' })} + +
+ {t('detailPanel.endpointDisableContent', { ns: 'plugin', name: data.name })} +
+
+ + + {t('operation.cancel', { ns: 'common' })} + + disableEndpoint(endpointID)}> + {t('operation.confirm', { ns: 'common' })} + + +
+
+ !open && hideDeleteConfirm()}> + +
+ + {t('detailPanel.endpointDeleteTip', { ns: 'plugin' })} + +
+ {t('detailPanel.endpointDeleteContent', { ns: 'plugin', name: data.name })} +
+
+ + {t('operation.cancel', { ns: 'common' })} + deleteEndpoint(endpointID)}> + {t('operation.confirm', { ns: 'common' })} + + +
+
{isShowEndpointModal && ( { return ( -
- +
+ {t(`${tPrefix}.title`, { ns: 'pluginTrigger', name: currentName })} - + {workflowsInUse > 0 ? t(`${tPrefix}.contentWithApps`, { ns: 'pluginTrigger', count: workflowsInUse }) : t(`${tPrefix}.content`, { ns: 'pluginTrigger' })} {workflowsInUse > 0 && (
-
+
{t(`${tPrefix}.confirmInputTip`, { ns: 'pluginTrigger', name: currentName })}
({ ), })) -// 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 ( -
-
{title}
-
{content}
- - -
- ) - }, -})) - // ==================== Test Utilities ==================== type ActionProps = { @@ -151,6 +127,9 @@ const createActionProps = (overrides: Partial = {}): ActionProps => ...overrides, }) +const getDeleteConfirmButton = () => screen.getByRole('button', { name: /common\.operation\.confirm/ }) +const getDeleteCancelButton = () => screen.getByRole('button', { name: 'common.operation.cancel' }) + // ==================== Tests ==================== // Helper to find action buttons (real ActionButton component uses type="button") @@ -277,8 +256,7 @@ describe('Action Component', () => { fireEvent.click(getActionButtons()[0]) // Assert - expect(screen.getByTestId('confirm-modal')).toBeInTheDocument() - expect(screen.getByTestId('confirm-title')).toHaveTextContent('plugin.action.delete') + expect(screen.getByText('plugin.action.delete')).toBeInTheDocument() }) it('should display plugin name in delete confirm content', () => { @@ -309,12 +287,14 @@ describe('Action Component', () => { // Act render() fireEvent.click(getActionButtons()[0]) - expect(screen.getByTestId('confirm-modal')).toBeInTheDocument() + expect(screen.getByText('plugin.action.delete')).toBeInTheDocument() - fireEvent.click(screen.getByTestId('confirm-cancel')) + fireEvent.click(getDeleteCancelButton()) // Assert - expect(screen.queryByTestId('confirm-modal')).not.toBeInTheDocument() + return waitFor(() => { + expect(screen.queryByText('plugin.action.delete')).not.toBeInTheDocument() + }) }) it('should call uninstallPlugin when confirm is clicked', async () => { @@ -329,7 +309,7 @@ describe('Action Component', () => { // Act render() fireEvent.click(getActionButtons()[0]) - fireEvent.click(screen.getByTestId('confirm-ok')) + fireEvent.click(getDeleteConfirmButton()) // Assert await waitFor(() => { @@ -351,7 +331,7 @@ describe('Action Component', () => { // Act render() fireEvent.click(getActionButtons()[0]) - fireEvent.click(screen.getByTestId('confirm-ok')) + fireEvent.click(getDeleteConfirmButton()) // Assert await waitFor(() => { @@ -373,7 +353,7 @@ describe('Action Component', () => { // Act render() fireEvent.click(getActionButtons()[0]) - fireEvent.click(screen.getByTestId('confirm-ok')) + fireEvent.click(getDeleteConfirmButton()) // Assert await waitFor(() => { @@ -395,7 +375,7 @@ describe('Action Component', () => { // Act render() fireEvent.click(getActionButtons()[0]) - fireEvent.click(screen.getByTestId('confirm-ok')) + fireEvent.click(getDeleteConfirmButton()) // Assert await waitFor(() => { @@ -422,17 +402,17 @@ describe('Action Component', () => { // Act render() fireEvent.click(getActionButtons()[0]) - fireEvent.click(screen.getByTestId('confirm-ok')) + fireEvent.click(getDeleteConfirmButton()) // Assert - Loading state await waitFor(() => { - expect(screen.getByTestId('confirm-modal')).toHaveAttribute('data-loading', 'true') + expect(getDeleteConfirmButton()).toBeDisabled() }) // Resolve and check modal closes resolveUninstall!({ success: true }) await waitFor(() => { - expect(screen.queryByTestId('confirm-modal')).not.toBeInTheDocument() + expect(screen.queryByText('plugin.action.delete')).not.toBeInTheDocument() }) }) }) @@ -699,7 +679,7 @@ describe('Action Component', () => { // Act - First render and delete const { rerender } = render() fireEvent.click(getActionButtons()[0]) - fireEvent.click(screen.getByTestId('confirm-ok')) + fireEvent.click(getDeleteConfirmButton()) await waitFor(() => { expect(mockUninstallPlugin).toHaveBeenCalledWith('stable-install-id') @@ -709,7 +689,7 @@ describe('Action Component', () => { mockUninstallPlugin.mockClear() rerender() fireEvent.click(getActionButtons()[0]) - fireEvent.click(screen.getByTestId('confirm-ok')) + fireEvent.click(getDeleteConfirmButton()) await waitFor(() => { expect(mockUninstallPlugin).toHaveBeenCalledWith('stable-install-id') @@ -735,7 +715,7 @@ describe('Action Component', () => { // Act const { rerender } = render() fireEvent.click(getActionButtons()[0]) - fireEvent.click(screen.getByTestId('confirm-ok')) + fireEvent.click(getDeleteConfirmButton()) await waitFor(() => { expect(mockUninstallPlugin).toHaveBeenCalledWith('install-1') @@ -744,7 +724,7 @@ describe('Action Component', () => { mockUninstallPlugin.mockClear() rerender() fireEvent.click(getActionButtons()[0]) - fireEvent.click(screen.getByTestId('confirm-ok')) + fireEvent.click(getDeleteConfirmButton()) await waitFor(() => { expect(mockUninstallPlugin).toHaveBeenCalledWith('install-2') @@ -772,7 +752,7 @@ describe('Action Component', () => { // Act const { rerender } = render() fireEvent.click(getActionButtons()[0]) - fireEvent.click(screen.getByTestId('confirm-ok')) + fireEvent.click(getDeleteConfirmButton()) await waitFor(() => { expect(onDelete1).toHaveBeenCalled() @@ -781,7 +761,7 @@ describe('Action Component', () => { rerender() fireEvent.click(getActionButtons()[0]) - fireEvent.click(screen.getByTestId('confirm-ok')) + fireEvent.click(getDeleteConfirmButton()) await waitFor(() => { expect(onDelete2).toHaveBeenCalled() @@ -847,17 +827,16 @@ describe('Action Component', () => { // Act render() fireEvent.click(getActionButtons()[0]) - fireEvent.click(screen.getByTestId('confirm-ok')) + fireEvent.click(getDeleteConfirmButton()) // 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(getDeleteConfirmButton()).toBeDisabled() // Resolve the deletion resolveFirst!({ success: true }) await waitFor(() => { - expect(screen.queryByTestId('confirm-modal')).not.toBeInTheDocument() + expect(screen.queryByText('plugin.action.delete')).not.toBeInTheDocument() }) }) diff --git a/web/app/components/plugins/plugin-item/action.tsx b/web/app/components/plugins/plugin-item/action.tsx index c01be54442..ed401b534f 100644 --- a/web/app/components/plugins/plugin-item/action.tsx +++ b/web/app/components/plugins/plugin-item/action.tsx @@ -7,12 +7,19 @@ import { useBoolean } from 'ahooks' import * as React from 'react' import { useCallback } from 'react' import { useTranslation } from 'react-i18next' +import { + AlertDialog, + AlertDialogActions, + AlertDialogCancelButton, + AlertDialogConfirmButton, + AlertDialogContent, + AlertDialogTitle, +} from '@/app/components/base/ui/alert-dialog' import { toast } from '@/app/components/base/ui/toast' 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 { checkForUpdates, fetchReleases } from '../install-plugin/hooks' import PluginInfo from '../plugin-page/plugin-info' @@ -151,24 +158,27 @@ const Action: FC = ({ onHide={hidePluginInfo} /> )} - - {t(`${i18nPrefix}.deleteContentLeft`, { ns: 'plugin' })} - {pluginName} - {t(`${i18nPrefix}.deleteContentRight`, { ns: 'plugin' })} -
- {/* // todo: add usedInApps */} - {/* {usedInApps > 0 && t(`${i18nPrefix}.usedInApps`, { num: usedInApps })} */} + !open && hideDeleteConfirm()}> + +
+ + {t(`${i18nPrefix}.delete`, { ns: 'plugin' })} + +
+ {t(`${i18nPrefix}.deleteContentLeft`, { ns: 'plugin' })} + {pluginName} + {t(`${i18nPrefix}.deleteContentRight`, { ns: 'plugin' })} +
+
- )} - onCancel={hideDeleteConfirm} - onConfirm={handleDelete} - isLoading={deleting} - isDisabled={deleting} - /> + + {t('operation.cancel', { ns: 'common' })} + + {t('operation.confirm', { ns: 'common' })} + + +
+
) } diff --git a/web/app/components/rag-pipeline/components/__tests__/conversion.spec.tsx b/web/app/components/rag-pipeline/components/__tests__/conversion.spec.tsx index 71bade3235..967e697813 100644 --- a/web/app/components/rag-pipeline/components/__tests__/conversion.spec.tsx +++ b/web/app/components/rag-pipeline/components/__tests__/conversion.spec.tsx @@ -1,4 +1,4 @@ -import { fireEvent, render, screen } from '@testing-library/react' +import { fireEvent, render, screen, waitFor } from '@testing-library/react' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import Conversion from '../conversion' @@ -47,29 +47,6 @@ vi.mock('@/app/components/base/ui/button', () => ({ ), })) -vi.mock('@/app/components/base/confirm', () => ({ - default: ({ - isShow, - onConfirm, - onCancel, - title, - }: { - isShow: boolean - onConfirm: () => void - onCancel: () => void - title: string - }) => - isShow - ? ( -
- {title} - - -
- ) - : null, -})) - vi.mock('../screenshot', () => ({ default: () =>
, })) @@ -112,11 +89,10 @@ describe('Conversion', () => { it('should show confirm modal when convert button clicked', () => { render() - expect(screen.queryByTestId('confirm-modal')).not.toBeInTheDocument() + expect(screen.queryByText('datasetPipeline.conversion.confirm.title')).not.toBeInTheDocument() fireEvent.click(screen.getByText('datasetPipeline.operations.convert')) - expect(screen.getByTestId('confirm-modal')).toBeInTheDocument() expect(screen.getByText('datasetPipeline.conversion.confirm.title')).toBeInTheDocument() }) @@ -124,17 +100,19 @@ describe('Conversion', () => { render() fireEvent.click(screen.getByText('datasetPipeline.operations.convert')) - expect(screen.getByTestId('confirm-modal')).toBeInTheDocument() + expect(screen.getByText('datasetPipeline.conversion.confirm.title')).toBeInTheDocument() - fireEvent.click(screen.getByTestId('cancel-btn')) - expect(screen.queryByTestId('confirm-modal')).not.toBeInTheDocument() + fireEvent.click(screen.getByRole('button', { name: 'common.operation.cancel' })) + return waitFor(() => { + expect(screen.queryByText('datasetPipeline.conversion.confirm.title')).not.toBeInTheDocument() + }) }) it('should call convert when confirm is clicked', () => { render() fireEvent.click(screen.getByText('datasetPipeline.operations.convert')) - fireEvent.click(screen.getByTestId('confirm-btn')) + fireEvent.click(screen.getByRole('button', { name: 'common.operation.confirm' })) expect(mockConvert).toHaveBeenCalledWith('ds-123', expect.objectContaining({ onSuccess: expect.any(Function), @@ -150,7 +128,7 @@ describe('Conversion', () => { render() fireEvent.click(screen.getByText('datasetPipeline.operations.convert')) - fireEvent.click(screen.getByTestId('confirm-btn')) + fireEvent.click(screen.getByRole('button', { name: 'common.operation.confirm' })) expect(mockToast.success).toHaveBeenCalledWith('datasetPipeline.conversion.successMessage') expect(mockInvalidDatasetDetail).toHaveBeenCalled() @@ -164,7 +142,7 @@ describe('Conversion', () => { render() fireEvent.click(screen.getByText('datasetPipeline.operations.convert')) - fireEvent.click(screen.getByTestId('confirm-btn')) + fireEvent.click(screen.getByRole('button', { name: 'common.operation.confirm' })) expect(mockToast.error).toHaveBeenCalledWith('datasetPipeline.conversion.errorMessage') }) @@ -177,7 +155,7 @@ describe('Conversion', () => { render() fireEvent.click(screen.getByText('datasetPipeline.operations.convert')) - fireEvent.click(screen.getByTestId('confirm-btn')) + fireEvent.click(screen.getByRole('button', { name: 'common.operation.confirm' })) expect(mockToast.error).toHaveBeenCalledWith('datasetPipeline.conversion.errorMessage') }) diff --git a/web/app/components/rag-pipeline/components/__tests__/index.spec.tsx b/web/app/components/rag-pipeline/components/__tests__/index.spec.tsx index c3341ecd83..0eec89b8b8 100644 --- a/web/app/components/rag-pipeline/components/__tests__/index.spec.tsx +++ b/web/app/components/rag-pipeline/components/__tests__/index.spec.tsx @@ -472,21 +472,23 @@ describe('Conversion', () => { const convertButton = screen.getByRole('button', { name: /datasetPipeline\.operations\.convert/i }) fireEvent.click(convertButton) - // Real Confirm renders title and content via portal + // AlertDialog renders title and content via portal. expect(screen.getByText('datasetPipeline.conversion.confirm.title')).toBeInTheDocument() expect(screen.getByText('datasetPipeline.conversion.confirm.content')).toBeInTheDocument() }) - it('should hide confirm modal when cancel is clicked', () => { + it('should hide confirm modal when cancel is clicked', async () => { render() const convertButton = screen.getByRole('button', { name: /datasetPipeline\.operations\.convert/i }) fireEvent.click(convertButton) expect(screen.getByText('datasetPipeline.conversion.confirm.title')).toBeInTheDocument() - // Real Confirm renders cancel button with i18n text + // AlertDialog close is async because it unmounts after state updates. fireEvent.click(screen.getByRole('button', { name: 'common.operation.cancel' })) - expect(screen.queryByText('datasetPipeline.conversion.confirm.title')).not.toBeInTheDocument() + await waitFor(() => { + expect(screen.queryByText('datasetPipeline.conversion.confirm.title')).not.toBeInTheDocument() + }) }) }) diff --git a/web/app/components/rag-pipeline/components/conversion.tsx b/web/app/components/rag-pipeline/components/conversion.tsx index a8f36d713d..27f25f6e2f 100644 --- a/web/app/components/rag-pipeline/components/conversion.tsx +++ b/web/app/components/rag-pipeline/components/conversion.tsx @@ -1,7 +1,15 @@ import * as React from 'react' import { useCallback, useState } from 'react' import { useTranslation } from 'react-i18next' -import Confirm from '@/app/components/base/confirm' +import { + AlertDialog, + AlertDialogActions, + AlertDialogCancelButton, + AlertDialogConfirmButton, + AlertDialogContent, + AlertDialogDescription, + AlertDialogTitle, +} from '@/app/components/base/ui/alert-dialog' import { Button } from '@/app/components/base/ui/button' import { toast } from '@/app/components/base/ui/toast' import { useParams } from '@/next/navigation' @@ -39,6 +47,8 @@ const Conversion = () => { const handleCancelConversion = useCallback(() => { setShowConfirmModal(false) }, []) + const confirmTitle = t('conversion.confirm.title', { ns: 'datasetPipeline' }) + const confirmContent = t('conversion.confirm.content', { ns: 'datasetPipeline' }) return (
@@ -69,7 +79,26 @@ const Conversion = () => {
- {showConfirmModal && ()} + !open && handleCancelConversion()}> + +
+ + {confirmTitle} + + + {confirmContent} + +
+ + + {t('operation.cancel', { ns: 'common' })} + + + {t('operation.confirm', { ns: 'common' })} + + +
+
) } diff --git a/web/app/components/rag-pipeline/components/rag-pipeline-header/publisher/__tests__/index.spec.tsx b/web/app/components/rag-pipeline/components/rag-pipeline-header/publisher/__tests__/index.spec.tsx index de2214d219..64a12edfec 100644 --- a/web/app/components/rag-pipeline/components/rag-pipeline-header/publisher/__tests__/index.spec.tsx +++ b/web/app/components/rag-pipeline/components/rag-pipeline-header/publisher/__tests__/index.spec.tsx @@ -688,15 +688,11 @@ describe('publisher', () => { expect(screen.getByText('pipeline.common.confirmPublish')).toBeInTheDocument() }) - const cancelButtons = screen.getAllByRole('button') - const cancelButton = cancelButtons.find(btn => - btn.className.includes('cancel') || btn.textContent?.includes('Cancel'), - ) - if (cancelButton) - fireEvent.click(cancelButton) + fireEvent.click(screen.getByRole('button', { name: 'common.operation.cancel' })) - // Note: This test verifies the confirm modal can be displayed - expect(screen.getByText('pipeline.common.confirmPublishContent')).toBeInTheDocument() + await waitFor(() => { + expect(screen.queryByRole('alertdialog')).not.toBeInTheDocument() + }) }) it('should publish when confirm is clicked in confirm modal', async () => { @@ -711,7 +707,11 @@ describe('publisher', () => { expect(screen.getByText('pipeline.common.confirmPublish')).toBeInTheDocument() }) - expect(screen.getByText('pipeline.common.confirmPublishContent')).toBeInTheDocument() + fireEvent.click(screen.getByRole('button', { name: 'common.operation.confirm' })) + + await waitFor(() => { + expect(mockPublishWorkflow).toHaveBeenCalled() + }) }) }) diff --git a/web/app/components/rag-pipeline/components/rag-pipeline-header/publisher/__tests__/popup.spec.tsx b/web/app/components/rag-pipeline/components/rag-pipeline-header/publisher/__tests__/popup.spec.tsx index 6834552201..a0076996a3 100644 --- a/web/app/components/rag-pipeline/components/rag-pipeline-header/publisher/__tests__/popup.spec.tsx +++ b/web/app/components/rag-pipeline/components/rag-pipeline-header/publisher/__tests__/popup.spec.tsx @@ -87,24 +87,6 @@ vi.mock('@/app/components/base/ui/button', () => ({ ), })) -vi.mock('@/app/components/base/confirm', () => ({ - default: ({ isShow, onConfirm, onCancel, title }: { - isShow: boolean - onConfirm: () => void - onCancel: () => void - title: string - }) => - isShow - ? ( -
- {title} - - -
- ) - : null, -})) - vi.mock('@/app/components/base/divider', () => ({ default: () =>
, })) diff --git a/web/app/components/rag-pipeline/components/rag-pipeline-header/publisher/popup.tsx b/web/app/components/rag-pipeline/components/rag-pipeline-header/publisher/popup.tsx index 68e59bafe8..70961c5ff9 100644 --- a/web/app/components/rag-pipeline/components/rag-pipeline-header/publisher/popup.tsx +++ b/web/app/components/rag-pipeline/components/rag-pipeline-header/publisher/popup.tsx @@ -5,10 +5,18 @@ import { useBoolean, useKeyPress } from 'ahooks' import { memo, useCallback, useState } from 'react' import { Trans, useTranslation } from 'react-i18next' import { trackEvent } from '@/app/components/base/amplitude' -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 { + AlertDialog, + AlertDialogActions, + AlertDialogCancelButton, + AlertDialogConfirmButton, + AlertDialogContent, + AlertDialogDescription, + AlertDialogTitle, +} from '@/app/components/base/ui/alert-dialog' import { Button } from '@/app/components/base/ui/button' import { toast } from '@/app/components/base/ui/toast' import { useChecklistBeforePublish } from '@/app/components/workflow/hooks' @@ -224,7 +232,29 @@ const Popup = () => {
- {confirmVisible && ()} + !open && hideConfirm()}> + +
+ + {t('common.confirmPublish', { ns: 'pipeline' })} + + + {t('common.confirmPublishContent', { ns: 'pipeline' })} + +
+ + + {t('operation.cancel', { ns: 'common' })} + + void handlePublish()}> + {t('operation.confirm', { ns: 'common' })} + + +
+
{showPublishAsKnowledgePipelineModal && ()}
) diff --git a/web/app/components/tools/mcp/__tests__/provider-card.spec.tsx b/web/app/components/tools/mcp/__tests__/provider-card.spec.tsx index d8f644112e..fc4c07275a 100644 --- a/web/app/components/tools/mcp/__tests__/provider-card.spec.tsx +++ b/web/app/components/tools/mcp/__tests__/provider-card.spec.tsx @@ -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 ( -
- - -
- ) - }, -})) - // Mock the OperationDropdown type OperationDropdownProps = { onEdit: () => void @@ -172,6 +147,9 @@ describe('MCPCard', () => { onDeleted: vi.fn(), } + const getDeleteConfirmButton = () => screen.getByRole('button', { name: 'common.operation.confirm' }) + const getDeleteCancelButton = () => screen.getByRole('button', { name: 'common.operation.cancel' }) + beforeEach(() => { mockUpdateMCP.mockClear() mockDeleteMCP.mockClear() @@ -450,7 +428,7 @@ describe('MCPCard', () => { // Confirm dialog should be shown await waitFor(() => { - expect(screen.getByTestId('confirm-dialog')).toBeInTheDocument() + expect(screen.getByText('tools.mcp.delete')).toBeInTheDocument() }) }) @@ -462,15 +440,14 @@ describe('MCPCard', () => { fireEvent.click(removeBtn) await waitFor(() => { - expect(screen.getByTestId('confirm-dialog')).toBeInTheDocument() + expect(screen.getByText('tools.mcp.delete')).toBeInTheDocument() }) // Cancel - const cancelBtn = screen.getByTestId('cancel-delete-btn') - fireEvent.click(cancelBtn) + fireEvent.click(getDeleteCancelButton()) await waitFor(() => { - expect(screen.queryByTestId('confirm-dialog')).not.toBeInTheDocument() + expect(screen.queryByText('tools.mcp.delete')).not.toBeInTheDocument() }) }) @@ -483,12 +460,11 @@ describe('MCPCard', () => { fireEvent.click(removeBtn) await waitFor(() => { - expect(screen.getByTestId('confirm-dialog')).toBeInTheDocument() + expect(screen.getByText('tools.mcp.delete')).toBeInTheDocument() }) // Confirm delete - const confirmBtn = screen.getByTestId('confirm-delete-btn') - fireEvent.click(confirmBtn) + fireEvent.click(getDeleteConfirmButton()) await waitFor(() => { expect(mockDeleteMCP).toHaveBeenCalledWith('mcp-1') @@ -506,12 +482,11 @@ describe('MCPCard', () => { fireEvent.click(removeBtn) await waitFor(() => { - expect(screen.getByTestId('confirm-dialog')).toBeInTheDocument() + expect(screen.getByText('tools.mcp.delete')).toBeInTheDocument() }) // Confirm delete - const confirmBtn = screen.getByTestId('confirm-delete-btn') - fireEvent.click(confirmBtn) + fireEvent.click(getDeleteConfirmButton()) await waitFor(() => { expect(mockDeleteMCP).toHaveBeenCalled() diff --git a/web/app/components/tools/mcp/detail/__tests__/content.spec.tsx b/web/app/components/tools/mcp/detail/__tests__/content.spec.tsx index 20a590459b..584c9d211a 100644 --- a/web/app/components/tools/mcp/detail/__tests__/content.spec.tsx +++ b/web/app/components/tools/mcp/detail/__tests__/content.spec.tsx @@ -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 ( -
- - -
- ) - }, -})) - // Mock OperationDropdown vi.mock('../operation-dropdown', () => ({ default: ({ onEdit, onRemove }: { onEdit: () => void, onRemove: () => void }) => ( @@ -165,6 +151,9 @@ describe('MCPDetailContent', () => { React.createElement(QueryClientProvider, { client: queryClient }, children) } + const getConfirmButton = () => screen.getByRole('button', { name: 'common.operation.confirm' }) + const getCancelButton = () => screen.getByRole('button', { name: 'common.operation.cancel' }) + const createMockDetail = (overrides = {}): ToolWithProvider => ({ id: 'mcp-1', name: 'Test MCP Server', @@ -494,7 +483,7 @@ describe('MCPDetailContent', () => { fireEvent.click(updateBtn) await waitFor(() => { - expect(screen.getByTestId('confirm-dialog')).toBeInTheDocument() + expect(screen.getByText('tools.mcp.toolUpdateConfirmTitle')).toBeInTheDocument() }) }) @@ -514,12 +503,11 @@ describe('MCPDetailContent', () => { fireEvent.click(updateBtn) await waitFor(() => { - expect(screen.getByTestId('confirm-dialog')).toBeInTheDocument() + expect(screen.getByText('tools.mcp.toolUpdateConfirmTitle')).toBeInTheDocument() }) // Confirm the update - const confirmBtn = screen.getByTestId('confirm-btn') - fireEvent.click(confirmBtn) + fireEvent.click(getConfirmButton()) await waitFor(() => { expect(mockUpdateTools).toHaveBeenCalledWith('mcp-1') @@ -636,7 +624,7 @@ describe('MCPDetailContent', () => { fireEvent.click(removeBtn) await waitFor(() => { - expect(screen.getByTestId('confirm-dialog')).toBeInTheDocument() + expect(screen.getByText('tools.mcp.delete')).toBeInTheDocument() }) }) @@ -648,15 +636,14 @@ describe('MCPDetailContent', () => { fireEvent.click(removeBtn) await waitFor(() => { - expect(screen.getByTestId('confirm-dialog')).toBeInTheDocument() + expect(screen.getByText('tools.mcp.delete')).toBeInTheDocument() }) // Cancel - const cancelBtn = screen.getByTestId('cancel-btn') - fireEvent.click(cancelBtn) + fireEvent.click(getCancelButton()) await waitFor(() => { - expect(screen.queryByTestId('confirm-dialog')).not.toBeInTheDocument() + expect(screen.queryByText('tools.mcp.delete')).not.toBeInTheDocument() }) }) @@ -669,12 +656,11 @@ describe('MCPDetailContent', () => { fireEvent.click(removeBtn) await waitFor(() => { - expect(screen.getByTestId('confirm-dialog')).toBeInTheDocument() + expect(screen.getByText('tools.mcp.delete')).toBeInTheDocument() }) // Confirm delete - const confirmBtn = screen.getByTestId('confirm-btn') - fireEvent.click(confirmBtn) + fireEvent.click(getConfirmButton()) await waitFor(() => { expect(mockDeleteMCP).toHaveBeenCalledWith('mcp-1') @@ -692,12 +678,11 @@ describe('MCPDetailContent', () => { fireEvent.click(removeBtn) await waitFor(() => { - expect(screen.getByTestId('confirm-dialog')).toBeInTheDocument() + expect(screen.getByText('tools.mcp.delete')).toBeInTheDocument() }) // Confirm delete - const confirmBtn = screen.getByTestId('confirm-btn') - fireEvent.click(confirmBtn) + fireEvent.click(getConfirmButton()) await waitFor(() => { expect(mockDeleteMCP).toHaveBeenCalled() @@ -840,15 +825,14 @@ describe('MCPDetailContent', () => { fireEvent.click(updateBtn) await waitFor(() => { - expect(screen.getByTestId('confirm-dialog')).toBeInTheDocument() + expect(screen.getByText('tools.mcp.toolUpdateConfirmTitle')).toBeInTheDocument() }) // Cancel the update - const cancelBtn = screen.getByTestId('cancel-btn') - fireEvent.click(cancelBtn) + fireEvent.click(getCancelButton()) await waitFor(() => { - expect(screen.queryByTestId('confirm-dialog')).not.toBeInTheDocument() + expect(screen.queryByText('tools.mcp.toolUpdateConfirmTitle')).not.toBeInTheDocument() }) }) }) diff --git a/web/app/components/tools/mcp/detail/content.tsx b/web/app/components/tools/mcp/detail/content.tsx index 4c95bd88f0..5c26bdcb7d 100644 --- a/web/app/components/tools/mcp/detail/content.tsx +++ b/web/app/components/tools/mcp/detail/content.tsx @@ -12,8 +12,16 @@ import * as React from 'react' import { useCallback, useEffect } from 'react' import { useTranslation } from 'react-i18next' import ActionButton from '@/app/components/base/action-button' -import Confirm from '@/app/components/base/confirm' import Tooltip from '@/app/components/base/tooltip' +import { + AlertDialog, + AlertDialogActions, + AlertDialogCancelButton, + AlertDialogConfirmButton, + AlertDialogContent, + AlertDialogDescription, + AlertDialogTitle, +} from '@/app/components/base/ui/alert-dialog' import { Button } from '@/app/components/base/ui/button' import Indicator from '@/app/components/header/indicator' import Icon from '@/app/components/plugins/card/base/card-icon' @@ -286,30 +294,42 @@ const MCPDetailContent: FC = ({ onHide={hideUpdateModal} /> )} - {isShowDeleteConfirm && ( - + !open && hideDeleteConfirm()}> + +
+ + {t('mcp.delete', { ns: 'tools' })} + +
{t('mcp.deleteConfirmTitle', { ns: 'tools', mcp: detail.name })}
- )} - onCancel={hideDeleteConfirm} - onConfirm={handleDelete} - isLoading={deleting} - isDisabled={deleting} - /> - )} - {isShowUpdateConfirm && ( - - )} +
+ + {t('operation.cancel', { ns: 'common' })} + + {t('operation.confirm', { ns: 'common' })} + + +
+
+ !open && hideUpdateConfirm()}> + +
+ + {t('mcp.toolUpdateConfirmTitle', { ns: 'tools' })} + + + {t('mcp.toolUpdateConfirmContent', { ns: 'tools' })} + +
+ + {t('operation.cancel', { ns: 'common' })} + + {t('operation.confirm', { ns: 'common' })} + + +
+
) } diff --git a/web/app/components/tools/mcp/mcp-service-card.tsx b/web/app/components/tools/mcp/mcp-service-card.tsx index 9ff2c7b7b3..3de157e4ac 100644 --- a/web/app/components/tools/mcp/mcp-service-card.tsx +++ b/web/app/components/tools/mcp/mcp-service-card.tsx @@ -6,12 +6,20 @@ import type { AppSSO } from '@/types/app' import { RiEditLine, RiLoopLeftLine } from '@remixicon/react' import { useState } from 'react' import { useTranslation } from 'react-i18next' -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 { + AlertDialog, + AlertDialogActions, + AlertDialogCancelButton, + AlertDialogConfirmButton, + AlertDialogContent, + AlertDialogDescription, + AlertDialogTitle, +} from '@/app/components/base/ui/alert-dialog' import { Button } from '@/app/components/base/ui/button' import Indicator from '@/app/components/header/indicator' import MCPServerModal from '@/app/components/tools/mcp/mcp-server-modal' @@ -295,16 +303,24 @@ const MCPServiceCard: FC = ({ /> )} - {showConfirmDelete && ( - - )} + !open && closeConfirmDelete()}> + +
+ + {t('overview.appInfo.regenerate', { ns: 'appOverview' })} + + + {t('mcp.server.reGen', { ns: 'tools' })} + +
+ + {t('operation.cancel', { ns: 'common' })} + + {t('operation.confirm', { ns: 'common' })} + + +
+
) } diff --git a/web/app/components/tools/mcp/provider-card.tsx b/web/app/components/tools/mcp/provider-card.tsx index d8a8e71a82..35b7e9f849 100644 --- a/web/app/components/tools/mcp/provider-card.tsx +++ b/web/app/components/tools/mcp/provider-card.tsx @@ -4,7 +4,14 @@ 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 { + AlertDialog, + AlertDialogActions, + AlertDialogCancelButton, + AlertDialogConfirmButton, + AlertDialogContent, + AlertDialogTitle, +} from '@/app/components/base/ui/alert-dialog' import Indicator from '@/app/components/header/indicator' import Icon from '@/app/components/plugins/card/base/card-icon' import { useAppContext } from '@/context/app-context' @@ -87,34 +94,34 @@ const MCPCard = ({
-
{data.name}
+
{data.name}
{data.server_identifier}
-
+
{data.tools.length > 0 && ( -
{t('mcp.toolsCount', { ns: 'tools', count: data.tools.length })}
+
{t('mcp.toolsCount', { ns: 'tools', count: data.tools.length })}
)} {!data.tools.length && ( -
{t('mcp.noTools', { ns: 'tools' })}
+
{t('mcp.noTools', { ns: 'tools' })}
)}
/
-
{`${t('mcp.updateTime', { ns: 'tools' })} ${formatTimeFromNow(data.updated_at! * 1000)}`}
+
{`${t('mcp.updateTime', { ns: 'tools' })} ${formatTimeFromNow(data.updated_at! * 1000)}`}
{data.is_team_authorization && data.tools.length > 0 && } {(!data.is_team_authorization || !data.tools.length) && ( -
+
{t('mcp.noConfigured', { ns: 'tools' })}
)}
{isCurrentWorkspaceManager && ( - ) diff --git a/web/app/components/workflow-app/components/workflow-onboarding-modal/index.tsx b/web/app/components/workflow-app/components/workflow-onboarding-modal/index.tsx index 65607abf28..d124705cf1 100644 --- a/web/app/components/workflow-app/components/workflow-onboarding-modal/index.tsx +++ b/web/app/components/workflow-app/components/workflow-onboarding-modal/index.tsx @@ -30,10 +30,10 @@ const WorkflowOnboardingModal: FC = ({
- + {t('onboarding.title', { ns: 'workflow' })} - + {t('onboarding.description', { ns: 'workflow' })}
@@ -47,7 +47,7 @@ const WorkflowOnboardingModal: FC = ({ {/* TODO: reduce z-1002 to match base/ui primitives after legacy overlay migration completes */} -
+
{t('onboarding.escTip.press', { ns: 'workflow' })} {t('onboarding.escTip.toDismiss', { ns: 'workflow' })} diff --git a/web/app/components/workflow/__tests__/workflow-edge-events.spec.tsx b/web/app/components/workflow/__tests__/workflow-edge-events.spec.tsx index b926646433..7ad6d0c13d 100644 --- a/web/app/components/workflow/__tests__/workflow-edge-events.spec.tsx +++ b/web/app/components/workflow/__tests__/workflow-edge-events.spec.tsx @@ -408,4 +408,46 @@ describe('Workflow edge event wiring', () => { expect(store.getState().edgeMenu).toBeUndefined() }) + + it('should render confirm description and clear showConfirm when cancelled', async () => { + const onConfirm = vi.fn() + const { store } = renderSubject({ + initialStoreState: { + showConfirm: { + title: 'Confirm title', + desc: 'Confirm description', + onConfirm, + }, + }, + }) + + expect(screen.getByRole('alertdialog')).toBeInTheDocument() + expect(screen.getByText('Confirm title')).toBeInTheDocument() + expect(screen.getByText('Confirm description')).toBeInTheDocument() + + fireEvent.click(screen.getByRole('button', { name: 'common.operation.cancel' })) + + await waitFor(() => { + expect(screen.queryByRole('alertdialog')).not.toBeInTheDocument() + }) + expect(store.getState().showConfirm).toBeUndefined() + expect(onConfirm).not.toHaveBeenCalled() + }) + + it('should call showConfirm.onConfirm when confirm is clicked', () => { + const onConfirm = vi.fn() + + renderSubject({ + initialStoreState: { + showConfirm: { + title: 'Confirm title', + onConfirm, + }, + }, + }) + + fireEvent.click(screen.getByRole('button', { name: 'common.operation.confirm' })) + + expect(onConfirm).toHaveBeenCalledTimes(1) + }) }) diff --git a/web/app/components/workflow/index.tsx b/web/app/components/workflow/index.tsx index 014193bc5c..f467a51160 100644 --- a/web/app/components/workflow/index.tsx +++ b/web/app/components/workflow/index.tsx @@ -23,6 +23,7 @@ import { useRef, useState, } from 'react' +import { useTranslation } from 'react-i18next' import ReactFlow, { Background, ReactFlowProvider, @@ -34,9 +35,17 @@ import ReactFlow, { useReactFlow, useStoreApi, } from 'reactflow' +import { + AlertDialog, + AlertDialogActions, + AlertDialogCancelButton, + AlertDialogConfirmButton, + AlertDialogContent, + AlertDialogDescription, + AlertDialogTitle, +} from '@/app/components/base/ui/alert-dialog' import { IS_DEV } from '@/config' import { useEventEmitterContextContext } from '@/context/event-emitter' -import dynamic from '@/next/dynamic' import { useAllBuiltInTools, useAllCustomTools, @@ -103,10 +112,6 @@ import { WorkflowHistoryProvider } from './workflow-history-store' import 'reactflow/dist/style.css' import './style.css' -const Confirm = dynamic(() => import('@/app/components/base/confirm'), { - ssr: false, -}) - const nodeTypes = { [CUSTOM_NODE]: CustomNode, [CUSTOM_NOTE_NODE]: CustomNoteNode, @@ -133,6 +138,7 @@ export const Workflow: FC = memo(({ children, onWorkflowDataUpdate, }) => { + const { t } = useTranslation() const workflowContainerRef = useRef(null) const workflowStore = useWorkflowStore() const reactflow = useReactFlow() @@ -396,7 +402,7 @@ export const Workflow: FC = memo(({
@@ -407,17 +413,26 @@ export const Workflow: FC = memo(({ - { - !!showConfirm && ( - setShowConfirm(undefined)} - onConfirm={showConfirm.onConfirm} - title={showConfirm.title} - content={showConfirm.desc} - /> - ) - } + !open && setShowConfirm(undefined)}> + +
+ + {showConfirm?.title} + + {showConfirm?.desc && ( + + {showConfirm.desc} + + )} +
+ + {t('operation.cancel', { ns: 'common' })} + + {t('operation.confirm', { ns: 'common' })} + + +
+
{children} { + it('should render title and content when open', () => { + render( + , + ) + + expect(screen.getByRole('alertdialog')).toBeInTheDocument() + expect(screen.getByText('workflow.common.effectVarConfirm.title')).toBeInTheDocument() + expect(screen.getByText('workflow.common.effectVarConfirm.content')).toBeInTheDocument() + }) + + it('should call onConfirm when confirm is clicked', () => { + const onConfirm = vi.fn() + + render( + , + ) + + fireEvent.click(screen.getByRole('button', { name: 'common.operation.confirm' })) + + expect(onConfirm).toHaveBeenCalledTimes(1) + }) + + it('should call onCancel when cancel is clicked', async () => { + const onCancel = vi.fn() + + render( + , + ) + + fireEvent.click(screen.getByRole('button', { name: 'common.operation.cancel' })) + + await waitFor(() => { + expect(onCancel).toHaveBeenCalledTimes(1) + }) + }) +}) diff --git a/web/app/components/workflow/nodes/_base/components/remove-effect-var-confirm.tsx b/web/app/components/workflow/nodes/_base/components/remove-effect-var-confirm.tsx index 903801880d..a3c4e5b0ad 100644 --- a/web/app/components/workflow/nodes/_base/components/remove-effect-var-confirm.tsx +++ b/web/app/components/workflow/nodes/_base/components/remove-effect-var-confirm.tsx @@ -2,7 +2,15 @@ import type { FC } from 'react' import * as React from 'react' import { useTranslation } from 'react-i18next' -import Confirm from '@/app/components/base/confirm' +import { + AlertDialog, + AlertDialogActions, + AlertDialogCancelButton, + AlertDialogConfirmButton, + AlertDialogContent, + AlertDialogDescription, + AlertDialogTitle, +} from '@/app/components/base/ui/alert-dialog' type Props = { isShow: boolean @@ -17,15 +25,30 @@ const RemoveVarConfirm: FC = ({ onCancel, }) => { const { t } = useTranslation() + const title = t(`${i18nPrefix}.title`, { ns: 'workflow' }) + const content = t(`${i18nPrefix}.content`, { ns: 'workflow' }) return ( - + !open && onCancel()}> + +
+ + {title} + + + {content} + +
+ + + {t('operation.cancel', { ns: 'common' })} + + + {t('operation.confirm', { ns: 'common' })} + + +
+
) } export default React.memo(RemoveVarConfirm) diff --git a/web/docs/overlay-migration.md b/web/docs/overlay-migration.md index 8d35e60982..d72e86404c 100644 --- a/web/docs/overlay-migration.md +++ b/web/docs/overlay-migration.md @@ -8,7 +8,6 @@ This document tracks the migration away from legacy overlay APIs. - `@/app/components/base/portal-to-follow-elem` - `@/app/components/base/tooltip` - `@/app/components/base/modal` - - `@/app/components/base/confirm` - `@/app/components/base/select` (including `custom` / `pure`) - `@/app/components/base/popover` - `@/app/components/base/dropdown` diff --git a/web/eslint-suppressions.json b/web/eslint-suppressions.json index 4efdaf36eb..6968859c1e 100644 --- a/web/eslint-suppressions.json +++ b/web/eslint-suppressions.json @@ -132,7 +132,10 @@ }, "app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/provider-config-modal.tsx": { "no-restricted-imports": { - "count": 2 + "count": 1 + }, + "perfectionist/sort-imports": { + "count": 1 } }, "app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/provider-panel.tsx": { @@ -281,7 +284,7 @@ }, "app/components/app-sidebar/dataset-info/dropdown.tsx": { "no-restricted-imports": { - "count": 2 + "count": 1 }, "ts/no-explicit-any": { "count": 1 @@ -334,14 +337,6 @@ "count": 1 } }, - "app/components/app/annotation/batch-action.tsx": { - "no-restricted-imports": { - "count": 1 - }, - "tailwindcss/enforce-consistent-class-order": { - "count": 4 - } - }, "app/components/app/annotation/batch-add-annotation-modal/csv-downloader.tsx": { "tailwindcss/enforce-consistent-class-order": { "count": 7 @@ -364,11 +359,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 @@ -381,9 +371,6 @@ } }, "app/components/app/annotation/edit-annotation-modal/index.tsx": { - "no-restricted-imports": { - "count": 1 - }, "tailwindcss/enforce-consistent-class-order": { "count": 2 } @@ -423,11 +410,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 @@ -437,9 +419,6 @@ "erasable-syntax-only/enums": { "count": 1 }, - "no-restricted-imports": { - "count": 1 - }, "react/set-state-in-effect": { "count": 5 }, @@ -469,9 +448,6 @@ } }, "app/components/app/app-publisher/features-wrapper.tsx": { - "no-restricted-imports": { - "count": 1 - }, "ts/no-explicit-any": { "count": 4 } @@ -586,9 +562,6 @@ }, "app/components/app/configuration/config-var/index.tsx": { "no-restricted-imports": { - "count": 2 - }, - "tailwindcss/enforce-consistent-class-order": { "count": 1 } }, @@ -699,7 +672,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 @@ -762,7 +735,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 @@ -1085,7 +1058,10 @@ }, "app/components/app/switch-app-modal/index.tsx": { "no-restricted-imports": { - "count": 2 + "count": 1 + }, + "perfectionist/sort-imports": { + "count": 1 }, "react/set-state-in-effect": { "count": 1 @@ -1387,21 +1363,12 @@ } }, "app/components/base/chat/chat-with-history/header-in-mobile.tsx": { - "no-restricted-imports": { - "count": 1 - }, - "tailwindcss/enforce-consistent-class-order": { - "count": 2 - }, "ts/no-explicit-any": { "count": 2 } }, "app/components/base/chat/chat-with-history/header/index.tsx": { "no-restricted-imports": { - "count": 2 - }, - "tailwindcss/enforce-consistent-class-order": { "count": 1 }, "ts/no-explicit-any": { @@ -1448,7 +1415,7 @@ } }, "app/components/base/chat/chat-with-history/sidebar/index.tsx": { - "no-restricted-imports": { + "perfectionist/sort-imports": { "count": 1 } }, @@ -1683,19 +1650,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 - } - }, "app/components/base/content-dialog/index.stories.tsx": { "react/set-state-in-effect": { "count": 1 @@ -3296,7 +3250,7 @@ }, "app/components/base/tag-management/tag-item-editor.tsx": { "no-restricted-imports": { - "count": 2 + "count": 1 }, "tailwindcss/enforce-consistent-class-order": { "count": 2 @@ -3709,7 +3663,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": { @@ -3986,10 +3940,7 @@ }, "app/components/datasets/documents/components/operations.tsx": { "no-restricted-imports": { - "count": 3 - }, - "tailwindcss/enforce-consistent-class-order": { - "count": 1 + "count": 2 } }, "app/components/datasets/documents/components/rename-modal.tsx": { @@ -4200,7 +4151,7 @@ } }, "app/components/datasets/documents/detail/completed/common/batch-action.tsx": { - "no-restricted-imports": { + "perfectionist/sort-imports": { "count": 1 } }, @@ -4293,10 +4244,7 @@ }, "app/components/datasets/documents/detail/completed/segment-card/index.tsx": { "no-restricted-imports": { - "count": 2 - }, - "tailwindcss/enforce-consistent-class-order": { - "count": 3 + "count": 1 } }, "app/components/datasets/documents/detail/completed/segment-detail.tsx": { @@ -4426,20 +4374,12 @@ }, "app/components/datasets/external-api/external-api-modal/index.tsx": { "no-restricted-imports": { - "count": 3 + "count": 2 }, "react/set-state-in-effect": { "count": 1 } }, - "app/components/datasets/external-api/external-knowledge-api-card/index.tsx": { - "no-restricted-imports": { - "count": 1 - }, - "tailwindcss/enforce-consistent-class-order": { - "count": 1 - } - }, "app/components/datasets/external-knowledge-base/create/ExternalApiSelect.tsx": { "react/set-state-in-effect": { "count": 1 @@ -4586,11 +4526,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 @@ -4695,7 +4630,10 @@ }, "app/components/datasets/metadata/metadata-dataset/dataset-metadata-drawer.tsx": { "no-restricted-imports": { - "count": 3 + "count": 2 + }, + "perfectionist/sort-imports": { + "count": 1 } }, "app/components/datasets/metadata/metadata-dataset/field.tsx": { @@ -4864,7 +4802,10 @@ }, "app/components/develop/secret-key/secret-key-modal.tsx": { "no-restricted-imports": { - "count": 2 + "count": 1 + }, + "perfectionist/sort-imports": { + "count": 1 } }, "app/components/develop/tag.tsx": { @@ -5094,11 +5035,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 @@ -5118,12 +5054,6 @@ } }, "app/components/header/account-setting/data-source-page-new/card.tsx": { - "no-restricted-imports": { - "count": 1 - }, - "tailwindcss/enforce-consistent-class-order": { - "count": 3 - }, "ts/no-explicit-any": { "count": 2 } @@ -5298,7 +5228,7 @@ }, "app/components/header/account-setting/model-provider-page/model-auth/authorized/index.tsx": { "no-restricted-imports": { - "count": 3 + "count": 2 }, "ts/no-explicit-any": { "count": 2 @@ -5502,7 +5432,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 @@ -5821,7 +5751,7 @@ }, "app/components/plugins/plugin-auth/authorized/index.tsx": { "no-restricted-imports": { - "count": 3 + "count": 2 }, "ts/no-explicit-any": { "count": 2 @@ -5954,10 +5884,7 @@ }, "app/components/plugins/plugin-detail-panel/endpoint-card.tsx": { "no-restricted-imports": { - "count": 2 - }, - "tailwindcss/enforce-consistent-class-order": { - "count": 5 + "count": 1 }, "ts/no-explicit-any": { "count": 2 @@ -6075,11 +6002,6 @@ "count": 1 } }, - "app/components/plugins/plugin-detail-panel/subscription-list/delete-confirm.tsx": { - "tailwindcss/enforce-consistent-class-order": { - "count": 4 - } - }, "app/components/plugins/plugin-detail-panel/subscription-list/edit/apikey-edit-modal.tsx": { "erasable-syntax-only/enums": { "count": 1 @@ -6212,7 +6134,7 @@ }, "app/components/plugins/plugin-item/action.tsx": { "no-restricted-imports": { - "count": 2 + "count": 1 } }, "app/components/plugins/plugin-item/index.tsx": { @@ -6392,11 +6314,6 @@ "count": 2 } }, - "app/components/rag-pipeline/components/conversion.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, "app/components/rag-pipeline/components/panel/input-field/editor/form/hidden-fields.tsx": { "react/component-hook-factories": { "count": 1 @@ -6542,11 +6459,6 @@ "count": 1 } }, - "app/components/rag-pipeline/components/rag-pipeline-header/publisher/popup.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, "app/components/rag-pipeline/components/rag-pipeline-header/run-mode.tsx": { "tailwindcss/enforce-consistent-class-order": { "count": 1 @@ -6760,7 +6672,7 @@ }, "app/components/tools/mcp/detail/content.tsx": { "no-restricted-imports": { - "count": 2 + "count": 1 }, "ts/no-explicit-any": { "count": 3 @@ -6807,7 +6719,7 @@ }, "app/components/tools/mcp/mcp-service-card.tsx": { "no-restricted-imports": { - "count": 2 + "count": 1 } }, "app/components/tools/mcp/modal.tsx": { @@ -6816,15 +6728,6 @@ } }, "app/components/tools/mcp/provider-card.tsx": { - "no-restricted-imports": { - "count": 1 - }, - "tailwindcss/enforce-consistent-class-order": { - "count": 7 - }, - "tailwindcss/no-unnecessary-whitespace": { - "count": 1 - }, "ts/no-explicit-any": { "count": 3 } @@ -6852,11 +6755,6 @@ "count": 1 } }, - "app/components/tools/provider/detail.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, "app/components/tools/provider/empty.tsx": { "tailwindcss/enforce-consistent-class-order": { "count": 1 @@ -6914,11 +6812,6 @@ "count": 2 } }, - "app/components/workflow-app/components/workflow-onboarding-modal/index.tsx": { - "tailwindcss/enforce-consistent-class-order": { - "count": 3 - } - }, "app/components/workflow-app/components/workflow-onboarding-modal/start-node-option.tsx": { "tailwindcss/enforce-consistent-class-order": { "count": 2 @@ -7371,9 +7264,6 @@ } }, "app/components/workflow/index.tsx": { - "tailwindcss/enforce-consistent-class-order": { - "count": 1 - }, "ts/no-explicit-any": { "count": 2 } @@ -7724,11 +7614,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 @@ -10363,9 +10248,6 @@ } }, "hooks/use-pay.tsx": { - "no-restricted-imports": { - "count": 2 - }, "react/set-state-in-effect": { "count": 4 } diff --git a/web/eslint.constants.mjs b/web/eslint.constants.mjs index d449042542..d6b6f834a1 100644 --- a/web/eslint.constants.mjs +++ b/web/eslint.constants.mjs @@ -52,13 +52,6 @@ export const OVERLAY_RESTRICTED_IMPORT_PATTERNS = [ ], message: 'Deprecated: use @/app/components/base/ui/select instead. See issue #32767.', }, - { - group: [ - '**/base/confirm', - '**/base/confirm/index', - ], - message: 'Deprecated: use @/app/components/base/ui/alert-dialog instead. See issue #32767.', - }, { group: [ '**/base/popover', diff --git a/web/hooks/use-pay.tsx b/web/hooks/use-pay.tsx index 3a90a79805..237913b433 100644 --- a/web/hooks/use-pay.tsx +++ b/web/hooks/use-pay.tsx @@ -1,13 +1,22 @@ 'use client' -import type { IConfirm } from '@/app/components/base/confirm' import { useCallback, useEffect, useState } from 'react' import { useTranslation } from 'react-i18next' -import Confirm from '@/app/components/base/confirm' +import { + AlertDialog, + AlertDialogActions, + AlertDialogConfirmButton, + AlertDialogContent, + AlertDialogDescription, + AlertDialogTitle, +} from '@/app/components/base/ui/alert-dialog' import { useRouter, useSearchParams } from '@/next/navigation' import { useNotionBinding } from '@/service/use-common' -type ConfirmType = Pick +type ConfirmType = { + type: 'info' | 'warning' + title: string +} const useAnthropicCheckPay = () => { const { t } = useTranslation() @@ -96,16 +105,32 @@ export const CheckModal = () => { if (!confirmInfo || !showPayStatusModal) return null + const description = (confirmInfo as { desc?: string }).desc || '' + return ( - + !open && handleCancelShowPayStatusModal()}> + +
+ + {confirmInfo.title} + + {description && ( + + {description} + + )} +
+ + + {confirmInfo.type === 'info' + ? t('operation.ok', { ns: 'common' }) + : t('operation.confirm', { ns: 'common' })} + + +
+
) }