diff --git a/web/app/(commonLayout)/marketplace/page.tsx b/web/app/(commonLayout)/marketplace/page.tsx index 8510df3c382..c3b4d5cf2e8 100644 --- a/web/app/(commonLayout)/marketplace/page.tsx +++ b/web/app/(commonLayout)/marketplace/page.tsx @@ -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 -} - -const MarketplacePage = ({ searchParams }: MarketplacePageProps) => { +const MarketplacePage = () => { return (
- ({ + useLocale: () => 'zh-Hans', +})) + +vi.mock('../home/banners', async (importOriginal) => { + const original = await importOriginal() + + return { + ...original, + fetchPluginBanners: (...args: unknown[]) => mockFetchPluginBanners(...args), + } +}) + +vi.mock('../view', () => ({ + MarketplaceView: ({ + banners, + showInstallButton, + }: { + banners: PluginBanner[] + showInstallButton: boolean + }) => ( +
+

Trending banners: {banners.length}

+

{showInstallButton ? 'Install enabled' : 'Install disabled'}

+
+ ), +})) + +let queryClient: QueryClient + +function Wrapper({ children }: { children: ReactNode }) { + return {children} +} + +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(, { 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(, { wrapper: Wrapper }) + + expect(screen.getByText('Trending banners: 0')).toBeInTheDocument() + expect(mockFetchPluginBanners).not.toHaveBeenCalled() + }) +}) diff --git a/web/app/components/plugins/marketplace/__tests__/server-entry.spec.tsx b/web/app/components/plugins/marketplace/__tests__/server-entry.spec.tsx new file mode 100644 index 00000000000..5712fb398be --- /dev/null +++ b/web/app/components/plugins/marketplace/__tests__/server-entry.spec.tsx @@ -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() + + return { + ...original, + fetchPluginBanners: mockFetchPluginBanners, + } +}) + +vi.mock('../hydration-server', () => ({ + HydrateQueryClient: ({ children }: { children: ReactNode }) => children, +})) + +vi.mock('../view', () => ({ + MarketplaceView: ({ banners }: { banners: PluginBanner[] }) => ( +

Server banners: {banners.length}

+ ), +})) + +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') + }) +}) diff --git a/web/app/components/plugins/marketplace/embedded.tsx b/web/app/components/plugins/marketplace/embedded.tsx new file mode 100644 index 00000000000..ac2256a4e45 --- /dev/null +++ b/web/app/components/plugins/marketplace/embedded.tsx @@ -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 + +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 +} diff --git a/web/app/components/plugins/marketplace/index.tsx b/web/app/components/plugins/marketplace/index.tsx index 4f1258c39e2..da9a3e0e2c2 100644 --- a/web/app/components/plugins/marketplace/index.tsx +++ b/web/app/components/plugins/marketplace/index.tsx @@ -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 & { 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 ( - - - - {variant === 'home' ? ( - - ) : ( - <> - - {!isMarketplacePlatform && ( - - )} - - - )} - - - + + + ) } diff --git a/web/app/components/plugins/marketplace/view.tsx b/web/app/components/plugins/marketplace/view.tsx new file mode 100644 index 00000000000..a77da4cf0c8 --- /dev/null +++ b/web/app/components/plugins/marketplace/view.tsx @@ -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 ( + + {variant === 'home' ? ( + + ) : ( + <> + + {!isMarketplacePlatform && ( + + )} + + + )} + + ) +}