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:
roomote[bot] 2025-08-21 02:17:41 -07:00 committed by GitHub
parent ce052dbd7e
commit 007b58db77
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
41 changed files with 438 additions and 145 deletions

View file

@ -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",

View file

@ -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",

View file

@ -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",

View file

@ -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",

View file

@ -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é",

View file

@ -38,6 +38,12 @@
"noResults": "कोई टैग नहीं मिले।",
"selected": "चयनित टैग्स में से किसी भी के साथ आइटम दिखा रहे हैं"
},
"installed": {
"label": "स्थिति के अनुसार फ़िल्टर करें",
"all": "सभी आइटम",
"installed": "स्थापित",
"notInstalled": "स्थापित नहीं"
},
"title": "मार्केटप्लेस"
},
"done": "हो गया",

View file

@ -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",

View file

@ -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",

View file

@ -38,6 +38,12 @@
"noResults": "タグが見つかりません。",
"selected": "選択されたタグのいずれかを持つアイテムを表示"
},
"installed": {
"label": "ステータスで絞り込む",
"all": "すべてのアイテム",
"installed": "インストール済み",
"notInstalled": "未インストール"
},
"title": "マーケットプレイス"
},
"done": "完了",

View file

@ -38,6 +38,12 @@
"noResults": "태그를 찾을 수 없습니다.",
"selected": "선택된 태그 중 하나를 가진 항목 표시"
},
"installed": {
"label": "상태별로 필터링",
"all": "모든 항목",
"installed": "설치됨",
"notInstalled": "설치되지 않음"
},
"title": "마켓플레이스"
},
"done": "완료",

View file

@ -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",

View file

@ -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",

View file

@ -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",

View file

@ -38,6 +38,12 @@
"noResults": "Теги не найдены.",
"selected": "Показ элементов с любым из выбранных тегов"
},
"installed": {
"label": "Фильтр по статусу",
"all": "Все элементы",
"installed": "Установленные",
"notInstalled": "Не установленные"
},
"title": "Marketplace"
},
"done": "Готово",

View file

@ -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",

View file

@ -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",

View file

@ -38,6 +38,12 @@
"noResults": "未找到标签。",
"selected": "显示包含任一选中标签的项目"
},
"installed": {
"label": "按状态筛选",
"all": "所有项目",
"installed": "已安装",
"notInstalled": "未安装"
},
"title": "Marketplace"
},
"done": "完成",

View file

@ -38,6 +38,12 @@
"noResults": "找不到標籤。",
"selected": "顯示包含任一選取標籤的項目"
},
"installed": {
"label": "按狀態篩選",
"all": "所有項目",
"installed": "已安裝",
"notInstalled": "未安裝"
},
"title": "Marketplace"
},
"done": "完成",

View file

@ -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">

View file

@ -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,
}
}

View file

@ -27,6 +27,7 @@ const mockState: ViewState = {
type: "",
search: "",
tags: [],
installed: "all",
},
}

View file

@ -64,6 +64,7 @@ describe("MarketplaceViewStateManager", () => {
type: "",
search: "",
tags: [],
installed: "all",
})
})

View file

@ -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,

View file

@ -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",

View file

@ -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",

View file

@ -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",

View file

@ -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",

View file

@ -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",

View file

@ -13,6 +13,12 @@
"placeholderMcp": "MCP खोजें...",
"placeholderMode": "मोड खोजें..."
},
"installed": {
"label": "स्थिति के अनुसार फ़िल्टर करें",
"all": "सभी आइटम",
"installed": "स्थापित",
"notInstalled": "स्थापित नहीं"
},
"type": {
"label": "प्रकार के अनुसार फ़िल्टर करें:",
"all": "सभी प्रकार",

View file

@ -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",

View file

@ -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",

View file

@ -13,6 +13,12 @@
"placeholderMcp": "MCPを検索...",
"placeholderMode": "モードを検索..."
},
"installed": {
"label": "ステータスで絞り込む",
"all": "すべてのアイテム",
"installed": "インストール済み",
"notInstalled": "未インストール"
},
"type": {
"label": "タイプでフィルター:",
"all": "すべてのタイプ",

View file

@ -13,6 +13,12 @@
"placeholderMcp": "MCP 검색...",
"placeholderMode": "모드 검색..."
},
"installed": {
"label": "상태별로 필터링",
"all": "모든 항목",
"installed": "설치됨",
"notInstalled": "설치되지 않음"
},
"type": {
"label": "유형별 필터:",
"all": "모든 유형",

View file

@ -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",

View file

@ -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",

View file

@ -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",

View file

@ -13,6 +13,12 @@
"placeholderMcp": "Поиск MCPs...",
"placeholderMode": "Поиск режимов..."
},
"installed": {
"label": "Фильтр по статусу",
"all": "Все элементы",
"installed": "Установленные",
"notInstalled": "Не установленные"
},
"type": {
"label": "Фильтр по типу:",
"all": "Все типы",

View file

@ -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",

View file

@ -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",

View file

@ -13,6 +13,12 @@
"placeholderMcp": "搜索 MCP...",
"placeholderMode": "搜索模式..."
},
"installed": {
"label": "按状态筛选",
"all": "所有项目",
"installed": "已安装",
"notInstalled": "未安装"
},
"type": {
"label": "按类型筛选:",
"all": "所有类型",

View file

@ -13,6 +13,12 @@
"placeholderMcp": "搜尋 MCP...",
"placeholderMode": "搜尋模式..."
},
"installed": {
"label": "按狀態篩選",
"all": "所有項目",
"installed": "已安裝",
"notInstalled": "未安裝"
},
"type": {
"label": "依類型篩選:",
"all": "所有類型",