From f5007fcb1197dd95b4af3a3c55e4536a4b6eff77 Mon Sep 17 00:00:00 2001 From: Blomios Date: Wed, 29 Jul 2026 15:58:00 +0200 Subject: [PATCH] =?UTF-8?q?feat(tickets):=20assignation=20de=20sprint=20en?= =?UTF-8?q?=20masse=20via=20multi-s=C3=A9lection?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Permet de sélectionner plusieurs tickets dans TicketsPanel et de leur assigner un sprint en une seule action, au lieu d'un ticket à la fois. Co-Authored-By: Claude Sonnet 5 --- .../src/features/tickets/TicketsPanel.tsx | 51 ++++++++++++++ .../src/features/tickets/tickets.test.tsx | 70 +++++++++++++++++++ frontend/src/features/tickets/useTickets.ts | 55 +++++++++++++++ 3 files changed, 176 insertions(+) diff --git a/frontend/src/features/tickets/TicketsPanel.tsx b/frontend/src/features/tickets/TicketsPanel.tsx index 049beb1..fdb4a15 100644 --- a/frontend/src/features/tickets/TicketsPanel.tsx +++ b/frontend/src/features/tickets/TicketsPanel.tsx @@ -74,6 +74,8 @@ export function TicketsPanel({ projectId, onOpen }: TicketsPanelProps) { const [showSprintPicker, setShowSprintPicker] = useState(false); const [selectedRefs, setSelectedRefs] = useState>(() => new Set()); const [bulkStatus, setBulkStatus] = useState(""); + const [showBulkSprintPicker, setShowBulkSprintPicker] = useState(false); + const [bulkNotice, setBulkNotice] = useState(null); const [confirmBulkDelete, setConfirmBulkDelete] = useState(false); const items = vm.list?.items ?? []; @@ -126,6 +128,7 @@ export function TicketsPanel({ projectId, onOpen }: TicketsPanelProps) { } function toggleTicketSelection(ref: string, selected: boolean) { + setBulkNotice(null); setSelectedRefs((prev) => { const next = new Set(prev); if (selected) next.add(ref); @@ -135,6 +138,7 @@ export function TicketsPanel({ projectId, onOpen }: TicketsPanelProps) { } async function applyBulkStatus(nextStatus: TicketStatus | "") { + setBulkNotice(null); setBulkStatus(nextStatus); if (!nextStatus || selectedVisibleRefs.length === 0) return; const result = await vm.bulkUpdateStatus(selectedVisibleRefs, nextStatus); @@ -144,7 +148,26 @@ export function TicketsPanel({ projectId, onOpen }: TicketsPanelProps) { setBulkStatus(""); } + async function applyBulkSprint(sprint: Sprint | null) { + setBulkNotice(null); + const refs = selectedVisibleRefs; + if (refs.length === 0) return; + const result = await vm.bulkAssignSprint(refs, sprint?.id ?? null); + if (!result) return; + const failed = new Set(result.items.filter((i) => !i.ok).map((i) => i.ref)); + const successCount = result.items.length - failed.size; + setSelectedRefs(new Set(refs.filter((ref) => failed.has(ref)))); + setBulkNotice( + failed.size === 0 + ? `Sprint mis à jour pour ${successCount} ticket${ + successCount > 1 ? "s" : "" + }.` + : `Sprint mis à jour pour ${successCount}/${result.items.length} tickets. Les échecs restent sélectionnés.`, + ); + } + async function applyBulkDelete() { + setBulkNotice(null); const result = await vm.bulkDelete(selectedVisibleRefs); if (!result) return; const failed = new Set(result.items.filter((i) => !i.ok).map((i) => i.ref)); @@ -214,6 +237,14 @@ export function TicketsPanel({ projectId, onOpen }: TicketsPanelProps) { ]} onChange={(next) => void applyBulkStatus(next as TicketStatus | "")} /> +