mirror of
https://github.com/langgenius/dify.git
synced 2026-03-21 06:18:27 +08:00
feat: implement enhanced sorting and category management in marketplace components for improved user experience
This commit is contained in:
@ -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<PluginsSort>(DEFAULT_SORT)
|
||||
export function useMarketplaceSort() {
|
||||
return useAtom(marketplaceSortAtom)
|
||||
const marketplacePluginSortAtom = atom<PluginsSort>(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<PluginsSort>(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])
|
||||
}
|
||||
|
||||
@ -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' })
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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 ? <Plugin className="mr-1.5 h-4 w-4" /> : 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)
|
||||
|
||||
@ -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 ? <Playground className="mr-1.5 h-4 w-4" /> : 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 ? <Playground className="mr-1.5 h-4 w-4" /> : null,
|
||||
}))
|
||||
|
||||
return (
|
||||
<CommonCategorySwitch
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { PluginCategoryEnum } from '../types'
|
||||
|
||||
export const DEFAULT_SORT = {
|
||||
export const DEFAULT_PLUGIN_SORT = {
|
||||
sortBy: 'install_count',
|
||||
sortOrder: 'DESC',
|
||||
}
|
||||
|
||||
@ -9,7 +9,7 @@ import { PluginCategoryEnum } from '@/app/components/plugins/types'
|
||||
// ================================
|
||||
|
||||
// Note: Import after mocks are set up
|
||||
import { DEFAULT_SORT, DEFAULT_TEMPLATE_SORT, PLUGIN_TYPE_SEARCH_MAP, SCROLL_BOTTOM_THRESHOLD } from './constants'
|
||||
import { DEFAULT_PLUGIN_SORT, DEFAULT_TEMPLATE_SORT, PLUGIN_TYPE_SEARCH_MAP, SCROLL_BOTTOM_THRESHOLD } from './constants'
|
||||
import {
|
||||
getFormattedPlugin,
|
||||
getPluginCondition,
|
||||
@ -406,20 +406,20 @@ const createMockCollection = (overrides?: Partial<PluginCollection>): 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)
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@ -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 (
|
||||
<div className="mb-4 flex items-center justify-between pt-3">
|
||||
<p className="title-xl-semi-bold text-text-primary">
|
||||
{t('marketplace.listTopInfo.tagsTitle', { ns: 'plugin' })}
|
||||
</p>
|
||||
<SortDropdown />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
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 (
|
||||
<div className="mb-4 flex items-center justify-between pt-3">
|
||||
<div className="flex min-w-0 flex-1 flex-col gap-0.5">
|
||||
<p className="title-xl-semi-bold truncate text-text-primary">
|
||||
{title}
|
||||
</p>
|
||||
<p className="system-xs-regular truncate text-text-tertiary">
|
||||
{t(subtitleKey, { ns: 'plugin' })}
|
||||
</p>
|
||||
</div>
|
||||
<SortDropdown />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ListTopInfo
|
||||
@ -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
|
||||
? <FlatList variant="plugins" items={plugins} showInstallButton={showInstallButton} />
|
||||
? (
|
||||
<>
|
||||
<ListTopInfo variant="plugins" />
|
||||
<FlatList variant="plugins" items={plugins} showInstallButton={showInstallButton} />
|
||||
</>
|
||||
)
|
||||
: (
|
||||
<ListWithCollection
|
||||
variant="plugins"
|
||||
@ -30,7 +36,12 @@ const ListWrapper = ({ showInstallButton }: ListWrapperProps) => {
|
||||
|
||||
const { templateCollections, templateCollectionTemplatesMap, templates } = marketplaceData
|
||||
return templates !== undefined
|
||||
? <FlatList variant="templates" items={templates} />
|
||||
? (
|
||||
<>
|
||||
<ListTopInfo variant="templates" />
|
||||
<FlatList variant="templates" items={templates} />
|
||||
</>
|
||||
)
|
||||
: (
|
||||
<ListWithCollection
|
||||
variant="templates"
|
||||
|
||||
@ -81,7 +81,12 @@ vi.mock('../atoms', () => ({
|
||||
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: {},
|
||||
}))
|
||||
|
||||
|
||||
@ -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 = () => {
|
||||
</div>
|
||||
)
|
||||
|
||||
const renderPluginsTab = () => {
|
||||
if (plugins.length === 0 && !pluginsQuery.isLoading)
|
||||
return <Empty />
|
||||
const renderTab = <T,>(
|
||||
items: T[],
|
||||
isItemLoading: boolean,
|
||||
renderSection: (items: T[]) => React.ReactNode,
|
||||
emptyText?: string,
|
||||
) => {
|
||||
if (items.length === 0 && !isItemLoading)
|
||||
return <Empty text={emptyText} />
|
||||
return (
|
||||
<div className="py-4">
|
||||
{renderPluginsSection(plugins)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const renderTemplatesTab = () => {
|
||||
if (templates.length === 0 && !templatesQuery.isLoading)
|
||||
return <Empty text={t('marketplace.noTemplateFound', { ns: 'plugin' })} />
|
||||
return (
|
||||
<div className="py-4">
|
||||
{renderTemplatesSection(templates)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const renderCreatorsTab = () => {
|
||||
if (creators.length === 0 && !creatorsQuery.isLoading)
|
||||
return <Empty text={t('marketplace.noCreatorFound', { ns: 'plugin' })} />
|
||||
return (
|
||||
<div className="py-4">
|
||||
{renderCreatorsSection(creators)}
|
||||
{renderSection(items)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@ -241,7 +220,7 @@ const SearchPage = () => {
|
||||
onChange={v => setSearchTab(v as SearchTab)}
|
||||
options={tabOptions}
|
||||
/>
|
||||
<SortDropdown />
|
||||
{(searchTab === 'templates' || searchTab === 'plugins') && <SortDropdown />}
|
||||
</div>
|
||||
|
||||
{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' }))}
|
||||
</>
|
||||
)}
|
||||
|
||||
|
||||
@ -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
|
||||
})
|
||||
|
||||
|
||||
@ -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]
|
||||
|
||||
|
||||
@ -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])
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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": "未找到插件",
|
||||
|
||||
Reference in New Issue
Block a user