diff --git a/frontend/src/features/tickets/TicketAssistantPanel.tsx b/frontend/src/features/tickets/TicketAssistantPanel.tsx index a6d9016..e9f5735 100644 --- a/frontend/src/features/tickets/TicketAssistantPanel.tsx +++ b/frontend/src/features/tickets/TicketAssistantPanel.tsx @@ -11,14 +11,9 @@ import { useEffect, useRef, useState } from "react"; import { Button, Spinner, cn } from "@/shared"; +import { TicketViewportSelect } from "./TicketViewportSelect"; import { useTicketAssistant } from "./useTicketAssistant"; -const selectClass = cn( - "h-9 rounded-md bg-raised px-3 text-sm text-content", - "border border-border outline-none transition-colors", - "focus:border-primary disabled:cursor-not-allowed disabled:opacity-50", -); - export interface TicketAssistantPanelProps { projectId: string; ticketRef: string; @@ -53,24 +48,25 @@ export function TicketAssistantPanel({ {!open ? ( // ── Session opener ──
- + options={[ + { + value: "", + label: + vm.profiles.length === 0 + ? "Aucun profil disponible" + : "Choisir un profil IA…", + }, + ...vm.profiles.map((profile) => ({ + value: profile.id, + label: profile.name, + })), + ]} + onChange={setProfileId} + />
@@ -426,19 +415,16 @@ export function TicketDetail({ {/* Add a link via the TicketPicker popup (#17): the kind is chosen here, the target ticket is selected in the popup (no manual #id). */}
- + options={TICKET_LINK_KINDS.map((kind) => ({ + value: kind, + label: linkKindLabel(kind), + }))} + onChange={(next) => setLinkKind(next as TicketLinkKind)} + /> + {open && + position && + createPortal( +
+ {options.map((option) => ( + + ))} +
, + document.body, + )} + + ); +} diff --git a/frontend/src/features/tickets/TicketsPanel.tsx b/frontend/src/features/tickets/TicketsPanel.tsx index b733b0d..cca7727 100644 --- a/frontend/src/features/tickets/TicketsPanel.tsx +++ b/frontend/src/features/tickets/TicketsPanel.tsx @@ -10,12 +10,13 @@ import { useEffect, useState } from "react"; import type { Sprint, TicketPriority, TicketSummary } from "@/domain"; -import { Button, Input, Panel, Spinner, cn } from "@/shared"; +import { Button, Input, Panel, Spinner } from "@/shared"; import { useTickets } from "./useTickets"; import { useProjectAgents } from "./useProjectAgents"; import { SprintManager } from "./SprintManager"; import { SprintPicker } from "./SprintPicker"; import { TicketFacetsBar } from "./TicketFacetsBar"; +import { TicketViewportSelect } from "./TicketViewportSelect"; import { PriorityBadge, StatusBadge, @@ -24,12 +25,6 @@ import { priorityLabel, } from "./ticketMeta"; -const selectClass = cn( - "h-8 rounded-md bg-raised px-2 text-xs text-content", - "border border-border outline-none transition-colors", - "focus:border-primary disabled:cursor-not-allowed disabled:opacity-50", -); - export interface TicketsPanelProps { projectId: string; /** Opens the detail overlay for the given `#ref` (F7). */ @@ -151,18 +146,15 @@ export function TicketsPanel({ projectId, onOpen }: TicketsPanelProps) { onChange={(e) => setNewTitle(e.target.value)} />
- + options={TICKET_PRIORITIES.map((priority) => ({ + value: priority, + label: priorityLabel(priority), + }))} + onChange={(next) => setNewPriority(next as TicketPriority)} + />
@@ -363,21 +354,20 @@ function SprintSection({
- + />
))} diff --git a/frontend/src/features/tickets/tickets.test.tsx b/frontend/src/features/tickets/tickets.test.tsx index cd4190f..ffad360 100644 --- a/frontend/src/features/tickets/tickets.test.tsx +++ b/frontend/src/features/tickets/tickets.test.tsx @@ -353,9 +353,8 @@ describe("TicketsView", () => { expect(listSpy.mock.calls.at(-1)?.[1]?.sort).toBeUndefined(); // Choose a field ⇒ ascending by default, relayed in the query. - fireEvent.change(screen.getByLabelText("sort tickets by"), { - target: { value: "title" }, - }); + fireEvent.click(screen.getByLabelText("sort tickets by")); + fireEvent.click(await screen.findByRole("option", { name: "Titre" })); await waitFor(() => expect(listSpy.mock.calls.at(-1)?.[1]?.sort).toEqual({ field: "title", @@ -375,9 +374,8 @@ describe("TicketsView", () => { ); // Back to « Par défaut » ⇒ `sort` dropped from the query again. - fireEvent.change(screen.getByLabelText("sort tickets by"), { - target: { value: "" }, - }); + fireEvent.click(screen.getByLabelText("sort tickets by")); + fireEvent.click(await screen.findByRole("option", { name: "Par défaut" })); await waitFor(() => expect(listSpy.mock.calls.at(-1)?.[1]?.sort).toBeUndefined(), ); @@ -390,8 +388,8 @@ describe("TicketsView", () => { fireEvent.click(await screen.findByText("Editable")); const dialog = await screen.findByRole("dialog"); - const statusSelect = within(dialog).getByLabelText("ticket status"); - fireEvent.change(statusSelect, { target: { value: "inProgress" } }); + fireEvent.click(within(dialog).getByLabelText("ticket status")); + fireEvent.click(await screen.findByRole("option", { name: "In progress" })); await waitFor(async () => { const fresh = await ticket.read(PROJECT_ID, t.ref); @@ -426,9 +424,8 @@ describe("TicketsView", () => { fireEvent.click(await screen.findByText("Assignable")); const dialog = await screen.findByRole("dialog"); - fireEvent.change(within(dialog).getByLabelText("assign agent"), { - target: { value: known.id }, - }); + fireEvent.click(within(dialog).getByLabelText("assign agent")); + fireEvent.click(await screen.findByRole("option", { name: "Backend" })); fireEvent.click(within(dialog).getByText("Assign")); await waitFor(async () => { @@ -495,9 +492,8 @@ describe("TicketsView", () => { // Change the priority — an immediate-apply mutation that re-fetches the ticket // and bumps its version. The unsaved draft must survive. - fireEvent.change(within(dialog).getByLabelText("ticket priority"), { - target: { value: "high" }, - }); + fireEvent.click(within(dialog).getByLabelText("ticket priority")); + fireEvent.click(await screen.findByRole("option", { name: "High" })); // The backend applied the priority bump… await waitFor(async () => { @@ -643,9 +639,8 @@ describe("TicketsView", () => { expect(within(bucket).getByText("Movable")).toBeTruthy(); // Pick the sprint in the row selector → assign it. - fireEvent.change(screen.getByLabelText(`sprint for ${t.ref}`), { - target: { value: "s1" }, - }); + fireEvent.click(screen.getByLabelText(`sprint for ${t.ref}`)); + fireEvent.click(await screen.findByRole("option", { name: "Sprint One" })); // The gateway recorded the membership… await waitFor(async () => { @@ -777,7 +772,8 @@ describe("TicketsView", () => { ); expect(within(assistant).getByText("Ouvrir la conversation")).toBeTruthy(); - fireEvent.change(profileSelect, { target: { value: "qa-assistant" } }); + fireEvent.click(profileSelect); + fireEvent.click(await screen.findByRole("option", { name: "QA Assistant" })); fireEvent.click(within(assistant).getByText("Ouvrir la conversation")); expect( @@ -837,9 +833,8 @@ describe("TicketsView", () => { expect(within(detail).queryByLabelText("link target ref")).toBeNull(); // Choose the link kind, then open the picker and select the target ticket. - fireEvent.change(within(detail).getByLabelText("link kind"), { - target: { value: "blocks" }, - }); + fireEvent.click(within(detail).getByLabelText("link kind")); + fireEvent.click(await screen.findByRole("option", { name: "blocks" })); fireEvent.click(within(detail).getByLabelText("add link")); // The picker excludes the ticket itself; the target is offered.