mirror of
https://github.com/langgenius/dify.git
synced 2026-06-23 04:05:14 +08:00
refactor(web): centralize main nav route access (#37612)
This commit is contained in:
parent
7bfcf9185c
commit
762321751c
@ -47,14 +47,14 @@ vi.mock('@/service/client', () => ({
|
||||
|
||||
const mockUseQuery = vi.mocked(useQuery)
|
||||
|
||||
function renderGuard(children: ReactNode) {
|
||||
function renderGuard(children: ReactNode, systemFeatures: { enable_app_deploy?: boolean } = {}) {
|
||||
return renderWithSystemFeatures(
|
||||
<RoleRouteGuard>
|
||||
{children}
|
||||
</RoleRouteGuard>,
|
||||
{
|
||||
systemFeatures: {
|
||||
enable_app_deploy: true,
|
||||
enable_app_deploy: systemFeatures.enable_app_deploy ?? true,
|
||||
},
|
||||
},
|
||||
)
|
||||
@ -95,6 +95,16 @@ describe('RoleRouteGuard', () => {
|
||||
expect(mocks.redirect).toHaveBeenCalledWith('/datasets')
|
||||
})
|
||||
|
||||
it('should allow dataset operator on routes outside the guarded list', () => {
|
||||
mockPathname = '/new-route'
|
||||
setCurrentWorkspaceQuery({ role: 'dataset_operator' })
|
||||
|
||||
renderGuard(<div>content</div>)
|
||||
|
||||
expect(screen.getByText('content')).toBeInTheDocument()
|
||||
expect(mocks.redirect).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should redirect dataset operator on deployments routes', () => {
|
||||
mockPathname = '/deployments/create'
|
||||
setCurrentWorkspaceQuery({ role: 'dataset_operator' })
|
||||
@ -104,6 +114,24 @@ describe('RoleRouteGuard', () => {
|
||||
expect(mocks.redirect).toHaveBeenCalledWith('/datasets')
|
||||
})
|
||||
|
||||
it('should prefer app deploy redirect when app deploy is disabled', () => {
|
||||
mockPathname = '/deployments/create'
|
||||
setCurrentWorkspaceQuery({ role: 'dataset_operator' })
|
||||
|
||||
expect(() => renderGuard(<div>content</div>, { enable_app_deploy: false })).toThrow('NEXT_REDIRECT:/')
|
||||
|
||||
expect(mocks.redirect).toHaveBeenCalledWith('/')
|
||||
})
|
||||
|
||||
it('should redirect app deploy routes when app deploy is disabled', () => {
|
||||
mockPathname = '/deployments/create'
|
||||
setCurrentWorkspaceQuery({ role: 'editor' })
|
||||
|
||||
expect(() => renderGuard(<div>content</div>, { enable_app_deploy: false })).toThrow('NEXT_REDIRECT:/')
|
||||
|
||||
expect(mocks.redirect).toHaveBeenCalledWith('/')
|
||||
})
|
||||
|
||||
it('should allow dataset operator on non-guarded routes', () => {
|
||||
mockPathname = '/plugins'
|
||||
setCurrentWorkspaceQuery({ role: 'dataset_operator' })
|
||||
|
||||
@ -30,7 +30,7 @@ export function RoleRouteGuard({ children }: { children: ReactNode }) {
|
||||
return <Loading type="app" />
|
||||
|
||||
if (shouldRedirectAppDeploy)
|
||||
redirect('/apps')
|
||||
redirect('/')
|
||||
|
||||
if (shouldRedirectDatasetOperator)
|
||||
redirect('/datasets')
|
||||
|
||||
@ -15,7 +15,6 @@ 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 { useAppContext } from '@/context/app-context'
|
||||
import { AgentDetailSection, AgentDetailTop } from '@/features/agent-v2/agent-detail/navigation'
|
||||
import { isAgentV2Enabled } from '@/features/agent-v2/feature-flag'
|
||||
@ -29,6 +28,7 @@ import MainNavLink from './components/nav-link'
|
||||
import { MainNavSearchButton } from './components/search-button'
|
||||
import WebAppsSection from './components/web-apps-section'
|
||||
import { WorkspaceCard } from './components/workspace-card'
|
||||
import { isMainNavRouteVisible, MAIN_NAV_ROUTES } from './routes'
|
||||
|
||||
const DATASET_COLLECTION_ROUTES = new Set(['create', 'create-from-pipeline', 'connect'])
|
||||
const DATASET_DOCUMENT_CREATION_ROUTES = new Set(['create', 'create-from-pipeline'])
|
||||
@ -185,73 +185,20 @@ const MainNav = ({
|
||||
ignoreInputs: false,
|
||||
})
|
||||
|
||||
const navItems = useMemo<MainNavItem[]>(() => [
|
||||
...(!isCurrentWorkspaceDatasetOperator
|
||||
? [
|
||||
{
|
||||
href: '/',
|
||||
label: t('mainNav.home', { ns: 'common' }),
|
||||
active: (path: string) => path === '/' || path === '/explore/apps',
|
||||
icon: 'i-custom-vender-main-nav-home',
|
||||
activeIcon: 'i-custom-vender-main-nav-home-active',
|
||||
},
|
||||
{
|
||||
href: '/apps',
|
||||
label: t('menus.apps', { ns: 'common' }),
|
||||
active: (path: string) => path.startsWith('/apps') || path.startsWith('/app/') || path.startsWith('/snippets'),
|
||||
icon: 'i-custom-vender-main-nav-studio',
|
||||
activeIcon: 'i-custom-vender-main-nav-studio-active',
|
||||
},
|
||||
...(agentV2Enabled
|
||||
? [{
|
||||
href: '/roster',
|
||||
label: t('menus.roster', { ns: 'common' }),
|
||||
active: (path: string) => path.startsWith('/roster'),
|
||||
icon: 'i-custom-vender-main-nav-roster',
|
||||
activeIcon: 'i-custom-vender-main-nav-roster-active',
|
||||
}]
|
||||
: []),
|
||||
]
|
||||
: []),
|
||||
...((isCurrentWorkspaceEditor || isCurrentWorkspaceDatasetOperator)
|
||||
? [
|
||||
{
|
||||
href: '/datasets',
|
||||
label: t('menus.datasets', { ns: 'common' }),
|
||||
active: (path: string) => path.startsWith('/datasets'),
|
||||
icon: 'i-custom-vender-main-nav-knowledge',
|
||||
activeIcon: 'i-custom-vender-main-nav-knowledge-active',
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...(!isCurrentWorkspaceDatasetOperator
|
||||
? [
|
||||
{
|
||||
href: buildIntegrationPath('provider'),
|
||||
label: t('mainNav.integrations', { ns: 'common' }),
|
||||
active: (path: string) => path.startsWith('/integrations') || path.startsWith('/tools'),
|
||||
icon: 'i-custom-vender-main-nav-integrations',
|
||||
activeIcon: 'i-custom-vender-main-nav-integrations-active',
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{
|
||||
href: '/marketplace',
|
||||
label: t('mainNav.marketplace', { ns: 'common' }),
|
||||
active: path => path.startsWith('/marketplace') || path.startsWith('/plugins'),
|
||||
icon: 'i-custom-vender-main-nav-marketplace',
|
||||
activeIcon: 'i-custom-vender-main-nav-marketplace-active',
|
||||
},
|
||||
...(canUseAppDeploy
|
||||
? [{
|
||||
href: '/deployments',
|
||||
label: t('menus.deployments', { ns: 'common' }),
|
||||
active: (path: string) => path.startsWith('/deployments'),
|
||||
icon: 'i-ri-rocket-line',
|
||||
activeIcon: 'i-ri-rocket-fill',
|
||||
}]
|
||||
: []),
|
||||
], [agentV2Enabled, canUseAppDeploy, isCurrentWorkspaceDatasetOperator, isCurrentWorkspaceEditor, t])
|
||||
const navItems = useMemo<MainNavItem[]>(() => MAIN_NAV_ROUTES
|
||||
.filter(route => isMainNavRouteVisible(route, {
|
||||
agentV2Enabled,
|
||||
canUseAppDeploy,
|
||||
isCurrentWorkspaceDatasetOperator,
|
||||
isCurrentWorkspaceEditor,
|
||||
}))
|
||||
.map(route => ({
|
||||
href: route.href,
|
||||
label: t(route.labelKey, { ns: 'common' }),
|
||||
active: route.active,
|
||||
icon: route.icon,
|
||||
activeIcon: route.activeIcon,
|
||||
})), [agentV2Enabled, canUseAppDeploy, isCurrentWorkspaceDatasetOperator, isCurrentWorkspaceEditor, t])
|
||||
|
||||
const renderLogo = () => {
|
||||
const appTitle = systemFeatures.branding.enabled && systemFeatures.branding.application_title ? systemFeatures.branding.application_title : 'Dify'
|
||||
|
||||
108
web/app/components/main-nav/routes.ts
Normal file
108
web/app/components/main-nav/routes.ts
Normal file
@ -0,0 +1,108 @@
|
||||
import { buildIntegrationPath } from '@/app/components/integrations/routes'
|
||||
|
||||
type MainNavRouteVisibility = 'all' | 'notDatasetOperator' | 'editorOrDatasetOperator' | 'appDeployEditor'
|
||||
|
||||
export type MainNavRouteConfig = {
|
||||
key: string
|
||||
href: string
|
||||
labelKey: string
|
||||
active: (pathname: string) => boolean
|
||||
icon: string
|
||||
activeIcon: string
|
||||
visibility: MainNavRouteVisibility
|
||||
feature?: 'agentV2'
|
||||
}
|
||||
|
||||
export type MainNavRouteVisibilityOptions = {
|
||||
agentV2Enabled: boolean
|
||||
canUseAppDeploy: boolean
|
||||
isCurrentWorkspaceDatasetOperator: boolean
|
||||
isCurrentWorkspaceEditor: boolean
|
||||
}
|
||||
|
||||
function isPathUnderRoute(pathname: string, route: string) {
|
||||
return pathname === route || pathname.startsWith(`${route}/`)
|
||||
}
|
||||
|
||||
export const MAIN_NAV_ROUTES = [
|
||||
{
|
||||
key: 'home',
|
||||
href: '/',
|
||||
labelKey: 'mainNav.home',
|
||||
active: (path: string) => path === '/' || path === '/explore/apps',
|
||||
icon: 'i-custom-vender-main-nav-home',
|
||||
activeIcon: 'i-custom-vender-main-nav-home-active',
|
||||
visibility: 'notDatasetOperator',
|
||||
},
|
||||
{
|
||||
key: 'apps',
|
||||
href: '/apps',
|
||||
labelKey: 'menus.apps',
|
||||
active: (path: string) => isPathUnderRoute(path, '/apps') || isPathUnderRoute(path, '/app') || isPathUnderRoute(path, '/snippets'),
|
||||
icon: 'i-custom-vender-main-nav-studio',
|
||||
activeIcon: 'i-custom-vender-main-nav-studio-active',
|
||||
visibility: 'notDatasetOperator',
|
||||
},
|
||||
{
|
||||
key: 'roster',
|
||||
href: '/roster',
|
||||
labelKey: 'menus.roster',
|
||||
active: (path: string) => isPathUnderRoute(path, '/roster'),
|
||||
icon: 'i-custom-vender-main-nav-roster',
|
||||
activeIcon: 'i-custom-vender-main-nav-roster-active',
|
||||
visibility: 'notDatasetOperator',
|
||||
feature: 'agentV2',
|
||||
},
|
||||
{
|
||||
key: 'datasets',
|
||||
href: '/datasets',
|
||||
labelKey: 'menus.datasets',
|
||||
active: (path: string) => isPathUnderRoute(path, '/datasets'),
|
||||
icon: 'i-custom-vender-main-nav-knowledge',
|
||||
activeIcon: 'i-custom-vender-main-nav-knowledge-active',
|
||||
visibility: 'editorOrDatasetOperator',
|
||||
},
|
||||
{
|
||||
key: 'integrations',
|
||||
href: buildIntegrationPath('provider'),
|
||||
labelKey: 'mainNav.integrations',
|
||||
active: (path: string) => isPathUnderRoute(path, '/integrations') || isPathUnderRoute(path, '/tools'),
|
||||
icon: 'i-custom-vender-main-nav-integrations',
|
||||
activeIcon: 'i-custom-vender-main-nav-integrations-active',
|
||||
visibility: 'notDatasetOperator',
|
||||
},
|
||||
{
|
||||
key: 'marketplace',
|
||||
href: '/marketplace',
|
||||
labelKey: 'mainNav.marketplace',
|
||||
active: (path: string) => isPathUnderRoute(path, '/marketplace') || isPathUnderRoute(path, '/plugins'),
|
||||
icon: 'i-custom-vender-main-nav-marketplace',
|
||||
activeIcon: 'i-custom-vender-main-nav-marketplace-active',
|
||||
visibility: 'all',
|
||||
},
|
||||
{
|
||||
key: 'deployments',
|
||||
href: '/deployments',
|
||||
labelKey: 'menus.deployments',
|
||||
active: (path: string) => isPathUnderRoute(path, '/deployments'),
|
||||
icon: 'i-ri-rocket-line',
|
||||
activeIcon: 'i-ri-rocket-fill',
|
||||
visibility: 'appDeployEditor',
|
||||
},
|
||||
] as const satisfies readonly MainNavRouteConfig[]
|
||||
|
||||
export function isMainNavRouteVisible(route: MainNavRouteConfig, options: MainNavRouteVisibilityOptions) {
|
||||
if (route.feature === 'agentV2' && !options.agentV2Enabled)
|
||||
return false
|
||||
|
||||
if (route.visibility === 'all')
|
||||
return true
|
||||
|
||||
if (route.visibility === 'notDatasetOperator')
|
||||
return !options.isCurrentWorkspaceDatasetOperator
|
||||
|
||||
if (route.visibility === 'editorOrDatasetOperator')
|
||||
return options.isCurrentWorkspaceEditor || options.isCurrentWorkspaceDatasetOperator
|
||||
|
||||
return options.canUseAppDeploy
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user