mirror of
https://github.com/langgenius/dify.git
synced 2026-05-03 17:08:03 +08:00
feat: implement dynamic plugin card icon URL generation
Added a utility function to generate plugin card icon URLs based on the plugin's source and workspace context. Updated the Card component to utilize this function for determining the correct icon source. Enhanced unit tests to verify the correct URL generation for both marketplace and package icons.
This commit is contained in:
@ -2,6 +2,7 @@ import type { Plugin } from '../../types'
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import * as React from 'react'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { API_PREFIX, MARKETPLACE_API_PREFIX } from '@/config'
|
||||
import { PluginCategoryEnum } from '../../types'
|
||||
import Card from '../index'
|
||||
|
||||
@ -40,6 +41,12 @@ vi.mock('@/utils/format', () => ({
|
||||
formatNumber: (num: number) => num.toLocaleString(),
|
||||
}))
|
||||
|
||||
vi.mock('@/context/app-context', () => ({
|
||||
useSelector: (selector: (value: { currentWorkspace: { id: string } }) => string) => selector({
|
||||
currentWorkspace: { id: 'workspace-123' },
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('@/utils/mcp', () => ({
|
||||
shouldUseMcpIcon: (src: unknown) => typeof src === 'object' && src !== null && (src as { content?: string })?.content === '🔗',
|
||||
}))
|
||||
@ -189,6 +196,36 @@ describe('Card', () => {
|
||||
expect(iconElement).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should normalize package icon filenames to workspace icon urls', () => {
|
||||
const plugin = createMockPlugin({
|
||||
from: 'package',
|
||||
icon: 'custom-icon.png',
|
||||
})
|
||||
|
||||
const { container } = render(<Card payload={plugin} />)
|
||||
|
||||
const iconElement = container.querySelector('[style*="background-image"]')
|
||||
expect(iconElement).toBeInTheDocument()
|
||||
expect(iconElement).toHaveStyle({
|
||||
backgroundImage: `url(${API_PREFIX}/workspaces/current/plugin/icon?tenant_id=workspace-123&filename=custom-icon.png)`,
|
||||
})
|
||||
})
|
||||
|
||||
it('should normalize marketplace icon filenames to marketplace icon urls', () => {
|
||||
const plugin = createMockPlugin({
|
||||
from: 'marketplace',
|
||||
icon: 'custom-icon.png',
|
||||
})
|
||||
|
||||
const { container } = render(<Card payload={plugin} />)
|
||||
|
||||
const iconElement = container.querySelector('[style*="background-image"]')
|
||||
expect(iconElement).toBeInTheDocument()
|
||||
expect(iconElement).toHaveStyle({
|
||||
backgroundImage: `url(${MARKETPLACE_API_PREFIX}/plugins/${plugin.org}/${plugin.name}/icon)`,
|
||||
})
|
||||
})
|
||||
|
||||
it('should use icon_dark when theme is dark and icon_dark is provided', () => {
|
||||
// Set theme to dark
|
||||
mockTheme = 'dark'
|
||||
|
||||
@ -3,6 +3,7 @@ import type { Plugin } from '../types'
|
||||
import { useTranslation } from '#i18n'
|
||||
import { RiAlertFill } from '@remixicon/react'
|
||||
import * as React from 'react'
|
||||
import { useSelector } from '@/context/app-context'
|
||||
import { useGetLanguage } from '@/context/i18n'
|
||||
import useTheme from '@/hooks/use-theme'
|
||||
import {
|
||||
@ -14,6 +15,7 @@ import Partner from '../base/badges/partner'
|
||||
import Verified from '../base/badges/verified'
|
||||
import Icon from '../card/base/card-icon'
|
||||
import { useCategories } from '../hooks'
|
||||
import { getPluginCardIconUrl } from '../utils'
|
||||
import CornerMark from './base/corner-mark'
|
||||
import Description from './base/description'
|
||||
import OrgInfo from './base/org-info'
|
||||
@ -50,9 +52,14 @@ const Card = ({
|
||||
const locale = useGetLanguage()
|
||||
const { t } = useTranslation()
|
||||
const { categoriesMap } = useCategories(true)
|
||||
const { category, type, name, org, label, brief, icon, icon_dark, verified, badges = [] } = payload
|
||||
const currentWorkspaceId = useSelector(s => s.currentWorkspace.id)
|
||||
const { category, type, name, org, label, brief, icon, icon_dark, verified, badges = [], from } = payload
|
||||
const { theme } = useTheme()
|
||||
const iconSrc = theme === Theme.dark && icon_dark ? icon_dark : icon
|
||||
const iconSrc = getPluginCardIconUrl(
|
||||
{ from, name, org, type },
|
||||
theme === Theme.dark && icon_dark ? icon_dark : icon,
|
||||
currentWorkspaceId,
|
||||
)
|
||||
const getLocalizedText = (obj: Record<string, string> | undefined) =>
|
||||
obj ? renderI18nObject(obj, locale) : ''
|
||||
const isPartner = badges.includes('partner')
|
||||
@ -101,7 +108,7 @@ const Card = ({
|
||||
&& (
|
||||
<div className="relative flex h-8 items-center gap-x-2 px-3 after:absolute after:bottom-0 after:left-0 after:right-0 after:top-0 after:bg-toast-warning-bg after:opacity-40">
|
||||
<RiAlertFill className="h-3 w-3 shrink-0 text-text-warning-secondary" />
|
||||
<p className="system-xs-regular z-10 grow text-text-secondary">
|
||||
<p className="z-10 grow text-text-secondary system-xs-regular">
|
||||
{t('installModal.installWarning', { ns: 'plugin' })}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@ -1,7 +1,9 @@
|
||||
import type {
|
||||
TagKey,
|
||||
} from './constants'
|
||||
import type { Plugin } from './types'
|
||||
|
||||
import { API_PREFIX, MARKETPLACE_API_PREFIX } from '@/config'
|
||||
import {
|
||||
categoryKeys,
|
||||
tagKeys,
|
||||
@ -14,3 +16,27 @@ export const getValidTagKeys = (tags: TagKey[]) => {
|
||||
export const getValidCategoryKeys = (category?: string) => {
|
||||
return categoryKeys.find(key => key === category)
|
||||
}
|
||||
|
||||
const hasUrlProtocol = (value: string) => /^[a-z][a-z\d+.-]*:/i.test(value)
|
||||
|
||||
export const getPluginCardIconUrl = (
|
||||
plugin: Pick<Plugin, 'from' | 'name' | 'org' | 'type'>,
|
||||
icon: string | undefined,
|
||||
tenantId: string,
|
||||
) => {
|
||||
if (!icon)
|
||||
return ''
|
||||
|
||||
if (hasUrlProtocol(icon) || icon.startsWith('/'))
|
||||
return icon
|
||||
|
||||
if (plugin.from === 'marketplace') {
|
||||
const basePath = plugin.type === 'bundle' ? 'bundles' : 'plugins'
|
||||
return `${MARKETPLACE_API_PREFIX}/${basePath}/${plugin.org}/${plugin.name}/icon`
|
||||
}
|
||||
|
||||
if (!tenantId)
|
||||
return icon
|
||||
|
||||
return `${API_PREFIX}/workspaces/current/plugin/icon?tenant_id=${tenantId}&filename=${icon}`
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user