feat: add description and tag filter

This commit is contained in:
Joel 2026-05-11 13:50:41 +08:00
parent 39b1062c20
commit 8e47e8dbf1
27 changed files with 77 additions and 43 deletions

View File

@ -58,7 +58,7 @@ describe('Category', () => {
renderComponent({ value: 'Unknown' })
const allCategoriesItem = screen.getByText('explore.apps.allCategories')
expect(allCategoriesItem.className).toContain('bg-components-main-nav-nav-button-bg-active')
expect(allCategoriesItem.className).toContain('bg-background-default')
})
it('should render raw category name when i18n key does not exist', () => {

View File

@ -201,6 +201,7 @@ describe('AppList', () => {
expect(screen.getByText('Alpha')).toBeInTheDocument()
expect(screen.getByText('Beta')).toBeInTheDocument()
expect(screen.getByText('explore.apps.description')).toBeInTheDocument()
})
it('should render continue work placeholders', () => {

View File

@ -3,7 +3,6 @@
import type { CreateAppModalProps } from '@/app/components/explore/create-app-modal'
import type { App } from '@/models/explore'
import type { TryAppSelection } from '@/types/try-app'
import { Button } from '@langgenius/dify-ui/button'
import { cn } from '@langgenius/dify-ui/cn'
import { useSuspenseQuery } from '@tanstack/react-query'
import { useDebounceFn } from 'ahooks'
@ -197,36 +196,35 @@ const Apps = ({
<LearnDify className="mt-4" />
<div className="sticky top-0 z-10 bg-background-body">
<div className={cn(
'flex items-center justify-between px-12 pt-6',
)}
>
<div className="flex items-center">
<div className="grow truncate system-xl-semibold text-text-primary">{!hasFilterCondition ? t('apps.title', { ns: 'explore' }) : t('apps.resultNum', { num: searchFilteredList.length, ns: 'explore' })}</div>
{hasFilterCondition && (
<>
<div className="mx-3 h-4 w-px bg-divider-regular"></div>
<Button size="medium" onClick={handleResetFilter}>{t('apps.resetFilter', { ns: 'explore' })}</Button>
</>
)}
<div className="px-12 pt-4">
<div className="flex min-w-0 flex-col gap-0.5">
<div className="flex min-w-0 items-center">
<div className="grow truncate system-xl-medium text-text-primary">{!hasFilterCondition ? t('apps.title', { ns: 'explore' }) : t('apps.resultNum', { num: searchFilteredList.length, ns: 'explore' })}</div>
</div>
<p className="truncate system-xs-regular text-text-tertiary">
{t('apps.description', { ns: 'explore' })}
</p>
</div>
<Input
showLeftIcon
showClearIcon
wrapperClassName="w-[200px] self-start"
value={keywords}
onChange={e => handleKeywordsChange(e.target.value)}
onClear={() => handleKeywordsChange('')}
/>
</div>
<div className="px-12 pt-2 pb-4">
<div className="flex items-end justify-between gap-4 px-12 pt-3 pb-3">
<Category
className="min-w-0"
list={categories}
value={currCategory}
onChange={setCurrCategory}
allCategoriesEn={allCategoriesEn}
/>
<div className="flex shrink-0 items-center gap-3">
<Input
showLeftIcon
showClearIcon
wrapperClassName="w-[200px] shrink-0"
value={keywords}
onChange={e => handleKeywordsChange(e.target.value)}
onClear={handleResetFilter}
/>
</div>
</div>
</div>

View File

@ -4,7 +4,6 @@ import type { AppCategory } from '@/models/explore'
import { cn } from '@langgenius/dify-ui/cn'
import * as React from 'react'
import { useTranslation } from 'react-i18next'
import { ThumbsUp } from '@/app/components/base/icons/src/vender/line/alertsAndFeedback'
import exploreI18n from '@/i18n/en-US/explore.json'
type ICategoryProps = {
@ -29,8 +28,8 @@ const Category: FC<ICategoryProps> = ({
const isAllCategories = !list.includes(value as AppCategory) || value === allCategoriesEn
const itemClassName = (isSelected: boolean) => cn(
'flex h-7 cursor-pointer items-center rounded-lg border border-transparent px-3 system-sm-medium text-text-tertiary hover:bg-components-main-nav-nav-button-bg-active',
isSelected && 'border-components-main-nav-nav-button-border bg-components-main-nav-nav-button-bg-active text-components-main-nav-nav-button-text-active shadow-xs',
'flex h-7 cursor-pointer items-center justify-center gap-0.5 rounded-lg border-[0.5px] border-transparent px-2 py-1 system-sm-medium text-text-secondary hover:bg-state-base-hover-alt',
isSelected && 'border-components-main-nav-nav-button-border bg-background-default text-text-accent-light-mode-only shadow-xs hover:bg-background-default',
)
const renderCategoryName = (name: AppCategory) => {
@ -39,23 +38,36 @@ const Category: FC<ICategoryProps> = ({
}
return (
<div className={cn(className, 'flex flex-wrap gap-1 text-[13px]')}>
<div
className={itemClassName(isAllCategories)}
onClick={() => onChange(allCategoriesEn)}
>
<ThumbsUp className="mr-1 h-3.5 w-3.5" />
{t('apps.allCategories', { ns: 'explore' })}
</div>
{list.filter(name => name !== allCategoriesEn).map(name => (
<div
key={name}
className={itemClassName(name === value)}
onClick={() => onChange(name)}
>
{renderCategoryName(name)}
</div>
))}
<div className={cn(className, 'inline-flex max-w-full flex-wrap items-center gap-px rounded-[10px] bg-components-input-bg-normal p-0.5 text-[13px]')}>
{[
{ name: allCategoriesEn, label: t('apps.allCategories', { ns: 'explore' }), isAll: true },
...list.filter(name => name !== allCategoriesEn).map(name => ({
name,
label: renderCategoryName(name),
isAll: false,
})),
].map((item, index, items) => {
const isSelected = item.isAll ? isAllCategories : item.name === value
const nextItem = items[index + 1]
const isNextSelected = nextItem
? nextItem.isAll ? isAllCategories : nextItem.name === value
: false
return (
<div key={item.isAll ? 'all' : item.name} className="relative flex items-center">
<div
className={itemClassName(isSelected)}
onClick={() => onChange(item.name)}
>
{item.isAll && <span className="i-custom-vender-line-alertsAndFeedback-thumbs-up h-4 w-4" />}
{item.label}
</div>
{!isSelected && !isNextSelected && index < items.length - 1 && (
<div className="absolute top-1/2 right-[-1px] h-3.5 w-px -translate-y-1/2 bg-divider-regular" />
)}
</div>
)
})}
</div>
)
}

View File

@ -5,6 +5,7 @@
"appCustomize.subTitle": "أيقونة التطبيق واسمه",
"appCustomize.title": "إنشاء تطبيق من {{name}}",
"apps.allCategories": "موصى به",
"apps.description": "قوالب جاهزة للاستخدام من المجتمع وفريق Dify.",
"apps.resetFilter": "مسح الفلتر",
"apps.resultNum": "{{num}} نتائج",
"apps.title": "قوالب",

View File

@ -5,6 +5,7 @@
"appCustomize.subTitle": "App-Symbol & Name",
"appCustomize.title": "App aus {{name}} erstellen",
"apps.allCategories": "Alle",
"apps.description": "Sofort nutzbare Vorlagen aus der Community und vom Dify-Team.",
"apps.resetFilter": "Filter löschen",
"apps.resultNum": "{{num}} Ergebnisse",
"apps.title": "Vorlagen",

View File

@ -5,6 +5,7 @@
"appCustomize.subTitle": "App icon & name",
"appCustomize.title": "Create app from {{name}}",
"apps.allCategories": "All",
"apps.description": "Ready-to-use templates from the community and Dify team.",
"apps.resetFilter": "Clear filter",
"apps.resultNum": "{{num}} results",
"apps.title": "Templates",

View File

@ -5,6 +5,7 @@
"appCustomize.subTitle": "Icono y nombre de la aplicación",
"appCustomize.title": "Crear aplicación a partir de {{name}}",
"apps.allCategories": "Todos",
"apps.description": "Plantillas listas para usar de la comunidad y el equipo de Dify.",
"apps.resetFilter": "Limpiar filtro",
"apps.resultNum": "{{num}} resultados",
"apps.title": "Plantillas",

View File

@ -5,6 +5,7 @@
"appCustomize.subTitle": "آیکون و نام برنامه",
"appCustomize.title": "ایجاد برنامه از {{name}}",
"apps.allCategories": "همه",
"apps.description": "الگوهای آماده استفاده از جامعه و تیم Dify.",
"apps.resetFilter": "پاک کردن فیلتر",
"apps.resultNum": "{{num}} نتیجه",
"apps.title": "الگوها",

View File

@ -5,6 +5,7 @@
"appCustomize.subTitle": "Icône de l'application & nom",
"appCustomize.title": "Créer une application à partir de {{name}}",
"apps.allCategories": "Tous",
"apps.description": "Des modèles prêts à l'emploi créés par la communauté et l'équipe Dify.",
"apps.resetFilter": "Effacer le filtre",
"apps.resultNum": "{{num}} résultats",
"apps.title": "Modèles",

View File

@ -5,6 +5,7 @@
"appCustomize.subTitle": "ऐप आइकन और नाम",
"appCustomize.title": "{{name}} से ऐप बनाएँ",
"apps.allCategories": "सभी",
"apps.description": "समुदाय और Dify टीम के उपयोग के लिए तैयार टेम्पलेट।",
"apps.resetFilter": "फ़िल्टर साफ़ करें",
"apps.resultNum": "{{num}} परिणाम",
"apps.title": "टेम्पलेट",

View File

@ -5,6 +5,7 @@
"appCustomize.subTitle": "Ikon & nama aplikasi",
"appCustomize.title": "Buat aplikasi dari {{name}}",
"apps.allCategories": "Semua",
"apps.description": "Templat siap pakai dari komunitas dan tim Dify.",
"apps.resetFilter": "Hapus filter",
"apps.resultNum": "{{num}} hasil",
"apps.title": "Templat",

View File

@ -5,6 +5,7 @@
"appCustomize.subTitle": "Icona & nome dell'app",
"appCustomize.title": "Crea app da {{name}}",
"apps.allCategories": "Tutti",
"apps.description": "Modelli pronti all'uso dalla community e dal team Dify.",
"apps.resetFilter": "Cancella filtro",
"apps.resultNum": "{{num}} risultati",
"apps.title": "Modelli",

View File

@ -5,6 +5,7 @@
"appCustomize.subTitle": "アプリアイコンと名前",
"appCustomize.title": "{{name}}からアプリを作成",
"apps.allCategories": "全て",
"apps.description": "コミュニティと Dify チームによる、すぐに使えるテンプレート。",
"apps.resetFilter": "クリア",
"apps.resultNum": "{{num}}件の結果",
"apps.title": "テンプレート",

View File

@ -5,6 +5,7 @@
"appCustomize.subTitle": "앱 아이콘 및 이름",
"appCustomize.title": "{{name}}으로 앱 만들기",
"apps.allCategories": "전체",
"apps.description": "커뮤니티와 Dify 팀이 만든 바로 사용할 수 있는 템플릿입니다.",
"apps.resetFilter": "필터 지우기",
"apps.resultNum": "{{num}}개 결과",
"apps.title": "템플릿",

View File

@ -5,6 +5,7 @@
"appCustomize.subTitle": "App icon & name",
"appCustomize.title": "Create app from {{name}}",
"apps.allCategories": "All",
"apps.description": "Gebruiksklare sjablonen van de community en het Dify-team.",
"apps.resetFilter": "Clear filter",
"apps.resultNum": "{{num}} results",
"apps.title": "Sjablonen",

View File

@ -5,6 +5,7 @@
"appCustomize.subTitle": "Ikona i nazwa aplikacji",
"appCustomize.title": "Utwórz aplikację z {{name}}",
"apps.allCategories": "Wszystkie",
"apps.description": "Gotowe do użycia szablony od społeczności i zespołu Dify.",
"apps.resetFilter": "Wyczyść filtr",
"apps.resultNum": "{{num}} wyników",
"apps.title": "Szablony",

View File

@ -5,6 +5,7 @@
"appCustomize.subTitle": "Ícone e nome do aplicativo",
"appCustomize.title": "Criar aplicativo a partir de {{name}}",
"apps.allCategories": "Todos",
"apps.description": "Modelos prontos para uso da comunidade e da equipe Dify.",
"apps.resetFilter": "Limpar filtro",
"apps.resultNum": "{{num}} resultados",
"apps.title": "Modelos",

View File

@ -5,6 +5,7 @@
"appCustomize.subTitle": "Pictogramă și nume aplicație",
"appCustomize.title": "Creați o aplicație din {{name}}",
"apps.allCategories": "Toate",
"apps.description": "Șabloane gata de utilizare de la comunitate și echipa Dify.",
"apps.resetFilter": "Șterge filtrul",
"apps.resultNum": "{{num}} rezultate",
"apps.title": "Șabloane",

View File

@ -5,6 +5,7 @@
"appCustomize.subTitle": "Значок и название приложения",
"appCustomize.title": "Создать приложение из {{name}}",
"apps.allCategories": "Все",
"apps.description": "Готовые к использованию шаблоны от сообщества и команды Dify.",
"apps.resetFilter": "Очистить фильтр",
"apps.resultNum": "{{num}} результатов",
"apps.title": "Шаблоны",

View File

@ -5,6 +5,7 @@
"appCustomize.subTitle": "Ikona aplikacije & ime",
"appCustomize.title": "Ustvari aplikacijo iz {{name}}",
"apps.allCategories": "Vse",
"apps.description": "Predloge, pripravljene za uporabo, iz skupnosti in ekipe Dify.",
"apps.resetFilter": "Počisti filter",
"apps.resultNum": "{{num}} rezultatov",
"apps.title": "Predloge",

View File

@ -5,6 +5,7 @@
"appCustomize.subTitle": "ไอคอนและชื่อแอป",
"appCustomize.title": "สร้างแอปจาก {{name}}",
"apps.allCategories": "ทั้งหมด",
"apps.description": "เทมเพลตพร้อมใช้งานจากชุมชนและทีม Dify",
"apps.resetFilter": "ล้างตัวกรอง",
"apps.resultNum": "{{num}} ผลลัพธ์",
"apps.title": "เทมเพลต",

View File

@ -5,6 +5,7 @@
"appCustomize.subTitle": "Uygulama simgesi ve ismi",
"appCustomize.title": "{{name}} uygulamasından uygulama oluştur",
"apps.allCategories": "Tümü",
"apps.description": "Topluluk ve Dify ekibinden kullanıma hazır şablonlar.",
"apps.resetFilter": "Filtreyi temizle",
"apps.resultNum": "{{num}} sonuç",
"apps.title": "Şablonlar",

View File

@ -5,6 +5,7 @@
"appCustomize.subTitle": "Значок програми та назва",
"appCustomize.title": "Створити додаток з {{name}}",
"apps.allCategories": "Всі",
"apps.description": "Готові до використання шаблони від спільноти та команди Dify.",
"apps.resetFilter": "Очистити фільтр",
"apps.resultNum": "{{num}} результатів",
"apps.title": "Шаблони",

View File

@ -5,6 +5,7 @@
"appCustomize.subTitle": "Biểu tượng và tên ứng dụng",
"appCustomize.title": "Tạo ứng dụng từ {{name}}",
"apps.allCategories": "Tất cả",
"apps.description": "Các mẫu sẵn sàng sử dụng từ cộng đồng và đội ngũ Dify.",
"apps.resetFilter": "Xóa bộ lọc",
"apps.resultNum": "{{num}} kết quả",
"apps.title": "Mẫu",

View File

@ -5,6 +5,7 @@
"appCustomize.subTitle": "应用程序图标和名称",
"appCustomize.title": "从 {{name}} 创建应用程序",
"apps.allCategories": "所有",
"apps.description": "来自社区和 Dify 团队的开箱即用模板。",
"apps.resetFilter": "清除筛选",
"apps.resultNum": "{{num}} 个结果",
"apps.title": "模板",

View File

@ -5,6 +5,7 @@
"appCustomize.subTitle": "應用程式圖示和名稱",
"appCustomize.title": "從 {{name}} 建立應用程式",
"apps.allCategories": "全部",
"apps.description": "來自社群和 Dify 團隊的即用範本。",
"apps.resetFilter": "清除篩選",
"apps.resultNum": "{{num}} 個結果",
"apps.title": "範本",