mirror of
https://github.com/langgenius/dify.git
synced 2026-09-05 08:48:10 +08:00
fix: isolate embedded marketplace client
This commit is contained in:
parent
aa8f1fcd3a
commit
dcb4b4f6af
@ -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={(
|
||||
|
||||
@ -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()
|
||||
})
|
||||
})
|
||||
@ -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')
|
||||
})
|
||||
})
|
||||
29
web/app/components/plugins/marketplace/embedded.tsx
Normal file
29
web/app/components/plugins/marketplace/embedded.tsx
Normal 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} />
|
||||
}
|
||||
@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
60
web/app/components/plugins/marketplace/view.tsx
Normal file
60
web/app/components/plugins/marketplace/view.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user