fix(web): fix rebase

This commit is contained in:
JzoNg 2026-06-22 12:12:04 +08:00
parent 16a0cfc40c
commit 01957f302f
2 changed files with 159 additions and 42 deletions

View File

@ -5,6 +5,7 @@ import type { ModalContextState } from '@/context/modal-context'
import type { ProviderContextState } from '@/context/provider-context'
import type { IWorkspace } from '@/models/common'
import type { InstalledApp } from '@/models/explore'
import type { SnippetDetail, SnippetInputField } from '@/models/snippet'
import { fireEvent, screen, waitFor } from '@testing-library/react'
import { createStore, Provider as JotaiProvider } from 'jotai'
import { createTestQueryClient, renderWithSystemFeatures } from '@/__tests__/utils/mock-system-features'
@ -16,6 +17,7 @@ import { ACCOUNT_SETTING_TAB } from '@/app/components/header/account-setting/con
import { useAppContext, useSelector as useAppContextSelector } from '@/context/app-context'
import { useModalContext } from '@/context/modal-context'
import { useProviderContext } from '@/context/provider-context'
import { PipelineInputVarType } from '@/models/pipeline'
import { usePathname, useRouter } from '@/next/navigation'
import { consoleQuery } from '@/service/client'
import { useGetInstalledApps, useUninstallApp, useUpdateAppPinStatus } from '@/service/use-explore'
@ -25,14 +27,28 @@ import { DETAIL_SIDEBAR_STORAGE_KEY } from '../storage'
const activeEdgeClassName = 'before:pointer-events-none'
const { mockIsAgentV2Enabled, mockSwitchWorkspace, mockToastSuccess, hotkeyRegistrations } = vi.hoisted(() => ({
type SnippetNavigationTestState = {
fields: SnippetInputField[]
onFieldsChange?: (fields: SnippetInputField[]) => void
readonly: boolean
snippet?: SnippetDetail
}
const { mockIsAgentV2Enabled, mockSnippetFieldsChange, mockSwitchWorkspace, mockToastSuccess, hotkeyRegistrations, snippetNavigationState } = vi.hoisted(() => ({
mockSwitchWorkspace: vi.fn(),
mockSnippetFieldsChange: vi.fn(),
mockToastSuccess: vi.fn(),
mockIsAgentV2Enabled: vi.fn(() => true),
hotkeyRegistrations: new Map<string, {
handler: (event: { preventDefault: () => void }) => void
options?: { ignoreInputs?: boolean }
}>(),
snippetNavigationState: {
fields: [],
readonly: true,
snippet: undefined,
onFieldsChange: undefined,
} as SnippetNavigationTestState,
}))
vi.mock('@/features/agent-v2/feature-flag', () => ({
@ -184,6 +200,38 @@ vi.mock('@/features/deployments/detail/deployment-sidebar', () => ({
),
}))
vi.mock('@/app/components/snippets/store', () => ({
useSnippetDetailStore: (selector: (state: SnippetNavigationTestState) => unknown) => selector(snippetNavigationState),
}))
vi.mock('@/app/components/snippets/components/snippet-sidebar', () => ({
SnippetSidebarContent: ({
fields,
onFieldsChange,
readonly,
snippet,
}: {
fields: SnippetInputField[]
onFieldsChange: (fields: SnippetInputField[]) => void
readonly: boolean
snippet: SnippetDetail
}) => (
<div data-testid="snippet-sidebar-content" data-readonly={String(readonly)}>
<span>{snippet.name}</span>
<span>{fields.map(field => field.variable).join(',')}</span>
<button type="button" onClick={() => onFieldsChange([])}>change snippet fields</button>
</div>
),
}))
vi.mock('../components/snippet-detail-top', () => ({
default: ({ expand, onToggle }: { expand: boolean, onToggle: () => void }) => (
<div data-testid="snippet-detail-top" data-expand={expand}>
<button type="button" data-testid="snippet-detail-toggle" onClick={onToggle}>Toggle</button>
</div>
),
}))
vi.mock('@/context/i18n', () => ({
useLocale: () => 'en-US',
useDocLink: () => (path: string) => `https://docs.dify.ai${path}`,
@ -241,6 +289,24 @@ const createInstalledApp = (overrides: Partial<InstalledApp> = {}): InstalledApp
},
})
const snippet: SnippetDetail = {
id: 'snippet-1',
name: 'Snippet',
description: 'Description',
updatedAt: '2026-03-29 10:00',
usage: '0',
tags: [],
}
const snippetFields: SnippetInputField[] = [
{
label: 'Query',
variable: 'query',
type: PipelineInputVarType.textInput,
required: true,
},
]
const appContextValue: AppContextValue = {
userProfile: {
id: 'user-1',
@ -357,6 +423,10 @@ describe('MainNav', () => {
})
mockSwitchWorkspace.mockReturnValue(new Promise(() => {}))
hotkeyRegistrations.clear()
snippetNavigationState.fields = []
snippetNavigationState.onFieldsChange = undefined
snippetNavigationState.readonly = true
snippetNavigationState.snippet = undefined
useAppStore.getState().setAppDetail()
})
@ -562,12 +632,24 @@ describe('MainNav', () => {
expect(screen.getByRole('link', { name: /common.mainNav.home/ })).not.toHaveAttribute('aria-current')
})
it('hides the main menu on snippet detail routes while keeping account settings available', () => {
it('replaces global navigation with snippet detail navigation on snippet routes', () => {
mockPathname = '/snippets/snippet-1/orchestrate'
snippetNavigationState.fields = snippetFields
snippetNavigationState.onFieldsChange = mockSnippetFieldsChange
snippetNavigationState.readonly = false
snippetNavigationState.snippet = snippet
renderMainNav()
expect(screen.getByRole('complementary')).toHaveClass('w-16')
expect(screen.getByRole('complementary')).toHaveClass('w-[248px]')
expect(screen.getByRole('complementary')).toHaveClass('p-1')
expect(screen.getByRole('complementary')).toHaveClass('bg-background-body')
expect(screen.getByTestId('snippet-detail-top')).toHaveAttribute('data-expand', 'true')
expect(screen.getByTestId('snippet-sidebar-content')).toHaveAttribute('data-readonly', 'false')
expect(screen.getByText(snippet.name)).toBeInTheDocument()
expect(screen.getByText('query')).toBeInTheDocument()
fireEvent.click(screen.getByRole('button', { name: 'change snippet fields' }))
expect(mockSnippetFieldsChange).toHaveBeenCalledWith([])
expect(screen.queryByLabelText('Dify')).not.toBeInTheDocument()
expect(screen.queryByRole('button', { name: 'common.mainNav.workspace.openMenu' })).not.toBeInTheDocument()
expect(screen.queryByRole('link', { name: /common.mainNav.home/ })).not.toBeInTheDocument()
@ -577,6 +659,22 @@ describe('MainNav', () => {
expect(screen.getByRole('button', { name: 'common.mainNav.help.openMenu' })).toBeInTheDocument()
})
it('collapses snippet detail navigation from the top-right toggle', () => {
mockPathname = '/snippets/snippet-1/orchestrate'
snippetNavigationState.fields = snippetFields
snippetNavigationState.onFieldsChange = mockSnippetFieldsChange
snippetNavigationState.snippet = snippet
renderMainNav()
fireEvent.click(screen.getByTestId('snippet-detail-toggle'))
expect(screen.getByRole('complementary')).toHaveClass('w-16')
expect(screen.getByRole('complementary')).toHaveClass('p-1')
expect(screen.getByTestId('snippet-detail-top')).toHaveAttribute('data-expand', 'false')
expect(screen.queryByTestId('snippet-sidebar-content')).not.toBeInTheDocument()
expect(localStorage.getItem(DETAIL_SIDEBAR_STORAGE_KEY)).toBe('collapse')
})
it('replaces global navigation with app detail navigation on app routes', () => {
mockPathname = '/app/app-1/overview'

View File

@ -14,9 +14,8 @@ import DatasetDetailTop from '@/app/components/app-sidebar/dataset-detail-top'
import { useStore as useAppStore } from '@/app/components/app/store'
import DifyLogo from '@/app/components/base/logo/dify-logo'
import EnvNav from '@/app/components/header/env-nav'
// import { buildIntegrationPath } from '@/app/components/integrations/routes'
// import { SnippetSidebarContent } from '@/app/components/snippets/components/snippet-sidebar'
// import { useSnippetDetailStore } from '@/app/components/snippets/store'
import { SnippetSidebarContent } from '@/app/components/snippets/components/snippet-sidebar'
import { useSnippetDetailStore } from '@/app/components/snippets/store'
import { useAppContext } from '@/context/app-context'
import { AgentDetailSection, AgentDetailTop } from '@/features/agent-v2/agent-detail/navigation'
import { isAgentV2Enabled } from '@/features/agent-v2/feature-flag'
@ -28,7 +27,7 @@ import AccountSection from './components/account-section'
import HelpMenu from './components/help-menu'
import MainNavLink from './components/nav-link'
import { MainNavSearchButton } from './components/search-button'
// import SnippetDetailTop from './components/snippet-detail-top'
import SnippetDetailTop from './components/snippet-detail-top'
import WebAppsSection from './components/web-apps-section'
import { WorkspaceCard } from './components/workspace-card'
import { isMainNavRouteVisible, MAIN_NAV_ROUTES } from './routes'
@ -99,8 +98,14 @@ const MainNav = ({
const showDatasetDetailNavigation = isDatasetDetailPathname(pathname)
const showAgentDetailNavigation = agentV2Enabled && !isCurrentWorkspaceDatasetOperator && isAgentDetailPathname(pathname)
const showDeploymentDetailNavigation = canUseAppDeploy && !isCurrentWorkspaceDatasetOperator && isDeploymentDetailPathname(pathname)
const showSnippetDetailBottomNavigation = isSnippetDetailPathname(pathname)
const showDetailNavigation = showAppDetailNavigation || showDatasetDetailNavigation || showAgentDetailNavigation || showDeploymentDetailNavigation
const showSnippetDetailNavigation = isSnippetDetailPathname(pathname)
const showDetailNavigation = showAppDetailNavigation || showDatasetDetailNavigation || showAgentDetailNavigation || showDeploymentDetailNavigation || showSnippetDetailNavigation
const snippetNavigation = useSnippetDetailStore(useShallow(state => ({
fields: state.fields,
onFieldsChange: state.onFieldsChange,
readonly: state.readonly,
snippet: state.snippet,
})))
const { hasAppDetail, setAppDetail } = useAppStore(useShallow(state => ({
hasAppDetail: !!state.appDetail,
setAppDetail: state.setAppDetail,
@ -265,25 +270,30 @@ const MainNav = ({
onToggle={handleToggleDetailNavigation}
/>
)
: (
<DeploymentDetailTop
expand={detailNavigationVisibleExpanded}
onToggle={handleToggleDetailNavigation}
/>
)
: showSnippetDetailBottomNavigation
? null
: (
<>
<div className="flex items-center justify-between pt-3 pr-2 pb-2 pl-4">
{renderLogo()}
<MainNavSearchButton />
</div>
<div className="p-2">
<WorkspaceCard />
</div>
</>
)}
: showDeploymentDetailNavigation
? (
<DeploymentDetailTop
expand={detailNavigationVisibleExpanded}
onToggle={handleToggleDetailNavigation}
/>
)
: (
<SnippetDetailTop
expand={detailNavigationVisibleExpanded}
onToggle={handleToggleDetailNavigation}
/>
)
: (
<>
<div className="flex items-center justify-between pt-3 pr-2 pb-2 pl-4">
{renderLogo()}
<MainNavSearchButton />
</div>
<div className="p-2">
<WorkspaceCard />
</div>
</>
)}
{showDetailNavigation
? showAppDetailNavigation
? <AppDetailSection expand={detailNavigationVisibleExpanded} />
@ -291,20 +301,29 @@ const MainNav = ({
? <DatasetDetailSection expand={detailNavigationVisibleExpanded} />
: showAgentDetailNavigation
? <AgentDetailSection expand={detailNavigationVisibleExpanded} />
: <DeploymentDetailSection expand={detailNavigationVisibleExpanded} />
: showSnippetDetailBottomNavigation
? null
: (
<>
<nav className="flex flex-col gap-px p-2">
{navItems.map(item => (
<MainNavLink key={item.href} item={item} pathname={pathname} />
))}
</nav>
{!isCurrentWorkspaceDatasetOperator && <WebAppsSection />}
</>
)}
{showEnvTag && !showSnippetDetailBottomNavigation && detailNavigationVisibleExpanded && (
: showDeploymentDetailNavigation
? <DeploymentDetailSection expand={detailNavigationVisibleExpanded} />
: detailNavigationVisibleExpanded && snippetNavigation.snippet && snippetNavigation.onFieldsChange
? (
<SnippetSidebarContent
snippet={snippetNavigation.snippet}
fields={snippetNavigation.fields}
readonly={snippetNavigation.readonly}
onFieldsChange={snippetNavigation.onFieldsChange}
/>
)
: null
: (
<>
<nav className="flex flex-col gap-px p-2">
{navItems.map(item => (
<MainNavLink key={item.href} item={item} pathname={pathname} />
))}
</nav>
{!isCurrentWorkspaceDatasetOperator && <WebAppsSection />}
</>
)}
{showEnvTag && detailNavigationVisibleExpanded && (
<div className="relative z-30 mt-auto shrink-0 px-3 pb-2">
<EnvNav />
</div>