feat(backend,domain,infrastructure,application,app-tauri,web-server,frontend): suppression d'un projet du registre IdeA sans toucher son dossier disque — #308 (QA verte)
cause: l'utilisateur ne pouvait pas retirer un projet du registre IdeA (projects.json) ; la seule issue laissait l'entrée fantôme dans la liste des projets, sans capacité générique de retrait. domain: port ProjectStore::remove_project(id) -> Option<Project> avec implémentation par défaut « not supported » pour ne pas casser les stores spécialisés. infrastructure: FsProjectStore implémente remove_project — retire l'entrée du registre persisté et la renvoie, None si id inconnu, ordre relatif du registre préservé ; test store dédié. application: use case RemoveProjectFromRegistry (remove.rs) — NotFound explicite si id absent, retourne le projet retiré + registre rafraîchi. backend/app-tauri/web-server: DTO RemoveProjectRequestDto/RemoveProjectResponseDto (removed, projects, restore_state, was_open), adapter partagé remove_project_for_adapter réutilisé par la commande Tauri remove_project et la route HTTP — même contrat desktop/web ; remove_project_and_deactivate désactive le projet s'il était ouvert, purge l'entrée open-projects restore correspondante et renvoie was_open/restore_state ; test adapter vérifiant le contrat de bout en bout (entrée retirée, restore purgé, root intact). frontend: DTO + ProjectGateway.removeProject (HTTP + mock), ProjectsView expose un menu d'actions projet (ProjectActionsMenu) avec entrée Retirer, RemoveProjectDialog — confirmation explicite précisant que le dossier disque n'est pas supprimé, invalidation/rechargement de la liste des projets après succès (useProjects), tests ProjectsView/projects/adapters/httpInvoker/services. QA verte: cargo targeted tests/check OK (domain, application project_usecases, infrastructure project_store, backend, app-tauri, web-server), npm targeted/typecheck/build OK ; suite npm test complète rouge non bloquante hors #308 (échecs préexistants listProjectSettings/web). Vérifications Git: git diff --check OK, cargo fmt --all -- --check OK. fichiers .ideai/tickets/308/, tickets/counter.json, tickets/index.json, attachments/ et scripts/ hors périmètre et préservés en worktree (sync store traité séparément) ; aucun push. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
94
frontend/src/features/projects/RemoveProjectDialog.tsx
Normal file
94
frontend/src/features/projects/RemoveProjectDialog.tsx
Normal file
@ -0,0 +1,94 @@
|
||||
import { useEffect, useRef } from "react";
|
||||
|
||||
import type { Project } from "@/domain";
|
||||
import { Button, zIndex } from "@/shared";
|
||||
|
||||
interface RemoveProjectDialogProps {
|
||||
project: Project;
|
||||
isOpen: boolean;
|
||||
pending: boolean;
|
||||
error: string | null;
|
||||
onCancel: () => void;
|
||||
onConfirm: () => void;
|
||||
}
|
||||
|
||||
export function RemoveProjectDialog({
|
||||
project,
|
||||
isOpen,
|
||||
pending,
|
||||
error,
|
||||
onCancel,
|
||||
onConfirm,
|
||||
}: RemoveProjectDialogProps) {
|
||||
const cancelRef = useRef<HTMLButtonElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
cancelRef.current?.focus();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
function cancelOnEscape(event: KeyboardEvent) {
|
||||
if (event.key === "Escape" && !pending) onCancel();
|
||||
}
|
||||
document.addEventListener("keydown", cancelOnEscape);
|
||||
return () => document.removeEventListener("keydown", cancelOnEscape);
|
||||
}, [onCancel, pending]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className="fixed inset-0 flex items-center justify-center bg-black/55 p-4"
|
||||
style={{ zIndex: zIndex.floatingWindowNested }}
|
||||
onMouseDown={(event) => {
|
||||
if (!pending && event.target === event.currentTarget) onCancel();
|
||||
}}
|
||||
>
|
||||
<div
|
||||
role="alertdialog"
|
||||
aria-modal="true"
|
||||
aria-label={`Retirer « ${project.name} » d'IdeA ?`}
|
||||
className="flex w-full max-w-md flex-col gap-3 rounded-lg border border-border bg-surface p-4 shadow-xl"
|
||||
>
|
||||
<h3 className="text-sm font-semibold text-content">
|
||||
Retirer « {project.name} » d'IdeA ?
|
||||
</h3>
|
||||
<code className="break-all rounded-md bg-raised px-3 py-2 text-xs text-muted">
|
||||
{project.root}
|
||||
</code>
|
||||
<p className="text-sm text-muted">
|
||||
Le dossier et les fichiers resteront sur le disque.
|
||||
</p>
|
||||
{isOpen && (
|
||||
<p className="text-sm text-muted">
|
||||
Les vues de ce projet seront fermées et les opérations IdeA en cours seront
|
||||
interrompues.
|
||||
</p>
|
||||
)}
|
||||
{error && (
|
||||
<p role="alert" className="rounded-md border border-danger/40 bg-danger/10 px-3 py-2 text-sm text-danger">
|
||||
{error}
|
||||
</p>
|
||||
)}
|
||||
<div className="flex justify-end gap-2">
|
||||
<Button
|
||||
ref={cancelRef}
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
disabled={pending}
|
||||
onClick={onCancel}
|
||||
>
|
||||
Annuler
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="danger"
|
||||
loading={pending}
|
||||
aria-label={pending ? "Retrait…" : undefined}
|
||||
onClick={onConfirm}
|
||||
>
|
||||
{pending ? "Retrait…" : "Retirer d'IdeA"}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user