mirror of
https://github.com/langgenius/dify.git
synced 2026-05-09 04:36:31 +08:00
fix: restore workflow versions via backend API (#35817)
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
This commit is contained in:
parent
5c68f12bb8
commit
b83f296634
@ -3642,7 +3642,7 @@
|
||||
},
|
||||
"web/app/components/workflow/hooks/index.ts": {
|
||||
"no-barrel-files/no-barrel-files": {
|
||||
"count": 26
|
||||
"count": 25
|
||||
}
|
||||
},
|
||||
"web/app/components/workflow/hooks/use-checklist.ts": {
|
||||
|
||||
@ -27,6 +27,12 @@ const reactFlowBridge = vi.hoisted(() => ({
|
||||
const collaborationBridge = vi.hoisted(() => ({
|
||||
graphImportHandler: null as null | ((payload: { nodes: Node[], edges: Edge[] }) => void),
|
||||
historyActionHandler: null as null | ((payload: unknown) => void),
|
||||
restoreIntentHandler: null as null | ((payload: {
|
||||
versionId: string
|
||||
versionName?: string
|
||||
initiatorUserId: string
|
||||
initiatorName: string
|
||||
}) => void),
|
||||
}))
|
||||
|
||||
const toastInfoMock = vi.hoisted(() => vi.fn())
|
||||
@ -180,6 +186,10 @@ vi.mock('../collaboration/core/collaboration-manager', () => ({
|
||||
collaborationBridge.historyActionHandler = handler
|
||||
return vi.fn()
|
||||
},
|
||||
onRestoreIntent: (handler: typeof collaborationBridge.restoreIntentHandler) => {
|
||||
collaborationBridge.restoreIntentHandler = handler
|
||||
return vi.fn()
|
||||
},
|
||||
},
|
||||
}))
|
||||
|
||||
@ -401,7 +411,6 @@ vi.mock('../hooks', () => ({
|
||||
useWorkflowRefreshDraft: () => ({
|
||||
handleRefreshWorkflowDraft: vi.fn(),
|
||||
}),
|
||||
useLeaderRestoreListener: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock('../hooks/use-workflow-search', () => ({
|
||||
@ -479,6 +488,7 @@ describe('Workflow edge event wiring', () => {
|
||||
reactFlowBridge.store = null
|
||||
collaborationBridge.graphImportHandler = null
|
||||
collaborationBridge.historyActionHandler = null
|
||||
collaborationBridge.restoreIntentHandler = null
|
||||
workflowCommentState.comments = []
|
||||
workflowCommentState.pendingComment = null
|
||||
workflowCommentState.activeComment = null
|
||||
@ -626,6 +636,26 @@ describe('Workflow edge event wiring', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('should show restore intent toast when another collaborator restores a workflow version', async () => {
|
||||
renderSubject()
|
||||
|
||||
act(() => {
|
||||
collaborationBridge.restoreIntentHandler?.({
|
||||
versionId: 'version-1',
|
||||
versionName: 'Version One',
|
||||
initiatorUserId: 'user-1',
|
||||
initiatorName: 'Alice',
|
||||
})
|
||||
})
|
||||
|
||||
await waitFor(() => {
|
||||
expect(toastInfoMock).toHaveBeenCalledWith(
|
||||
'workflow.versionHistory.action.restoreInProgress:{"userName":"Alice","versionName":"Version One"}',
|
||||
{ timeout: 3000 },
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
it('should render comment overlays and execute comment actions in comment mode', async () => {
|
||||
workflowCommentState.comments = [
|
||||
{ id: 'comment-1', resolved: false },
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import type { LoroMap } from 'loro-crdt'
|
||||
import type { OnlineUser, RestoreRequestData } from '../../types/collaboration'
|
||||
import type { OnlineUser } from '../../types/collaboration'
|
||||
import type { NoteNodeType } from '@/app/components/workflow/note-node/types'
|
||||
import type { Edge, Node } from '@/app/components/workflow/types'
|
||||
import { LoroDoc } from 'loro-crdt'
|
||||
@ -67,18 +67,6 @@ const createEdge = (id: string, source: string, target: string): Edge => ({
|
||||
},
|
||||
})
|
||||
|
||||
const createRestoreRequestData = (): RestoreRequestData => ({
|
||||
versionId: 'version-1',
|
||||
versionName: 'Version One',
|
||||
initiatorUserId: 'user-1',
|
||||
initiatorName: 'Alice',
|
||||
graphData: {
|
||||
nodes: [createNode('n-restore')],
|
||||
edges: [],
|
||||
viewport: { x: 1, y: 2, zoom: 0.5 },
|
||||
},
|
||||
})
|
||||
|
||||
const setupManagerWithDoc = () => {
|
||||
const manager = new CollaborationManager()
|
||||
const doc = new LoroDoc()
|
||||
@ -275,7 +263,6 @@ describe('CollaborationManager logs and event helpers', () => {
|
||||
|
||||
manager.emitCommentsUpdate('app-1')
|
||||
manager.emitHistoryAction('undo')
|
||||
manager.emitRestoreRequest(createRestoreRequestData())
|
||||
manager.emitRestoreIntent({
|
||||
versionId: 'version-1',
|
||||
versionName: 'Version One',
|
||||
@ -289,13 +276,11 @@ describe('CollaborationManager logs and event helpers', () => {
|
||||
|
||||
manager.emitCommentsUpdate('app-1')
|
||||
manager.emitHistoryAction('undo')
|
||||
manager.emitRestoreRequest(createRestoreRequestData())
|
||||
expect(sendSpy).not.toHaveBeenCalled()
|
||||
|
||||
isConnectedSpy.mockReturnValue(true)
|
||||
manager.emitCommentsUpdate('app-1')
|
||||
manager.emitHistoryAction('redo')
|
||||
manager.emitRestoreRequest(createRestoreRequestData())
|
||||
manager.emitRestoreIntent({
|
||||
versionId: 'version-2',
|
||||
initiatorUserId: 'u-2',
|
||||
@ -309,7 +294,6 @@ describe('CollaborationManager logs and event helpers', () => {
|
||||
expect(eventTypes).toEqual([
|
||||
'comments_update',
|
||||
'workflow_history_action',
|
||||
'workflow_restore_request',
|
||||
'workflow_restore_intent',
|
||||
'workflow_restore_complete',
|
||||
])
|
||||
|
||||
@ -203,7 +203,6 @@ describe('CollaborationManager socket and subscription behavior', () => {
|
||||
const mcpHandler = vi.fn()
|
||||
const workflowUpdateHandler = vi.fn()
|
||||
const commentsHandler = vi.fn()
|
||||
const restoreRequestHandler = vi.fn()
|
||||
const restoreIntentHandler = vi.fn()
|
||||
const restoreCompleteHandler = vi.fn()
|
||||
const historyHandler = vi.fn()
|
||||
@ -218,7 +217,6 @@ describe('CollaborationManager socket and subscription behavior', () => {
|
||||
manager.onMcpServerUpdate(mcpHandler)
|
||||
manager.onWorkflowUpdate(workflowUpdateHandler)
|
||||
manager.onCommentsUpdate(commentsHandler)
|
||||
manager.onRestoreRequest(restoreRequestHandler)
|
||||
manager.onRestoreIntent(restoreIntentHandler)
|
||||
manager.onRestoreComplete(restoreCompleteHandler)
|
||||
manager.onHistoryAction(historyHandler)
|
||||
@ -296,11 +294,6 @@ describe('CollaborationManager socket and subscription behavior', () => {
|
||||
type: 'graph_resync_request',
|
||||
data: {},
|
||||
} satisfies CollaborationUpdate)
|
||||
socket.trigger('collaboration_update', {
|
||||
...baseUpdate,
|
||||
type: 'workflow_restore_request',
|
||||
data: { versionId: 'v1', initiatorUserId: 'u-1', initiatorName: 'Alice', graphData: { nodes: [], edges: [] } } as unknown as Record<string, unknown>,
|
||||
} satisfies CollaborationUpdate)
|
||||
socket.trigger('collaboration_update', {
|
||||
...baseUpdate,
|
||||
type: 'workflow_restore_intent',
|
||||
@ -335,7 +328,6 @@ describe('CollaborationManager socket and subscription behavior', () => {
|
||||
expect(latestPresence).toMatchObject({ 'n-1': { 'socket-events': { userId: 'u-1' } } })
|
||||
expect(syncRequestHandler).toHaveBeenCalledTimes(1)
|
||||
expect(broadcastSpy).toHaveBeenCalledTimes(1)
|
||||
expect(restoreRequestHandler).toHaveBeenCalledTimes(1)
|
||||
expect(restoreIntentHandler).toHaveBeenCalledWith({ versionId: 'v1', initiatorUserId: 'u-1', initiatorName: 'Alice' } satisfies RestoreIntentData)
|
||||
expect(restoreCompleteHandler).toHaveBeenCalledWith({ versionId: 'v1', success: true } satisfies RestoreCompleteData)
|
||||
expect(historyHandler).toHaveBeenCalledWith({ action: 'undo', userId: 'u-1' })
|
||||
|
||||
@ -16,7 +16,6 @@ import type {
|
||||
OnlineUser,
|
||||
RestoreCompleteData,
|
||||
RestoreIntentData,
|
||||
RestoreRequestData,
|
||||
} from '../types/collaboration'
|
||||
import { cloneDeep } from 'es-toolkit/object'
|
||||
import { isEqual } from 'es-toolkit/predicate'
|
||||
@ -770,17 +769,6 @@ export class CollaborationManager {
|
||||
return this.eventEmitter.on('historyAction', callback)
|
||||
}
|
||||
|
||||
emitRestoreRequest(data: RestoreRequestData): void {
|
||||
if (!this.currentAppId || !webSocketClient.isConnected(this.currentAppId))
|
||||
return
|
||||
|
||||
this.sendCollaborationEvent({
|
||||
type: 'workflow_restore_request',
|
||||
data: data as unknown as Record<string, unknown>,
|
||||
timestamp: Date.now(),
|
||||
})
|
||||
}
|
||||
|
||||
emitRestoreIntent(data: RestoreIntentData): void {
|
||||
if (!this.currentAppId || !webSocketClient.isConnected(this.currentAppId))
|
||||
return
|
||||
@ -803,10 +791,6 @@ export class CollaborationManager {
|
||||
})
|
||||
}
|
||||
|
||||
onRestoreRequest(callback: (data: RestoreRequestData) => void): () => void {
|
||||
return this.eventEmitter.on('restoreRequest', callback)
|
||||
}
|
||||
|
||||
onRestoreIntent(callback: (data: RestoreIntentData) => void): () => void {
|
||||
return this.eventEmitter.on('restoreIntent', callback)
|
||||
}
|
||||
@ -1476,10 +1460,6 @@ export class CollaborationManager {
|
||||
if (this.isLeader)
|
||||
this.broadcastCurrentGraph()
|
||||
}
|
||||
else if (update.type === 'workflow_restore_request') {
|
||||
if (this.isLeader)
|
||||
this.eventEmitter.emit('restoreRequest', update.data as RestoreRequestData)
|
||||
}
|
||||
else if (update.type === 'workflow_restore_intent') {
|
||||
this.eventEmitter.emit('restoreIntent', update.data as RestoreIntentData)
|
||||
}
|
||||
|
||||
@ -1,7 +1,3 @@
|
||||
import type { Viewport } from 'reactflow'
|
||||
import type { ConversationVariable, Edge, EnvironmentVariable, Node } from '../../types'
|
||||
import type { Features } from '@/app/components/base/features/types'
|
||||
|
||||
export type OnlineUser = {
|
||||
user_id: string
|
||||
username: string
|
||||
@ -51,7 +47,6 @@ type CollaborationEventType
|
||||
| 'node_panel_presence'
|
||||
| 'app_publish_update'
|
||||
| 'graph_resync_request'
|
||||
| 'workflow_restore_request'
|
||||
| 'workflow_restore_intent'
|
||||
| 'workflow_restore_complete'
|
||||
| 'workflow_history_action'
|
||||
@ -63,21 +58,6 @@ export type CollaborationUpdate = {
|
||||
timestamp: number
|
||||
}
|
||||
|
||||
export type RestoreRequestData = {
|
||||
versionId: string
|
||||
versionName?: string
|
||||
initiatorUserId: string
|
||||
initiatorName: string
|
||||
graphData: {
|
||||
nodes: Node[]
|
||||
edges: Edge[]
|
||||
viewport?: Viewport
|
||||
}
|
||||
features?: Features
|
||||
environmentVariables?: EnvironmentVariable[]
|
||||
conversationVariables?: ConversationVariable[]
|
||||
}
|
||||
|
||||
export type RestoreIntentData = {
|
||||
versionId: string
|
||||
versionName?: string
|
||||
|
||||
@ -7,9 +7,9 @@ import HeaderInRestoring from '../header-in-restoring'
|
||||
|
||||
const mockRestoreWorkflow = vi.fn()
|
||||
const mockInvalidAllLastRun = vi.fn()
|
||||
const mockResetWorkflowVersionHistory = vi.fn()
|
||||
const mockHandleLoadBackupDraft = vi.fn()
|
||||
const mockHandleRefreshWorkflowDraft = vi.fn()
|
||||
const mockRequestRestore = vi.fn()
|
||||
|
||||
vi.mock('@/hooks/use-theme', () => ({
|
||||
default: () => ({
|
||||
@ -31,6 +31,7 @@ vi.mock('@/hooks/use-format-time-from-now', () => ({
|
||||
|
||||
vi.mock('@/service/use-workflow', () => ({
|
||||
useInvalidAllLastRun: () => mockInvalidAllLastRun,
|
||||
useResetWorkflowVersionHistory: () => mockResetWorkflowVersionHistory,
|
||||
useRestoreWorkflow: () => ({
|
||||
mutateAsync: mockRestoreWorkflow,
|
||||
}),
|
||||
@ -43,9 +44,6 @@ vi.mock('../../hooks', () => ({
|
||||
useWorkflowRefreshDraft: () => ({
|
||||
handleRefreshWorkflowDraft: mockHandleRefreshWorkflowDraft,
|
||||
}),
|
||||
useLeaderRestore: () => ({
|
||||
requestRestore: mockRequestRestore,
|
||||
}),
|
||||
}))
|
||||
|
||||
const createVersion = (overrides: Partial<VersionHistory> = {}): VersionHistory => ({
|
||||
@ -92,7 +90,7 @@ describe('HeaderInRestoring', () => {
|
||||
expect(screen.getByRole('button', { name: 'workflow.common.restore' })).toBeDisabled()
|
||||
})
|
||||
|
||||
it('should enable restore when version and flow config are both ready', () => {
|
||||
it('should enable restore when version and flow id are both ready', () => {
|
||||
renderWorkflowComponent(<HeaderInRestoring />, {
|
||||
initialStoreState: {
|
||||
currentVersion: createVersion(),
|
||||
@ -100,7 +98,7 @@ describe('HeaderInRestoring', () => {
|
||||
hooksStoreProps: {
|
||||
configsMap: {
|
||||
flowId: 'app-1',
|
||||
flowType: FlowType.appFlow,
|
||||
flowType: undefined as never,
|
||||
fileSettings: {} as never,
|
||||
},
|
||||
},
|
||||
|
||||
@ -14,7 +14,11 @@ const mockHandleNodeSelect = vi.fn()
|
||||
const mockHandleRefreshWorkflowDraft = vi.fn()
|
||||
const mockCloseAllInputFieldPanels = vi.fn()
|
||||
const mockInvalidAllLastRun = vi.fn()
|
||||
const mockRequestRestore = vi.fn()
|
||||
const mockRestoreWorkflow = vi.fn()
|
||||
const mockResetWorkflowVersionHistory = vi.fn()
|
||||
const mockEmitRestoreIntent = vi.fn()
|
||||
const mockEmitRestoreComplete = vi.fn()
|
||||
const mockEmitWorkflowUpdate = vi.fn()
|
||||
const mockNotify = vi.fn()
|
||||
const mockRunAndHistory = vi.fn()
|
||||
const mockViewHistory = vi.fn()
|
||||
@ -33,9 +37,6 @@ vi.mock('../../hooks', () => ({
|
||||
handleBackupDraft: mockHandleBackupDraft,
|
||||
handleLoadBackupDraft: mockHandleLoadBackupDraft,
|
||||
}),
|
||||
useLeaderRestore: () => ({
|
||||
requestRestore: mockRequestRestore,
|
||||
}),
|
||||
useNodesSyncDraft: () => ({
|
||||
handleSyncWorkflowDraft: vi.fn(),
|
||||
}),
|
||||
@ -58,6 +59,18 @@ vi.mock('@/hooks/use-theme', () => ({
|
||||
|
||||
vi.mock('@/service/use-workflow', () => ({
|
||||
useInvalidAllLastRun: () => mockInvalidAllLastRun,
|
||||
useResetWorkflowVersionHistory: () => mockResetWorkflowVersionHistory,
|
||||
useRestoreWorkflow: () => ({
|
||||
mutateAsync: mockRestoreWorkflow,
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('../../collaboration/core/collaboration-manager', () => ({
|
||||
collaborationManager: {
|
||||
emitRestoreIntent: mockEmitRestoreIntent,
|
||||
emitRestoreComplete: mockEmitRestoreComplete,
|
||||
emitWorkflowUpdate: mockEmitWorkflowUpdate,
|
||||
},
|
||||
}))
|
||||
|
||||
vi.mock('@langgenius/dify-ui/toast', () => ({
|
||||
@ -166,13 +179,7 @@ describe('Header layout components', () => {
|
||||
mockNodesReadOnly = false
|
||||
mockTheme = 'light'
|
||||
mockUseNodes.mockReturnValue([])
|
||||
mockRequestRestore.mockImplementation((_payload: unknown, callbacks?: {
|
||||
onSuccess?: () => void
|
||||
onSettled?: () => void
|
||||
}) => {
|
||||
callbacks?.onSuccess?.()
|
||||
callbacks?.onSettled?.()
|
||||
})
|
||||
mockRestoreWorkflow.mockResolvedValue({})
|
||||
})
|
||||
|
||||
describe('HeaderInNormal', () => {
|
||||
@ -277,7 +284,7 @@ describe('Header layout components', () => {
|
||||
fireEvent.click(screen.getByRole('button', { name: 'workflow.common.restore' }))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockRequestRestore).toHaveBeenCalledTimes(1)
|
||||
expect(mockRestoreWorkflow).toHaveBeenCalledWith('/apps/flow-1/workflows/version-1/restore')
|
||||
expect(store.getState().showWorkflowVersionHistoryPanel).toBe(false)
|
||||
expect(store.getState().isRestoring).toBe(false)
|
||||
expect(store.getState().backupDraft).toBeUndefined()
|
||||
@ -289,8 +296,53 @@ describe('Header layout components', () => {
|
||||
message: 'workflow.versionHistory.action.restoreSuccess',
|
||||
})
|
||||
})
|
||||
expect(mockEmitRestoreIntent).toHaveBeenCalledWith({
|
||||
versionId: currentVersion.id,
|
||||
versionName: currentVersion.marked_name,
|
||||
initiatorUserId: '',
|
||||
initiatorName: '',
|
||||
})
|
||||
expect(mockEmitRestoreComplete).toHaveBeenCalledWith({
|
||||
versionId: currentVersion.id,
|
||||
success: true,
|
||||
})
|
||||
expect(mockEmitWorkflowUpdate).toHaveBeenCalledWith('flow-1')
|
||||
expect(mockResetWorkflowVersionHistory).toHaveBeenCalledTimes(1)
|
||||
expect(onRestoreSettled).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('should restore rag pipeline versions without emitting collaboration events', async () => {
|
||||
const currentVersion = createCurrentVersion()
|
||||
|
||||
renderWorkflowComponent(
|
||||
<HeaderInRestoring />,
|
||||
{
|
||||
initialStoreState: {
|
||||
isRestoring: true,
|
||||
showWorkflowVersionHistoryPanel: true,
|
||||
backupDraft: createBackupDraft(),
|
||||
currentVersion,
|
||||
},
|
||||
hooksStoreProps: {
|
||||
configsMap: {
|
||||
flowType: FlowType.ragPipeline,
|
||||
flowId: 'pipeline-1',
|
||||
fileSettings: {},
|
||||
},
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'workflow.common.restore' }))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockRestoreWorkflow).toHaveBeenCalledWith('/rag/pipelines/pipeline-1/workflows/version-1/restore')
|
||||
expect(mockHandleRefreshWorkflowDraft).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
expect(mockEmitRestoreIntent).not.toHaveBeenCalled()
|
||||
expect(mockEmitRestoreComplete).not.toHaveBeenCalled()
|
||||
expect(mockEmitWorkflowUpdate).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
describe('HeaderInHistory', () => {
|
||||
|
||||
@ -6,12 +6,11 @@ import {
|
||||
useCallback,
|
||||
} from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useFeaturesStore } from '@/app/components/base/features/hooks'
|
||||
import { useSelector as useAppContextSelector } from '@/context/app-context'
|
||||
import useTheme from '@/hooks/use-theme'
|
||||
import { useInvalidAllLastRun } from '@/service/use-workflow'
|
||||
import { useInvalidAllLastRun, useResetWorkflowVersionHistory, useRestoreWorkflow } from '@/service/use-workflow'
|
||||
import { FlowType } from '@/types/common'
|
||||
import {
|
||||
useLeaderRestore,
|
||||
useWorkflowRefreshDraft,
|
||||
useWorkflowRun,
|
||||
} from '../hooks'
|
||||
@ -35,7 +34,6 @@ const HeaderInRestoring = ({
|
||||
const { theme } = useTheme()
|
||||
const workflowStore = useWorkflowStore()
|
||||
const userProfile = useAppContextSelector(s => s.userProfile)
|
||||
const featuresStore = useFeaturesStore()
|
||||
const configsMap = useHooksStore(s => s.configsMap)
|
||||
const invalidAllLastRun = useInvalidAllLastRun(configsMap?.flowType, configsMap?.flowId)
|
||||
const {
|
||||
@ -47,9 +45,11 @@ const HeaderInRestoring = ({
|
||||
const {
|
||||
handleLoadBackupDraft,
|
||||
} = useWorkflowRun()
|
||||
const { requestRestore } = useLeaderRestore()
|
||||
const { handleRefreshWorkflowDraft } = useWorkflowRefreshDraft()
|
||||
const { mutateAsync: restoreWorkflow } = useRestoreWorkflow()
|
||||
const resetWorkflowVersionHistory = useResetWorkflowVersionHistory()
|
||||
const canRestore = !!currentVersion?.id && !!configsMap?.flowId && currentVersion.version !== WorkflowVersion.Draft
|
||||
const canEmitCollaborationEvents = configsMap?.flowType === FlowType.appFlow
|
||||
|
||||
const handleCancelRestore = useCallback(() => {
|
||||
handleLoadBackupDraft()
|
||||
@ -57,47 +57,86 @@ const HeaderInRestoring = ({
|
||||
setShowWorkflowVersionHistoryPanel(false)
|
||||
}, [workflowStore, handleLoadBackupDraft, setShowWorkflowVersionHistoryPanel])
|
||||
|
||||
const handleRestore = useCallback(() => {
|
||||
const restoreVersionUrl = useCallback((versionId: string) => {
|
||||
if (!configsMap?.flowId)
|
||||
return ''
|
||||
if (configsMap.flowType === FlowType.ragPipeline)
|
||||
return `/rag/pipelines/${configsMap.flowId}/workflows/${versionId}/restore`
|
||||
return `/apps/${configsMap.flowId}/workflows/${versionId}/restore`
|
||||
}, [configsMap?.flowId, configsMap?.flowType])
|
||||
|
||||
const emitRestoreIntent = useCallback(async () => {
|
||||
if (!currentVersion || !canEmitCollaborationEvents)
|
||||
return
|
||||
try {
|
||||
const { collaborationManager } = await import('../collaboration/core/collaboration-manager')
|
||||
collaborationManager.emitRestoreIntent({
|
||||
versionId: currentVersion.id,
|
||||
versionName: currentVersion.marked_name,
|
||||
initiatorUserId: userProfile.id,
|
||||
initiatorName: userProfile.name,
|
||||
})
|
||||
}
|
||||
catch (error) {
|
||||
console.error('Failed to emit restore intent:', error)
|
||||
}
|
||||
}, [canEmitCollaborationEvents, currentVersion, userProfile.id, userProfile.name])
|
||||
|
||||
const emitRestoreComplete = useCallback(async (success: boolean, errorMessage?: string) => {
|
||||
if (!currentVersion || !canEmitCollaborationEvents)
|
||||
return
|
||||
try {
|
||||
const { collaborationManager } = await import('../collaboration/core/collaboration-manager')
|
||||
collaborationManager.emitRestoreComplete({
|
||||
versionId: currentVersion.id,
|
||||
success,
|
||||
...(errorMessage ? { error: errorMessage } : {}),
|
||||
})
|
||||
}
|
||||
catch (error) {
|
||||
console.error('Failed to emit restore complete:', error)
|
||||
}
|
||||
}, [canEmitCollaborationEvents, currentVersion])
|
||||
|
||||
const emitWorkflowUpdate = useCallback(async () => {
|
||||
if (!configsMap?.flowId || !canEmitCollaborationEvents)
|
||||
return
|
||||
try {
|
||||
const { collaborationManager } = await import('../collaboration/core/collaboration-manager')
|
||||
collaborationManager.emitWorkflowUpdate(configsMap.flowId)
|
||||
}
|
||||
catch (error) {
|
||||
console.error('Failed to emit workflow update:', error)
|
||||
}
|
||||
}, [canEmitCollaborationEvents, configsMap?.flowId])
|
||||
|
||||
const handleRestore = useCallback(async () => {
|
||||
if (!canRestore || !currentVersion)
|
||||
return
|
||||
|
||||
setShowWorkflowVersionHistoryPanel(false)
|
||||
workflowStore.setState({ isRestoring: false })
|
||||
workflowStore.setState({ backupDraft: undefined })
|
||||
await emitRestoreIntent()
|
||||
|
||||
const { graph } = currentVersion
|
||||
const features = featuresStore?.getState().features
|
||||
const environmentVariables = currentVersion.environment_variables || []
|
||||
const conversationVariables = currentVersion.conversation_variables || []
|
||||
|
||||
requestRestore({
|
||||
versionId: currentVersion.id,
|
||||
versionName: currentVersion.marked_name,
|
||||
initiatorUserId: userProfile.id,
|
||||
initiatorName: userProfile.name,
|
||||
graphData: {
|
||||
nodes: graph.nodes,
|
||||
edges: graph.edges,
|
||||
viewport: graph.viewport,
|
||||
},
|
||||
features,
|
||||
environmentVariables,
|
||||
conversationVariables,
|
||||
}, {
|
||||
onSuccess: () => {
|
||||
handleRefreshWorkflowDraft()
|
||||
toast.success(t('versionHistory.action.restoreSuccess', { ns: 'workflow' }))
|
||||
deleteAllInspectVars()
|
||||
invalidAllLastRun()
|
||||
},
|
||||
onError: () => {
|
||||
toast.error(t('versionHistory.action.restoreFailure', { ns: 'workflow' }))
|
||||
},
|
||||
onSettled: () => {
|
||||
onRestoreSettled?.()
|
||||
},
|
||||
})
|
||||
}, [canRestore, currentVersion, setShowWorkflowVersionHistoryPanel, workflowStore, featuresStore, requestRestore, userProfile, handleRefreshWorkflowDraft, deleteAllInspectVars, invalidAllLastRun, t, onRestoreSettled])
|
||||
try {
|
||||
await restoreWorkflow(restoreVersionUrl(currentVersion.id))
|
||||
workflowStore.setState({ isRestoring: false })
|
||||
workflowStore.setState({ backupDraft: undefined })
|
||||
handleRefreshWorkflowDraft()
|
||||
toast.success(t('versionHistory.action.restoreSuccess', { ns: 'workflow' }))
|
||||
deleteAllInspectVars()
|
||||
invalidAllLastRun()
|
||||
await emitRestoreComplete(true)
|
||||
await emitWorkflowUpdate()
|
||||
}
|
||||
catch {
|
||||
toast.error(t('versionHistory.action.restoreFailure', { ns: 'workflow' }))
|
||||
await emitRestoreComplete(false, 'restore failed')
|
||||
}
|
||||
finally {
|
||||
resetWorkflowVersionHistory()
|
||||
onRestoreSettled?.()
|
||||
}
|
||||
}, [canRestore, currentVersion, setShowWorkflowVersionHistoryPanel, emitRestoreIntent, restoreWorkflow, restoreVersionUrl, workflowStore, handleRefreshWorkflowDraft, t, deleteAllInspectVars, invalidAllLastRun, emitRestoreComplete, emitWorkflowUpdate, resetWorkflowVersionHistory, onRestoreSettled])
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
@ -1,262 +0,0 @@
|
||||
import type { RestoreIntentData, RestoreRequestData } from '../../collaboration/types/collaboration'
|
||||
import type { SyncDraftCallback } from '../../hooks-store/store'
|
||||
import type { Edge, Node } from '../../types'
|
||||
import { act, renderHook } from '@testing-library/react'
|
||||
import { renderHookWithSystemFeatures } from '@/__tests__/utils/mock-system-features'
|
||||
import { ChatVarType } from '../../panel/chat-variable-panel/type'
|
||||
import { useLeaderRestore, useLeaderRestoreListener } from '../use-leader-restore'
|
||||
|
||||
const mockSetViewport = vi.hoisted(() => vi.fn())
|
||||
const mockSetFeatures = vi.hoisted(() => vi.fn())
|
||||
const mockSetEnvironmentVariables = vi.hoisted(() => vi.fn())
|
||||
const mockSetConversationVariables = vi.hoisted(() => vi.fn())
|
||||
const mockDoSyncWorkflowDraft = vi.hoisted(() => vi.fn())
|
||||
const mockToastInfo = vi.hoisted(() => vi.fn())
|
||||
|
||||
const mockEmitRestoreIntent = vi.hoisted(() => vi.fn())
|
||||
const mockEmitRestoreComplete = vi.hoisted(() => vi.fn())
|
||||
const mockEmitWorkflowUpdate = vi.hoisted(() => vi.fn())
|
||||
const mockEmitRestoreRequest = vi.hoisted(() => vi.fn())
|
||||
const mockIsConnected = vi.hoisted(() => vi.fn())
|
||||
const mockGetIsLeader = vi.hoisted(() => vi.fn())
|
||||
const mockSetNodes = vi.hoisted(() => vi.fn())
|
||||
const mockSetEdges = vi.hoisted(() => vi.fn())
|
||||
const mockRefreshGraphSynchronously = vi.hoisted(() => vi.fn())
|
||||
const mockGetNodes = vi.hoisted(() => vi.fn(() => [{ id: 'old-node' } as unknown as Node]))
|
||||
const mockGetEdges = vi.hoisted(() => vi.fn(() => [{ id: 'old-edge' } as unknown as Edge]))
|
||||
|
||||
let restoreCompleteCallback: ((data: { versionId: string, success: boolean }) => void) | null = null
|
||||
let restoreRequestCallback: ((data: RestoreRequestData) => void) | null = null
|
||||
let restoreIntentCallback: ((data: RestoreIntentData) => void) | null = null
|
||||
|
||||
const unsubscribeRestoreComplete = vi.hoisted(() => vi.fn())
|
||||
const unsubscribeRestoreRequest = vi.hoisted(() => vi.fn())
|
||||
const unsubscribeRestoreIntent = vi.hoisted(() => vi.fn())
|
||||
|
||||
let isCollaborationEnabled = true
|
||||
|
||||
vi.mock('react-i18next', () => ({
|
||||
useTranslation: () => ({
|
||||
t: (key: string, options?: { ns?: string, userName?: string, versionName?: string }) => {
|
||||
const ns = options?.ns ? `${options.ns}.` : ''
|
||||
const extra = options?.userName ? `:${options.userName}:${options.versionName}` : ''
|
||||
return `${ns}${key}${extra}`
|
||||
},
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('reactflow', () => ({
|
||||
useReactFlow: () => ({
|
||||
setViewport: mockSetViewport,
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/app/store', () => ({
|
||||
useStore: {
|
||||
getState: () => ({
|
||||
appDetail: { id: 'app-1' },
|
||||
}),
|
||||
},
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/base/features/hooks', () => ({
|
||||
useFeaturesStore: () => ({
|
||||
getState: () => ({
|
||||
setFeatures: mockSetFeatures,
|
||||
}),
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('@langgenius/dify-ui/toast', () => ({
|
||||
toast: {
|
||||
info: (...args: unknown[]) => mockToastInfo(...args),
|
||||
},
|
||||
}))
|
||||
|
||||
vi.mock('../../store', () => ({
|
||||
useWorkflowStore: () => ({
|
||||
getState: () => ({
|
||||
setEnvironmentVariables: mockSetEnvironmentVariables,
|
||||
setConversationVariables: mockSetConversationVariables,
|
||||
}),
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('../use-nodes-sync-draft', () => ({
|
||||
useNodesSyncDraft: () => ({
|
||||
doSyncWorkflowDraft: (...args: unknown[]) => mockDoSyncWorkflowDraft(...args),
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('../../collaboration/core/collaboration-manager', () => ({
|
||||
collaborationManager: {
|
||||
emitRestoreIntent: (...args: unknown[]) => mockEmitRestoreIntent(...args),
|
||||
emitRestoreComplete: (...args: unknown[]) => mockEmitRestoreComplete(...args),
|
||||
emitWorkflowUpdate: (...args: unknown[]) => mockEmitWorkflowUpdate(...args),
|
||||
emitRestoreRequest: (...args: unknown[]) => mockEmitRestoreRequest(...args),
|
||||
isConnected: (...args: unknown[]) => mockIsConnected(...args),
|
||||
getIsLeader: (...args: unknown[]) => mockGetIsLeader(...args),
|
||||
setNodes: (...args: unknown[]) => mockSetNodes(...args),
|
||||
setEdges: (...args: unknown[]) => mockSetEdges(...args),
|
||||
refreshGraphSynchronously: (...args: unknown[]) => mockRefreshGraphSynchronously(...args),
|
||||
getNodes: () => mockGetNodes(),
|
||||
getEdges: () => mockGetEdges(),
|
||||
onRestoreComplete: (callback: (data: { versionId: string, success: boolean }) => void) => {
|
||||
restoreCompleteCallback = callback
|
||||
return unsubscribeRestoreComplete
|
||||
},
|
||||
onRestoreRequest: (callback: (data: RestoreRequestData) => void) => {
|
||||
restoreRequestCallback = callback
|
||||
return unsubscribeRestoreRequest
|
||||
},
|
||||
onRestoreIntent: (callback: (data: RestoreIntentData) => void) => {
|
||||
restoreIntentCallback = callback
|
||||
return unsubscribeRestoreIntent
|
||||
},
|
||||
},
|
||||
}))
|
||||
|
||||
describe('useLeaderRestore', () => {
|
||||
const restoreData: RestoreRequestData = {
|
||||
versionId: 'v-1',
|
||||
versionName: 'Version One',
|
||||
initiatorUserId: 'u-1',
|
||||
initiatorName: 'Alice',
|
||||
features: { moreLikeThis: { enabled: true } },
|
||||
environmentVariables: [{ id: 'env-1', name: 'A', value: '1', value_type: ChatVarType.String, description: '' }],
|
||||
conversationVariables: [{ id: 'conv-1', name: 'B', value: '2', value_type: ChatVarType.String, description: '' }],
|
||||
graphData: {
|
||||
nodes: [{ id: 'new-node' } as unknown as Node],
|
||||
edges: [{ id: 'new-edge' } as unknown as Edge],
|
||||
viewport: { x: 1, y: 2, zoom: 0.5 },
|
||||
},
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
restoreCompleteCallback = null
|
||||
restoreRequestCallback = null
|
||||
restoreIntentCallback = null
|
||||
isCollaborationEnabled = true
|
||||
mockIsConnected.mockReturnValue(true)
|
||||
mockGetIsLeader.mockReturnValue(false)
|
||||
mockDoSyncWorkflowDraft.mockImplementation((_sync: boolean, callbacks?: SyncDraftCallback) => {
|
||||
callbacks?.onSuccess?.()
|
||||
callbacks?.onSettled?.()
|
||||
})
|
||||
})
|
||||
|
||||
it('performs restore locally when collaboration is disabled', async () => {
|
||||
isCollaborationEnabled = false
|
||||
const onSuccess = vi.fn()
|
||||
const onSettled = vi.fn()
|
||||
|
||||
const { result } = renderHookWithSystemFeatures(() => useLeaderRestore(), {
|
||||
systemFeatures: { enable_collaboration_mode: isCollaborationEnabled },
|
||||
})
|
||||
|
||||
await act(async () => {
|
||||
result.current.requestRestore(restoreData, { onSuccess, onSettled })
|
||||
})
|
||||
|
||||
expect(mockEmitRestoreIntent).toHaveBeenCalledWith(expect.objectContaining({
|
||||
versionId: 'v-1',
|
||||
initiatorName: 'Alice',
|
||||
}))
|
||||
expect(mockSetFeatures).toHaveBeenCalledWith({ moreLikeThis: { enabled: true } })
|
||||
expect(mockSetEnvironmentVariables).toHaveBeenCalled()
|
||||
expect(mockSetConversationVariables).toHaveBeenCalled()
|
||||
expect(mockSetNodes).toHaveBeenCalledWith([{ id: 'old-node' }], [{ id: 'new-node' }], 'leader-restore:apply-graph')
|
||||
expect(mockSetEdges).toHaveBeenCalledWith([{ id: 'old-edge' }], [{ id: 'new-edge' }])
|
||||
expect(mockRefreshGraphSynchronously).toHaveBeenCalled()
|
||||
expect(mockSetViewport).toHaveBeenCalledWith({ x: 1, y: 2, zoom: 0.5 })
|
||||
expect(mockEmitRestoreComplete).toHaveBeenCalledWith({ versionId: 'v-1', success: true })
|
||||
expect(mockEmitWorkflowUpdate).toHaveBeenCalledWith('app-1')
|
||||
expect(onSuccess).toHaveBeenCalled()
|
||||
expect(onSettled).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('emits restore request and resolves callbacks from restore-complete events', async () => {
|
||||
isCollaborationEnabled = true
|
||||
mockIsConnected.mockReturnValue(true)
|
||||
mockGetIsLeader.mockReturnValue(false)
|
||||
const onSuccess = vi.fn()
|
||||
const onError = vi.fn()
|
||||
const onSettled = vi.fn()
|
||||
|
||||
const { result } = renderHookWithSystemFeatures(() => useLeaderRestore(), {
|
||||
systemFeatures: { enable_collaboration_mode: isCollaborationEnabled },
|
||||
})
|
||||
|
||||
act(() => {
|
||||
result.current.requestRestore(restoreData, { onSuccess, onError, onSettled })
|
||||
})
|
||||
|
||||
expect(mockEmitRestoreRequest).toHaveBeenCalledWith(restoreData)
|
||||
expect(mockDoSyncWorkflowDraft).not.toHaveBeenCalled()
|
||||
|
||||
act(() => {
|
||||
restoreCompleteCallback?.({ versionId: 'v-1', success: true })
|
||||
})
|
||||
expect(onSuccess).toHaveBeenCalledTimes(1)
|
||||
expect(onSettled).toHaveBeenCalledTimes(1)
|
||||
|
||||
act(() => {
|
||||
result.current.requestRestore(restoreData, { onSuccess, onError, onSettled })
|
||||
restoreCompleteCallback?.({ versionId: 'v-1', success: false })
|
||||
})
|
||||
expect(onError).toHaveBeenCalledTimes(1)
|
||||
expect(onSettled).toHaveBeenCalledTimes(2)
|
||||
})
|
||||
})
|
||||
|
||||
describe('useLeaderRestoreListener', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
restoreRequestCallback = null
|
||||
restoreIntentCallback = null
|
||||
mockDoSyncWorkflowDraft.mockImplementation((_sync: boolean, callbacks?: SyncDraftCallback) => {
|
||||
callbacks?.onSuccess?.()
|
||||
callbacks?.onSettled?.()
|
||||
})
|
||||
})
|
||||
|
||||
it('shows restore notifications for request and intent events', () => {
|
||||
const { unmount } = renderHook(() => useLeaderRestoreListener())
|
||||
|
||||
act(() => {
|
||||
restoreRequestCallback?.({
|
||||
...{
|
||||
versionId: 'v-2',
|
||||
versionName: 'Version Two',
|
||||
initiatorUserId: 'u-2',
|
||||
initiatorName: 'Bob',
|
||||
graphData: { nodes: [], edges: [] },
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
expect(mockToastInfo).toHaveBeenCalledWith(
|
||||
'workflow.versionHistory.action.restoreInProgress:Bob:Version Two',
|
||||
{ timeout: 3000 },
|
||||
)
|
||||
expect(mockEmitRestoreIntent).toHaveBeenCalled()
|
||||
|
||||
act(() => {
|
||||
restoreIntentCallback?.({
|
||||
versionId: 'v-3',
|
||||
versionName: 'Version Three',
|
||||
initiatorUserId: 'u-3',
|
||||
initiatorName: 'Carol',
|
||||
})
|
||||
})
|
||||
expect(mockToastInfo).toHaveBeenCalledWith(
|
||||
'workflow.versionHistory.action.restoreInProgress:Carol:Version Three',
|
||||
{ timeout: 3000 },
|
||||
)
|
||||
|
||||
unmount()
|
||||
expect(unsubscribeRestoreRequest).toHaveBeenCalled()
|
||||
expect(unsubscribeRestoreIntent).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
@ -4,7 +4,6 @@ export * from './use-checklist'
|
||||
export * from './use-DSL'
|
||||
export * from './use-edges-interactions'
|
||||
export * from './use-inspect-vars-crud'
|
||||
export * from './use-leader-restore'
|
||||
export * from './use-node-data-update'
|
||||
export * from './use-nodes-interactions'
|
||||
export * from './use-nodes-meta-data'
|
||||
|
||||
@ -1,164 +0,0 @@
|
||||
import type { RestoreCompleteData, RestoreIntentData, RestoreRequestData } from '../collaboration/types/collaboration'
|
||||
import type { SyncCallback } from './use-nodes-sync-draft'
|
||||
import { toast } from '@langgenius/dify-ui/toast'
|
||||
import { useSuspenseQuery } from '@tanstack/react-query'
|
||||
import { useCallback, useEffect, useRef } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useReactFlow } from 'reactflow'
|
||||
import { useStore as useAppStore } from '@/app/components/app/store'
|
||||
import { useFeaturesStore } from '@/app/components/base/features/hooks'
|
||||
import { systemFeaturesQueryOptions } from '@/service/system-features'
|
||||
import { collaborationManager } from '../collaboration/core/collaboration-manager'
|
||||
import { useWorkflowStore } from '../store'
|
||||
import { useNodesSyncDraft } from './use-nodes-sync-draft'
|
||||
|
||||
type RestoreCallbacks = SyncCallback
|
||||
|
||||
const usePerformRestore = () => {
|
||||
const { doSyncWorkflowDraft } = useNodesSyncDraft()
|
||||
const appDetail = useAppStore.getState().appDetail
|
||||
const featuresStore = useFeaturesStore()
|
||||
const workflowStore = useWorkflowStore()
|
||||
const reactflow = useReactFlow()
|
||||
|
||||
return useCallback((data: RestoreRequestData, callbacks?: RestoreCallbacks) => {
|
||||
collaborationManager.emitRestoreIntent({
|
||||
versionId: data.versionId,
|
||||
versionName: data.versionName,
|
||||
initiatorUserId: data.initiatorUserId,
|
||||
initiatorName: data.initiatorName,
|
||||
})
|
||||
|
||||
if (data.features && featuresStore) {
|
||||
const { setFeatures } = featuresStore.getState()
|
||||
setFeatures(data.features)
|
||||
}
|
||||
|
||||
if (data.environmentVariables) {
|
||||
workflowStore.getState().setEnvironmentVariables(data.environmentVariables)
|
||||
}
|
||||
|
||||
if (data.conversationVariables) {
|
||||
workflowStore.getState().setConversationVariables(data.conversationVariables)
|
||||
}
|
||||
|
||||
const { nodes, edges, viewport } = data.graphData
|
||||
const currentNodes = collaborationManager.getNodes()
|
||||
const currentEdges = collaborationManager.getEdges()
|
||||
|
||||
collaborationManager.setNodes(currentNodes, nodes, 'leader-restore:apply-graph')
|
||||
collaborationManager.setEdges(currentEdges, edges)
|
||||
collaborationManager.refreshGraphSynchronously()
|
||||
|
||||
if (viewport)
|
||||
reactflow.setViewport(viewport)
|
||||
|
||||
doSyncWorkflowDraft(false, {
|
||||
onSuccess: () => {
|
||||
collaborationManager.emitRestoreComplete({
|
||||
versionId: data.versionId,
|
||||
success: true,
|
||||
})
|
||||
|
||||
if (appDetail)
|
||||
collaborationManager.emitWorkflowUpdate(appDetail.id)
|
||||
|
||||
callbacks?.onSuccess?.()
|
||||
},
|
||||
onError: () => {
|
||||
collaborationManager.emitRestoreComplete({
|
||||
versionId: data.versionId,
|
||||
success: false,
|
||||
error: 'Failed to sync restore to server',
|
||||
})
|
||||
callbacks?.onError?.()
|
||||
},
|
||||
onSettled: () => {
|
||||
callbacks?.onSettled?.()
|
||||
},
|
||||
})
|
||||
}, [appDetail, doSyncWorkflowDraft, featuresStore, reactflow, workflowStore])
|
||||
}
|
||||
|
||||
export const useLeaderRestoreListener = () => {
|
||||
const { t } = useTranslation()
|
||||
const performRestore = usePerformRestore()
|
||||
|
||||
useEffect(() => {
|
||||
const unsubscribe = collaborationManager.onRestoreRequest((data: RestoreRequestData) => {
|
||||
toast.info(t('versionHistory.action.restoreInProgress', {
|
||||
ns: 'workflow',
|
||||
userName: data.initiatorName,
|
||||
versionName: data.versionName || data.versionId,
|
||||
}), { timeout: 3000 })
|
||||
performRestore(data)
|
||||
})
|
||||
|
||||
return unsubscribe
|
||||
}, [performRestore, t])
|
||||
|
||||
useEffect(() => {
|
||||
const unsubscribe = collaborationManager.onRestoreIntent((data: RestoreIntentData) => {
|
||||
toast.info(t('versionHistory.action.restoreInProgress', {
|
||||
ns: 'workflow',
|
||||
userName: data.initiatorName,
|
||||
versionName: data.versionName || data.versionId,
|
||||
}), { timeout: 3000 })
|
||||
})
|
||||
|
||||
return unsubscribe
|
||||
}, [t])
|
||||
}
|
||||
|
||||
export const useLeaderRestore = () => {
|
||||
const performRestore = usePerformRestore()
|
||||
const pendingCallbacksRef = useRef<{
|
||||
versionId: string
|
||||
callbacks: RestoreCallbacks | null
|
||||
} | null>(null)
|
||||
const { data: isCollaborationEnabled } = useSuspenseQuery({
|
||||
...systemFeaturesQueryOptions(),
|
||||
select: s => s.enable_collaboration_mode,
|
||||
})
|
||||
|
||||
const requestRestore = useCallback((data: RestoreRequestData, callbacks?: RestoreCallbacks) => {
|
||||
if (!isCollaborationEnabled || !collaborationManager.isConnected() || collaborationManager.getIsLeader()) {
|
||||
performRestore(data, callbacks)
|
||||
return
|
||||
}
|
||||
|
||||
pendingCallbacksRef.current = {
|
||||
versionId: data.versionId,
|
||||
callbacks: callbacks || null,
|
||||
}
|
||||
collaborationManager.emitRestoreRequest(data)
|
||||
}, [isCollaborationEnabled, performRestore])
|
||||
|
||||
useEffect(() => {
|
||||
const unsubscribe = collaborationManager.onRestoreComplete((data: RestoreCompleteData) => {
|
||||
const pending = pendingCallbacksRef.current
|
||||
if (!pending || pending.versionId !== data.versionId)
|
||||
return
|
||||
|
||||
const callbacks = pending.callbacks
|
||||
if (!callbacks) {
|
||||
pendingCallbacksRef.current = null
|
||||
return
|
||||
}
|
||||
|
||||
if (data.success)
|
||||
callbacks.onSuccess?.()
|
||||
else
|
||||
callbacks.onError?.()
|
||||
|
||||
callbacks.onSettled?.()
|
||||
pendingCallbacksRef.current = null
|
||||
})
|
||||
|
||||
return unsubscribe
|
||||
}, [])
|
||||
|
||||
return {
|
||||
requestRestore,
|
||||
}
|
||||
}
|
||||
@ -81,7 +81,6 @@ import EdgeContextmenu from './edge-contextmenu'
|
||||
import HelpLine from './help-line'
|
||||
import {
|
||||
useEdgesInteractions,
|
||||
useLeaderRestoreListener,
|
||||
useNodesInteractions,
|
||||
useNodesReadOnly,
|
||||
useNodesSyncDraft,
|
||||
@ -277,6 +276,17 @@ export const Workflow: FC<WorkflowProps> = memo(({
|
||||
toast.info(t('collaboration.historyAction.generic', { ns: 'workflow' }))
|
||||
})
|
||||
}, [t])
|
||||
|
||||
useEffect(() => {
|
||||
return collaborationManager.onRestoreIntent((data) => {
|
||||
toast.info(t('versionHistory.action.restoreInProgress', {
|
||||
ns: 'workflow',
|
||||
userName: data.initiatorName,
|
||||
versionName: data.versionName || data.versionId,
|
||||
}), { timeout: 3000 })
|
||||
})
|
||||
}, [t])
|
||||
|
||||
const {
|
||||
handleSyncWorkflowDraft,
|
||||
syncWorkflowDraftWhenPageClose,
|
||||
@ -533,8 +543,6 @@ export const Workflow: FC<WorkflowProps> = memo(({
|
||||
// Initialize workflow node search functionality
|
||||
useWorkflowSearch()
|
||||
|
||||
useLeaderRestoreListener()
|
||||
|
||||
// Set up scroll to node event listener using the utility function
|
||||
useEffect(() => {
|
||||
return setupScrollToNodeListener(nodes, reactflow)
|
||||
|
||||
Loading…
Reference in New Issue
Block a user