mirror of
https://github.com/langgenius/dify.git
synced 2026-05-05 18:08:07 +08:00
feat: add template icon URL utility and integrate AppIcon component in template card and search dropdown for improved icon rendering
This commit is contained in:
@ -75,7 +75,6 @@ const ListWithCollection = (props: ListWithCollectionProps) => {
|
|||||||
itemKeyField="id"
|
itemKeyField="id"
|
||||||
renderCard={renderTemplateCard}
|
renderCard={renderTemplateCard}
|
||||||
carouselCollectionNames={[CAROUSEL_COLLECTION_NAMES.featured]}
|
carouselCollectionNames={[CAROUSEL_COLLECTION_NAMES.featured]}
|
||||||
viewMoreSearchTab="templates"
|
|
||||||
cardContainerClassName={cardContainerClassName}
|
cardContainerClassName={cardContainerClassName}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
|||||||
@ -0,0 +1,246 @@
|
|||||||
|
import type { Template } from '../types'
|
||||||
|
import { render } from '@testing-library/react'
|
||||||
|
import { describe, expect, it, vi } from 'vitest'
|
||||||
|
import TemplateCard from './template-card'
|
||||||
|
|
||||||
|
// Mock AppIcon component to capture props for assertion
|
||||||
|
vi.mock('@/app/components/base/app-icon', () => ({
|
||||||
|
default: ({ size, iconType, icon, imageUrl, background }: {
|
||||||
|
size?: string
|
||||||
|
iconType?: string
|
||||||
|
icon?: string
|
||||||
|
imageUrl?: string | null
|
||||||
|
background?: string | null
|
||||||
|
}) => (
|
||||||
|
<span
|
||||||
|
data-testid="app-icon"
|
||||||
|
data-size={size}
|
||||||
|
data-icon-type={iconType}
|
||||||
|
data-icon={icon}
|
||||||
|
data-image-url={imageUrl || ''}
|
||||||
|
data-background={background || ''}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
}))
|
||||||
|
|
||||||
|
// Mock i18n
|
||||||
|
vi.mock('#i18n', () => ({
|
||||||
|
useTranslation: () => ({
|
||||||
|
t: (key: string, options?: Record<string, unknown>) => {
|
||||||
|
if (key === 'marketplace.templateCard.by')
|
||||||
|
return `by ${options?.author || ''}`
|
||||||
|
if (key === 'usedCount')
|
||||||
|
return `${options?.num || 0} used`
|
||||||
|
return key
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
useLocale: () => 'en-US',
|
||||||
|
}))
|
||||||
|
|
||||||
|
// Mock next/link
|
||||||
|
vi.mock('next/link', () => ({
|
||||||
|
default: ({ children, href, ...props }: { children: React.ReactNode, href: string }) => (
|
||||||
|
<a href={href} {...props}>{children}</a>
|
||||||
|
),
|
||||||
|
}))
|
||||||
|
|
||||||
|
// Mock next-themes
|
||||||
|
vi.mock('next-themes', () => ({
|
||||||
|
useTheme: () => ({ theme: 'light' }),
|
||||||
|
}))
|
||||||
|
|
||||||
|
// Mock marketplace utils
|
||||||
|
vi.mock('@/utils/get-icon', () => ({
|
||||||
|
getIconFromMarketPlace: (id: string) => `https://marketplace.dify.ai/api/v1/plugins/${id}/icon`,
|
||||||
|
}))
|
||||||
|
|
||||||
|
vi.mock('@/utils/template', () => ({
|
||||||
|
formatUsedCount: (count: number) => String(count),
|
||||||
|
}))
|
||||||
|
|
||||||
|
vi.mock('@/utils/var', () => ({
|
||||||
|
getMarketplaceUrl: (path: string) => `https://marketplace.dify.ai${path}`,
|
||||||
|
}))
|
||||||
|
|
||||||
|
vi.mock('@/app/components/plugins/card/base/corner-mark', () => ({
|
||||||
|
default: ({ text }: { text: string }) => <span data-testid="corner-mark">{text}</span>,
|
||||||
|
}))
|
||||||
|
|
||||||
|
// Mock marketplace utils (getTemplateIconUrl)
|
||||||
|
vi.mock('../utils', () => ({
|
||||||
|
getTemplateIconUrl: (template: { id: string, icon?: string, icon_file_key?: string }): string => {
|
||||||
|
if (template.icon?.startsWith('http'))
|
||||||
|
return template.icon
|
||||||
|
if (template.icon_file_key)
|
||||||
|
return `https://marketplace.dify.ai/api/v1/templates/${template.id}/icon`
|
||||||
|
return ''
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
|
||||||
|
// ================================
|
||||||
|
// Test Data Factories
|
||||||
|
// ================================
|
||||||
|
|
||||||
|
const createMockTemplate = (overrides?: Partial<Template>): Template => ({
|
||||||
|
id: 'test-template-id',
|
||||||
|
index_id: 'test-template-id',
|
||||||
|
template_name: 'test-template',
|
||||||
|
icon: '📄',
|
||||||
|
icon_background: '',
|
||||||
|
icon_file_key: '',
|
||||||
|
categories: ['Agent'],
|
||||||
|
overview: 'A test template',
|
||||||
|
readme: 'readme content',
|
||||||
|
partner_link: '',
|
||||||
|
deps_plugins: [],
|
||||||
|
preferred_languages: ['en'],
|
||||||
|
publisher_handle: 'test-publisher',
|
||||||
|
publisher_type: 'individual',
|
||||||
|
kind: 'classic',
|
||||||
|
status: 'published',
|
||||||
|
usage_count: 100,
|
||||||
|
created_at: '2026-01-01T00:00:00Z',
|
||||||
|
updated_at: '2026-01-01T00:00:00Z',
|
||||||
|
...overrides,
|
||||||
|
})
|
||||||
|
|
||||||
|
// ================================
|
||||||
|
// Tests
|
||||||
|
// ================================
|
||||||
|
|
||||||
|
describe('TemplateCard', () => {
|
||||||
|
describe('Icon Rendering via AppIcon', () => {
|
||||||
|
it('should pass emoji id to AppIcon when icon is an emoji id like sweat_smile', () => {
|
||||||
|
const template = createMockTemplate({ icon: 'sweat_smile' })
|
||||||
|
const { container } = render(<TemplateCard template={template} />)
|
||||||
|
|
||||||
|
const appIcon = container.querySelector('[data-testid="app-icon"]')
|
||||||
|
expect(appIcon).toBeInTheDocument()
|
||||||
|
expect(appIcon?.getAttribute('data-icon-type')).toBe('emoji')
|
||||||
|
expect(appIcon?.getAttribute('data-icon')).toBe('sweat_smile')
|
||||||
|
expect(appIcon?.getAttribute('data-size')).toBe('large')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should pass unicode emoji to AppIcon when icon is a unicode character', () => {
|
||||||
|
const template = createMockTemplate({ icon: '😅' })
|
||||||
|
const { container } = render(<TemplateCard template={template} />)
|
||||||
|
|
||||||
|
const appIcon = container.querySelector('[data-testid="app-icon"]')
|
||||||
|
expect(appIcon).toBeInTheDocument()
|
||||||
|
expect(appIcon?.getAttribute('data-icon-type')).toBe('emoji')
|
||||||
|
expect(appIcon?.getAttribute('data-icon')).toBe('😅')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should pass default fallback icon to AppIcon when icon and icon_file_key are both empty', () => {
|
||||||
|
const template = createMockTemplate({ icon: '', icon_file_key: '' })
|
||||||
|
const { container } = render(<TemplateCard template={template} />)
|
||||||
|
|
||||||
|
const appIcon = container.querySelector('[data-testid="app-icon"]')
|
||||||
|
expect(appIcon).toBeInTheDocument()
|
||||||
|
expect(appIcon?.getAttribute('data-icon-type')).toBe('emoji')
|
||||||
|
expect(appIcon?.getAttribute('data-icon')).toBe('📄')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should pass image URL to AppIcon when icon is a URL', () => {
|
||||||
|
const template = createMockTemplate({ icon: 'https://example.com/icon.png' })
|
||||||
|
const { container } = render(<TemplateCard template={template} />)
|
||||||
|
|
||||||
|
const appIcon = container.querySelector('[data-testid="app-icon"]')
|
||||||
|
expect(appIcon).toBeInTheDocument()
|
||||||
|
expect(appIcon?.getAttribute('data-icon-type')).toBe('image')
|
||||||
|
expect(appIcon?.getAttribute('data-image-url')).toBe('https://example.com/icon.png')
|
||||||
|
// icon prop should not be set for URL icons
|
||||||
|
expect(appIcon?.hasAttribute('data-icon')).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should resolve image URL from icon_file_key when icon is empty but icon_file_key is set', () => {
|
||||||
|
const template = createMockTemplate({
|
||||||
|
id: 'tpl-123',
|
||||||
|
icon: '',
|
||||||
|
icon_file_key: 'fa3b0f86-bc64-47ec-ad83-8e3cfc6739ae.jpg',
|
||||||
|
})
|
||||||
|
const { container } = render(<TemplateCard template={template} />)
|
||||||
|
|
||||||
|
const appIcon = container.querySelector('[data-testid="app-icon"]')
|
||||||
|
expect(appIcon).toBeInTheDocument()
|
||||||
|
expect(appIcon?.getAttribute('data-icon-type')).toBe('image')
|
||||||
|
expect(appIcon?.getAttribute('data-image-url')).toBe('https://marketplace.dify.ai/api/v1/templates/tpl-123/icon')
|
||||||
|
// icon prop should not be set when rendering as image
|
||||||
|
expect(appIcon?.hasAttribute('data-icon')).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should prefer icon URL over icon_file_key when both are present', () => {
|
||||||
|
const template = createMockTemplate({
|
||||||
|
icon: 'https://example.com/custom-icon.png',
|
||||||
|
icon_file_key: 'fa3b0f86-bc64-47ec-ad83-8e3cfc6739ae.jpg',
|
||||||
|
})
|
||||||
|
const { container } = render(<TemplateCard template={template} />)
|
||||||
|
|
||||||
|
const appIcon = container.querySelector('[data-testid="app-icon"]')
|
||||||
|
expect(appIcon?.getAttribute('data-icon-type')).toBe('image')
|
||||||
|
expect(appIcon?.getAttribute('data-image-url')).toBe('https://example.com/custom-icon.png')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('Avatar Background', () => {
|
||||||
|
it('should pass icon_background to AppIcon when provided', () => {
|
||||||
|
const template = createMockTemplate({ icon: 'sweat_smile', icon_background: '#FFEAD5' })
|
||||||
|
const { container } = render(<TemplateCard template={template} />)
|
||||||
|
|
||||||
|
const appIcon = container.querySelector('[data-testid="app-icon"]')
|
||||||
|
expect(appIcon?.getAttribute('data-background')).toBe('#FFEAD5')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should not pass background to AppIcon when icon_background is empty', () => {
|
||||||
|
const template = createMockTemplate({ icon: 'sweat_smile', icon_background: '' })
|
||||||
|
const { container } = render(<TemplateCard template={template} />)
|
||||||
|
|
||||||
|
const appIcon = container.querySelector('[data-testid="app-icon"]')
|
||||||
|
// Empty string means no background was passed (undefined becomes '')
|
||||||
|
expect(appIcon?.getAttribute('data-background')).toBe('')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('Sandbox', () => {
|
||||||
|
it('should render CornerMark when kind is sandboxed', () => {
|
||||||
|
const template = createMockTemplate({ kind: 'sandboxed' })
|
||||||
|
const { container } = render(<TemplateCard template={template} />)
|
||||||
|
|
||||||
|
const cornerMark = container.querySelector('[data-testid="corner-mark"]')
|
||||||
|
expect(cornerMark).toBeInTheDocument()
|
||||||
|
expect(cornerMark?.textContent).toBe('Sandbox')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should not render CornerMark when kind is classic', () => {
|
||||||
|
const template = createMockTemplate({ kind: 'classic' })
|
||||||
|
const { container } = render(<TemplateCard template={template} />)
|
||||||
|
|
||||||
|
const cornerMark = container.querySelector('[data-testid="corner-mark"]')
|
||||||
|
expect(cornerMark).not.toBeInTheDocument()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('Deps Plugins', () => {
|
||||||
|
it('should render dep plugin icons', () => {
|
||||||
|
const template = createMockTemplate({
|
||||||
|
deps_plugins: ['langgenius/google-search', 'langgenius/dalle'],
|
||||||
|
})
|
||||||
|
const { container } = render(<TemplateCard template={template} />)
|
||||||
|
|
||||||
|
const pluginIcons = container.querySelectorAll('.h-6.w-6 img')
|
||||||
|
expect(pluginIcons.length).toBe(2)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should show +N when deps_plugins exceed MAX_VISIBLE_DEPS_PLUGINS', () => {
|
||||||
|
const deps = Array.from({ length: 10 }, (_, i) => `org/plugin-${i}`)
|
||||||
|
const template = createMockTemplate({ deps_plugins: deps })
|
||||||
|
const { container } = render(<TemplateCard template={template} />)
|
||||||
|
|
||||||
|
// Should show 7 visible + "+3"
|
||||||
|
const pluginIcons = container.querySelectorAll('.h-6.w-6 img')
|
||||||
|
expect(pluginIcons.length).toBe(7)
|
||||||
|
|
||||||
|
expect(container.textContent).toContain('+3')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
@ -2,16 +2,17 @@
|
|||||||
|
|
||||||
import type { Template } from '../types'
|
import type { Template } from '../types'
|
||||||
import { useLocale, useTranslation } from '#i18n'
|
import { useLocale, useTranslation } from '#i18n'
|
||||||
import Image from 'next/image'
|
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
import { useCallback, useMemo } from 'react'
|
import { useCallback } from 'react'
|
||||||
|
import AppIcon from '@/app/components/base/app-icon'
|
||||||
import CornerMark from '@/app/components/plugins/card/base/corner-mark'
|
import CornerMark from '@/app/components/plugins/card/base/corner-mark'
|
||||||
import useTheme from '@/hooks/use-theme'
|
import useTheme from '@/hooks/use-theme'
|
||||||
import { cn } from '@/utils/classnames'
|
import { cn } from '@/utils/classnames'
|
||||||
import { getIconFromMarketPlace } from '@/utils/get-icon'
|
import { getIconFromMarketPlace } from '@/utils/get-icon'
|
||||||
import { formatUsedCount } from '@/utils/template'
|
import { formatUsedCount } from '@/utils/template'
|
||||||
import { getMarketplaceUrl } from '@/utils/var'
|
import { getMarketplaceUrl } from '@/utils/var'
|
||||||
|
import { getTemplateIconUrl } from '../utils'
|
||||||
|
|
||||||
type TemplateCardProps = {
|
type TemplateCardProps = {
|
||||||
template: Template
|
template: Template
|
||||||
@ -21,31 +22,6 @@ type TemplateCardProps = {
|
|||||||
// Number of tag icons to show before showing "+X"
|
// Number of tag icons to show before showing "+X"
|
||||||
const MAX_VISIBLE_DEPS_PLUGINS = 7
|
const MAX_VISIBLE_DEPS_PLUGINS = 7
|
||||||
|
|
||||||
// Soft background color palette for avatar
|
|
||||||
const AVATAR_BG_COLORS = [
|
|
||||||
'bg-components-icon-bg-red-soft',
|
|
||||||
'bg-components-icon-bg-orange-dark-soft',
|
|
||||||
'bg-components-icon-bg-yellow-soft',
|
|
||||||
'bg-components-icon-bg-green-soft',
|
|
||||||
'bg-components-icon-bg-teal-soft',
|
|
||||||
'bg-components-icon-bg-blue-light-soft',
|
|
||||||
'bg-components-icon-bg-blue-soft',
|
|
||||||
'bg-components-icon-bg-indigo-soft',
|
|
||||||
'bg-components-icon-bg-violet-soft',
|
|
||||||
'bg-components-icon-bg-pink-soft',
|
|
||||||
]
|
|
||||||
|
|
||||||
// Simple hash function to get consistent color per template
|
|
||||||
const getAvatarBgClass = (id: string): string => {
|
|
||||||
let hash = 0
|
|
||||||
for (let i = 0; i < id.length; i++) {
|
|
||||||
const char = id.charCodeAt(i)
|
|
||||||
hash = ((hash << 5) - hash) + char
|
|
||||||
hash = hash & hash
|
|
||||||
}
|
|
||||||
return AVATAR_BG_COLORS[Math.abs(hash) % AVATAR_BG_COLORS.length]
|
|
||||||
}
|
|
||||||
|
|
||||||
const TemplateCardComponent = ({
|
const TemplateCardComponent = ({
|
||||||
template,
|
template,
|
||||||
className,
|
className,
|
||||||
@ -55,21 +31,7 @@ const TemplateCardComponent = ({
|
|||||||
const { theme } = useTheme()
|
const { theme } = useTheme()
|
||||||
const { id, template_name, overview, icon, publisher_handle, usage_count, icon_background, deps_plugins, kind } = template
|
const { id, template_name, overview, icon, publisher_handle, usage_count, icon_background, deps_plugins, kind } = template
|
||||||
const isSandbox = kind === 'sandboxed'
|
const isSandbox = kind === 'sandboxed'
|
||||||
const isIconUrl = !!icon && /^(?:https?:)?\/\//.test(icon)
|
const iconUrl = getTemplateIconUrl(template)
|
||||||
|
|
||||||
const avatarBgStyle = useMemo(() => {
|
|
||||||
// If icon_background is provided (hex or rgba), use it directly
|
|
||||||
if (icon_background)
|
|
||||||
return { backgroundColor: icon_background }
|
|
||||||
return undefined
|
|
||||||
}, [icon_background])
|
|
||||||
|
|
||||||
const avatarBgClass = useMemo(() => {
|
|
||||||
// Only use class-based color if no inline style
|
|
||||||
if (icon_background)
|
|
||||||
return ''
|
|
||||||
return getAvatarBgClass(id)
|
|
||||||
}, [icon_background, id])
|
|
||||||
|
|
||||||
const handleClick = useCallback(() => {
|
const handleClick = useCallback(() => {
|
||||||
const url = getMarketplaceUrl(`/templates/${publisher_handle}/${template_name}`, {
|
const url = getMarketplaceUrl(`/templates/${publisher_handle}/${template_name}`, {
|
||||||
@ -98,27 +60,13 @@ const TemplateCardComponent = ({
|
|||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="flex shrink-0 items-center gap-3 px-4 pb-2 pt-4">
|
<div className="flex shrink-0 items-center gap-3 px-4 pb-2 pt-4">
|
||||||
{/* Avatar */}
|
{/* Avatar */}
|
||||||
<div
|
<AppIcon
|
||||||
className={cn(
|
size="large"
|
||||||
'flex h-10 w-10 shrink-0 items-center justify-center overflow-hidden rounded-[10px] border-[0.5px] border-divider-regular p-1',
|
iconType={iconUrl ? 'image' : 'emoji'}
|
||||||
avatarBgClass,
|
icon={iconUrl ? undefined : (icon || '📄')}
|
||||||
)}
|
imageUrl={iconUrl || undefined}
|
||||||
style={avatarBgStyle}
|
background={icon_background || undefined}
|
||||||
>
|
/>
|
||||||
{isIconUrl
|
|
||||||
? (
|
|
||||||
<Image
|
|
||||||
src={icon}
|
|
||||||
alt={template_name}
|
|
||||||
width={24}
|
|
||||||
height={24}
|
|
||||||
className="h-6 w-6 object-contain"
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
: (
|
|
||||||
<span className="text-2xl leading-[1.2]">{icon || '📄'}</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
{/* Title */}
|
{/* Title */}
|
||||||
<div className="flex min-w-0 flex-1 flex-col justify-center gap-0.5">
|
<div className="flex min-w-0 flex-1 flex-col justify-center gap-0.5">
|
||||||
<p className="system-md-medium truncate text-text-primary">{template_name}</p>
|
<p className="system-md-medium truncate text-text-primary">{template_name}</p>
|
||||||
|
|||||||
@ -3,6 +3,7 @@ import type { Plugin } from '@/app/components/plugins/types'
|
|||||||
import { useTranslation } from '#i18n'
|
import { useTranslation } from '#i18n'
|
||||||
import { RiArrowRightLine } from '@remixicon/react'
|
import { RiArrowRightLine } from '@remixicon/react'
|
||||||
import { Fragment } from 'react'
|
import { Fragment } from 'react'
|
||||||
|
import AppIcon from '@/app/components/base/app-icon'
|
||||||
import Loading from '@/app/components/base/loading'
|
import Loading from '@/app/components/base/loading'
|
||||||
import { useCategories } from '@/app/components/plugins/hooks'
|
import { useCategories } from '@/app/components/plugins/hooks'
|
||||||
import { useRenderI18nObject } from '@/hooks/use-i18n'
|
import { useRenderI18nObject } from '@/hooks/use-i18n'
|
||||||
@ -10,7 +11,7 @@ import { cn } from '@/utils/classnames'
|
|||||||
import { formatUsedCount } from '@/utils/template'
|
import { formatUsedCount } from '@/utils/template'
|
||||||
import { getMarketplaceUrl } from '@/utils/var'
|
import { getMarketplaceUrl } from '@/utils/var'
|
||||||
import { MARKETPLACE_TYPE_ICON_COMPONENTS } from '../../plugin-type-icons'
|
import { MARKETPLACE_TYPE_ICON_COMPONENTS } from '../../plugin-type-icons'
|
||||||
import { getCreatorAvatarUrl, getPluginDetailLinkInMarketplace } from '../../utils'
|
import { getCreatorAvatarUrl, getPluginDetailLinkInMarketplace, getTemplateIconUrl } from '../../utils'
|
||||||
|
|
||||||
const DROPDOWN_PANEL = 'w-[472px] max-h-[710px] overflow-y-auto rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur shadow-xl backdrop-blur-sm'
|
const DROPDOWN_PANEL = 'w-[472px] max-h-[710px] overflow-y-auto rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur shadow-xl backdrop-blur-sm'
|
||||||
const ICON_BOX_BASE = 'flex shrink-0 items-center justify-center overflow-hidden border-[0.5px] border-components-panel-border-subtle bg-background-default-dodge'
|
const ICON_BOX_BASE = 'flex shrink-0 items-center justify-center overflow-hidden border-[0.5px] border-components-panel-border-subtle bg-background-default-dodge'
|
||||||
@ -175,18 +176,20 @@ function TemplatesSection({ templates, t }: {
|
|||||||
const descriptionText = template.overview
|
const descriptionText = template.overview
|
||||||
const formattedUsedCount = formatUsedCount(template.usage_count, { precision: 0, rounding: 'floor' })
|
const formattedUsedCount = formatUsedCount(template.usage_count, { precision: 0, rounding: 'floor' })
|
||||||
const usedLabel = t('usedCount', { ns: 'plugin', num: formattedUsedCount || 0 })
|
const usedLabel = t('usedCount', { ns: 'plugin', num: formattedUsedCount || 0 })
|
||||||
const iconBgStyle = template.icon_background
|
const iconUrl = getTemplateIconUrl(template)
|
||||||
? { backgroundColor: template.icon_background }
|
|
||||||
: undefined
|
|
||||||
return (
|
return (
|
||||||
<DropdownItem
|
<DropdownItem
|
||||||
key={template.id}
|
key={template.id}
|
||||||
href={getMarketplaceUrl(`/templates/${template.publisher_handle}/${template.template_name}`, { templateId: template.id })}
|
href={getMarketplaceUrl(`/templates/${template.publisher_handle}/${template.template_name}`, { templateId: template.id })}
|
||||||
icon={(
|
icon={(
|
||||||
<div className="flex shrink-0 items-start py-1">
|
<div className="flex shrink-0 items-start py-1">
|
||||||
<IconBox shape="rounded-lg" style={iconBgStyle}>
|
<AppIcon
|
||||||
<span className="text-xl leading-[1.2]">{template.icon || '📄'}</span>
|
size="small"
|
||||||
</IconBox>
|
iconType={iconUrl ? 'image' : 'emoji'}
|
||||||
|
icon={iconUrl ? undefined : (template.icon || '📄')}
|
||||||
|
imageUrl={iconUrl || undefined}
|
||||||
|
background={template.icon_background || undefined}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
|||||||
@ -58,6 +58,14 @@ export const getPluginIconInMarketplace = (plugin: Plugin) => {
|
|||||||
return `${MARKETPLACE_API_PREFIX}/plugins/${plugin.org}/${plugin.name}/icon`
|
return `${MARKETPLACE_API_PREFIX}/plugins/${plugin.org}/${plugin.name}/icon`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const getTemplateIconUrl = (template: { id: string, icon?: string, icon_file_key?: string }): string => {
|
||||||
|
if (template.icon?.startsWith('http'))
|
||||||
|
return template.icon
|
||||||
|
if (template.icon_file_key)
|
||||||
|
return `${MARKETPLACE_API_PREFIX}/templates/${template.id}/icon`
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
export const getCreatorAvatarUrl = (uniqueHandle: string) => {
|
export const getCreatorAvatarUrl = (uniqueHandle: string) => {
|
||||||
return `${MARKETPLACE_API_PREFIX}/creators/${uniqueHandle}/avatar`
|
return `${MARKETPLACE_API_PREFIX}/creators/${uniqueHandle}/avatar`
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user