From 08508b006db2a7113c65d6968e9410770e8ac372 Mon Sep 17 00:00:00 2001 From: yessenia Date: Thu, 5 Feb 2026 23:46:35 +0800 Subject: [PATCH] feat: add disableOrgLink prop to Card component and update OrgInfo to conditionally render organization link --- .../components/plugins/card/base/org-info.tsx | 28 ++++-- web/app/components/plugins/card/index.tsx | 3 + .../plugins/marketplace/description/index.tsx | 13 ++- .../plugins/marketplace/list/card-wrapper.tsx | 1 + .../marketplace/list/template-card.tsx | 12 +-- .../marketplace/marketplace-header.tsx | 7 +- .../plugins/marketplace/search-params.ts | 3 + .../plugins/plugin-page/nav-operations.tsx | 8 +- web/i18n/en-US/plugin.json | 8 +- web/i18n/zh-Hans/plugin.json | 8 +- web/utils/template.ts | 88 +++++++++++++++++++ 11 files changed, 148 insertions(+), 31 deletions(-) create mode 100644 web/utils/template.ts diff --git a/web/app/components/plugins/card/base/org-info.tsx b/web/app/components/plugins/card/base/org-info.tsx index 004ef0f37e..80733a1c0e 100644 --- a/web/app/components/plugins/card/base/org-info.tsx +++ b/web/app/components/plugins/card/base/org-info.tsx @@ -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 && ( by - e.stopPropagation()} - > - {orgName} - + {linkToOrg + ? ( + e.stopPropagation()} + > + {orgName} + + ) + : ( + + {orgName} + + )} )} · diff --git a/web/app/components/plugins/card/index.tsx b/web/app/components/plugins/card/index.tsx index 8c4f35718f..8a508f64ca 100644 --- a/web/app/components/plugins/card/index.tsx +++ b/web/app/components/plugins/card/index.tsx @@ -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} /> diff --git a/web/app/components/plugins/marketplace/description/index.tsx b/web/app/components/plugins/marketplace/description/index.tsx index 72f612f5cf..797f76ce35 100644 --- a/web/app/components/plugins/marketplace/description/index.tsx +++ b/web/app/components/plugins/marketplace/description/index.tsx @@ -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(null) const lastProgressRef = useRef(0) const titleRef = useRef(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 = ({ }} >

- {t('marketplace.heroTitle')} + {t(heroTitleKey)}

- {t('marketplace.heroSubtitle')} + {t(heroSubtitleKey)}

diff --git a/web/app/components/plugins/marketplace/list/card-wrapper.tsx b/web/app/components/plugins/marketplace/list/card-wrapper.tsx index ffeb6c1716..0328d1952e 100644 --- a/web/app/components/plugins/marketplace/list/card-wrapper.tsx +++ b/web/app/components/plugins/marketplace/list/card-wrapper.tsx @@ -93,6 +93,7 @@ const CardWrapperComponent = ({ "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 (
{ - 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 diff --git a/web/app/components/plugins/marketplace/search-params.ts b/web/app/components/plugins/marketplace/search-params.ts index ad0b16977f..32dd1743c9 100644 --- a/web/app/components/plugins/marketplace/search-params.ts +++ b/web/app/components/plugins/marketplace/search-params.ts @@ -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(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(['plugins', 'templates']).withDefault('plugins').withOptions({ history: 'replace' }), } diff --git a/web/app/components/plugins/plugin-page/nav-operations.tsx b/web/app/components/plugins/plugin-page/nav-operations.tsx index 91c05fd65e..a638f8deb8 100644 --- a/web/app/components/plugins/plugin-page/nav-operations.tsx +++ b/web/app/components/plugins/plugin-page/nav-operations.tsx @@ -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 (
diff --git a/web/i18n/en-US/plugin.json b/web/i18n/en-US/plugin.json index 9cbef2d774..ad16d92c67 100644 --- a/web/i18n/en-US/plugin.json +++ b/web/i18n/en-US/plugin.json @@ -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", diff --git a/web/i18n/zh-Hans/plugin.json b/web/i18n/zh-Hans/plugin.json index 4722d22c32..935093e09e 100644 --- a/web/i18n/zh-Hans/plugin.json +++ b/web/i18n/zh-Hans/plugin.json @@ -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": "安装成功", diff --git a/web/utils/template.ts b/web/utils/template.ts new file mode 100644 index 0000000000..f041d6c150 --- /dev/null +++ b/web/utils/template.ts @@ -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 + +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) }) +}