feat(tickets): attribution d'un sprint à la création via popup SprintPicker (#38)

Nouveau composant SprintPicker permettant de choisir un sprint lors de
la création d'un ticket depuis TicketsPanel ; export ajouté à l'index
des features tickets. Frontend-pur.

QA vert : tsc --noEmit exit 0, vitest 63 fichiers / 634 tests passés.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-12 20:03:41 +02:00
parent a3c0dd410a
commit 0039958b82
5 changed files with 563 additions and 5 deletions

View File

@ -14,6 +14,7 @@ import { Button, Input, Panel, Spinner, cn } from "@/shared";
import { useTickets } from "./useTickets";
import { useProjectAgents } from "./useProjectAgents";
import { SprintManager } from "./SprintManager";
import { SprintPicker } from "./SprintPicker";
import { TicketFacetsBar } from "./TicketFacetsBar";
import {
PriorityBadge,
@ -42,6 +43,10 @@ export function TicketsPanel({ projectId, onOpen }: TicketsPanelProps) {
const [showSprints, setShowSprints] = useState(false);
const [newTitle, setNewTitle] = useState("");
const [newPriority, setNewPriority] = useState<TicketPriority>("medium");
// Sprint chosen for the new ticket (`null` ⇒ "Sans sprint"), and whether its
// picker popup is open (#38).
const [newSprint, setNewSprint] = useState<Sprint | null>(null);
const [showSprintPicker, setShowSprintPicker] = useState(false);
const items = vm.list?.items ?? [];
@ -66,12 +71,18 @@ export function TicketsPanel({ projectId, onOpen }: TicketsPanelProps) {
title: newTitle.trim(),
priority: newPriority,
});
if (created) {
setNewTitle("");
setNewPriority("medium");
setShowCreate(false);
onOpen(created.ref);
if (!created) return; // create failed; `vm.error` already carries the reason.
// Two-step assignment (#38): the ticket exists either way. If the sprint
// assignment fails, the ticket is kept and `vm.assignSprint` surfaces the
// failure via `vm.error` — we never lose the created ticket.
if (newSprint) {
await vm.assignSprint(created.ref, newSprint.id);
}
setNewTitle("");
setNewPriority("medium");
setNewSprint(null);
setShowCreate(false);
onOpen(created.ref);
}
return (
@ -146,6 +157,22 @@ export function TicketsPanel({ projectId, onOpen }: TicketsPanelProps) {
Create
</Button>
</div>
{/* Sprint field (#38): opens the SprintPicker popup; shows the choice
(or "Sans sprint"). */}
<div className="flex items-center gap-2">
<span className="text-xs text-muted">Sprint</span>
<Button
type="button"
size="sm"
variant="ghost"
aria-label="choose sprint for new ticket"
disabled={vm.busy}
onClick={() => setShowSprintPicker(true)}
className="border border-border"
>
{newSprint ? newSprint.name : "Sans sprint"}
</Button>
</div>
</form>
)}
@ -252,6 +279,17 @@ export function TicketsPanel({ projectId, onOpen }: TicketsPanelProps) {
onClose={() => setShowSprints(false)}
/>
)}
{/* Sprint picker for the create form (#38) — chrome-level, nested z-index,
focus-trapped. Selecting an entry sets the pending sprint and closes. */}
<SprintPicker
open={showSprintPicker}
projectId={projectId}
selectedSprintId={newSprint?.id ?? null}
title="Sprint du nouveau ticket"
onSelect={(sprint) => setNewSprint(sprint)}
onClose={() => setShowSprintPicker(false)}
/>
</Panel>
);
}