mirror of
https://github.com/langgenius/dify.git
synced 2026-05-13 08:57:28 +08:00
feat: support language filter
This commit is contained in:
parent
f49e8954d0
commit
0645eaeef9
@ -83,6 +83,10 @@ export function useFilterPluginTags() {
|
|||||||
return useQueryState('tags', marketplaceSearchParamsParsers.tags)
|
return useQueryState('tags', marketplaceSearchParamsParsers.tags)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function useFilterTemplateLanguages() {
|
||||||
|
return useQueryState('languages', marketplaceSearchParamsParsers.languages)
|
||||||
|
}
|
||||||
|
|
||||||
export function useSearchTab() {
|
export function useSearchTab() {
|
||||||
const isAtMarketplace = useAtomValue(isMarketplacePlatformAtom)
|
const isAtMarketplace = useAtomValue(isMarketplacePlatformAtom)
|
||||||
|
|
||||||
@ -151,6 +155,7 @@ export function useMarketplaceSearchMode() {
|
|||||||
const [searchText] = useSearchText()
|
const [searchText] = useSearchText()
|
||||||
const [searchTab] = useSearchTab()
|
const [searchTab] = useSearchTab()
|
||||||
const [filterPluginTags] = useFilterPluginTags()
|
const [filterPluginTags] = useFilterPluginTags()
|
||||||
|
const [filterTemplateLanguages] = useFilterTemplateLanguages()
|
||||||
const [activePluginCategory] = useActivePluginCategory()
|
const [activePluginCategory] = useActivePluginCategory()
|
||||||
const [activeTemplateCategory] = useActiveTemplateCategory()
|
const [activeTemplateCategory] = useActiveTemplateCategory()
|
||||||
const isPluginsView = creationType === CREATION_TYPE.plugins
|
const isPluginsView = creationType === CREATION_TYPE.plugins
|
||||||
@ -160,6 +165,7 @@ export function useMarketplaceSearchMode() {
|
|||||||
|| (isPluginsView && filterPluginTags.length > 0)
|
|| (isPluginsView && filterPluginTags.length > 0)
|
||||||
|| (searchMode ?? (isPluginsView && !PLUGIN_CATEGORY_WITH_COLLECTIONS.has(activePluginCategory)))
|
|| (searchMode ?? (isPluginsView && !PLUGIN_CATEGORY_WITH_COLLECTIONS.has(activePluginCategory)))
|
||||||
|| (!isPluginsView && activeTemplateCategory !== CATEGORY_ALL)
|
|| (!isPluginsView && activeTemplateCategory !== CATEGORY_ALL)
|
||||||
|
|| (!isPluginsView && filterTemplateLanguages.length > 0)
|
||||||
return isSearchMode
|
return isSearchMode
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -0,0 +1,152 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import { useTranslation } from '#i18n'
|
||||||
|
import { RiArrowDownSLine, RiCloseCircleFill, RiGlobalLine } from '@remixicon/react'
|
||||||
|
import * as React from 'react'
|
||||||
|
import { useMemo, useState } from 'react'
|
||||||
|
import Checkbox from '@/app/components/base/checkbox'
|
||||||
|
import Input from '@/app/components/base/input'
|
||||||
|
import {
|
||||||
|
PortalToFollowElem,
|
||||||
|
PortalToFollowElemContent,
|
||||||
|
PortalToFollowElemTrigger,
|
||||||
|
} from '@/app/components/base/portal-to-follow-elem'
|
||||||
|
import { cn } from '@/utils/classnames'
|
||||||
|
import { LANGUAGE_OPTIONS } from '../search-page/constants'
|
||||||
|
|
||||||
|
type HeroLanguagesFilterProps = {
|
||||||
|
languages: string[]
|
||||||
|
onLanguagesChange: (languages: string[]) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const LANGUAGE_LABEL_MAP: Record<string, string> = LANGUAGE_OPTIONS.reduce((acc, option) => {
|
||||||
|
acc[option.value] = option.nativeLabel
|
||||||
|
return acc
|
||||||
|
}, {} as Record<string, string>)
|
||||||
|
|
||||||
|
const HeroLanguagesFilter = ({
|
||||||
|
languages,
|
||||||
|
onLanguagesChange,
|
||||||
|
}: HeroLanguagesFilterProps) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const [open, setOpen] = useState(false)
|
||||||
|
const [searchText, setSearchText] = useState('')
|
||||||
|
const selectedLanguagesLength = languages.length
|
||||||
|
const hasSelected = selectedLanguagesLength > 0
|
||||||
|
|
||||||
|
const filteredOptions = useMemo(() => {
|
||||||
|
if (!searchText)
|
||||||
|
return LANGUAGE_OPTIONS
|
||||||
|
const normalizedSearchText = searchText.toLowerCase()
|
||||||
|
return LANGUAGE_OPTIONS.filter(option =>
|
||||||
|
option.nativeLabel.toLowerCase().includes(normalizedSearchText)
|
||||||
|
|| option.label.toLowerCase().includes(normalizedSearchText),
|
||||||
|
)
|
||||||
|
}, [searchText])
|
||||||
|
|
||||||
|
const handleCheck = (value: string) => {
|
||||||
|
if (languages.includes(value))
|
||||||
|
onLanguagesChange(languages.filter(language => language !== value))
|
||||||
|
else
|
||||||
|
onLanguagesChange([...languages, value])
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PortalToFollowElem
|
||||||
|
placement="bottom-start"
|
||||||
|
offset={{
|
||||||
|
mainAxis: 4,
|
||||||
|
crossAxis: -6,
|
||||||
|
}}
|
||||||
|
open={open}
|
||||||
|
onOpenChange={setOpen}
|
||||||
|
>
|
||||||
|
<PortalToFollowElemTrigger
|
||||||
|
className="shrink-0"
|
||||||
|
onClick={() => setOpen(v => !v)}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
'flex h-8 cursor-pointer select-none items-center gap-1.5 rounded-lg px-2.5 py-1.5',
|
||||||
|
!hasSelected && 'border border-white/30 text-text-primary-on-surface',
|
||||||
|
!hasSelected && open && 'bg-state-base-hover',
|
||||||
|
!hasSelected && !open && 'hover:bg-state-base-hover',
|
||||||
|
hasSelected && 'border-effect-highlight border bg-components-button-secondary-bg-hover shadow-md backdrop-blur-[5px]',
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<RiGlobalLine
|
||||||
|
className={cn(
|
||||||
|
'size-4 shrink-0',
|
||||||
|
hasSelected ? 'text-saas-dify-blue-inverted' : 'text-text-primary-on-surface',
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<div className="system-md-medium flex items-center gap-0.5">
|
||||||
|
{!hasSelected && (
|
||||||
|
<span>{t('marketplace.searchFilterLanguage', { ns: 'plugin' })}</span>
|
||||||
|
)}
|
||||||
|
{hasSelected && (
|
||||||
|
<span className="text-saas-dify-blue-inverted">
|
||||||
|
{languages
|
||||||
|
.map(language => LANGUAGE_LABEL_MAP[language])
|
||||||
|
.filter(Boolean)
|
||||||
|
.slice(0, 2)
|
||||||
|
.join(', ')}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{selectedLanguagesLength > 2 && (
|
||||||
|
<div className="flex min-w-4 items-center justify-center rounded-[5px] border border-saas-dify-blue-inverted px-1 py-0.5">
|
||||||
|
<span className="system-2xs-medium-uppercase text-saas-dify-blue-inverted">
|
||||||
|
+
|
||||||
|
{selectedLanguagesLength - 2}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{hasSelected && (
|
||||||
|
<RiCloseCircleFill
|
||||||
|
className="size-4 shrink-0 text-saas-dify-blue-inverted"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
onLanguagesChange([])
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{!hasSelected && (
|
||||||
|
<RiArrowDownSLine className="size-4 shrink-0 text-text-primary-on-surface" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</PortalToFollowElemTrigger>
|
||||||
|
<PortalToFollowElemContent className="z-[1000]">
|
||||||
|
<div className="w-[240px] rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur shadow-lg backdrop-blur-sm">
|
||||||
|
<div className="p-2 pb-1">
|
||||||
|
<Input
|
||||||
|
showLeftIcon
|
||||||
|
value={searchText}
|
||||||
|
onChange={e => setSearchText(e.target.value)}
|
||||||
|
placeholder={t('marketplace.searchFilterLanguage', { ns: 'plugin' })}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="max-h-[448px] overflow-y-auto p-1">
|
||||||
|
{filteredOptions.map(option => (
|
||||||
|
<div
|
||||||
|
key={option.value}
|
||||||
|
className="flex h-7 cursor-pointer select-none items-center rounded-lg px-2 py-1.5 hover:bg-state-base-hover"
|
||||||
|
onClick={() => handleCheck(option.value)}
|
||||||
|
>
|
||||||
|
<Checkbox
|
||||||
|
className="mr-1"
|
||||||
|
checked={languages.includes(option.value)}
|
||||||
|
/>
|
||||||
|
<div className="system-sm-medium px-1 text-text-secondary">
|
||||||
|
{option.nativeLabel}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</PortalToFollowElemContent>
|
||||||
|
</PortalToFollowElem>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default React.memo(HeroLanguagesFilter)
|
||||||
@ -1,10 +1,11 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
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, useFilterTemplateLanguages } from '../atoms'
|
||||||
import { CATEGORY_ALL, TEMPLATE_CATEGORY_MAP } from '../constants'
|
import { CATEGORY_ALL, TEMPLATE_CATEGORY_MAP } from '../constants'
|
||||||
import { useTemplateCategoryText } from './category-text'
|
import { useTemplateCategoryText } from './category-text'
|
||||||
import { CommonCategorySwitch } from './common'
|
import { CommonCategorySwitch } from './common'
|
||||||
|
import HeroLanguagesFilter from './hero-languages-filter'
|
||||||
|
|
||||||
type TemplateCategorySwitchProps = {
|
type TemplateCategorySwitchProps = {
|
||||||
className?: string
|
className?: string
|
||||||
@ -27,6 +28,7 @@ export const TemplateCategorySwitch = ({
|
|||||||
variant = 'default',
|
variant = 'default',
|
||||||
}: TemplateCategorySwitchProps) => {
|
}: TemplateCategorySwitchProps) => {
|
||||||
const [activeTemplateCategory, handleActiveTemplateCategoryChange] = useActiveTemplateCategory()
|
const [activeTemplateCategory, handleActiveTemplateCategoryChange] = useActiveTemplateCategory()
|
||||||
|
const [filterTemplateLanguages, setFilterTemplateLanguages] = useFilterTemplateLanguages()
|
||||||
const getTemplateCategoryText = useTemplateCategoryText()
|
const getTemplateCategoryText = useTemplateCategoryText()
|
||||||
|
|
||||||
const isHeroVariant = variant === 'hero'
|
const isHeroVariant = variant === 'hero'
|
||||||
@ -37,13 +39,34 @@ export const TemplateCategorySwitch = ({
|
|||||||
icon: value === CATEGORY_ALL && isHeroVariant ? <Playground className="mr-1.5 h-4 w-4" /> : null,
|
icon: value === CATEGORY_ALL && isHeroVariant ? <Playground className="mr-1.5 h-4 w-4" /> : null,
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
if (!isHeroVariant) {
|
||||||
|
return (
|
||||||
|
<CommonCategorySwitch
|
||||||
|
className={className}
|
||||||
|
variant={variant}
|
||||||
|
options={options}
|
||||||
|
activeValue={activeTemplateCategory}
|
||||||
|
onChange={handleActiveTemplateCategoryChange}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CommonCategorySwitch
|
<div className="flex shrink-0 items-center gap-2">
|
||||||
className={className}
|
<HeroLanguagesFilter
|
||||||
variant={variant}
|
languages={filterTemplateLanguages}
|
||||||
options={options}
|
onLanguagesChange={languages => setFilterTemplateLanguages(languages.length ? languages : null)}
|
||||||
activeValue={activeTemplateCategory}
|
/>
|
||||||
onChange={handleActiveTemplateCategoryChange}
|
<div className="text-text-primary-on-surface">
|
||||||
/>
|
·
|
||||||
|
</div>
|
||||||
|
<CommonCategorySwitch
|
||||||
|
className={className}
|
||||||
|
variant={variant}
|
||||||
|
options={options}
|
||||||
|
activeValue={activeTemplateCategory}
|
||||||
|
onChange={handleActiveTemplateCategoryChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -183,7 +183,9 @@ async function getDehydratedState(
|
|||||||
queryFn: () => getMarketplaceTemplateCollectionsAndTemplates(),
|
queryFn: () => getMarketplaceTemplateCollectionsAndTemplates(),
|
||||||
}))
|
}))
|
||||||
|
|
||||||
const isSearchMode = !!parsedSearchParams.q || category !== CATEGORY_ALL
|
const isSearchMode = !!parsedSearchParams.q
|
||||||
|
|| category !== CATEGORY_ALL
|
||||||
|
|| parsedSearchParams.languages.length > 0
|
||||||
|
|
||||||
if (isSearchMode) {
|
if (isSearchMode) {
|
||||||
const templatesParams: TemplateSearchParams = {
|
const templatesParams: TemplateSearchParams = {
|
||||||
@ -191,6 +193,7 @@ async function getDehydratedState(
|
|||||||
categories: category === CATEGORY_ALL ? undefined : [category],
|
categories: category === CATEGORY_ALL ? undefined : [category],
|
||||||
sort_by: DEFAULT_TEMPLATE_SORT.sortBy,
|
sort_by: DEFAULT_TEMPLATE_SORT.sortBy,
|
||||||
sort_order: DEFAULT_TEMPLATE_SORT.sortOrder,
|
sort_order: DEFAULT_TEMPLATE_SORT.sortOrder,
|
||||||
|
...(parsedSearchParams.languages.length > 0 ? { languages: parsedSearchParams.languages } : {}),
|
||||||
}
|
}
|
||||||
|
|
||||||
prefetches.push(queryClient.prefetchInfiniteQuery({
|
prefetches.push(queryClient.prefetchInfiniteQuery({
|
||||||
|
|||||||
@ -12,6 +12,7 @@ export type SearchTab = (typeof SEARCH_TABS)[number] | ''
|
|||||||
export const marketplaceSearchParamsParsers = {
|
export const marketplaceSearchParamsParsers = {
|
||||||
q: parseAsString.withDefault('').withOptions({ history: 'replace' }),
|
q: parseAsString.withDefault('').withOptions({ history: 'replace' }),
|
||||||
tags: parseAsArrayOf(parseAsString).withDefault([]).withOptions({ history: 'replace' }),
|
tags: parseAsArrayOf(parseAsString).withDefault([]).withOptions({ history: 'replace' }),
|
||||||
|
languages: parseAsArrayOf(parseAsString).withDefault([]).withOptions({ history: 'replace' }),
|
||||||
// Search-page-specific filters (independent from list-page category/tags)
|
// Search-page-specific filters (independent from list-page category/tags)
|
||||||
searchCategories: parseAsArrayOf(parseAsString).withDefault([]).withOptions({ history: 'replace' }),
|
searchCategories: parseAsArrayOf(parseAsString).withDefault([]).withOptions({ history: 'replace' }),
|
||||||
searchLanguages: parseAsArrayOf(parseAsString).withDefault([]).withOptions({ history: 'replace' }),
|
searchLanguages: parseAsArrayOf(parseAsString).withDefault([]).withOptions({ history: 'replace' }),
|
||||||
|
|||||||
@ -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, useMarketplacePluginSortValue, useMarketplaceSearchMode, useMarketplaceTemplateSortValue, useSearchText } from './atoms'
|
import { useActivePluginCategory, useActiveTemplateCategory, useCreationType, useFilterPluginTags, useFilterTemplateLanguages, 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'
|
||||||
@ -75,6 +75,7 @@ export function useTemplatesMarketplaceData(enabled = true) {
|
|||||||
const [searchTextOriginal] = useSearchText()
|
const [searchTextOriginal] = useSearchText()
|
||||||
const searchText = useDebounce(searchTextOriginal, { wait: 500 })
|
const searchText = useDebounce(searchTextOriginal, { wait: 500 })
|
||||||
const [activeTemplateCategory] = useActiveTemplateCategory()
|
const [activeTemplateCategory] = useActiveTemplateCategory()
|
||||||
|
const [filterTemplateLanguages] = useFilterTemplateLanguages()
|
||||||
|
|
||||||
// Template collections query (for non-search mode)
|
// Template collections query (for non-search mode)
|
||||||
const templateCollectionsQuery = useMarketplaceTemplateCollectionsAndTemplates(undefined, { enabled })
|
const templateCollectionsQuery = useMarketplaceTemplateCollectionsAndTemplates(undefined, { enabled })
|
||||||
@ -94,8 +95,9 @@ export function useTemplatesMarketplaceData(enabled = true) {
|
|||||||
categories: activeTemplateCategory === CATEGORY_ALL ? undefined : [activeTemplateCategory],
|
categories: activeTemplateCategory === CATEGORY_ALL ? undefined : [activeTemplateCategory],
|
||||||
sort_by: sort.sortBy,
|
sort_by: sort.sortBy,
|
||||||
sort_order: sort.sortOrder,
|
sort_order: sort.sortOrder,
|
||||||
|
...(filterTemplateLanguages.length > 0 ? { languages: filterTemplateLanguages } : {}),
|
||||||
}
|
}
|
||||||
}, [isSearchMode, searchText, activeTemplateCategory, sort])
|
}, [isSearchMode, searchText, activeTemplateCategory, sort, filterTemplateLanguages])
|
||||||
|
|
||||||
// Templates search query (for search mode)
|
// Templates search query (for search mode)
|
||||||
const templatesQuery = useMarketplaceTemplates(queryParams, { enabled })
|
const templatesQuery = useMarketplaceTemplates(queryParams, { enabled })
|
||||||
|
|||||||
@ -337,16 +337,17 @@ export const getMarketplaceTemplates = async (
|
|||||||
} = queryParams
|
} = queryParams
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
const body = {
|
||||||
|
page: pageParam,
|
||||||
|
page_size,
|
||||||
|
query,
|
||||||
|
sort_by,
|
||||||
|
sort_order,
|
||||||
|
...(categories ? { categories } : {}),
|
||||||
|
...(languages ? { languages } : {}),
|
||||||
|
}
|
||||||
const res = await marketplaceClient.templates.searchAdvanced({
|
const res = await marketplaceClient.templates.searchAdvanced({
|
||||||
body: {
|
body,
|
||||||
page: pageParam,
|
|
||||||
page_size,
|
|
||||||
query,
|
|
||||||
sort_by,
|
|
||||||
sort_order,
|
|
||||||
categories,
|
|
||||||
languages,
|
|
||||||
},
|
|
||||||
}, { signal })
|
}, { signal })
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user