diff --git a/web/app/components/plugins/install-plugin/install-bundle/index.tsx b/web/app/components/plugins/install-plugin/install-bundle/index.tsx index 5a7cf2e5f5..0a1021591a 100644 --- a/web/app/components/plugins/install-plugin/install-bundle/index.tsx +++ b/web/app/components/plugins/install-plugin/install-bundle/index.tsx @@ -57,7 +57,7 @@ const InstallBundle: FC = ({ foldAnimInto() }} > - +
diff --git a/web/app/components/rag-pipeline/components/publish-as-knowledge-pipeline-modal.tsx b/web/app/components/rag-pipeline/components/publish-as-knowledge-pipeline-modal.tsx index a7f06197b5..1a6ed46ec0 100644 --- a/web/app/components/rag-pipeline/components/publish-as-knowledge-pipeline-modal.tsx +++ b/web/app/components/rag-pipeline/components/publish-as-knowledge-pipeline-modal.tsx @@ -77,7 +77,7 @@ const PublishAsKnowledgePipelineModal = ({ return ( <> - +
{t('common.publishAs', { ns: 'pipeline' })} 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 afd7c04ed1..ffc7d684ed 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 @@ -480,7 +480,9 @@ describe('publisher', () => { it('should show publish as knowledge pipeline modal when permitted', async () => { mockPublishedAt.mockReturnValue(1700000000) mockIsAllowPublishAsCustomKnowledgePipelineTemplate.mockReturnValue(true) - renderWithQueryClient() + renderWithQueryClient() + + fireEvent.click(screen.getByText('workflow.common.publish')) const publishAsButton = screen.getAllByRole('button').find(btn => btn.textContent?.includes('pipeline.common.publishAs'), @@ -495,7 +497,9 @@ describe('publisher', () => { it('should close publish as knowledge pipeline modal when cancel is clicked', async () => { mockPublishedAt.mockReturnValue(1700000000) mockIsAllowPublishAsCustomKnowledgePipelineTemplate.mockReturnValue(true) - renderWithQueryClient() + renderWithQueryClient() + + fireEvent.click(screen.getByText('workflow.common.publish')) const publishAsButton = screen.getAllByRole('button').find(btn => btn.textContent?.includes('pipeline.common.publishAs'), @@ -516,7 +520,9 @@ describe('publisher', () => { it('should call publishAsCustomizedPipeline when confirm is clicked in modal', async () => { mockPublishedAt.mockReturnValue(1700000000) mockPublishAsCustomizedPipeline.mockResolvedValue({}) - renderWithQueryClient() + renderWithQueryClient() + + fireEvent.click(screen.getByText('workflow.common.publish')) const publishAsButton = screen.getAllByRole('button').find(btn => btn.textContent?.includes('pipeline.common.publishAs'), @@ -538,6 +544,35 @@ describe('publisher', () => { }) }) }) + + it('should publish as template with empty pipeline id fallback', async () => { + mockPublishedAt.mockReturnValue(1700000000) + mockPipelineId.mockReturnValue(undefined as unknown as string) + mockPublishAsCustomizedPipeline.mockResolvedValue({}) + renderWithQueryClient() + + fireEvent.click(screen.getByText('workflow.common.publish')) + + const publishAsButton = screen.getAllByRole('button').find(btn => + btn.textContent?.includes('pipeline.common.publishAs'), + ) + fireEvent.click(publishAsButton!) + + await waitFor(() => { + expect(screen.getByTestId('publish-as-knowledge-pipeline-modal')).toBeInTheDocument() + }) + + fireEvent.click(screen.getByTestId('modal-confirm')) + + await waitFor(() => { + expect(mockPublishAsCustomizedPipeline).toHaveBeenCalledWith({ + pipelineId: '', + name: 'Test Pipeline', + icon_info: { type: 'emoji', emoji: '📚', background: '#fff' }, + description: 'Test description', + }) + }) + }) }) describe('API Calls and Async Operations', () => { @@ -607,7 +642,9 @@ describe('publisher', () => { it('should show success notification for publish as template', async () => { mockPublishedAt.mockReturnValue(1700000000) mockPublishAsCustomizedPipeline.mockResolvedValue({}) - renderWithQueryClient() + renderWithQueryClient() + + fireEvent.click(screen.getByText('workflow.common.publish')) const publishAsButton = screen.getAllByRole('button').find(btn => btn.textContent?.includes('pipeline.common.publishAs'), @@ -633,7 +670,9 @@ describe('publisher', () => { it('should invalidate customized template list after publish as template', async () => { mockPublishedAt.mockReturnValue(1700000000) mockPublishAsCustomizedPipeline.mockResolvedValue({}) - renderWithQueryClient() + renderWithQueryClient() + + fireEvent.click(screen.getByText('workflow.common.publish')) const publishAsButton = screen.getAllByRole('button').find(btn => btn.textContent?.includes('pipeline.common.publishAs'), @@ -686,7 +725,9 @@ describe('publisher', () => { it('should show error notification when publish as template fails', async () => { mockPublishedAt.mockReturnValue(1700000000) mockPublishAsCustomizedPipeline.mockRejectedValue(new Error('Template publish failed')) - renderWithQueryClient() + renderWithQueryClient() + + fireEvent.click(screen.getByText('workflow.common.publish')) const publishAsButton = screen.getAllByRole('button').find(btn => btn.textContent?.includes('pipeline.common.publishAs'), @@ -710,7 +751,9 @@ describe('publisher', () => { it('should close modal after publish as template error', async () => { mockPublishedAt.mockReturnValue(1700000000) mockPublishAsCustomizedPipeline.mockRejectedValue(new Error('Template publish failed')) - renderWithQueryClient() + renderWithQueryClient() + + fireEvent.click(screen.getByText('workflow.common.publish')) const publishAsButton = screen.getAllByRole('button').find(btn => btn.textContent?.includes('pipeline.common.publishAs'), @@ -1051,7 +1094,9 @@ describe('publisher', () => { it('should complete full publish as template flow', async () => { mockPublishedAt.mockReturnValue(1700000000) mockPublishAsCustomizedPipeline.mockResolvedValue({}) - renderWithQueryClient() + renderWithQueryClient() + + fireEvent.click(screen.getByText('workflow.common.publish')) const publishAsButton = screen.getAllByRole('button').find(btn => btn.textContent?.includes('pipeline.common.publishAs'), 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 dab8046c43..6d8ee0ca04 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 @@ -327,11 +327,18 @@ describe('Popup', () => { it('should request closing the outer popover before opening publish-as modal', () => { const onRequestClose = vi.fn() - render() + const onShowPublishAsKnowledgePipelineModal = vi.fn() + render( + , + ) fireEvent.click(screen.getByText('pipeline.common.publishAs')) expect(onRequestClose).toHaveBeenCalledTimes(1) + expect(onShowPublishAsKnowledgePipelineModal).toHaveBeenCalledTimes(1) }) }) @@ -352,27 +359,6 @@ describe('Popup', () => { }) }) - describe('Publish params', () => { - it('should publish as template with empty pipeline id fallback', async () => { - mockPipelineId = undefined - mockUseBoolean - .mockImplementationOnce((initial: boolean) => [initial, { setFalse: vi.fn(), setTrue: vi.fn() }]) - .mockImplementationOnce((initial: boolean) => [initial, { setFalse: vi.fn(), setTrue: vi.fn() }]) - .mockImplementationOnce(() => [true, { setFalse: vi.fn(), setTrue: vi.fn() }]) - .mockImplementationOnce((initial: boolean) => [initial, { setFalse: vi.fn(), setTrue: vi.fn() }]) - render() - - fireEvent.click(screen.getByTestId('publish-as-confirm')) - - expect(mockPublishAsCustomizedPipeline).toHaveBeenCalledWith({ - pipelineId: '', - name: 'My Pipeline', - icon_info: { icon_type: 'emoji' }, - description: 'desc', - }) - }) - }) - describe('Time formatting', () => { it('should format published time', () => { render() diff --git a/web/app/components/rag-pipeline/components/rag-pipeline-header/publisher/index.tsx b/web/app/components/rag-pipeline/components/rag-pipeline-header/publisher/index.tsx index 1d2ef242f8..9e9f4b2b79 100644 --- a/web/app/components/rag-pipeline/components/rag-pipeline-header/publisher/index.tsx +++ b/web/app/components/rag-pipeline/components/rag-pipeline-header/publisher/index.tsx @@ -1,6 +1,8 @@ +import type { IconInfo } from '@/models/datasets' import { Button } from '@langgenius/dify-ui/button' import { cn } from '@langgenius/dify-ui/cn' import { Popover, PopoverContent, PopoverTrigger } from '@langgenius/dify-ui/popover' +import { toast } from '@langgenius/dify-ui/toast' import { RiArrowDownSLine } from '@remixicon/react' import { useBoolean } from 'ahooks' import { @@ -10,6 +12,11 @@ import { } from 'react' import { useTranslation } from 'react-i18next' import { useNodesSyncDraft } from '@/app/components/workflow/hooks' +import { useStore } from '@/app/components/workflow/store' +import { useDocLink } from '@/context/i18n' +import Link from '@/next/link' +import { useInvalidCustomizedTemplateList, usePublishAsCustomizedPipeline } from '@/service/use-pipeline' +import PublishAsKnowledgePipelineModal from '../../publish-as-knowledge-pipeline-modal' import Popup from './popup' const Publisher = () => { @@ -17,6 +24,12 @@ const Publisher = () => { const [open, setOpen] = useState(false) const [confirmVisible, { setFalse: hideConfirm, setTrue: showConfirm }] = useBoolean(false) const { handleSyncWorkflowDraft } = useNodesSyncDraft() + const docLink = useDocLink() + const pipelineId = useStore(s => s.pipelineId) + const { mutateAsync: publishAsCustomizedPipeline } = usePublishAsCustomizedPipeline() + const invalidCustomizedTemplateList = useInvalidCustomizedTemplateList() + const [showPublishAsKnowledgePipelineModal, setShowPublishAsKnowledgePipelineModal] = useState(false) + const [isPublishingAsCustomizedPipeline, setIsPublishingAsCustomizedPipeline] = useState(false) const handleOpenChange = useCallback((newOpen: boolean) => { if (!newOpen && confirmVisible) @@ -28,38 +41,86 @@ const Publisher = () => { const closePopover = useCallback(() => { setOpen(false) }, []) + const openPublishAsKnowledgePipelineModal = useCallback(() => { + setShowPublishAsKnowledgePipelineModal(true) + }, []) + const hidePublishAsKnowledgePipelineModal = useCallback(() => { + setShowPublishAsKnowledgePipelineModal(false) + }, []) + const handlePublishAsKnowledgePipeline = useCallback(async (name: string, icon: IconInfo, description?: string) => { + try { + setIsPublishingAsCustomizedPipeline(true) + await publishAsCustomizedPipeline({ + pipelineId: pipelineId || '', + name, + icon_info: icon, + description, + }) + toast.success(t('publishTemplate.success.message', { ns: 'datasetPipeline' }), { + description: ( +
+ + {t('publishTemplate.success.tip', { ns: 'datasetPipeline' })} + + + {t('publishTemplate.success.learnMore', { ns: 'datasetPipeline' })} + +
+ ), + }) + invalidCustomizedTemplateList() + } + catch { + toast.error(t('publishTemplate.error.message', { ns: 'datasetPipeline' })) + } + finally { + setIsPublishingAsCustomizedPipeline(false) + hidePublishAsKnowledgePipelineModal() + } + }, [docLink, hidePublishAsKnowledgePipelineModal, invalidCustomizedTemplateList, pipelineId, publishAsCustomizedPipeline, t]) return ( - - - {t('common.publish', { ns: 'workflow' })} - - - )} - /> - + - + {t('common.publish', { ns: 'workflow' })} + + + )} /> - - + + + + + {showPublishAsKnowledgePipelineModal && ( + + )} + ) } 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 1c2f7177a5..ba18f2d963 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 @@ -1,4 +1,3 @@ -import type { IconInfo } from '@/models/datasets' import type { PublishWorkflowParams } from '@/types/workflow' import { AlertDialog, @@ -25,7 +24,6 @@ import ShortcutsName from '@/app/components/workflow/shortcuts-name' import { useStore, useWorkflowStore } from '@/app/components/workflow/store' import { getKeyboardKeyCodeBySystem } from '@/app/components/workflow/utils' import { useDatasetDetailContextWithSelector } from '@/context/dataset-detail' -import { useDocLink } from '@/context/i18n' import { useModalContextSelector } from '@/context/modal-context' import { useProviderContextSelector } from '@/context/provider-context' import { useDatasetApiAccessUrl } from '@/hooks/use-api-access-url' @@ -34,9 +32,8 @@ import Link from '@/next/link' import { useParams, useRouter } from '@/next/navigation' import { useInvalidDatasetList } from '@/service/knowledge/use-dataset' import { useInvalid } from '@/service/use-base' -import { publishedPipelineInfoQueryKeyPrefix, useInvalidCustomizedTemplateList, usePublishAsCustomizedPipeline } from '@/service/use-pipeline' +import { publishedPipelineInfoQueryKeyPrefix } from '@/service/use-pipeline' import { usePublishWorkflow } from '@/service/use-workflow' -import PublishAsKnowledgePipelineModal from '../../publish-as-knowledge-pipeline-modal' const PUBLISH_SHORTCUT = ['ctrl', '⇧', 'P'] type PopupProps = { @@ -44,6 +41,8 @@ type PopupProps = { confirmVisible?: boolean onShowConfirm?: () => void onHideConfirm?: () => void + isPublishingAsCustomizedPipeline?: boolean + onShowPublishAsKnowledgePipelineModal?: () => void } const Popup = ({ @@ -51,11 +50,12 @@ const Popup = ({ confirmVisible: controlledConfirmVisible, onShowConfirm, onHideConfirm, + isPublishingAsCustomizedPipeline = false, + onShowPublishAsKnowledgePipelineModal, }: PopupProps) => { const { t } = useTranslation() const { datasetId } = useParams() const { push } = useRouter() - const docLink = useDocLink() const publishedAt = useStore(s => s.publishedAt) const draftUpdatedAt = useStore(s => s.draftUpdatedAt) const pipelineId = useStore(s => s.pipelineId) @@ -73,9 +73,6 @@ const Popup = ({ const showConfirm = onShowConfirm ?? showLocalConfirm const hideConfirm = onHideConfirm ?? hideLocalConfirm const [publishing, { setFalse: hidePublishing, setTrue: showPublishing }] = useBoolean(false) - const { mutateAsync: publishAsCustomizedPipeline } = usePublishAsCustomizedPipeline() - const [showPublishAsKnowledgePipelineModal, { setFalse: hidePublishAsKnowledgePipelineModal, setTrue: setShowPublishAsKnowledgePipelineModal }] = useBoolean(false) - const [isPublishingAsCustomizedPipeline, { setFalse: hidePublishingAsCustomizedPipeline, setTrue: showPublishingAsCustomizedPipeline }] = useBoolean(false) const invalidPublishedPipelineInfo = useInvalid([...publishedPipelineInfoQueryKeyPrefix, pipelineId]) const invalidDatasetList = useInvalidDatasetList() const handleHideConfirm = useCallback(() => { @@ -145,47 +142,15 @@ const Popup = ({ const goToAddDocuments = useCallback(() => { push(`/datasets/${datasetId}/documents/create-from-pipeline`) }, [datasetId, push]) - const invalidCustomizedTemplateList = useInvalidCustomizedTemplateList() - const handlePublishAsKnowledgePipeline = useCallback(async (name: string, icon: IconInfo, description?: string) => { - try { - showPublishingAsCustomizedPipeline() - await publishAsCustomizedPipeline({ - pipelineId: pipelineId || '', - name, - icon_info: icon, - description, - }) - toast.success(t('publishTemplate.success.message', { ns: 'datasetPipeline' }), { - description: ( -
- - {t('publishTemplate.success.tip', { ns: 'datasetPipeline' })} - - - {t('publishTemplate.success.learnMore', { ns: 'datasetPipeline' })} - -
- ), - }) - invalidCustomizedTemplateList() - } - catch { - toast.error(t('publishTemplate.error.message', { ns: 'datasetPipeline' })) - } - finally { - hidePublishingAsCustomizedPipeline() - hidePublishAsKnowledgePipelineModal() - } - }, [showPublishingAsCustomizedPipeline, publishAsCustomizedPipeline, pipelineId, t, invalidCustomizedTemplateList, hidePublishingAsCustomizedPipeline, hidePublishAsKnowledgePipelineModal, docLink]) const handleClickPublishAsKnowledgePipeline = useCallback(() => { onRequestClose?.() if (!isAllowPublishAsCustomKnowledgePipelineTemplate) { setShowPricingModal() } else { - setShowPublishAsKnowledgePipelineModal() + onShowPublishAsKnowledgePipelineModal?.() } - }, [isAllowPublishAsCustomKnowledgePipelineTemplate, onRequestClose, setShowPublishAsKnowledgePipelineModal, setShowPricingModal]) + }, [isAllowPublishAsCustomKnowledgePipelineTemplate, onRequestClose, onShowPublishAsKnowledgePipelineModal, setShowPricingModal]) return (
@@ -279,7 +244,6 @@ const Popup = ({ - {showPublishAsKnowledgePipelineModal && ()}
) }