feat: sync API config selector style with mode selector from PR #6140 (#6148)

Co-authored-by: Daniel Riccio <ricciodaniel98@gmail.com>
This commit is contained in:
Hannes Rudolph 2025-07-26 23:15:10 -06:00 • committed by GitHub
parent 8625b451f4
commit 7a6e852248
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
21 changed files with 705 additions and 138 deletions

View file

@ -0,0 +1,242 @@
import React, { useState, useMemo, useCallback } from "react"
import { cn } from "@/lib/utils"
import { useRooPortal } from "@/components/ui/hooks/useRooPortal"
import { Popover, PopoverContent, PopoverTrigger, StandardTooltip } from "@/components/ui"
import { IconButton } from "./IconButton"
import { useAppTranslation } from "@/i18n/TranslationContext"
import { vscode } from "@/utils/vscode"
import { Fzf } from "fzf"
import { Button } from "@/components/ui"
interface ApiConfigSelectorProps {
value: string
displayName: string
disabled?: boolean
title?: string
onChange: (value: string) => void
triggerClassName?: string
listApiConfigMeta: Array<{ id: string; name: string }>
pinnedApiConfigs?: Record<string, boolean>
togglePinnedApiConfig: (id: string) => void
}
export const ApiConfigSelector = ({
value,
displayName,
disabled = false,
title = "",
onChange,
triggerClassName = "",
listApiConfigMeta,
pinnedApiConfigs,
togglePinnedApiConfig,
}: ApiConfigSelectorProps) => {
const { t } = useAppTranslation()
const [open, setOpen] = useState(false)
const [searchValue, setSearchValue] = useState("")
const portalContainer = useRooPortal("roo-portal")
// Create searchable items for fuzzy search
const searchableItems = useMemo(() => {
return listApiConfigMeta.map((config) => ({
original: config,
searchStr: config.name,
}))
}, [listApiConfigMeta])
// Create Fzf instance
const fzfInstance = useMemo(() => {
return new Fzf(searchableItems, {
selector: (item) => item.searchStr,
})
}, [searchableItems])
// Filter configs based on search
const filteredConfigs = useMemo(() => {
if (!searchValue) return listApiConfigMeta
const matchingItems = fzfInstance.find(searchValue).map((result) => result.item.original)
return matchingItems
}, [listApiConfigMeta, searchValue, fzfInstance])
// Separate pinned and unpinned configs
const { pinnedConfigs, unpinnedConfigs } = useMemo(() => {
const pinned = filteredConfigs.filter((config) => pinnedApiConfigs?.[config.id])
const unpinned = filteredConfigs.filter((config) => !pinnedApiConfigs?.[config.id])
return { pinnedConfigs: pinned, unpinnedConfigs: unpinned }
}, [filteredConfigs, pinnedApiConfigs])
const handleSelect = useCallback(
(configId: string) => {
onChange(configId)
setOpen(false)
setSearchValue("")
},
[onChange],
)
const handleEditClick = useCallback(() => {
vscode.postMessage({
type: "switchTab",
tab: "settings",
})
setOpen(false)
}, [])
const renderConfigItem = useCallback(
(config: { id: string; name: string }, isPinned: boolean) => {
const isCurrentConfig = config.id === value
return (
<div
key={config.id}
onClick={() => handleSelect(config.id)}
className={cn(
"px-3 py-1.5 text-sm cursor-pointer flex items-center group",
"hover:bg-vscode-list-hoverBackground",
isCurrentConfig &&
"bg-vscode-list-activeSelectionBackground text-vscode-list-activeSelectionForeground",
)}>
<span className="flex-1 truncate">{config.name}</span>
<div className="flex items-center gap-1">
{isCurrentConfig && (
<div className="size-5 p-1 flex items-center justify-center">
<span className="codicon codicon-check text-xs" />
</div>
)}
<StandardTooltip content={isPinned ? t("chat:unpin") : t("chat:pin")}>
<Button
variant="ghost"
size="icon"
tabIndex={-1}
onClick={(e) => {
e.stopPropagation()
togglePinnedApiConfig(config.id)
vscode.postMessage({
type: "toggleApiConfigPin",
text: config.id,
})
}}
className={cn("size-5 flex items-center justify-center", {
"opacity-0 group-hover:opacity-100": !isPinned && !isCurrentConfig,
"bg-accent opacity-100": isPinned,
})}>
<span className="codicon codicon-pin text-xs opacity-50" />
</Button>
</StandardTooltip>
</div>
</div>
)
},
[value, handleSelect, t, togglePinnedApiConfig],
)
const triggerContent = (
<PopoverTrigger
disabled={disabled}
data-testid="dropdown-trigger"
className={cn(
"w-full min-w-0 max-w-full inline-flex items-center gap-1.5 relative whitespace-nowrap px-1.5 py-1 text-xs",
"bg-transparent border border-[rgba(255,255,255,0.08)] rounded-md text-vscode-foreground",
"transition-all duration-150 focus:outline-none focus-visible:ring-1 focus-visible:ring-vscode-focusBorder focus-visible:ring-inset",
disabled
? "opacity-50 cursor-not-allowed"
: "opacity-90 hover:opacity-100 hover:bg-[rgba(255,255,255,0.03)] hover:border-[rgba(255,255,255,0.15)] cursor-pointer",
triggerClassName,
)}>
<span
className={cn(
"codicon codicon-chevron-up pointer-events-none opacity-80 flex-shrink-0 text-xs transition-transform duration-200",
open && "rotate-180",
)}
/>
<span className="truncate">{displayName}</span>
</PopoverTrigger>
)
return (
<Popover open={open} onOpenChange={setOpen}>
{title ? <StandardTooltip content={title}>{triggerContent}</StandardTooltip> : triggerContent}
<PopoverContent
align="start"
sideOffset={4}
container={portalContainer}
className="p-0 overflow-hidden w-[300px]">
<div className="flex flex-col w-full">
{/* Search input or info blurb */}
{listApiConfigMeta.length > 6 ? (
<div className="relative p-2 border-b border-vscode-dropdown-border">
<input
aria-label={t("common:ui.search_placeholder")}
value={searchValue}
onChange={(e) => setSearchValue(e.target.value)}
placeholder={t("common:ui.search_placeholder")}
className="w-full h-8 px-2 py-1 text-xs bg-vscode-input-background text-vscode-input-foreground border border-vscode-input-border rounded focus:outline-0"
autoFocus
/>
{searchValue.length > 0 && (
<div className="absolute right-4 top-0 bottom-0 flex items-center justify-center">
<span
className="codicon codicon-close text-vscode-input-foreground opacity-50 hover:opacity-100 text-xs cursor-pointer"
onClick={() => setSearchValue("")}
/>
</div>
)}
</div>
) : (
<div className="p-3 border-b border-vscode-dropdown-border">
<p className="text-xs text-vscode-descriptionForeground m-0">
{t("prompts:apiConfiguration.select")}
</p>
</div>
)}
{/* Config list */}
<div className="max-h-[300px] overflow-y-auto">
{filteredConfigs.length === 0 && searchValue ? (
<div className="py-2 px-3 text-sm text-vscode-foreground/70">
{t("common:ui.no_results")}
</div>
) : (
<div className="py-1">
{/* Pinned configs */}
{pinnedConfigs.map((config) => renderConfigItem(config, true))}
{/* Separator between pinned and unpinned */}
{pinnedConfigs.length > 0 && unpinnedConfigs.length > 0 && (
<div className="mx-1 my-1 h-px bg-vscode-dropdown-foreground/10" />
)}
{/* Unpinned configs */}
{unpinnedConfigs.map((config) => renderConfigItem(config, false))}
</div>
)}
</div>
{/* Bottom bar with buttons on left and title on right */}
<div className="flex flex-row items-center justify-between p-2 border-t border-vscode-dropdown-border">
<div className="flex flex-row gap-1">
<IconButton
iconClass="codicon-settings-gear"
title={t("chat:edit")}
onClick={handleEditClick}
/>
</div>
{/* Info icon and title on the right with matching spacing */}
<div className="flex items-center gap-1 pr-1">
{listApiConfigMeta.length > 6 && (
<StandardTooltip content={t("prompts:apiConfiguration.select")}>
<span className="codicon codicon-info text-xs text-vscode-descriptionForeground opacity-70 hover:opacity-100 cursor-help" />
</StandardTooltip>
)}
<h4 className="m-0 font-medium text-sm text-vscode-descriptionForeground">
{t("prompts:apiConfiguration.title")}
</h4>
</div>
</div>
</div>
</PopoverContent>
</Popover>
)
}

View file

@ -19,13 +19,14 @@ import {
SearchResult,
} from "@src/utils/context-mentions"
import { convertToMentionPath } from "@/utils/path-mentions"
import { SelectDropdown, DropdownOptionType, Button, StandardTooltip } from "@/components/ui"
import { StandardTooltip } from "@/components/ui"
import Thumbnails from "../common/Thumbnails"
import ModeSelector from "./ModeSelector"
import { ApiConfigSelector } from "./ApiConfigSelector"
import { MAX_IMAGES_PER_MESSAGE } from "./ChatView"
import ContextMenu from "./ContextMenu"
import { VolumeX, Pin, Check, Image, WandSparkles, SendHorizontal } from "lucide-react"
import { VolumeX, Image, WandSparkles, SendHorizontal } from "lucide-react"
import { IndexingStatusBadge } from "./IndexingStatusBadge"
import { cn } from "@/lib/utils"
import { usePromptHistory } from "./hooks/usePromptHistory"
@ -856,122 +857,11 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
/>
)
// Helper function to get API config dropdown options
const getApiConfigOptions = useMemo(() => {
const pinnedConfigs = (listApiConfigMeta || [])
.filter((config) => pinnedApiConfigs && pinnedApiConfigs[config.id])
.map((config) => ({
value: config.id,
label: config.name,
name: config.name,
type: DropdownOptionType.ITEM,
pinned: true,
}))
.sort((a, b) => a.label.localeCompare(b.label))
const unpinnedConfigs = (listApiConfigMeta || [])
.filter((config) => !pinnedApiConfigs || !pinnedApiConfigs[config.id])
.map((config) => ({
value: config.id,
label: config.name,
name: config.name,
type: DropdownOptionType.ITEM,
pinned: false,
}))
.sort((a, b) => a.label.localeCompare(b.label))
const hasPinnedAndUnpinned = pinnedConfigs.length > 0 && unpinnedConfigs.length > 0
return [
...pinnedConfigs,
...(hasPinnedAndUnpinned
? [
{
value: "sep-pinned",
label: t("chat:separator"),
type: DropdownOptionType.SEPARATOR,
},
]
: []),
...unpinnedConfigs,
{
value: "sep-2",
label: t("chat:separator"),
type: DropdownOptionType.SEPARATOR,
},
{
value: "settingsButtonClicked",
label: t("chat:edit"),
type: DropdownOptionType.ACTION,
},
]
}, [listApiConfigMeta, pinnedApiConfigs, t])
// Helper function to handle API config change
const handleApiConfigChange = useCallback((value: string) => {
if (value === "settingsButtonClicked") {
vscode.postMessage({
type: "loadApiConfiguration",
text: value,
values: { section: "providers" },
})
} else {
vscode.postMessage({ type: "loadApiConfigurationById", text: value })
}
vscode.postMessage({ type: "loadApiConfigurationById", text: value })
}, [])
// Helper function to render API config item
const renderApiConfigItem = useCallback(
({ type, value, label, pinned }: any) => {
if (type !== DropdownOptionType.ITEM) {
return label
}
const config = listApiConfigMeta?.find((c) => c.id === value)
const isCurrentConfig = config?.name === currentApiConfigName
return (
<div className="flex justify-between gap-2 w-full h-5">
<div
className={cn("truncate min-w-0 overflow-hidden", {
"font-medium": isCurrentConfig,
})}>
{label}
</div>
<div className="flex justify-end w-10 flex-shrink-0">
<div
className={cn("size-5 p-1", {
"block group-hover:hidden": !pinned,
hidden: !isCurrentConfig,
})}>
<Check className="size-3" />
</div>
<StandardTooltip content={pinned ? t("chat:unpin") : t("chat:pin")}>
<Button
variant="ghost"
size="icon"
onClick={(e) => {
e.stopPropagation()
togglePinnedApiConfig(value)
vscode.postMessage({
type: "toggleApiConfigPin",
text: value,
})
}}
className={cn("size-5", {
"hidden group-hover:flex": !pinned,
"bg-accent": pinned,
})}>
<Pin className="size-3 p-0.5 opacity-50" />
</Button>
</StandardTooltip>
</div>
</div>
)
},
[listApiConfigMeta, currentApiConfigName, t, togglePinnedApiConfig],
)
// Helper function to render non-edit mode controls
const renderNonEditModeControls = () => (
<div className={cn("flex", "justify-between", "items-center", "mt-auto")}>
@ -979,17 +869,16 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
<div className="shrink-0">{renderModeSelector()}</div>
<div className={cn("flex-1", "min-w-0", "overflow-hidden")}>
<SelectDropdown
<ApiConfigSelector
value={currentConfigId}
displayName={displayName}
disabled={selectApiConfigDisabled}
title={t("chat:selectApiConfig")}
disableSearch={false}
placeholder={displayName}
options={getApiConfigOptions}
onChange={handleApiConfigChange}
triggerClassName="w-full text-ellipsis overflow-hidden"
itemClassName="group"
renderItem={renderApiConfigItem}
listApiConfigMeta={listApiConfigMeta || []}
pinnedApiConfigs={pinnedApiConfigs}
togglePinnedApiConfig={togglePinnedApiConfig}
/>
</div>
</div>

View file

@ -0,0 +1,418 @@
import React from "react"
import { render, screen, fireEvent, waitFor } from "@/utils/test-utils"
import { describe, test, expect, vi, beforeEach } from "vitest"
import { ApiConfigSelector } from "../ApiConfigSelector"
import { vscode } from "@/utils/vscode"
// Mock the dependencies
vi.mock("@/utils/vscode", () => ({
vscode: {
postMessage: vi.fn(),
},
}))
vi.mock("@/i18n/TranslationContext", () => ({
useAppTranslation: () => ({
t: (key: string) => key,
}),
}))
vi.mock("@/components/ui/hooks/useRooPortal", () => ({
useRooPortal: () => document.body,
}))
// Mock Popover components to be testable
vi.mock("@/components/ui", () => ({
Popover: ({ children, open }: any) => (
<div data-testid="popover-root" data-open={open}>
{children}
</div>
),
PopoverTrigger: ({ children, disabled, ...props }: any) => (
<button data-testid="dropdown-trigger" disabled={disabled} onClick={() => props.onClick?.()} {...props}>
{children}
</button>
),
PopoverContent: ({ children }: any) => <div data-testid="popover-content">{children}</div>,
StandardTooltip: ({ children }: any) => <>{children}</>,
Button: ({ children, onClick, ...props }: any) => (
<button onClick={onClick} {...props}>
{children}
</button>
),
}))
describe("ApiConfigSelector", () => {
const mockOnChange = vi.fn()
const mockTogglePinnedApiConfig = vi.fn()
const defaultProps = {
value: "config1",
displayName: "Config 1",
onChange: mockOnChange,
listApiConfigMeta: [
{ id: "config1", name: "Config 1" },
{ id: "config2", name: "Config 2" },
{ id: "config3", name: "Config 3" },
],
pinnedApiConfigs: { config1: true },
togglePinnedApiConfig: mockTogglePinnedApiConfig,
}
beforeEach(() => {
vi.clearAllMocks()
})
test("renders correctly with default props", () => {
render(<ApiConfigSelector {...defaultProps} />)
const trigger = screen.getByTestId("dropdown-trigger")
expect(trigger).toBeInTheDocument()
expect(trigger).toHaveTextContent("Config 1")
})
test("renders with ChevronUp icon", () => {
render(<ApiConfigSelector {...defaultProps} />)
const trigger = screen.getByTestId("dropdown-trigger")
// Check for the icon by looking for the codicon span element
const icon = trigger.querySelector(".codicon-chevron-up")
expect(icon).toBeInTheDocument()
})
test("handles disabled state correctly", () => {
render(<ApiConfigSelector {...defaultProps} disabled={true} />)
const trigger = screen.getByTestId("dropdown-trigger")
expect(trigger).toBeDisabled()
})
test("renders with custom title tooltip", () => {
const customTitle = "Custom tooltip text"
render(<ApiConfigSelector {...defaultProps} title={customTitle} />)
// The component should render with the tooltip wrapper
const trigger = screen.getByTestId("dropdown-trigger")
expect(trigger).toBeInTheDocument()
})
test("applies custom trigger className", () => {
const customClass = "custom-trigger-class"
render(<ApiConfigSelector {...defaultProps} triggerClassName={customClass} />)
const trigger = screen.getByTestId("dropdown-trigger")
expect(trigger.className).toContain(customClass)
})
test("opens popover when trigger is clicked", () => {
render(<ApiConfigSelector {...defaultProps} />)
const trigger = screen.getByTestId("dropdown-trigger")
fireEvent.click(trigger)
// Check if popover content is rendered
const popoverContent = screen.getByTestId("popover-content")
expect(popoverContent).toBeInTheDocument()
})
test("renders search input when popover is open and more than 6 configs", () => {
const props = {
...defaultProps,
listApiConfigMeta: [
{ id: "config1", name: "Config 1" },
{ id: "config2", name: "Config 2" },
{ id: "config3", name: "Config 3" },
{ id: "config4", name: "Config 4" },
{ id: "config5", name: "Config 5" },
{ id: "config6", name: "Config 6" },
{ id: "config7", name: "Config 7" },
],
}
render(<ApiConfigSelector {...props} />)
const trigger = screen.getByTestId("dropdown-trigger")
fireEvent.click(trigger)
const searchInput = screen.getByPlaceholderText("common:ui.search_placeholder")
expect(searchInput).toBeInTheDocument()
})
test("renders info blurb instead of search when 6 or fewer configs", () => {
render(<ApiConfigSelector {...defaultProps} />)
const trigger = screen.getByTestId("dropdown-trigger")
fireEvent.click(trigger)
// Should not have search input
expect(screen.queryByPlaceholderText("common:ui.search_placeholder")).not.toBeInTheDocument()
// Should have info blurb
expect(screen.getByText("prompts:apiConfiguration.select")).toBeInTheDocument()
})
test("filters configs based on search input", async () => {
const props = {
...defaultProps,
listApiConfigMeta: [
{ id: "config1", name: "Config 1" },
{ id: "config2", name: "Config 2" },
{ id: "config3", name: "Config 3" },
{ id: "config4", name: "Config 4" },
{ id: "config5", name: "Config 5" },
{ id: "config6", name: "Config 6" },
{ id: "config7", name: "Config 7" },
],
}
render(<ApiConfigSelector {...props} />)
const trigger = screen.getByTestId("dropdown-trigger")
fireEvent.click(trigger)
const searchInput = screen.getByPlaceholderText("common:ui.search_placeholder")
fireEvent.change(searchInput, { target: { value: "Config 2" } })
// Wait for the filtering to take effect
await waitFor(() => {
// Config 2 should be visible
expect(screen.getByText("Config 2")).toBeInTheDocument()
// Config 3 should not be visible (assuming exact match filtering)
expect(screen.queryByText("Config 3")).not.toBeInTheDocument()
})
})
test("shows no results message when search has no matches", async () => {
const props = {
...defaultProps,
listApiConfigMeta: [
{ id: "config1", name: "Config 1" },
{ id: "config2", name: "Config 2" },
{ id: "config3", name: "Config 3" },
{ id: "config4", name: "Config 4" },
{ id: "config5", name: "Config 5" },
{ id: "config6", name: "Config 6" },
{ id: "config7", name: "Config 7" },
],
}
render(<ApiConfigSelector {...props} />)
const trigger = screen.getByTestId("dropdown-trigger")
fireEvent.click(trigger)
const searchInput = screen.getByPlaceholderText("common:ui.search_placeholder")
fireEvent.change(searchInput, { target: { value: "NonExistentConfig" } })
await waitFor(() => {
expect(screen.getByText("common:ui.no_results")).toBeInTheDocument()
})
})
test("clears search when X button is clicked", async () => {
const props = {
...defaultProps,
listApiConfigMeta: [
{ id: "config1", name: "Config 1" },
{ id: "config2", name: "Config 2" },
{ id: "config3", name: "Config 3" },
{ id: "config4", name: "Config 4" },
{ id: "config5", name: "Config 5" },
{ id: "config6", name: "Config 6" },
{ id: "config7", name: "Config 7" },
],
}
render(<ApiConfigSelector {...props} />)
const trigger = screen.getByTestId("dropdown-trigger")
fireEvent.click(trigger)
const searchInput = screen.getByPlaceholderText("common:ui.search_placeholder") as HTMLInputElement
fireEvent.change(searchInput, { target: { value: "test" } })
expect(searchInput.value).toBe("test")
// Find and click the X button
const clearButton = screen.getByTestId("popover-content").querySelector(".cursor-pointer")
if (clearButton) {
fireEvent.click(clearButton)
}
await waitFor(() => {
expect(searchInput.value).toBe("")
})
})
test("calls onChange when a config is selected", () => {
render(<ApiConfigSelector {...defaultProps} />)
const trigger = screen.getByTestId("dropdown-trigger")
fireEvent.click(trigger)
const config2 = screen.getByText("Config 2")
fireEvent.click(config2)
expect(mockOnChange).toHaveBeenCalledWith("config2")
})
test("shows check mark for selected config", () => {
render(<ApiConfigSelector {...defaultProps} />)
const trigger = screen.getByTestId("dropdown-trigger")
fireEvent.click(trigger)
// The selected config (config1) should have a check mark
// Use getAllByText since there might be multiple elements with "Config 1"
const config1Elements = screen.getAllByText("Config 1")
// Find the one that's in the dropdown content (not the trigger)
const configInDropdown = config1Elements.find((el) => el.closest('[data-testid="popover-content"]'))
const selectedConfigRow = configInDropdown?.closest("div")
const checkIcon = selectedConfigRow?.querySelector(".codicon-check")
expect(checkIcon).toBeInTheDocument()
})
test("separates pinned and unpinned configs", () => {
const props = {
...defaultProps,
pinnedApiConfigs: { config1: true, config3: true },
}
render(<ApiConfigSelector {...props} />)
const trigger = screen.getByTestId("dropdown-trigger")
fireEvent.click(trigger)
const content = screen.getByTestId("popover-content")
const configTexts = content.querySelectorAll(".truncate")
// Pinned configs should appear first
expect(configTexts[0]).toHaveTextContent("Config 1")
expect(configTexts[1]).toHaveTextContent("Config 3")
// Unpinned config should appear after separator
expect(configTexts[2]).toHaveTextContent("Config 2")
})
test("toggles pin status when pin button is clicked", () => {
render(<ApiConfigSelector {...defaultProps} />)
const trigger = screen.getByTestId("dropdown-trigger")
fireEvent.click(trigger)
// Find the pin button for Config 2 (unpinned)
const config2Row = screen.getByText("Config 2").closest("div")
const pinButton = config2Row?.querySelector("button")
if (pinButton) {
fireEvent.click(pinButton)
}
expect(mockTogglePinnedApiConfig).toHaveBeenCalledWith("config2")
expect(vi.mocked(vscode.postMessage)).toHaveBeenCalledWith({
type: "toggleApiConfigPin",
text: "config2",
})
})
test("opens settings when edit button is clicked", () => {
render(<ApiConfigSelector {...defaultProps} />)
const trigger = screen.getByTestId("dropdown-trigger")
fireEvent.click(trigger)
// Find the settings button by its icon class within the popover content
const popoverContent = screen.getByTestId("popover-content")
const settingsButton = popoverContent.querySelector('[aria-label="chat:edit"]') as HTMLElement
expect(settingsButton).toBeInTheDocument()
fireEvent.click(settingsButton)
expect(vi.mocked(vscode.postMessage)).toHaveBeenCalledWith({
type: "switchTab",
tab: "settings",
})
})
test("renders bottom bar with title and info icon when more than 6 configs", () => {
const props = {
...defaultProps,
listApiConfigMeta: [
{ id: "config1", name: "Config 1" },
{ id: "config2", name: "Config 2" },
{ id: "config3", name: "Config 3" },
{ id: "config4", name: "Config 4" },
{ id: "config5", name: "Config 5" },
{ id: "config6", name: "Config 6" },
{ id: "config7", name: "Config 7" },
],
}
render(<ApiConfigSelector {...props} />)
const trigger = screen.getByTestId("dropdown-trigger")
fireEvent.click(trigger)
// Check for the title
expect(screen.getByText("prompts:apiConfiguration.title")).toBeInTheDocument()
// Check for the info icon
const infoIcon = screen.getByTestId("popover-content").querySelector(".codicon-info")
expect(infoIcon).toBeInTheDocument()
})
test("renders bottom bar with title but no info icon when 6 or fewer configs", () => {
render(<ApiConfigSelector {...defaultProps} />)
const trigger = screen.getByTestId("dropdown-trigger")
fireEvent.click(trigger)
// Check for the title
expect(screen.getByText("prompts:apiConfiguration.title")).toBeInTheDocument()
// Check that info icon is not present
const infoIcon = screen.getByTestId("popover-content").querySelector(".codicon-info")
expect(infoIcon).not.toBeInTheDocument()
})
test("handles empty config list gracefully", () => {
const props = {
...defaultProps,
listApiConfigMeta: [],
}
render(<ApiConfigSelector {...props} />)
const trigger = screen.getByTestId("dropdown-trigger")
fireEvent.click(trigger)
// Should render info blurb instead of search for empty list
expect(screen.queryByPlaceholderText("common:ui.search_placeholder")).not.toBeInTheDocument()
expect(screen.getByText("prompts:apiConfiguration.select")).toBeInTheDocument()
expect(screen.getByText("prompts:apiConfiguration.title")).toBeInTheDocument()
})
test("maintains search value when pinning/unpinning", async () => {
const props = {
...defaultProps,
listApiConfigMeta: [
{ id: "config1", name: "Config 1" },
{ id: "config2", name: "Config 2" },
{ id: "config3", name: "Config 3" },
{ id: "config4", name: "Config 4" },
{ id: "config5", name: "Config 5" },
{ id: "config6", name: "Config 6" },
{ id: "config7", name: "Config 7" },
],
}
render(<ApiConfigSelector {...props} />)
const trigger = screen.getByTestId("dropdown-trigger")
fireEvent.click(trigger)
const searchInput = screen.getByPlaceholderText("common:ui.search_placeholder") as HTMLInputElement
fireEvent.change(searchInput, { target: { value: "Config" } })
// Pin a config
const config2Row = screen.getByText("Config 2").closest("div")
const pinButton = config2Row?.querySelector("button")
if (pinButton) {
fireEvent.click(pinButton)
}
// Search value should be maintained
expect(searchInput.value).toBe("Config")
})
})

View file

@ -20,7 +20,8 @@
"billion_suffix": "b"
},
"ui": {
"search_placeholder": "Cerca..."
"search_placeholder": "Cerca...",
"no_results": "No s'han trobat resultats"
},
"mermaid": {
"loading": "Generant diagrama mermaid...",

View file

@ -20,7 +20,8 @@
"billion_suffix": "b"
},
"ui": {
"search_placeholder": "Suchen..."
"search_placeholder": "Suchen...",
"no_results": "Keine Ergebnisse gefunden"
},
"mermaid": {
"loading": "Mermaid-Diagramm wird generiert...",

View file

@ -20,7 +20,8 @@
"billion_suffix": "b"
},
"ui": {
"search_placeholder": "Search..."
"search_placeholder": "Search...",
"no_results": "No results found"
},
"mermaid": {
"loading": "Generating mermaid diagram...",

View file

@ -20,7 +20,8 @@
"billion_suffix": "b"
},
"ui": {
"search_placeholder": "Buscar..."
"search_placeholder": "Buscar...",
"no_results": "No se encontraron resultados"
},
"mermaid": {
"loading": "Generando diagrama mermaid...",

View file

@ -20,7 +20,8 @@
"billion_suffix": "b"
},
"ui": {
"search_placeholder": "Rechercher..."
"search_placeholder": "Rechercher...",
"no_results": "Aucun résultat trouvé"
},
"mermaid": {
"loading": "Génération du diagramme mermaid...",

View file

@ -20,7 +20,8 @@
"billion_suffix": "b"
},
"ui": {
"search_placeholder": "खोजें..."
"search_placeholder": "खोजें...",
"no_results": "कोई परिणाम नहीं मिला"
},
"mermaid": {
"loading": "मरमेड डायग्राम जनरेट हो रहा है...",

View file

@ -20,7 +20,8 @@
"billion_suffix": "m"
},
"ui": {
"search_placeholder": "Cari..."
"search_placeholder": "Cari...",
"no_results": "Tidak ada hasil yang ditemukan"
},
"mermaid": {
"loading": "Membuat diagram mermaid...",

View file

@ -20,7 +20,8 @@
"billion_suffix": "b"
},
"ui": {
"search_placeholder": "Cerca..."
"search_placeholder": "Cerca...",
"no_results": "Nessun risultato trovato"
},
"mermaid": {
"loading": "Generazione del diagramma mermaid...",

View file

@ -20,7 +20,8 @@
"billion_suffix": "b"
},
"ui": {
"search_placeholder": "検索..."
"search_placeholder": "検索...",
"no_results": "結果が見つかりません"
},
"mermaid": {
"loading": "Mermaidダイアグラムを生成中...",

View file

@ -20,7 +20,8 @@
"billion_suffix": "b"
},
"ui": {
"search_placeholder": "검색..."
"search_placeholder": "검색...",
"no_results": "결과를 찾을 수 없습니다"
},
"mermaid": {
"loading": "머메이드 다이어그램 생성 중...",

View file

@ -20,7 +20,8 @@
"billion_suffix": "mrd"
},
"ui": {
"search_placeholder": "Zoeken..."
"search_placeholder": "Zoeken...",
"no_results": "Geen resultaten gevonden"
},
"mermaid": {
"loading": "Mermaid-diagram genereren...",

View file

@ -20,7 +20,8 @@
"keep": "Zachowaj"
},
"ui": {
"search_placeholder": "Szukaj..."
"search_placeholder": "Szukaj...",
"no_results": "Nie znaleziono wyników"
},
"mermaid": {
"loading": "Generowanie diagramu mermaid...",

View file

@ -20,7 +20,8 @@
"keep": "Manter"
},
"ui": {
"search_placeholder": "Pesquisar..."
"search_placeholder": "Pesquisar...",
"no_results": "Nenhum resultado encontrado"
},
"mermaid": {
"loading": "Gerando diagrama mermaid...",

View file

@ -20,7 +20,8 @@
"keep": "Оставить"
},
"ui": {
"search_placeholder": "Поиск..."
"search_placeholder": "Поиск...",
"no_results": "Результатов не найдено"
},
"mermaid": {
"loading": "Создание диаграммы mermaid...",

View file

@ -20,7 +20,8 @@
"billion_suffix": "b"
},
"ui": {
"search_placeholder": "Ara..."
"search_placeholder": "Ara...",
"no_results": "Sonuç bulunamadı"
},
"mermaid": {
"loading": "Mermaid diyagramı oluşturuluyor...",

View file

@ -20,7 +20,8 @@
"billion_suffix": "b"
},
"ui": {
"search_placeholder": "Tìm kiếm..."
"search_placeholder": "Tìm kiếm...",
"no_results": "Không tìm thấy kết quả nào"
},
"mermaid": {
"loading": "Đang tạo biểu đồ mermaid...",

View file

@ -20,7 +20,8 @@
"billion_suffix": "b"
},
"ui": {
"search_placeholder": "搜索..."
"search_placeholder": "搜索...",
"no_results": "未找到结果"
},
"mermaid": {
"loading": "生成 Mermaid 图表中...",

View file

@ -20,7 +20,8 @@
"billion_suffix": "b"
},
"ui": {
"search_placeholder": "搜尋..."
"search_placeholder": "搜尋...",
"no_results": "找不到結果"
},
"mermaid": {
"loading": "產生 Mermaid 圖表中...",