From 870a6427c99b8c69803aee147a7dd6162732014a Mon Sep 17 00:00:00 2001 From: wangxiaolei Date: Tue, 23 Dec 2025 19:01:29 +0800 Subject: [PATCH] feat: allow user close the tab to sync the draft (#30034) --- web/app/components/workflow/index.tsx | 16 +++++-- .../store/workflow/workflow-draft-slice.ts | 47 +++++++++++++------ 2 files changed, 44 insertions(+), 19 deletions(-) diff --git a/web/app/components/workflow/index.tsx b/web/app/components/workflow/index.tsx index ab31f36406..1d0c594c23 100644 --- a/web/app/components/workflow/index.tsx +++ b/web/app/components/workflow/index.tsx @@ -224,23 +224,31 @@ export const Workflow: FC = memo(({ return () => { handleSyncWorkflowDraft(true, true) } - }, []) + }, [handleSyncWorkflowDraft]) const { handleRefreshWorkflowDraft } = useWorkflowRefreshDraft() const handleSyncWorkflowDraftWhenPageClose = useCallback(() => { if (document.visibilityState === 'hidden') syncWorkflowDraftWhenPageClose() + else if (document.visibilityState === 'visible') setTimeout(() => handleRefreshWorkflowDraft(), 500) - }, [syncWorkflowDraftWhenPageClose, handleRefreshWorkflowDraft]) + }, [syncWorkflowDraftWhenPageClose, handleRefreshWorkflowDraft, workflowStore]) + + // Also add beforeunload handler as additional safety net for tab close + const handleBeforeUnload = useCallback(() => { + syncWorkflowDraftWhenPageClose() + }, [syncWorkflowDraftWhenPageClose]) useEffect(() => { document.addEventListener('visibilitychange', handleSyncWorkflowDraftWhenPageClose) + window.addEventListener('beforeunload', handleBeforeUnload) return () => { document.removeEventListener('visibilitychange', handleSyncWorkflowDraftWhenPageClose) + window.removeEventListener('beforeunload', handleBeforeUnload) } - }, [handleSyncWorkflowDraftWhenPageClose]) + }, [handleSyncWorkflowDraftWhenPageClose, handleBeforeUnload]) useEventListener('keydown', (e) => { if ((e.key === 'd' || e.key === 'D') && (e.ctrlKey || e.metaKey)) @@ -419,7 +427,7 @@ export const Workflow: FC = memo(({ onPaneContextMenu={handlePaneContextMenu} onSelectionContextMenu={handleSelectionContextMenu} connectionLineComponent={CustomConnectionLine} - // TODO: For LOOP node, how to distinguish between ITERATION and LOOP here? Maybe both are the same? + // NOTE: For LOOP node, how to distinguish between ITERATION and LOOP here? Maybe both are the same? connectionLineContainerStyle={{ zIndex: ITERATION_CHILDREN_Z_INDEX }} defaultViewport={viewport} multiSelectionKeyCode={null} diff --git a/web/app/components/workflow/store/workflow/workflow-draft-slice.ts b/web/app/components/workflow/store/workflow/workflow-draft-slice.ts index 6c08c50e4a..83792e84a6 100644 --- a/web/app/components/workflow/store/workflow/workflow-draft-slice.ts +++ b/web/app/components/workflow/store/workflow/workflow-draft-slice.ts @@ -7,6 +7,12 @@ import type { } from '@/app/components/workflow/types' import { debounce } from 'lodash-es' +type DebouncedFunc = { + (fn: () => void): void + cancel?: () => void + flush?: () => void +} + export type WorkflowDraftSliceShape = { backupDraft?: { nodes: Node[] @@ -16,7 +22,7 @@ export type WorkflowDraftSliceShape = { environmentVariables: EnvironmentVariable[] } setBackupDraft: (backupDraft?: WorkflowDraftSliceShape['backupDraft']) => void - debouncedSyncWorkflowDraft: (fn: () => void) => void + debouncedSyncWorkflowDraft: DebouncedFunc syncWorkflowDraftHash: string setSyncWorkflowDraftHash: (hash: string) => void isSyncingWorkflowDraft: boolean @@ -25,20 +31,31 @@ export type WorkflowDraftSliceShape = { setIsWorkflowDataLoaded: (loaded: boolean) => void nodes: Node[] setNodes: (nodes: Node[]) => void + flushPendingSync: () => void } -export const createWorkflowDraftSlice: StateCreator = set => ({ - backupDraft: undefined, - setBackupDraft: backupDraft => set(() => ({ backupDraft })), - debouncedSyncWorkflowDraft: debounce((syncWorkflowDraft) => { +export const createWorkflowDraftSlice: StateCreator = (set) => { + // Create the debounced function and store it with access to cancel/flush methods + const debouncedFn = debounce((syncWorkflowDraft) => { syncWorkflowDraft() - }, 5000), - syncWorkflowDraftHash: '', - setSyncWorkflowDraftHash: syncWorkflowDraftHash => set(() => ({ syncWorkflowDraftHash })), - isSyncingWorkflowDraft: false, - setIsSyncingWorkflowDraft: isSyncingWorkflowDraft => set(() => ({ isSyncingWorkflowDraft })), - isWorkflowDataLoaded: false, - setIsWorkflowDataLoaded: loaded => set(() => ({ isWorkflowDataLoaded: loaded })), - nodes: [], - setNodes: nodes => set(() => ({ nodes })), -}) + }, 5000) + + return { + backupDraft: undefined, + setBackupDraft: backupDraft => set(() => ({ backupDraft })), + debouncedSyncWorkflowDraft: debouncedFn, + syncWorkflowDraftHash: '', + setSyncWorkflowDraftHash: syncWorkflowDraftHash => set(() => ({ syncWorkflowDraftHash })), + isSyncingWorkflowDraft: false, + setIsSyncingWorkflowDraft: isSyncingWorkflowDraft => set(() => ({ isSyncingWorkflowDraft })), + isWorkflowDataLoaded: false, + setIsWorkflowDataLoaded: loaded => set(() => ({ isWorkflowDataLoaded: loaded })), + nodes: [], + setNodes: nodes => set(() => ({ nodes })), + flushPendingSync: () => { + // Flush any pending debounced sync operations + if (debouncedFn.flush) + debouncedFn.flush() + }, + } +}