Skip to content

Commit 272ddad

Browse files
committed
feat(settings): add searchable dropdown to API config profiles setting page
- Replace Select with Command+Popover components in ApiConfigManager - Add search functionality to filter configuration profiles - Add clear search button and selected item indicator - Add internationalization support for all languages - Match UX pattern from ModelPicker component for consistency
1 parent a3e6c91 commit 272ddad

File tree

17 files changed

+181
-19
lines changed

17 files changed

+181
-19
lines changed

webview-ui/src/components/settings/ApiConfigManager.tsx

Lines changed: 107 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,26 @@
11
import { memo, useEffect, useRef, useState } from "react"
22
import { VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
3+
import { ChevronsUpDown, Check, X } from "lucide-react"
34

45
import { ApiConfigMeta } from "../../../../src/shared/ExtensionMessage"
56

67
import { useAppTranslation } from "@/i18n/TranslationContext"
8+
import { cn } from "@/lib/utils"
79
import {
810
Button,
911
Input,
1012
Dialog,
1113
DialogContent,
1214
DialogTitle,
13-
Select,
14-
SelectTrigger,
15-
SelectValue,
16-
SelectContent,
17-
SelectItem,
15+
Command,
16+
CommandEmpty,
17+
CommandGroup,
18+
CommandInput,
19+
CommandItem,
20+
CommandList,
21+
Popover,
22+
PopoverContent,
23+
PopoverTrigger,
1824
} from "@/components/ui"
1925

2026
interface ApiConfigManagerProps {
@@ -41,8 +47,11 @@ const ApiConfigManager = ({
4147
const [inputValue, setInputValue] = useState("")
4248
const [newProfileName, setNewProfileName] = useState("")
4349
const [error, setError] = useState<string | null>(null)
50+
const [open, setOpen] = useState(false)
51+
const [searchValue, setSearchValue] = useState("")
4452
const inputRef = useRef<any>(null)
4553
const newProfileInputRef = useRef<any>(null)
54+
const searchInputRef = useRef<HTMLInputElement>(null)
4655

4756
const validateName = (name: string, isNewProfile: boolean): string | null => {
4857
const trimmed = name.trim()
@@ -95,8 +104,31 @@ const ApiConfigManager = ({
95104
useEffect(() => {
96105
resetCreateState()
97106
resetRenameState()
107+
// Reset search value when current profile changes
108+
setTimeout(() => setSearchValue(""), 100)
98109
}, [currentApiConfigName])
99110

111+
const onOpenChange = (open: boolean) => {
112+
setOpen(open)
113+
114+
// Reset search when closing the popover
115+
if (!open) {
116+
setTimeout(() => setSearchValue(""), 100)
117+
}
118+
}
119+
120+
const onClearSearch = () => {
121+
setSearchValue("")
122+
searchInputRef.current?.focus()
123+
}
124+
125+
const handleSelectConfig = (configName: string) => {
126+
if (!configName) return
127+
128+
setOpen(false)
129+
onSelectConfig(configName)
130+
}
131+
100132
const handleAdd = () => {
101133
resetCreateState()
102134
setIsCreating(true)
@@ -206,18 +238,76 @@ const ApiConfigManager = ({
206238
) : (
207239
<>
208240
<div className="flex items-center gap-1">
209-
<Select value={currentApiConfigName} onValueChange={onSelectConfig}>
210-
<SelectTrigger className="grow">
211-
<SelectValue placeholder={t("settings:common.select")} />
212-
</SelectTrigger>
213-
<SelectContent>
214-
{listApiConfigMeta.map((config) => (
215-
<SelectItem key={config.name} value={config.name}>
216-
{config.name}
217-
</SelectItem>
218-
))}
219-
</SelectContent>
220-
</Select>
241+
<Popover open={open} onOpenChange={onOpenChange}>
242+
<PopoverTrigger asChild>
243+
<Button
244+
variant="combobox"
245+
role="combobox"
246+
aria-expanded={open}
247+
className="grow justify-between"
248+
// Use select-component data-testid for test compatibility
249+
data-testid="select-component">
250+
<div>{currentApiConfigName || t("settings:common.select")}</div>
251+
<ChevronsUpDown className="opacity-50" />
252+
</Button>
253+
</PopoverTrigger>
254+
<PopoverContent className="p-0 w-[var(--radix-popover-trigger-width)]">
255+
<Command>
256+
<div className="relative">
257+
<CommandInput
258+
ref={searchInputRef}
259+
value={searchValue}
260+
onValueChange={setSearchValue}
261+
placeholder={t("settings:providers.searchPlaceholder")}
262+
className="h-9 mr-4"
263+
data-testid="profile-search-input"
264+
/>
265+
{searchValue.length > 0 && (
266+
<div className="absolute right-2 top-0 bottom-0 flex items-center justify-center">
267+
<X
268+
className="text-vscode-input-foreground opacity-50 hover:opacity-100 size-4 p-0.5 cursor-pointer"
269+
onClick={onClearSearch}
270+
/>
271+
</div>
272+
)}
273+
</div>
274+
<CommandList>
275+
<CommandEmpty>
276+
{searchValue && (
277+
<div className="py-2 px-1 text-sm">
278+
{t("settings:providers.noMatchFound")}
279+
</div>
280+
)}
281+
</CommandEmpty>
282+
<CommandGroup>
283+
{listApiConfigMeta
284+
.filter((config) =>
285+
searchValue
286+
? config.name.toLowerCase().includes(searchValue.toLowerCase())
287+
: true,
288+
)
289+
.map((config) => (
290+
<CommandItem
291+
key={config.name}
292+
value={config.name}
293+
onSelect={handleSelectConfig}
294+
data-testid={`profile-option-${config.name}`}>
295+
{config.name}
296+
<Check
297+
className={cn(
298+
"size-4 p-0.5 ml-auto",
299+
config.name === currentApiConfigName
300+
? "opacity-100"
301+
: "opacity-0",
302+
)}
303+
/>
304+
</CommandItem>
305+
))}
306+
</CommandGroup>
307+
</CommandList>
308+
</Command>
309+
</PopoverContent>
310+
</Popover>
221311
<Button
222312
variant="ghost"
223313
size="icon"

webview-ui/src/components/settings/__tests__/ApiConfigManager.test.tsx

Lines changed: 44 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,34 @@ jest.mock("@/components/ui", () => ({
4242
data-testid={dataTestId}
4343
/>
4444
),
45+
// New components for searchable dropdown
46+
Popover: ({ children, open, onOpenChange }: any) => (
47+
<div className="popover" style={{ position: "relative" }}>
48+
{children}
49+
{open && <div className="popover-content" style={{ position: "absolute", top: "100%", left: 0 }}></div>}
50+
</div>
51+
),
52+
PopoverTrigger: ({ children, asChild }: any) => <div className="popover-trigger">{children}</div>,
53+
PopoverContent: ({ children, className }: any) => <div className="popover-content">{children}</div>,
54+
Command: ({ children }: any) => <div className="command">{children}</div>,
55+
CommandInput: ({ value, onValueChange, placeholder, className, "data-testid": dataTestId, ref }: any) => (
56+
<input
57+
value={value}
58+
onChange={(e) => onValueChange(e.target.value)}
59+
placeholder={placeholder}
60+
className={className}
61+
data-testid={dataTestId}
62+
/>
63+
),
64+
CommandList: ({ children }: any) => <div className="command-list">{children}</div>,
65+
CommandEmpty: ({ children }: any) => (children ? <div className="command-empty">{children}</div> : null),
66+
CommandGroup: ({ children }: any) => <div className="command-group">{children}</div>,
67+
CommandItem: ({ children, value, onSelect }: any) => (
68+
<div className="command-item" onClick={() => onSelect(value)} data-value={value}>
69+
{children}
70+
</div>
71+
),
72+
// Keep old components for backward compatibility
4573
Select: ({ children, value, onValueChange }: any) => (
4674
<select
4775
value={value}
@@ -215,8 +243,22 @@ describe("ApiConfigManager", () => {
215243
it("allows selecting a different config", () => {
216244
render(<ApiConfigManager {...defaultProps} />)
217245

218-
const select = screen.getByTestId("select-component")
219-
fireEvent.change(select, { target: { value: "Another Config" } })
246+
// Click the select component to open the dropdown
247+
const selectButton = screen.getByTestId("select-component")
248+
fireEvent.click(selectButton)
249+
250+
// Find all command items and click the one with "Another Config"
251+
const commandItems = document.querySelectorAll('.command-item')
252+
// Find the item with "Another Config" text
253+
const anotherConfigItem = Array.from(commandItems).find(
254+
item => item.textContent?.includes("Another Config")
255+
)
256+
257+
if (!anotherConfigItem) {
258+
throw new Error("Could not find 'Another Config' option")
259+
}
260+
261+
fireEvent.click(anotherConfigItem)
220262

221263
expect(mockOnSelectConfig).toHaveBeenCalledWith("Another Config")
222264
})

webview-ui/src/i18n/locales/ca/settings.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,8 @@
9898
"enterProfileName": "Introduïu el nom del perfil",
9999
"createProfile": "Crea perfil",
100100
"cannotDeleteOnlyProfile": "No es pot eliminar l'únic perfil",
101+
"searchPlaceholder": "Cerca perfils",
102+
"noMatchFound": "No s'han trobat perfils coincidents",
101103
"vscodeLmDescription": "L'API del model de llenguatge de VS Code us permet executar models proporcionats per altres extensions de VS Code (incloent-hi, però no limitat a, GitHub Copilot). La manera més senzilla de començar és instal·lar les extensions Copilot i Copilot Chat des del VS Code Marketplace.",
102104
"awsCustomArnUse": "Introduïu un ARN vàlid d'AWS Bedrock per al model que voleu utilitzar. Exemples de format:",
103105
"awsCustomArnDesc": "Assegureu-vos que la regió a l'ARN coincideix amb la regió d'AWS seleccionada anteriorment.",

webview-ui/src/i18n/locales/de/settings.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,8 @@
9898
"enterProfileName": "Profilnamen eingeben",
9999
"createProfile": "Profil erstellen",
100100
"cannotDeleteOnlyProfile": "Das einzige Profil kann nicht gelöscht werden",
101+
"searchPlaceholder": "Profile durchsuchen",
102+
"noMatchFound": "Keine passenden Profile gefunden",
101103
"vscodeLmDescription": "Die VS Code Language Model API ermöglicht das Ausführen von Modellen, die von anderen VS Code-Erweiterungen bereitgestellt werden (einschließlich, aber nicht beschränkt auf GitHub Copilot). Der einfachste Weg, um zu starten, besteht darin, die Erweiterungen Copilot und Copilot Chat aus dem VS Code Marketplace zu installieren.",
102104
"awsCustomArnUse": "Geben Sie eine gültige AWS Bedrock ARN für das Modell ein, das Sie verwenden möchten. Formatbeispiele:",
103105
"awsCustomArnDesc": "Stellen Sie sicher, dass die Region in der ARN mit Ihrer oben ausgewählten AWS-Region übereinstimmt.",

webview-ui/src/i18n/locales/en/settings.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,8 @@
9898
"enterProfileName": "Enter profile name",
9999
"createProfile": "Create Profile",
100100
"cannotDeleteOnlyProfile": "Cannot delete the only profile",
101+
"searchPlaceholder": "Search profiles",
102+
"noMatchFound": "No matching profiles found",
101103
"vscodeLmDescription": " The VS Code Language Model API allows you to run models provided by other VS Code extensions (including but not limited to GitHub Copilot). The easiest way to get started is to install the Copilot and Copilot Chat extensions from the VS Code Marketplace.",
102104
"awsCustomArnUse": "Enter a valid AWS Bedrock ARN for the model you want to use. Format examples:",
103105
"awsCustomArnDesc": "Make sure the region in the ARN matches your selected AWS Region above.",

webview-ui/src/i18n/locales/es/settings.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,8 @@
9898
"enterProfileName": "Ingrese el nombre del perfil",
9999
"createProfile": "Crear perfil",
100100
"cannotDeleteOnlyProfile": "No se puede eliminar el único perfil",
101+
"searchPlaceholder": "Buscar perfiles",
102+
"noMatchFound": "No se encontraron perfiles coincidentes",
101103
"vscodeLmDescription": "La API del Modelo de Lenguaje de VS Code le permite ejecutar modelos proporcionados por otras extensiones de VS Code (incluido, entre otros, GitHub Copilot). La forma más sencilla de empezar es instalar las extensiones Copilot y Copilot Chat desde el VS Code Marketplace.",
102104
"awsCustomArnUse": "Ingrese un ARN de AWS Bedrock válido para el modelo que desea utilizar. Ejemplos de formato:",
103105
"awsCustomArnDesc": "Asegúrese de que la región en el ARN coincida con la región de AWS seleccionada anteriormente.",

webview-ui/src/i18n/locales/fr/settings.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,8 @@
9898
"enterProfileName": "Entrez le nom du profil",
9999
"createProfile": "Créer un profil",
100100
"cannotDeleteOnlyProfile": "Impossible de supprimer le seul profil",
101+
"searchPlaceholder": "Rechercher des profils",
102+
"noMatchFound": "Aucun profil correspondant trouvé",
101103
"vscodeLmDescription": "L'API du modèle de langage VS Code vous permet d'exécuter des modèles fournis par d'autres extensions VS Code (y compris, mais sans s'y limiter, GitHub Copilot). Le moyen le plus simple de commencer est d'installer les extensions Copilot et Copilot Chat depuis le VS Code Marketplace.",
102104
"awsCustomArnUse": "Entrez un ARN AWS Bedrock valide pour le modèle que vous souhaitez utiliser. Exemples de format :",
103105
"awsCustomArnDesc": "Assurez-vous que la région dans l'ARN correspond à la région AWS sélectionnée ci-dessus.",

webview-ui/src/i18n/locales/hi/settings.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,8 @@
9898
"enterProfileName": "प्रोफ़ाइल नाम दर्ज करें",
9999
"createProfile": "प्रोफ़ाइल बनाएं",
100100
"cannotDeleteOnlyProfile": "केवल एकमात्र प्रोफ़ाइल को हटाया नहीं जा सकता",
101+
"searchPlaceholder": "प्रोफ़ाइल खोजें",
102+
"noMatchFound": "कोई मिलान प्रोफ़ाइल नहीं मिला",
101103
"vscodeLmDescription": "VS कोड भाषा मॉडल API आपको अन्य VS कोड एक्सटेंशन (जैसे GitHub Copilot) द्वारा प्रदान किए गए मॉडल चलाने की अनुमति देता है। शुरू करने का सबसे आसान तरीका VS कोड मार्केटप्लेस से Copilot और Copilot चैट एक्सटेंशन इंस्टॉल करना है।",
102104
"awsCustomArnUse": "आप जिस मॉडल का उपयोग करना चाहते हैं, उसके लिए एक वैध AWS बेडरॉक ARN दर्ज करें। प्रारूप उदाहरण:",
103105
"awsCustomArnDesc": "सुनिश्चित करें कि ARN में क्षेत्र ऊपर चयनित AWS क्षेत्र से मेल खाता है।",

webview-ui/src/i18n/locales/it/settings.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,8 @@
9898
"enterProfileName": "Inserisci il nome del profilo",
9999
"createProfile": "Crea profilo",
100100
"cannotDeleteOnlyProfile": "Impossibile eliminare l'unico profilo",
101+
"searchPlaceholder": "Cerca profili",
102+
"noMatchFound": "Nessun profilo corrispondente trovato",
101103
"vscodeLmDescription": "L'API del Modello di Linguaggio di VS Code consente di eseguire modelli forniti da altre estensioni di VS Code (incluso, ma non limitato a, GitHub Copilot). Il modo più semplice per iniziare è installare le estensioni Copilot e Copilot Chat dal VS Code Marketplace.",
102104
"awsCustomArnUse": "Inserisci un ARN AWS Bedrock valido per il modello che desideri utilizzare. Esempi di formato:",
103105
"awsCustomArnDesc": "Assicurati che la regione nell'ARN corrisponda alla regione AWS selezionata sopra.",

webview-ui/src/i18n/locales/ja/settings.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,8 @@
9898
"enterProfileName": "プロファイル名を入力",
9999
"createProfile": "プロファイルを作成",
100100
"cannotDeleteOnlyProfile": "唯一のプロファイルは削除できません",
101+
"searchPlaceholder": "プロファイルを検索",
102+
"noMatchFound": "一致するプロファイルが見つかりません",
101103
"vscodeLmDescription": "VS Code言語モデルAPIを使用すると、他のVS Code拡張機能(GitHub Copilotなど)が提供するモデルを実行できます。最も簡単な方法は、VS Code MarketplaceからCopilotおよびCopilot Chat拡張機能をインストールすることです。",
102104
"awsCustomArnUse": "使用したいモデルの有効なAWS Bedrock ARNを入力してください。形式の例:",
103105
"awsCustomArnDesc": "ARN内のリージョンが上で選択したAWSリージョンと一致していることを確認してください。",

0 commit comments

Comments
 (0)