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:
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user