From dbe9309ab88ef950d224375a1751e1bfdbc1e333 Mon Sep 17 00:00:00 2001 From: CodingOnStar Date: Thu, 30 Jul 2026 20:09:30 +0800 Subject: [PATCH] fix: isolate standalone marketplace shared UI --- .../plugins/card/__tests__/index.spec.tsx | 85 +++++++++++++++++++ web/app/components/plugins/card/index.tsx | 54 ++++++++++-- .../home/__tests__/home-header.spec.tsx | 28 +++++- .../plugins/marketplace/home/home-guide.tsx | 36 ++++++++ .../plugins/marketplace/home/home-header.tsx | 20 +---- 5 files changed, 195 insertions(+), 28 deletions(-) create mode 100644 web/app/components/plugins/card/__tests__/index.spec.tsx create mode 100644 web/app/components/plugins/marketplace/home/home-guide.tsx diff --git a/web/app/components/plugins/card/__tests__/index.spec.tsx b/web/app/components/plugins/card/__tests__/index.spec.tsx new file mode 100644 index 00000000000..fe2b299a111 --- /dev/null +++ b/web/app/components/plugins/card/__tests__/index.spec.tsx @@ -0,0 +1,85 @@ +import type { CardPayload } from '../index' +import { render } from '@testing-library/react' +import { useAtomValue } from 'jotai' +import { describe, expect, it, vi } from 'vitest' +import { MARKETPLACE_API_PREFIX } from '@/config' +import { PluginCategoryEnum } from '../../types' +import Card from '../index' + +vi.mock('jotai', () => ({ + useAtomValue: vi.fn(), +})) + +vi.mock('@/context/workspace-state', () => ({ + currentWorkspaceIdAtom: Symbol('currentWorkspaceIdAtom'), +})) + +vi.mock('#i18n', () => ({ + useTranslation: () => ({ + t: (key: string | ((...args: never[]) => unknown)) => + typeof key === 'string' ? key : 'translated', + }), +})) + +vi.mock('@/context/i18n', () => ({ + useGetLanguage: () => 'en-US', +})) + +vi.mock('@/hooks/use-theme', () => ({ + default: () => ({ theme: 'light' }), +})) + +vi.mock('@/i18n-config', () => ({ + renderI18nObject: (value: Record) => value['en-US'] ?? '', +})) + +vi.mock('../../hooks', () => ({ + useCategories: () => ({ + categoriesMap: { + tool: { label: 'Tool' }, + }, + }), +})) + +const marketplacePlugin = { + badges: [], + brief: { 'en-US': 'Marketplace plugin description' }, + category: PluginCategoryEnum.tool, + description: { 'en-US': 'Marketplace plugin description' }, + endpoint: { settings: [] }, + from: 'marketplace', + icon: 'icon.png', + install_count: 0, + introduction: '', + label: { 'en-US': 'Marketplace plugin' }, + latest_package_identifier: 'langgenius/demo-plugin:1.0.0', + latest_version: '1.0.0', + name: 'demo-plugin', + org: 'langgenius', + plugin_id: 'langgenius/demo-plugin', + repository: '', + tags: [], + type: 'plugin', + verified: false, + verification: { authorized_category: 'langgenius' }, + version: '1.0.0', +} satisfies CardPayload + +describe('Plugin card workspace boundary', () => { + it('renders Marketplace variant icons without reading Dify workspace state', () => { + vi.mocked(useAtomValue).mockImplementation(() => { + throw new Error('Dify workspace state must not be read') + }) + + const payloadWithoutSource = { + ...marketplacePlugin, + from: undefined, + } as unknown as CardPayload + const { container } = render() + + expect(container.querySelector('[style*="background-image"]')).toHaveStyle({ + backgroundImage: `url("${MARKETPLACE_API_PREFIX}/plugins/langgenius/demo-plugin/icon")`, + }) + expect(useAtomValue).not.toHaveBeenCalled() + }) +}) diff --git a/web/app/components/plugins/card/index.tsx b/web/app/components/plugins/card/index.tsx index f5dc18cb55b..977052a8cc1 100644 --- a/web/app/components/plugins/card/index.tsx +++ b/web/app/components/plugins/card/index.tsx @@ -42,6 +42,37 @@ type Props = Readonly<{ variant?: 'default' | 'marketplace' }> +type CardIconProps = { + icon: CardPayload['icon'] + installFailed?: boolean + installed?: boolean + marketplace?: boolean + plugin: Pick +} + +const WorkspaceCardIcon = ({ icon, installFailed, installed, plugin }: CardIconProps) => { + const currentWorkspaceId = useAtomValue(currentWorkspaceIdAtom) + const iconSrc = getPluginCardIconUrl(plugin, icon, currentWorkspaceId) + + return +} + +const CardIcon = ({ icon, installFailed, installed, marketplace, plugin }: CardIconProps) => { + if (marketplace || plugin.from === 'marketplace') { + const iconSrc = getPluginCardIconUrl({ ...plugin, from: 'marketplace' }, icon, '') + return + } + + return ( + + ) +} + const Card = ({ className, payload, @@ -60,15 +91,11 @@ const Card = ({ const locale = useGetLanguage() const { t } = useTranslation() const { categoriesMap } = useCategories(true) - const currentWorkspaceId = useAtomValue(currentWorkspaceIdAtom) const { category, type, name, org, label, brief, icon, icon_dark, verified, from } = payload const badges = payload.badges ?? [] const { theme } = useTheme() - const iconSrc = getPluginCardIconUrl( - { from, name, org, type }, - theme === Theme.dark && icon_dark ? icon_dark : icon, - currentWorkspaceId, - ) + const activeIcon = theme === Theme.dark && icon_dark ? icon_dark : icon + const pluginIdentity = { from, name, org, type } const getLocalizedText = (obj: Record | undefined) => obj ? renderI18nObject(obj, locale) : '' const isPartner = badges.includes('partner') @@ -92,7 +119,13 @@ const Card = ({
{!hideCornerMark && }
- +
@@ -152,7 +185,12 @@ const Card = ({ {!hideCornerMark && } {/* Header */}
- +
diff --git a/web/app/components/plugins/marketplace/home/__tests__/home-header.spec.tsx b/web/app/components/plugins/marketplace/home/__tests__/home-header.spec.tsx index 2fd6a203f47..efe2a36f820 100644 --- a/web/app/components/plugins/marketplace/home/__tests__/home-header.spec.tsx +++ b/web/app/components/plugins/marketplace/home/__tests__/home-header.spec.tsx @@ -1,18 +1,27 @@ import { render, screen } from '@testing-library/react' -import { describe, expect, it, vi } from 'vitest' +import { beforeEach, describe, expect, it, vi } from 'vitest' import HomeHeader from '../home-header' +const mocks = vi.hoisted(() => ({ + useDocLink: vi.fn(() => () => 'https://docs.dify.ai/en/home'), +})) + vi.mock('#i18n', async () => { const { withSelectorKey } = await import('@/test/i18n-mock') return { useTranslation: () => ({ + i18n: { + language: 'en-US', + }, t: withSelectorKey((key: string) => key), }), } }) vi.mock('@/context/i18n', () => ({ - useDocLink: () => () => 'https://docs.dify.ai/en/home', + defaultDocBaseUrl: 'https://docs.dify.ai', + getDocHomePath: () => '/home', + useDocLink: mocks.useDocLink, })) vi.mock('../home-sticky-state-provider', () => ({ @@ -20,6 +29,10 @@ vi.mock('../home-sticky-state-provider', () => ({ })) describe('HomeHeader', () => { + beforeEach(() => { + vi.clearAllMocks() + }) + it('links the Guide action to Dify documentation', () => { render(<HomeHeader isMarketplacePlatform />) @@ -40,6 +53,17 @@ describe('HomeHeader', () => { expect(guideLink).toHaveAttribute('href', 'https://docs.dify.ai/en/home') expect(guideLink).toHaveAttribute('target', '_blank') expect(guideLink).toHaveAttribute('rel', 'noopener noreferrer') + expect(mocks.useDocLink).not.toHaveBeenCalled() + }) + + it('uses the Dify deployment-aware documentation link inside Dify', () => { + render(<HomeHeader isMarketplacePlatform={false} />) + + expect(screen.getByRole('link', { name: 'marketplace.home.guide' })).toHaveAttribute( + 'href', + 'https://docs.dify.ai/en/home', + ) + expect(mocks.useDocLink).toHaveBeenCalledOnce() }) it('shows Templates as the active compact tab on the Templates catalog', () => { diff --git a/web/app/components/plugins/marketplace/home/home-guide.tsx b/web/app/components/plugins/marketplace/home/home-guide.tsx new file mode 100644 index 00000000000..c57881b59d7 --- /dev/null +++ b/web/app/components/plugins/marketplace/home/home-guide.tsx @@ -0,0 +1,36 @@ +'use client' + +import { Button } from '@langgenius/dify-ui/button' +import { useTranslation } from '#i18n' +import { defaultDocBaseUrl, getDocHomePath, useDocLink } from '@/context/i18n' +import { getDocLanguage } from '@/i18n-config/language' +import Link from '@/next/link' +import styles from './home-sticky.module.css' + +function GuideLink({ href }: { href: string }) { + const { t } = useTranslation('plugin') + + return ( + <Link href={href} target="_blank" rel="noopener noreferrer" className={styles.guide}> + <Button variant="ghost" size="large" className="min-w-[94px] gap-0.5 px-3 text-text-primary"> + <span aria-hidden className="i-ri-map-2-line size-5" /> + <span className="px-1 system-md-medium">{t(($) => $['marketplace.home.guide'])}</span> + </Button> + </Link> + ) +} + +function MarketplaceGuide() { + const { i18n } = useTranslation() + const docLanguage = getDocLanguage(i18n.language) + return <GuideLink href={`${defaultDocBaseUrl}/${docLanguage}${getDocHomePath()}`} /> +} + +function DifyGuide() { + const docLink = useDocLink() + return <GuideLink href={docLink()} /> +} + +export default function HomeGuide({ isMarketplacePlatform }: { isMarketplacePlatform: boolean }) { + return isMarketplacePlatform ? <MarketplaceGuide /> : <DifyGuide /> +} diff --git a/web/app/components/plugins/marketplace/home/home-header.tsx b/web/app/components/plugins/marketplace/home/home-header.tsx index 60d9cf6cdc9..992317d1309 100644 --- a/web/app/components/plugins/marketplace/home/home-header.tsx +++ b/web/app/components/plugins/marketplace/home/home-header.tsx @@ -1,12 +1,10 @@ import type { HomeCatalogTab } from './home-catalog-tabs' -import { Button } from '@langgenius/dify-ui/button' import { cn } from '@langgenius/dify-ui/cn' -import { useTranslation } from '#i18n' -import { useDocLink } from '@/context/i18n' import Link from '@/next/link' import MarketplaceLogoDark from '@/public/marketplace/dify-marketplace-logo-dark.svg' import MarketplaceLogo from '@/public/marketplace/dify-marketplace-logo.svg' import HomeCatalogTabs from './home-catalog-tabs' +import HomeGuide from './home-guide' import { HomeStickyCatalogTabs } from './home-sticky-state-provider' import styles from './home-sticky.module.css' @@ -16,20 +14,6 @@ type HomeHeaderProps = { isMarketplacePlatform: boolean } -function Guide() { - const docLink = useDocLink() - const { t } = useTranslation('plugin') - - return ( - <Link href={docLink()} target="_blank" rel="noopener noreferrer" className={styles.guide}> - <Button variant="ghost" size="large" className="min-w-[94px] gap-0.5 px-3 text-text-primary"> - <span aria-hidden className="i-ri-map-2-line size-5" /> - <span className="px-1 system-md-medium">{t(($) => $['marketplace.home.guide'])}</span> - </Button> - </Link> - ) -} - const HomeHeader = ({ activeTab = 'plugins', actions, isMarketplacePlatform }: HomeHeaderProps) => { return ( <header @@ -77,7 +61,7 @@ const HomeHeader = ({ activeTab = 'plugins', actions, isMarketplacePlatform }: H </div> <div className="flex h-full min-w-0 flex-1 items-center justify-end gap-2.5"> - <Guide /> + <HomeGuide isMarketplacePlatform={isMarketplacePlatform} /> {actions} </div> </header>