'use client' import type { VersionHistory } from '@/types/workflow' import { toast } from '@langgenius/dify-ui/toast' import { RiArrowDownDoubleLine, RiCloseLine, RiLoader2Line } from '@remixicon/react' import copy from 'copy-to-clipboard' import { useAtomValue } from 'jotai' import * as React from 'react' import { useCallback, useState } from 'react' import { useTranslation } from 'react-i18next' import VersionInfoModal from '@/app/components/app/app-publisher/version-info-modal' import Divider from '@/app/components/base/divider' import { PlanUpgradeModal } from '@/app/components/billing/plan-upgrade-modal' import { Plan } from '@/app/components/billing/type' import { userProfileAtom } from '@/context/account-state' import { useProviderContext } from '@/context/provider-context' import { useDeleteWorkflow, useInvalidAllLastRun, useResetWorkflowVersionHistory, useRestoreWorkflow, useUpdateWorkflow, useWorkflowVersionHistory, } from '@/service/use-workflow' import { useDSL, useWorkflowRefreshDraft, useWorkflowRun } from '../../hooks' import { useHooksStore } from '../../hooks-store' import { useStore, useWorkflowStore } from '../../store' import { VersionHistoryContextMenuOptions, WorkflowVersion, WorkflowVersionFilterOptions, } from '../../types' import DeleteConfirmModal from './delete-confirm-modal' import Empty from './empty' import Filter from './filter' import Loading from './loading' import RestoreConfirmModal from './restore-confirm-modal' import VersionHistoryItem from './version-history-item' const HISTORY_PER_PAGE = 10 const INITIAL_PAGE = 1 export type VersionHistoryPanelProps = { getVersionListUrl?: string deleteVersionUrl?: (versionId: string) => string restoreVersionUrl: (versionId: string) => string updateVersionUrl?: (versionId: string) => string latestVersionId?: string } export const VersionHistoryPanel = ({ getVersionListUrl, deleteVersionUrl, restoreVersionUrl, updateVersionUrl, latestVersionId, }: VersionHistoryPanelProps) => { const [filterValue, setFilterValue] = useState(WorkflowVersionFilterOptions.all) const [isOnlyShowNamedVersions, setIsOnlyShowNamedVersions] = useState(false) const [operatedItem, setOperatedItem] = useState() const [restoreConfirmOpen, setRestoreConfirmOpen] = useState(false) const [isRestorePlanUpgradeModalOpen, setIsRestorePlanUpgradeModalOpen] = useState(false) const [deleteConfirmOpen, setDeleteConfirmOpen] = useState(false) const [editModalOpen, setEditModalOpen] = useState(false) const { plan, enableBilling } = useProviderContext() const canUseWorkflowVersionAction = !enableBilling || plan.type !== Plan.sandbox const workflowStore = useWorkflowStore() const { handleRestoreFromPublishedWorkflow, handleLoadBackupDraft } = useWorkflowRun() const { handleRefreshWorkflowDraft } = useWorkflowRefreshDraft() const { handleExportDSL } = useDSL() const setShowWorkflowVersionHistoryPanel = useStore((s) => s.setShowWorkflowVersionHistoryPanel) const currentVersion = useStore((s) => s.currentVersion) const setCurrentVersion = useStore((s) => s.setCurrentVersion) const userProfile = useAtomValue(userProfileAtom) const configsMap = useHooksStore((s) => s.configsMap) const canImportExportDSL = useHooksStore((s) => s.accessControl.canImportExportDSL) const invalidAllLastRun = useInvalidAllLastRun(configsMap?.flowType, configsMap?.flowId) const { deleteAllInspectVars } = workflowStore.getState() const { t } = useTranslation() const { data: versionHistory, fetchNextPage, hasNextPage, isFetching, } = useWorkflowVersionHistory({ url: getVersionListUrl || '', initialPage: INITIAL_PAGE, limit: HISTORY_PER_PAGE, userId: filterValue === WorkflowVersionFilterOptions.onlyYours ? userProfile.id : '', namedOnly: isOnlyShowNamedVersions, }) const handleVersionClick = useCallback( (item: VersionHistory) => { if (item.id !== currentVersion?.id) { setCurrentVersion(item) if (item.version === WorkflowVersion.Draft) handleLoadBackupDraft() else handleRestoreFromPublishedWorkflow(item) } }, [ currentVersion?.id, setCurrentVersion, handleLoadBackupDraft, handleRestoreFromPublishedWorkflow, ], ) const handleNextPage = () => { if (hasNextPage) fetchNextPage() } const handleClose = () => { handleLoadBackupDraft() workflowStore.setState({ isRestoring: false }) setShowWorkflowVersionHistoryPanel(false) } const handleClickFilterItem = useCallback((value: WorkflowVersionFilterOptions) => { setFilterValue(value) }, []) const handleSwitch = useCallback((value: boolean) => { setIsOnlyShowNamedVersions(value) }, []) const handleResetFilter = useCallback(() => { setFilterValue(WorkflowVersionFilterOptions.all) setIsOnlyShowNamedVersions(false) }, []) const handleClickActionMenuItem = useCallback( (item: VersionHistory, operation: VersionHistoryContextMenuOptions) => { setOperatedItem(item) switch (operation) { case VersionHistoryContextMenuOptions.restore: if (!canUseWorkflowVersionAction) { setIsRestorePlanUpgradeModalOpen(true) break } setRestoreConfirmOpen(true) break case VersionHistoryContextMenuOptions.edit: setEditModalOpen(true) break case VersionHistoryContextMenuOptions.delete: setDeleteConfirmOpen(true) break case VersionHistoryContextMenuOptions.copyId: copy(item.id) toast.success(t(($) => $['versionHistory.action.copyIdSuccess'], { ns: 'workflow' })) break case VersionHistoryContextMenuOptions.exportDSL: if (!canUseWorkflowVersionAction) { setIsRestorePlanUpgradeModalOpen(true) break } if (!canImportExportDSL) return handleExportDSL?.(false, item.id) break } }, [canUseWorkflowVersionAction, canImportExportDSL, t, handleExportDSL], ) const handleCancel = useCallback((operation: VersionHistoryContextMenuOptions) => { switch (operation) { case VersionHistoryContextMenuOptions.restore: setRestoreConfirmOpen(false) break case VersionHistoryContextMenuOptions.edit: setEditModalOpen(false) break case VersionHistoryContextMenuOptions.delete: setDeleteConfirmOpen(false) break } }, []) const emitRestoreIntent = useCallback( async (item: VersionHistory) => { try { const { collaborationManager } = await import('../../collaboration/core/collaboration-manager') collaborationManager.emitRestoreIntent({ versionId: item.id, versionName: item.marked_name, initiatorUserId: userProfile.id, initiatorName: userProfile.name, }) } catch (error) { console.error('Failed to emit restore intent:', error) } }, [userProfile.id, userProfile.name], ) const emitRestoreComplete = useCallback( async (item: VersionHistory, success: boolean, errorMessage?: string) => { try { const { collaborationManager } = await import('../../collaboration/core/collaboration-manager') collaborationManager.emitRestoreComplete({ versionId: item.id, success, ...(errorMessage ? { error: errorMessage } : {}), }) } catch (error) { console.error('Failed to emit restore complete:', error) } }, [], ) const emitWorkflowUpdate = useCallback(async () => { try { const appId = configsMap?.flowId if (!appId) return const { collaborationManager } = await import('../../collaboration/core/collaboration-manager') collaborationManager.emitWorkflowUpdate(appId) } catch (error) { console.error('Failed to emit workflow update:', error) } }, [configsMap?.flowId]) const resetWorkflowVersionHistory = useResetWorkflowVersionHistory() const { mutateAsync: restoreWorkflow } = useRestoreWorkflow() const handleRestore = useCallback( async (item: VersionHistory) => { setShowWorkflowVersionHistoryPanel(false) await emitRestoreIntent(item) try { await restoreWorkflow(restoreVersionUrl(item.id)) setCurrentVersion(item) workflowStore.setState({ isRestoring: false }) workflowStore.setState({ backupDraft: undefined }) handleRefreshWorkflowDraft() toast.success(t(($) => $['versionHistory.action.restoreSuccess'], { ns: 'workflow' })) deleteAllInspectVars() invalidAllLastRun() await emitRestoreComplete(item, true) await emitWorkflowUpdate() } catch { toast.error(t(($) => $['versionHistory.action.restoreFailure'], { ns: 'workflow' })) await emitRestoreComplete(item, false, 'restore failed') } finally { resetWorkflowVersionHistory() } }, [ setShowWorkflowVersionHistoryPanel, emitRestoreIntent, restoreWorkflow, restoreVersionUrl, setCurrentVersion, workflowStore, handleRefreshWorkflowDraft, t, deleteAllInspectVars, invalidAllLastRun, emitRestoreComplete, emitWorkflowUpdate, resetWorkflowVersionHistory, ], ) const { mutateAsync: deleteWorkflow } = useDeleteWorkflow() const handleDelete = useCallback( async (id: string) => { await deleteWorkflow(deleteVersionUrl?.(id) || '', { onSuccess: () => { setDeleteConfirmOpen(false) toast.success(t(($) => $['versionHistory.action.deleteSuccess'], { ns: 'workflow' })) resetWorkflowVersionHistory() deleteAllInspectVars() invalidAllLastRun() }, onError: () => { toast.error(t(($) => $['versionHistory.action.deleteFailure'], { ns: 'workflow' })) }, onSettled: () => { setDeleteConfirmOpen(false) }, }) }, [ deleteWorkflow, t, resetWorkflowVersionHistory, deleteAllInspectVars, invalidAllLastRun, deleteVersionUrl, ], ) const { mutateAsync: updateWorkflow } = useUpdateWorkflow() const handleUpdateWorkflow = useCallback( async (params: { id?: string; title: string; releaseNotes: string }) => { const { id, ...rest } = params await updateWorkflow( { url: updateVersionUrl?.(id || '') || '', ...rest, }, { onSuccess: () => { setEditModalOpen(false) toast.success(t(($) => $['versionHistory.action.updateSuccess'], { ns: 'workflow' })) resetWorkflowVersionHistory() }, onError: () => { toast.error(t(($) => $['versionHistory.action.updateFailure'], { ns: 'workflow' })) }, onSettled: () => { setEditModalOpen(false) }, }, ) }, [t, updateWorkflow, resetWorkflowVersionHistory, updateVersionUrl], ) return (
{t(($) => $['versionHistory.title'], { ns: 'workflow' })}
{isFetching && !versionHistory?.pages?.length ? ( ) : ( <> {versionHistory?.pages?.map((page, pageNumber) => page.items?.map((item, idx) => { const isLast = pageNumber === versionHistory.pages.length - 1 && idx === page.items.length - 1 return ( ) }), )} {!isFetching && (!versionHistory?.pages?.length || !versionHistory.pages[0]!.items.length) && ( )} )}
{hasNextPage && (
{isFetching ? ( ) : ( )}
{t(($) => $['common.loadMore'], { ns: 'workflow' })}
)}
{restoreConfirmOpen && ( )} {isRestorePlanUpgradeModalOpen && ( setIsRestorePlanUpgradeModalOpen(false)} title={t(($) => $['upgrade.workflowRestore.title'], { ns: 'billing' })!} description={t(($) => $['upgrade.workflowRestore.description'], { ns: 'billing' })!} /> )} {deleteConfirmOpen && ( )} {editModalOpen && ( )}
) } export default React.memo(VersionHistoryPanel)