From 7bed801b0d50636fbc34ff5127ab9bcc384816b8 Mon Sep 17 00:00:00 2001 From: Jingyi Date: Mon, 15 Jun 2026 17:22:54 -0700 Subject: [PATCH] fix(workflow): reset block selector tab on reopen (#37469) --- .../block-selector/__tests__/hooks.spec.tsx | 18 ++++++++++ .../block-selector/__tests__/main.spec.tsx | 34 +++++++++++++++++++ .../workflow/block-selector/hooks.ts | 8 +++-- .../workflow/block-selector/main.tsx | 4 ++- .../operator/__tests__/add-block.spec.tsx | 5 ++- .../workflow/operator/add-block.tsx | 12 ------- 6 files changed, 63 insertions(+), 18 deletions(-) diff --git a/web/app/components/workflow/block-selector/__tests__/hooks.spec.tsx b/web/app/components/workflow/block-selector/__tests__/hooks.spec.tsx index 7ad791fc49..bce33482f2 100644 --- a/web/app/components/workflow/block-selector/__tests__/hooks.spec.tsx +++ b/web/app/components/workflow/block-selector/__tests__/hooks.spec.tsx @@ -81,4 +81,22 @@ describe('block-selector hooks', () => { expect(result.current.tabs.some(tab => tab.key === TabsEnum.Snippets)).toBe(false) expect(result.current.activeTab).toBe(TabsEnum.Blocks) }) + + it('resets the active tab to the current default tab', () => { + const { result } = renderHook(() => useTabs({ + noStart: false, + })) + + act(() => { + result.current.setActiveTab(TabsEnum.Start) + }) + + expect(result.current.activeTab).toBe(TabsEnum.Start) + + act(() => { + result.current.resetActiveTab() + }) + + expect(result.current.activeTab).toBe(TabsEnum.Blocks) + }) }) diff --git a/web/app/components/workflow/block-selector/__tests__/main.spec.tsx b/web/app/components/workflow/block-selector/__tests__/main.spec.tsx index 884e45f625..aaf26eedc5 100644 --- a/web/app/components/workflow/block-selector/__tests__/main.spec.tsx +++ b/web/app/components/workflow/block-selector/__tests__/main.spec.tsx @@ -127,6 +127,40 @@ describe('NodeSelector', () => { expect(screen.getByText('End')).toBeInTheDocument() }) + it('resets to the default tab after closing', async () => { + const user = userEvent.setup() + + renderNodeSelector( + ( + + )} + />, + ) + + await user.click(screen.getByRole('button', { name: 'selector-closed' })) + await user.click(screen.getByText('workflow.tabs.start')) + + expect(screen.getByPlaceholderText('workflow.tabs.searchTrigger')).toBeInTheDocument() + + await user.click(screen.getByRole('button', { name: 'selector-open' })) + await waitFor(() => { + expect(screen.queryByPlaceholderText('workflow.tabs.searchTrigger')).not.toBeInTheDocument() + }) + + await user.click(screen.getByRole('button', { name: 'selector-closed' })) + + expect(screen.getByPlaceholderText('workflow.tabs.searchBlock')).toBeInTheDocument() + }) + it('does not open or emit open changes when disabled', async () => { const user = userEvent.setup() const onOpenChange = vi.fn() diff --git a/web/app/components/workflow/block-selector/hooks.ts b/web/app/components/workflow/block-selector/hooks.ts index c46f8fdb78..f406dfb970 100644 --- a/web/app/components/workflow/block-selector/hooks.ts +++ b/web/app/components/workflow/block-selector/hooks.ts @@ -119,17 +119,21 @@ export const useTabs = ({ return fallbackTab }, [defaultActiveTab, noBlocks, noSources, noTools, noSnippets, noStart, tabs, getValidTabKey]) const [activeTab, setActiveTab] = useState(initialTab) + const resetActiveTab = useCallback(() => { + setActiveTab(initialTab) + }, [initialTab]) useEffect(() => { const currentTab = tabs.find(tab => tab.key === activeTab) if (!currentTab || currentTab.disabled) - setActiveTab(initialTab) - }, [tabs, activeTab, initialTab]) + resetActiveTab() + }, [tabs, activeTab, resetActiveTab]) return { tabs, activeTab, setActiveTab, + resetActiveTab, } } diff --git a/web/app/components/workflow/block-selector/main.tsx b/web/app/components/workflow/block-selector/main.tsx index 315352cfdc..595426a262 100644 --- a/web/app/components/workflow/block-selector/main.tsx +++ b/web/app/components/workflow/block-selector/main.tsx @@ -135,6 +135,7 @@ function NodeSelector({ const disableSnippetsTab = flowType === FlowType.snippet const { activeTab, + resetActiveTab, setActiveTab, tabs, } = useTabs({ @@ -158,6 +159,7 @@ function NodeSelector({ if (!newOpen) { setSearchText('') setSnippetsLoading(false) + resetActiveTab() } else if (activeTab === TabsEnum.Snippets) { setSnippetsLoading(true) @@ -165,7 +167,7 @@ function NodeSelector({ if (onOpenChange) onOpenChange(newOpen) - }, [activeTab, disabled, onOpenChange]) + }, [activeTab, disabled, onOpenChange, resetActiveTab]) const handleTrigger = useCallback>((e) => { e.stopPropagation() }, []) diff --git a/web/app/components/workflow/operator/__tests__/add-block.spec.tsx b/web/app/components/workflow/operator/__tests__/add-block.spec.tsx index 84bbe149e6..0e27d2cc15 100644 --- a/web/app/components/workflow/operator/__tests__/add-block.spec.tsx +++ b/web/app/components/workflow/operator/__tests__/add-block.spec.tsx @@ -3,7 +3,6 @@ import { act, screen, waitFor } from '@testing-library/react' import { FlowType } from '@/types/common' import { createNode } from '../../__tests__/fixtures' import { renderWorkflowFlowComponent } from '../../__tests__/workflow-test-env' -import { TabsEnum } from '../../block-selector/types' import { BlockEnum } from '../../types' import AddBlock from '../add-block' @@ -21,7 +20,7 @@ type BlockSelectorMockProps = { popupClassName: string availableBlocksTypes: BlockEnum[] showStartTab: boolean - defaultActiveTab?: TabsEnum + defaultActiveTab?: unknown } const { @@ -129,10 +128,10 @@ describe('AddBlock', () => { disabled: false, availableBlocksTypes: mockAvailableNextBlocks, showStartTab: true, - defaultActiveTab: TabsEnum.Start, placement: 'right-start', popupClassName: 'min-w-[256px]!', }) + expect(latestBlockSelectorProps?.defaultActiveTab).toBeUndefined() expect(latestBlockSelectorProps?.offset).toEqual({ mainAxis: 4, crossAxis: -8, diff --git a/web/app/components/workflow/operator/add-block.tsx b/web/app/components/workflow/operator/add-block.tsx index 6a20c10c80..52b7176439 100644 --- a/web/app/components/workflow/operator/add-block.tsx +++ b/web/app/components/workflow/operator/add-block.tsx @@ -11,16 +11,13 @@ import { } from 'react' import { useTranslation } from 'react-i18next' import { - useNodes, useStoreApi, } from 'reactflow' import BlockSelector from '@/app/components/workflow/block-selector' import { BlockEnum, - isTriggerNode, } from '@/app/components/workflow/types' import { FlowType } from '@/types/common' -import { TabsEnum } from '../block-selector/types' import { useAvailableBlocks, useIsChatMode, @@ -55,18 +52,10 @@ const AddBlock = ({ const { nodesReadOnly } = useNodesReadOnly() const { handlePaneContextmenuCancel } = usePanelInteractions() const [open, setOpen] = useState(false) - const nodes = useNodes() const { availableNextBlocks } = useAvailableBlocks(BlockEnum.Start, false) const { nodesMap: nodesMetaDataMap } = useNodesMetaData() const flowType = useHooksStore(s => s.configsMap?.flowType) const showStartTab = flowType !== FlowType.ragPipeline && !isChatMode - const hasEntryNode = nodes.some((node) => { - const nodeData = node.data as { type?: BlockEnum } - const nodeType = nodeData.type - return nodeType === BlockEnum.Start || (nodeType ? isTriggerNode(nodeType) : false) - }) - - const defaultActiveTab = showStartTab && !hasEntryNode ? TabsEnum.Start : undefined const handleOpenChange = useCallback((open: boolean) => { setOpen(open) @@ -134,7 +123,6 @@ const AddBlock = ({ popupClassName="min-w-[256px]!" availableBlocksTypes={availableNextBlocks} showStartTab={showStartTab} - defaultActiveTab={defaultActiveTab} /> ) }