mirror of
https://github.com/langgenius/dify.git
synced 2026-10-04 13:47:55 +08:00
feat(marketplace): use stable template UUID links
This commit is contained in:
1 parent
f67f8afbb5
commit
7f7e909826
8 files changed
+148
-65
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 'vite-plus/test'
|
||||
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
|
||||
}
|
||||
@@ -843,7 +843,7 @@ describe('HomeTrending', () => {
|
||||
)
|
||||
})
|
||||
|
||||
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',
|
||||
@@ -877,6 +877,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()
|
||||
|
||||
+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 {
|
||||
@@ -117,7 +121,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 '/'
|
||||
}
|
||||
|
||||
@@ -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 'vite-plus/test'
|
||||
import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from 'vite-plus/test'
|
||||
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,
|
||||
|
||||
@@ -107,31 +107,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