diff --git a/web/app/components/plugins/marketplace/atoms.ts b/web/app/components/plugins/marketplace/atoms.ts index c5353dedec..15fa9cf6f5 100644 --- a/web/app/components/plugins/marketplace/atoms.ts +++ b/web/app/components/plugins/marketplace/atoms.ts @@ -2,19 +2,30 @@ import type { SearchTab } from './search-params' import type { PluginsSort, SearchParamsFromCollection } from './types' import { atom, useAtom, useAtomValue, useSetAtom } from 'jotai' import { useQueryState } from 'nuqs' -import { useCallback } from 'react' -import { CATEGORY_ALL, DEFAULT_SORT, getValidatedPluginCategory, getValidatedTemplateCategory, PLUGIN_CATEGORY_WITH_COLLECTIONS } from './constants' +import { useCallback, useMemo } from 'react' +import { CATEGORY_ALL, DEFAULT_PLUGIN_SORT, DEFAULT_TEMPLATE_SORT, getValidatedPluginCategory, getValidatedTemplateCategory, PLUGIN_CATEGORY_WITH_COLLECTIONS } from './constants' import { CREATION_TYPE, marketplaceSearchParamsParsers } from './search-params' -const marketplaceSortAtom = atom(DEFAULT_SORT) -export function useMarketplaceSort() { - return useAtom(marketplaceSortAtom) +const marketplacePluginSortAtom = atom(DEFAULT_PLUGIN_SORT) +export function useMarketplacePluginSort() { + return useAtom(marketplacePluginSortAtom) } -export function useMarketplaceSortValue() { - return useAtomValue(marketplaceSortAtom) +export function useMarketplacePluginSortValue() { + return useAtomValue(marketplacePluginSortAtom) } -export function useSetMarketplaceSort() { - return useSetAtom(marketplaceSortAtom) +export function useSetMarketplacePluginSort() { + return useSetAtom(marketplacePluginSortAtom) +} + +const marketplaceTemplateSortAtom = atom(DEFAULT_TEMPLATE_SORT) +export function useMarketplaceTemplateSort() { + return useAtom(marketplaceTemplateSortAtom) +} +export function useMarketplaceTemplateSortValue() { + return useAtomValue(marketplaceTemplateSortAtom) +} +export function useSetMarketplaceTemplateSort() { + return useSetAtom(marketplaceTemplateSortAtom) } export function useSearchText() { @@ -64,22 +75,56 @@ export function useMarketplaceSearchMode() { return isSearchMode } +/** + * Returns the active sort state based on the current creationType. + * Plugins use `marketplacePluginSortAtom`, templates use `marketplaceTemplateSortAtom`. + */ +export function useActiveSort(): [PluginsSort, (sort: PluginsSort) => void] { + const [creationType] = useCreationType() + const [pluginSort, setPluginSort] = useAtom(marketplacePluginSortAtom) + const [templateSort, setTemplateSort] = useAtom(marketplaceTemplateSortAtom) + const isTemplates = creationType === CREATION_TYPE.templates + + const sort = isTemplates ? templateSort : pluginSort + const setSort = useMemo( + () => isTemplates ? setTemplateSort : setPluginSort, + [isTemplates, setTemplateSort, setPluginSort], + ) + return [sort, setSort] +} + +export function useActiveSortValue(): PluginsSort { + const [creationType] = useCreationType() + const pluginSort = useAtomValue(marketplacePluginSortAtom) + const templateSort = useAtomValue(marketplaceTemplateSortAtom) + return creationType === CREATION_TYPE.templates ? templateSort : pluginSort +} + export function useMarketplaceMoreClick() { const [, setQ] = useSearchText() const [, setSearchTab] = useSearchTab() - const setSort = useSetAtom(marketplaceSortAtom) + const setPluginSort = useSetAtom(marketplacePluginSortAtom) + const setTemplateSort = useSetAtom(marketplaceTemplateSortAtom) const setSearchMode = useSetAtom(searchModeAtom) return useCallback((searchParams?: SearchParamsFromCollection, searchTab?: SearchTab) => { if (!searchParams) return setQ(searchParams?.query || '') - setSort({ - sortBy: searchParams?.sort_by || DEFAULT_SORT.sortBy, - sortOrder: searchParams?.sort_order || DEFAULT_SORT.sortOrder, - }) + if (searchTab === 'templates') { + setTemplateSort({ + sortBy: searchParams?.sort_by || DEFAULT_TEMPLATE_SORT.sortBy, + sortOrder: searchParams?.sort_order || DEFAULT_TEMPLATE_SORT.sortOrder, + }) + } + else { + setPluginSort({ + sortBy: searchParams?.sort_by || DEFAULT_PLUGIN_SORT.sortBy, + sortOrder: searchParams?.sort_order || DEFAULT_PLUGIN_SORT.sortOrder, + }) + } setSearchMode(true) if (searchTab) setSearchTab(searchTab) - }, [setQ, setSearchTab, setSort, setSearchMode]) + }, [setQ, setSearchTab, setPluginSort, setTemplateSort, setSearchMode]) } diff --git a/web/app/components/plugins/marketplace/category-switch/category-text.ts b/web/app/components/plugins/marketplace/category-switch/category-text.ts new file mode 100644 index 0000000000..3cd4340c40 --- /dev/null +++ b/web/app/components/plugins/marketplace/category-switch/category-text.ts @@ -0,0 +1,67 @@ +'use client' + +import type { ActivePluginType, ActiveTemplateCategory } from '../constants' +import { useTranslation } from '#i18n' +import { PLUGIN_TYPE_SEARCH_MAP, TEMPLATE_CATEGORY_MAP } from '../constants' + +/** + * Returns a getter that translates a plugin category value to its display text. + * Pass `allAsAllTypes = true` to use "All types" instead of "All" for the `all` category + * (e.g. hero variant in category switch). + */ +export function usePluginCategoryText() { + const { t } = useTranslation() + + return (category: ActivePluginType, allAsAllTypes = false): string => { + switch (category) { + case PLUGIN_TYPE_SEARCH_MAP.model: + return t('category.models', { ns: 'plugin' }) + case PLUGIN_TYPE_SEARCH_MAP.tool: + return t('category.tools', { ns: 'plugin' }) + case PLUGIN_TYPE_SEARCH_MAP.datasource: + return t('category.datasources', { ns: 'plugin' }) + case PLUGIN_TYPE_SEARCH_MAP.trigger: + return t('category.triggers', { ns: 'plugin' }) + case PLUGIN_TYPE_SEARCH_MAP.agent: + return t('category.agents', { ns: 'plugin' }) + case PLUGIN_TYPE_SEARCH_MAP.extension: + return t('category.extensions', { ns: 'plugin' }) + case PLUGIN_TYPE_SEARCH_MAP.bundle: + return t('category.bundles', { ns: 'plugin' }) + case PLUGIN_TYPE_SEARCH_MAP.all: + default: + return allAsAllTypes + ? t('category.allTypes', { ns: 'plugin' }) + : t('category.all', { ns: 'plugin' }) + } + } +} + +/** + * Returns a getter that translates a template category value to its display text. + */ +export function useTemplateCategoryText() { + const { t } = useTranslation() + + return (category: ActiveTemplateCategory): string => { + switch (category) { + case TEMPLATE_CATEGORY_MAP.marketing: + return t('marketplace.templateCategory.marketing', { ns: 'plugin' }) + case TEMPLATE_CATEGORY_MAP.sales: + return t('marketplace.templateCategory.sales', { ns: 'plugin' }) + case TEMPLATE_CATEGORY_MAP.support: + return t('marketplace.templateCategory.support', { ns: 'plugin' }) + case TEMPLATE_CATEGORY_MAP.operations: + return t('marketplace.templateCategory.operations', { ns: 'plugin' }) + case TEMPLATE_CATEGORY_MAP.it: + return t('marketplace.templateCategory.it', { ns: 'plugin' }) + case TEMPLATE_CATEGORY_MAP.knowledge: + return t('marketplace.templateCategory.knowledge', { ns: 'plugin' }) + case TEMPLATE_CATEGORY_MAP.design: + return t('marketplace.templateCategory.design', { ns: 'plugin' }) + case TEMPLATE_CATEGORY_MAP.all: + default: + return t('marketplace.templateCategory.all', { ns: 'plugin' }) + } + } +} diff --git a/web/app/components/plugins/marketplace/category-switch/plugin.tsx b/web/app/components/plugins/marketplace/category-switch/plugin.tsx index ab3f5d6fb4..96de666722 100644 --- a/web/app/components/plugins/marketplace/category-switch/plugin.tsx +++ b/web/app/components/plugins/marketplace/category-switch/plugin.tsx @@ -2,13 +2,13 @@ import type { ActivePluginType } from '../constants' import type { PluginCategoryEnum } from '@/app/components/plugins/types' -import { useTranslation } from '#i18n' import { RiArchive2Line } from '@remixicon/react' import { useSetAtom } from 'jotai' import { Plugin } from '@/app/components/base/icons/src/vender/plugin' import { searchModeAtom, useActivePluginCategory, useFilterPluginTags } from '../atoms' import { PLUGIN_CATEGORY_WITH_COLLECTIONS, PLUGIN_TYPE_SEARCH_MAP } from '../constants' import { MARKETPLACE_TYPE_ICON_COMPONENTS } from '../plugin-type-icons' +import { usePluginCategoryText } from './category-text' import { CommonCategorySwitch } from './common' import HeroTagsFilter from './hero-tags-filter' @@ -17,6 +17,17 @@ type PluginTypeSwitchProps = { variant?: 'default' | 'hero' } +const categoryValues = [ + PLUGIN_TYPE_SEARCH_MAP.all, + PLUGIN_TYPE_SEARCH_MAP.model, + PLUGIN_TYPE_SEARCH_MAP.tool, + PLUGIN_TYPE_SEARCH_MAP.datasource, + PLUGIN_TYPE_SEARCH_MAP.trigger, + PLUGIN_TYPE_SEARCH_MAP.agent, + PLUGIN_TYPE_SEARCH_MAP.extension, + PLUGIN_TYPE_SEARCH_MAP.bundle, +] as const + const getTypeIcon = (value: ActivePluginType, isHeroVariant?: boolean) => { if (value === PLUGIN_TYPE_SEARCH_MAP.all) return isHeroVariant ? : null @@ -30,55 +41,18 @@ export const PluginCategorySwitch = ({ className, variant = 'default', }: PluginTypeSwitchProps) => { - const { t } = useTranslation() const [activePluginCategory, handleActivePluginCategoryChange] = useActivePluginCategory() const [filterPluginTags, setFilterPluginTags] = useFilterPluginTags() const setSearchMode = useSetAtom(searchModeAtom) + const getPluginCategoryText = usePluginCategoryText() const isHeroVariant = variant === 'hero' - const options = [ - { - value: PLUGIN_TYPE_SEARCH_MAP.all, - text: isHeroVariant ? t('category.allTypes', { ns: 'plugin' }) : t('category.all', { ns: 'plugin' }), - icon: getTypeIcon(PLUGIN_TYPE_SEARCH_MAP.all, isHeroVariant), - }, - { - value: PLUGIN_TYPE_SEARCH_MAP.model, - text: t('category.models', { ns: 'plugin' }), - icon: getTypeIcon(PLUGIN_TYPE_SEARCH_MAP.model, isHeroVariant), - }, - { - value: PLUGIN_TYPE_SEARCH_MAP.tool, - text: t('category.tools', { ns: 'plugin' }), - icon: getTypeIcon(PLUGIN_TYPE_SEARCH_MAP.tool, isHeroVariant), - }, - { - value: PLUGIN_TYPE_SEARCH_MAP.datasource, - text: t('category.datasources', { ns: 'plugin' }), - icon: getTypeIcon(PLUGIN_TYPE_SEARCH_MAP.datasource, isHeroVariant), - }, - { - value: PLUGIN_TYPE_SEARCH_MAP.trigger, - text: t('category.triggers', { ns: 'plugin' }), - icon: getTypeIcon(PLUGIN_TYPE_SEARCH_MAP.trigger, isHeroVariant), - }, - { - value: PLUGIN_TYPE_SEARCH_MAP.agent, - text: t('category.agents', { ns: 'plugin' }), - icon: getTypeIcon(PLUGIN_TYPE_SEARCH_MAP.agent, isHeroVariant), - }, - { - value: PLUGIN_TYPE_SEARCH_MAP.extension, - text: t('category.extensions', { ns: 'plugin' }), - icon: getTypeIcon(PLUGIN_TYPE_SEARCH_MAP.extension, isHeroVariant), - }, - { - value: PLUGIN_TYPE_SEARCH_MAP.bundle, - text: t('category.bundles', { ns: 'plugin' }), - icon: getTypeIcon(PLUGIN_TYPE_SEARCH_MAP.bundle, isHeroVariant), - }, - ] + const options = categoryValues.map(value => ({ + value, + text: getPluginCategoryText(value, isHeroVariant), + icon: getTypeIcon(value, isHeroVariant), + })) const handleChange = (value: string) => { handleActivePluginCategoryChange(value) diff --git a/web/app/components/plugins/marketplace/category-switch/template.tsx b/web/app/components/plugins/marketplace/category-switch/template.tsx index 946b01f43f..d6e7063daa 100644 --- a/web/app/components/plugins/marketplace/category-switch/template.tsx +++ b/web/app/components/plugins/marketplace/category-switch/template.tsx @@ -1,9 +1,9 @@ 'use client' -import { useTranslation } from '#i18n' import { Playground } from '@/app/components/base/icons/src/vender/plugin' import { useActiveTemplateCategory } from '../atoms' import { CATEGORY_ALL, TEMPLATE_CATEGORY_MAP } from '../constants' +import { useTemplateCategoryText } from './category-text' import { CommonCategorySwitch } from './common' type TemplateCategorySwitchProps = { @@ -11,57 +11,31 @@ type TemplateCategorySwitchProps = { variant?: 'default' | 'hero' } +const categoryValues = [ + CATEGORY_ALL, + TEMPLATE_CATEGORY_MAP.marketing, + TEMPLATE_CATEGORY_MAP.sales, + TEMPLATE_CATEGORY_MAP.support, + TEMPLATE_CATEGORY_MAP.operations, + TEMPLATE_CATEGORY_MAP.it, + TEMPLATE_CATEGORY_MAP.knowledge, + TEMPLATE_CATEGORY_MAP.design, +] as const + export const TemplateCategorySwitch = ({ className, variant = 'default', }: TemplateCategorySwitchProps) => { - const { t } = useTranslation() const [activeTemplateCategory, handleActiveTemplateCategoryChange] = useActiveTemplateCategory() + const getTemplateCategoryText = useTemplateCategoryText() const isHeroVariant = variant === 'hero' - const options = [ - { - value: CATEGORY_ALL, - text: t('marketplace.templateCategory.all', { ns: 'plugin' }), - icon: isHeroVariant ? : null, - }, - { - value: TEMPLATE_CATEGORY_MAP.marketing, - text: t('marketplace.templateCategory.marketing', { ns: 'plugin' }), - icon: null, - }, - { - value: TEMPLATE_CATEGORY_MAP.sales, - text: t('marketplace.templateCategory.sales', { ns: 'plugin' }), - icon: null, - }, - { - value: TEMPLATE_CATEGORY_MAP.support, - text: t('marketplace.templateCategory.support', { ns: 'plugin' }), - icon: null, - }, - { - value: TEMPLATE_CATEGORY_MAP.operations, - text: t('marketplace.templateCategory.operations', { ns: 'plugin' }), - icon: null, - }, - { - value: TEMPLATE_CATEGORY_MAP.it, - text: t('marketplace.templateCategory.it', { ns: 'plugin' }), - icon: null, - }, - { - value: TEMPLATE_CATEGORY_MAP.knowledge, - text: t('marketplace.templateCategory.knowledge', { ns: 'plugin' }), - icon: null, - }, - { - value: TEMPLATE_CATEGORY_MAP.design, - text: t('marketplace.templateCategory.design', { ns: 'plugin' }), - icon: null, - }, - ] + const options = categoryValues.map(value => ({ + value, + text: getTemplateCategoryText(value), + icon: value === CATEGORY_ALL && isHeroVariant ? : null, + })) return ( ): PluginColl // Constants Tests // ================================ describe('constants', () => { - describe('DEFAULT_SORT', () => { + describe('DEFAULT_PLUGIN_SORT', () => { it('should have correct default sort values', () => { - expect(DEFAULT_SORT).toEqual({ + expect(DEFAULT_PLUGIN_SORT).toEqual({ sortBy: 'install_count', sortOrder: 'DESC', }) }) it('should be immutable at runtime', () => { - const originalSortBy = DEFAULT_SORT.sortBy - const originalSortOrder = DEFAULT_SORT.sortOrder + const originalSortBy = DEFAULT_PLUGIN_SORT.sortBy + const originalSortOrder = DEFAULT_PLUGIN_SORT.sortOrder - expect(DEFAULT_SORT.sortBy).toBe(originalSortBy) - expect(DEFAULT_SORT.sortOrder).toBe(originalSortOrder) + expect(DEFAULT_PLUGIN_SORT.sortBy).toBe(originalSortBy) + expect(DEFAULT_PLUGIN_SORT.sortOrder).toBe(originalSortOrder) }) }) diff --git a/web/app/components/plugins/marketplace/list/list-top-info.tsx b/web/app/components/plugins/marketplace/list/list-top-info.tsx new file mode 100644 index 0000000000..1dfd177c81 --- /dev/null +++ b/web/app/components/plugins/marketplace/list/list-top-info.tsx @@ -0,0 +1,77 @@ +'use client' + +import { useTranslation } from '#i18n' +import { + useActivePluginCategory, + useActiveTemplateCategory, + useFilterPluginTags, +} from '../atoms' +import { usePluginCategoryText, useTemplateCategoryText } from '../category-switch/category-text' +import { + CATEGORY_ALL, + TEMPLATE_CATEGORY_MAP, +} from '../constants' +import SortDropdown from '../sort-dropdown' + +type ListTopInfoProps = { + variant: 'plugins' | 'templates' +} + +const ListTopInfo = ({ variant }: ListTopInfoProps) => { + const { t } = useTranslation() + const [filterPluginTags] = useFilterPluginTags() + const [activePluginCategory] = useActivePluginCategory() + const [activeTemplateCategory] = useActiveTemplateCategory() + const getPluginCategoryText = usePluginCategoryText() + const getTemplateCategoryText = useTemplateCategoryText() + + const hasTags = variant === 'plugins' && filterPluginTags.length > 0 + + if (hasTags) { + return ( +
+

+ {t('marketplace.listTopInfo.tagsTitle', { ns: 'plugin' })} +

+ +
+ ) + } + + const isPlugins = variant === 'plugins' + const isAllCategory = isPlugins + ? activePluginCategory === CATEGORY_ALL + : activeTemplateCategory === TEMPLATE_CATEGORY_MAP.all + + const categoryText = isPlugins + ? getPluginCategoryText(activePluginCategory) + : getTemplateCategoryText(activeTemplateCategory) + + const title = isPlugins + ? isAllCategory + ? t('marketplace.listTopInfo.pluginsTitleAll', { ns: 'plugin' }) + : t('marketplace.listTopInfo.pluginsTitleByCategory', { ns: 'plugin', category: categoryText }) + : isAllCategory + ? t('marketplace.listTopInfo.templatesTitleAll', { ns: 'plugin' }) + : t('marketplace.listTopInfo.templatesTitleByCategory', { ns: 'plugin', category: categoryText }) + + const subtitleKey = isPlugins + ? 'marketplace.listTopInfo.pluginsSubtitle' + : 'marketplace.listTopInfo.templatesSubtitle' + + return ( +
+
+

+ {title} +

+

+ {t(subtitleKey, { ns: 'plugin' })} +

+
+ +
+ ) +} + +export default ListTopInfo diff --git a/web/app/components/plugins/marketplace/list/list-wrapper.tsx b/web/app/components/plugins/marketplace/list/list-wrapper.tsx index 1e76a29dcf..e2cd82bd9b 100644 --- a/web/app/components/plugins/marketplace/list/list-wrapper.tsx +++ b/web/app/components/plugins/marketplace/list/list-wrapper.tsx @@ -3,6 +3,7 @@ import Loading from '@/app/components/base/loading' import { isPluginsData, useMarketplaceData } from '../state' import FlatList from './flat-list' +import ListTopInfo from './list-top-info' import ListWithCollection from './list-with-collection' type ListWrapperProps = { @@ -17,7 +18,12 @@ const ListWrapper = ({ showInstallButton }: ListWrapperProps) => { if (isPluginsData(marketplaceData)) { const { pluginCollections, pluginCollectionPluginsMap, plugins } = marketplaceData return plugins !== undefined - ? + ? ( + <> + + + + ) : ( { const { templateCollections, templateCollectionTemplatesMap, templates } = marketplaceData return templates !== undefined - ? + ? ( + <> + + + + ) : ( ({ useSearchText: () => [mockSearchText, mockHandleSearchTextChange], useFilterPluginTags: () => [mockFilterPluginTags, mockHandleFilterPluginTagsChange], useActivePluginCategory: () => [mockActivePluginCategory, vi.fn()], - useMarketplaceSortValue: () => mockSortValue, + useMarketplacePluginSortValue: () => mockSortValue, + useMarketplaceTemplateSortValue: () => ({ sortBy: 'usage_count', sortOrder: 'DESC' }), + useActiveSort: () => [mockSortValue, vi.fn()], + useActiveSortValue: () => mockSortValue, + useCreationType: () => ['plugins', vi.fn()], + useSearchTab: () => ['', vi.fn()], searchModeAtom: {}, })) diff --git a/web/app/components/plugins/marketplace/search-page/index.tsx b/web/app/components/plugins/marketplace/search-page/index.tsx index c8eb597db1..8e7bae7c54 100644 --- a/web/app/components/plugins/marketplace/search-page/index.tsx +++ b/web/app/components/plugins/marketplace/search-page/index.tsx @@ -8,7 +8,7 @@ import { useDebounce } from 'ahooks' import { useCallback, useMemo } from 'react' import Loading from '@/app/components/base/loading' import SegmentedControl from '@/app/components/base/segmented-control' -import { useMarketplaceSortValue, useSearchTab, useSearchText } from '../atoms' +import { useMarketplacePluginSortValue, useMarketplaceTemplateSortValue, useSearchTab, useSearchText } from '../atoms' import { PLUGIN_TYPE_SEARCH_MAP } from '../constants' import Empty from '../empty' import { useMarketplaceContainerScroll } from '../hooks' @@ -23,17 +23,12 @@ const PAGE_SIZE = 40 const ALL_TAB_PREVIEW_SIZE = 8 const ZERO_WIDTH_SPACE = '\u200B' -type SortValue = { sortBy: string, sortOrder: string } +// type SortValue = { sortBy: string, sortOrder: string } -function mapSortForTemplates(sort: SortValue): { sort_by: string, sort_order: string } { - const sortBy = sort.sortBy === 'install_count' ? 'usage_count' : sort.sortBy === 'version_updated_at' ? 'updated_at' : sort.sortBy - return { sort_by: sortBy, sort_order: sort.sortOrder } -} - -function mapSortForCreators(sort: SortValue): { sort_by: string, sort_order: string } { - const sortBy = sort.sortBy === 'install_count' ? 'created_at' : sort.sortBy === 'version_updated_at' ? 'updated_at' : sort.sortBy - return { sort_by: sortBy, sort_order: sort.sortOrder } -} +// function mapSortForCreators(sort: SortValue): { sort_by: string, sort_order: string } { +// const sortBy = sort.sortBy === 'install_count' ? 'created_at' : sort.sortBy === 'version_updated_at' ? 'updated_at' : sort.sortBy +// return { sort_by: sortBy, sort_order: sort.sortOrder } +// } const SearchPage = () => { const { t } = useTranslation() @@ -41,7 +36,8 @@ const SearchPage = () => { const debouncedQuery = useDebounce(searchText, { wait: 500 }) const [searchTabParam, setSearchTab] = useSearchTab() const searchTab = (searchTabParam || 'all') as SearchTab - const sort = useMarketplaceSortValue() + const pluginSort = useMarketplacePluginSortValue() + const templateSort = useMarketplaceTemplateSortValue() const query = debouncedQuery === ZERO_WIDTH_SPACE ? '' : debouncedQuery.trim() const hasQuery = !!searchText && (!!query || searchText === ZERO_WIDTH_SPACE) @@ -52,35 +48,33 @@ const SearchPage = () => { return { query, page_size: searchTab === 'all' ? ALL_TAB_PREVIEW_SIZE : PAGE_SIZE, - sort_by: sort.sortBy, - sort_order: sort.sortOrder, + sort_by: pluginSort.sortBy, + sort_order: pluginSort.sortOrder, type: getPluginFilterType(PLUGIN_TYPE_SEARCH_MAP.all), } as PluginsSearchParams - }, [hasQuery, query, searchTab, sort]) + }, [hasQuery, query, searchTab, pluginSort]) const templatesParams = useMemo(() => { if (!hasQuery) return undefined - const { sort_by, sort_order } = mapSortForTemplates(sort) return { query, page_size: searchTab === 'all' ? ALL_TAB_PREVIEW_SIZE : PAGE_SIZE, - sort_by, - sort_order, + sort_by: templateSort.sortBy, + sort_order: templateSort.sortOrder, } - }, [hasQuery, query, searchTab, sort]) + }, [hasQuery, query, searchTab, templateSort]) const creatorsParams = useMemo(() => { if (!hasQuery) return undefined - const { sort_by, sort_order } = mapSortForCreators(sort) return { query, page_size: searchTab === 'all' ? ALL_TAB_PREVIEW_SIZE : PAGE_SIZE, - sort_by, - sort_order, + // sort_by, + // sort_order, } - }, [hasQuery, query, searchTab, sort]) + }, [hasQuery, query, searchTab]) const fetchPlugins = searchTab === 'all' || searchTab === 'plugins' const fetchTemplates = searchTab === 'all' || searchTab === 'templates' @@ -198,32 +192,17 @@ const SearchPage = () => { ) - const renderPluginsTab = () => { - if (plugins.length === 0 && !pluginsQuery.isLoading) - return + const renderTab = ( + items: T[], + isItemLoading: boolean, + renderSection: (items: T[]) => React.ReactNode, + emptyText?: string, + ) => { + if (items.length === 0 && !isItemLoading) + return return (
- {renderPluginsSection(plugins)} -
- ) - } - - const renderTemplatesTab = () => { - if (templates.length === 0 && !templatesQuery.isLoading) - return - return ( -
- {renderTemplatesSection(templates)} -
- ) - } - - const renderCreatorsTab = () => { - if (creators.length === 0 && !creatorsQuery.isLoading) - return - return ( -
- {renderCreatorsSection(creators)} + {renderSection(items)}
) } @@ -241,7 +220,7 @@ const SearchPage = () => { onChange={v => setSearchTab(v as SearchTab)} options={tabOptions} /> - + {(searchTab === 'templates' || searchTab === 'plugins') && } {isLoading && ( @@ -253,9 +232,9 @@ const SearchPage = () => { {!isLoading && ( <> {searchTab === 'all' && renderAllTab()} - {searchTab === 'plugins' && renderPluginsTab()} - {searchTab === 'templates' && renderTemplatesTab()} - {searchTab === 'creators' && renderCreatorsTab()} + {searchTab === 'plugins' && renderTab(plugins, pluginsQuery.isLoading, renderPluginsSection)} + {searchTab === 'templates' && renderTab(templates, templatesQuery.isLoading, renderTemplatesSection, t('marketplace.noTemplateFound', { ns: 'plugin' }))} + {searchTab === 'creators' && renderTab(creators, creatorsQuery.isLoading, renderCreatorsSection, t('marketplace.noCreatorFound', { ns: 'plugin' }))} )} diff --git a/web/app/components/plugins/marketplace/sort-dropdown/index.spec.tsx b/web/app/components/plugins/marketplace/sort-dropdown/index.spec.tsx index f91c7ba4d3..6483e304c0 100644 --- a/web/app/components/plugins/marketplace/sort-dropdown/index.spec.tsx +++ b/web/app/components/plugins/marketplace/sort-dropdown/index.spec.tsx @@ -30,9 +30,15 @@ vi.mock('#i18n', () => ({ // Mock marketplace atoms with controllable values let mockSort: { sortBy: string, sortOrder: string } = { sortBy: 'install_count', sortOrder: 'DESC' } const mockHandleSortChange = vi.fn() +let mockCreationType = 'plugins' vi.mock('../atoms', () => ({ - useMarketplaceSort: () => [mockSort, mockHandleSortChange], + useActiveSort: () => [mockSort, mockHandleSortChange], + useCreationType: () => [mockCreationType, vi.fn()], +})) + +vi.mock('../search-params', () => ({ + CREATION_TYPE: { plugins: 'plugins', templates: 'templates' }, })) // Mock portal component with controllable open state @@ -91,6 +97,7 @@ describe('SortDropdown', () => { beforeEach(() => { vi.clearAllMocks() mockSort = { sortBy: 'install_count', sortOrder: 'DESC' } + mockCreationType = 'plugins' mockPortalOpenState = false }) diff --git a/web/app/components/plugins/marketplace/sort-dropdown/index.tsx b/web/app/components/plugins/marketplace/sort-dropdown/index.tsx index 1f7bab1005..f710546a6e 100644 --- a/web/app/components/plugins/marketplace/sort-dropdown/index.tsx +++ b/web/app/components/plugins/marketplace/sort-dropdown/index.tsx @@ -10,33 +10,36 @@ import { PortalToFollowElemContent, PortalToFollowElemTrigger, } from '@/app/components/base/portal-to-follow-elem' -import { useMarketplaceSort } from '../atoms' +import { useActiveSort, useCreationType } from '../atoms' +import { CREATION_TYPE } from '../search-params' + +const PLUGIN_SORT_OPTIONS = [ + { value: 'install_count', order: 'DESC', labelKey: 'marketplace.sortOption.mostPopular' }, + { value: 'version_updated_at', order: 'DESC', labelKey: 'marketplace.sortOption.recentlyUpdated' }, + { value: 'created_at', order: 'DESC', labelKey: 'marketplace.sortOption.newlyReleased' }, + { value: 'created_at', order: 'ASC', labelKey: 'marketplace.sortOption.firstReleased' }, +] as const + +const TEMPLATE_SORT_OPTIONS = [ + { value: 'usage_count', order: 'DESC', labelKey: 'marketplace.sortOption.mostPopular' }, + { value: 'updated_at', order: 'DESC', labelKey: 'marketplace.sortOption.recentlyUpdated' }, + { value: 'created_at', order: 'DESC', labelKey: 'marketplace.sortOption.newlyReleased' }, + { value: 'created_at', order: 'ASC', labelKey: 'marketplace.sortOption.firstReleased' }, +] as const const SortDropdown = () => { const { t } = useTranslation() - const options = [ - { - value: 'install_count', - order: 'DESC', - text: t('marketplace.sortOption.mostPopular', { ns: 'plugin' }), - }, - { - value: 'version_updated_at', - order: 'DESC', - text: t('marketplace.sortOption.recentlyUpdated', { ns: 'plugin' }), - }, - { - value: 'created_at', - order: 'DESC', - text: t('marketplace.sortOption.newlyReleased', { ns: 'plugin' }), - }, - { - value: 'created_at', - order: 'ASC', - text: t('marketplace.sortOption.firstReleased', { ns: 'plugin' }), - }, - ] - const [sort, handleSortChange] = useMarketplaceSort() + const [creationType] = useCreationType() + const isTemplates = creationType === CREATION_TYPE.templates + + const rawOptions = isTemplates ? TEMPLATE_SORT_OPTIONS : PLUGIN_SORT_OPTIONS + const options = rawOptions.map(opt => ({ + value: opt.value, + order: opt.order, + text: t(opt.labelKey, { ns: 'plugin' }), + })) + + const [sort, handleSortChange] = useActiveSort() const [open, setOpen] = useState(false) const selectedOption = options.find(option => option.value === sort.sortBy && option.order === sort.sortOrder) ?? options[0] diff --git a/web/app/components/plugins/marketplace/state.ts b/web/app/components/plugins/marketplace/state.ts index 2ddd9ef535..96b412ff80 100644 --- a/web/app/components/plugins/marketplace/state.ts +++ b/web/app/components/plugins/marketplace/state.ts @@ -1,7 +1,7 @@ import type { PluginsSearchParams, TemplateSearchParams } from './types' import { useDebounce } from 'ahooks' import { useCallback, useMemo } from 'react' -import { useActivePluginCategory, useActiveTemplateCategory, useCreationType, useFilterPluginTags, useMarketplaceSearchMode, useMarketplaceSortValue, useSearchText } from './atoms' +import { useActivePluginCategory, useActiveTemplateCategory, useCreationType, useFilterPluginTags, useMarketplacePluginSortValue, useMarketplaceSearchMode, useMarketplaceTemplateSortValue, useSearchText } from './atoms' import { CATEGORY_ALL } from './constants' import { useMarketplaceContainerScroll } from './hooks' import { useMarketplaceCollectionsAndPlugins, useMarketplacePlugins, useMarketplaceTemplateCollectionsAndTemplates, useMarketplaceTemplates } from './query' @@ -29,7 +29,7 @@ export function usePluginsMarketplaceData(enabled = true) { { enabled }, ) - const sort = useMarketplaceSortValue() + const sort = useMarketplacePluginSortValue() const isSearchMode = useMarketplaceSearchMode() const queryParams = useMemo((): PluginsSearchParams | undefined => { if (!isSearchMode) @@ -79,8 +79,8 @@ export function useTemplatesMarketplaceData(enabled = true) { // Template collections query (for non-search mode) const templateCollectionsQuery = useMarketplaceTemplateCollectionsAndTemplates(undefined, { enabled }) - // Sort value - const sort = useMarketplaceSortValue() + // Template-specific sort value (independent from plugin sort) + const sort = useMarketplaceTemplateSortValue() // Search mode: when there's search text or non-default category const isSearchMode = useMarketplaceSearchMode() @@ -89,11 +89,10 @@ 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: sortBy, + sort_by: sort.sortBy, sort_order: sort.sortOrder, } }, [isSearchMode, searchText, activeTemplateCategory, sort]) diff --git a/web/i18n/en-US/plugin.json b/web/i18n/en-US/plugin.json index 52e55ff599..fa0fb5b18f 100644 --- a/web/i18n/en-US/plugin.json +++ b/web/i18n/en-US/plugin.json @@ -197,6 +197,13 @@ "marketplace.empower": "Empower your AI development", "marketplace.featured": "Featured", "marketplace.installs": "installs", + "marketplace.listTopInfo.pluginsSubtitle": "Plugins designed to speed up your coding and AI development", + "marketplace.listTopInfo.pluginsTitleAll": "All plugins", + "marketplace.listTopInfo.pluginsTitleByCategory": "All {{category}} plugins", + "marketplace.listTopInfo.tagsTitle": "Showing results filtered by tags", + "marketplace.listTopInfo.templatesSubtitle": "Workflows designed to speed up your coding and AI development", + "marketplace.listTopInfo.templatesTitleAll": "All templates", + "marketplace.listTopInfo.templatesTitleByCategory": "All {{category}} templates", "marketplace.moreFrom": "More from Marketplace", "marketplace.noCreatorFound": "No creator found", "marketplace.noPluginFound": "No plugin found", diff --git a/web/i18n/zh-Hans/plugin.json b/web/i18n/zh-Hans/plugin.json index 2eee31072f..d302990209 100644 --- a/web/i18n/zh-Hans/plugin.json +++ b/web/i18n/zh-Hans/plugin.json @@ -197,6 +197,13 @@ "marketplace.empower": "助力您的 AI 开发", "marketplace.featured": "精选", "marketplace.installs": "次安装", + "marketplace.listTopInfo.pluginsSubtitle": "帮助你更快进行编码与 AI 开发的插件", + "marketplace.listTopInfo.pluginsTitleAll": "全部插件", + "marketplace.listTopInfo.pluginsTitleByCategory": "全部{{category}}插件", + "marketplace.listTopInfo.tagsTitle": "显示按标签筛选的结果", + "marketplace.listTopInfo.templatesSubtitle": "帮助你更快进行编码与 AI 开发的工作流", + "marketplace.listTopInfo.templatesTitleAll": "全部模板", + "marketplace.listTopInfo.templatesTitleByCategory": "全部{{category}}模板", "marketplace.moreFrom": "更多来自市场", "marketplace.noCreatorFound": "未找到创作者", "marketplace.noPluginFound": "未找到插件",