mirror of
https://github.com/langgenius/dify.git
synced 2026-10-04 13:47:55 +08:00
feat(marketplace): port stable template UUID links to dev
Port 7f7e909826 while preserving deploy/dev Banner fallbacks and embedded navigation guards. Align the existing Banner call and tests with its unchanged analytics payload contract so the dev static gate passes.
This commit is contained in:
1 parent
0fb011126e
commit
ba7df082bc
8 files changed
+148
-69
No files matched your search
@@ -13,7 +13,13 @@ vi.mock('@/config', () => ({
|
||||
|
||||
// Mock var utils
|
||||
vi.mock('@/utils/var', () => ({
|
||||
getMarketplaceUrl: (path: string) => `https://marketplace.dify.ai${path}`,
|
||||
getMarketplaceUrl: (path: string, params?: Record<string, string | undefined>) => {
|
||||
const url = new URL(path, 'https://marketplace.dify.ai')
|
||||
for (const [key, value] of Object.entries(params ?? {})) {
|
||||
if (value !== undefined) url.searchParams.set(key, value)
|
||||
}
|
||||
return url.toString()
|
||||
},
|
||||
}))
|
||||
|
||||
// Mock marketplace client
|
||||
@@ -139,30 +145,68 @@ describe('getPluginDetailLinkInMarketplace', () => {
|
||||
})
|
||||
|
||||
describe('getTemplateDetailLinkInMarketplace', () => {
|
||||
it('should return the local template detail link', async () => {
|
||||
it('keeps the same local link after a title change and prefers the unique handle', async () => {
|
||||
const { getTemplateDetailLinkInMarketplace } = await import('../utils')
|
||||
const template = {
|
||||
id: '5d107b52-8d4d-447b-9bad-1c5c16b01721',
|
||||
template_name: 'Legal Research Agent',
|
||||
publisher_handle: 'Dify Display Name',
|
||||
publisher_unique_handle: 'dify-unique',
|
||||
}
|
||||
|
||||
expect(
|
||||
getTemplateDetailLinkInMarketplace({
|
||||
id: 'template-1',
|
||||
template_name: 'Legal Research Agent',
|
||||
publisher_handle: 'dify',
|
||||
publisher_unique_handle: 'dify-unique',
|
||||
}),
|
||||
).toBe('/template/dify/Legal%20Research%20Agent?templateId=template-1')
|
||||
const link = getTemplateDetailLinkInMarketplace(template)
|
||||
expect(link).toBe('/template/dify-unique/5d107b52-8d4d-447b-9bad-1c5c16b01721')
|
||||
const renamedTemplate = { ...template, template_name: 'Renamed research agent' }
|
||||
expect(getTemplateDetailLinkInMarketplace(renamedTemplate)).toBe(link)
|
||||
})
|
||||
|
||||
it('should fall back to the unique publisher handle', async () => {
|
||||
it('encodes both path segments and falls back to the publisher handle', async () => {
|
||||
const { getTemplateDetailLinkInMarketplace } = await import('../utils')
|
||||
|
||||
expect(
|
||||
getTemplateDetailLinkInMarketplace({
|
||||
id: 'template-2',
|
||||
template_name: 'Inbox',
|
||||
publisher_handle: '',
|
||||
publisher_unique_handle: 'langgenius',
|
||||
id: 'template/one',
|
||||
publisher_handle: 'Research / 团队',
|
||||
publisher_unique_handle: '',
|
||||
}),
|
||||
).toBe('/template/langgenius/Inbox?templateId=template-2')
|
||||
).toBe('/template/Research%20%2F%20%E5%9B%A2%E9%98%9F/template%2Fone')
|
||||
})
|
||||
|
||||
it('uses the template fallback when publisher handles are absent', async () => {
|
||||
const { getTemplateDetailLinkInMarketplace } = await import('../utils')
|
||||
|
||||
expect(getTemplateDetailLinkInMarketplace({ id: 'template-2' })).toBe(
|
||||
'/template/template/template-2',
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('getTemplateLinkInMarketplace', () => {
|
||||
it('preserves embedded context while dropping legacy identity parameters', async () => {
|
||||
const { getTemplateLinkInMarketplace } = await import('../utils')
|
||||
|
||||
const link = new URL(
|
||||
getTemplateLinkInMarketplace(
|
||||
{ id: 'template-1', publisher_handle: 'Display Name', publisher_unique_handle: 'dify' },
|
||||
{
|
||||
tid: 'legacy-short-id',
|
||||
templateId: 'legacy-id',
|
||||
creationType: 'templates',
|
||||
language: 'zh-Hans',
|
||||
theme: 'dark',
|
||||
source: 'https://cloud.dify.ai',
|
||||
view: 'modal',
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
expect(link.origin + link.pathname).toBe('https://marketplace.dify.ai/template/dify/template-1')
|
||||
expect(Object.fromEntries(link.searchParams)).toEqual({
|
||||
language: 'zh-Hans',
|
||||
theme: 'dark',
|
||||
source: 'https://cloud.dify.ai',
|
||||
view: 'modal',
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import type {
|
||||
} from '@dify/contracts/marketplace'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import {
|
||||
adaptCreations,
|
||||
adaptCreatorProfile,
|
||||
getStandaloneCreationHref,
|
||||
normalizeCreatorSocialLink,
|
||||
@@ -41,6 +42,7 @@ const template = {
|
||||
icon_background: '#fff',
|
||||
icon_file_key: '',
|
||||
publisher_unique_handle: 'dify',
|
||||
publisher_handle: 'Dify Display Name',
|
||||
usage_count: 10,
|
||||
categories: [],
|
||||
deps_plugins: ['dify/search'],
|
||||
@@ -100,8 +102,35 @@ describe('creator profile model', () => {
|
||||
'/bundles/dify/research?language=zh-Hans',
|
||||
)
|
||||
expect(getStandaloneCreationHref(viewModel.creations[1]!, 'zh-Hans')).toBe(
|
||||
'/template/dify/Research%20template?templateId=template%2Fone&creationType=templates&language=zh-Hans',
|
||||
'/template/dify/template%2Fone?language=zh-Hans',
|
||||
)
|
||||
expect(getStandaloneCreationHref(viewModel.creations[1]!)).toBe('/template/dify/template%2Fone')
|
||||
})
|
||||
|
||||
it('uses stable template links for publisher handle fallbacks without exposing creator email', () => {
|
||||
const creations = adaptCreations({
|
||||
locale: 'en-US',
|
||||
plugins: [],
|
||||
templates: [
|
||||
{ ...template, publisher_unique_handle: '', publisher_handle: '研究 / Team' },
|
||||
{
|
||||
...template,
|
||||
publisher_unique_handle: '',
|
||||
publisher_handle: '',
|
||||
creator_email: 'private@example.com',
|
||||
},
|
||||
{ ...template, template_name: 'Renamed research template' },
|
||||
],
|
||||
resolvePluginIcon: () => '',
|
||||
resolveTemplateIcon: () => '',
|
||||
resolveDependencyIcon: () => '',
|
||||
})
|
||||
|
||||
expect(creations.map((creation) => getStandaloneCreationHref(creation))).toEqual([
|
||||
'/template/%E7%A0%94%E7%A9%B6%20%2F%20Team/template%2Fone',
|
||||
'/template/template/template%2Fone',
|
||||
'/template/dify/template%2Fone',
|
||||
])
|
||||
})
|
||||
|
||||
it('normalizes Unix-second, Unix-millisecond, and ISO timestamps', () => {
|
||||
|
||||
@@ -261,11 +261,7 @@ export const adaptCreations = ({
|
||||
const dependencyIds = (template.deps_plugins ?? []).filter(
|
||||
(id): id is string => typeof id === 'string' && id.length > 0,
|
||||
)
|
||||
const publisher =
|
||||
template.publisher_handle ||
|
||||
template.publisher_unique_handle ||
|
||||
template.creator_email ||
|
||||
'template'
|
||||
const publisher = template.publisher_unique_handle || template.publisher_handle || 'template'
|
||||
|
||||
return {
|
||||
id: `template:${template.id}`,
|
||||
@@ -352,10 +348,6 @@ export const getStandaloneCreationHref = (creation: CreatorCreation, locale?: st
|
||||
return language ? `${path}?${language}` : path
|
||||
}
|
||||
|
||||
const params = new URLSearchParams({
|
||||
templateId: creation.target.id,
|
||||
creationType: 'templates',
|
||||
})
|
||||
if (locale) params.set('language', locale)
|
||||
return `/template/${encodeURIComponent(creation.target.publisher)}/${encodeURIComponent(creation.target.templateName)}?${params.toString()}`
|
||||
const path = `/template/${encodeURIComponent(creation.target.publisher)}/${encodeURIComponent(creation.target.id)}`
|
||||
return language ? `${path}?${language}` : path
|
||||
}
|
||||
@@ -791,7 +791,6 @@ describe('HomeTrending', () => {
|
||||
click_target: 'recommendation',
|
||||
item_id: 'langgenius/dropbox',
|
||||
item_type: 'plugin',
|
||||
item_name: 'Dropbox',
|
||||
}),
|
||||
)
|
||||
})
|
||||
@@ -838,7 +837,6 @@ describe('HomeTrending', () => {
|
||||
click_target: 'recommendation',
|
||||
item_id: 'tpl-1',
|
||||
item_type: 'template',
|
||||
item_name: 'Support Bot',
|
||||
}),
|
||||
)
|
||||
})
|
||||
@@ -872,7 +870,7 @@ describe('HomeTrending', () => {
|
||||
expect(screen.getByRole('dialog', { name: 'plugin-detail' })).toHaveTextContent('baserow')
|
||||
})
|
||||
|
||||
it('shows the served author on recommend template cards the same way as plugins', () => {
|
||||
it('links recommend template cards by ID with the served author or template fallback', () => {
|
||||
const banner: PluginBanner = {
|
||||
id: 'recommend-templates',
|
||||
style_type: 'recommend',
|
||||
@@ -906,6 +904,8 @@ describe('HomeTrending', () => {
|
||||
const authored = screen.getByRole('link', { name: 'Go-to-Market' })
|
||||
const anonymous = screen.getByRole('link', { name: 'Untitled Flow' })
|
||||
|
||||
expect(authored).toHaveAttribute('href', '/template/aisa-team/tpl-authored')
|
||||
expect(anonymous).toHaveAttribute('href', '/template/template/tpl-anonymous')
|
||||
expect(
|
||||
within(authored).getByText('plugin.marketplace.home.trendingByCreator'),
|
||||
).toBeInTheDocument()
|
||||
@@ -978,7 +978,6 @@ describe('HomeTrending', () => {
|
||||
click_target: 'recommendation',
|
||||
item_id: 'langgenius/dropbox',
|
||||
item_type: 'plugin',
|
||||
item_name: 'Dropbox',
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
+1
-3
@@ -139,9 +139,7 @@ describe('MarketplaceSearchAutocomplete', () => {
|
||||
expect(screen.getByText('Research legal questions with cited sources.')).toBeInTheDocument()
|
||||
|
||||
await user.click(screen.getByText('Legal Research Agent'))
|
||||
expect(mockAssign).toHaveBeenCalledWith(
|
||||
'/template/dify/Legal%20Research%20Agent?templateId=template-1',
|
||||
)
|
||||
expect(mockAssign).toHaveBeenCalledWith('/template/dify/template-1')
|
||||
|
||||
expect(container.querySelector('form')).toHaveAttribute('action', '/templates/knowledge')
|
||||
expect(container.querySelector('input[role="combobox"]')).toHaveAttribute('name', 'q')
|
||||
|
||||
@@ -29,7 +29,11 @@ import {
|
||||
} from '@/utils/marketplace-site-track'
|
||||
import MarketplaceDetailDialog from '../detail-dialog'
|
||||
import TemplateDetailDialog from '../templates/template-detail-dialog'
|
||||
import { getPluginLinkInMarketplace, getTemplateLinkInMarketplace } from '../utils'
|
||||
import {
|
||||
getPluginLinkInMarketplace,
|
||||
getTemplateDetailLinkInMarketplace,
|
||||
getTemplateLinkInMarketplace,
|
||||
} from '../utils'
|
||||
import background from './assets/background.webp'
|
||||
import difyUpdatesArt from './assets/dify-updates-art.png'
|
||||
import {
|
||||
@@ -147,7 +151,11 @@ const getLocalCardHref = (card: BannerRecommendCard) => {
|
||||
return `/plugin/${encodeURIComponent(identity.org)}/${encodeURIComponent(identity.name)}`
|
||||
}
|
||||
|
||||
if (card.item_type === 'template') return `/templates?tid=${encodeURIComponent(card.item_id)}`
|
||||
if (card.item_type === 'template')
|
||||
return getTemplateDetailLinkInMarketplace({
|
||||
id: card.item_id,
|
||||
publisher_unique_handle: card.creator || 'template',
|
||||
})
|
||||
|
||||
return '/'
|
||||
}
|
||||
@@ -251,7 +259,6 @@ function trackRecommendCardClick(
|
||||
trackMarketplaceBannerClick(banner, {
|
||||
item_id: card.item_id,
|
||||
item_type: card.item_type,
|
||||
display_name: card.display_name,
|
||||
link: href,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import type { MarketplaceTemplate } from '@dify/contracts/marketplace'
|
||||
import type { Window } from 'happy-dom'
|
||||
import { fireEvent, render, screen } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import { ThemeProvider } from 'next-themes'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import TemplateCard from '../template-card'
|
||||
|
||||
const { mockPush } = vi.hoisted(() => ({
|
||||
@@ -13,12 +13,13 @@ vi.mock('@/next/navigation', () => ({
|
||||
useRouter: () => ({ push: mockPush }),
|
||||
}))
|
||||
|
||||
vi.mock('../../utils', () => ({
|
||||
getTemplateLinkInMarketplace: (
|
||||
currentTemplate: MarketplaceTemplate,
|
||||
params: { language: string; source?: string; theme?: string; view: string },
|
||||
) =>
|
||||
`about:blank?templateId=${currentTemplate.id}&language=${params.language}&source=${params.source}&theme=${params.theme}&view=${params.view}`,
|
||||
vi.mock('next-themes', () => ({
|
||||
useTheme: () => ({ resolvedTheme: 'dark' }),
|
||||
}))
|
||||
|
||||
vi.mock('@/config', async (importOriginal) => ({
|
||||
...(await importOriginal<typeof import('@/config')>()),
|
||||
MARKETPLACE_URL_PREFIX: 'https://marketplace.example.com',
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/base/app-icon', () => ({
|
||||
@@ -39,17 +40,24 @@ const template: MarketplaceTemplate = {
|
||||
}
|
||||
|
||||
describe('TemplateCard', () => {
|
||||
const navigationSettings = (window as unknown as Window).happyDOM.settings.navigation
|
||||
const originalDisableChildFrameNavigation = navigationSettings.disableChildFrameNavigation
|
||||
|
||||
beforeAll(() => {
|
||||
navigationSettings.disableChildFrameNavigation = true
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
navigationSettings.disableChildFrameNavigation = originalDisableChildFrameNavigation
|
||||
})
|
||||
|
||||
it('opens template detail before starting the Dify import flow', async () => {
|
||||
const user = userEvent.setup()
|
||||
render(
|
||||
<ThemeProvider forcedTheme="dark">
|
||||
<TemplateCard partnerText="Verified by a Dify partner" template={template} />
|
||||
</ThemeProvider>,
|
||||
)
|
||||
render(<TemplateCard partnerText="Verified by a Dify partner" template={template} />)
|
||||
|
||||
expect(screen.queryByRole('link', { name: 'Campaign planner' })).not.toBeInTheDocument()
|
||||
await user.click(screen.getByRole('button', { name: 'Campaign planner' }))
|
||||
@@ -60,7 +68,15 @@ describe('TemplateCard', () => {
|
||||
const frame = screen.getByTitle(
|
||||
'Campaign planner · plugin.detailPanel.operation.detail',
|
||||
) as HTMLIFrameElement
|
||||
const marketplaceOrigin = new URL(frame.getAttribute('src')!, window.location.href).origin
|
||||
const frameURL = new URL(frame.getAttribute('src')!, window.location.href)
|
||||
expect(frameURL.pathname).toBe('/template/dify/template%2Fone')
|
||||
expect(Object.fromEntries(frameURL.searchParams)).toEqual({
|
||||
language: 'en-US',
|
||||
source: window.location.origin,
|
||||
theme: 'dark',
|
||||
view: 'modal',
|
||||
})
|
||||
const marketplaceOrigin = frameURL.origin
|
||||
const installRequest = {
|
||||
type: 'dify-marketplace:install-template',
|
||||
templateId: template.id,
|
||||
|
||||
@@ -111,31 +111,25 @@ export const getPluginDetailLinkInMarketplace = (
|
||||
}
|
||||
|
||||
export const getTemplateDetailLinkInMarketplace = (
|
||||
template: Pick<
|
||||
MarketplaceTemplate,
|
||||
'id' | 'publisher_handle' | 'publisher_unique_handle' | 'template_name'
|
||||
>,
|
||||
template: Pick<MarketplaceTemplate, 'id' | 'publisher_handle' | 'publisher_unique_handle'>,
|
||||
) => {
|
||||
const publisher = template.publisher_handle || template.publisher_unique_handle || 'template'
|
||||
const search = new URLSearchParams({ templateId: template.id })
|
||||
const publisher = template.publisher_unique_handle || template.publisher_handle || 'template'
|
||||
|
||||
return `/template/${encodeURIComponent(publisher)}/${encodeURIComponent(template.template_name)}?${search.toString()}`
|
||||
return `/template/${encodeURIComponent(publisher)}/${encodeURIComponent(template.id)}`
|
||||
}
|
||||
|
||||
export const getTemplateLinkInMarketplace = (
|
||||
template: Pick<
|
||||
MarketplaceTemplate,
|
||||
'id' | 'publisher_handle' | 'publisher_unique_handle' | 'template_name'
|
||||
>,
|
||||
template: Pick<MarketplaceTemplate, 'id' | 'publisher_handle' | 'publisher_unique_handle'>,
|
||||
params?: Record<string, string | undefined>,
|
||||
) => {
|
||||
const publisher = template.publisher_handle || template.publisher_unique_handle || 'template'
|
||||
const path = `/template/${encodeURIComponent(publisher)}/${encodeURIComponent(template.template_name)}`
|
||||
const {
|
||||
tid: _tid,
|
||||
templateId: _templateId,
|
||||
creationType: _creationType,
|
||||
...detailParams
|
||||
} = params ?? {}
|
||||
|
||||
return getMarketplaceUrl(path, {
|
||||
...params,
|
||||
templateId: template.id,
|
||||
})
|
||||
return getMarketplaceUrl(getTemplateDetailLinkInMarketplace(template), detailParams)
|
||||
}
|
||||
|
||||
export const getMarketplaceCategoryUrl = (
|
||||
|
||||
Reference in new issue
Block a user