feat: explore page to home page

This commit is contained in:
Joel 2026-05-11 15:13:01 +08:00
parent c67235ca8c
commit b20de089ce
11 changed files with 51 additions and 45 deletions

View File

@ -1,8 +1,23 @@
import * as React from 'react'
import AppList from '@/app/components/explore/app-list'
import { redirect } from '@/next/navigation'
const Apps = () => {
return <AppList />
type AppsPageProps = {
searchParams: Promise<Record<string, string | string[] | undefined>>
}
export default React.memo(Apps)
const Apps = async ({ searchParams }: AppsPageProps) => {
const resolvedSearchParams = await searchParams
const urlSearchParams = new URLSearchParams()
Object.entries(resolvedSearchParams).forEach(([key, value]) => {
if (value === undefined)
return
if (Array.isArray(value)) {
value.forEach(item => urlSearchParams.append(key, item))
return
}
urlSearchParams.set(key, value)
})
const queryString = urlSearchParams.toString()
redirect(queryString ? `/?${queryString}` : '/')
}
export default Apps

View File

@ -0,0 +1,8 @@
import * as React from 'react'
import AppList from '@/app/components/explore/app-list'
const Home = () => {
return <AppList />
}
export default React.memo(Home)

View File

@ -131,7 +131,7 @@ const CreateAppCard = ({
setShowCreateFromDSLModal(false)
if (dslUrl)
replace('/')
replace('/apps')
}}
activeTab={activeTab}
dslUrl={dslUrl}

View File

@ -18,6 +18,7 @@ let mockInstalledApps: InstalledApp[] = []
let mockMediaType: string = MediaType.pc
vi.mock('@/next/navigation', () => ({
usePathname: () => '/',
useSelectedLayoutSegments: () => mockSegments,
useRouter: () => ({
push: mockPush,

View File

@ -18,7 +18,7 @@ import { useTranslation } from 'react-i18next'
import Divider from '@/app/components/base/divider'
import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints'
import Link from '@/next/link'
import { useSelectedLayoutSegments } from '@/next/navigation'
import { usePathname, useSelectedLayoutSegments } from '@/next/navigation'
import { useGetInstalledApps, useUninstallApp, useUpdateAppPinStatus } from '@/service/use-explore'
import Item from './app-nav-item'
import NoApps from './no-apps'
@ -31,9 +31,10 @@ const expandedSidebarScrollAreaClassNames = {
const SideBar = () => {
const { t } = useTranslation()
const pathname = usePathname()
const segments = useSelectedLayoutSegments()
const lastSegment = segments.slice(-1)[0]
const isDiscoverySelected = lastSegment === 'apps'
const isDiscoverySelected = pathname === '/' || lastSegment === 'apps'
const { data, isPending } = useGetInstalledApps()
const installedApps = data?.installed_apps ?? []
const { mutateAsync: uninstallApp, isPending: isUninstalling } = useUninstallApp()
@ -89,7 +90,7 @@ const SideBar = () => {
<div className={cn('flex h-full w-fit shrink-0 cursor-pointer flex-col px-3 pt-6 sm:w-[240px]', isFold && 'sm:w-[56px]')}>
<div className={cn(isDiscoverySelected ? 'text-text-accent' : 'text-text-tertiary')}>
<Link
href="/explore/apps"
href="/"
aria-label={isMobile || isFold ? t('sidebar.title', { ns: 'explore' }) : undefined}
className={cn(isDiscoverySelected ? 'bg-state-base-active' : 'hover:bg-state-base-hover', 'flex h-8 items-center gap-2 rounded-lg px-1 mobile:w-fit mobile:justify-center pc:w-full pc:justify-start')}
>

View File

@ -1,15 +1,17 @@
import type { Mock } from 'vitest'
import { render, screen } from '@testing-library/react'
import { useSelectedLayoutSegment } from '@/next/navigation'
import { usePathname, useSelectedLayoutSegment } from '@/next/navigation'
import ExploreNav from '../index'
vi.mock('@/next/navigation', () => ({
usePathname: vi.fn(),
useSelectedLayoutSegment: vi.fn(),
}))
describe('ExploreNav', () => {
beforeEach(() => {
vi.clearAllMocks()
;(usePathname as Mock).mockReturnValue('/apps')
})
it('should render correctly when not active', () => {
@ -18,14 +20,15 @@ describe('ExploreNav', () => {
const link = screen.getByRole('link')
expect(link).toBeInTheDocument()
expect(link).toHaveAttribute('href', '/explore/apps')
expect(link).toHaveAttribute('href', '/')
expect(link).toHaveClass('text-components-main-nav-nav-button-text')
expect(link).not.toHaveClass('bg-components-main-nav-nav-button-bg-active')
expect(screen.getByText('common.menus.explore')).toBeInTheDocument()
})
it('should render correctly when active', () => {
(useSelectedLayoutSegment as Mock).mockReturnValue('explore')
;(usePathname as Mock).mockReturnValue('/')
;(useSelectedLayoutSegment as Mock).mockReturnValue('explore')
render(<ExploreNav />)
const link = screen.getByRole('link')

View File

@ -7,7 +7,7 @@ import {
} from '@remixicon/react'
import { useTranslation } from 'react-i18next'
import Link from '@/next/link'
import { useSelectedLayoutSegment } from '@/next/navigation'
import { usePathname, useSelectedLayoutSegment } from '@/next/navigation'
type ExploreNavProps = {
className?: string
@ -17,12 +17,13 @@ const ExploreNav = ({
className,
}: ExploreNavProps) => {
const { t } = useTranslation()
const pathname = usePathname()
const selectedSegment = useSelectedLayoutSegment()
const activated = selectedSegment === 'explore'
const activated = pathname === '/' || selectedSegment === 'explore'
return (
<Link
href="/explore/apps"
href="/"
className={cn(className, 'group', activated && 'bg-components-main-nav-nav-button-bg-active shadow-md', activated ? 'text-components-main-nav-nav-button-text-active' : 'text-components-main-nav-nav-button-text hover:bg-components-main-nav-nav-button-bg-hover')}
>
{

View File

@ -211,7 +211,7 @@ describe('MainNav', () => {
expect(screen.getAllByText(Plan.team)).toHaveLength(1)
expect(screen.getByRole('button', { name: 'common.account.account' })).not.toHaveTextContent(Plan.team)
expect(screen.getByRole('link', { name: /common.mainNav.home/ })).toHaveAttribute('href', '/explore/apps')
expect(screen.getByRole('link', { name: /common.mainNav.home/ })).toHaveAttribute('href', '/')
expect(screen.getByRole('link', { name: /common.menus.apps/ })).toHaveAttribute('href', '/apps')
expect(screen.getByRole('link', { name: /common.menus.datasets/ })).toHaveAttribute('href', '/datasets')
expect(screen.getByRole('link', { name: /common.mainNav.integrations/ })).toHaveAttribute('href', '/tools?section=provider')
@ -314,7 +314,7 @@ describe('MainNav', () => {
})
it('applies the Figma glass active state to the Home route', () => {
mockPathname = '/explore/apps'
mockPathname = '/'
renderMainNav()

View File

@ -30,9 +30,9 @@ const MainNav = ({
...(!isCurrentWorkspaceDatasetOperator
? [
{
href: '/explore/apps',
href: '/',
label: t('mainNav.home', { ns: 'common' }),
active: (path: string) => path.startsWith('/explore'),
active: (path: string) => path === '/' || path.startsWith('/explore'),
icon: 'i-custom-vender-main-nav-home',
activeIcon: 'i-custom-vender-main-nav-home-active',
},
@ -78,7 +78,7 @@ const MainNav = ({
const renderLogo = () => (
<h1 className="min-w-0">
<Link href={isCurrentWorkspaceDatasetOperator ? '/datasets' : '/apps'} className="flex h-8 shrink-0 items-center overflow-hidden px-2 indent-[-9999px] whitespace-nowrap">
<Link href="/" className="flex h-8 shrink-0 items-center overflow-hidden px-2 indent-[-9999px] whitespace-nowrap">
{systemFeatures.branding.enabled && systemFeatures.branding.application_title ? systemFeatures.branding.application_title : 'Dify'}
{systemFeatures.branding.enabled && systemFeatures.branding.workspace_logo
? (

View File

@ -1,23 +0,0 @@
import { redirect } from '@/next/navigation'
type HomePageProps = {
searchParams: Promise<Record<string, string | string[] | undefined>>
}
const Home = async ({ searchParams }: HomePageProps) => {
const resolvedSearchParams = await searchParams
const urlSearchParams = new URLSearchParams()
Object.entries(resolvedSearchParams).forEach(([key, value]) => {
if (value === undefined)
return
if (Array.isArray(value)) {
value.forEach(item => urlSearchParams.append(key, item))
return
}
urlSearchParams.set(key, value)
})
const queryString = urlSearchParams.toString()
redirect(queryString ? `/apps?${queryString}` : '/apps')
}
export default Home

View File

@ -28,8 +28,8 @@ const nextConfig: NextConfig = {
async redirects() {
return [
{
source: '/',
destination: '/apps',
source: '/explore/apps',
destination: '/',
permanent: false,
},
]