From ee35f72861b4eea14cd4e705f7417b61274a5f74 Mon Sep 17 00:00:00 2001 From: yyh Date: Thu, 22 Jan 2026 13:14:01 +0800 Subject: [PATCH] fix workflow view switch refresh --- .../components/workflow-header/index.tsx | 2 + .../workflow-header/view-picker-trigger.tsx | 28 ++++++ .../hooks/use-workflow-refresh-draft.ts | 5 +- web/app/components/workflow-app/index.tsx | 92 +++++++++++++------ .../workflow/header/header-in-normal.tsx | 6 +- web/app/components/workflow/header/index.tsx | 32 +++++-- web/app/components/workflow/view-picker.tsx | 2 +- 7 files changed, 123 insertions(+), 44 deletions(-) create mode 100644 web/app/components/workflow-app/components/workflow-header/view-picker-trigger.tsx diff --git a/web/app/components/workflow-app/components/workflow-header/index.tsx b/web/app/components/workflow-app/components/workflow-header/index.tsx index 3fe679925a..993cbb6d78 100644 --- a/web/app/components/workflow-app/components/workflow-header/index.tsx +++ b/web/app/components/workflow-app/components/workflow-header/index.tsx @@ -11,6 +11,7 @@ import { useResetWorkflowVersionHistory } from '@/service/use-workflow' import { useIsChatMode } from '../../hooks' import ChatVariableTrigger from './chat-variable-trigger' import FeaturesTrigger from './features-trigger' +import ViewPickerTrigger from './view-picker-trigger' const WorkflowHeader = () => { const { appDetail, setCurrentLogItem, setShowMessageLogModal } = useAppStore(useShallow(state => ({ @@ -37,6 +38,7 @@ const WorkflowHeader = () => { return { normal: { components: { + left: , middle: , chatVariableTrigger: , }, diff --git a/web/app/components/workflow-app/components/workflow-header/view-picker-trigger.tsx b/web/app/components/workflow-app/components/workflow-header/view-picker-trigger.tsx new file mode 100644 index 0000000000..be49a412ed --- /dev/null +++ b/web/app/components/workflow-app/components/workflow-header/view-picker-trigger.tsx @@ -0,0 +1,28 @@ +'use client' + +import { useQueryState } from 'nuqs' +import { useCallback } from 'react' +import { useFeatures } from '@/app/components/base/features/hooks' +import { ViewType } from '@/app/components/workflow/types' +import ViewPicker from '@/app/components/workflow/view-picker' +import { parseAsViewType, WORKFLOW_VIEW_PARAM_KEY } from '../../search-params' + +const ViewPickerTrigger = () => { + const isSupportSandbox = useFeatures(s => !!s.features.sandbox?.enabled) + const [viewType, doSetViewType] = useQueryState(WORKFLOW_VIEW_PARAM_KEY, parseAsViewType) + const handleViewTypeChange = useCallback((type: ViewType) => { + doSetViewType(type) + }, [doSetViewType]) + + if (!isSupportSandbox) + return null + + return ( + + ) +} + +export default ViewPickerTrigger diff --git a/web/app/components/workflow-app/hooks/use-workflow-refresh-draft.ts b/web/app/components/workflow-app/hooks/use-workflow-refresh-draft.ts index fa4a44d894..7ea6eeef50 100644 --- a/web/app/components/workflow-app/hooks/use-workflow-refresh-draft.ts +++ b/web/app/components/workflow-app/hooks/use-workflow-refresh-draft.ts @@ -21,8 +21,9 @@ export const useWorkflowRefreshDraft = () => { debouncedSyncWorkflowDraft, } = workflowStore.getState() - if (debouncedSyncWorkflowDraft && typeof (debouncedSyncWorkflowDraft as any).cancel === 'function') - (debouncedSyncWorkflowDraft as any).cancel() + const { cancel } = debouncedSyncWorkflowDraft + if (typeof cancel === 'function') + cancel() const wasLoaded = isWorkflowDataLoaded if (wasLoaded) diff --git a/web/app/components/workflow-app/index.tsx b/web/app/components/workflow-app/index.tsx index 08972bad5b..c6b280064d 100644 --- a/web/app/components/workflow-app/index.tsx +++ b/web/app/components/workflow-app/index.tsx @@ -10,8 +10,8 @@ import { useCallback, useEffect, useMemo, + useReducer, useRef, - useState, } from 'react' import { useStore as useAppStore } from '@/app/components/app/store' import { FeaturesProvider } from '@/app/components/base/features' @@ -21,6 +21,7 @@ import WorkflowWithDefaultContext from '@/app/components/workflow' import { WorkflowContextProvider, } from '@/app/components/workflow/context' +import { HeaderShell } from '@/app/components/workflow/header' import { useWorkflowStore } from '@/app/components/workflow/store' import { useTriggerStatusStore } from '@/app/components/workflow/store/trigger-status' import { @@ -36,7 +37,7 @@ import { fetchRunDetail } from '@/service/log' import { useAppTriggers } from '@/service/use-tools' import { AppModeEnum } from '@/types/app' import { useFeatures } from '../base/features/hooks' -import ViewPicker from '../workflow/view-picker' +import ViewPickerTrigger from './components/workflow-header/view-picker-trigger' import WorkflowAppMain from './components/workflow-main' import { useGetRunAndTraceUrl } from './hooks/use-get-run-and-trace-url' import { useNodesSyncDraft } from './hooks/use-nodes-sync-draft' @@ -55,52 +56,85 @@ type WorkflowViewContentProps = { reload: () => Promise } +const WorkflowViewPickerDock = () => { + return ( + +
+
+ +
+
+
+ ) +} + const WorkflowViewContent = ({ graphContent, reload, }: WorkflowViewContentProps) => { const features = useFeatures(s => s.features) const isSupportSandbox = !!features.sandbox?.enabled - const [viewType, doSetViewType] = useQueryState(WORKFLOW_VIEW_PARAM_KEY, parseAsViewType) + const [viewType] = useQueryState(WORKFLOW_VIEW_PARAM_KEY, parseAsViewType) const { syncWorkflowDraftImmediately } = useNodesSyncDraft() + const [isGraphRefreshing, setGraphRefreshing] = useReducer((_: boolean, next: boolean) => next, false) const pendingSyncRef = useRef | null>(null) - const [isGraphRefreshing, setIsGraphRefreshing] = useState(false) + const refreshInFlightRef = useRef | null>(null) + const previousViewTypeRef = useRef(viewType) + const viewTypeRef = useRef(viewType) - const refreshGraph = useCallback(() => { - setIsGraphRefreshing(true) - return reload().finally(() => { - setIsGraphRefreshing(false) - }) + const refreshGraph = useCallback((waitFor?: Promise) => { + if (refreshInFlightRef.current) + return refreshInFlightRef.current + + setGraphRefreshing(true) + const runRefresh = () => { + if (viewTypeRef.current !== ViewType.graph) { + refreshInFlightRef.current = null + setGraphRefreshing(false) + return Promise.resolve() + } + + return reload().finally(() => { + refreshInFlightRef.current = null + setGraphRefreshing(false) + }) + } + refreshInFlightRef.current = waitFor ? waitFor.then(runRefresh, runRefresh) : runRefresh() + return refreshInFlightRef.current }, [reload]) - const handleViewTypeChange = useCallback((type: ViewType) => { - if (viewType === ViewType.graph && type !== viewType) - pendingSyncRef.current = syncWorkflowDraftImmediately(true).catch(() => { }) + useEffect(() => { + viewTypeRef.current = viewType + if (!isSupportSandbox) { + previousViewTypeRef.current = viewType + return + } - doSetViewType(type) - if (type === ViewType.graph) { - const pending = pendingSyncRef.current - if (pending) { + const previousView = previousViewTypeRef.current + if (previousView === ViewType.graph && viewType !== ViewType.graph) { + if (!pendingSyncRef.current) { + const pending = syncWorkflowDraftImmediately(true).catch(() => { }) + pendingSyncRef.current = pending pending.finally(() => { - refreshGraph() + if (pendingSyncRef.current === pending) + pendingSyncRef.current = null }) - pendingSyncRef.current = null - } - else { - refreshGraph() } } - }, [doSetViewType, refreshGraph, syncWorkflowDraftImmediately, viewType]) - if (!isSupportSandbox) { + if (previousView !== ViewType.graph && viewType === ViewType.graph) { + refreshGraph(pendingSyncRef.current ?? undefined) + } + + previousViewTypeRef.current = viewType + }, [isSupportSandbox, refreshGraph, syncWorkflowDraftImmediately, viewType]) + + if (!isSupportSandbox) return graphContent - } + return ( -
- +
+ {viewType !== ViewType.graph ? : null} {viewType === ViewType.graph ? ( isGraphRefreshing diff --git a/web/app/components/workflow/header/header-in-normal.tsx b/web/app/components/workflow/header/header-in-normal.tsx index be4985fb83..e48228030f 100644 --- a/web/app/components/workflow/header/header-in-normal.tsx +++ b/web/app/components/workflow/header/header-in-normal.tsx @@ -61,18 +61,18 @@ const HeaderInNormal = ({ setShowChatVariablePanel(false) setShowGlobalVariablePanel(false) closeAllInputFieldPanels() - }, [workflowStore, handleBackupDraft, selectedNode, handleNodeSelect, setShowWorkflowVersionHistoryPanel, setShowEnvPanel, setShowDebugAndPreviewPanel, setShowVariableInspectPanel, setShowChatVariablePanel, setShowGlobalVariablePanel]) + }, [workflowStore, handleBackupDraft, selectedNode, handleNodeSelect, setShowWorkflowVersionHistoryPanel, setShowEnvPanel, setShowDebugAndPreviewPanel, setShowVariableInspectPanel, setShowChatVariablePanel, setShowGlobalVariablePanel, closeAllInputFieldPanels]) return (
-
+
+ {components?.left}
- {components?.left}
diff --git a/web/app/components/workflow/header/index.tsx b/web/app/components/workflow/header/index.tsx index 0590c016f2..d0f91a69ff 100644 --- a/web/app/components/workflow/header/index.tsx +++ b/web/app/components/workflow/header/index.tsx @@ -1,3 +1,4 @@ +import type { ReactNode } from 'react' import type { HeaderInNormalProps } from './header-in-normal' import type { HeaderInRestoringProps } from './header-in-restoring' import type { HeaderInHistoryProps } from './header-in-view-history' @@ -16,6 +17,26 @@ const HeaderInRestoring = dynamic(() => import('./header-in-restoring'), { ssr: false, }) +type HeaderShellProps = { + children: ReactNode +} + +export const HeaderShell = ({ children }: HeaderShellProps) => { + const pathname = usePathname() + const inWorkflowCanvas = pathname.endsWith('/workflow') + const isPipelineCanvas = pathname.endsWith('/pipeline') + const maximizeCanvas = useStore(s => s.maximizeCanvas) + + return ( +
+ {(inWorkflowCanvas || isPipelineCanvas) && maximizeCanvas &&
} + {children} +
+ ) +} + export type HeaderProps = { normal?: HeaderInNormalProps viewHistory?: HeaderInHistoryProps @@ -26,21 +47,14 @@ const Header = ({ viewHistory: viewHistoryProps, restoring: restoringProps, }: HeaderProps) => { - const pathname = usePathname() - const inWorkflowCanvas = pathname.endsWith('/workflow') - const isPipelineCanvas = pathname.endsWith('/pipeline') const { normal, restoring, viewHistory, } = useWorkflowMode() - const maximizeCanvas = useStore(s => s.maximizeCanvas) return ( -
- {(inWorkflowCanvas || isPipelineCanvas) && maximizeCanvas &&
} + { normal && ( ) } -
+ ) } diff --git a/web/app/components/workflow/view-picker.tsx b/web/app/components/workflow/view-picker.tsx index 43b20d52fb..e81b35774e 100644 --- a/web/app/components/workflow/view-picker.tsx +++ b/web/app/components/workflow/view-picker.tsx @@ -33,7 +33,7 @@ const ViewPicker: FC = ({ return (