From ed7ea68f7de7d5bf614d8509f92b6ca04e7b6e86 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9D=9E=E6=B3=95=E6=93=8D=E4=BD=9C?= Date: Wed, 29 Apr 2026 15:03:28 +0800 Subject: [PATCH] fix: restore app nav create submenu interaction (#35681) --- web/__tests__/header/nav-flow.test.tsx | 12 +- .../header/nav/__tests__/index.spec.tsx | 14 +- .../nav/nav-selector/__tests__/index.spec.tsx | 30 +++- .../header/nav/nav-selector/index.tsx | 132 +++++++++++------- 4 files changed, 126 insertions(+), 62 deletions(-) diff --git a/web/__tests__/header/nav-flow.test.tsx b/web/__tests__/header/nav-flow.test.tsx index 05955a6c83..667f1e36b7 100644 --- a/web/__tests__/header/nav-flow.test.tsx +++ b/web/__tests__/header/nav-flow.test.tsx @@ -195,9 +195,19 @@ describe('Header Nav Flow', () => { renderNav() fireEvent.click(screen.getByRole('button', { name: /Alpha/i })) - fireEvent.click(await screen.findByText('menus.newApp')) + + const openCreateMenu = async () => { + fireEvent.click(await screen.findByText('menus.newApp')) + return screen.findByText('newApp.startFromBlank') + } + + await openCreateMenu() fireEvent.click(await screen.findByText('newApp.startFromBlank')) + + await openCreateMenu() fireEvent.click(await screen.findByText('newApp.startFromTemplate')) + + await openCreateMenu() fireEvent.click(await screen.findByText('importDSL')) expect(mockOnCreate).toHaveBeenNthCalledWith(1, 'blank') diff --git a/web/app/components/header/nav/__tests__/index.spec.tsx b/web/app/components/header/nav/__tests__/index.spec.tsx index 3dce8375b3..f4a1399638 100644 --- a/web/app/components/header/nav/__tests__/index.spec.tsx +++ b/web/app/components/header/nav/__tests__/index.spec.tsx @@ -298,11 +298,15 @@ describe('Nav Component', () => { fireEvent.click(selectorButton) }) - const createButton = await screen.findByText('Create New') - await act(async () => { - fireEvent.click(createButton) - }) + const openCreateMenu = async () => { + const createButton = await screen.findByText('Create New') + await act(async () => { + fireEvent.click(createButton) + }) + return screen.findByText(/app\.newApp\.startFromBlank/i) + } + await openCreateMenu() const blankOption = await screen.findByText( /app\.newApp\.startFromBlank/i, ) @@ -311,6 +315,7 @@ describe('Nav Component', () => { }) expect(mockOnCreate).toHaveBeenCalledWith('blank') + await openCreateMenu() const templateOption = await screen.findByText( /app\.newApp\.startFromTemplate/i, ) @@ -319,6 +324,7 @@ describe('Nav Component', () => { }) expect(mockOnCreate).toHaveBeenCalledWith('template') + await openCreateMenu() const dslOption = await screen.findByText(/app\.importDSL/i) await act(async () => { fireEvent.click(dslOption) diff --git a/web/app/components/header/nav/nav-selector/__tests__/index.spec.tsx b/web/app/components/header/nav/nav-selector/__tests__/index.spec.tsx index 97443bb759..b1de3ab5e3 100644 --- a/web/app/components/header/nav/nav-selector/__tests__/index.spec.tsx +++ b/web/app/components/header/nav/nav-selector/__tests__/index.spec.tsx @@ -203,23 +203,30 @@ describe('NavSelector Component', () => { await act(async () => { fireEvent.click(button) }) - const createBtn = screen.getByText('Create New') - await act(async () => { - fireEvent.click(createBtn) - }) + const openCreateMenu = async () => { + const createBtn = screen.getByText('Create New') + await act(async () => { + fireEvent.click(createBtn) + }) + return screen.findByText(/app\.newApp\.startFromBlank/i) + } + + await openCreateMenu() const blank = await screen.findByText(/app\.newApp\.startFromBlank/i) await act(async () => { fireEvent.click(blank) }) expect(mockOnCreate).toHaveBeenCalledWith('blank') + await openCreateMenu() const template = await screen.findByText(/app\.newApp\.startFromTemplate/i) await act(async () => { fireEvent.click(template) }) expect(mockOnCreate).toHaveBeenCalledWith('template') + await openCreateMenu() const dsl = await screen.findByText(/app\.importDSL/i) await act(async () => { fireEvent.click(dsl) @@ -227,6 +234,21 @@ describe('NavSelector Component', () => { expect(mockOnCreate).toHaveBeenCalledWith('dsl') }) + it('should open extended create menu on hover in app mode', async () => { + render() + const button = screen.getByRole('button') + await act(async () => { + fireEvent.click(button) + }) + + const createBtn = screen.getByText('Create New') + await act(async () => { + fireEvent.mouseEnter(createBtn) + }) + + expect(await screen.findByText(/app\.newApp\.startFromBlank/i))!.toBeInTheDocument() + }) + it('should not show create button for non-editors', async () => { vi.mocked(useAppContext).mockReturnValue({ isCurrentWorkspaceEditor: false, diff --git a/web/app/components/header/nav/nav-selector/index.tsx b/web/app/components/header/nav/nav-selector/index.tsx index 285639adf7..a46c200e3d 100644 --- a/web/app/components/header/nav/nav-selector/index.tsx +++ b/web/app/components/header/nav/nav-selector/index.tsx @@ -1,6 +1,6 @@ 'use client' import type { AppIconType, AppModeEnum } from '@/types/app' -import { Menu, MenuButton, MenuItem, MenuItems, Transition } from '@headlessui/react' +import { Menu, MenuButton, MenuItem, MenuItems } from '@headlessui/react' import { cn } from '@langgenius/dify-ui/cn' import { RiAddLine, @@ -8,7 +8,7 @@ import { RiArrowRightSLine, } from '@remixicon/react' import { debounce } from 'es-toolkit/compat' -import { Fragment, useCallback } from 'react' +import { useCallback, useState } from 'react' import { useTranslation } from 'react-i18next' import { useStore as useAppStore } from '@/app/components/app/store' import { AppTypeIcon } from '@/app/components/app/type-selector' @@ -38,6 +38,75 @@ export type INavSelectorProps = { isLoadingMore?: boolean } +type AppCreateMenuProps = { + createText: string + startFromBlankText: string + startFromTemplateText: string + importDSLText: string + onCreate: (state: string) => void +} + +const AppCreateMenu = ({ + createText, + startFromBlankText, + startFromTemplateText, + importDSLText, + onCreate, +}: AppCreateMenuProps) => { + const [open, setOpen] = useState(false) + + const handleCreate = (state: string) => { + setOpen(false) + onCreate(state) + } + + return ( +
setOpen(false)}> + + {open && ( +
setOpen(true)} + > +
+ + +
+
+ +
+
+ )} +
+ ) +} + const NavSelector = ({ curNav, navigationItems, createText, isApp, onCreate, onLoadMore, isLoadingMore }: INavSelectorProps) => { const { t } = useTranslation() const router = useRouter() @@ -72,7 +141,7 @@ const NavSelector = ({ curNav, navigationItems, createText, isApp, onCreate, onL className=" absolute right-0 -left-11 mt-1.5 w-60 max-w-80 origin-top-right divide-y divide-divider-regular rounded-lg bg-components-panel-bg-blur - shadow-lg + shadow-lg outline-none " >
@@ -130,56 +199,13 @@ const NavSelector = ({ curNav, navigationItems, createText, isApp, onCreate, onL )} {isApp && isCurrentWorkspaceEditor && ( - - {({ open }) => ( - <> - -
-
- -
-
{createText}
- -
-
- - -
-
onCreate('blank')}> - - {t('newApp.startFromBlank', { ns: 'app' })} -
-
onCreate('template')}> - - {t('newApp.startFromTemplate', { ns: 'app' })} -
-
-
-
onCreate('dsl')}> - - {t('importDSL', { ns: 'app' })} -
-
-
-
- - )} -
+ )}