From dd9db937514800417e35d7e743c164f0935ab058 Mon Sep 17 00:00:00 2001 From: Matthieu Date: Wed, 18 Mar 2026 11:07:41 +0100 Subject: [PATCH] feat(project) : add delete button for empty projects with confirmation modal Adds taskCount virtual field on Project entity, delete button in ProjectDrawer (visible only when taskCount === 0), and a reusable ConfirmDeleteProjectModal. Co-Authored-By: Claude Opus 4.6 (1M context) --- frontend/components/project/ProjectDrawer.vue | 32 +++++++++- .../ui/ConfirmDeleteProjectModal.vue | 58 +++++++++++++++++++ frontend/i18n/locales/fr.json | 5 +- frontend/services/dto/project.ts | 1 + src/Entity/Project.php | 17 ++++++ src/Entity/Task.php | 2 +- 6 files changed, 111 insertions(+), 4 deletions(-) create mode 100644 frontend/components/ui/ConfirmDeleteProjectModal.vue diff --git a/frontend/components/project/ProjectDrawer.vue b/frontend/components/project/ProjectDrawer.vue index 5007f89..8dc26af 100644 --- a/frontend/components/project/ProjectDrawer.vue +++ b/frontend/components/project/ProjectDrawer.vue @@ -64,7 +64,7 @@ -
+
+
+ + @@ -104,6 +118,7 @@ const isOpen = computed({ const isEditing = computed(() => !!props.project) const isSubmitting = ref(false) +const confirmDeleteOpen = ref(false) const { listRepositories } = useGiteaService() const giteaRepos = ref([]) @@ -164,7 +179,7 @@ watch(() => props.modelValue, (open) => { } }) -const { create, update } = useProjectService() +const { create, update, remove } = useProjectService() async function handleSubmit() { touched.name = true @@ -213,6 +228,19 @@ async function handleSubmit() { } } +async function handleDelete() { + if (!props.project) return + isSubmitting.value = true + try { + await remove(props.project.id) + emit('saved') + isOpen.value = false + } finally { + confirmDeleteOpen.value = false + isSubmitting.value = false + } +} + async function handleArchiveToggle() { if (!props.project) return isSubmitting.value = true diff --git a/frontend/components/ui/ConfirmDeleteProjectModal.vue b/frontend/components/ui/ConfirmDeleteProjectModal.vue new file mode 100644 index 0000000..8e0fbe8 --- /dev/null +++ b/frontend/components/ui/ConfirmDeleteProjectModal.vue @@ -0,0 +1,58 @@ + + + + + diff --git a/frontend/i18n/locales/fr.json b/frontend/i18n/locales/fr.json index 05abaf3..87c72c9 100644 --- a/frontend/i18n/locales/fr.json +++ b/frontend/i18n/locales/fr.json @@ -39,7 +39,10 @@ "noArchivedProjects": "Aucun projet archivé.", "addProject": "Ajouter un projet", "addProjectShort": "Projet", - "editProject": "Modifier un projet" + "editProject": "Modifier un projet", + "deleteConfirmTitle": "Supprimer le projet", + "deleteConfirmMessage": "Êtes-vous sûr de vouloir supprimer ce projet ? Cette action est irréversible.", + "cannotDelete": "Impossible de supprimer un projet contenant des tickets." }, "taskStatuses": { "created": "Statut créé avec succès.", diff --git a/frontend/services/dto/project.ts b/frontend/services/dto/project.ts index 7d64ae3..10cbb3c 100644 --- a/frontend/services/dto/project.ts +++ b/frontend/services/dto/project.ts @@ -13,6 +13,7 @@ export type Project = { bookstackShelfId: number | null bookstackShelfName: string | null archived: boolean + taskCount: number } export type ProjectWrite = { diff --git a/src/Entity/Project.php b/src/Entity/Project.php index 7e62b9f..0183078 100644 --- a/src/Entity/Project.php +++ b/src/Entity/Project.php @@ -13,6 +13,8 @@ use ApiPlatform\Metadata\GetCollection; use ApiPlatform\Metadata\Patch; use ApiPlatform\Metadata\Post; use App\Repository\ProjectRepository; +use Doctrine\Common\Collections\ArrayCollection; +use Doctrine\Common\Collections\Collection; use Doctrine\ORM\Mapping as ORM; use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity; use Symfony\Component\Serializer\Attribute\Groups; @@ -87,6 +89,15 @@ class Project #[Groups(['project:read', 'project:write'])] private bool $archived = false; + /** @var Collection */ + #[ORM\OneToMany(targetEntity: Task::class, mappedBy: 'project')] + private Collection $tasks; + + public function __construct() + { + $this->tasks = new ArrayCollection(); + } + public function getId(): ?int { return $this->id; @@ -216,4 +227,10 @@ class Project return $this; } + + #[Groups(['project:read'])] + public function getTaskCount(): int + { + return $this->tasks->count(); + } } diff --git a/src/Entity/Task.php b/src/Entity/Task.php index 22890b7..001c461 100644 --- a/src/Entity/Task.php +++ b/src/Entity/Task.php @@ -82,7 +82,7 @@ class Task #[Groups(['task:read', 'task:write'])] private ?TaskGroup $group = null; - #[ORM\ManyToOne(targetEntity: Project::class)] + #[ORM\ManyToOne(targetEntity: Project::class, inversedBy: 'tasks')] #[ORM\JoinColumn(nullable: false, onDelete: 'CASCADE')] #[Groups(['task:read', 'task:write'])] private ?Project $project = null;