Lot en un seul commit: les deux features partagent les mêmes contrats (domain/settings.rs, DTO, commandes Tauri/HTTP, web-server) et les mêmes surfaces UI — la séparation par fichiers était impossible sans chirurgie de hunks risquant des états intermédiaires non compilables (convention lot des batches #310-#313 et #315). #325 — icône configurable par projet, watermark CLI custom: - domaine: ProjectIconSet (devicon, simple-icons, lucide, idea), ProjectIconRef validé (nom ASCII 1..=64, -_. internes), champ icon optionnel sur ProjectSettings; sérialisation camelCase et try_from strict - application: UpdateProjectSettings.execute accepte un patch theme_color OU icon (absent = inchangé, null = défaut produit), tests in-dependants avec store mémoire - backend/app-tauri/web-server: ProjectIconRefDto, UpdateProjectSettingsRequestDto avec icon optionnel (désérialisation additive), commandes update_project_settings alignées Tauri/HTTP, tests DTO et HTTP (défaut null, set, clear, nom invalide rejeté) - frontend: catalogue projectIcons (recherche EN), ProjectIconPicker en grille multi-lignes dans Paramètres projet, affichage de l'icône dans ProjectTabs et watermark grand format à faible opacité en fond de CLI custom; ports ProjectSettings.icon, Partial<ProjectSettings>, adapters tauri/mock/http et tests alignés #326 — localisation FR/EN et traduction des textes IdeA: - domaine: IdeaLanguage (fr | en), champ language optionnel sur IdeaUserSettings (absence = défaut produit, série additive validée) - backend/web-server: UserSettingsDto.language, update_user_settings accepte language, tests HTTP alignés; fixtures structured_launch_d3 mises à jour - frontend: i18n/ (catalogue FR/EN, I18nProvider, scan anti-régression des sources visibles), GeneralSettings avec sélecteur de langue dans les paramètres, setupI18n dans vitest; migration de l'ensemble des textes hardcodés vers le catalogue sur toutes les surfaces (agents, layout, projets, tickets, sprints, web, plugins, permissions, git, templates, skills, mémoire, model servers, embedder, first-run, devices, scheduled tasks, workstate, dialogs), FR conservé par défaut; recherche d'icônes #325 en anglais conformément au ticket QA verte sur feature/batch-325-326-project-icons-localisation. Aucun push.
96 lines
3.0 KiB
TypeScript
96 lines
3.0 KiB
TypeScript
import { useEffect, useRef } from "react";
|
||
|
||
import type { Project } from "@/domain";
|
||
import { Button, zIndex } from "@/shared";
|
||
import { useI18n } from "@/i18n";
|
||
|
||
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 { tr } = useI18n();
|
||
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={tr(`Retirer « ${project.name} » d’IdeA ?`, `Remove “${project.name}” from 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">
|
||
{tr(`Retirer « ${project.name} » d’IdeA ?`, `Remove “${project.name}” from 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">
|
||
{tr("Le dossier et les fichiers resteront sur le disque.", "The folder and files will remain on disk.")}
|
||
</p>
|
||
{isOpen && (
|
||
<p className="text-sm text-muted">
|
||
{tr("Les vues de ce projet seront fermées et les opérations IdeA en cours seront interrompues.", "This project's views will close and its active IdeA operations will be interrupted.")}
|
||
</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}
|
||
>
|
||
{tr("Annuler", "Cancel")}
|
||
</Button>
|
||
<Button
|
||
size="sm"
|
||
variant="danger"
|
||
loading={pending}
|
||
aria-label={pending ? tr("Retrait…", "Removing…") : undefined}
|
||
onClick={onConfirm}
|
||
>
|
||
{pending ? tr("Retrait…", "Removing…") : tr("Retirer d’IdeA", "Remove from IdeA")}
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|