mirror of
https://github.com/langgenius/dify.git
synced 2026-09-08 02:43:49 +08:00
feat: implement enhanced sorting and category management in marketplace components for improved user experience
This commit is contained in:
parent
36f42ec0a9
commit
7b41fc4d64
@ -2,19 +2,30 @@ import type { SearchTab } from './search-params'
|
|||||||
import type { PluginsSort, SearchParamsFromCollection } from './types'
|
import type { PluginsSort, SearchParamsFromCollection } from './types'
|
||||||
import { atom, useAtom, useAtomValue, useSetAtom } from 'jotai'
|
import { atom, useAtom, useAtomValue, useSetAtom } from 'jotai'
|
||||||
import { useQueryState } from 'nuqs'
|
import { useQueryState } from 'nuqs'
|
||||||
import { useCallback } from 'react'
|
import { useCallback, useMemo } from 'react'
|
||||||
import { CATEGORY_ALL, DEFAULT_SORT, getValidatedPluginCategory, getValidatedTemplateCategory, PLUGIN_CATEGORY_WITH_COLLECTIONS } from './constants'
|
import { CATEGORY_ALL, DEFAULT_PLUGIN_SORT, DEFAULT_TEMPLATE_SORT, getValidatedPluginCategory, getValidatedTemplateCategory, PLUGIN_CATEGORY_WITH_COLLECTIONS } from './constants'
|
||||||
import { CREATION_TYPE, marketplaceSearchParamsParsers } from './search-params'
|
import { CREATION_TYPE, marketplaceSearchParamsParsers } from './search-params'
|
||||||
|
|
||||||
const marketplaceSortAtom = atom<PluginsSort>(DEFAULT_SORT)
|
const marketplacePluginSortAtom = atom<PluginsSort>(DEFAULT_PLUGIN_SORT)
|
||||||
export function useMarketplaceSort() {
|
export function useMarketplacePluginSort() {
|
||||||
return useAtom(marketplaceSortAtom)
|
return useAtom(marketplacePluginSortAtom)
|
||||||
}
|
}
|
||||||
export function useMarketplaceSortValue() {
|
export function useMarketplacePluginSortValue() {
|
||||||
return useAtomValue(marketplaceSortAtom)
|
return useAtomValue(marketplacePluginSortAtom)
|
||||||
}
|
}
|
||||||
export function useSetMarketplaceSort() {
|
export function useSetMarketplacePluginSort() {
|
||||||
return useSetAtom(marketplaceSortAtom)
|
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() {
|
export function useSearchText() {
|
||||||
@ -64,22 +75,56 @@ export function useMarketplaceSearchMode() {
|
|||||||
return isSearchMode
|
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() {
|
export function useMarketplaceMoreClick() {
|
||||||
const [, setQ] = useSearchText()
|
const [, setQ] = useSearchText()
|
||||||
const [, setSearchTab] = useSearchTab()
|
const [, setSearchTab] = useSearchTab()
|
||||||
const setSort = useSetAtom(marketplaceSortAtom)
|
const setPluginSort = useSetAtom(marketplacePluginSortAtom)
|
||||||
|
const setTemplateSort = useSetAtom(marketplaceTemplateSortAtom)
|
||||||
const setSearchMode = useSetAtom(searchModeAtom)
|
const setSearchMode = useSetAtom(searchModeAtom)
|
||||||
|
|
||||||
return useCallback((searchParams?: SearchParamsFromCollection, searchTab?: SearchTab) => {
|
return useCallback((searchParams?: SearchParamsFromCollection, searchTab?: SearchTab) => {
|
||||||
if (!searchParams)
|
if (!searchParams)
|
||||||
return
|
return
|
||||||
setQ(searchParams?.query || '')
|
setQ(searchParams?.query || '')
|
||||||
setSort({
|
if (searchTab === 'templates') {
|
||||||
sortBy: searchParams?.sort_by || DEFAULT_SORT.sortBy,
|
setTemplateSort({
|
||||||
sortOrder: searchParams?.sort_order || DEFAULT_SORT.sortOrder,
|
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)
|
setSearchMode(true)
|
||||||
if (searchTab)
|
if (searchTab)
|
||||||
setSearchTab(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 { ActivePluginType } from '../constants'
|
||||||
import type { PluginCategoryEnum } from '@/app/components/plugins/types'
|
import type { PluginCategoryEnum } from '@/app/components/plugins/types'
|
||||||
import { useTranslation } from '#i18n'
|
|
||||||
import { RiArchive2Line } from '@remixicon/react'
|
import { RiArchive2Line } from '@remixicon/react'
|
||||||
import { useSetAtom } from 'jotai'
|
import { useSetAtom } from 'jotai'
|
||||||
import { Plugin } from '@/app/components/base/icons/src/vender/plugin'
|
import { Plugin } from '@/app/components/base/icons/src/vender/plugin'
|
||||||
import { searchModeAtom, useActivePluginCategory, useFilterPluginTags } from '../atoms'
|
import { searchModeAtom, useActivePluginCategory, useFilterPluginTags } from '../atoms'
|
||||||
import { PLUGIN_CATEGORY_WITH_COLLECTIONS, PLUGIN_TYPE_SEARCH_MAP } from '../constants'
|
import { PLUGIN_CATEGORY_WITH_COLLECTIONS, PLUGIN_TYPE_SEARCH_MAP } from '../constants'
|
||||||
import { MARKETPLACE_TYPE_ICON_COMPONENTS } from '../plugin-type-icons'
|
import { MARKETPLACE_TYPE_ICON_COMPONENTS } from '../plugin-type-icons'
|
||||||
|
import { usePluginCategoryText } from './category-text'
|
||||||
import { CommonCategorySwitch } from './common'
|
import { CommonCategorySwitch } from './common'
|
||||||
import HeroTagsFilter from './hero-tags-filter'
|
import HeroTagsFilter from './hero-tags-filter'
|
||||||
|
|
||||||
@ -17,6 +17,17 @@ type PluginTypeSwitchProps = {
|
|||||||
variant?: 'default' | 'hero'
|
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) => {
|
const getTypeIcon = (value: ActivePluginType, isHeroVariant?: boolean) => {
|
||||||
if (value === PLUGIN_TYPE_SEARCH_MAP.all)
|
if (value === PLUGIN_TYPE_SEARCH_MAP.all)
|
||||||
return isHeroVariant ? <Plugin className="mr-1.5 h-4 w-4" /> : null
|
return isHeroVariant ? <Plugin className="mr-1.5 h-4 w-4" /> : null
|
||||||
@ -30,55 +41,18 @@ export const PluginCategorySwitch = ({
|
|||||||
className,
|
className,
|
||||||
variant = 'default',
|
variant = 'default',
|
||||||
}: PluginTypeSwitchProps) => {
|
}: PluginTypeSwitchProps) => {
|
||||||
const { t } = useTranslation()
|
|
||||||
const [activePluginCategory, handleActivePluginCategoryChange] = useActivePluginCategory()
|
const [activePluginCategory, handleActivePluginCategoryChange] = useActivePluginCategory()
|
||||||
const [filterPluginTags, setFilterPluginTags] = useFilterPluginTags()
|
const [filterPluginTags, setFilterPluginTags] = useFilterPluginTags()
|
||||||
const setSearchMode = useSetAtom(searchModeAtom)
|
const setSearchMode = useSetAtom(searchModeAtom)
|
||||||
|
const getPluginCategoryText = usePluginCategoryText()
|
||||||
|
|
||||||
const isHeroVariant = variant === 'hero'
|
const isHeroVariant = variant === 'hero'
|
||||||
|
|
||||||
const options = [
|
const options = categoryValues.map(value => ({
|
||||||
{
|
value,
|
||||||
value: PLUGIN_TYPE_SEARCH_MAP.all,
|
text: getPluginCategoryText(value, isHeroVariant),
|
||||||
text: isHeroVariant ? t('category.allTypes', { ns: 'plugin' }) : t('category.all', { ns: 'plugin' }),
|
icon: getTypeIcon(value, isHeroVariant),
|
||||||
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 handleChange = (value: string) => {
|
const handleChange = (value: string) => {
|
||||||
handleActivePluginCategoryChange(value)
|
handleActivePluginCategoryChange(value)
|
||||||
|
|||||||
@ -1,9 +1,9 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import { useTranslation } from '#i18n'
|
|
||||||
import { Playground } from '@/app/components/base/icons/src/vender/plugin'
|
import { Playground } from '@/app/components/base/icons/src/vender/plugin'
|
||||||
import { useActiveTemplateCategory } from '../atoms'
|
import { useActiveTemplateCategory } from '../atoms'
|
||||||
import { CATEGORY_ALL, TEMPLATE_CATEGORY_MAP } from '../constants'
|
import { CATEGORY_ALL, TEMPLATE_CATEGORY_MAP } from '../constants'
|
||||||
|
import { useTemplateCategoryText } from './category-text'
|
||||||
import { CommonCategorySwitch } from './common'
|
import { CommonCategorySwitch } from './common'
|
||||||
|
|
||||||
type TemplateCategorySwitchProps = {
|
type TemplateCategorySwitchProps = {
|
||||||
@ -11,57 +11,31 @@ type TemplateCategorySwitchProps = {
|
|||||||
variant?: 'default' | 'hero'
|
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 = ({
|
export const TemplateCategorySwitch = ({
|
||||||
className,
|
className,
|
||||||
variant = 'default',
|
variant = 'default',
|
||||||
}: TemplateCategorySwitchProps) => {
|
}: TemplateCategorySwitchProps) => {
|
||||||
const { t } = useTranslation()
|
|
||||||
const [activeTemplateCategory, handleActiveTemplateCategoryChange] = useActiveTemplateCategory()
|
const [activeTemplateCategory, handleActiveTemplateCategoryChange] = useActiveTemplateCategory()
|
||||||
|
const getTemplateCategoryText = useTemplateCategoryText()
|
||||||
|
|
||||||
const isHeroVariant = variant === 'hero'
|
const isHeroVariant = variant === 'hero'
|
||||||
|
|
||||||
const options = [
|
const options = categoryValues.map(value => ({
|
||||||
{
|
value,
|
||||||
value: CATEGORY_ALL,
|
text: getTemplateCategoryText(value),
|
||||||
text: t('marketplace.templateCategory.all', { ns: 'plugin' }),
|
icon: value === CATEGORY_ALL && isHeroVariant ? <Playground className="mr-1.5 h-4 w-4" /> : null,
|
||||||
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,
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CommonCategorySwitch
|
<CommonCategorySwitch
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { PluginCategoryEnum } from '../types'
|
import { PluginCategoryEnum } from '../types'
|
||||||
|
|
||||||
export const DEFAULT_SORT = {
|
export const DEFAULT_PLUGIN_SORT = {
|
||||||
sortBy: 'install_count',
|
sortBy: 'install_count',
|
||||||
sortOrder: 'DESC',
|
sortOrder: 'DESC',
|
||||||
}
|
}
|
||||||
|
|||||||
@ -9,7 +9,7 @@ import { PluginCategoryEnum } from '@/app/components/plugins/types'
|
|||||||
// ================================
|
// ================================
|
||||||
|
|
||||||
// Note: Import after mocks are set up
|
// 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 {
|
import {
|
||||||
getFormattedPlugin,
|
getFormattedPlugin,
|
||||||
getPluginCondition,
|
getPluginCondition,
|
||||||
@ -406,20 +406,20 @@ const createMockCollection = (overrides?: Partial<PluginCollection>): PluginColl
|
|||||||
// Constants Tests
|
// Constants Tests
|
||||||
// ================================
|
// ================================
|
||||||
describe('constants', () => {
|
describe('constants', () => {
|
||||||
describe('DEFAULT_SORT', () => {
|
describe('DEFAULT_PLUGIN_SORT', () => {
|
||||||
it('should have correct default sort values', () => {
|
it('should have correct default sort values', () => {
|
||||||
expect(DEFAULT_SORT).toEqual({
|
expect(DEFAULT_PLUGIN_SORT).toEqual({
|
||||||
sortBy: 'install_count',
|
sortBy: 'install_count',
|
||||||
sortOrder: 'DESC',
|
sortOrder: 'DESC',
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should be immutable at runtime', () => {
|
it('should be immutable at runtime', () => {
|
||||||
const originalSortBy = DEFAULT_SORT.sortBy
|
const originalSortBy = DEFAULT_PLUGIN_SORT.sortBy
|
||||||
const originalSortOrder = DEFAULT_SORT.sortOrder
|
const originalSortOrder = DEFAULT_PLUGIN_SORT.sortOrder
|
||||||
|
|
||||||
expect(DEFAULT_SORT.sortBy).toBe(originalSortBy)
|
expect(DEFAULT_PLUGIN_SORT.sortBy).toBe(originalSortBy)
|
||||||
expect(DEFAULT_SORT.sortOrder).toBe(originalSortOrder)
|
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 Loading from '@/app/components/base/loading'
|
||||||
import { isPluginsData, useMarketplaceData } from '../state'
|
import { isPluginsData, useMarketplaceData } from '../state'
|
||||||
import FlatList from './flat-list'
|
import FlatList from './flat-list'
|
||||||
|
import ListTopInfo from './list-top-info'
|
||||||
import ListWithCollection from './list-with-collection'
|
import ListWithCollection from './list-with-collection'
|
||||||
|
|
||||||
type ListWrapperProps = {
|
type ListWrapperProps = {
|
||||||
@ -17,7 +18,12 @@ const ListWrapper = ({ showInstallButton }: ListWrapperProps) => {
|
|||||||
if (isPluginsData(marketplaceData)) {
|
if (isPluginsData(marketplaceData)) {
|
||||||
const { pluginCollections, pluginCollectionPluginsMap, plugins } = marketplaceData
|
const { pluginCollections, pluginCollectionPluginsMap, plugins } = marketplaceData
|
||||||
return plugins !== undefined
|
return plugins !== undefined
|
||||||
? <FlatList variant="plugins" items={plugins} showInstallButton={showInstallButton} />
|
? (
|
||||||
|
<>
|
||||||
|
<ListTopInfo variant="plugins" />
|
||||||
|
<FlatList variant="plugins" items={plugins} showInstallButton={showInstallButton} />
|
||||||
|
</>
|
||||||
|
)
|
||||||
: (
|
: (
|
||||||
<ListWithCollection
|
<ListWithCollection
|
||||||
variant="plugins"
|
variant="plugins"
|
||||||
@ -30,7 +36,12 @@ const ListWrapper = ({ showInstallButton }: ListWrapperProps) => {
|
|||||||
|
|
||||||
const { templateCollections, templateCollectionTemplatesMap, templates } = marketplaceData
|
const { templateCollections, templateCollectionTemplatesMap, templates } = marketplaceData
|
||||||
return templates !== undefined
|
return templates !== undefined
|
||||||
? <FlatList variant="templates" items={templates} />
|
? (
|
||||||
|
<>
|
||||||
|
<ListTopInfo variant="templates" />
|
||||||
|
<FlatList variant="templates" items={templates} />
|
||||||
|
</>
|
||||||
|
)
|
||||||
: (
|
: (
|
||||||
<ListWithCollection
|
<ListWithCollection
|
||||||
variant="templates"
|
variant="templates"
|
||||||
|
|||||||
@ -81,7 +81,12 @@ vi.mock('../atoms', () => ({
|
|||||||
useSearchText: () => [mockSearchText, mockHandleSearchTextChange],
|
useSearchText: () => [mockSearchText, mockHandleSearchTextChange],
|
||||||
useFilterPluginTags: () => [mockFilterPluginTags, mockHandleFilterPluginTagsChange],
|
useFilterPluginTags: () => [mockFilterPluginTags, mockHandleFilterPluginTagsChange],
|
||||||
useActivePluginCategory: () => [mockActivePluginCategory, vi.fn()],
|
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: {},
|
searchModeAtom: {},
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
|||||||
@ -8,7 +8,7 @@ import { useDebounce } from 'ahooks'
|
|||||||
import { useCallback, useMemo } from 'react'
|
import { useCallback, useMemo } from 'react'
|
||||||
import Loading from '@/app/components/base/loading'
|
import Loading from '@/app/components/base/loading'
|
||||||
import SegmentedControl from '@/app/components/base/segmented-control'
|
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 { PLUGIN_TYPE_SEARCH_MAP } from '../constants'
|
||||||
import Empty from '../empty'
|
import Empty from '../empty'
|
||||||
import { useMarketplaceContainerScroll } from '../hooks'
|
import { useMarketplaceContainerScroll } from '../hooks'
|
||||||
@ -23,17 +23,12 @@ const PAGE_SIZE = 40
|
|||||||
const ALL_TAB_PREVIEW_SIZE = 8
|
const ALL_TAB_PREVIEW_SIZE = 8
|
||||||
const ZERO_WIDTH_SPACE = '\u200B'
|
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 } {
|
// function mapSortForCreators(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
|
// 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 }
|
// 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 SearchPage = () => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
@ -41,7 +36,8 @@ const SearchPage = () => {
|
|||||||
const debouncedQuery = useDebounce(searchText, { wait: 500 })
|
const debouncedQuery = useDebounce(searchText, { wait: 500 })
|
||||||
const [searchTabParam, setSearchTab] = useSearchTab()
|
const [searchTabParam, setSearchTab] = useSearchTab()
|
||||||
const searchTab = (searchTabParam || 'all') as SearchTab
|
const searchTab = (searchTabParam || 'all') as SearchTab
|
||||||
const sort = useMarketplaceSortValue()
|
const pluginSort = useMarketplacePluginSortValue()
|
||||||
|
const templateSort = useMarketplaceTemplateSortValue()
|
||||||
|
|
||||||
const query = debouncedQuery === ZERO_WIDTH_SPACE ? '' : debouncedQuery.trim()
|
const query = debouncedQuery === ZERO_WIDTH_SPACE ? '' : debouncedQuery.trim()
|
||||||
const hasQuery = !!searchText && (!!query || searchText === ZERO_WIDTH_SPACE)
|
const hasQuery = !!searchText && (!!query || searchText === ZERO_WIDTH_SPACE)
|
||||||
@ -52,35 +48,33 @@ const SearchPage = () => {
|
|||||||
return {
|
return {
|
||||||
query,
|
query,
|
||||||
page_size: searchTab === 'all' ? ALL_TAB_PREVIEW_SIZE : PAGE_SIZE,
|
page_size: searchTab === 'all' ? ALL_TAB_PREVIEW_SIZE : PAGE_SIZE,
|
||||||
sort_by: sort.sortBy,
|
sort_by: pluginSort.sortBy,
|
||||||
sort_order: sort.sortOrder,
|
sort_order: pluginSort.sortOrder,
|
||||||
type: getPluginFilterType(PLUGIN_TYPE_SEARCH_MAP.all),
|
type: getPluginFilterType(PLUGIN_TYPE_SEARCH_MAP.all),
|
||||||
} as PluginsSearchParams
|
} as PluginsSearchParams
|
||||||
}, [hasQuery, query, searchTab, sort])
|
}, [hasQuery, query, searchTab, pluginSort])
|
||||||
|
|
||||||
const templatesParams = useMemo(() => {
|
const templatesParams = useMemo(() => {
|
||||||
if (!hasQuery)
|
if (!hasQuery)
|
||||||
return undefined
|
return undefined
|
||||||
const { sort_by, sort_order } = mapSortForTemplates(sort)
|
|
||||||
return {
|
return {
|
||||||
query,
|
query,
|
||||||
page_size: searchTab === 'all' ? ALL_TAB_PREVIEW_SIZE : PAGE_SIZE,
|
page_size: searchTab === 'all' ? ALL_TAB_PREVIEW_SIZE : PAGE_SIZE,
|
||||||
sort_by,
|
sort_by: templateSort.sortBy,
|
||||||
sort_order,
|
sort_order: templateSort.sortOrder,
|
||||||
}
|
}
|
||||||
}, [hasQuery, query, searchTab, sort])
|
}, [hasQuery, query, searchTab, templateSort])
|
||||||
|
|
||||||
const creatorsParams = useMemo(() => {
|
const creatorsParams = useMemo(() => {
|
||||||
if (!hasQuery)
|
if (!hasQuery)
|
||||||
return undefined
|
return undefined
|
||||||
const { sort_by, sort_order } = mapSortForCreators(sort)
|
|
||||||
return {
|
return {
|
||||||
query,
|
query,
|
||||||
page_size: searchTab === 'all' ? ALL_TAB_PREVIEW_SIZE : PAGE_SIZE,
|
page_size: searchTab === 'all' ? ALL_TAB_PREVIEW_SIZE : PAGE_SIZE,
|
||||||
sort_by,
|
// sort_by,
|
||||||
sort_order,
|
// sort_order,
|
||||||
}
|
}
|
||||||
}, [hasQuery, query, searchTab, sort])
|
}, [hasQuery, query, searchTab])
|
||||||
|
|
||||||
const fetchPlugins = searchTab === 'all' || searchTab === 'plugins'
|
const fetchPlugins = searchTab === 'all' || searchTab === 'plugins'
|
||||||
const fetchTemplates = searchTab === 'all' || searchTab === 'templates'
|
const fetchTemplates = searchTab === 'all' || searchTab === 'templates'
|
||||||
@ -198,32 +192,17 @@ const SearchPage = () => {
|
|||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
||||||
const renderPluginsTab = () => {
|
const renderTab = <T,>(
|
||||||
if (plugins.length === 0 && !pluginsQuery.isLoading)
|
items: T[],
|
||||||
return <Empty />
|
isItemLoading: boolean,
|
||||||
|
renderSection: (items: T[]) => React.ReactNode,
|
||||||
|
emptyText?: string,
|
||||||
|
) => {
|
||||||
|
if (items.length === 0 && !isItemLoading)
|
||||||
|
return <Empty text={emptyText} />
|
||||||
return (
|
return (
|
||||||
<div className="py-4">
|
<div className="py-4">
|
||||||
{renderPluginsSection(plugins)}
|
{renderSection(items)}
|
||||||
</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)}
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -241,7 +220,7 @@ const SearchPage = () => {
|
|||||||
onChange={v => setSearchTab(v as SearchTab)}
|
onChange={v => setSearchTab(v as SearchTab)}
|
||||||
options={tabOptions}
|
options={tabOptions}
|
||||||
/>
|
/>
|
||||||
<SortDropdown />
|
{(searchTab === 'templates' || searchTab === 'plugins') && <SortDropdown />}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{isLoading && (
|
{isLoading && (
|
||||||
@ -253,9 +232,9 @@ const SearchPage = () => {
|
|||||||
{!isLoading && (
|
{!isLoading && (
|
||||||
<>
|
<>
|
||||||
{searchTab === 'all' && renderAllTab()}
|
{searchTab === 'all' && renderAllTab()}
|
||||||
{searchTab === 'plugins' && renderPluginsTab()}
|
{searchTab === 'plugins' && renderTab(plugins, pluginsQuery.isLoading, renderPluginsSection)}
|
||||||
{searchTab === 'templates' && renderTemplatesTab()}
|
{searchTab === 'templates' && renderTab(templates, templatesQuery.isLoading, renderTemplatesSection, t('marketplace.noTemplateFound', { ns: 'plugin' }))}
|
||||||
{searchTab === 'creators' && renderCreatorsTab()}
|
{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
|
// Mock marketplace atoms with controllable values
|
||||||
let mockSort: { sortBy: string, sortOrder: string } = { sortBy: 'install_count', sortOrder: 'DESC' }
|
let mockSort: { sortBy: string, sortOrder: string } = { sortBy: 'install_count', sortOrder: 'DESC' }
|
||||||
const mockHandleSortChange = vi.fn()
|
const mockHandleSortChange = vi.fn()
|
||||||
|
let mockCreationType = 'plugins'
|
||||||
|
|
||||||
vi.mock('../atoms', () => ({
|
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
|
// Mock portal component with controllable open state
|
||||||
@ -91,6 +97,7 @@ describe('SortDropdown', () => {
|
|||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.clearAllMocks()
|
vi.clearAllMocks()
|
||||||
mockSort = { sortBy: 'install_count', sortOrder: 'DESC' }
|
mockSort = { sortBy: 'install_count', sortOrder: 'DESC' }
|
||||||
|
mockCreationType = 'plugins'
|
||||||
mockPortalOpenState = false
|
mockPortalOpenState = false
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@ -10,33 +10,36 @@ import {
|
|||||||
PortalToFollowElemContent,
|
PortalToFollowElemContent,
|
||||||
PortalToFollowElemTrigger,
|
PortalToFollowElemTrigger,
|
||||||
} from '@/app/components/base/portal-to-follow-elem'
|
} 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 SortDropdown = () => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const options = [
|
const [creationType] = useCreationType()
|
||||||
{
|
const isTemplates = creationType === CREATION_TYPE.templates
|
||||||
value: 'install_count',
|
|
||||||
order: 'DESC',
|
const rawOptions = isTemplates ? TEMPLATE_SORT_OPTIONS : PLUGIN_SORT_OPTIONS
|
||||||
text: t('marketplace.sortOption.mostPopular', { ns: 'plugin' }),
|
const options = rawOptions.map(opt => ({
|
||||||
},
|
value: opt.value,
|
||||||
{
|
order: opt.order,
|
||||||
value: 'version_updated_at',
|
text: t(opt.labelKey, { ns: 'plugin' }),
|
||||||
order: 'DESC',
|
}))
|
||||||
text: t('marketplace.sortOption.recentlyUpdated', { ns: 'plugin' }),
|
|
||||||
},
|
const [sort, handleSortChange] = useActiveSort()
|
||||||
{
|
|
||||||
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 [open, setOpen] = useState(false)
|
const [open, setOpen] = useState(false)
|
||||||
const selectedOption = options.find(option => option.value === sort.sortBy && option.order === sort.sortOrder) ?? options[0]
|
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 type { PluginsSearchParams, TemplateSearchParams } from './types'
|
||||||
import { useDebounce } from 'ahooks'
|
import { useDebounce } from 'ahooks'
|
||||||
import { useCallback, useMemo } from 'react'
|
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 { CATEGORY_ALL } from './constants'
|
||||||
import { useMarketplaceContainerScroll } from './hooks'
|
import { useMarketplaceContainerScroll } from './hooks'
|
||||||
import { useMarketplaceCollectionsAndPlugins, useMarketplacePlugins, useMarketplaceTemplateCollectionsAndTemplates, useMarketplaceTemplates } from './query'
|
import { useMarketplaceCollectionsAndPlugins, useMarketplacePlugins, useMarketplaceTemplateCollectionsAndTemplates, useMarketplaceTemplates } from './query'
|
||||||
@ -29,7 +29,7 @@ export function usePluginsMarketplaceData(enabled = true) {
|
|||||||
{ enabled },
|
{ enabled },
|
||||||
)
|
)
|
||||||
|
|
||||||
const sort = useMarketplaceSortValue()
|
const sort = useMarketplacePluginSortValue()
|
||||||
const isSearchMode = useMarketplaceSearchMode()
|
const isSearchMode = useMarketplaceSearchMode()
|
||||||
const queryParams = useMemo((): PluginsSearchParams | undefined => {
|
const queryParams = useMemo((): PluginsSearchParams | undefined => {
|
||||||
if (!isSearchMode)
|
if (!isSearchMode)
|
||||||
@ -79,8 +79,8 @@ export function useTemplatesMarketplaceData(enabled = true) {
|
|||||||
// Template collections query (for non-search mode)
|
// Template collections query (for non-search mode)
|
||||||
const templateCollectionsQuery = useMarketplaceTemplateCollectionsAndTemplates(undefined, { enabled })
|
const templateCollectionsQuery = useMarketplaceTemplateCollectionsAndTemplates(undefined, { enabled })
|
||||||
|
|
||||||
// Sort value
|
// Template-specific sort value (independent from plugin sort)
|
||||||
const sort = useMarketplaceSortValue()
|
const sort = useMarketplaceTemplateSortValue()
|
||||||
|
|
||||||
// Search mode: when there's search text or non-default category
|
// Search mode: when there's search text or non-default category
|
||||||
const isSearchMode = useMarketplaceSearchMode()
|
const isSearchMode = useMarketplaceSearchMode()
|
||||||
@ -89,11 +89,10 @@ export function useTemplatesMarketplaceData(enabled = true) {
|
|||||||
const queryParams = useMemo((): TemplateSearchParams | undefined => {
|
const queryParams = useMemo((): TemplateSearchParams | undefined => {
|
||||||
if (!isSearchMode)
|
if (!isSearchMode)
|
||||||
return undefined
|
return undefined
|
||||||
const sortBy = sort.sortBy === 'install_count' ? 'usage_count' : sort.sortBy === 'version_updated_at' ? 'updated_at' : sort.sortBy
|
|
||||||
return {
|
return {
|
||||||
query: searchText,
|
query: searchText,
|
||||||
categories: activeTemplateCategory === CATEGORY_ALL ? undefined : [activeTemplateCategory],
|
categories: activeTemplateCategory === CATEGORY_ALL ? undefined : [activeTemplateCategory],
|
||||||
sort_by: sortBy,
|
sort_by: sort.sortBy,
|
||||||
sort_order: sort.sortOrder,
|
sort_order: sort.sortOrder,
|
||||||
}
|
}
|
||||||
}, [isSearchMode, searchText, activeTemplateCategory, sort])
|
}, [isSearchMode, searchText, activeTemplateCategory, sort])
|
||||||
|
|||||||
@ -197,6 +197,13 @@
|
|||||||
"marketplace.empower": "Empower your AI development",
|
"marketplace.empower": "Empower your AI development",
|
||||||
"marketplace.featured": "Featured",
|
"marketplace.featured": "Featured",
|
||||||
"marketplace.installs": "installs",
|
"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.moreFrom": "More from Marketplace",
|
||||||
"marketplace.noCreatorFound": "No creator found",
|
"marketplace.noCreatorFound": "No creator found",
|
||||||
"marketplace.noPluginFound": "No plugin found",
|
"marketplace.noPluginFound": "No plugin found",
|
||||||
|
|||||||
@ -197,6 +197,13 @@
|
|||||||
"marketplace.empower": "助力您的 AI 开发",
|
"marketplace.empower": "助力您的 AI 开发",
|
||||||
"marketplace.featured": "精选",
|
"marketplace.featured": "精选",
|
||||||
"marketplace.installs": "次安装",
|
"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.moreFrom": "更多来自市场",
|
||||||
"marketplace.noCreatorFound": "未找到创作者",
|
"marketplace.noCreatorFound": "未找到创作者",
|
||||||
"marketplace.noPluginFound": "未找到插件",
|
"marketplace.noPluginFound": "未找到插件",
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user