mirror of
https://github.com/langgenius/dify.git
synced 2026-08-28 19:38:30 +08:00
Merge b0c6999646 into 8031ea6d73
This commit is contained in:
commit
7e9b37b6a2
@ -22,6 +22,7 @@ const mockOnGraphReadyChange = vi.hoisted(() => vi.fn())
|
||||
const mockRefreshGraphSynchronously = vi.hoisted(() => vi.fn())
|
||||
const mockReplaceGraphFromReactFlow = vi.hoisted(() => vi.fn())
|
||||
const mockCanPersistLocalGraph = vi.hoisted(() => vi.fn())
|
||||
const mockCanUseLocalDraftFallback = vi.hoisted(() => vi.fn())
|
||||
const mockIsGraphReloadCurrent = vi.hoisted(() => vi.fn())
|
||||
const mockRetryGraphReload = vi.hoisted(() => vi.fn())
|
||||
|
||||
@ -68,6 +69,7 @@ const collaborationRuntime = vi.hoisted(() => ({
|
||||
cursors: {} as Record<string, { x: number; y: number; userId: string; timestamp: number }>,
|
||||
isConnected: false,
|
||||
isEnabled: false,
|
||||
connectionError: null as string | null,
|
||||
}))
|
||||
|
||||
const collaborationListeners = vi.hoisted(() => ({
|
||||
@ -142,6 +144,7 @@ vi.mock('@/app/components/workflow/collaboration/hooks/use-collaboration', () =>
|
||||
cursors: collaborationRuntime.cursors,
|
||||
isConnected: collaborationRuntime.isConnected,
|
||||
isEnabled: collaborationRuntime.isEnabled,
|
||||
connectionError: collaborationRuntime.connectionError,
|
||||
}),
|
||||
}))
|
||||
|
||||
@ -187,6 +190,7 @@ vi.mock('@/app/components/workflow/collaboration/core/collaboration-manager', ()
|
||||
refreshGraphSynchronously: mockRefreshGraphSynchronously,
|
||||
replaceGraphFromReactFlow: mockReplaceGraphFromReactFlow,
|
||||
canPersistLocalGraph: mockCanPersistLocalGraph,
|
||||
canUseLocalDraftFallback: mockCanUseLocalDraftFallback,
|
||||
isGraphReloadCurrent: mockIsGraphReloadCurrent,
|
||||
retryGraphReload: mockRetryGraphReload,
|
||||
getIsLeader: mockGetIsLeader,
|
||||
@ -436,6 +440,7 @@ describe('WorkflowMain', () => {
|
||||
collaborationRuntime.cursors = {}
|
||||
collaborationRuntime.isConnected = false
|
||||
collaborationRuntime.isEnabled = false
|
||||
collaborationRuntime.connectionError = null
|
||||
collaborationListeners.varsAndFeaturesUpdate = null
|
||||
collaborationListeners.workflowUpdate = null
|
||||
collaborationListeners.syncRequest = null
|
||||
@ -445,6 +450,7 @@ describe('WorkflowMain', () => {
|
||||
hookFns.doSyncWorkflowDraft.mockReset()
|
||||
mockGetIsLeader.mockReturnValue(true)
|
||||
mockCanPersistLocalGraph.mockReturnValue(true)
|
||||
mockCanUseLocalDraftFallback.mockReturnValue(false)
|
||||
mockIsGraphReloadCurrent.mockReturnValue(true)
|
||||
mockReplaceGraphFromReactFlow.mockReturnValue(true)
|
||||
hookFns.doSyncWorkflowDraft.mockResolvedValue({ hash: 'saved-hash', updatedAt: 2 })
|
||||
@ -590,6 +596,31 @@ describe('WorkflowMain', () => {
|
||||
expect(screen.queryByRole('status')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('uses the local draft when the initial collaboration connection fails', () => {
|
||||
collaborationRuntime.isEnabled = true
|
||||
collaborationRuntime.connectionError = 'websocket error'
|
||||
mockCanUseLocalDraftFallback.mockReturnValue(true)
|
||||
|
||||
render(<WorkflowMain nodes={[]} edges={[]} viewport={{ x: 0, y: 0, zoom: 1 }} />)
|
||||
|
||||
act(() => collaborationListeners.graphReadyChange?.(false))
|
||||
|
||||
expect(screen.queryByTestId('collaboration-graph-loading')).not.toBeInTheDocument()
|
||||
expect(screen.getByTestId('workflow-inner-context')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('keeps blocking after an established collaboration session disconnects', () => {
|
||||
collaborationRuntime.isEnabled = true
|
||||
collaborationRuntime.connectionError = 'transport close'
|
||||
mockCanUseLocalDraftFallback.mockReturnValue(false)
|
||||
|
||||
render(<WorkflowMain nodes={[]} edges={[]} viewport={{ x: 0, y: 0, zoom: 1 }} />)
|
||||
|
||||
act(() => collaborationListeners.graphReadyChange?.(false))
|
||||
|
||||
expect(screen.getByRole('status')).toHaveTextContent('workflow.common.syncingData')
|
||||
})
|
||||
|
||||
it('subscribes collaboration listeners and handles sync/workflow update callbacks', async () => {
|
||||
collaborationRuntime.isEnabled = true
|
||||
mockFetchWorkflowDraft.mockResolvedValue({
|
||||
|
||||
@ -80,6 +80,7 @@ const WorkflowMain = ({ nodes, edges, viewport }: WorkflowMainProps) => {
|
||||
onlineUsers,
|
||||
cursors,
|
||||
isConnected,
|
||||
connectionError,
|
||||
isEnabled: isCollaborationEnabled,
|
||||
} = useCollaboration(appId || '', reactFlowStore)
|
||||
const myUserId = useMemo(
|
||||
@ -537,6 +538,7 @@ const WorkflowMain = ({ nodes, edges, viewport }: WorkflowMainProps) => {
|
||||
<WorkflowChildren />
|
||||
</WorkflowWithInnerContext>
|
||||
{isCollaborationEnabled &&
|
||||
!(connectionError && collaborationManager.canUseLocalDraftFallback()) &&
|
||||
(collaborationGraphState.appId !== appId || !collaborationGraphState.isReady) && (
|
||||
<div
|
||||
data-testid="collaboration-graph-loading"
|
||||
|
||||
@ -169,4 +169,26 @@ describe('useCollaboration', () => {
|
||||
result.current.startCursorTracking({ current: document.createElement('div') })
|
||||
expect(mockStartTracking).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('exposes an initial connection error and clears it after reconnecting', async () => {
|
||||
const { result } = renderHookWithConsoleQuery(() => useCollaboration('app-1'), {
|
||||
systemFeatures: { enable_collaboration_mode: true },
|
||||
})
|
||||
|
||||
await waitFor(() => {
|
||||
expect(onStateChangeCallback).not.toBeNull()
|
||||
})
|
||||
|
||||
onStateChangeCallback?.({ isConnected: false, error: 'websocket error' })
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.connectionError).toBe('websocket error')
|
||||
})
|
||||
|
||||
onStateChangeCallback?.({ isConnected: true })
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.connectionError).toBeNull()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -13,6 +13,7 @@ import { CursorService } from '../services/cursor-service'
|
||||
|
||||
type CollaborationViewState = {
|
||||
isConnected: boolean
|
||||
connectionError: string | null
|
||||
onlineUsers: OnlineUser[]
|
||||
cursors: Record<string, CursorPosition>
|
||||
nodePanelPresence: NodePanelPresenceMap
|
||||
@ -23,6 +24,7 @@ type ReactFlowStore = NonNullable<Parameters<typeof collaborationManager.connect
|
||||
|
||||
const initialState: CollaborationViewState = {
|
||||
isConnected: false,
|
||||
connectionError: null,
|
||||
onlineUsers: [],
|
||||
cursors: {},
|
||||
nodePanelPresence: {},
|
||||
@ -76,7 +78,14 @@ export function useCollaboration(appId: string, reactFlowStore?: ReactFlowStore)
|
||||
|
||||
if (newState.isConnected === undefined) return
|
||||
|
||||
setState((prev) => ({ ...prev, isConnected: newState.isConnected ?? prev.isConnected }))
|
||||
setState((prev) => ({
|
||||
...prev,
|
||||
isConnected: newState.isConnected ?? prev.isConnected,
|
||||
connectionError:
|
||||
newState.isConnected === true
|
||||
? null
|
||||
: (newState.error ?? newState.disconnectReason ?? prev.connectionError),
|
||||
}))
|
||||
},
|
||||
)
|
||||
|
||||
@ -145,6 +154,7 @@ export function useCollaboration(appId: string, reactFlowStore?: ReactFlowStore)
|
||||
|
||||
const result = {
|
||||
isConnected: state.isConnected || false,
|
||||
connectionError: state.connectionError,
|
||||
onlineUsers: state.onlineUsers || [],
|
||||
cursors: state.cursors || {},
|
||||
nodePanelPresence: state.nodePanelPresence || {},
|
||||
|
||||
Loading…
Reference in New Issue
Block a user