From 23291398ec9b8d33df68d3173b8d423261bdefb2 Mon Sep 17 00:00:00 2001 From: JzoNg Date: Fri, 10 Apr 2026 18:13:13 +0800 Subject: [PATCH] feat(web): switch warining dialog --- .../app-publisher/__tests__/index.spec.tsx | 115 +++++++++++++ ...luation-workflow-switch-confirm-dialog.tsx | 158 ++++++++++++++++++ .../components/app/app-publisher/index.tsx | 78 ++++++++- web/contract/console/evaluation.ts | 13 ++ web/contract/router.ts | 2 + web/eslint-suppressions.json | 26 +-- web/i18n/en-US/workflow.json | 10 ++ web/service/use-evaluation.ts | 19 +++ web/types/evaluation.ts | 12 ++ 9 files changed, 407 insertions(+), 26 deletions(-) create mode 100644 web/app/components/app/app-publisher/evaluation-workflow-switch-confirm-dialog.tsx diff --git a/web/app/components/app/app-publisher/__tests__/index.spec.tsx b/web/app/components/app/app-publisher/__tests__/index.spec.tsx index 1c81d84612..4ee8c18fa2 100644 --- a/web/app/components/app/app-publisher/__tests__/index.spec.tsx +++ b/web/app/components/app/app-publisher/__tests__/index.spec.tsx @@ -16,6 +16,7 @@ const mockFetchInstalledAppList = vi.fn() const mockFetchAppDetailDirect = vi.fn() const mockToastError = vi.fn() const mockConvertWorkflowType = vi.fn() +const mockRefetchEvaluationWorkflowAssociatedTargets = vi.fn() const sectionProps = vi.hoisted(() => ({ summary: null as null | Record, @@ -27,6 +28,7 @@ const ahooksMocks = vi.hoisted(() => ({ })) let mockAppDetail: Record | null = null +let mockEvaluationWorkflowAssociatedTargets: Record | undefined vi.mock('react-i18next', () => ({ useTranslation: () => ({ @@ -96,6 +98,14 @@ vi.mock('@/service/use-apps', () => ({ }), })) +vi.mock('@/service/use-evaluation', () => ({ + useEvaluationWorkflowAssociatedTargets: () => ({ + data: mockEvaluationWorkflowAssociatedTargets, + refetch: mockRefetchEvaluationWorkflowAssociatedTargets, + isFetching: false, + }), +})) + vi.mock('@/app/components/base/ui/toast', () => ({ toast: { error: (...args: unknown[]) => mockToastError(...args), @@ -198,6 +208,11 @@ describe('AppPublisher', () => { access_mode: AccessMode.PUBLIC, }) mockConvertWorkflowType.mockResolvedValue({}) + mockEvaluationWorkflowAssociatedTargets = { items: [] } + mockRefetchEvaluationWorkflowAssociatedTargets.mockResolvedValue({ + data: { items: [] }, + isError: false, + }) mockOpenAsyncWindow.mockImplementation(async (resolver: () => Promise) => { await resolver() }) @@ -518,6 +533,106 @@ describe('AppPublisher', () => { }) }) + it('should confirm before switching an evaluation workflow with associated targets to a standard workflow', async () => { + mockAppDetail = { + ...mockAppDetail, + type: AppTypeEnum.EVALUATION, + } + mockEvaluationWorkflowAssociatedTargets = { + items: [ + { + target_type: 'app', + target_id: 'dependent-app-1', + target_name: 'Dependent App', + }, + { + target_type: 'knowledge_base', + target_id: 'knowledge-1', + target_name: 'Knowledge Base', + }, + ], + } + mockRefetchEvaluationWorkflowAssociatedTargets.mockResolvedValueOnce({ + data: mockEvaluationWorkflowAssociatedTargets, + isError: false, + }) + + render( + , + ) + + fireEvent.click(screen.getByText('common.publish')) + fireEvent.click(screen.getByText('publisher-switch-workflow-type')) + + await waitFor(() => { + expect(mockRefetchEvaluationWorkflowAssociatedTargets).toHaveBeenCalledTimes(1) + }) + expect(mockConvertWorkflowType).not.toHaveBeenCalled() + expect(screen.getByText('Dependent App')).toBeInTheDocument() + expect(screen.getByText('Knowledge Base')).toBeInTheDocument() + + fireEvent.click(screen.getByRole('button', { name: 'common.switchToStandardWorkflowConfirm.switch' })) + + await waitFor(() => { + expect(mockConvertWorkflowType).toHaveBeenCalledWith({ + params: { appId: 'app-1' }, + query: { target_type: AppTypeEnum.WORKFLOW }, + }) + }) + }) + + it('should switch an evaluation workflow directly when there are no associated targets', async () => { + mockAppDetail = { + ...mockAppDetail, + type: AppTypeEnum.EVALUATION, + } + + render( + , + ) + + fireEvent.click(screen.getByText('common.publish')) + fireEvent.click(screen.getByText('publisher-switch-workflow-type')) + + await waitFor(() => { + expect(mockRefetchEvaluationWorkflowAssociatedTargets).toHaveBeenCalledTimes(1) + expect(mockConvertWorkflowType).toHaveBeenCalledWith({ + params: { appId: 'app-1' }, + query: { target_type: AppTypeEnum.WORKFLOW }, + }) + }) + expect(screen.queryByText('common.switchToStandardWorkflowConfirm.title')).not.toBeInTheDocument() + }) + + it('should block switching an evaluation workflow when associated targets fail to load', async () => { + mockAppDetail = { + ...mockAppDetail, + type: AppTypeEnum.EVALUATION, + } + mockRefetchEvaluationWorkflowAssociatedTargets.mockResolvedValueOnce({ + data: undefined, + isError: true, + }) + + render( + , + ) + + fireEvent.click(screen.getByText('common.publish')) + fireEvent.click(screen.getByText('publisher-switch-workflow-type')) + + await waitFor(() => { + expect(mockToastError).toHaveBeenCalledWith('common.switchToStandardWorkflowConfirm.loadFailed') + }) + expect(mockConvertWorkflowType).not.toHaveBeenCalled() + }) + it('should block switching to evaluation workflow when restricted nodes exist', async () => { render( void + onConfirm: () => void +} + +const TARGET_TYPE_META: Record + href: (targetId: string) => string +}> = { + app: { + icon: 'i-ri-flow-chart', + iconClassName: 'bg-components-icon-bg-teal-soft text-util-colors-teal-teal-600', + labelKey: 'common.switchToStandardWorkflowConfirm.targetTypes.app', + href: targetId => `/app/${targetId}/workflow`, + }, + snippets: { + icon: 'i-ri-edit-2-line', + iconClassName: 'bg-components-icon-bg-violet-soft text-util-colors-violet-violet-600', + labelKey: 'common.switchToStandardWorkflowConfirm.targetTypes.snippets', + href: targetId => `/snippets/${targetId}/orchestrate`, + }, + knowledge_base: { + icon: 'i-ri-book-2-line', + iconClassName: 'bg-components-icon-bg-indigo-soft text-util-colors-blue-blue-600', + labelKey: 'common.switchToStandardWorkflowConfirm.targetTypes.knowledge_base', + href: targetId => `/datasets/${targetId}/documents`, + }, +} + +const getTargetMeta = (targetType: EvaluationWorkflowAssociatedTargetType) => { + return TARGET_TYPE_META[targetType] ?? TARGET_TYPE_META.app +} + +const DependentTargetItem = ({ + target, +}: { + target: EvaluationWorkflowAssociatedTarget +}) => { + const { t } = useTranslation() + const meta = getTargetMeta(target.target_type) + const targetName = target.target_name || target.target_id + + return ( + +