mirror of
https://github.com/langgenius/dify.git
synced 2026-09-08 11:04:27 +08:00
feat: add marketplace template detail dialog
This commit is contained in:
parent
fe05ace87b
commit
8220f05b34
@ -0,0 +1,96 @@
|
||||
'use client'
|
||||
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import { Dialog, DialogCloseButton, DialogContent, DialogTitle } from '@langgenius/dify-ui/dialog'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { useTranslation } from '#i18n'
|
||||
|
||||
type MarketplaceDetailDialogFrameProps = {
|
||||
open: boolean
|
||||
src: string
|
||||
title: string
|
||||
onMessage?: (data: unknown) => void
|
||||
onOpenChange: (open: boolean) => void
|
||||
}
|
||||
|
||||
export default function MarketplaceDetailDialogFrame({
|
||||
open,
|
||||
src,
|
||||
title,
|
||||
onMessage,
|
||||
onOpenChange,
|
||||
}: MarketplaceDetailDialogFrameProps) {
|
||||
const { t } = useTranslation()
|
||||
const iframeRef = useRef<HTMLIFrameElement>(null)
|
||||
const [isLoading, setIsLoading] = useState(true)
|
||||
|
||||
useEffect(() => {
|
||||
if (!open || !onMessage) return
|
||||
|
||||
const marketplaceOrigin = new URL(src, window.location.href).origin
|
||||
const handleMessage = (event: MessageEvent) => {
|
||||
if (event.source !== iframeRef.current?.contentWindow || event.origin !== marketplaceOrigin)
|
||||
return
|
||||
|
||||
onMessage(event.data)
|
||||
}
|
||||
|
||||
window.addEventListener('message', handleMessage)
|
||||
return () => window.removeEventListener('message', handleMessage)
|
||||
}, [onMessage, open, src])
|
||||
|
||||
const handleOpenChange = (nextOpen: boolean) => {
|
||||
if (!nextOpen) setIsLoading(true)
|
||||
onOpenChange(nextOpen)
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={handleOpenChange}>
|
||||
<DialogContent className="h-[min(800px,calc(100dvh-48px))] max-h-none! w-[min(1200px,calc(100vw-48px))] max-w-none! overflow-hidden! border-0 p-0 shadow-xl">
|
||||
<DialogTitle className="sr-only">{title}</DialogTitle>
|
||||
<div
|
||||
aria-hidden
|
||||
className={cn(
|
||||
'absolute inset-0 bg-background-default transition-opacity',
|
||||
isLoading ? 'opacity-100' : 'pointer-events-none opacity-0',
|
||||
)}
|
||||
>
|
||||
<div className="flex h-[52px] items-center px-6">
|
||||
<div className="h-4 w-40 animate-pulse rounded-md bg-state-base-hover motion-reduce:animate-none" />
|
||||
</div>
|
||||
<div className="mx-auto flex w-full max-w-[1000px] gap-8 px-12 py-8">
|
||||
<div className="flex flex-1 flex-col gap-4">
|
||||
<div className="h-16 w-2/3 animate-pulse rounded-xl bg-state-base-hover motion-reduce:animate-none" />
|
||||
<div className="h-4 w-full animate-pulse rounded-md bg-state-base-hover motion-reduce:animate-none" />
|
||||
<div className="h-4 w-5/6 animate-pulse rounded-md bg-state-base-hover motion-reduce:animate-none" />
|
||||
<div className="mt-8 h-72 w-full animate-pulse rounded-xl bg-state-base-hover motion-reduce:animate-none" />
|
||||
</div>
|
||||
<div className="hidden w-60 flex-col gap-4 lg:flex">
|
||||
<div className="h-24 animate-pulse rounded-xl bg-state-base-hover motion-reduce:animate-none" />
|
||||
<div className="h-52 animate-pulse rounded-xl bg-state-base-hover motion-reduce:animate-none" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<iframe
|
||||
ref={iframeRef}
|
||||
className={cn(
|
||||
'size-full border-0 bg-background-default transition-opacity',
|
||||
isLoading ? 'opacity-0' : 'opacity-100',
|
||||
)}
|
||||
onLoad={() => setIsLoading(false)}
|
||||
referrerPolicy="strict-origin-when-cross-origin"
|
||||
src={src}
|
||||
title={title}
|
||||
/>
|
||||
<DialogCloseButton
|
||||
aria-label={t(($) => $['operation.close'], { ns: 'common' })}
|
||||
className="top-5 right-5 size-8 rounded-lg"
|
||||
/>
|
||||
<div
|
||||
aria-hidden
|
||||
className="pointer-events-none absolute inset-x-0 bottom-0 h-12 bg-linear-to-t from-background-default to-transparent"
|
||||
/>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
@ -1,12 +1,11 @@
|
||||
'use client'
|
||||
|
||||
import type { Plugin } from '@/app/components/plugins/types'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import { Dialog, DialogCloseButton, DialogContent, DialogTitle } from '@langgenius/dify-ui/dialog'
|
||||
import { useTheme } from 'next-themes'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { useCallback } from 'react'
|
||||
import { useLocale, useTranslation } from '#i18n'
|
||||
import { getPluginLinkInMarketplace } from '../utils'
|
||||
import MarketplaceDetailDialogFrame from './frame'
|
||||
|
||||
const MARKETPLACE_INSTALL_MESSAGE_TYPE = 'dify-marketplace:install-plugin'
|
||||
|
||||
@ -28,8 +27,6 @@ function MarketplaceDetailDialog({
|
||||
const { t } = useTranslation()
|
||||
const locale = useLocale()
|
||||
const { theme } = useTheme()
|
||||
const iframeRef = useRef<HTMLIFrameElement>(null)
|
||||
const [isLoading, setIsLoading] = useState(true)
|
||||
const pluginLabel = plugin.label[locale] ?? plugin.label['en-US'] ?? plugin.name
|
||||
const detailLabel = t(($) => $['detailPanel.operation.detail'], { ns: 'plugin' })
|
||||
const detailURL = getPluginLinkInMarketplace(plugin, {
|
||||
@ -40,85 +37,31 @@ function MarketplaceDetailDialog({
|
||||
view: 'modal',
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
if (!open || isInstalled) return
|
||||
|
||||
const marketplaceOrigin = new URL(detailURL, window.location.href).origin
|
||||
const handleMessage = (event: MessageEvent) => {
|
||||
if (event.source !== iframeRef.current?.contentWindow || event.origin !== marketplaceOrigin)
|
||||
return
|
||||
const handleMessage = useCallback(
|
||||
(data: unknown) => {
|
||||
if (
|
||||
typeof event.data !== 'object' ||
|
||||
event.data === null ||
|
||||
event.data.type !== MARKETPLACE_INSTALL_MESSAGE_TYPE ||
|
||||
event.data.pluginUniqueIdentifier !== plugin.latest_package_identifier
|
||||
typeof data !== 'object' ||
|
||||
data === null ||
|
||||
!('type' in data) ||
|
||||
!('pluginUniqueIdentifier' in data) ||
|
||||
data.type !== MARKETPLACE_INSTALL_MESSAGE_TYPE ||
|
||||
data.pluginUniqueIdentifier !== plugin.latest_package_identifier
|
||||
)
|
||||
return
|
||||
|
||||
onInstall()
|
||||
}
|
||||
|
||||
window.addEventListener('message', handleMessage)
|
||||
return () => window.removeEventListener('message', handleMessage)
|
||||
}, [detailURL, isInstalled, onInstall, open, plugin.latest_package_identifier])
|
||||
|
||||
const handleOpenChange = (nextOpen: boolean) => {
|
||||
if (!nextOpen) setIsLoading(true)
|
||||
onOpenChange(nextOpen)
|
||||
}
|
||||
},
|
||||
[onInstall, plugin.latest_package_identifier],
|
||||
)
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={handleOpenChange}>
|
||||
<DialogContent className="h-[min(800px,calc(100dvh-48px))] max-h-none! w-[min(1200px,calc(100vw-48px))] max-w-none! overflow-hidden! border-0 p-0 shadow-xl">
|
||||
<DialogTitle className="sr-only">
|
||||
{pluginLabel}
|
||||
{' · '}
|
||||
{detailLabel}
|
||||
</DialogTitle>
|
||||
<div
|
||||
aria-hidden
|
||||
className={cn(
|
||||
'absolute inset-0 bg-background-default transition-opacity',
|
||||
isLoading ? 'opacity-100' : 'pointer-events-none opacity-0',
|
||||
)}
|
||||
>
|
||||
<div className="flex h-[52px] items-center px-6">
|
||||
<div className="h-4 w-40 animate-pulse rounded-md bg-state-base-hover motion-reduce:animate-none" />
|
||||
</div>
|
||||
<div className="mx-auto flex w-full max-w-[1000px] gap-8 px-12 py-8">
|
||||
<div className="flex flex-1 flex-col gap-4">
|
||||
<div className="h-16 w-2/3 animate-pulse rounded-xl bg-state-base-hover motion-reduce:animate-none" />
|
||||
<div className="h-4 w-full animate-pulse rounded-md bg-state-base-hover motion-reduce:animate-none" />
|
||||
<div className="h-4 w-5/6 animate-pulse rounded-md bg-state-base-hover motion-reduce:animate-none" />
|
||||
<div className="mt-8 h-72 w-full animate-pulse rounded-xl bg-state-base-hover motion-reduce:animate-none" />
|
||||
</div>
|
||||
<div className="hidden w-60 flex-col gap-4 lg:flex">
|
||||
<div className="h-24 animate-pulse rounded-xl bg-state-base-hover motion-reduce:animate-none" />
|
||||
<div className="h-52 animate-pulse rounded-xl bg-state-base-hover motion-reduce:animate-none" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<iframe
|
||||
ref={iframeRef}
|
||||
className={cn(
|
||||
'size-full border-0 bg-background-default transition-opacity',
|
||||
isLoading ? 'opacity-0' : 'opacity-100',
|
||||
)}
|
||||
onLoad={() => setIsLoading(false)}
|
||||
referrerPolicy="strict-origin-when-cross-origin"
|
||||
src={detailURL}
|
||||
title={`${pluginLabel} · ${detailLabel}`}
|
||||
/>
|
||||
<DialogCloseButton
|
||||
aria-label={t(($) => $['operation.close'], { ns: 'common' })}
|
||||
className="top-5 right-5 size-8 rounded-lg"
|
||||
/>
|
||||
<div
|
||||
aria-hidden
|
||||
className="pointer-events-none absolute inset-x-0 bottom-0 h-12 bg-linear-to-t from-background-default to-transparent"
|
||||
/>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
<MarketplaceDetailDialogFrame
|
||||
open={open}
|
||||
src={detailURL}
|
||||
title={`${pluginLabel} · ${detailLabel}`}
|
||||
onMessage={isInstalled ? undefined : handleMessage}
|
||||
onOpenChange={onOpenChange}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@ -1,8 +1,26 @@
|
||||
import type { MarketplaceTemplate } from '@dify/contracts/marketplace'
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
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 TemplateCard from '../template-card'
|
||||
|
||||
const { mockPush } = vi.hoisted(() => ({
|
||||
mockPush: vi.fn(),
|
||||
}))
|
||||
|
||||
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('@/app/components/base/app-icon', () => ({
|
||||
default: () => <div aria-hidden />,
|
||||
}))
|
||||
@ -21,13 +39,51 @@ const template: MarketplaceTemplate = {
|
||||
}
|
||||
|
||||
describe('TemplateCard', () => {
|
||||
it('opens a Marketplace template through the Dify import flow', () => {
|
||||
render(<TemplateCard partnerText="Verified by a Dify partner" template={template} />)
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
expect(screen.getByRole('link', { name: 'Campaign planner' })).toHaveAttribute(
|
||||
'href',
|
||||
'/apps?template-id=template%2Fone',
|
||||
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>,
|
||||
)
|
||||
|
||||
expect(screen.queryByRole('link', { name: 'Campaign planner' })).not.toBeInTheDocument()
|
||||
await user.click(screen.getByRole('button', { name: 'Campaign planner' }))
|
||||
|
||||
expect(screen.getByRole('dialog')).toBeInTheDocument()
|
||||
expect(mockPush).not.toHaveBeenCalled()
|
||||
|
||||
const frame = screen.getByTitle(
|
||||
'Campaign planner · plugin.detailPanel.operation.detail',
|
||||
) as HTMLIFrameElement
|
||||
const marketplaceOrigin = new URL(frame.getAttribute('src')!, window.location.href).origin
|
||||
const installRequest = {
|
||||
type: 'dify-marketplace:install-template',
|
||||
templateId: template.id,
|
||||
}
|
||||
fireEvent(
|
||||
window,
|
||||
new MessageEvent('message', {
|
||||
data: { ...installRequest, templateId: 'another-template' },
|
||||
origin: marketplaceOrigin,
|
||||
source: frame.contentWindow,
|
||||
}),
|
||||
)
|
||||
expect(mockPush).not.toHaveBeenCalled()
|
||||
|
||||
fireEvent(
|
||||
window,
|
||||
new MessageEvent('message', {
|
||||
data: installRequest,
|
||||
origin: marketplaceOrigin,
|
||||
source: frame.contentWindow,
|
||||
}),
|
||||
)
|
||||
expect(mockPush).toHaveBeenCalledWith('/apps?template-id=template%2Fone')
|
||||
expect(screen.getByText('dify')).toBeInTheDocument()
|
||||
expect(screen.getByText('1.2k')).toBeInTheDocument()
|
||||
expect(screen.getByLabelText('Verified by a Dify partner')).toBeInTheDocument()
|
||||
|
||||
@ -1,11 +1,16 @@
|
||||
'use client'
|
||||
|
||||
import type { MarketplaceTemplate } from '@dify/contracts/marketplace'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import { useBoolean } from 'ahooks'
|
||||
import { useCallback } from 'react'
|
||||
import AppIcon from '@/app/components/base/app-icon'
|
||||
import Partner from '@/app/components/plugins/base/badges/partner'
|
||||
import { MARKETPLACE_API_PREFIX } from '@/config'
|
||||
import Link from '@/next/link'
|
||||
import { useRouter } from '@/next/navigation'
|
||||
import { formatNumberAbbreviated } from '@/utils/format'
|
||||
import { getIconFromMarketPlace } from '@/utils/get-icon'
|
||||
import TemplateDetailDialog from './template-detail-dialog'
|
||||
|
||||
type TemplateCardProps = {
|
||||
template: MarketplaceTemplate
|
||||
@ -16,6 +21,8 @@ type TemplateCardProps = {
|
||||
const MAX_VISIBLE_PLUGIN_DEPENDENCIES = 7
|
||||
|
||||
export default function TemplateCard({ template, className, partnerText }: TemplateCardProps) {
|
||||
const router = useRouter()
|
||||
const [isDetailOpen, { setTrue: showDetail, setFalse: hideDetail }] = useBoolean(false)
|
||||
const publisher =
|
||||
template.publisher_handle || template.publisher_unique_handle || template.creator_email || ''
|
||||
const visiblePlugins = template.deps_plugins?.slice(0, MAX_VISIBLE_PLUGIN_DEPENDENCIES) ?? []
|
||||
@ -26,60 +33,77 @@ export default function TemplateCard({ template, className, partnerText }: Templ
|
||||
const imageUrl = template.icon_file_key
|
||||
? `${MARKETPLACE_API_PREFIX}/templates/${template.id}/icon`
|
||||
: undefined
|
||||
const handleOpenChange = (open: boolean) => {
|
||||
if (open) showDetail()
|
||||
else hideDetail()
|
||||
}
|
||||
const handleInstall = useCallback(() => {
|
||||
hideDetail()
|
||||
router.push(`/apps?template-id=${encodeURIComponent(template.id)}`)
|
||||
}, [hideDetail, router, template.id])
|
||||
|
||||
return (
|
||||
<article
|
||||
className={cn(
|
||||
'relative flex h-full flex-col overflow-hidden rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-on-panel-item-bg pb-3 shadow-xs hover:bg-components-panel-on-panel-item-bg-hover',
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<div className="flex shrink-0 items-center gap-3 px-4 pt-4 pb-2">
|
||||
<AppIcon
|
||||
size="large"
|
||||
iconType={imageUrl ? 'image' : 'emoji'}
|
||||
icon={imageUrl ? undefined : template.icon || '📄'}
|
||||
imageUrl={imageUrl}
|
||||
background={template.icon_background}
|
||||
/>
|
||||
<div className="flex min-w-0 flex-1 flex-col justify-center gap-0.5">
|
||||
<div className="flex items-center">
|
||||
<Link
|
||||
href={`/apps?template-id=${encodeURIComponent(template.id)}`}
|
||||
className="truncate system-md-medium text-text-primary after:absolute after:inset-0"
|
||||
>
|
||||
{template.template_name}
|
||||
</Link>
|
||||
{template.badges?.includes('partner') && (
|
||||
<Partner className="relative z-[1] ml-0.5 size-4 shrink-0" text={partnerText} />
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-2 system-xs-regular text-text-tertiary">
|
||||
{publisher && <span className="truncate">{publisher}</span>}
|
||||
{publisher && <span>·</span>}
|
||||
<span>{formatNumberAbbreviated(template.usage_count)}</span>
|
||||
<>
|
||||
<article
|
||||
className={cn(
|
||||
'relative flex h-full flex-col overflow-hidden rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-on-panel-item-bg pb-3 shadow-xs hover:bg-components-panel-on-panel-item-bg-hover',
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<div className="flex shrink-0 items-center gap-3 px-4 pt-4 pb-2">
|
||||
<AppIcon
|
||||
size="large"
|
||||
iconType={imageUrl ? 'image' : 'emoji'}
|
||||
icon={imageUrl ? undefined : template.icon || '📄'}
|
||||
imageUrl={imageUrl}
|
||||
background={template.icon_background}
|
||||
/>
|
||||
<div className="flex min-w-0 flex-1 flex-col justify-center gap-0.5">
|
||||
<div className="flex items-center">
|
||||
<button
|
||||
type="button"
|
||||
onClick={showDetail}
|
||||
className="truncate text-left system-md-medium text-text-primary outline-hidden after:absolute after:inset-0 focus-visible:after:ring-2 focus-visible:after:ring-state-accent-solid focus-visible:after:ring-inset"
|
||||
>
|
||||
{template.template_name}
|
||||
</button>
|
||||
{template.badges?.includes('partner') && (
|
||||
<Partner className="relative z-[1] ml-0.5 size-4 shrink-0" text={partnerText} />
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-2 system-xs-regular text-text-tertiary">
|
||||
{publisher && <span className="truncate">{publisher}</span>}
|
||||
{publisher && <span>·</span>}
|
||||
<span>{formatNumberAbbreviated(template.usage_count)}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="min-h-8 px-4 pt-1 pb-2 system-xs-regular text-text-secondary">
|
||||
<p className="line-clamp-2" title={template.overview}>
|
||||
{template.overview}
|
||||
</p>
|
||||
</div>
|
||||
<div className="mt-auto flex min-h-7 items-center gap-1 px-4 py-1">
|
||||
{visiblePlugins.map((pluginId) => (
|
||||
<img
|
||||
key={pluginId}
|
||||
className="size-6 rounded-md border-[0.5px] border-effects-icon-border object-cover"
|
||||
src={getIconFromMarketPlace(pluginId)}
|
||||
alt=""
|
||||
title={pluginId}
|
||||
/>
|
||||
))}
|
||||
{remainingPluginCount > 0 && (
|
||||
<span className="system-xs-regular text-text-tertiary">+{remainingPluginCount}</span>
|
||||
)}
|
||||
</div>
|
||||
</article>
|
||||
<div className="min-h-8 px-4 pt-1 pb-2 system-xs-regular text-text-secondary">
|
||||
<p className="line-clamp-2" title={template.overview}>
|
||||
{template.overview}
|
||||
</p>
|
||||
</div>
|
||||
<div className="mt-auto flex min-h-7 items-center gap-1 px-4 py-1">
|
||||
{visiblePlugins.map((pluginId) => (
|
||||
<img
|
||||
key={pluginId}
|
||||
className="size-6 rounded-md border-[0.5px] border-effects-icon-border object-cover"
|
||||
src={getIconFromMarketPlace(pluginId)}
|
||||
alt=""
|
||||
title={pluginId}
|
||||
/>
|
||||
))}
|
||||
{remainingPluginCount > 0 && (
|
||||
<span className="system-xs-regular text-text-tertiary">+{remainingPluginCount}</span>
|
||||
)}
|
||||
</div>
|
||||
</article>
|
||||
<TemplateDetailDialog
|
||||
open={isDetailOpen}
|
||||
template={template}
|
||||
onInstall={handleInstall}
|
||||
onOpenChange={handleOpenChange}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@ -0,0 +1,61 @@
|
||||
'use client'
|
||||
|
||||
import type { MarketplaceTemplate } from '@dify/contracts/marketplace'
|
||||
import { useTheme } from 'next-themes'
|
||||
import { useCallback } from 'react'
|
||||
import { useLocale, useTranslation } from '#i18n'
|
||||
import MarketplaceDetailDialogFrame from '../detail-dialog/frame'
|
||||
import { getTemplateLinkInMarketplace } from '../utils'
|
||||
|
||||
const MARKETPLACE_INSTALL_MESSAGE_TYPE = 'dify-marketplace:install-template'
|
||||
|
||||
type TemplateDetailDialogProps = {
|
||||
open: boolean
|
||||
template: MarketplaceTemplate
|
||||
onInstall: () => void
|
||||
onOpenChange: (open: boolean) => void
|
||||
}
|
||||
|
||||
export default function TemplateDetailDialog({
|
||||
open,
|
||||
template,
|
||||
onInstall,
|
||||
onOpenChange,
|
||||
}: TemplateDetailDialogProps) {
|
||||
const { t } = useTranslation()
|
||||
const locale = useLocale()
|
||||
const { theme } = useTheme()
|
||||
const detailLabel = t(($) => $['detailPanel.operation.detail'], { ns: 'plugin' })
|
||||
const detailURL = getTemplateLinkInMarketplace(template, {
|
||||
language: locale,
|
||||
source: globalThis.location?.origin,
|
||||
theme,
|
||||
view: 'modal',
|
||||
})
|
||||
const handleMessage = useCallback(
|
||||
(data: unknown) => {
|
||||
if (
|
||||
typeof data !== 'object' ||
|
||||
data === null ||
|
||||
!('type' in data) ||
|
||||
!('templateId' in data) ||
|
||||
data.type !== MARKETPLACE_INSTALL_MESSAGE_TYPE ||
|
||||
data.templateId !== template.id
|
||||
)
|
||||
return
|
||||
|
||||
onInstall()
|
||||
},
|
||||
[onInstall, template.id],
|
||||
)
|
||||
|
||||
return (
|
||||
<MarketplaceDetailDialogFrame
|
||||
open={open}
|
||||
src={detailURL}
|
||||
title={`${template.template_name} · ${detailLabel}`}
|
||||
onMessage={handleMessage}
|
||||
onOpenChange={onOpenChange}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@ -2,6 +2,7 @@ import type {
|
||||
CollectionsAndPluginsSearchParams,
|
||||
MarketplaceCollection,
|
||||
MarketplacePlugin,
|
||||
MarketplaceTemplate,
|
||||
PluginsSearchParams,
|
||||
} from '@dify/contracts/marketplace'
|
||||
import type { ActivePluginType } from './constants'
|
||||
@ -70,6 +71,22 @@ export const getPluginDetailLinkInMarketplace = (
|
||||
return `/plugin/${org}/${name}`
|
||||
}
|
||||
|
||||
export const getTemplateLinkInMarketplace = (
|
||||
template: Pick<
|
||||
MarketplaceTemplate,
|
||||
'id' | 'publisher_handle' | 'publisher_unique_handle' | 'template_name'
|
||||
>,
|
||||
params?: Record<string, string | undefined>,
|
||||
) => {
|
||||
const publisher = template.publisher_handle || template.publisher_unique_handle || 'template'
|
||||
const path = `/template/${encodeURIComponent(publisher)}/${encodeURIComponent(template.template_name)}`
|
||||
|
||||
return getMarketplaceUrl(path, {
|
||||
...params,
|
||||
templateId: template.id,
|
||||
})
|
||||
}
|
||||
|
||||
export const getMarketplaceCategoryUrl = (
|
||||
category?: string,
|
||||
params?: Record<string, string | undefined>,
|
||||
|
||||
Loading…
Reference in New Issue
Block a user