feat(tickets): surface frontend V1 du système de tickets
Surface UI complète des tickets (domaine Issue exposé « ticket »), validée QA de bout en bout : cargo build + tests backend verts, npm run build + npm test (459) verts. - Domaine : DTO Ticket, 9 events Issue* + guard isTicketEvent. - Ports : TicketGateway (+ types query/input) ajouté à Gateways. - Adapters : TauriTicketGateway réel (+ isTicketVersionConflict), wiring, et MockTicketGateway pour les tests. - Feature tickets : hooks (useTickets, useTicketDetail, useProjectAgents), TicketsPanel, TicketDetail, TicketsView, ticketMeta + tests. - ProjectsView : onglet sidebar « Tickets ». Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
125
frontend/src/adapters/ticket.ts
Normal file
125
frontend/src/adapters/ticket.ts
Normal file
@ -0,0 +1,125 @@
|
||||
/**
|
||||
* Tauri adapter for {@link TicketGateway}.
|
||||
*
|
||||
* The single frontend owner of the `ticket_*` command names and their request
|
||||
* envelopes (every command takes one camelCase `request` object). Features
|
||||
* consume the gateway port through DI and never call `invoke()` directly.
|
||||
*
|
||||
* Optimistic concurrency: the backend rejects a stale write with an `INVALID`
|
||||
* {@link GatewayError} whose message contains `"version conflict"`; use
|
||||
* {@link isTicketVersionConflict} to branch on it in the UI.
|
||||
*/
|
||||
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
|
||||
import type {
|
||||
GatewayError,
|
||||
Ticket,
|
||||
TicketCarnet,
|
||||
TicketLinkKind,
|
||||
TicketList,
|
||||
} from "@/domain";
|
||||
import type {
|
||||
CreateTicketInput,
|
||||
TicketGateway,
|
||||
TicketListQuery,
|
||||
UpdateTicketInput,
|
||||
} from "@/ports";
|
||||
|
||||
/**
|
||||
* Whether a gateway error is an optimistic-concurrency conflict (the ticket was
|
||||
* mutated since it was read). The backend returns a generic `INVALID` code, so
|
||||
* we key off the stable message fragment emitted by the domain store error.
|
||||
*/
|
||||
export function isTicketVersionConflict(error: unknown): boolean {
|
||||
if (!error || typeof error !== "object") return false;
|
||||
const message = (error as GatewayError).message;
|
||||
return typeof message === "string" && message.includes("version conflict");
|
||||
}
|
||||
|
||||
export class TauriTicketGateway implements TicketGateway {
|
||||
async create(projectId: string, input: CreateTicketInput): Promise<Ticket> {
|
||||
return invoke<Ticket>("ticket_create", {
|
||||
request: { projectId, ...input },
|
||||
});
|
||||
}
|
||||
|
||||
async read(
|
||||
projectId: string,
|
||||
ref: string,
|
||||
includeCarnet = false,
|
||||
): Promise<Ticket> {
|
||||
return invoke<Ticket>("ticket_read", {
|
||||
request: { projectId, ref, includeCarnet },
|
||||
});
|
||||
}
|
||||
|
||||
async list(projectId: string, query?: TicketListQuery): Promise<TicketList> {
|
||||
return invoke<TicketList>("ticket_list", {
|
||||
request: { projectId, ...(query ?? {}) },
|
||||
});
|
||||
}
|
||||
|
||||
async update(
|
||||
projectId: string,
|
||||
ref: string,
|
||||
input: UpdateTicketInput,
|
||||
): Promise<Ticket> {
|
||||
return invoke<Ticket>("ticket_update", {
|
||||
request: { projectId, ref, ...input },
|
||||
});
|
||||
}
|
||||
|
||||
async readCarnet(projectId: string, ref: string): Promise<TicketCarnet> {
|
||||
return invoke<TicketCarnet>("ticket_read_carnet", {
|
||||
request: { projectId, ref },
|
||||
});
|
||||
}
|
||||
|
||||
async updateCarnet(
|
||||
projectId: string,
|
||||
ref: string,
|
||||
carnet: string,
|
||||
expectedVersion: number,
|
||||
): Promise<Ticket> {
|
||||
return invoke<Ticket>("ticket_update_carnet", {
|
||||
request: { projectId, ref, carnet, expectedVersion },
|
||||
});
|
||||
}
|
||||
|
||||
async link(
|
||||
projectId: string,
|
||||
ref: string,
|
||||
targetRef: string,
|
||||
kind: TicketLinkKind,
|
||||
expectedVersion: number,
|
||||
): Promise<Ticket> {
|
||||
return invoke<Ticket>("ticket_link", {
|
||||
request: { projectId, ref, targetRef, kind, expectedVersion },
|
||||
});
|
||||
}
|
||||
|
||||
async unlink(
|
||||
projectId: string,
|
||||
ref: string,
|
||||
targetRef: string,
|
||||
expectedVersion: number,
|
||||
kind?: TicketLinkKind,
|
||||
): Promise<Ticket> {
|
||||
return invoke<Ticket>("ticket_unlink", {
|
||||
request: { projectId, ref, targetRef, expectedVersion, kind },
|
||||
});
|
||||
}
|
||||
|
||||
async assign(
|
||||
projectId: string,
|
||||
ref: string,
|
||||
agentId: string,
|
||||
assigned: boolean,
|
||||
expectedVersion: number,
|
||||
): Promise<Ticket> {
|
||||
return invoke<Ticket>("ticket_assign", {
|
||||
request: { projectId, ref, agentId, assigned, expectedVersion },
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user