Files
dify/web/app/components/plugins/card/index.tsx
CodingOnStar c167ee199c 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.
2026-03-12 14:58:16 +08:00

121 lines
4.1 KiB
TypeScript

'use client'
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 {
renderI18nObject,
} from '@/i18n-config'
import { Theme } from '@/types/app'
import { cn } from '@/utils/classnames'
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'
import Placeholder from './base/placeholder'
import Title from './base/title'
export type Props = {
className?: string
payload: Plugin
titleLeft?: React.ReactNode
installed?: boolean
installFailed?: boolean
hideCornerMark?: boolean
descriptionLineRows?: number
footer?: React.ReactNode
isLoading?: boolean
loadingFileName?: string
limitedInstall?: boolean
}
const Card = ({
className,
payload,
titleLeft,
installed,
installFailed,
hideCornerMark,
descriptionLineRows = 2,
footer,
isLoading = false,
loadingFileName,
limitedInstall = false,
}: Props) => {
const locale = useGetLanguage()
const { t } = useTranslation()
const { categoriesMap } = useCategories(true)
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 = 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')
const wrapClassName = cn('hover-bg-components-panel-on-panel-item-bg relative overflow-hidden rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-on-panel-item-bg shadow-xs', className)
if (isLoading) {
return (
<Placeholder
wrapClassName={wrapClassName}
loadingFileName={loadingFileName!}
/>
)
}
return (
<div className={wrapClassName}>
<div className={cn('p-4 pb-3', limitedInstall && 'pb-1')}>
{!hideCornerMark && <CornerMark text={categoriesMap[type === 'bundle' ? type : category]?.label} />}
{/* Header */}
<div className="flex">
<Icon src={iconSrc} installed={installed} installFailed={installFailed} />
<div className="ml-3 w-0 grow">
<div className="flex h-5 items-center">
<Title title={getLocalizedText(label)} />
{isPartner && <Partner className="ml-0.5 h-4 w-4" text={t('marketplace.partnerTip', { ns: 'plugin' })} />}
{verified && <Verified className="ml-0.5 h-4 w-4" text={t('marketplace.verifiedTip', { ns: 'plugin' })} />}
{titleLeft}
{' '}
{/* This can be version badge */}
</div>
<OrgInfo
className="mt-0.5"
orgName={org}
packageName={name}
/>
</div>
</div>
<Description
className="mt-3"
text={getLocalizedText(brief)}
descriptionLineRows={descriptionLineRows}
/>
{!!footer && <div>{footer}</div>}
</div>
{limitedInstall
&& (
<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="z-10 grow text-text-secondary system-xs-regular">
{t('installModal.installWarning', { ns: 'plugin' })}
</p>
</div>
)}
</div>
)
}
export default React.memo(Card)