feat(sprints): interface de création et gestion des sprints (frontend)
Ticket #11 — surface UI de gestion des sprints (frontend pur). - Nouveau composant SprintManager : création (nom optionnel), édition et gestion du cycle de vie d'un sprint, contrôles accessibles (pas de DnD). - useTickets étendu aux opérations de gestion de sprint ; ports, adaptateurs (ticket.ts, mock/index.ts) et TicketsPanel câblés en conséquence. - Tests Vitest associés (tickets.test.tsx). Typecheck / vitest (497+ tests) / build verts. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@ -40,6 +40,20 @@ export interface TicketsViewModel {
|
||||
* version column. Refreshes on the resulting `issueSprintChanged` event.
|
||||
*/
|
||||
assignSprint: (ref: string, sprintId: string | null) => Promise<boolean>;
|
||||
/** Creates a sprint (#11). Returns the created sprint, or `null` on error. */
|
||||
createSprint: (name: string) => Promise<Sprint | null>;
|
||||
/** Renames a sprint using its current version (#11). */
|
||||
renameSprint: (sprintId: string, name: string) => Promise<boolean>;
|
||||
/**
|
||||
* Moves a sprint one slot up/down in execution order via `reorderSprints`
|
||||
* (#11). A no-op at the ends. Accessible (button-driven), non-DnD.
|
||||
*/
|
||||
moveSprint: (sprintId: string, direction: "up" | "down") => Promise<boolean>;
|
||||
/**
|
||||
* Deletes a sprint (#11). The backend unassigns its tickets (does NOT delete
|
||||
* them). Refreshes both the sprint list and the tickets.
|
||||
*/
|
||||
deleteSprint: (sprintId: string) => Promise<boolean>;
|
||||
}
|
||||
|
||||
function describe(e: unknown): string {
|
||||
@ -116,6 +130,80 @@ export function useTickets(projectId: string): TicketsViewModel {
|
||||
[projectId, ticket, refresh, refreshSprints],
|
||||
);
|
||||
|
||||
const createSprint = useCallback(
|
||||
async (name: string): Promise<Sprint | null> => {
|
||||
setError(null);
|
||||
try {
|
||||
const created = await ticket.createSprint(projectId, name);
|
||||
await refreshSprints();
|
||||
return created;
|
||||
} catch (e) {
|
||||
setError(describe(e));
|
||||
return null;
|
||||
}
|
||||
},
|
||||
[projectId, ticket, refreshSprints],
|
||||
);
|
||||
|
||||
const renameSprint = useCallback(
|
||||
async (sprintId: string, name: string): Promise<boolean> => {
|
||||
setError(null);
|
||||
try {
|
||||
const current = sprints.find((s) => s.id === sprintId);
|
||||
if (!current) return false;
|
||||
await ticket.renameSprint(projectId, sprintId, name, current.version);
|
||||
await refreshSprints();
|
||||
return true;
|
||||
} catch (e) {
|
||||
setError(describe(e));
|
||||
return false;
|
||||
}
|
||||
},
|
||||
[projectId, ticket, sprints, refreshSprints],
|
||||
);
|
||||
|
||||
const moveSprint = useCallback(
|
||||
async (sprintId: string, direction: "up" | "down"): Promise<boolean> => {
|
||||
setError(null);
|
||||
const ordered = [...sprints].sort((a, b) => a.order - b.order);
|
||||
const index = ordered.findIndex((s) => s.id === sprintId);
|
||||
const swapWith = direction === "up" ? index - 1 : index + 1;
|
||||
if (index === -1 || swapWith < 0 || swapWith >= ordered.length) {
|
||||
return false;
|
||||
}
|
||||
const next = [...ordered];
|
||||
[next[index], next[swapWith]] = [next[swapWith], next[index]];
|
||||
try {
|
||||
await ticket.reorderSprints(
|
||||
projectId,
|
||||
next.map((s) => s.id),
|
||||
);
|
||||
await refreshSprints();
|
||||
return true;
|
||||
} catch (e) {
|
||||
setError(describe(e));
|
||||
return false;
|
||||
}
|
||||
},
|
||||
[projectId, ticket, sprints, refreshSprints],
|
||||
);
|
||||
|
||||
const deleteSprint = useCallback(
|
||||
async (sprintId: string): Promise<boolean> => {
|
||||
setError(null);
|
||||
try {
|
||||
await ticket.deleteSprint(projectId, sprintId);
|
||||
// Delete unassigns tickets → refresh both projections.
|
||||
await Promise.all([refresh(), refreshSprints()]);
|
||||
return true;
|
||||
} catch (e) {
|
||||
setError(describe(e));
|
||||
return false;
|
||||
}
|
||||
},
|
||||
[projectId, ticket, refresh, refreshSprints],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
void refresh();
|
||||
}, [refresh]);
|
||||
@ -157,5 +245,9 @@ export function useTickets(projectId: string): TicketsViewModel {
|
||||
refresh,
|
||||
create,
|
||||
assignSprint,
|
||||
createSprint,
|
||||
renameSprint,
|
||||
moveSprint,
|
||||
deleteSprint,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user