From 6ef87550e6cccaec404f6f3636a916736fb424f5 Mon Sep 17 00:00:00 2001 From: yessenia Date: Wed, 11 Feb 2026 03:40:41 +0800 Subject: [PATCH] feat: enhance templates marketplace with new sorting options and improved template data structure --- .../components/plugins/marketplace/atoms.ts | 4 +- .../plugins/marketplace/constants.ts | 5 ++ .../plugins/marketplace/index.spec.tsx | 19 ++++- .../marketplace/list/collection-constants.ts | 19 +++++ .../marketplace/list/collection-list.tsx | 66 +++++---------- .../plugins/marketplace/list/flat-list.tsx | 4 +- .../plugins/marketplace/list/index.tsx | 2 +- .../marketplace/list/list-with-collection.tsx | 5 +- .../list/list-wrapper-flat.spec.tsx | 2 +- .../marketplace/list/template-card.tsx | 81 ++++++++++--------- .../search-box/search-dropdown/index.tsx | 28 +++---- .../plugins/marketplace/search-page/index.tsx | 2 +- .../components/plugins/marketplace/state.ts | 3 +- .../components/plugins/marketplace/types.ts | 58 +++++-------- .../components/plugins/marketplace/utils.ts | 40 ++------- web/eslint-suppressions.json | 10 --- web/i18n/en-US/plugin.json | 18 +++-- web/i18n/zh-Hans/plugin.json | 18 +++-- 18 files changed, 180 insertions(+), 204 deletions(-) create mode 100644 web/app/components/plugins/marketplace/list/collection-constants.ts diff --git a/web/app/components/plugins/marketplace/atoms.ts b/web/app/components/plugins/marketplace/atoms.ts index 7342140425..c5353dedec 100644 --- a/web/app/components/plugins/marketplace/atoms.ts +++ b/web/app/components/plugins/marketplace/atoms.ts @@ -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(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 } diff --git a/web/app/components/plugins/marketplace/constants.ts b/web/app/components/plugins/marketplace/constants.ts index 20ff724ac8..f5e8ca939b 100644 --- a/web/app/components/plugins/marketplace/constants.ts +++ b/web/app/components/plugins/marketplace/constants.ts @@ -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' diff --git a/web/app/components/plugins/marketplace/index.spec.tsx b/web/app/components/plugins/marketplace/index.spec.tsx index efa45ff3a6..09a5692b3b 100644 --- a/web/app/components/plugins/marketplace/index.spec.tsx +++ b/web/app/components/plugins/marketplace/index.spec.tsx @@ -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) diff --git a/web/app/components/plugins/marketplace/list/collection-constants.ts b/web/app/components/plugins/marketplace/list/collection-constants.ts new file mode 100644 index 0000000000..4421393394 --- /dev/null +++ b/web/app/components/plugins/marketplace/list/collection-constants.ts @@ -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 + description: Record + searchable?: boolean + search_params?: { query?: string, sort_by?: string, sort_order?: string } +} diff --git a/web/app/components/plugins/marketplace/list/collection-list.tsx b/web/app/components/plugins/marketplace/list/collection-list.tsx index 1b7846762a..353000b5f6 100644 --- a/web/app/components/plugins/marketplace/list/collection-list.tsx +++ b/web/app/components/plugins/marketplace/list/collection-list.tsx @@ -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 - description: Record - 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 = { collection: TCollection itemsLength: number @@ -62,7 +42,7 @@ type CollectionHeaderProps = { viewMore: React.ReactNode } -function CollectionHeader({ +export function CollectionHeader({ collection, itemsLength, locale, @@ -87,8 +67,6 @@ function CollectionHeader({ ) } -export { CarouselCollection, CollectionHeader } - type CarouselCollectionProps = { items: TItem[] getItemKey: (item: TItem) => string @@ -96,7 +74,7 @@ type CarouselCollectionProps = { cardContainerClassName?: string } -function CarouselCollection({ +export function CarouselCollection({ items, getItemKey, renderCard, @@ -132,7 +110,7 @@ function CarouselCollection({ type CollectionListProps = { collections: TCollection[] collectionItemsMap: Record - /** 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({ /> {isCarouselCollection ? ( - getItemKeyByField(item, itemKeyField)} - renderCard={renderCard} - cardContainerClassName={cardContainerClassName} - /> - ) + getItemKeyByField(item, itemKeyField)} + renderCard={renderCard} + cardContainerClassName={cardContainerClassName} + /> + ) : ( -
- {items.slice(0, GRID_DISPLAY_LIMIT).map(item => ( -
- {renderCard(item)} -
- ))} -
- )} +
+ {items.slice(0, GRID_DISPLAY_LIMIT).map(item => ( +
+ {renderCard(item)} +
+ ))} +
+ )} ) }) diff --git a/web/app/components/plugins/marketplace/list/flat-list.tsx b/web/app/components/plugins/marketplace/list/flat-list.tsx index 8f47041eb7..e94faf4f1d 100644 --- a/web/app/components/plugins/marketplace/list/flat-list.tsx +++ b/web/app/components/plugins/marketplace/list/flat-list.tsx @@ -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) => {
{items.map(template => ( ))} diff --git a/web/app/components/plugins/marketplace/list/index.tsx b/web/app/components/plugins/marketplace/list/index.tsx index 8a579ca886..7c9c0ed79e 100644 --- a/web/app/components/plugins/marketplace/list/index.tsx +++ b/web/app/components/plugins/marketplace/list/index.tsx @@ -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 = { diff --git a/web/app/components/plugins/marketplace/list/list-with-collection.tsx b/web/app/components/plugins/marketplace/list/list-with-collection.tsx index 89b53e1f22..634e34e326 100644 --- a/web/app/components/plugins/marketplace/list/list-with-collection.tsx +++ b/web/app/components/plugins/marketplace/list/list-with-collection.tsx @@ -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) => { { 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() diff --git a/web/app/components/plugins/marketplace/list/template-card.tsx b/web/app/components/plugins/marketplace/list/template-card.tsx index 3813ac609d..eac248ae78 100644 --- a/web/app/components/plugins/marketplace/list/template-card.tsx +++ b/web/app/components/plugins/marketplace/list/template-card.tsx @@ -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 (
{/* Title */}
-

{name}

+

{template_name}

- by - {author} + {t('marketplace.templateCard.by', { ns: 'plugin' })} + e.stopPropagation()} + > + {publisher_handle} + + + · + + {t('usedCount', { ns: 'plugin', num: formattedUsedCount || 0 })} - {formattedUsedCount && ( - <> - · - - {formattedUsedCount} - {' '} - used - - - )}
@@ -141,30 +144,34 @@ const TemplateCardComponent = ({

- {descriptionText} + {overview}

{/* Bottom Info Bar - Tags as icons */}
- {tags && tags.length > 0 && ( + {deps_plugins && deps_plugins.length > 0 && ( <> - {visibleTags.map((tag, index) => ( + {visibleDepsPlugins.map((depsPlugin, index) => (
- {tag} + {depsPlugin}
))} - {remainingTagsCount > 0 && ( + {remainingDepsPluginsCount > 0 && (
+ - {remainingTagsCount} + {remainingDepsPluginsCount}
)} diff --git a/web/app/components/plugins/marketplace/search-box/search-dropdown/index.tsx b/web/app/components/plugins/marketplace/search-box/search-dropdown/index.tsx index ee8f5c4c04..1f3ac581c0 100644 --- a/web/app/components/plugins/marketplace/search-box/search-dropdown/index.tsx +++ b/web/app/components/plugins/marketplace/search-box/search-dropdown/index.tsx @@ -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 = ({ , ) @@ -168,22 +165,23 @@ const SearchDropdown = ({ /* ---------- Templates Section ---------- */ -function TemplatesSection({ templates, locale, t }: { +function TemplatesSection({ templates, t }: { templates: Template[] - locale: Locale t: ReturnType['t'] }) { return ( {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 ( @@ -192,16 +190,14 @@ function TemplatesSection({ templates, locale, t }: {
)} > -
{template.name}
+
{template.template_name}
{!!descriptionText && (
{descriptionText}
)} 0 - ? [{template.tags.join(', ')}] - : []), + t('marketplace.searchDropdown.byAuthor', { ns: 'plugin', author: template.publisher_handle }), + usedLabel, ]} /> diff --git a/web/app/components/plugins/marketplace/search-page/index.tsx b/web/app/components/plugins/marketplace/search-page/index.tsx index f1179e711c..5ac5580786 100644 --- a/web/app/components/plugins/marketplace/search-page/index.tsx +++ b/web/app/components/plugins/marketplace/search-page/index.tsx @@ -144,7 +144,7 @@ const SearchPage = () => { return (
{toShow.map(template => ( -
+
))} diff --git a/web/app/components/plugins/marketplace/state.ts b/web/app/components/plugins/marketplace/state.ts index 81e77cfb1e..2ddd9ef535 100644 --- a/web/app/components/plugins/marketplace/state.ts +++ b/web/app/components/plugins/marketplace/state.ts @@ -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]) diff --git a/web/app/components/plugins/marketplace/types.ts b/web/app/components/plugins/marketplace/types.ts index 73faa6389a..e6eb446ede 100644 --- a/web/app/components/plugins/marketplace/types.ts +++ b/web/app/components/plugins/marketplace/types.ts @@ -72,13 +72,23 @@ export type TemplateCollection = { } export type Template = { - template_id: string - name: string - description: Record + 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 & { diff --git a/web/app/components/plugins/marketplace/utils.ts b/web/app/components/plugins/marketplace/utils.ts index c28bcc6809..dfc1f9b334 100644 --- a/web/app/components/plugins/marketplace/utils.ts +++ b/web/app/components/plugins/marketplace/utils.ts @@ -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(item: T, field: keyof T): string { return String((item as Record)[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 } /** diff --git a/web/eslint-suppressions.json b/web/eslint-suppressions.json index f1e7af211d..6d6243a444 100644 --- a/web/eslint-suppressions.json +++ b/web/eslint-suppressions.json @@ -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 diff --git a/web/i18n/en-US/plugin.json b/web/i18n/en-US/plugin.json index 3f63592dd3..f567601e63 100644 --- a/web/i18n/en-US/plugin.json +++ b/web/i18n/en-US/plugin.json @@ -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" } diff --git a/web/i18n/zh-Hans/plugin.json b/web/i18n/zh-Hans/plugin.json index 7b105f053d..6c06e17fde 100644 --- a/web/i18n/zh-Hans/plugin.json +++ b/web/i18n/zh-Hans/plugin.json @@ -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}} 次使用" }