mirror of
https://github.com/langgenius/dify.git
synced 2026-05-03 08:58:09 +08:00
feat: add disableOrgLink prop to Card component and update OrgInfo to conditionally render organization link
This commit is contained in:
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
|
||||
@ -93,6 +93,7 @@ const CardWrapperComponent = ({
|
||||
<Card
|
||||
key={plugin.name}
|
||||
payload={plugin}
|
||||
disableOrgLink
|
||||
footer={(
|
||||
<CardMoreInfo
|
||||
tags={tagLabels}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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 />
|
||||
|
||||
@ -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' }),
|
||||
}
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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
88
web/utils/template.ts
Normal 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) })
|
||||
}
|
||||
Reference in New Issue
Block a user