feat: enhance templates marketplace with new sorting options and improved template data structure

This commit is contained in:
yessenia 2026-02-11 03:40:41 +08:00
parent 5c6da34539
commit 6ef87550e6
18 changed files with 180 additions and 204 deletions

View File

@ -3,7 +3,7 @@ import type { PluginsSort, SearchParamsFromCollection } from './types'
import { atom, useAtom, useAtomValue, useSetAtom } from 'jotai'
import { useQueryState } from 'nuqs'
import { useCallback } from 'react'
import { DEFAULT_SORT, getValidatedPluginCategory, getValidatedTemplateCategory, PLUGIN_CATEGORY_WITH_COLLECTIONS } from './constants'
import { CATEGORY_ALL, DEFAULT_SORT, getValidatedPluginCategory, getValidatedTemplateCategory, PLUGIN_CATEGORY_WITH_COLLECTIONS } from './constants'
import { CREATION_TYPE, marketplaceSearchParamsParsers } from './search-params'
const marketplaceSortAtom = atom<PluginsSort>(DEFAULT_SORT)
@ -53,12 +53,14 @@ export function useMarketplaceSearchMode() {
const [searchTab] = useSearchTab()
const [filterPluginTags] = useFilterPluginTags()
const [activePluginCategory] = useActivePluginCategory()
const [activeTemplateCategory] = useActiveTemplateCategory()
const isPluginsView = creationType === CREATION_TYPE.plugins
const searchMode = useAtomValue(searchModeAtom)
const isSearchMode = searchTab || searchText
|| (isPluginsView && filterPluginTags.length > 0)
|| (searchMode ?? (isPluginsView && !PLUGIN_CATEGORY_WITH_COLLECTIONS.has(activePluginCategory)))
|| (!isPluginsView && activeTemplateCategory !== CATEGORY_ALL)
return isSearchMode
}

View File

@ -5,6 +5,11 @@ export const DEFAULT_SORT = {
sortOrder: 'DESC',
}
export const DEFAULT_TEMPLATE_SORT = {
sortBy: 'usage_count',
sortOrder: 'DESC',
}
export const SCROLL_BOTTOM_THRESHOLD = 100
export const CATEGORY_ALL = 'all'

View File

@ -9,7 +9,7 @@ import { PluginCategoryEnum } from '@/app/components/plugins/types'
// ================================
// Note: Import after mocks are set up
import { DEFAULT_SORT, PLUGIN_TYPE_SEARCH_MAP, SCROLL_BOTTOM_THRESHOLD } from './constants'
import { DEFAULT_SORT, DEFAULT_TEMPLATE_SORT, PLUGIN_TYPE_SEARCH_MAP, SCROLL_BOTTOM_THRESHOLD } from './constants'
import {
getFormattedPlugin,
getPluginCondition,
@ -423,6 +423,23 @@ describe('constants', () => {
})
})
describe('DEFAULT_TEMPLATE_SORT', () => {
it('should have correct default sort values for templates', () => {
expect(DEFAULT_TEMPLATE_SORT).toEqual({
sortBy: 'usage_count',
sortOrder: 'DESC',
})
})
it('should be immutable at runtime', () => {
const originalSortBy = DEFAULT_TEMPLATE_SORT.sortBy
const originalSortOrder = DEFAULT_TEMPLATE_SORT.sortOrder
expect(DEFAULT_TEMPLATE_SORT.sortBy).toBe(originalSortBy)
expect(DEFAULT_TEMPLATE_SORT.sortOrder).toBe(originalSortOrder)
})
})
describe('SCROLL_BOTTOM_THRESHOLD', () => {
it('should be 100 pixels', () => {
expect(SCROLL_BOTTOM_THRESHOLD).toBe(100)

View File

@ -0,0 +1,19 @@
export const GRID_CLASS = 'grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4'
export const GRID_DISPLAY_LIMIT = 8
export const CAROUSEL_COLUMN_CLASS = 'flex w-[calc((100%-0px)/1)] shrink-0 flex-col gap-3 sm:w-[calc((100%-12px)/2)] lg:w-[calc((100%-24px)/3)] xl:w-[calc((100%-36px)/4)]'
/** Collection name key that triggers carousel display (plugins: partners, templates: featured) */
export const CAROUSEL_COLLECTION_NAMES = {
partners: 'partners',
featured: 'featured',
} as const
export type BaseCollection = {
name: string
label: Record<string, string>
description: Record<string, string>
searchable?: boolean
search_params?: { query?: string, sort_by?: string, sort_order?: string }
}

View File

@ -2,42 +2,24 @@
import type { SearchTab } from '../search-params'
import type { SearchParamsFromCollection } from '../types'
import type { BaseCollection } from './collection-constants'
import type { Locale } from '@/i18n-config/language'
import { useLocale, useTranslation } from '#i18n'
import { RiArrowRightSLine } from '@remixicon/react'
import { getLanguage } from '@/i18n-config/language'
import { cn } from '@/utils/classnames'
import { useMarketplaceMoreClick } from '../atoms'
import { getItemKeyByField } from '../utils'
import Empty from '../empty'
import { getItemKeyByField } from '../utils'
import Carousel from './carousel'
export const GRID_CLASS = 'grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4'
export const GRID_DISPLAY_LIMIT = 8
export const CAROUSEL_COLUMN_CLASS = 'flex w-[calc((100%-0px)/1)] shrink-0 flex-col gap-3 sm:w-[calc((100%-12px)/2)] lg:w-[calc((100%-24px)/3)] xl:w-[calc((100%-36px)/4)]'
/** Collection name key that triggers carousel display (plugins: partners, templates: featured) */
export const CAROUSEL_COLLECTION_NAMES = {
partners: 'partners',
featured: 'featured',
} as const
export type BaseCollection = {
name: string
label: Record<string, string>
description: Record<string, string>
searchable?: boolean
search_params?: { query?: string, sort_by?: string, sort_order?: string }
}
import { CAROUSEL_COLUMN_CLASS, GRID_CLASS, GRID_DISPLAY_LIMIT } from './collection-constants'
type ViewMoreButtonProps = {
searchParams?: SearchParamsFromCollection
searchTab?: SearchTab
}
function ViewMoreButton({ searchParams, searchTab }: ViewMoreButtonProps) {
export function ViewMoreButton({ searchParams, searchTab }: ViewMoreButtonProps) {
const { t } = useTranslation()
const onMoreClick = useMarketplaceMoreClick()
@ -52,8 +34,6 @@ function ViewMoreButton({ searchParams, searchTab }: ViewMoreButtonProps) {
)
}
export { ViewMoreButton }
type CollectionHeaderProps<TCollection extends BaseCollection> = {
collection: TCollection
itemsLength: number
@ -62,7 +42,7 @@ type CollectionHeaderProps<TCollection extends BaseCollection> = {
viewMore: React.ReactNode
}
function CollectionHeader<TCollection extends BaseCollection>({
export function CollectionHeader<TCollection extends BaseCollection>({
collection,
itemsLength,
locale,
@ -87,8 +67,6 @@ function CollectionHeader<TCollection extends BaseCollection>({
)
}
export { CarouselCollection, CollectionHeader }
type CarouselCollectionProps<TItem> = {
items: TItem[]
getItemKey: (item: TItem) => string
@ -96,7 +74,7 @@ type CarouselCollectionProps<TItem> = {
cardContainerClassName?: string
}
function CarouselCollection<TItem>({
export function CarouselCollection<TItem>({
items,
getItemKey,
renderCard,
@ -132,7 +110,7 @@ function CarouselCollection<TItem>({
type CollectionListProps<TItem, TCollection extends BaseCollection> = {
collections: TCollection[]
collectionItemsMap: Record<string, TItem[]>
/** Field name to use as item key (e.g. 'plugin_id', 'template_id'). */
/** Field name to use as item key (e.g. 'plugin_id', 'id'). */
itemKeyField: keyof TItem
renderCard: (item: TItem) => React.ReactNode
/** Collection names that use carousel layout (e.g. ['partners'], ['featured']). */
@ -186,22 +164,22 @@ function CollectionList<TItem, TCollection extends BaseCollection>({
/>
{isCarouselCollection
? (
<CarouselCollection
items={items}
getItemKey={(item) => getItemKeyByField(item, itemKeyField)}
renderCard={renderCard}
cardContainerClassName={cardContainerClassName}
/>
)
<CarouselCollection
items={items}
getItemKey={item => getItemKeyByField(item, itemKeyField)}
renderCard={renderCard}
cardContainerClassName={cardContainerClassName}
/>
)
: (
<div className={cn(gridClassName, cardContainerClassName)}>
{items.slice(0, GRID_DISPLAY_LIMIT).map(item => (
<div key={getItemKeyByField(item, itemKeyField)}>
{renderCard(item)}
</div>
))}
</div>
)}
<div className={cn(gridClassName, cardContainerClassName)}>
{items.slice(0, GRID_DISPLAY_LIMIT).map(item => (
<div key={getItemKeyByField(item, itemKeyField)}>
{renderCard(item)}
</div>
))}
</div>
)}
</div>
)
})

View File

@ -4,7 +4,7 @@ import type { Template } from '../types'
import type { Plugin } from '@/app/components/plugins/types'
import Empty from '../empty'
import CardWrapper from './card-wrapper'
import { GRID_CLASS } from './collection-list'
import { GRID_CLASS } from './collection-constants'
import TemplateCard from './template-card'
type PluginsVariant = {
@ -44,7 +44,7 @@ const FlatList = (props: FlatListProps) => {
<div className={GRID_CLASS}>
{items.map(template => (
<TemplateCard
key={template.template_id}
key={template.id}
template={template}
/>
))}

View File

@ -5,7 +5,7 @@ import type { PluginCollection } from '../types'
import { cn } from '@/utils/classnames'
import Empty from '../empty'
import CardWrapper from './card-wrapper'
import { GRID_CLASS } from './collection-list'
import { GRID_CLASS } from './collection-constants'
import ListWithCollection from './list-with-collection'
type ListProps = {

View File

@ -3,7 +3,8 @@
import type { PluginCollection, Template, TemplateCollection } from '../types'
import type { Plugin } from '@/app/components/plugins/types'
import CardWrapper from './card-wrapper'
import CollectionList, { CAROUSEL_COLLECTION_NAMES } from './collection-list'
import { CAROUSEL_COLLECTION_NAMES } from './collection-constants'
import CollectionList from './collection-list'
import TemplateCard from './template-card'
type BaseProps = {
@ -71,7 +72,7 @@ const ListWithCollection = (props: ListWithCollectionProps) => {
<CollectionList
collections={collections}
collectionItemsMap={collectionItemsMap}
itemKeyField="template_id"
itemKeyField="id"
renderCard={renderTemplateCard}
carouselCollectionNames={[CAROUSEL_COLLECTION_NAMES.featured]}
viewMoreSearchTab="templates"

View File

@ -64,7 +64,7 @@ describe('ListWrapper flat rendering', () => {
it('renders template flat list when template items exist', () => {
mockMarketplaceData.creationType = 'templates'
mockMarketplaceData.templates = [{ template_id: 't1' } as Template]
mockMarketplaceData.templates = [{ id: 't1' } as Template]
render(<ListWrapper />)

View File

@ -1,13 +1,14 @@
'use client'
import type { Template } from '../types'
import { useLocale } from '#i18n'
import { useLocale, useTranslation } from '#i18n'
import Image from 'next/image'
import Link from 'next/link'
import * as React from 'react'
import { useCallback, useMemo } from 'react'
import useTheme from '@/hooks/use-theme'
import { getLanguage } from '@/i18n-config/language'
import { cn } from '@/utils/classnames'
import { getIconFromMarketPlace } from '@/utils/get-icon'
import { formatUsedCount } from '@/utils/template'
import { getMarketplaceUrl } from '@/utils/var'
@ -17,7 +18,7 @@ type TemplateCardProps = {
}
// Number of tag icons to show before showing "+X"
const MAX_VISIBLE_TAGS = 7
const MAX_VISIBLE_DEPS_PLUGINS = 7
// Soft background color palette for avatar
const AVATAR_BG_COLORS = [
@ -49,8 +50,9 @@ const TemplateCardComponent = ({
className,
}: TemplateCardProps) => {
const locale = useLocale()
const { t } = useTranslation()
const { theme } = useTheme()
const { template_id, name, description, icon, tags, author, used_count, icon_background } = template as Template & { used_count?: number, icon_background?: string }
const { id, template_name, overview, icon, publisher_handle, usage_count, icon_background, deps_plugins } = template
const isIconUrl = !!icon && /^(?:https?:)?\/\//.test(icon)
const avatarBgStyle = useMemo(() => {
@ -64,24 +66,23 @@ const TemplateCardComponent = ({
// Only use class-based color if no inline style
if (icon_background)
return ''
return getAvatarBgClass(template_id)
}, [icon_background, template_id])
const descriptionText = description[getLanguage(locale)] || description.en_US || ''
return getAvatarBgClass(id)
}, [icon_background, id])
const handleClick = useCallback(() => {
const url = getMarketplaceUrl(`/templates/${author}/${name}`, {
const url = getMarketplaceUrl(`/templates/${publisher_handle}/${template_name}`, {
theme,
language: locale,
templateId: template_id,
templateId: id,
creationType: 'templates',
})
window.open(url, '_blank')
}, [author, name, theme, locale, template_id])
}, [publisher_handle, template_name, theme, locale, id])
const visibleTags = tags?.slice(0, MAX_VISIBLE_TAGS) || []
const remainingTagsCount = tags ? Math.max(0, tags.length - MAX_VISIBLE_TAGS) : 0
const visibleDepsPlugins = deps_plugins?.slice(0, MAX_VISIBLE_DEPS_PLUGINS) || []
const remainingDepsPluginsCount = deps_plugins ? Math.max(0, deps_plugins.length - MAX_VISIBLE_DEPS_PLUGINS) : 0
const formattedUsedCount = formatUsedCount(used_count, { precision: 0, rounding: 'floor' })
const formattedUsedCount = formatUsedCount(usage_count, { precision: 0, rounding: 'floor' })
return (
<div
@ -105,7 +106,7 @@ const TemplateCardComponent = ({
? (
<Image
src={icon}
alt={name}
alt={template_name}
width={24}
height={24}
className="h-6 w-6 object-contain"
@ -117,22 +118,24 @@ const TemplateCardComponent = ({
</div>
{/* Title */}
<div className="flex min-w-0 flex-1 flex-col justify-center gap-0.5">
<p className="system-md-medium truncate text-text-primary">{name}</p>
<p className="system-md-medium truncate text-text-primary">{template_name}</p>
<div className="system-xs-regular flex items-center gap-2 text-text-tertiary">
<span className="flex shrink-0 items-center gap-1">
<span>by</span>
<span className="truncate">{author}</span>
<span className="shrink-0">{t('marketplace.templateCard.by', { ns: 'plugin' })}</span>
<Link
href={`/creators/${publisher_handle}`}
target="_blank"
rel="noopener noreferrer"
className="truncate hover:text-text-secondary hover:underline"
onClick={e => e.stopPropagation()}
>
{publisher_handle}
</Link>
</span>
<span className="shrink-0">·</span>
<span className="shrink-0">
{t('usedCount', { ns: 'plugin', num: formattedUsedCount || 0 })}
</span>
{formattedUsedCount && (
<>
<span className="shrink-0">·</span>
<span className="shrink-0">
{formattedUsedCount}
{' '}
used
</span>
</>
)}
</div>
</div>
</div>
@ -141,30 +144,34 @@ const TemplateCardComponent = ({
<div className="shrink-0 px-4 pb-2 pt-1">
<p
className="system-xs-regular line-clamp-2 min-h-[32px] text-text-secondary"
title={descriptionText}
title={overview}
>
{descriptionText}
{overview}
</p>
</div>
{/* Bottom Info Bar - Tags as icons */}
<div className="mt-auto flex min-h-7 shrink-0 items-center gap-1 px-4 py-1">
{tags && tags.length > 0 && (
{deps_plugins && deps_plugins.length > 0 && (
<>
{visibleTags.map((tag, index) => (
{visibleDepsPlugins.map((depsPlugin, index) => (
<div
key={`${template_id}-tag-${index}`}
key={`${id}-depsPlugin-${index}`}
className="flex h-6 w-6 shrink-0 items-center justify-center overflow-hidden rounded-md border-[0.5px] border-effects-icon-border bg-background-default-dodge"
title={tag}
title={depsPlugin}
>
<span className="text-sm">{tag}</span>
<img
className="h-full w-full object-cover"
src={getIconFromMarketPlace(depsPlugin)}
alt={depsPlugin}
/>
</div>
))}
{remainingTagsCount > 0 && (
{remainingDepsPluginsCount > 0 && (
<div className="flex items-center justify-center p-0.5">
<span className="system-xs-regular text-text-tertiary">
+
{remainingTagsCount}
{remainingDepsPluginsCount}
</span>
</div>
)}

View File

@ -1,19 +1,18 @@
import type { Creator, Template } from '../../types'
import type { Plugin } from '@/app/components/plugins/types'
import type { Locale } from '@/i18n-config/language'
import { useLocale, useTranslation } from '#i18n'
import { useTranslation } from '#i18n'
import { RiArrowRightLine } from '@remixicon/react'
import { Fragment } from 'react'
import Loading from '@/app/components/base/loading'
import { useCategories } from '@/app/components/plugins/hooks'
import { useRenderI18nObject } from '@/hooks/use-i18n'
import { getLanguage } from '@/i18n-config/language'
import { cn } from '@/utils/classnames'
import { formatUsedCount } from '@/utils/template'
import { getMarketplaceUrl } from '@/utils/var'
import { MARKETPLACE_TYPE_ICON_COMPONENTS } from '../../plugin-type-icons'
import { getCreatorAvatarUrl, getPluginDetailLinkInMarketplace } from '../../utils'
const DROPDOWN_PANEL = 'w-[472px] rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur shadow-xl backdrop-blur-sm'
const DROPDOWN_PANEL = 'w-[472px] max-h-[710px] overflow-y-auto rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur shadow-xl backdrop-blur-sm'
const ICON_BOX_BASE = 'flex shrink-0 items-center justify-center overflow-hidden border-[0.5px] border-components-panel-border-subtle bg-background-default-dodge'
const SectionDivider = () => (
@ -87,7 +86,6 @@ const SearchDropdown = ({
isLoading = false,
}: SearchDropdownProps) => {
const { t } = useTranslation()
const locale = useLocale()
const getValueFromI18nObject = useRenderI18nObject()
const { categoriesMap } = useCategories(true)
@ -101,7 +99,6 @@ const SearchDropdown = ({
<TemplatesSection
key="templates"
templates={templates}
locale={locale}
t={t}
/>,
)
@ -168,22 +165,23 @@ const SearchDropdown = ({
/* ---------- Templates Section ---------- */
function TemplatesSection({ templates, locale, t }: {
function TemplatesSection({ templates, t }: {
templates: Template[]
locale: Locale
t: ReturnType<typeof useTranslation>['t']
}) {
return (
<DropdownSection title={t('templates', { ns: 'plugin' })}>
{templates.map((template) => {
const descriptionText = template.description[getLanguage(locale)] || template.description.en_US || ''
const descriptionText = template.overview
const formattedUsedCount = formatUsedCount(template.usage_count, { precision: 0, rounding: 'floor' })
const usedLabel = t('usedCount', { ns: 'plugin', num: formattedUsedCount || 0 })
const iconBgStyle = template.icon_background
? { backgroundColor: template.icon_background }
: undefined
return (
<DropdownItem
key={template.template_id}
href={getMarketplaceUrl(`/templates/${template.template_id}`)}
key={template.id}
href={getMarketplaceUrl(`/templates/${template.publisher_handle}/${template.template_name}`, { templateId: template.id })}
icon={(
<div className="flex shrink-0 items-start py-1">
<IconBox shape="rounded-lg" style={iconBgStyle}>
@ -192,16 +190,14 @@ function TemplatesSection({ templates, locale, t }: {
</div>
)}
>
<div className="system-md-medium truncate text-text-primary">{template.name}</div>
<div className="system-md-medium truncate text-text-primary">{template.template_name}</div>
{!!descriptionText && (
<div className="system-xs-regular line-clamp-2 text-text-tertiary">{descriptionText}</div>
)}
<ItemMeta
items={[
t('marketplace.searchDropdown.byAuthor', { ns: 'plugin', author: template.author }),
...(template.tags.length > 0
? [<span key="tags" className="system-xs-regular truncate">{template.tags.join(', ')}</span>]
: []),
t('marketplace.searchDropdown.byAuthor', { ns: 'plugin', author: template.publisher_handle }),
usedLabel,
]}
/>
</DropdownItem>

View File

@ -144,7 +144,7 @@ const SearchPage = () => {
return (
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
{toShow.map(template => (
<div key={template.template_id}>
<div key={template.id}>
<TemplateCard template={template} />
</div>
))}

View File

@ -89,10 +89,11 @@ export function useTemplatesMarketplaceData(enabled = true) {
const queryParams = useMemo((): TemplateSearchParams | undefined => {
if (!isSearchMode)
return undefined
const sortBy = sort.sortBy === 'install_count' ? 'usage_count' : sort.sortBy === 'version_updated_at' ? 'updated_at' : sort.sortBy
return {
query: searchText,
categories: activeTemplateCategory === CATEGORY_ALL ? undefined : [activeTemplateCategory],
sort_by: sort.sortBy,
sort_by: sortBy,
sort_order: sort.sortOrder,
}
}, [isSearchMode, searchText, activeTemplateCategory, sort])

View File

@ -72,13 +72,23 @@ export type TemplateCollection = {
}
export type Template = {
template_id: string
name: string
description: Record<string, string>
id: string
index_id: string
template_name: string
icon: string
icon_background?: string
tags: string[]
author: string
icon_file_key: string
categories: string[]
overview: string
readme: string
partner_link: string
deps_plugins: string[]
preferred_languages: string[]
publisher_handle: string
publisher_type: string
kind: string
status: string
usage_count: number
created_at: string
updated_at: string
}
@ -150,25 +160,12 @@ export type SyncCreatorProfileRequest = {
status?: 'active' | 'inactive'
}
// Template Detail (full template info from API)
export type TemplateDetail = {
id: string
publisher_type: 'individual' | 'organization'
// Template Detail (full template info from API, extends Template with extra fields)
export type TemplateDetail = Template & {
publisher_unique_handle: string
creator_email: string
template_name: string
icon: string
icon_background: string
icon_file_key: string
dsl_file_key: string
categories: string[]
overview: string
readme: string
partner_link: string
status: 'published' | 'draft' | 'pending' | 'rejected'
review_comment: string
created_at: string
updated_at: string
}
export type TemplatesListResponse = {
@ -202,25 +199,8 @@ export type UnifiedPluginItem = Plugin & {
index_id: string
}
// Template item shape from /search/unified (differs from TemplateDetail)
export type UnifiedTemplateItem = {
id: string
index_id: string
template_name: string
icon: string
icon_background?: string
icon_file_key: string
categories: string[]
overview: string
readme: string
partner_link: string
publisher_handle: string
publisher_type: 'individual' | 'organization'
status: string
usage_count: number
created_at: string
updated_at: string
}
// Template item shape from /search/unified (same as Template)
export type UnifiedTemplateItem = Template
// Creator item shape from /search/unified (superset of Creator with index_id)
export type UnifiedCreatorItem = Creator & {

View File

@ -13,7 +13,6 @@ import type {
UnifiedPluginItem,
UnifiedSearchParams,
UnifiedSearchResponse,
UnifiedTemplateItem,
} from '@/app/components/plugins/marketplace/types'
import type { Plugin } from '@/app/components/plugins/types'
import { PluginCategoryEnum } from '@/app/components/plugins/types'
@ -28,7 +27,7 @@ type MarketplaceFetchOptions = {
signal?: AbortSignal
}
/** Get a string key from an item by field name (e.g. plugin_id, template_id). */
/** Get a string key from an item by field name (e.g. plugin_id, id). */
export function getItemKeyByField<T>(item: T, field: keyof T): string {
return String((item as Record<string, unknown>)[field as string])
}
@ -133,20 +132,11 @@ export const getMarketplaceCollectionsAndPlugins = async (
}
export function mapTemplateDetailToTemplate(template: TemplateDetail): Template {
const descriptionText = template.overview || template.readme || ''
// TemplateDetail extends Template; just override publisher_handle from the detail-specific field
return {
template_id: template.id,
name: template.template_name,
description: {
en_US: descriptionText,
zh_Hans: descriptionText,
},
icon: template.icon || '',
icon_background: template.icon_background || undefined,
tags: template.categories || [],
author: template.publisher_unique_handle || template.creator_email || '',
created_at: template.created_at,
updated_at: template.updated_at,
...template,
publisher_handle: template.publisher_handle || template.publisher_unique_handle || template.creator_email || '',
index_id: template.index_id || template.id,
}
}
@ -437,24 +427,10 @@ export function mapUnifiedPluginToPlugin(item: UnifiedPluginItem): Plugin {
}
/**
* Map unified search template item to Template type
* Map unified search template item to Template type (identity since UnifiedTemplateItem = Template)
*/
export function mapUnifiedTemplateToTemplate(item: UnifiedTemplateItem): Template {
const descriptionText = item.overview || item.readme || ''
return {
template_id: item.id,
name: item.template_name,
description: {
en_US: descriptionText,
zh_Hans: descriptionText,
},
icon: item.icon || '',
icon_background: item.icon_background || undefined,
tags: item.categories || [],
author: item.publisher_handle || '',
created_at: item.created_at,
updated_at: item.updated_at,
}
export function mapUnifiedTemplateToTemplate(item: Template): Template {
return item
}
/**

View File

@ -2820,16 +2820,6 @@
"count": 2
}
},
"app/components/plugins/marketplace/list/card-wrapper.tsx": {
"tailwindcss/no-unnecessary-whitespace": {
"count": 1
}
},
"app/components/plugins/marketplace/list/list-with-collection.tsx": {
"tailwindcss/no-unnecessary-whitespace": {
"count": 1
}
},
"app/components/plugins/marketplace/sort-dropdown/index.spec.tsx": {
"unused-imports/no-unused-vars": {
"count": 1

View File

@ -221,16 +221,17 @@
"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.templateCard.by": "By",
"marketplace.templateCategory.all": "All",
"marketplace.templateCategory.marketing": "Marketing",
"marketplace.templateCategory.sales": "Sales",
"marketplace.templateCategory.support": "Support",
"marketplace.templateCategory.operations": "Operations",
"marketplace.templateCategory.design": "Design",
"marketplace.templateCategory.it": "IT",
"marketplace.templateCategory.knowledge": "Knowledge",
"marketplace.templateCategory.design": "Design",
"marketplace.templateCategory.marketing": "Marketing",
"marketplace.templateCategory.operations": "Operations",
"marketplace.templateCategory.sales": "Sales",
"marketplace.templateCategory.support": "Support",
"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",
@ -278,5 +279,6 @@
"upgrade.title": "Install Plugin",
"upgrade.upgrade": "Install",
"upgrade.upgrading": "Installing...",
"upgrade.usedInApps": "Used in {{num}} apps"
"upgrade.usedInApps": "Used in {{num}} apps",
"usedCount": "{{num}} used"
}

View File

@ -221,16 +221,17 @@
"marketplace.sortOption.mostPopular": "最受欢迎",
"marketplace.sortOption.newlyReleased": "最新发布",
"marketplace.sortOption.recentlyUpdated": "最近更新",
"marketplace.templatesHeroSubtitle": "社区构建的工作流模板 —— 随时可使用、复刻和部署。",
"marketplace.templatesHeroTitle": "创建。复刻。部署。",
"marketplace.templateCard.by": "来自",
"marketplace.templateCategory.all": "全部",
"marketplace.templateCategory.marketing": "营销",
"marketplace.templateCategory.sales": "销售",
"marketplace.templateCategory.support": "支持",
"marketplace.templateCategory.operations": "运营",
"marketplace.templateCategory.design": "设计",
"marketplace.templateCategory.it": "IT",
"marketplace.templateCategory.knowledge": "知识",
"marketplace.templateCategory.design": "设计",
"marketplace.templateCategory.marketing": "营销",
"marketplace.templateCategory.operations": "运营",
"marketplace.templateCategory.sales": "销售",
"marketplace.templateCategory.support": "支持",
"marketplace.templatesHeroSubtitle": "社区构建的工作流模板 —— 随时可使用、复刻和部署。",
"marketplace.templatesHeroTitle": "创建。复刻。部署。",
"marketplace.verifiedTip": "此插件由 Dify 认证",
"marketplace.viewMore": "查看更多",
"metadata.title": "插件",
@ -278,5 +279,6 @@
"upgrade.title": "安装插件",
"upgrade.upgrade": "安装",
"upgrade.upgrading": "安装中...",
"upgrade.usedInApps": "在 {{num}} 个应用中使用"
"upgrade.usedInApps": "在 {{num}} 个应用中使用",
"usedCount": "{{num}} 次使用"
}