feat(tickets): suppression d'un ticket (backend + frontend)

Ajoute la suppression complète d'un ticket (#6).

Backend Rust :
- port IssueStore::delete (NotFound si absent) + event IssueDeleted{freed_sprint}
- FsIssueStore::delete : supprime .ideai/tickets/<N>/ et l'index sous lock
- use case DeleteIssue + adaptations des tests sprint/ticket_assistant au port
- commande Tauri ticket_delete + câblage events/state/lib

Frontend :
- gateway delete (ports, adapter ticket + mock, domain)
- useTicketDetail : retrait de la liste et fermeture du détail via event issueDeleted
- intégration TicketDetail + tests

Tests verts : application/issue_usecases (6), infrastructure/issue_store (7),
app-tauri --lib (56), frontend vitest (503), npm run build (exit 0).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-06 06:27:09 +02:00
parent d25e340b44
commit 5417bd756b
20 changed files with 689 additions and 95 deletions

View File

@ -104,6 +104,14 @@ export function TicketDetail({
const [copied, setCopied] = useState(false);
// Confirmation shown when the user tries to close with unsaved changes.
const [confirmClose, setConfirmClose] = useState(false);
// Confirmation shown before deleting the ticket (#6).
const [confirmDelete, setConfirmDelete] = useState(false);
// The ticket was deleted (by this view or another actor): close the surface.
// The removal from lists flows from the same `issueDeleted` event (#6).
useEffect(() => {
if (vm.deleted) onClose();
}, [vm.deleted, onClose]);
const reloadCount = vm.reloadCount;
const seededReload = useRef(-1);
@ -205,6 +213,16 @@ export function TicketDetail({
>
{copied ? "Copied" : "Delegation context"}
</Button>
<Button
size="sm"
variant="ghost"
disabled={!t || vm.busy}
onClick={() => setConfirmDelete(true)}
className="text-danger hover:text-danger"
title="Supprimer définitivement ce ticket"
>
Supprimer
</Button>
<Button size="sm" variant="ghost" onClick={requestClose}>
Close
</Button>
@ -559,6 +577,52 @@ export function TicketDetail({
</div>
</div>
)}
{/* ── Delete confirmation (#6) ── */}
{confirmDelete && (
<div
role="dialog"
aria-modal="true"
aria-label="confirmer la suppression"
className="fixed inset-0 z-[60] flex items-center justify-center bg-black/55 p-4"
>
<div className="w-full max-w-sm rounded-lg border border-border bg-surface p-5 shadow-xl">
<h4 className="text-sm font-semibold text-content">
Supprimer le ticket
</h4>
<p className="mt-2 text-sm text-muted">
Supprimer définitivement le ticket&nbsp;{ticketRef}&nbsp;? Cette
action est irréversible.
</p>
<div className="mt-4 flex flex-wrap items-center justify-end gap-2">
<Button
size="sm"
variant="ghost"
aria-label="cancel delete"
disabled={vm.busy}
onClick={() => setConfirmDelete(false)}
>
Annuler
</Button>
<Button
size="sm"
variant="danger"
aria-label="confirm delete"
loading={vm.busy}
disabled={vm.busy}
onClick={async () => {
const ok = await vm.remove();
// On success the surface closes via the `issueDeleted` event
// (the `deleted` flag → onClose effect); just drop the dialog.
if (ok) setConfirmDelete(false);
}}
>
Supprimer
</Button>
</div>
</div>
</div>
)}
</div>
);
}