Skip to content

Commit 638e912

Browse files
committed
fix(ui): persist API config custom order on sort; add onSortModeChange/onCustomOrderChange; fix double-selection keydown in settings; add tests
1 parent 0d95ce0 commit 638e912

File tree

3 files changed

+107
-48
lines changed

3 files changed

+107
-48
lines changed

webview-ui/src/components/chat/ApiConfigSelector.tsx

Lines changed: 79 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,8 @@ interface ApiConfigSelectorProps {
100100
listApiConfigMeta: Array<{ id: string; name: string; modelId?: string }>
101101
pinnedApiConfigs?: Record<string, boolean>
102102
togglePinnedApiConfig: (id: string) => void
103+
onSortModeChange?: (mode: SortMode) => void
104+
onCustomOrderChange?: (order: Array<{ id: string; index: number; pinned: boolean }>) => void
103105
}
104106

105107
export const ApiConfigSelector = ({
@@ -112,6 +114,8 @@ export const ApiConfigSelector = ({
112114
listApiConfigMeta,
113115
pinnedApiConfigs,
114116
togglePinnedApiConfig,
117+
onSortModeChange,
118+
onCustomOrderChange,
115119
}: ApiConfigSelectorProps) => {
116120
const { t } = useAppTranslation()
117121
const { apiConfigsCustomOrder: customOrder = [] } = useExtensionState()
@@ -146,6 +150,17 @@ export const ApiConfigSelector = ({
146150
return sorted
147151
}, [listApiConfigMeta, sortMode, customOrder])
148152

153+
// Current visible order for callbacks when switching to custom
154+
const currentOrder = useMemo(
155+
() =>
156+
sortedConfigs.map((config, index) => ({
157+
id: config.id,
158+
index,
159+
pinned: Boolean(pinnedApiConfigs?.[config.id]),
160+
})),
161+
[sortedConfigs, pinnedApiConfigs],
162+
)
163+
149164
// Filter configs based on search.
150165
const filteredConfigs = useMemo(() => {
151166
if (!searchValue) {
@@ -178,6 +193,22 @@ export const ApiConfigSelector = ({
178193
setOpen(false)
179194
}, [])
180195

196+
const handleSortModeChange = useCallback(
197+
(mode: SortMode) => {
198+
setSortMode(mode)
199+
onSortModeChange?.(mode)
200+
if (mode === "custom") {
201+
// Persist current visible order as custom order baseline
202+
vscode.postMessage({
203+
type: "setApiConfigsCustomOrder",
204+
values: { customOrder: currentOrder },
205+
})
206+
onCustomOrderChange?.(currentOrder)
207+
}
208+
},
209+
[onSortModeChange, onCustomOrderChange, currentOrder],
210+
)
211+
181212
return (
182213
<Popover open={open} onOpenChange={setOpen} data-testid="api-config-selector-root">
183214
<StandardTooltip content={title}>
@@ -230,54 +261,54 @@ export const ApiConfigSelector = ({
230261
</div>
231262
)}
232263

233-
{/* Config list - single scroll container with a11y attributes and sticky pinned structure */}
234-
{filteredConfigs.length === 0 && searchValue ? (
235-
<div className="py-2 px-3 text-sm text-vscode-foreground/70">{t("common:ui.no_results")}</div>
236-
) : (
237-
<div
238-
className="max-h-[300px] overflow-y-auto"
239-
role="listbox"
240-
aria-label={t("prompts:apiConfiguration.select")}>
241-
{/* Pinned configs - sticky header */}
242-
{pinnedConfigs.length > 0 && (
243-
<div
244-
className={cn(
245-
"sticky top-0 z-10 bg-vscode-dropdown-background py-1",
246-
unpinnedConfigs.length > 0 && "border-b border-vscode-dropdown-foreground/10",
247-
)}
248-
aria-label="Pinned configurations">
249-
{pinnedConfigs.map((config, index) => (
250-
<ConfigItem
251-
key={config.id}
252-
config={config}
253-
isPinned
254-
index={index}
255-
value={value}
256-
onSelect={handleSelect}
257-
togglePinnedApiConfig={togglePinnedApiConfig}
258-
/>
259-
))}
260-
</div>
261-
)}
264+
{/* Config list - single scroll container with a11y attributes and sticky pinned structure */}
265+
{filteredConfigs.length === 0 && searchValue ? (
266+
<div className="py-2 px-3 text-sm text-vscode-foreground/70">{t("common:ui.no_results")}</div>
267+
) : (
268+
<div
269+
className="max-h-[300px] overflow-y-auto"
270+
role="listbox"
271+
aria-label={t("prompts:apiConfiguration.select")}>
272+
{/* Pinned configs - sticky header */}
273+
{pinnedConfigs.length > 0 && (
274+
<div
275+
className={cn(
276+
"sticky top-0 z-10 bg-vscode-dropdown-background py-1",
277+
unpinnedConfigs.length > 0 && "border-b border-vscode-dropdown-foreground/10",
278+
)}
279+
aria-label="Pinned configurations">
280+
{pinnedConfigs.map((config, index) => (
281+
<ConfigItem
282+
key={config.id}
283+
config={config}
284+
isPinned
285+
index={index}
286+
value={value}
287+
onSelect={handleSelect}
288+
togglePinnedApiConfig={togglePinnedApiConfig}
289+
/>
290+
))}
291+
</div>
292+
)}
262293

263-
{/* Unpinned configs */}
264-
{unpinnedConfigs.length > 0 && (
265-
<div className="py-1" aria-label="All configurations">
266-
{unpinnedConfigs.map((config, index) => (
267-
<ConfigItem
268-
key={config.id}
269-
config={config}
270-
isPinned={false}
271-
index={pinnedConfigs.length + index}
272-
value={value}
273-
onSelect={handleSelect}
274-
togglePinnedApiConfig={togglePinnedApiConfig}
275-
/>
276-
))}
277-
</div>
278-
)}
279-
</div>
280-
)}
294+
{/* Unpinned configs */}
295+
{unpinnedConfigs.length > 0 && (
296+
<div className="py-1" aria-label="All configurations">
297+
{unpinnedConfigs.map((config, index) => (
298+
<ConfigItem
299+
key={config.id}
300+
config={config}
301+
isPinned={false}
302+
index={pinnedConfigs.length + index}
303+
value={value}
304+
onSelect={handleSelect}
305+
togglePinnedApiConfig={togglePinnedApiConfig}
306+
/>
307+
))}
308+
</div>
309+
)}
310+
</div>
311+
)}
281312

282313
{/* Bottom bar with controls */}
283314
<div className="flex flex-col border-t border-vscode-dropdown-border">
@@ -295,7 +326,7 @@ export const ApiConfigSelector = ({
295326
size="sm"
296327
aria-label={`${t("chat:apiConfigSelector.sort")} ${mode === "alphabetical" ? t("chat:apiConfigSelector.alphabetical") : t("chat:apiConfigSelector.custom")}`}
297328
aria-pressed={sortMode === mode}
298-
onClick={() => setSortMode(mode)}
329+
onClick={() => handleSortModeChange(mode)}
299330
className={cn(
300331
"h-6 px-2 text-xs",
301332
sortMode === mode &&

webview-ui/src/components/chat/__tests__/ApiConfigSelector.spec.tsx

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -436,6 +436,33 @@ describe("ApiConfigSelector", () => {
436436
expect(searchInput.value).toBe("Config")
437437
})
438438

439+
test("calls onSortModeChange when switching sort mode", () => {
440+
const mockOnSortModeChange = vi.fn()
441+
render(<ApiConfigSelector {...defaultProps} onSortModeChange={mockOnSortModeChange} />)
442+
443+
const trigger = screen.getByTestId("dropdown-trigger")
444+
fireEvent.click(trigger)
445+
446+
const customButton = screen.getByText("chat:apiConfigSelector.custom")
447+
fireEvent.click(customButton)
448+
449+
expect(mockOnSortModeChange).toHaveBeenCalledWith("custom")
450+
})
451+
452+
test("persists custom order when switching to custom sort", () => {
453+
render(<ApiConfigSelector {...defaultProps} />)
454+
455+
const trigger = screen.getByTestId("dropdown-trigger")
456+
fireEvent.click(trigger)
457+
458+
const customButton = screen.getByText("chat:apiConfigSelector.custom")
459+
fireEvent.click(customButton)
460+
461+
expect(vi.mocked(vscode.postMessage)).toHaveBeenCalledWith(
462+
expect.objectContaining({ type: "setApiConfigsCustomOrder" }),
463+
)
464+
})
465+
439466
test("switches between sort modes", () => {
440467
render(<ApiConfigSelector {...defaultProps} />)
441468

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,6 +205,7 @@ const ConfigListItem = memo(function ConfigListItemComponent({
205205
onKeyDown(e, index)
206206
if (!isReorderingMode && (e.key === "Enter" || e.key === " ")) {
207207
e.preventDefault()
208+
e.stopPropagation()
208209
onSelectConfig(config.name)
209210
}
210211
}}

0 commit comments

Comments
 (0)