mirror of
https://github.com/langgenius/dify.git
synced 2026-05-01 01:00:51 -04:00
fix: restore app nav create submenu interaction (#35681)
This commit is contained in:
@@ -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')
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
|
||||
Reference in New Issue
Block a user