fix: restore app nav create submenu interaction (#35681)

This commit is contained in:
非法操作
2026-04-29 15:03:28 +08:00
committed by GitHub
parent afbc30c9ed
commit ed7ea68f7d
4 changed files with 126 additions and 62 deletions

View File

@@ -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')

View File

@@ -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)

View File

@@ -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(<NavSelector {...defaultProps} isApp />)
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,

View File

@@ -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 (
<div className="relative h-full w-full" onMouseLeave={() => setOpen(false)}>
<button
type="button"
className="w-full p-1 text-left"
onClick={() => setOpen(value => !value)}
onMouseEnter={() => setOpen(true)}
>
<div className={cn(
'flex cursor-pointer items-center gap-2 rounded-lg px-3 py-[6px] hover:bg-state-base-hover',
open && 'bg-state-base-hover!',
)}
>
<div className="flex h-6 w-6 shrink-0 items-center justify-center rounded-md border-[0.5px] border-divider-regular bg-background-default">
<RiAddLine className="h-4 w-4 text-text-primary" />
</div>
<div className="grow text-left text-[14px] font-normal text-text-secondary">{createText}</div>
<RiArrowRightSLine className="h-3.5 w-3.5 shrink-0 text-text-primary" />
</div>
</button>
{open && (
<div
className="absolute top-[3px] right-[-198px] z-10 min-w-[200px] rounded-lg border-[0.5px] border-components-panel-border bg-components-panel-bg-blur shadow-lg"
onMouseEnter={() => setOpen(true)}
>
<div className="p-1">
<button type="button" className={cn('flex w-full cursor-pointer items-center rounded-lg px-3 py-[6px] text-left font-normal text-text-secondary hover:bg-state-base-hover')} onClick={() => handleCreate('blank')}>
<FilePlus01 className="mr-2 h-4 w-4 shrink-0 text-text-secondary" />
{startFromBlankText}
</button>
<button type="button" className={cn('flex w-full cursor-pointer items-center rounded-lg px-3 py-[6px] text-left font-normal text-text-secondary hover:bg-state-base-hover')} onClick={() => handleCreate('template')}>
<FilePlus02 className="mr-2 h-4 w-4 shrink-0 text-text-secondary" />
{startFromTemplateText}
</button>
</div>
<div className="border-t border-divider-regular p-1">
<button type="button" className={cn('flex w-full cursor-pointer items-center rounded-lg px-3 py-[6px] text-left font-normal text-text-secondary hover:bg-state-base-hover')} onClick={() => handleCreate('dsl')}>
<FileArrow01 className="mr-2 h-4 w-4 shrink-0 text-text-secondary" />
{importDSLText}
</button>
</div>
</div>
)}
</div>
)
}
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
"
>
<div className="overflow-auto px-1 py-1" style={{ maxHeight: '50vh' }} onScroll={handleScroll}>
@@ -130,56 +199,13 @@ const NavSelector = ({ curNav, navigationItems, createText, isApp, onCreate, onL
</MenuItem>
)}
{isApp && isCurrentWorkspaceEditor && (
<Menu as="div" className="relative h-full w-full">
{({ open }) => (
<>
<MenuButton className="w-full p-1">
<div className={cn(
'flex cursor-pointer items-center gap-2 rounded-lg px-3 py-[6px] hover:bg-state-base-hover',
open && 'bg-state-base-hover!',
)}
>
<div className="flex h-6 w-6 shrink-0 items-center justify-center rounded-md border-[0.5px] border-divider-regular bg-background-default">
<RiAddLine className="h-4 w-4 text-text-primary" />
</div>
<div className="grow text-left text-[14px] font-normal text-text-secondary">{createText}</div>
<RiArrowRightSLine className="h-3.5 w-3.5 shrink-0 text-text-primary" />
</div>
</MenuButton>
<Transition
as={Fragment}
enter="transition ease-out duration-100"
enterFrom="transform opacity-0 scale-95"
enterTo="transform opacity-100 scale-100"
leave="transition ease-in duration-75"
leaveFrom="transform opacity-100 scale-100"
leaveTo="transform opacity-0 scale-95"
>
<MenuItems className={cn(
'absolute top-[3px] right-[-198px] z-10 min-w-[200px] rounded-lg bg-components-panel-bg-blur shadow-lg',
)}
>
<div className="p-1">
<div className={cn('flex cursor-pointer items-center rounded-lg px-3 py-[6px] font-normal text-text-secondary hover:bg-state-base-hover')} onClick={() => onCreate('blank')}>
<FilePlus01 className="mr-2 h-4 w-4 shrink-0 text-text-secondary" />
{t('newApp.startFromBlank', { ns: 'app' })}
</div>
<div className={cn('flex cursor-pointer items-center rounded-lg px-3 py-[6px] font-normal text-text-secondary hover:bg-state-base-hover')} onClick={() => onCreate('template')}>
<FilePlus02 className="mr-2 h-4 w-4 shrink-0 text-text-secondary" />
{t('newApp.startFromTemplate', { ns: 'app' })}
</div>
</div>
<div className="border-t border-divider-regular p-1">
<div className={cn('flex cursor-pointer items-center rounded-lg px-3 py-[6px] font-normal text-text-secondary hover:bg-state-base-hover')} onClick={() => onCreate('dsl')}>
<FileArrow01 className="mr-2 h-4 w-4 shrink-0 text-text-secondary" />
{t('importDSL', { ns: 'app' })}
</div>
</div>
</MenuItems>
</Transition>
</>
)}
</Menu>
<AppCreateMenu
createText={createText}
startFromBlankText={t('newApp.startFromBlank', { ns: 'app' })}
startFromTemplateText={t('newApp.startFromTemplate', { ns: 'app' })}
importDSLText={t('importDSL', { ns: 'app' })}
onCreate={onCreate}
/>
)}
</MenuItems>
</>