mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-09-08 22:21:23 +00:00
feat: add "Installed" filter to Roo Marketplace (#7007)
* feat: add "Installed" filter to Roo Marketplace - Added installed boolean filter to ViewState interface - Implemented UI checkbox for "Show installed only" filter - Updated filterItems method to filter by installation status - Added translation strings for the new filter - Updated tests to include the new filter property Fixes #7004 * fix: add missing translations for "Installed" filter in marketplace - Added "filters.installed.label" and "filters.installed.description" keys - Updated all backend marketplace.json files (src/i18n/locales/*/marketplace.json) - Updated all frontend marketplace.json files (webview-ui/src/i18n/locales/*/marketplace.json) - All translation checks now pass successfully * fix: improve code quality for installed filter feature - Replace plain HTML checkbox with UI library Checkbox component for consistency - Add proper TypeScript typing for installedMetadata (MarketplaceInstalledMetadata) - Optimize filterItems method to avoid unnecessary object copying - Remove dead code for subcomponent filtering * refactor: improve installed filter UI and functionality - Replace checkbox with dropdown for better UX - Place dropdown inline with search field for better layout - Add three filter options: All Items, Installed Only, Not Installed - Update filter logic to handle all three states - Improve translation labels to clarify it's a filter - Optimize filterItems method for better performance - Add proper TypeScript types throughout * fix: ensure both filter dropdowns have matching height - Set both Select and Button components to h-7 for visual consistency - Installation status dropdown and tags dropdown now have identical height - Improved visual alignment for better UX * chore: remove redundant useEffect in MarketplaceListView to prevent re-render loop; rely on state manager handleMessage sync * i18n: update tri-state Installed filter translations across all locales - Replace checkbox-based installed keys with dropdown keys: { label, all, installed, notInstalled } - Updated both frontend and backend marketplace.json files for all non-English locales - Ensures translation completeness for CI check * i18n: change Installed option text to 'Installed' (remove 'Only') across all locales (frontend + backend) --------- Co-authored-by: Roo Code <roomote@roocode.com> Co-authored-by: daniel-lxs <ricciodaniel98@gmail.com>
This commit is contained in:
parent
ce052dbd7e
commit
007b58db77
41 changed files with 438 additions and 145 deletions
6
src/i18n/locales/ca/marketplace.json
generated
6
src/i18n/locales/ca/marketplace.json
generated
|
|
@ -38,6 +38,12 @@
|
|||
"noResults": "No s'han trobat etiquetes.",
|
||||
"selected": "Mostrant elements amb qualsevol de les etiquetes seleccionades"
|
||||
},
|
||||
"installed": {
|
||||
"label": "Filtra per estat",
|
||||
"all": "Tots els articles",
|
||||
"installed": "Instal·lats",
|
||||
"notInstalled": "No instal·lats"
|
||||
},
|
||||
"title": "Marketplace"
|
||||
},
|
||||
"done": "Fet",
|
||||
|
|
|
|||
6
src/i18n/locales/de/marketplace.json
generated
6
src/i18n/locales/de/marketplace.json
generated
|
|
@ -38,6 +38,12 @@
|
|||
"noResults": "Keine Tags gefunden.",
|
||||
"selected": "Zeige Elemente mit einem der ausgewählten Tags"
|
||||
},
|
||||
"installed": {
|
||||
"label": "Nach Status filtern",
|
||||
"all": "Alle Artikel",
|
||||
"installed": "Installierte",
|
||||
"notInstalled": "Nicht installiert"
|
||||
},
|
||||
"title": "Marketplace"
|
||||
},
|
||||
"done": "Fertig",
|
||||
|
|
|
|||
|
|
@ -38,6 +38,12 @@
|
|||
"noResults": "No tags found.",
|
||||
"selected": "Showing items with any of the selected tags"
|
||||
},
|
||||
"installed": {
|
||||
"label": "Filter by status",
|
||||
"all": "All Items",
|
||||
"installed": "Installed",
|
||||
"notInstalled": "Not Installed"
|
||||
},
|
||||
"title": "Marketplace"
|
||||
},
|
||||
"done": "Done",
|
||||
|
|
|
|||
6
src/i18n/locales/es/marketplace.json
generated
6
src/i18n/locales/es/marketplace.json
generated
|
|
@ -38,6 +38,12 @@
|
|||
"noResults": "No se encontraron etiquetas.",
|
||||
"selected": "Mostrando elementos con cualquiera de las etiquetas seleccionadas"
|
||||
},
|
||||
"installed": {
|
||||
"label": "Filtrar por estado",
|
||||
"all": "Todos los artículos",
|
||||
"installed": "Instalados",
|
||||
"notInstalled": "No instalados"
|
||||
},
|
||||
"title": "Marketplace"
|
||||
},
|
||||
"done": "Hecho",
|
||||
|
|
|
|||
6
src/i18n/locales/fr/marketplace.json
generated
6
src/i18n/locales/fr/marketplace.json
generated
|
|
@ -38,6 +38,12 @@
|
|||
"noResults": "Aucune étiquette trouvée.",
|
||||
"selected": "Affichage des éléments avec l'une des étiquettes sélectionnées"
|
||||
},
|
||||
"installed": {
|
||||
"label": "Filtrer par statut",
|
||||
"all": "Tous les articles",
|
||||
"installed": "Installés",
|
||||
"notInstalled": "Non installés"
|
||||
},
|
||||
"title": "Marketplace"
|
||||
},
|
||||
"done": "Terminé",
|
||||
|
|
|
|||
6
src/i18n/locales/hi/marketplace.json
generated
6
src/i18n/locales/hi/marketplace.json
generated
|
|
@ -38,6 +38,12 @@
|
|||
"noResults": "कोई टैग नहीं मिले।",
|
||||
"selected": "चयनित टैग्स में से किसी भी के साथ आइटम दिखा रहे हैं"
|
||||
},
|
||||
"installed": {
|
||||
"label": "स्थिति के अनुसार फ़िल्टर करें",
|
||||
"all": "सभी आइटम",
|
||||
"installed": "स्थापित",
|
||||
"notInstalled": "स्थापित नहीं"
|
||||
},
|
||||
"title": "मार्केटप्लेस"
|
||||
},
|
||||
"done": "हो गया",
|
||||
|
|
|
|||
6
src/i18n/locales/id/marketplace.json
generated
6
src/i18n/locales/id/marketplace.json
generated
|
|
@ -38,6 +38,12 @@
|
|||
"noResults": "Tidak ada tag ditemukan.",
|
||||
"selected": "Menampilkan item dengan salah satu tag yang dipilih"
|
||||
},
|
||||
"installed": {
|
||||
"label": "Filter berdasarkan status",
|
||||
"all": "Semua Item",
|
||||
"installed": "Terpasang",
|
||||
"notInstalled": "Tidak Terpasang"
|
||||
},
|
||||
"title": "Marketplace"
|
||||
},
|
||||
"done": "Selesai",
|
||||
|
|
|
|||
6
src/i18n/locales/it/marketplace.json
generated
6
src/i18n/locales/it/marketplace.json
generated
|
|
@ -38,6 +38,12 @@
|
|||
"noResults": "Nessun tag trovato.",
|
||||
"selected": "Mostrando elementi con uno qualsiasi dei tag selezionati"
|
||||
},
|
||||
"installed": {
|
||||
"label": "Filtra per stato",
|
||||
"all": "Tutti gli articoli",
|
||||
"installed": "Installati",
|
||||
"notInstalled": "Non installati"
|
||||
},
|
||||
"title": "Marketplace"
|
||||
},
|
||||
"done": "Fatto",
|
||||
|
|
|
|||
6
src/i18n/locales/ja/marketplace.json
generated
6
src/i18n/locales/ja/marketplace.json
generated
|
|
@ -38,6 +38,12 @@
|
|||
"noResults": "タグが見つかりません。",
|
||||
"selected": "選択されたタグのいずれかを持つアイテムを表示"
|
||||
},
|
||||
"installed": {
|
||||
"label": "ステータスで絞り込む",
|
||||
"all": "すべてのアイテム",
|
||||
"installed": "インストール済み",
|
||||
"notInstalled": "未インストール"
|
||||
},
|
||||
"title": "マーケットプレイス"
|
||||
},
|
||||
"done": "完了",
|
||||
|
|
|
|||
6
src/i18n/locales/ko/marketplace.json
generated
6
src/i18n/locales/ko/marketplace.json
generated
|
|
@ -38,6 +38,12 @@
|
|||
"noResults": "태그를 찾을 수 없습니다.",
|
||||
"selected": "선택된 태그 중 하나를 가진 항목 표시"
|
||||
},
|
||||
"installed": {
|
||||
"label": "상태별로 필터링",
|
||||
"all": "모든 항목",
|
||||
"installed": "설치됨",
|
||||
"notInstalled": "설치되지 않음"
|
||||
},
|
||||
"title": "마켓플레이스"
|
||||
},
|
||||
"done": "완료",
|
||||
|
|
|
|||
6
src/i18n/locales/nl/marketplace.json
generated
6
src/i18n/locales/nl/marketplace.json
generated
|
|
@ -38,6 +38,12 @@
|
|||
"noResults": "Geen tags gevonden.",
|
||||
"selected": "Items tonen met een van de geselecteerde tags"
|
||||
},
|
||||
"installed": {
|
||||
"label": "Filteren op status",
|
||||
"all": "Alle items",
|
||||
"installed": "Geïnstalleerd",
|
||||
"notInstalled": "Niet geïnstalleerd"
|
||||
},
|
||||
"title": "Marketplace"
|
||||
},
|
||||
"done": "Klaar",
|
||||
|
|
|
|||
6
src/i18n/locales/pl/marketplace.json
generated
6
src/i18n/locales/pl/marketplace.json
generated
|
|
@ -38,6 +38,12 @@
|
|||
"noResults": "Nie znaleziono tagów.",
|
||||
"selected": "Pokazywanie elementów z dowolnym z wybranych tagów"
|
||||
},
|
||||
"installed": {
|
||||
"label": "Filtruj według statusu",
|
||||
"all": "Wszystkie elementy",
|
||||
"installed": "Zainstalowane",
|
||||
"notInstalled": "Niezainstalowane"
|
||||
},
|
||||
"title": "Marketplace"
|
||||
},
|
||||
"done": "Gotowe",
|
||||
|
|
|
|||
6
src/i18n/locales/pt-BR/marketplace.json
generated
6
src/i18n/locales/pt-BR/marketplace.json
generated
|
|
@ -38,6 +38,12 @@
|
|||
"noResults": "Nenhuma tag encontrada.",
|
||||
"selected": "Mostrando itens com qualquer uma das tags selecionadas"
|
||||
},
|
||||
"installed": {
|
||||
"label": "Filtrar por status",
|
||||
"all": "Todos os itens",
|
||||
"installed": "Instalados",
|
||||
"notInstalled": "Não instalados"
|
||||
},
|
||||
"title": "Marketplace"
|
||||
},
|
||||
"done": "Concluído",
|
||||
|
|
|
|||
6
src/i18n/locales/ru/marketplace.json
generated
6
src/i18n/locales/ru/marketplace.json
generated
|
|
@ -38,6 +38,12 @@
|
|||
"noResults": "Теги не найдены.",
|
||||
"selected": "Показ элементов с любым из выбранных тегов"
|
||||
},
|
||||
"installed": {
|
||||
"label": "Фильтр по статусу",
|
||||
"all": "Все элементы",
|
||||
"installed": "Установленные",
|
||||
"notInstalled": "Не установленные"
|
||||
},
|
||||
"title": "Marketplace"
|
||||
},
|
||||
"done": "Готово",
|
||||
|
|
|
|||
6
src/i18n/locales/tr/marketplace.json
generated
6
src/i18n/locales/tr/marketplace.json
generated
|
|
@ -38,6 +38,12 @@
|
|||
"noResults": "Etiket bulunamadı.",
|
||||
"selected": "Seçilen etiketlerden herhangi birine sahip öğeleri göster"
|
||||
},
|
||||
"installed": {
|
||||
"label": "Duruma göre filtrele",
|
||||
"all": "Tüm Öğeler",
|
||||
"installed": "Yüklü",
|
||||
"notInstalled": "Yüklü Değil"
|
||||
},
|
||||
"title": "Marketplace"
|
||||
},
|
||||
"done": "Tamam",
|
||||
|
|
|
|||
6
src/i18n/locales/vi/marketplace.json
generated
6
src/i18n/locales/vi/marketplace.json
generated
|
|
@ -38,6 +38,12 @@
|
|||
"noResults": "Không tìm thấy thẻ nào.",
|
||||
"selected": "Hiển thị các mục có bất kỳ thẻ nào được chọn"
|
||||
},
|
||||
"installed": {
|
||||
"label": "Lọc theo trạng thái",
|
||||
"all": "Tất cả các mục",
|
||||
"installed": "Đã cài đặt",
|
||||
"notInstalled": "Chưa cài đặt"
|
||||
},
|
||||
"title": "Marketplace"
|
||||
},
|
||||
"done": "Hoàn thành",
|
||||
|
|
|
|||
6
src/i18n/locales/zh-CN/marketplace.json
generated
6
src/i18n/locales/zh-CN/marketplace.json
generated
|
|
@ -38,6 +38,12 @@
|
|||
"noResults": "未找到标签。",
|
||||
"selected": "显示包含任一选中标签的项目"
|
||||
},
|
||||
"installed": {
|
||||
"label": "按状态筛选",
|
||||
"all": "所有项目",
|
||||
"installed": "已安装",
|
||||
"notInstalled": "未安装"
|
||||
},
|
||||
"title": "Marketplace"
|
||||
},
|
||||
"done": "完成",
|
||||
|
|
|
|||
6
src/i18n/locales/zh-TW/marketplace.json
generated
6
src/i18n/locales/zh-TW/marketplace.json
generated
|
|
@ -38,6 +38,12 @@
|
|||
"noResults": "找不到標籤。",
|
||||
"selected": "顯示包含任一選取標籤的項目"
|
||||
},
|
||||
"installed": {
|
||||
"label": "按狀態篩選",
|
||||
"all": "所有項目",
|
||||
"installed": "已安裝",
|
||||
"notInstalled": "未安裝"
|
||||
},
|
||||
"title": "Marketplace"
|
||||
},
|
||||
"done": "完成",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import * as React from "react"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"
|
||||
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "@/components/ui/command"
|
||||
import { X, ChevronsUpDown } from "lucide-react"
|
||||
|
|
@ -27,6 +28,9 @@ export function MarketplaceListView({ stateManager, allTags, filteredTags, filte
|
|||
const allItems = state.displayItems || []
|
||||
const organizationMcps = state.displayOrganizationMcps || []
|
||||
|
||||
// NOTE: installed metadata is already synchronized into the state manager via handleMessage("state"/"marketplaceData")
|
||||
// in MarketplaceViewStateManager; avoid dispatching UPDATE_FILTERS here to prevent render loops.
|
||||
|
||||
// Filter items by type if specified
|
||||
const items = filterByType ? allItems.filter((item) => item.type === filterByType) : allItems
|
||||
const orgMcps = filterByType === "mcp" ? organizationMcps : []
|
||||
|
|
@ -55,115 +59,132 @@ export function MarketplaceListView({ stateManager, allTags, filteredTags, filte
|
|||
}
|
||||
/>
|
||||
</div>
|
||||
{allTags.length > 0 && (
|
||||
<div className="mt-2">
|
||||
<div className="flex items-center justify-between mb-1">
|
||||
<div className="flex items-center gap-1">
|
||||
<label className="font-medium text-sm">{t("marketplace:filters.tags.label")}</label>
|
||||
</div>
|
||||
{state.filters.tags.length > 0 && (
|
||||
<Button
|
||||
className="shadow-none font-normal flex items-center gap-1 h-auto py-0.5 px-1.5 text-xs"
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation() // Prevent popover from closing if it's open
|
||||
manager.transition({
|
||||
type: "UPDATE_FILTERS",
|
||||
payload: { filters: { tags: [] } },
|
||||
})
|
||||
}}>
|
||||
<span className="codicon codicon-close"></span>
|
||||
{t("marketplace:filters.tags.clear")}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Popover open={isTagPopoverOpen} onOpenChange={(open) => setIsTagPopoverOpen(open)}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="combobox"
|
||||
role="combobox"
|
||||
aria-expanded={isTagPopoverOpen}
|
||||
className="w-full justify-between h-7">
|
||||
<span className="truncate">
|
||||
{state.filters.tags.length > 0
|
||||
? state.filters.tags
|
||||
.map((t: string) => t.charAt(0).toUpperCase() + t.slice(1))
|
||||
.join(", ")
|
||||
: t("marketplace:filters.none")}
|
||||
</span>
|
||||
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent
|
||||
className="w-[var(--radix-popover-trigger-width)] p-0"
|
||||
onClick={(e) => e.stopPropagation()}>
|
||||
<Command>
|
||||
<div className="relative">
|
||||
<CommandInput
|
||||
className="h-9 pr-8"
|
||||
placeholder={t("marketplace:filters.tags.placeholder")}
|
||||
value={tagSearch}
|
||||
onValueChange={setTagSearch}
|
||||
/>
|
||||
{tagSearch && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="absolute right-1 top-1/2 transform -translate-y-1/2 h-7 w-7"
|
||||
onClick={() => setTagSearch("")}>
|
||||
<X className="h-4 w-4" />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
<CommandList className="max-h-[200px] overflow-y-auto bg-vscode-dropdown-background divide-y divide-vscode-panel-border">
|
||||
<CommandEmpty className="p-2 text-sm text-vscode-descriptionForeground">
|
||||
{t("marketplace:filters.tags.noResults")}
|
||||
</CommandEmpty>
|
||||
<CommandGroup>
|
||||
{filteredTags.map((tag: string) => (
|
||||
<CommandItem
|
||||
key={tag}
|
||||
value={tag}
|
||||
onSelect={() => {
|
||||
const isSelected = state.filters.tags.includes(tag)
|
||||
manager.transition({
|
||||
type: "UPDATE_FILTERS",
|
||||
payload: {
|
||||
filters: {
|
||||
tags: isSelected
|
||||
? state.filters.tags.filter((t) => t !== tag)
|
||||
: [...state.filters.tags, tag],
|
||||
<div className="mt-2 flex gap-2">
|
||||
<Select
|
||||
value={state.filters.installed}
|
||||
onValueChange={(value: "all" | "installed" | "not_installed") =>
|
||||
manager.transition({
|
||||
type: "UPDATE_FILTERS",
|
||||
payload: { filters: { installed: value } },
|
||||
})
|
||||
}>
|
||||
<SelectTrigger className="flex-1 h-7">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">{t("marketplace:filters.installed.all")}</SelectItem>
|
||||
<SelectItem value="installed">{t("marketplace:filters.installed.installed")}</SelectItem>
|
||||
<SelectItem value="not_installed">
|
||||
{t("marketplace:filters.installed.notInstalled")}
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
{allTags.length > 0 && (
|
||||
<div className="flex-1">
|
||||
<Popover open={isTagPopoverOpen} onOpenChange={(open) => setIsTagPopoverOpen(open)}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="combobox"
|
||||
role="combobox"
|
||||
aria-expanded={isTagPopoverOpen}
|
||||
className="w-full justify-between h-7">
|
||||
<span className="truncate">
|
||||
{state.filters.tags.length > 0
|
||||
? state.filters.tags
|
||||
.map((t: string) => t.charAt(0).toUpperCase() + t.slice(1))
|
||||
.join(", ")
|
||||
: t("marketplace:filters.tags.label")}
|
||||
</span>
|
||||
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent
|
||||
className="w-[var(--radix-popover-trigger-width)] p-0"
|
||||
onClick={(e) => e.stopPropagation()}>
|
||||
<Command>
|
||||
<div className="relative">
|
||||
<CommandInput
|
||||
className="h-9 pr-8"
|
||||
placeholder={t("marketplace:filters.tags.placeholder")}
|
||||
value={tagSearch}
|
||||
onValueChange={setTagSearch}
|
||||
/>
|
||||
{tagSearch && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="absolute right-1 top-1/2 transform -translate-y-1/2 h-7 w-7"
|
||||
onClick={() => setTagSearch("")}>
|
||||
<X className="h-4 w-4" />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
<CommandList className="max-h-[200px] overflow-y-auto bg-vscode-dropdown-background divide-y divide-vscode-panel-border">
|
||||
<CommandEmpty className="p-2 text-sm text-vscode-descriptionForeground">
|
||||
{t("marketplace:filters.tags.noResults")}
|
||||
</CommandEmpty>
|
||||
<CommandGroup>
|
||||
{filteredTags.map((tag: string) => (
|
||||
<CommandItem
|
||||
key={tag}
|
||||
value={tag}
|
||||
onSelect={() => {
|
||||
const isSelected = state.filters.tags.includes(tag)
|
||||
manager.transition({
|
||||
type: "UPDATE_FILTERS",
|
||||
payload: {
|
||||
filters: {
|
||||
tags: isSelected
|
||||
? state.filters.tags.filter(
|
||||
(t) => t !== tag,
|
||||
)
|
||||
: [...state.filters.tags, tag],
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
}}
|
||||
data-selected={state.filters.tags.includes(tag)}
|
||||
className="grid grid-cols-[1rem_1fr] gap-2 cursor-pointer text-sm capitalize"
|
||||
onMouseDown={(e) => {
|
||||
e.stopPropagation()
|
||||
e.preventDefault()
|
||||
}}>
|
||||
{state.filters.tags.includes(tag) ? (
|
||||
<span className="codicon codicon-check" />
|
||||
) : (
|
||||
<span />
|
||||
)}
|
||||
{tag}
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
{state.filters.tags.length > 0 && (
|
||||
<div className="text-xs text-vscode-descriptionForeground mt-2 flex items-center min-h-[16px]">
|
||||
<span className="codicon codicon-tag mr-1"></span>
|
||||
{t("marketplace:filters.tags.selected")}
|
||||
</div>
|
||||
)}
|
||||
})
|
||||
}}
|
||||
data-selected={state.filters.tags.includes(tag)}
|
||||
className="grid grid-cols-[1rem_1fr] gap-2 cursor-pointer text-sm capitalize"
|
||||
onMouseDown={(e) => {
|
||||
e.stopPropagation()
|
||||
e.preventDefault()
|
||||
}}>
|
||||
{state.filters.tags.includes(tag) ? (
|
||||
<span className="codicon codicon-check" />
|
||||
) : (
|
||||
<span />
|
||||
)}
|
||||
{tag}
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{state.filters.tags.length > 0 && (
|
||||
<div className="text-xs text-vscode-descriptionForeground mt-2 flex items-center justify-between">
|
||||
<div className="flex items-center">
|
||||
<span className="codicon codicon-tag mr-1"></span>
|
||||
{t("marketplace:filters.tags.selected")}
|
||||
</div>
|
||||
<Button
|
||||
className="shadow-none font-normal flex items-center gap-1 h-auto py-0.5 px-1.5 text-xs"
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
manager.transition({
|
||||
type: "UPDATE_FILTERS",
|
||||
payload: { filters: { tags: [] } },
|
||||
})
|
||||
}}>
|
||||
<span className="codicon codicon-close"></span>
|
||||
{t("marketplace:filters.tags.clear")}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
|
@ -187,7 +208,7 @@ export function MarketplaceListView({ stateManager, allTags, filteredTags, filte
|
|||
onClick={() =>
|
||||
manager.transition({
|
||||
type: "UPDATE_FILTERS",
|
||||
payload: { filters: { search: "", type: "", tags: [] } },
|
||||
payload: { filters: { search: "", type: "", tags: [], installed: "all" } },
|
||||
})
|
||||
}
|
||||
className="mt-4 bg-vscode-button-secondaryBackground text-vscode-button-secondaryForeground hover:bg-vscode-button-secondaryHoverBackground transition-colors">
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@
|
|||
import { MarketplaceItem } from "@roo-code/types"
|
||||
import { vscode } from "../../utils/vscode"
|
||||
import { WebviewMessage } from "../../../../src/shared/WebviewMessage"
|
||||
import type { MarketplaceInstalledMetadata } from "../../../../src/shared/ExtensionMessage"
|
||||
|
||||
export interface ViewState {
|
||||
allItems: MarketplaceItem[]
|
||||
|
|
@ -26,7 +27,9 @@ export interface ViewState {
|
|||
type: string
|
||||
search: string
|
||||
tags: string[]
|
||||
installed: "all" | "installed" | "not_installed" // Filter by installation status
|
||||
}
|
||||
installedMetadata?: MarketplaceInstalledMetadata // Store installed metadata for filtering
|
||||
}
|
||||
|
||||
type TransitionPayloads = {
|
||||
|
|
@ -65,6 +68,7 @@ export class MarketplaceViewStateManager {
|
|||
type: "",
|
||||
search: "",
|
||||
tags: [],
|
||||
installed: "all",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
@ -189,8 +193,11 @@ export class MarketplaceViewStateManager {
|
|||
let newDisplayItems: MarketplaceItem[]
|
||||
let newDisplayOrganizationMcps: MarketplaceItem[]
|
||||
if (this.isFilterActive()) {
|
||||
newDisplayItems = this.filterItems([...items])
|
||||
newDisplayOrganizationMcps = this.filterItems([...this.state.organizationMcps])
|
||||
newDisplayItems = this.filterItems([...items], this.state.installedMetadata)
|
||||
newDisplayOrganizationMcps = this.filterItems(
|
||||
[...this.state.organizationMcps],
|
||||
this.state.installedMetadata,
|
||||
)
|
||||
} else {
|
||||
// No filters active - show all items
|
||||
newDisplayItems = [...items]
|
||||
|
|
@ -251,6 +258,7 @@ export class MarketplaceViewStateManager {
|
|||
type: filters.type !== undefined ? filters.type : this.state.filters.type,
|
||||
search: filters.search !== undefined ? filters.search : this.state.filters.search,
|
||||
tags: filters.tags !== undefined ? filters.tags : this.state.filters.tags,
|
||||
installed: filters.installed !== undefined ? filters.installed : this.state.filters.installed,
|
||||
}
|
||||
|
||||
// Update filters first
|
||||
|
|
@ -260,8 +268,11 @@ export class MarketplaceViewStateManager {
|
|||
}
|
||||
|
||||
// Apply filters to displayItems and displayOrganizationMcps with the updated filters
|
||||
const newDisplayItems = this.filterItems(this.state.allItems)
|
||||
const newDisplayOrganizationMcps = this.filterItems(this.state.organizationMcps)
|
||||
const newDisplayItems = this.filterItems(this.state.allItems, this.state.installedMetadata)
|
||||
const newDisplayOrganizationMcps = this.filterItems(
|
||||
this.state.organizationMcps,
|
||||
this.state.installedMetadata,
|
||||
)
|
||||
|
||||
// Update state with filtered items
|
||||
this.state = {
|
||||
|
|
@ -284,39 +295,54 @@ export class MarketplaceViewStateManager {
|
|||
}
|
||||
|
||||
public isFilterActive(): boolean {
|
||||
return !!(this.state.filters.type || this.state.filters.search || this.state.filters.tags.length > 0)
|
||||
return !!(
|
||||
this.state.filters.type ||
|
||||
this.state.filters.search ||
|
||||
this.state.filters.tags.length > 0 ||
|
||||
this.state.filters.installed !== "all"
|
||||
)
|
||||
}
|
||||
|
||||
public filterItems(items: MarketplaceItem[]): MarketplaceItem[] {
|
||||
const { type, search, tags } = this.state.filters
|
||||
public filterItems(items: MarketplaceItem[], installedMetadata?: MarketplaceInstalledMetadata): MarketplaceItem[] {
|
||||
const { type, search, tags, installed } = this.state.filters
|
||||
const searchLower = search?.toLowerCase()
|
||||
|
||||
return items
|
||||
.map((item) => {
|
||||
// Create a copy of the item to modify
|
||||
const itemCopy = { ...item }
|
||||
return items.filter((item) => {
|
||||
// Check type match
|
||||
if (type && item.type !== type) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Check specific match conditions for the main item
|
||||
const typeMatch = !type || item.type === type
|
||||
const nameMatch = search ? item.name.toLowerCase().includes(search.toLowerCase()) : false
|
||||
const descriptionMatch = search
|
||||
? (item.description || "").toLowerCase().includes(search.toLowerCase())
|
||||
: false
|
||||
const tagMatch = tags.length > 0 ? item.tags?.some((tag) => tags.includes(tag)) : false
|
||||
|
||||
// Determine if the main item matches all filters
|
||||
const mainItemMatches =
|
||||
typeMatch && (!search || nameMatch || descriptionMatch) && (!tags.length || tagMatch)
|
||||
|
||||
const hasMatchingSubcomponents = false
|
||||
|
||||
// Return the item if it matches or has matching subcomponents
|
||||
if (mainItemMatches || Boolean(hasMatchingSubcomponents)) {
|
||||
return itemCopy
|
||||
// Check search match
|
||||
if (searchLower) {
|
||||
const nameMatch = item.name.toLowerCase().includes(searchLower)
|
||||
const descriptionMatch = (item.description || "").toLowerCase().includes(searchLower)
|
||||
if (!nameMatch && !descriptionMatch) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
})
|
||||
.filter((item): item is MarketplaceItem => item !== null)
|
||||
// Check tag match
|
||||
if (tags.length > 0 && !item.tags?.some((tag) => tags.includes(tag))) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Check installed status if filter is active
|
||||
if (installed !== "all" && installedMetadata) {
|
||||
const isInstalledGlobally = !!installedMetadata?.global?.[item.id]
|
||||
const isInstalledInProject = !!installedMetadata?.project?.[item.id]
|
||||
const isInstalled = isInstalledGlobally || isInstalledInProject
|
||||
|
||||
if (installed === "installed" && !isInstalled) {
|
||||
return false
|
||||
}
|
||||
if (installed === "not_installed" && isInstalled) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
public async handleMessage(message: any): Promise<void> {
|
||||
|
|
@ -343,20 +369,29 @@ export class MarketplaceViewStateManager {
|
|||
// Handle state updates for marketplace items
|
||||
// The state.marketplaceItems come from ClineProvider, see the file src/core/webview/ClineProvider.ts
|
||||
const marketplaceItems = message.state.marketplaceItems
|
||||
const marketplaceInstalledMetadata = message.state.marketplaceInstalledMetadata
|
||||
|
||||
if (marketplaceItems !== undefined) {
|
||||
// Always use the marketplace items from the extension when they're provided
|
||||
// This ensures fresh data is always displayed
|
||||
const items = [...marketplaceItems]
|
||||
|
||||
// Update installed metadata if provided
|
||||
if (marketplaceInstalledMetadata !== undefined) {
|
||||
this.state.installedMetadata = marketplaceInstalledMetadata
|
||||
}
|
||||
|
||||
// Calculate display items based on current filters
|
||||
// If no filters are active, show all items
|
||||
// If filters are active, apply filtering
|
||||
let newDisplayItems: MarketplaceItem[]
|
||||
let newDisplayOrganizationMcps: MarketplaceItem[]
|
||||
if (this.isFilterActive()) {
|
||||
newDisplayItems = this.filterItems(items)
|
||||
newDisplayOrganizationMcps = this.filterItems(this.state.organizationMcps)
|
||||
newDisplayItems = this.filterItems(items, this.state.installedMetadata)
|
||||
newDisplayOrganizationMcps = this.filterItems(
|
||||
this.state.organizationMcps,
|
||||
this.state.installedMetadata,
|
||||
)
|
||||
} else {
|
||||
// No filters active - show all items
|
||||
newDisplayItems = items
|
||||
|
|
@ -370,6 +405,7 @@ export class MarketplaceViewStateManager {
|
|||
allItems: items,
|
||||
displayItems: newDisplayItems,
|
||||
displayOrganizationMcps: newDisplayOrganizationMcps,
|
||||
installedMetadata: marketplaceInstalledMetadata || this.state.installedMetadata,
|
||||
}
|
||||
// Notification is handled below after all state parts are processed
|
||||
}
|
||||
|
|
@ -411,14 +447,25 @@ export class MarketplaceViewStateManager {
|
|||
if (message.type === "marketplaceData") {
|
||||
const marketplaceItems = message.marketplaceItems
|
||||
const organizationMcps = message.organizationMcps || []
|
||||
const marketplaceInstalledMetadata = message.marketplaceInstalledMetadata
|
||||
|
||||
if (marketplaceItems !== undefined) {
|
||||
// Always use the marketplace items from the extension when they're provided
|
||||
// This ensures fresh data is always displayed
|
||||
const items = [...marketplaceItems]
|
||||
const orgMcps = [...organizationMcps]
|
||||
const newDisplayItems = this.isFilterActive() ? this.filterItems(items) : items
|
||||
const newDisplayOrganizationMcps = this.isFilterActive() ? this.filterItems(orgMcps) : orgMcps
|
||||
|
||||
// Update installed metadata if provided
|
||||
if (marketplaceInstalledMetadata !== undefined) {
|
||||
this.state.installedMetadata = marketplaceInstalledMetadata
|
||||
}
|
||||
|
||||
const newDisplayItems = this.isFilterActive()
|
||||
? this.filterItems(items, this.state.installedMetadata)
|
||||
: items
|
||||
const newDisplayOrganizationMcps = this.isFilterActive()
|
||||
? this.filterItems(orgMcps, this.state.installedMetadata)
|
||||
: orgMcps
|
||||
|
||||
// Update state in a single operation
|
||||
this.state = {
|
||||
|
|
@ -428,6 +475,7 @@ export class MarketplaceViewStateManager {
|
|||
organizationMcps: orgMcps,
|
||||
displayItems: newDisplayItems,
|
||||
displayOrganizationMcps: newDisplayOrganizationMcps,
|
||||
installedMetadata: marketplaceInstalledMetadata || this.state.installedMetadata,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ const mockState: ViewState = {
|
|||
type: "",
|
||||
search: "",
|
||||
tags: [],
|
||||
installed: "all",
|
||||
},
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -64,6 +64,7 @@ describe("MarketplaceViewStateManager", () => {
|
|||
type: "",
|
||||
search: "",
|
||||
tags: [],
|
||||
installed: "all",
|
||||
})
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -78,6 +78,7 @@ describe("MarketplaceItemCard", () => {
|
|||
type: "",
|
||||
search: "",
|
||||
tags: [],
|
||||
installed: "all" as "all" | "installed" | "not_installed",
|
||||
},
|
||||
setFilters: vi.fn(),
|
||||
installed: {
|
||||
|
|
@ -158,7 +159,12 @@ describe("MarketplaceItemCard", () => {
|
|||
renderWithProviders(
|
||||
<MarketplaceItemCard
|
||||
item={item}
|
||||
filters={{ type: "", search: "", tags: [] }}
|
||||
filters={{
|
||||
type: "",
|
||||
search: "",
|
||||
tags: [],
|
||||
installed: "all" as "all" | "installed" | "not_installed",
|
||||
}}
|
||||
setFilters={setFilters}
|
||||
installed={{
|
||||
project: undefined,
|
||||
|
|
|
|||
6
webview-ui/src/i18n/locales/ca/marketplace.json
generated
6
webview-ui/src/i18n/locales/ca/marketplace.json
generated
|
|
@ -13,6 +13,12 @@
|
|||
"placeholderMcp": "Cercar MCPs...",
|
||||
"placeholderMode": "Cercar modes..."
|
||||
},
|
||||
"installed": {
|
||||
"label": "Filtra per estat",
|
||||
"all": "Tots els articles",
|
||||
"installed": "Instal·lats",
|
||||
"notInstalled": "No instal·lats"
|
||||
},
|
||||
"type": {
|
||||
"label": "Filtrar per tipus:",
|
||||
"all": "Tots els tipus",
|
||||
|
|
|
|||
6
webview-ui/src/i18n/locales/de/marketplace.json
generated
6
webview-ui/src/i18n/locales/de/marketplace.json
generated
|
|
@ -13,6 +13,12 @@
|
|||
"placeholderMcp": "MCPs durchsuchen...",
|
||||
"placeholderMode": "Modi durchsuchen..."
|
||||
},
|
||||
"installed": {
|
||||
"label": "Nach Status filtern",
|
||||
"all": "Alle Artikel",
|
||||
"installed": "Installierte",
|
||||
"notInstalled": "Nicht installiert"
|
||||
},
|
||||
"type": {
|
||||
"label": "Nach Typ filtern:",
|
||||
"all": "Alle Typen",
|
||||
|
|
|
|||
|
|
@ -13,6 +13,12 @@
|
|||
"placeholderMcp": "Search MCPs...",
|
||||
"placeholderMode": "Search Modes..."
|
||||
},
|
||||
"installed": {
|
||||
"label": "Filter by status",
|
||||
"all": "All Items",
|
||||
"installed": "Installed",
|
||||
"notInstalled": "Not Installed"
|
||||
},
|
||||
"type": {
|
||||
"label": "Filter by type:",
|
||||
"all": "All types",
|
||||
|
|
|
|||
6
webview-ui/src/i18n/locales/es/marketplace.json
generated
6
webview-ui/src/i18n/locales/es/marketplace.json
generated
|
|
@ -13,6 +13,12 @@
|
|||
"placeholderMcp": "Buscar MCPs...",
|
||||
"placeholderMode": "Buscar modos..."
|
||||
},
|
||||
"installed": {
|
||||
"label": "Filtrar por estado",
|
||||
"all": "Todos los artículos",
|
||||
"installed": "Instalados",
|
||||
"notInstalled": "No instalados"
|
||||
},
|
||||
"type": {
|
||||
"label": "Filtrar por tipo:",
|
||||
"all": "Todos los tipos",
|
||||
|
|
|
|||
6
webview-ui/src/i18n/locales/fr/marketplace.json
generated
6
webview-ui/src/i18n/locales/fr/marketplace.json
generated
|
|
@ -13,6 +13,12 @@
|
|||
"placeholderMcp": "Rechercher des MCPs...",
|
||||
"placeholderMode": "Rechercher des modes..."
|
||||
},
|
||||
"installed": {
|
||||
"label": "Filtrer par statut",
|
||||
"all": "Tous les articles",
|
||||
"installed": "Installés",
|
||||
"notInstalled": "Non installés"
|
||||
},
|
||||
"type": {
|
||||
"label": "Filtrer par type :",
|
||||
"all": "Tous les types",
|
||||
|
|
|
|||
6
webview-ui/src/i18n/locales/hi/marketplace.json
generated
6
webview-ui/src/i18n/locales/hi/marketplace.json
generated
|
|
@ -13,6 +13,12 @@
|
|||
"placeholderMcp": "MCP खोजें...",
|
||||
"placeholderMode": "मोड खोजें..."
|
||||
},
|
||||
"installed": {
|
||||
"label": "स्थिति के अनुसार फ़िल्टर करें",
|
||||
"all": "सभी आइटम",
|
||||
"installed": "स्थापित",
|
||||
"notInstalled": "स्थापित नहीं"
|
||||
},
|
||||
"type": {
|
||||
"label": "प्रकार के अनुसार फ़िल्टर करें:",
|
||||
"all": "सभी प्रकार",
|
||||
|
|
|
|||
6
webview-ui/src/i18n/locales/id/marketplace.json
generated
6
webview-ui/src/i18n/locales/id/marketplace.json
generated
|
|
@ -13,6 +13,12 @@
|
|||
"placeholderMcp": "Cari MCP...",
|
||||
"placeholderMode": "Cari Mode..."
|
||||
},
|
||||
"installed": {
|
||||
"label": "Filter berdasarkan status",
|
||||
"all": "Semua Item",
|
||||
"installed": "Terpasang",
|
||||
"notInstalled": "Tidak Terpasang"
|
||||
},
|
||||
"type": {
|
||||
"label": "Filter berdasarkan tipe:",
|
||||
"all": "Semua tipe",
|
||||
|
|
|
|||
6
webview-ui/src/i18n/locales/it/marketplace.json
generated
6
webview-ui/src/i18n/locales/it/marketplace.json
generated
|
|
@ -13,6 +13,12 @@
|
|||
"placeholderMcp": "Cerca MCP...",
|
||||
"placeholderMode": "Cerca modalità..."
|
||||
},
|
||||
"installed": {
|
||||
"label": "Filtra per stato",
|
||||
"all": "Tutti gli articoli",
|
||||
"installed": "Installati",
|
||||
"notInstalled": "Non installati"
|
||||
},
|
||||
"type": {
|
||||
"label": "Filtra per tipo:",
|
||||
"all": "Tutti i tipi",
|
||||
|
|
|
|||
6
webview-ui/src/i18n/locales/ja/marketplace.json
generated
6
webview-ui/src/i18n/locales/ja/marketplace.json
generated
|
|
@ -13,6 +13,12 @@
|
|||
"placeholderMcp": "MCPを検索...",
|
||||
"placeholderMode": "モードを検索..."
|
||||
},
|
||||
"installed": {
|
||||
"label": "ステータスで絞り込む",
|
||||
"all": "すべてのアイテム",
|
||||
"installed": "インストール済み",
|
||||
"notInstalled": "未インストール"
|
||||
},
|
||||
"type": {
|
||||
"label": "タイプでフィルター:",
|
||||
"all": "すべてのタイプ",
|
||||
|
|
|
|||
6
webview-ui/src/i18n/locales/ko/marketplace.json
generated
6
webview-ui/src/i18n/locales/ko/marketplace.json
generated
|
|
@ -13,6 +13,12 @@
|
|||
"placeholderMcp": "MCP 검색...",
|
||||
"placeholderMode": "모드 검색..."
|
||||
},
|
||||
"installed": {
|
||||
"label": "상태별로 필터링",
|
||||
"all": "모든 항목",
|
||||
"installed": "설치됨",
|
||||
"notInstalled": "설치되지 않음"
|
||||
},
|
||||
"type": {
|
||||
"label": "유형별 필터:",
|
||||
"all": "모든 유형",
|
||||
|
|
|
|||
6
webview-ui/src/i18n/locales/nl/marketplace.json
generated
6
webview-ui/src/i18n/locales/nl/marketplace.json
generated
|
|
@ -13,6 +13,12 @@
|
|||
"placeholderMcp": "MCP's zoeken...",
|
||||
"placeholderMode": "Modi zoeken..."
|
||||
},
|
||||
"installed": {
|
||||
"label": "Filteren op status",
|
||||
"all": "Alle items",
|
||||
"installed": "Geïnstalleerd",
|
||||
"notInstalled": "Niet geïnstalleerd"
|
||||
},
|
||||
"type": {
|
||||
"label": "Filteren op type:",
|
||||
"all": "Alle types",
|
||||
|
|
|
|||
6
webview-ui/src/i18n/locales/pl/marketplace.json
generated
6
webview-ui/src/i18n/locales/pl/marketplace.json
generated
|
|
@ -13,6 +13,12 @@
|
|||
"placeholderMcp": "Szukaj MCPs...",
|
||||
"placeholderMode": "Szukaj trybów..."
|
||||
},
|
||||
"installed": {
|
||||
"label": "Filtruj według statusu",
|
||||
"all": "Wszystkie elementy",
|
||||
"installed": "Zainstalowane",
|
||||
"notInstalled": "Niezainstalowane"
|
||||
},
|
||||
"type": {
|
||||
"label": "Filtruj według typu:",
|
||||
"all": "Wszystkie typy",
|
||||
|
|
|
|||
|
|
@ -13,6 +13,12 @@
|
|||
"placeholderMcp": "Buscar MCPs...",
|
||||
"placeholderMode": "Buscar modos..."
|
||||
},
|
||||
"installed": {
|
||||
"label": "Filtrar por status",
|
||||
"all": "Todos os itens",
|
||||
"installed": "Instalados",
|
||||
"notInstalled": "Não instalados"
|
||||
},
|
||||
"type": {
|
||||
"label": "Filtrar por tipo:",
|
||||
"all": "Todos os tipos",
|
||||
|
|
|
|||
6
webview-ui/src/i18n/locales/ru/marketplace.json
generated
6
webview-ui/src/i18n/locales/ru/marketplace.json
generated
|
|
@ -13,6 +13,12 @@
|
|||
"placeholderMcp": "Поиск MCPs...",
|
||||
"placeholderMode": "Поиск режимов..."
|
||||
},
|
||||
"installed": {
|
||||
"label": "Фильтр по статусу",
|
||||
"all": "Все элементы",
|
||||
"installed": "Установленные",
|
||||
"notInstalled": "Не установленные"
|
||||
},
|
||||
"type": {
|
||||
"label": "Фильтр по типу:",
|
||||
"all": "Все типы",
|
||||
|
|
|
|||
6
webview-ui/src/i18n/locales/tr/marketplace.json
generated
6
webview-ui/src/i18n/locales/tr/marketplace.json
generated
|
|
@ -13,6 +13,12 @@
|
|||
"placeholderMcp": "MCP'leri ara...",
|
||||
"placeholderMode": "Modları ara..."
|
||||
},
|
||||
"installed": {
|
||||
"label": "Duruma göre filtrele",
|
||||
"all": "Tüm Öğeler",
|
||||
"installed": "Yüklü",
|
||||
"notInstalled": "Yüklü Değil"
|
||||
},
|
||||
"type": {
|
||||
"label": "Türe göre filtrele:",
|
||||
"all": "Tüm türler",
|
||||
|
|
|
|||
6
webview-ui/src/i18n/locales/vi/marketplace.json
generated
6
webview-ui/src/i18n/locales/vi/marketplace.json
generated
|
|
@ -13,6 +13,12 @@
|
|||
"placeholderMcp": "Tìm kiếm MCP...",
|
||||
"placeholderMode": "Tìm kiếm Chế độ..."
|
||||
},
|
||||
"installed": {
|
||||
"label": "Lọc theo trạng thái",
|
||||
"all": "Tất cả các mục",
|
||||
"installed": "Đã cài đặt",
|
||||
"notInstalled": "Chưa cài đặt"
|
||||
},
|
||||
"type": {
|
||||
"label": "Lọc theo loại:",
|
||||
"all": "Tất cả loại",
|
||||
|
|
|
|||
|
|
@ -13,6 +13,12 @@
|
|||
"placeholderMcp": "搜索 MCP...",
|
||||
"placeholderMode": "搜索模式..."
|
||||
},
|
||||
"installed": {
|
||||
"label": "按状态筛选",
|
||||
"all": "所有项目",
|
||||
"installed": "已安装",
|
||||
"notInstalled": "未安装"
|
||||
},
|
||||
"type": {
|
||||
"label": "按类型筛选:",
|
||||
"all": "所有类型",
|
||||
|
|
|
|||
|
|
@ -13,6 +13,12 @@
|
|||
"placeholderMcp": "搜尋 MCP...",
|
||||
"placeholderMode": "搜尋模式..."
|
||||
},
|
||||
"installed": {
|
||||
"label": "按狀態篩選",
|
||||
"all": "所有項目",
|
||||
"installed": "已安裝",
|
||||
"notInstalled": "未安裝"
|
||||
},
|
||||
"type": {
|
||||
"label": "依類型篩選:",
|
||||
"all": "所有類型",
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue