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:
@ -47,6 +47,7 @@ import type {
|
||||
DomainEvent,
|
||||
LayoutInfo,
|
||||
OpenProjectRestoreFailure,
|
||||
Project,
|
||||
} from "@/domain";
|
||||
import { LayoutGrid, LayoutTabs } from "@/features/layout";
|
||||
import { ConversationViewer } from "@/features/conversations";
|
||||
@ -78,6 +79,8 @@ import { useGateways } from "@/app/di";
|
||||
import { useProjects } from "./useProjects";
|
||||
import { useProjectActivity } from "./useProjectActivity";
|
||||
import { ProjectTabs } from "./ProjectTabs";
|
||||
import { ProjectActionsMenu } from "./ProjectActionsMenu";
|
||||
import { RemoveProjectDialog } from "./RemoveProjectDialog";
|
||||
import { ViewPanelBody, type ViewPanelId } from "./ViewPanelBody";
|
||||
import {
|
||||
PANEL_TITLE,
|
||||
@ -266,6 +269,10 @@ export function ProjectsView() {
|
||||
const [taskToasts, setTaskToasts] = useState<BackgroundTaskToast[]>([]);
|
||||
const [detachErrorPanel, setDetachErrorPanel] = useState<PanelId | null>(null);
|
||||
const [restoreDetailsOpen, setRestoreDetailsOpen] = useState(false);
|
||||
const [removalTarget, setRemovalTarget] = useState<Project | null>(null);
|
||||
const [removalPending, setRemovalPending] = useState(false);
|
||||
const [removalError, setRemovalError] = useState<string | null>(null);
|
||||
const [removalNotice, setRemovalNotice] = useState<Project | null>(null);
|
||||
|
||||
const active = vm.openTabs.find((t) => t.id === vm.activeTabId) ?? null;
|
||||
const activeViewerConversation =
|
||||
@ -273,6 +280,24 @@ export function ProjectsView() {
|
||||
const showRestoreNotification =
|
||||
vm.restoreNotificationVisible && vm.restoreFailures.length > 0;
|
||||
|
||||
async function confirmProjectRemoval() {
|
||||
if (!removalTarget || removalPending) return;
|
||||
const removed = removalTarget;
|
||||
setRemovalPending(true);
|
||||
setRemovalError(null);
|
||||
try {
|
||||
await vm.removeProject(removed.id);
|
||||
setRemovalTarget(null);
|
||||
setRemovalNotice(removed);
|
||||
} catch {
|
||||
setRemovalError(
|
||||
"Impossible de retirer ce projet d'IdeA. Le projet est toujours enregistré. Réessayez.",
|
||||
);
|
||||
} finally {
|
||||
setRemovalPending(false);
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (vm.restoreFailures.length === 0) setRestoreDetailsOpen(false);
|
||||
}, [vm.restoreFailures.length]);
|
||||
@ -751,9 +776,20 @@ export function ProjectsView() {
|
||||
<span className="font-medium text-content">{p.name}</span>
|
||||
<code className="truncate text-xs text-muted">{p.root}</code>
|
||||
</span>
|
||||
<Button size="sm" onClick={() => void vm.openProject(p.id)}>
|
||||
Open
|
||||
</Button>
|
||||
<span className="flex shrink-0 items-center gap-1">
|
||||
<Button size="sm" onClick={() => void vm.openProject(p.id)}>
|
||||
Open
|
||||
</Button>
|
||||
<ProjectActionsMenu
|
||||
projectName={p.name}
|
||||
disabled={vm.busy}
|
||||
onOpen={() => void vm.openProject(p.id)}
|
||||
onRemove={() => {
|
||||
setRemovalError(null);
|
||||
setRemovalTarget(p);
|
||||
}}
|
||||
/>
|
||||
</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
@ -1076,8 +1112,21 @@ export function ProjectsView() {
|
||||
</ul>
|
||||
</FloatingWindow>
|
||||
|
||||
{removalTarget && (
|
||||
<RemoveProjectDialog
|
||||
project={removalTarget}
|
||||
isOpen={vm.openTabs.some((tab) => tab.id === removalTarget.id)}
|
||||
pending={removalPending}
|
||||
error={removalError}
|
||||
onCancel={() => {
|
||||
if (!removalPending) setRemovalTarget(null);
|
||||
}}
|
||||
onConfirm={() => void confirmProjectRemoval()}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* ── Non-blocking notifications (above floating windows) ── */}
|
||||
{(showRestoreNotification || taskToasts.length > 0) && (
|
||||
{(showRestoreNotification || removalNotice || taskToasts.length > 0) && (
|
||||
<div
|
||||
className="fixed bottom-4 right-4 flex w-80 max-w-[calc(100vw-2rem)] flex-col gap-2"
|
||||
style={{ zIndex: zIndex.toast }}
|
||||
@ -1117,6 +1166,27 @@ export function ProjectsView() {
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{removalNotice && (
|
||||
<div
|
||||
role="status"
|
||||
className="rounded-md border border-border bg-surface px-3 py-2 shadow-lg"
|
||||
>
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
<span className="text-sm text-content">
|
||||
« {removalNotice.name} » a été retiré d'IdeA. Les fichiers sont restés
|
||||
sur le disque.
|
||||
</span>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
aria-label="Fermer la notification de retrait"
|
||||
onClick={() => setRemovalNotice(null)}
|
||||
>
|
||||
×
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{taskToasts.map((toast) => (
|
||||
<button
|
||||
key={toast.id}
|
||||
|
||||
Reference in New Issue
Block a user