diff --git a/frontend/src/features/tickets/SprintManager.tsx b/frontend/src/features/tickets/SprintManager.tsx index 8de7c84..dd64e73 100644 --- a/frontend/src/features/tickets/SprintManager.tsx +++ b/frontend/src/features/tickets/SprintManager.tsx @@ -13,22 +13,18 @@ import { useState } from "react"; -import { Button, Input, cn } from "@/shared"; +import { Button, Input } from "@/shared"; import { TicketRef } from "./ticketMeta"; +import { TicketPicker } from "./TicketPicker"; import type { TicketsViewModel } from "./useTickets"; -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 SprintManagerProps { + projectId: string; vm: TicketsViewModel; onClose: () => void; } -export function SprintManager({ vm, onClose }: SprintManagerProps) { +export function SprintManager({ projectId, vm, onClose }: SprintManagerProps) { const items = vm.list?.items ?? []; const sprints = [...vm.sprints].sort((a, b) => a.order - b.order); @@ -39,6 +35,11 @@ export function SprintManager({ vm, onClose }: SprintManagerProps) { const [confirmDelete, setConfirmDelete] = useState< { id: string; name: string } | null >(null); + // The sprint whose "add ticket" TicketPicker is open, or null (#19). Held as + // {id,name} so the picker keeps rendering while the underlying list refreshes. + const [pickerSprint, setPickerSprint] = useState< + { id: string; name: string } | null + >(null); async function handleCreate(e: React.FormEvent) { e.preventDefault(); @@ -208,28 +209,23 @@ export function SprintManager({ vm, onClose }: SprintManagerProps) { )} - {/* Add an existing ticket (backlog or another sprint). */} - + : "Ajouter un ticket"} + + ); @@ -281,6 +277,29 @@ export function SprintManager({ vm, onClose }: SprintManagerProps) { )} + + {/* ── Add-ticket picker (#19) — mounts at floatingWindowNested (60), above + this sprint dialog. Excludes tickets already in the target sprint. ── */} + t.sprintId === pickerSprint?.id) + .map((t) => t.ref)} + onSelect={(result) => { + const picked = Array.isArray(result) ? result[0] : result; + if (picked && pickerSprint) { + void vm.assignSprint(picked.ref, pickerSprint.id); + } + }} + onClose={() => setPickerSprint(null)} + /> ); } diff --git a/frontend/src/features/tickets/TicketsPanel.tsx b/frontend/src/features/tickets/TicketsPanel.tsx index cd95fec..65b0d19 100644 --- a/frontend/src/features/tickets/TicketsPanel.tsx +++ b/frontend/src/features/tickets/TicketsPanel.tsx @@ -241,7 +241,11 @@ export function TicketsPanel({ projectId, onOpen }: TicketsPanelProps) { )} {showSprints && ( - setShowSprints(false)} /> + setShowSprints(false)} + /> )} ); diff --git a/frontend/src/features/tickets/tickets.test.tsx b/frontend/src/features/tickets/tickets.test.tsx index ae5ef44..10880ff 100644 --- a/frontend/src/features/tickets/tickets.test.tsx +++ b/frontend/src/features/tickets/tickets.test.tsx @@ -805,23 +805,46 @@ describe("SprintManager (#11)", () => { expect(fresh.title).toBe("Kept"); }); - it("adds a backlog ticket to a sprint via the add selector", async () => { + it("adds a backlog ticket to a sprint via the TicketPicker popup (#19)", async () => { ticket._seedSprint(PROJECT_ID, { id: "s1", order: 1, name: "Target" }); const t = await ticket.create(PROJECT_ID, { title: "Backlog item" }); const dialog = await openManager(); - fireEvent.change( + // Open the add-ticket picker for the Target sprint. + fireEvent.click( await within(dialog).findByLabelText("add ticket to sprint Target"), - { target: { value: t.ref } }, ); + // The picker popup lists the backlog ticket — select it. + fireEvent.click(await screen.findByLabelText(`select ticket ${t.ref}`)); + await waitFor(async () => { const fresh = await ticket.read(PROJECT_ID, t.ref); expect(fresh.sprintId).toBe("s1"); }); }); + it("excludes tickets already in the sprint from the add picker (#19)", async () => { + ticket._seedSprint(PROJECT_ID, { id: "s1", order: 1, name: "Target" }); + const member = await ticket.create(PROJECT_ID, { title: "Already in" }); + await ticket.setTicketSprint(PROJECT_ID, member.ref, "s1", member.version); + const backlog = await ticket.create(PROJECT_ID, { title: "Still backlog" }); + + const dialog = await openManager(); + fireEvent.click( + await within(dialog).findByLabelText("add ticket to sprint Target"), + ); + + // The backlog ticket is offered; the one already in the sprint is excluded. + expect( + await screen.findByLabelText(`select ticket ${backlog.ref}`), + ).toBeTruthy(); + expect( + screen.queryByLabelText(`select ticket ${member.ref}`), + ).toBeNull(); + }); + it("removes a ticket from a sprint", async () => { ticket._seedSprint(PROJECT_ID, { id: "s1", order: 1, name: "Holder" }); const t = await ticket.create(PROJECT_ID, { title: "Member" });