feat: add disableOrgLink prop to Card component and update OrgInfo to conditionally render organization link

This commit is contained in:
yessenia
2026-02-05 23:46:35 +08:00
parent ccaccc8b34
commit 08508b006d
11 changed files with 148 additions and 31 deletions

View File

@ -8,6 +8,7 @@ type Props = {
packageName?: string
packageNameClassName?: string
downloadCount?: number
linkToOrg?: boolean
}
const OrgInfo = ({
@ -16,6 +17,7 @@ const OrgInfo = ({
packageName,
packageNameClassName,
downloadCount,
linkToOrg = true,
}: Props) => {
// New format: "by {orgName} · {downloadCount} installs" (for marketplace cards)
if (downloadCount !== undefined) {
@ -24,15 +26,23 @@ const OrgInfo = ({
{orgName && (
<span className="shrink-0">
<span className="mr-1 text-text-tertiary">by</span>
<Link
href={`/creators/${orgName}`}
target="_blank"
rel="noopener noreferrer"
className="hover:text-text-secondary hover:underline"
onClick={e => e.stopPropagation()}
>
{orgName}
</Link>
{linkToOrg
? (
<Link
href={`/creators/${orgName}`}
target="_blank"
rel="noopener noreferrer"
className="hover:text-text-secondary hover:underline"
onClick={e => e.stopPropagation()}
>
{orgName}
</Link>
)
: (
<span className="text-text-tertiary">
{orgName}
</span>
)}
</span>
)}
<span className="shrink-0">·</span>

View File

@ -32,6 +32,7 @@ export type Props = {
isLoading?: boolean
loadingFileName?: string
limitedInstall?: boolean
disableOrgLink?: boolean
}
const Card = ({
@ -46,6 +47,7 @@ const Card = ({
isLoading = false,
loadingFileName,
limitedInstall = false,
disableOrgLink = false,
}: Props) => {
const locale = useGetLanguage()
const { t } = useTranslation()
@ -87,6 +89,7 @@ const Card = ({
className="mt-0.5"
orgName={org}
downloadCount={install_count}
linkToOrg={!disableOrgLink}
/>
</div>
</div>

View File

@ -8,6 +8,7 @@ import marketPlaceBg from '@/public/marketplace/hero-bg.jpg'
import marketplaceGradientNoise from '@/public/marketplace/hero-gradient-noise.svg'
import { cn } from '@/utils/classnames'
import PluginTypeSwitch from '../plugin-type-switch'
import { useMarketplaceData } from '../state'
type DescriptionProps = {
className?: string
@ -28,6 +29,10 @@ export const Description = ({
marketplaceNav,
}: DescriptionProps) => {
const { t } = useTranslation('plugin')
const { creationType } = useMarketplaceData()
const isTemplatesView = creationType === 'templates'
const heroTitleKey = isTemplatesView ? 'marketplace.templatesHeroTitle' : 'marketplace.pluginsHeroTitle'
const heroSubtitleKey = isTemplatesView ? 'marketplace.templatesHeroSubtitle' : 'marketplace.pluginsHeroSubtitle'
const rafRef = useRef<number | null>(null)
const lastProgressRef = useRef(0)
const titleRef = useRef<HTMLDivElement | null>(null)
@ -67,7 +72,9 @@ export const Description = ({
// Use requestAnimationFrame for smooth updates
rafRef.current = requestAnimationFrame(() => {
const scrollTop = Math.round(container.scrollTop)
const rawProgress = Math.min(Math.max(scrollTop / MAX_SCROLL, 0), 1)
const heightDelta = container.scrollHeight - container.clientHeight
const effectiveMaxScroll = Math.max(1, Math.min(MAX_SCROLL, heightDelta))
const rawProgress = Math.min(Math.max(scrollTop / effectiveMaxScroll, 0), 1)
const snappedProgress = rawProgress >= 0.95
? 1
: rawProgress <= 0.05
@ -153,10 +160,10 @@ export const Description = ({
}}
>
<h1 className="title-4xl-semi-bold mb-2 shrink-0 text-text-primary-on-surface">
{t('marketplace.heroTitle')}
{t(heroTitleKey)}
</h1>
<h2 className="body-md-regular shrink-0 text-text-secondary-on-surface">
{t('marketplace.heroSubtitle')}
{t(heroSubtitleKey)}
</h2>
</motion.div>

View File

@ -93,6 +93,7 @@ const CardWrapperComponent = ({
<Card
key={plugin.name}
payload={plugin}
disableOrgLink
footer={(
<CardMoreInfo
tags={tagLabels}

View File

@ -8,6 +8,7 @@ import { useCallback, useMemo } from 'react'
import useTheme from '@/hooks/use-theme'
import { getLanguage } from '@/i18n-config/language'
import { cn } from '@/utils/classnames'
import { formatUsedCount } from '@/utils/template'
import { getMarketplaceUrl } from '@/utils/var'
type TemplateCardProps = {
@ -80,16 +81,7 @@ const TemplateCardComponent = ({
const visibleTags = tags?.slice(0, MAX_VISIBLE_TAGS) || []
const remainingTagsCount = tags ? Math.max(0, tags.length - MAX_VISIBLE_TAGS) : 0
// Format used count (e.g., 134000 -> "134k")
const formatUsedCount = (count?: number) => {
if (!count)
return null
if (count >= 1000)
return `${Math.floor(count / 1000)}k`
return String(count)
}
const formattedUsedCount = formatUsedCount(used_count)
const formattedUsedCount = formatUsedCount(used_count, { precision: 0, rounding: 'floor' })
return (
<div

View File

@ -3,6 +3,7 @@
import { useMarketplaceSearchMode } from './atoms'
import { Description } from './description'
import SearchResultsHeader from './search-results-header'
import { useMarketplaceData } from './state'
type MarketplaceHeaderProps = {
descriptionClassName?: string
@ -10,7 +11,11 @@ type MarketplaceHeaderProps = {
}
const MarketplaceHeader = ({ descriptionClassName, marketplaceNav }: MarketplaceHeaderProps) => {
const isSearchMode = useMarketplaceSearchMode()
const { creationType, isSearchMode: templatesSearchMode } = useMarketplaceData()
const pluginsSearchMode = useMarketplaceSearchMode()
// Use templates search mode when viewing templates, otherwise use plugins search mode
const isSearchMode = creationType === 'templates' ? templatesSearchMode : pluginsSearchMode
if (isSearchMode)
return <SearchResultsHeader />

View File

@ -2,8 +2,11 @@ import type { ActivePluginType } from './constants'
import { parseAsArrayOf, parseAsString, parseAsStringEnum } from 'nuqs/server'
import { PLUGIN_TYPE_SEARCH_MAP } from './constants'
export type CreationType = 'plugins' | 'templates'
export const marketplaceSearchParamsParsers = {
category: parseAsStringEnum<ActivePluginType>(Object.values(PLUGIN_TYPE_SEARCH_MAP) as ActivePluginType[]).withDefault('all').withOptions({ history: 'replace', clearOnDefault: false }),
q: parseAsString.withDefault('').withOptions({ history: 'replace' }),
tags: parseAsArrayOf(parseAsString).withDefault([]).withOptions({ history: 'replace' }),
creationType: parseAsStringEnum<CreationType>(['plugins', 'templates']).withDefault('plugins').withOptions({ history: 'replace' }),
}

View File

@ -78,10 +78,14 @@ export const SubmitRequestDropdown = () => {
)
}
export const CreationTypeTabs = () => {
type CreationTypeTabsProps = {
creationType?: string
}
export const CreationTypeTabs = ({ creationType: creationTypeProp }: CreationTypeTabsProps = {}) => {
const { t } = useTranslation()
const searchParams = useSearchParams()
const creationType = searchParams.get('creationType') || 'plugins'
const creationType = creationTypeProp || searchParams.get('creationType') || 'plugins'
return (
<div className="flex items-center gap-1">

View File

@ -196,13 +196,13 @@
"marketplace.discover": "Discover",
"marketplace.empower": "Empower your AI development",
"marketplace.featured": "Featured",
"marketplace.heroSubtitle": "Use community-built plugins to power your AI development.",
"marketplace.heroTitle": "Discover. Extend. Build.",
"marketplace.installs": "installs",
"marketplace.moreFrom": "More from Marketplace",
"marketplace.noPluginFound": "No plugin found",
"marketplace.ourTopPicks": "Our top picks to get you started",
"marketplace.partnerTip": "Verified by a Dify partner",
"marketplace.pluginsHeroSubtitle": "Use community-built plugins to power your AI development.",
"marketplace.pluginsHeroTitle": "Discover. Extend. Build.",
"marketplace.pluginsResult": "{{num}} results",
"marketplace.searchBreadcrumbMarketplace": "Marketplace",
"marketplace.searchBreadcrumbSearch": "Search",
@ -221,6 +221,8 @@
"marketplace.sortOption.mostPopular": "Most Popular",
"marketplace.sortOption.newlyReleased": "Newly Released",
"marketplace.sortOption.recentlyUpdated": "Recently Updated",
"marketplace.templatesHeroSubtitle": "Community-built workflow templates — ready to use, remix, and deploy.",
"marketplace.templatesHeroTitle": "Create. Remix. Deploy.",
"marketplace.verifiedTip": "Verified by Dify",
"marketplace.viewMore": "View more",
"metadata.title": "Plugins",
@ -229,7 +231,6 @@
"pluginInfoModal.repository": "Repository",
"pluginInfoModal.title": "Plugin info",
"plugins": "Plugins",
"templates": "Templates",
"privilege.admins": "Admins",
"privilege.everyone": "Everyone",
"privilege.noone": "No one",
@ -262,6 +263,7 @@
"task.installingWithSuccess": "Installing {{installingLength}} plugins, {{successLength}} success.",
"task.runningPlugins": "Installing Plugins",
"task.successPlugins": "Successfully Installed Plugins",
"templates": "Templates",
"upgrade.close": "Close",
"upgrade.description": "About to install the following plugin",
"upgrade.successfulTitle": "Install successful",

View File

@ -196,13 +196,13 @@
"marketplace.discover": "探索",
"marketplace.empower": "助力您的 AI 开发",
"marketplace.featured": "精选",
"marketplace.heroSubtitle": "使用社区构建的插件为您的 AI 开发提供动力。",
"marketplace.heroTitle": "探索。扩展。构建。",
"marketplace.installs": "次安装",
"marketplace.moreFrom": "更多来自市场",
"marketplace.noPluginFound": "未找到插件",
"marketplace.ourTopPicks": "我们精选推荐",
"marketplace.partnerTip": "此插件由 Dify 合作伙伴认证",
"marketplace.pluginsHeroSubtitle": "使用社区构建的插件为您的 AI 开发提供动力。",
"marketplace.pluginsHeroTitle": "探索。扩展。构建。",
"marketplace.pluginsResult": "{{num}} 个插件结果",
"marketplace.searchBreadcrumbMarketplace": "市场",
"marketplace.searchBreadcrumbSearch": "搜索",
@ -221,6 +221,8 @@
"marketplace.sortOption.mostPopular": "最受欢迎",
"marketplace.sortOption.newlyReleased": "最新发布",
"marketplace.sortOption.recentlyUpdated": "最近更新",
"marketplace.templatesHeroSubtitle": "社区构建的工作流模板 —— 随时可使用、复刻和部署。",
"marketplace.templatesHeroTitle": "创建。复刻。部署。",
"marketplace.verifiedTip": "此插件由 Dify 认证",
"marketplace.viewMore": "查看更多",
"metadata.title": "插件",
@ -229,7 +231,6 @@
"pluginInfoModal.repository": "仓库",
"pluginInfoModal.title": "插件信息",
"plugins": "插件",
"templates": "模板",
"privilege.admins": "管理员",
"privilege.everyone": "所有人",
"privilege.noone": "无人",
@ -262,6 +263,7 @@
"task.installingWithSuccess": "{{installingLength}} 个插件安装中,{{successLength}} 安装成功",
"task.runningPlugins": "正在安装的插件",
"task.successPlugins": "安装成功的插件",
"templates": "模板",
"upgrade.close": "关闭",
"upgrade.description": "即将安装以下插件",
"upgrade.successfulTitle": "安装成功",

88
web/utils/template.ts Normal file
View File

@ -0,0 +1,88 @@
import type { Viewport } from 'reactflow'
import type { Edge, Node } from '@/app/components/workflow/types'
import { load as yamlLoad } from 'js-yaml'
type GraphPayload = {
nodes?: Node[]
edges?: Edge[]
viewport?: Viewport
}
type DslPayload = {
workflow?: {
graph?: GraphPayload
}
graph?: GraphPayload
} | null
export type ParsedGraph = {
nodes: Node[]
edges: Edge[]
viewport: Viewport
} | null
export const parseGraphFromDsl = (dslContent: string): ParsedGraph => {
if (!dslContent)
return null
try {
const data = yamlLoad(dslContent) as DslPayload
const graph = data?.workflow?.graph ?? data?.graph
if (!graph || !graph.nodes || !graph.edges)
return null
return {
nodes: graph.nodes || [],
edges: graph.edges || [],
viewport: graph.viewport || { x: 0, y: 0, zoom: 0.5 },
}
}
catch {
return null
}
}
type UsedCountFormatOptions = {
precision?: number
rounding?: 'round' | 'floor'
}
export const formatUsedCount = (count?: number, options: UsedCountFormatOptions = {}) => {
if (!count)
return null
if (count < 1000)
return String(count)
const precision = options.precision ?? 1
const rounding = options.rounding ?? 'round'
const base = count / 1000
const factor = 10 ** precision
const rounded = rounding === 'floor'
? Math.floor(base * factor) / factor
: Math.round(base * factor) / factor
const display = precision <= 0
? String(rounded)
: (rounded % 1 === 0 ? String(rounded) : rounded.toFixed(precision))
return `${display}k`
}
type TranslationFn = (key: string, options?: Record<string, unknown>) => string
export const formatRelativeTime = (dateStr: string, t: TranslationFn) => {
const date = new Date(dateStr)
const now = new Date()
const diffMs = now.getTime() - date.getTime()
const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24))
if (diffDays < 1)
return t('detail.today')
if (diffDays < 7)
return t('detail.daysAgo', { count: diffDays })
if (diffDays < 30)
return t('detail.weeksAgo', { count: Math.floor(diffDays / 7) })
if (diffDays < 365)
return t('detail.monthsAgo', { count: Math.floor(diffDays / 30) })
return t('detail.yearsAgo', { count: Math.floor(diffDays / 365) })
}