From 075173e67d2e85f8e246dbfa25821493e70d6caa Mon Sep 17 00:00:00 2001 From: lyzno1 Date: Tue, 21 Oct 2025 11:19:26 +0800 Subject: [PATCH] fix(workflow): reset onboarding auto-open flag across flows --- .../workflow-onboarding-integration.test.tsx | 76 ++++++++++++++----- .../rag-pipeline/hooks/use-pipeline-init.ts | 5 +- .../components/workflow-children.tsx | 2 + .../workflow-app/hooks/use-auto-onboarding.ts | 14 +++- .../workflow-app/hooks/use-workflow-init.ts | 1 + .../store/workflow/workflow-slice.ts | 4 + .../nodes/_base/components/node-handle.tsx | 28 ++++++- 7 files changed, 108 insertions(+), 22 deletions(-) diff --git a/web/__tests__/workflow-onboarding-integration.test.tsx b/web/__tests__/workflow-onboarding-integration.test.tsx index 577bc20db1..c1a922bb1f 100644 --- a/web/__tests__/workflow-onboarding-integration.test.tsx +++ b/web/__tests__/workflow-onboarding-integration.test.tsx @@ -18,6 +18,7 @@ describe('Workflow Onboarding Integration Logic', () => { const mockSetShowOnboarding = jest.fn() const mockSetHasSelectedStartNode = jest.fn() const mockSetHasShownOnboarding = jest.fn() + const mockSetShouldAutoOpenStartNodeSelector = jest.fn() beforeEach(() => { jest.clearAllMocks() @@ -31,6 +32,8 @@ describe('Workflow Onboarding Integration Logic', () => { hasShownOnboarding: false, setHasShownOnboarding: mockSetHasShownOnboarding, notInitialWorkflow: false, + shouldAutoOpenStartNodeSelector: false, + setShouldAutoOpenStartNodeSelector: mockSetShouldAutoOpenStartNodeSelector, }) }) @@ -180,17 +183,17 @@ describe('Workflow Onboarding Integration Logic', () => { }) }) - describe('Auto-expand Logic for Node Handles', () => { + describe('Auto-open Logic for Node Handles', () => { /** - * Test the auto-expand logic from node-handle.tsx - * This ensures all trigger types auto-expand the block selector + * Test the auto-open logic from node-handle.tsx + * This ensures all trigger types auto-open the block selector when flagged */ it('should auto-expand for Start node in new workflow', () => { - const notInitialWorkflow = true + const shouldAutoOpenStartNodeSelector = true const nodeType = BlockEnum.Start const isChatMode = false - const shouldAutoExpand = notInitialWorkflow && ( + const shouldAutoExpand = shouldAutoOpenStartNodeSelector && ( nodeType === BlockEnum.Start || nodeType === BlockEnum.TriggerSchedule || nodeType === BlockEnum.TriggerWebhook @@ -201,11 +204,11 @@ describe('Workflow Onboarding Integration Logic', () => { }) it('should auto-expand for TriggerSchedule in new workflow', () => { - const notInitialWorkflow = true + const shouldAutoOpenStartNodeSelector = true const nodeType = BlockEnum.TriggerSchedule const isChatMode = false - const shouldAutoExpand = notInitialWorkflow && ( + const shouldAutoExpand = shouldAutoOpenStartNodeSelector && ( nodeType === BlockEnum.Start || nodeType === BlockEnum.TriggerSchedule || nodeType === BlockEnum.TriggerWebhook @@ -216,11 +219,11 @@ describe('Workflow Onboarding Integration Logic', () => { }) it('should auto-expand for TriggerWebhook in new workflow', () => { - const notInitialWorkflow = true + const shouldAutoOpenStartNodeSelector = true const nodeType = BlockEnum.TriggerWebhook const isChatMode = false - const shouldAutoExpand = notInitialWorkflow && ( + const shouldAutoExpand = shouldAutoOpenStartNodeSelector && ( nodeType === BlockEnum.Start || nodeType === BlockEnum.TriggerSchedule || nodeType === BlockEnum.TriggerWebhook @@ -231,11 +234,11 @@ describe('Workflow Onboarding Integration Logic', () => { }) it('should auto-expand for TriggerPlugin in new workflow', () => { - const notInitialWorkflow = true + const shouldAutoOpenStartNodeSelector = true const nodeType = BlockEnum.TriggerPlugin const isChatMode = false - const shouldAutoExpand = notInitialWorkflow && ( + const shouldAutoExpand = shouldAutoOpenStartNodeSelector && ( nodeType === BlockEnum.Start || nodeType === BlockEnum.TriggerSchedule || nodeType === BlockEnum.TriggerWebhook @@ -246,11 +249,11 @@ describe('Workflow Onboarding Integration Logic', () => { }) it('should not auto-expand for non-trigger nodes', () => { - const notInitialWorkflow = true + const shouldAutoOpenStartNodeSelector = true const nodeType = BlockEnum.LLM const isChatMode = false - const shouldAutoExpand = notInitialWorkflow && ( + const shouldAutoExpand = shouldAutoOpenStartNodeSelector && ( nodeType === BlockEnum.Start || nodeType === BlockEnum.TriggerSchedule || nodeType === BlockEnum.TriggerWebhook @@ -261,11 +264,11 @@ describe('Workflow Onboarding Integration Logic', () => { }) it('should not auto-expand in chat mode', () => { - const notInitialWorkflow = true + const shouldAutoOpenStartNodeSelector = true const nodeType = BlockEnum.Start const isChatMode = true - const shouldAutoExpand = notInitialWorkflow && ( + const shouldAutoExpand = shouldAutoOpenStartNodeSelector && ( nodeType === BlockEnum.Start || nodeType === BlockEnum.TriggerSchedule || nodeType === BlockEnum.TriggerWebhook @@ -276,11 +279,11 @@ describe('Workflow Onboarding Integration Logic', () => { }) it('should not auto-expand for existing workflows', () => { - const notInitialWorkflow = false + const shouldAutoOpenStartNodeSelector = false const nodeType = BlockEnum.Start const isChatMode = false - const shouldAutoExpand = notInitialWorkflow && ( + const shouldAutoExpand = shouldAutoOpenStartNodeSelector && ( nodeType === BlockEnum.Start || nodeType === BlockEnum.TriggerSchedule || nodeType === BlockEnum.TriggerWebhook @@ -289,6 +292,24 @@ describe('Workflow Onboarding Integration Logic', () => { expect(shouldAutoExpand).toBe(false) }) + it('should reset auto-open flag after triggering once', () => { + let shouldAutoOpenStartNodeSelector = true + const nodeType = BlockEnum.Start + const isChatMode = false + + const shouldAutoExpand = shouldAutoOpenStartNodeSelector && ( + nodeType === BlockEnum.Start + || nodeType === BlockEnum.TriggerSchedule + || nodeType === BlockEnum.TriggerWebhook + || nodeType === BlockEnum.TriggerPlugin + ) && !isChatMode + + if (shouldAutoExpand) + shouldAutoOpenStartNodeSelector = false + + expect(shouldAutoExpand).toBe(true) + expect(shouldAutoOpenStartNodeSelector).toBe(false) + }) }) describe('Node Creation Without Auto-selection', () => { @@ -450,12 +471,19 @@ describe('Workflow Onboarding Integration Logic', () => { notInitialWorkflow: false, setShowOnboarding: mockSetShowOnboarding, setHasShownOnboarding: mockSetHasShownOnboarding, + hasSelectedStartNode: false, + setHasSelectedStartNode: mockSetHasSelectedStartNode, + shouldAutoOpenStartNodeSelector: false, + setShouldAutoOpenStartNodeSelector: mockSetShouldAutoOpenStartNodeSelector, getState: () => ({ showOnboarding: false, hasShownOnboarding: false, notInitialWorkflow: false, setShowOnboarding: mockSetShowOnboarding, setHasShownOnboarding: mockSetHasShownOnboarding, + hasSelectedStartNode: false, + setHasSelectedStartNode: mockSetHasSelectedStartNode, + setShouldAutoOpenStartNodeSelector: mockSetShouldAutoOpenStartNodeSelector, }), }) @@ -526,12 +554,19 @@ describe('Workflow Onboarding Integration Logic', () => { notInitialWorkflow: false, setShowOnboarding: mockSetShowOnboarding, setHasShownOnboarding: mockSetHasShownOnboarding, + hasSelectedStartNode: false, + setHasSelectedStartNode: mockSetHasSelectedStartNode, + shouldAutoOpenStartNodeSelector: false, + setShouldAutoOpenStartNodeSelector: mockSetShouldAutoOpenStartNodeSelector, getState: () => ({ showOnboarding: false, hasShownOnboarding: true, notInitialWorkflow: false, setShowOnboarding: mockSetShowOnboarding, setHasShownOnboarding: mockSetHasShownOnboarding, + hasSelectedStartNode: false, + setHasSelectedStartNode: mockSetHasSelectedStartNode, + setShouldAutoOpenStartNodeSelector: mockSetShouldAutoOpenStartNodeSelector, }), }) @@ -553,12 +588,19 @@ describe('Workflow Onboarding Integration Logic', () => { notInitialWorkflow: true, // Initial workflow creation setShowOnboarding: mockSetShowOnboarding, setHasShownOnboarding: mockSetHasShownOnboarding, + hasSelectedStartNode: false, + setHasSelectedStartNode: mockSetHasSelectedStartNode, + shouldAutoOpenStartNodeSelector: false, + setShouldAutoOpenStartNodeSelector: mockSetShouldAutoOpenStartNodeSelector, getState: () => ({ showOnboarding: false, hasShownOnboarding: false, notInitialWorkflow: true, setShowOnboarding: mockSetShowOnboarding, setHasShownOnboarding: mockSetHasShownOnboarding, + hasSelectedStartNode: false, + setHasSelectedStartNode: mockSetHasSelectedStartNode, + setShouldAutoOpenStartNodeSelector: mockSetShouldAutoOpenStartNodeSelector, }), }) diff --git a/web/app/components/rag-pipeline/hooks/use-pipeline-init.ts b/web/app/components/rag-pipeline/hooks/use-pipeline-init.ts index c70bce8523..6af72bee05 100644 --- a/web/app/components/rag-pipeline/hooks/use-pipeline-init.ts +++ b/web/app/components/rag-pipeline/hooks/use-pipeline-init.ts @@ -60,7 +60,10 @@ export const usePipelineInit = () => { if (error && error.json && !error.bodyUsed && datasetId) { error.json().then((err: any) => { if (err.code === 'draft_workflow_not_exist') { - workflowStore.setState({ notInitialWorkflow: true }) + workflowStore.setState({ + notInitialWorkflow: true, + shouldAutoOpenStartNodeSelector: true, + }) syncWorkflowDraft({ url: `/rag/pipelines/${datasetId}/workflows/draft`, params: { diff --git a/web/app/components/workflow-app/components/workflow-children.tsx b/web/app/components/workflow-app/components/workflow-children.tsx index 6321706347..35b6219a9b 100644 --- a/web/app/components/workflow-app/components/workflow-children.tsx +++ b/web/app/components/workflow-app/components/workflow-children.tsx @@ -75,6 +75,7 @@ const WorkflowChildren = () => { const showOnboarding = useStore(s => s.showOnboarding) const setShowOnboarding = useStore(s => s.setShowOnboarding) const setHasSelectedStartNode = useStore(s => s.setHasSelectedStartNode) + const setShouldAutoOpenStartNodeSelector = useStore(s => s.setShouldAutoOpenStartNodeSelector) const reactFlowStore = useStoreApi() const availableNodesMetaData = useAvailableNodesMetaData() const { handleSyncWorkflowDraft } = useNodesSyncDraft() @@ -142,6 +143,7 @@ const WorkflowChildren = () => { setShowOnboarding?.(false) setHasSelectedStartNode?.(true) + setShouldAutoOpenStartNodeSelector?.(true) handleSyncWorkflowDraft(true, false, { onSuccess: () => { diff --git a/web/app/components/workflow-app/hooks/use-auto-onboarding.ts b/web/app/components/workflow-app/hooks/use-auto-onboarding.ts index bce2a5117b..e4f5774adf 100644 --- a/web/app/components/workflow-app/hooks/use-auto-onboarding.ts +++ b/web/app/components/workflow-app/hooks/use-auto-onboarding.ts @@ -14,6 +14,7 @@ export const useAutoOnboarding = () => { notInitialWorkflow, setShowOnboarding, setHasShownOnboarding, + setShouldAutoOpenStartNodeSelector, } = workflowStore.getState() // Skip if already showing onboarding or it's the initial workflow creation @@ -30,13 +31,24 @@ export const useAutoOnboarding = () => { if (isCompletelyEmpty && !hasShownOnboarding) { setShowOnboarding?.(true) setHasShownOnboarding?.(true) + setShouldAutoOpenStartNodeSelector?.(true) } }, [store, workflowStore]) const handleOnboardingClose = useCallback(() => { - const { setShowOnboarding, setHasShownOnboarding } = workflowStore.getState() + const { + setShowOnboarding, + setHasShownOnboarding, + setShouldAutoOpenStartNodeSelector, + hasSelectedStartNode, + setHasSelectedStartNode, + } = workflowStore.getState() setShowOnboarding?.(false) setHasShownOnboarding?.(true) + if (hasSelectedStartNode) + setHasSelectedStartNode?.(false) + else + setShouldAutoOpenStartNodeSelector?.(false) }, [workflowStore]) // Check on mount and when nodes change diff --git a/web/app/components/workflow-app/hooks/use-workflow-init.ts b/web/app/components/workflow-app/hooks/use-workflow-init.ts index f2524b8bc9..080aa2f592 100644 --- a/web/app/components/workflow-app/hooks/use-workflow-init.ts +++ b/web/app/components/workflow-app/hooks/use-workflow-init.ts @@ -77,6 +77,7 @@ export const useWorkflowInit = () => { workflowStore.setState({ notInitialWorkflow: true, showOnboarding: !isAdvancedChat, + shouldAutoOpenStartNodeSelector: !isAdvancedChat, hasShownOnboarding: false, }) const nodesData = isAdvancedChat ? nodesTemplate : [] diff --git a/web/app/components/workflow-app/store/workflow/workflow-slice.ts b/web/app/components/workflow-app/store/workflow/workflow-slice.ts index f73eb27b9e..72230629f0 100644 --- a/web/app/components/workflow-app/store/workflow/workflow-slice.ts +++ b/web/app/components/workflow-app/store/workflow/workflow-slice.ts @@ -5,6 +5,8 @@ export type WorkflowSliceShape = { appName: string notInitialWorkflow: boolean setNotInitialWorkflow: (notInitialWorkflow: boolean) => void + shouldAutoOpenStartNodeSelector: boolean + setShouldAutoOpenStartNodeSelector: (shouldAutoOpen: boolean) => void nodesDefaultConfigs: Record setNodesDefaultConfigs: (nodesDefaultConfigs: Record) => void showOnboarding: boolean @@ -21,6 +23,8 @@ export const createWorkflowSlice: StateCreator = set => ({ appName: '', notInitialWorkflow: false, setNotInitialWorkflow: notInitialWorkflow => set(() => ({ notInitialWorkflow })), + shouldAutoOpenStartNodeSelector: false, + setShouldAutoOpenStartNodeSelector: shouldAutoOpenStartNodeSelector => set(() => ({ shouldAutoOpenStartNodeSelector })), nodesDefaultConfigs: {}, setNodesDefaultConfigs: nodesDefaultConfigs => set(() => ({ nodesDefaultConfigs })), showOnboarding: false, diff --git a/web/app/components/workflow/nodes/_base/components/node-handle.tsx b/web/app/components/workflow/nodes/_base/components/node-handle.tsx index 516542aa0a..6cfa7a7b9e 100644 --- a/web/app/components/workflow/nodes/_base/components/node-handle.tsx +++ b/web/app/components/workflow/nodes/_base/components/node-handle.tsx @@ -25,6 +25,7 @@ import { } from '../../../hooks' import { useStore, + useWorkflowStore, } from '../../../store' import cn from '@/utils/classnames' @@ -127,7 +128,10 @@ export const NodeSourceHandle = memo(({ showExceptionStatus, }: NodeHandleProps) => { const { t } = useTranslation() - const notInitialWorkflow = useStore(s => s.notInitialWorkflow) + const shouldAutoOpenStartNodeSelector = useStore(s => s.shouldAutoOpenStartNodeSelector) + const setShouldAutoOpenStartNodeSelector = useStore(s => s.setShouldAutoOpenStartNodeSelector) + const setHasSelectedStartNode = useStore(s => s.setHasSelectedStartNode) + const workflowStoreApi = useWorkflowStore() const [open, setOpen] = useState(false) const { handleNodeAdd } = useNodesInteractions() const { getNodesReadOnly } = useNodesReadOnly() @@ -157,9 +161,27 @@ export const NodeSourceHandle = memo(({ }, [handleNodeAdd, id, handleId]) useEffect(() => { - if (notInitialWorkflow && (data.type === BlockEnum.Start || data.type === BlockEnum.TriggerSchedule || data.type === BlockEnum.TriggerWebhook || data.type === BlockEnum.TriggerPlugin) && !isChatMode) + if (!shouldAutoOpenStartNodeSelector) + return + + if (isChatMode) { + setShouldAutoOpenStartNodeSelector?.(false) + return + } + + if (data.type === BlockEnum.Start || data.type === BlockEnum.TriggerSchedule || data.type === BlockEnum.TriggerWebhook || data.type === BlockEnum.TriggerPlugin) { setOpen(true) - }, [notInitialWorkflow, data.type, isChatMode]) + if (setShouldAutoOpenStartNodeSelector) + setShouldAutoOpenStartNodeSelector(false) + else + workflowStoreApi?.setState?.({ shouldAutoOpenStartNodeSelector: false }) + + if (setHasSelectedStartNode) + setHasSelectedStartNode(false) + else + workflowStoreApi?.setState?.({ hasSelectedStartNode: false }) + } + }, [shouldAutoOpenStartNodeSelector, data.type, isChatMode, setShouldAutoOpenStartNodeSelector, setHasSelectedStartNode, workflowStoreApi]) return (