feat(ui): uniformise les droplist dynamiques sur le style du picker d'agent

Introduit SmallDropdown comme composant partagé et le fait adopter par
AgentsPanel, ModelServerSelect, OpenCodeModeFields, TemplateEditor et
TicketViewportSelect, pour que toutes les listes déroulantes dynamiques
(agent, modèle, template, viewport) partagent la même apparence que le
sélecteur d'agent de la création de ticket.

Refs #114

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-30 12:55:57 +02:00
parent fda4126a5f
commit 9b1ced50be
14 changed files with 550 additions and 360 deletions

View File

@ -25,6 +25,11 @@ import { DIProvider } from "@/app/di";
import { FirstRunWizard } from "./FirstRunWizard";
import { DETECT_TIMEOUT_MS } from "./useFirstRun";
function chooseDropdownOption(label: string | RegExp, optionName: string | RegExp) {
fireEvent.click(screen.getByLabelText(label));
fireEvent.click(screen.getByRole("option", { name: optionName }));
}
function renderWizard(
profile: MockProfileGateway = new MockProfileGateway(),
onDone = vi.fn(),
@ -474,11 +479,11 @@ describe("FirstRunWizard — OpenCode cloud provider (ticket #92)", () => {
await waitForLoaded();
fireEvent.click(screen.getByRole("radio", { name: "Provider cloud" }));
const providerSelect = await screen.findByLabelText(`${OPENCODE} provider`);
const modelSelect = screen.getByLabelText(`${OPENCODE} model`) as HTMLSelectElement;
await screen.findByLabelText(`${OPENCODE} provider`);
const modelSelect = screen.getByLabelText(`${OPENCODE} model`) as HTMLButtonElement;
expect(modelSelect.disabled).toBe(true);
fireEvent.change(providerSelect, { target: { value: "anthropic" } });
chooseDropdownOption(`${OPENCODE} provider`, "Anthropic");
expect(modelSelect.disabled).toBe(false);
const saveButton = screen.getByRole("button", {
@ -486,7 +491,7 @@ describe("FirstRunWizard — OpenCode cloud provider (ticket #92)", () => {
}) as HTMLButtonElement;
expect(saveButton.disabled).toBe(true);
fireEvent.change(modelSelect, { target: { value: "claude-sonnet-5" } });
chooseDropdownOption(`${OPENCODE} model`, "claude-sonnet-5");
expect(saveButton.disabled).toBe(true);
fireEvent.change(screen.getByLabelText(`${OPENCODE} api key`), {
@ -500,12 +505,9 @@ describe("FirstRunWizard — OpenCode cloud provider (ticket #92)", () => {
await waitForLoaded();
fireEvent.click(screen.getByRole("radio", { name: "Provider cloud" }));
fireEvent.change(await screen.findByLabelText(`${OPENCODE} provider`), {
target: { value: "anthropic" },
});
fireEvent.change(screen.getByLabelText(`${OPENCODE} model`), {
target: { value: "claude-sonnet-5" },
});
await screen.findByLabelText(`${OPENCODE} provider`);
chooseDropdownOption(`${OPENCODE} provider`, "Anthropic");
chooseDropdownOption(`${OPENCODE} model`, "claude-sonnet-5");
const apiKeyInput = screen.getByLabelText(
`${OPENCODE} api key`,
) as HTMLInputElement;
@ -547,8 +549,8 @@ describe("FirstRunWizard — OpenCode custom cloud provider (ticket #92, dynamic
async function goToCloudCustomMode() {
fireEvent.click(screen.getByRole("radio", { name: "Provider cloud" }));
const providerSelect = await screen.findByLabelText(`${OPENCODE} provider`);
fireEvent.change(providerSelect, { target: { value: "__custom__" } });
await screen.findByLabelText(`${OPENCODE} provider`);
chooseDropdownOption(`${OPENCODE} provider`, "Autre / personnalisé…");
}
it("selecting 'Autre / personnalisé' swaps the cascade for free-form fields", async () => {
@ -904,8 +906,8 @@ describe("FirstRunWizard — several local OpenCode profiles (F36)", () => {
expect(
screen.queryByLabelText(`${CLONE1} local model server id`),
).toBeNull();
const select = await screen.findByLabelText(`${CLONE1} local model server`);
fireEvent.change(select, { target: { value: server.id } });
await screen.findByLabelText(`${CLONE1} local model server`);
chooseDropdownOption(`${CLONE1} local model server`, /Local A/);
fireEvent.click(screen.getByRole("button", { name: "Save and continue" }));
await waitFor(async () => {