fix: isolate embedded marketplace client

This commit is contained in:
CodingOnStar 2026-07-28 19:09:07 +08:00
parent aa8f1fcd3a
commit dcb4b4f6af
6 changed files with 254 additions and 63 deletions

View File

@ -1,21 +1,15 @@
import type { SearchParams } from 'nuqs'
import AccountDropdown from '@/app/components/header/account-dropdown'
import Marketplace from '@/app/components/plugins/marketplace'
import { EmbeddedMarketplace } from '@/app/components/plugins/marketplace/embedded'
import MarketplaceInstallPermissionProvider from '@/app/components/plugins/marketplace/marketplace-install-permission-provider'
type MarketplacePageProps = {
searchParams?: Promise<SearchParams>
}
const MarketplacePage = ({ searchParams }: MarketplacePageProps) => {
const MarketplacePage = () => {
return (
<div
id="marketplace-container"
className="flex h-full min-h-0 flex-col overflow-y-auto bg-background-default"
>
<MarketplaceInstallPermissionProvider>
<Marketplace
searchParams={searchParams}
<EmbeddedMarketplace
showInstallButton
variant="home"
homeHeaderActions={(

View File

@ -0,0 +1,89 @@
import type { ReactNode } from 'react'
import type { PluginBanner } from '../home/banners'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { render, screen } from '@testing-library/react'
import { beforeEach, describe, expect, it, vi } from 'vitest'
const mockFetchPluginBanners = vi.fn()
vi.mock('@/context/i18n', () => ({
useLocale: () => 'zh-Hans',
}))
vi.mock('../home/banners', async (importOriginal) => {
const original = await importOriginal<typeof import('../home/banners')>()
return {
...original,
fetchPluginBanners: (...args: unknown[]) => mockFetchPluginBanners(...args),
}
})
vi.mock('../view', () => ({
MarketplaceView: ({
banners,
showInstallButton,
}: {
banners: PluginBanner[]
showInstallButton: boolean
}) => (
<div>
<p>Trending banners: {banners.length}</p>
<p>{showInstallButton ? 'Install enabled' : 'Install disabled'}</p>
</div>
),
}))
let queryClient: QueryClient
function Wrapper({ children }: { children: ReactNode }) {
return <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
}
describe('EmbeddedMarketplace', () => {
beforeEach(() => {
vi.clearAllMocks()
queryClient = new QueryClient({
defaultOptions: {
queries: {
retry: false,
gcTime: 0,
},
},
})
})
it('loads homepage banners on the client for the active locale', async () => {
mockFetchPluginBanners.mockResolvedValue([
{
id: 'banner-1',
title: 'Trending',
sort: 1,
language: 'zh-Hans',
style_type: 'blog',
content: {
blog_title: 'Dify update',
link: 'https://dify.ai/blog',
link_target_type: 'blog',
},
},
] satisfies PluginBanner[])
const { EmbeddedMarketplace } = await import('../embedded')
render(<EmbeddedMarketplace showInstallButton variant="home" />, { wrapper: Wrapper })
expect(await screen.findByText('Trending banners: 1')).toBeInTheDocument()
expect(screen.getByText('Install enabled')).toBeInTheDocument()
expect(mockFetchPluginBanners).toHaveBeenCalledWith('zh-Hans')
})
it('does not request homepage banners for the default catalog variant', async () => {
const { EmbeddedMarketplace } = await import('../embedded')
render(<EmbeddedMarketplace variant="default" />, { wrapper: Wrapper })
expect(screen.getByText('Trending banners: 0')).toBeInTheDocument()
expect(mockFetchPluginBanners).not.toHaveBeenCalled()
})
})

View File

@ -0,0 +1,65 @@
import type { ReactNode } from 'react'
import type { PluginBanner } from '../home/banners'
import { render, screen } from '@testing-library/react'
import { beforeEach, describe, expect, it, vi } from 'vitest'
const { mockFetchPluginBanners, mockGetLocaleOnServer } = vi.hoisted(() => ({
mockFetchPluginBanners: vi.fn(),
mockGetLocaleOnServer: vi.fn(),
}))
vi.mock('@/i18n-config/server', () => ({
getLocaleOnServer: mockGetLocaleOnServer,
}))
vi.mock('../home/banners', async (importOriginal) => {
const original = await importOriginal<typeof import('../home/banners')>()
return {
...original,
fetchPluginBanners: mockFetchPluginBanners,
}
})
vi.mock('../hydration-server', () => ({
HydrateQueryClient: ({ children }: { children: ReactNode }) => children,
}))
vi.mock('../view', () => ({
MarketplaceView: ({ banners }: { banners: PluginBanner[] }) => (
<p>Server banners: {banners.length}</p>
),
}))
describe('Marketplace server entry', () => {
beforeEach(() => {
vi.clearAllMocks()
})
it('prefetches localized homepage banners before rendering the standalone view', async () => {
mockGetLocaleOnServer.mockResolvedValue('en-US')
mockFetchPluginBanners.mockResolvedValue([
{
id: 'banner-1',
title: 'Trending',
sort: 1,
language: 'en-US',
style_type: 'blog',
content: {
blog_title: 'Dify update',
link: 'https://dify.ai/blog',
link_target_type: 'blog',
},
},
] satisfies PluginBanner[])
const { default: Marketplace } = await import('../index')
const element = await Marketplace({ variant: 'home' })
render(element)
expect(screen.getByText('Server banners: 1')).toBeInTheDocument()
expect(mockGetLocaleOnServer).toHaveBeenCalledOnce()
expect(mockFetchPluginBanners).toHaveBeenCalledWith('en-US')
})
})

View File

@ -0,0 +1,29 @@
'use client'
import type { MarketplaceViewProps } from './view'
import { queryOptions, useQuery } from '@tanstack/react-query'
import { useLocale } from '@/context/i18n'
import { marketplaceQuery } from '@/service/client'
import { fetchPluginBanners } from './home/banners'
import { MarketplaceView } from './view'
export type EmbeddedMarketplaceProps = Omit<MarketplaceViewProps, 'banners'>
export function EmbeddedMarketplace({ variant = 'default', ...props }: EmbeddedMarketplaceProps) {
const locale = useLocale()
const input = {
query: {
page: 'plugins' as const,
language: locale,
},
}
const { data: banners = [] } = useQuery(
queryOptions({
queryKey: [...marketplaceQuery.banners.list.queryKey({ input }), locale],
queryFn: () => fetchPluginBanners(locale),
enabled: variant === 'home',
}),
)
return <MarketplaceView {...props} banners={banners} variant={variant} />
}

View File

@ -1,26 +1,13 @@
import type { SearchParams } from 'nuqs'
import type { PluginBanner } from './home/banners'
import { PluginInstallPermissionProviderGuard } from '@/app/components/plugins/install-plugin/components/plugin-install-permission-provider'
import { TanStackQueryProvider } from '@/app/query-provider'
import type { MarketplaceViewProps } from './view'
import { getLocaleOnServer } from '@/i18n-config/server'
import Description from './description'
import MarketplaceHome from './home'
import { fetchPluginBanners } from './home/banners'
import { HydrateQueryClient } from './hydration-server'
import ListWrapper from './list/list-wrapper'
import StickySearchAndSwitchWrapper from './sticky-search-and-switch-wrapper'
import { MarketplaceView } from './view'
type MarketplaceVariant = 'default' | 'home'
type MarketplaceProps = {
showInstallButton?: boolean
linkToMarketplaceDetail?: boolean
pluginTypeSwitchClassName?: string
isMarketplacePlatform?: boolean
marketplaceNav?: React.ReactNode
variant?: MarketplaceVariant
type MarketplaceProps = Omit<MarketplaceViewProps, 'banners'> & {
language?: string
homeHeaderActions?: React.ReactNode
/**
* Pass the search params from the request to prefetch data on the server.
*/
@ -28,15 +15,10 @@ type MarketplaceProps = {
}
const Marketplace = async ({
showInstallButton = false,
linkToMarketplaceDetail = false,
pluginTypeSwitchClassName,
isMarketplacePlatform = false,
marketplaceNav,
variant = 'default',
language,
homeHeaderActions,
searchParams,
variant = 'default',
...viewProps
}: MarketplaceProps) => {
let trendingBanners: PluginBanner[] = []
@ -51,37 +33,9 @@ const Marketplace = async ({
}
return (
<TanStackQueryProvider>
<HydrateQueryClient searchParams={searchParams}>
<PluginInstallPermissionProviderGuard canInstallPlugin={showInstallButton}>
{variant === 'home' ? (
<MarketplaceHome
actions={homeHeaderActions}
banners={trendingBanners}
isMarketplacePlatform={isMarketplacePlatform}
linkToMarketplaceDetail={linkToMarketplaceDetail}
showInstallButton={showInstallButton}
/>
) : (
<>
<Description
isMarketplacePlatform={isMarketplacePlatform}
marketplaceNav={marketplaceNav}
/>
{!isMarketplacePlatform && (
<StickySearchAndSwitchWrapper
pluginTypeSwitchClassName={pluginTypeSwitchClassName}
/>
)}
<ListWrapper
showInstallButton={showInstallButton}
linkToMarketplaceDetail={linkToMarketplaceDetail}
/>
</>
)}
</PluginInstallPermissionProviderGuard>
</HydrateQueryClient>
</TanStackQueryProvider>
<HydrateQueryClient searchParams={searchParams}>
<MarketplaceView {...viewProps} banners={trendingBanners} variant={variant} />
</HydrateQueryClient>
)
}

View File

@ -0,0 +1,60 @@
import type { PluginBanner } from './home/banners'
import { PluginInstallPermissionProviderGuard } from '@/app/components/plugins/install-plugin/components/plugin-install-permission-provider'
import Description from './description'
import MarketplaceHome from './home'
import ListWrapper from './list/list-wrapper'
import StickySearchAndSwitchWrapper from './sticky-search-and-switch-wrapper'
export type MarketplaceVariant = 'default' | 'home'
export type MarketplaceViewProps = {
banners: PluginBanner[]
showInstallButton?: boolean
linkToMarketplaceDetail?: boolean
pluginTypeSwitchClassName?: string
isMarketplacePlatform?: boolean
marketplaceNav?: React.ReactNode
variant?: MarketplaceVariant
homeHeaderActions?: React.ReactNode
}
export function MarketplaceView({
banners,
showInstallButton = false,
linkToMarketplaceDetail = false,
pluginTypeSwitchClassName,
isMarketplacePlatform = false,
marketplaceNav,
variant = 'default',
homeHeaderActions,
}: MarketplaceViewProps) {
return (
<PluginInstallPermissionProviderGuard canInstallPlugin={showInstallButton}>
{variant === 'home' ? (
<MarketplaceHome
actions={homeHeaderActions}
banners={banners}
isMarketplacePlatform={isMarketplacePlatform}
linkToMarketplaceDetail={linkToMarketplaceDetail}
showInstallButton={showInstallButton}
/>
) : (
<>
<Description
isMarketplacePlatform={isMarketplacePlatform}
marketplaceNav={marketplaceNav}
/>
{!isMarketplacePlatform && (
<StickySearchAndSwitchWrapper
pluginTypeSwitchClassName={pluginTypeSwitchClassName}
/>
)}
<ListWrapper
showInstallButton={showInstallButton}
linkToMarketplaceDetail={linkToMarketplaceDetail}
/>
</>
)}
</PluginInstallPermissionProviderGuard>
)
}