feat(sprints): surface frontend du modèle de sprints
Ticket #10 — volet frontend des sprints. - Domaine et ports frontend étendus au modèle de sprints (domain/index.ts, ports/index.ts). - Adaptateurs : ticket.ts et mock/index.ts câblent les opérations sprints. - UI tickets : useTickets + TicketsPanel exposent le rattachement/gestion des sprints, avec tests Vitest associés. Typecheck / vitest / build verts. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@ -36,6 +36,7 @@ import type {
|
||||
ResumableAgent,
|
||||
Skill,
|
||||
SkillScope,
|
||||
Sprint,
|
||||
Template,
|
||||
TerminalSession,
|
||||
Ticket,
|
||||
@ -1813,12 +1814,14 @@ export class MockTicketGateway implements TicketGateway {
|
||||
private tickets = new Map<string, Map<string, Ticket>>();
|
||||
private carnets = new Map<string, Map<string, string>>();
|
||||
private counters = new Map<string, number>();
|
||||
private sprints = new Map<string, Map<string, Sprint>>();
|
||||
|
||||
constructor(private readonly system?: MockSystemGateway) {}
|
||||
|
||||
/** Seeds a ticket for deterministic tests; returns the stored clone. */
|
||||
_seedTicket(projectId: string, ticket: Ticket): Ticket {
|
||||
const stored = structuredClone(ticket);
|
||||
if (stored.sprintId === undefined) stored.sprintId = null;
|
||||
this.projectTickets(projectId).set(stored.ref, stored);
|
||||
this.counters.set(
|
||||
projectId,
|
||||
@ -1827,6 +1830,41 @@ export class MockTicketGateway implements TicketGateway {
|
||||
return structuredClone(stored);
|
||||
}
|
||||
|
||||
/** Seeds a sprint for deterministic tests; returns the stored clone. */
|
||||
_seedSprint(
|
||||
projectId: string,
|
||||
sprint: Pick<Sprint, "id" | "order" | "name"> &
|
||||
Partial<Pick<Sprint, "status" | "ticketCount" | "version">>,
|
||||
): Sprint {
|
||||
const stored: Sprint = {
|
||||
status: "planned",
|
||||
ticketCount: 0,
|
||||
version: 1,
|
||||
...sprint,
|
||||
};
|
||||
this.projectSprints(projectId).set(stored.id, stored);
|
||||
return structuredClone(stored);
|
||||
}
|
||||
|
||||
private projectSprints(projectId: string): Map<string, Sprint> {
|
||||
let map = this.sprints.get(projectId);
|
||||
if (!map) {
|
||||
map = new Map();
|
||||
this.sprints.set(projectId, map);
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
/** Recomputes `ticketCount` for every sprint from the current tickets. */
|
||||
private recountSprints(projectId: string): void {
|
||||
const sprints = this.projectSprints(projectId);
|
||||
const counts = new Map<string, number>();
|
||||
for (const t of this.projectTickets(projectId).values()) {
|
||||
if (t.sprintId) counts.set(t.sprintId, (counts.get(t.sprintId) ?? 0) + 1);
|
||||
}
|
||||
for (const s of sprints.values()) s.ticketCount = counts.get(s.id) ?? 0;
|
||||
}
|
||||
|
||||
private projectTickets(projectId: string): Map<string, Ticket> {
|
||||
let map = this.tickets.get(projectId);
|
||||
if (!map) {
|
||||
@ -1874,6 +1912,7 @@ export class MockTicketGateway implements TicketGateway {
|
||||
description: input.description ?? "",
|
||||
status: input.status ?? "open",
|
||||
priority: input.priority ?? "medium",
|
||||
sprintId: null,
|
||||
links: [],
|
||||
assignedAgentIds: [...(input.assignedAgentIds ?? [])],
|
||||
createdBy: { kind: "user" },
|
||||
@ -1930,6 +1969,7 @@ export class MockTicketGateway implements TicketGateway {
|
||||
title: t.title,
|
||||
status: t.status,
|
||||
priority: t.priority,
|
||||
sprintId: t.sprintId ?? null,
|
||||
assignedAgentIds: [...t.assignedAgentIds],
|
||||
updatedAt: t.updatedAt,
|
||||
})),
|
||||
@ -2066,6 +2106,41 @@ export class MockTicketGateway implements TicketGateway {
|
||||
});
|
||||
return structuredClone(ticket);
|
||||
}
|
||||
|
||||
async listSprints(projectId: string): Promise<Sprint[]> {
|
||||
this.recountSprints(projectId);
|
||||
return [...this.projectSprints(projectId).values()]
|
||||
.sort((a, b) => a.order - b.order)
|
||||
.map((s) => structuredClone(s));
|
||||
}
|
||||
|
||||
async setTicketSprint(
|
||||
projectId: string,
|
||||
ref: string,
|
||||
sprintId: string | null,
|
||||
expectedVersion: number,
|
||||
): Promise<Ticket> {
|
||||
const ticket = this.require(projectId, ref);
|
||||
this.guard(ticket, expectedVersion);
|
||||
if (sprintId !== null && !this.projectSprints(projectId).has(sprintId)) {
|
||||
throw {
|
||||
code: "NOT_FOUND",
|
||||
message: `sprint ${sprintId} not found`,
|
||||
} as GatewayError;
|
||||
}
|
||||
const from = ticket.sprintId ?? null;
|
||||
ticket.sprintId = sprintId;
|
||||
this.bump(ticket);
|
||||
this.recountSprints(projectId);
|
||||
this.system?.emit({
|
||||
type: "issueSprintChanged",
|
||||
issueRef: ref,
|
||||
from,
|
||||
to: sprintId,
|
||||
version: ticket.version,
|
||||
});
|
||||
return structuredClone(ticket);
|
||||
}
|
||||
}
|
||||
|
||||
function mostRestrictive(
|
||||
|
||||
@ -14,6 +14,7 @@ import { invoke } from "@tauri-apps/api/core";
|
||||
|
||||
import type {
|
||||
GatewayError,
|
||||
Sprint,
|
||||
Ticket,
|
||||
TicketCarnet,
|
||||
TicketLinkKind,
|
||||
@ -122,4 +123,29 @@ export class TauriTicketGateway implements TicketGateway {
|
||||
request: { projectId, ref, agentId, assigned, expectedVersion },
|
||||
});
|
||||
}
|
||||
|
||||
async listSprints(projectId: string): Promise<Sprint[]> {
|
||||
// `sprint_list` returns a `SprintListDto { items }`; unwrap to the array.
|
||||
const list = await invoke<{ items: Sprint[] }>("sprint_list", {
|
||||
request: { projectId },
|
||||
});
|
||||
return list.items;
|
||||
}
|
||||
|
||||
async setTicketSprint(
|
||||
projectId: string,
|
||||
ref: string,
|
||||
sprintId: string | null,
|
||||
expectedVersion: number,
|
||||
): Promise<Ticket> {
|
||||
// Two backend commands: assign to a sprint, or unassign (clear membership).
|
||||
if (sprintId === null) {
|
||||
return invoke<Ticket>("ticket_unassign_sprint", {
|
||||
request: { projectId, ref, expectedVersion },
|
||||
});
|
||||
}
|
||||
return invoke<Ticket>("ticket_assign_sprint", {
|
||||
request: { projectId, ref, sprintId, expectedVersion },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user