feat(tickets): assignation de sprint en masse via multi-sélection
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 <noreply@anthropic.com>
This commit is contained in:
@ -74,6 +74,8 @@ export function TicketsPanel({ projectId, onOpen }: TicketsPanelProps) {
|
||||
const [showSprintPicker, setShowSprintPicker] = useState(false);
|
||||
const [selectedRefs, setSelectedRefs] = useState<Set<string>>(() => new Set());
|
||||
const [bulkStatus, setBulkStatus] = useState<TicketStatus | "">("");
|
||||
const [showBulkSprintPicker, setShowBulkSprintPicker] = useState(false);
|
||||
const [bulkNotice, setBulkNotice] = useState<string | null>(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 | "")}
|
||||
/>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
disabled={vm.busy}
|
||||
onClick={() => setShowBulkSprintPicker(true)}
|
||||
>
|
||||
Sprint
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="danger"
|
||||
@ -233,6 +264,15 @@ export function TicketsPanel({ projectId, onOpen }: TicketsPanelProps) {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{bulkNotice && (
|
||||
<p
|
||||
role="status"
|
||||
className="mb-3 rounded-md border border-border bg-raised/60 px-3 py-2 text-sm text-content"
|
||||
>
|
||||
{bulkNotice}
|
||||
</p>
|
||||
)}
|
||||
|
||||
{showCreate && (
|
||||
<form
|
||||
onSubmit={submitCreate}
|
||||
@ -419,6 +459,17 @@ export function TicketsPanel({ projectId, onOpen }: TicketsPanelProps) {
|
||||
onClose={() => setShowSprintPicker(false)}
|
||||
/>
|
||||
|
||||
<SprintPicker
|
||||
open={showBulkSprintPicker}
|
||||
projectId={projectId}
|
||||
selectedSprintId={null}
|
||||
title={`Sprint pour ${selectedVisibleRefs.length} ticket${
|
||||
selectedVisibleRefs.length > 1 ? "s" : ""
|
||||
}`}
|
||||
onSelect={(sprint) => void applyBulkSprint(sprint)}
|
||||
onClose={() => setShowBulkSprintPicker(false)}
|
||||
/>
|
||||
|
||||
{confirmBulkDelete && (
|
||||
<div
|
||||
role="dialog"
|
||||
|
||||
Reference in New Issue
Block a user