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:
@ -22,6 +22,12 @@ export interface TicketDetailViewModel {
|
||||
error: string | null;
|
||||
/** Set when the last write lost an optimistic-concurrency race (F3). */
|
||||
conflict: boolean;
|
||||
/**
|
||||
* Set once the open ticket has been deleted — by this view or any other actor
|
||||
* (ticket #6). Driven by the `issueDeleted` domain event (single source of
|
||||
* truth); the surface reacts by closing itself.
|
||||
*/
|
||||
deleted: boolean;
|
||||
busy: boolean;
|
||||
/**
|
||||
* Monotonic counter bumped **only** when the ticket is (re)loaded from the
|
||||
@ -42,6 +48,12 @@ export interface TicketDetailViewModel {
|
||||
link: (targetRef: string, kind: TicketLinkKind) => Promise<boolean>;
|
||||
unlink: (targetRef: string, kind?: TicketLinkKind) => Promise<boolean>;
|
||||
assign: (agentId: string, assigned: boolean) => Promise<boolean>;
|
||||
/**
|
||||
* Deletes this ticket (ticket #6). Returns `true` on success. The removal from
|
||||
* lists and the closing of this surface flow from the resulting `issueDeleted`
|
||||
* event (see {@link TicketDetailViewModel.deleted}), not from local mutation.
|
||||
*/
|
||||
remove: () => Promise<boolean>;
|
||||
}
|
||||
|
||||
function describe(e: unknown): string {
|
||||
@ -69,6 +81,7 @@ export function useTicketDetail(
|
||||
const [ticket, setTicket] = useState<Ticket | null>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [conflict, setConflict] = useState(false);
|
||||
const [deleted, setDeleted] = useState(false);
|
||||
const [busy, setBusy] = useState(false);
|
||||
const [reloadCount, setReloadCount] = useState(0);
|
||||
|
||||
@ -101,6 +114,12 @@ export function useTicketDetail(
|
||||
let cancelled = false;
|
||||
void system
|
||||
.onDomainEvent((event) => {
|
||||
if (event.type === "issueDeleted" && event.issueRef === ref) {
|
||||
// The open ticket was deleted (here or elsewhere): don't refresh (it
|
||||
// would 404) — flag it so the surface closes (#6).
|
||||
setDeleted(true);
|
||||
return;
|
||||
}
|
||||
if (isTicketEvent(event) && event.issueRef === ref) void refresh();
|
||||
})
|
||||
.then((u) => {
|
||||
@ -186,10 +205,27 @@ export function useTicketDetail(
|
||||
[run, gateway, projectId, ref],
|
||||
);
|
||||
|
||||
const remove: TicketDetailViewModel["remove"] = useCallback(async () => {
|
||||
setBusy(true);
|
||||
setError(null);
|
||||
try {
|
||||
await gateway.delete(projectId, ref);
|
||||
// Closing/removal flows from the `issueDeleted` event (single source of
|
||||
// truth) — see the `deleted` flag set in the subscription above.
|
||||
return true;
|
||||
} catch (e) {
|
||||
setError(describe(e));
|
||||
return false;
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
}, [gateway, projectId, ref]);
|
||||
|
||||
return {
|
||||
ticket,
|
||||
error,
|
||||
conflict,
|
||||
deleted,
|
||||
busy,
|
||||
reloadCount,
|
||||
refresh,
|
||||
@ -198,5 +234,6 @@ export function useTicketDetail(
|
||||
link,
|
||||
unlink,
|
||||
assign,
|
||||
remove,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user