From 5657bf52f097b5ea546f8b7f945aad284bf03dd5 Mon Sep 17 00:00:00 2001 From: Joel Date: Wed, 21 Jan 2026 13:56:02 +0800 Subject: [PATCH] fix: can not save when switch to skill --- .../hooks/use-nodes-sync-draft.ts | 11 +++ web/app/components/workflow-app/index.tsx | 76 +++++++++++++------ .../hooks/use-serial-async-callback.ts | 17 ++++- 3 files changed, 79 insertions(+), 25 deletions(-) diff --git a/web/app/components/workflow-app/hooks/use-nodes-sync-draft.ts b/web/app/components/workflow-app/hooks/use-nodes-sync-draft.ts index 24c07ce456..fc9c6c0ef3 100644 --- a/web/app/components/workflow-app/hooks/use-nodes-sync-draft.ts +++ b/web/app/components/workflow-app/hooks/use-nodes-sync-draft.ts @@ -131,9 +131,20 @@ export const useNodesSyncDraft = () => { }, [workflowStore, getPostParams, getNodesReadOnly, handleRefreshWorkflowDraft]) const doSyncWorkflowDraft = useSerialAsyncCallback(performSync, getNodesReadOnly) + const syncWorkflowDraftImmediately = useCallback(( + notRefreshWhenSyncError?: boolean, + callback?: { + onSuccess?: () => void + onError?: () => void + onSettled?: () => void + }, + ) => { + return performSync(notRefreshWhenSyncError, callback) + }, [performSync]) return { doSyncWorkflowDraft, syncWorkflowDraftWhenPageClose, + syncWorkflowDraftImmediately, } } diff --git a/web/app/components/workflow-app/index.tsx b/web/app/components/workflow-app/index.tsx index cee481d473..9cda25f9aa 100644 --- a/web/app/components/workflow-app/index.tsx +++ b/web/app/components/workflow-app/index.tsx @@ -1,5 +1,6 @@ 'use client' +import type { ReactNode } from 'react' import type { Features as FeaturesData } from '@/app/components/base/features/types' import type { InjectWorkflowStoreSliceFn } from '@/app/components/workflow/store' import dynamic from 'next/dynamic' @@ -9,6 +10,7 @@ import { useCallback, useEffect, useMemo, + useRef, } from 'react' import { useStore as useAppStore } from '@/app/components/app/store' import { FeaturesProvider } from '@/app/components/base/features' @@ -35,9 +37,11 @@ import { AppModeEnum } from '@/types/app' import ViewPicker from '../workflow/view-picker' import WorkflowAppMain from './components/workflow-main' import { useGetRunAndTraceUrl } from './hooks/use-get-run-and-trace-url' +import { useNodesSyncDraft } from './hooks/use-nodes-sync-draft' import { useWorkflowInit, } from './hooks/use-workflow-init' +import { useWorkflowRefreshDraft } from './hooks/use-workflow-refresh-draft' import { parseAsViewType, WORKFLOW_VIEW_PARAM_KEY } from './search-params' import { createWorkflowSlice } from './store/workflow/workflow-slice' @@ -45,23 +49,61 @@ const SkillMain = dynamic(() => import('@/app/components/workflow/skill/main'), ssr: false, }) +type WorkflowViewContentProps = { + graphContent: ReactNode +} + +const WorkflowViewContent = ({ + graphContent, +}: WorkflowViewContentProps) => { + const [viewType, doSetViewType] = useQueryState(WORKFLOW_VIEW_PARAM_KEY, parseAsViewType) + const { syncWorkflowDraftImmediately } = useNodesSyncDraft() + const { handleRefreshWorkflowDraft } = useWorkflowRefreshDraft() + const pendingSyncRef = useRef | null>(null) + + const handleViewTypeChange = useCallback((type: ViewType) => { + if (viewType === ViewType.graph && type !== viewType) + pendingSyncRef.current = syncWorkflowDraftImmediately(true).catch(() => {}) + + doSetViewType(type) + if (type === ViewType.graph) { + const pending = pendingSyncRef.current + if (pending) { + pending.finally(() => { + handleRefreshWorkflowDraft() + }) + pendingSyncRef.current = null + } + else { + handleRefreshWorkflowDraft() + } + } + }, [doSetViewType, handleRefreshWorkflowDraft, syncWorkflowDraftImmediately, viewType]) + + return ( +
+ + {viewType === ViewType.graph + ? graphContent + : ( + + )} +
+ ) +} + const WorkflowAppWithAdditionalContext = () => { const { data, isLoading, fileUploadConfigResponse, - reload, } = useWorkflowInit() const workflowStore = useWorkflowStore() const { isLoadingCurrentWorkspace, currentWorkspace } = useAppContext() - const [viewType, doSetViewType] = useQueryState(WORKFLOW_VIEW_PARAM_KEY, parseAsViewType) - const setViewType = useCallback((type: ViewType) => { - doSetViewType(type) - if (type === ViewType.graph) - reload() - }, [doSetViewType, reload]) - // Initialize trigger status at application level const { setTriggerStatuses } = useTriggerStatusStore() const appDetail = useAppStore(s => s.appDetail) @@ -231,21 +273,11 @@ const WorkflowAppWithAdditionalContext = () => { edges={edgesData} nodes={nodesData} > -
- + - {viewType === ViewType.graph - ? ( - - {GraphMain} - - ) - : ( - - )} -
+ ) } diff --git a/web/app/components/workflow/hooks/use-serial-async-callback.ts b/web/app/components/workflow/hooks/use-serial-async-callback.ts index c36409a776..b0c7bf36dd 100644 --- a/web/app/components/workflow/hooks/use-serial-async-callback.ts +++ b/web/app/components/workflow/hooks/use-serial-async-callback.ts @@ -1,5 +1,6 @@ import { useCallback, + useEffect, useRef, } from 'react' @@ -8,15 +9,25 @@ export const useSerialAsyncCallback = ( shouldSkip?: () => boolean, ) => { const queueRef = useRef>(Promise.resolve()) + const fnRef = useRef(fn) + const shouldSkipRef = useRef(shouldSkip) + + useEffect(() => { + fnRef.current = fn + }, [fn]) + + useEffect(() => { + shouldSkipRef.current = shouldSkip + }, [shouldSkip]) return useCallback((...args: Args) => { - if (shouldSkip?.()) + if (shouldSkipRef.current?.()) return Promise.resolve(undefined as Result) const lastPromise = queueRef.current.catch(() => undefined) - const nextPromise = lastPromise.then(() => fn(...args)) + const nextPromise = lastPromise.then(() => fnRef.current(...args)) queueRef.current = nextPromise return nextPromise - }, [fn, shouldSkip]) + }, []) }