Mise en place de la possibilité de supprimer des lores / campagnes d'un seul coup

This commit is contained in:
2026-04-23 11:51:03 +02:00
parent 84ccdd53ad
commit 96bc5de942
15 changed files with 585 additions and 54 deletions

View File

@@ -257,22 +257,37 @@ export class CampaignDetailComponent implements OnInit, OnDestroy {
}
/**
* Suppression protégée : refus si la campagne contient des arcs.
* Les arcs contiennent potentiellement des chapitres/scènes construits longuement.
* Suppression en cascade : récupère d'abord le détail de ce qui sera effacé
* (arcs / chapitres / scènes / personnages), affiche le récapitulatif dans
* la confirmation, puis supprime. Le cascade est orchestré côté backend dans
* une seule transaction.
*/
deleteCampaign(): void {
if (!this.campaign) return;
if (this.arcs.length > 0) {
alert(
`Impossible de supprimer "${this.campaign.name}" : elle contient encore ${this.arcs.length} arc(s).\n` +
`Videz la campagne (arcs et chapitres) avant de la supprimer.`
);
return;
}
if (!confirm(`Supprimer définitivement la campagne "${this.campaign.name}" ?`)) return;
this.campaignService.deleteCampaign(this.campaign.id!).subscribe({
next: () => this.router.navigate(['/campaigns']),
error: () => console.error('Erreur lors de la suppression de la campagne')
const campaign = this.campaign;
this.campaignService.getCampaignDeletionImpact(campaign.id!).subscribe({
next: impact => {
const parts: string[] = [];
if (impact.arcs > 0) parts.push(`${impact.arcs} arc${impact.arcs > 1 ? 's' : ''}`);
if (impact.chapters > 0) parts.push(`${impact.chapters} chapitre${impact.chapters > 1 ? 's' : ''}`);
if (impact.scenes > 0) parts.push(`${impact.scenes} scène${impact.scenes > 1 ? 's' : ''}`);
if (impact.characters > 0) parts.push(`${impact.characters} personnage${impact.characters > 1 ? 's' : ''}`);
const lines = [`Supprimer définitivement la campagne "${campaign.name}" ?`];
if (parts.length) {
lines.push('');
lines.push(`Cette action supprimera aussi : ${parts.join(', ')}.`);
}
lines.push('');
lines.push('Cette action est irréversible.');
if (!confirm(lines.join('\n'))) return;
this.campaignService.deleteCampaign(campaign.id!).subscribe({
next: () => this.router.navigate(['/campaigns']),
error: () => console.error('Erreur lors de la suppression de la campagne')
});
},
error: () => console.error('Impossible de récupérer les dépendances de la campagne')
});
}

View File

@@ -110,24 +110,43 @@ export class LoreDetailComponent implements OnInit, OnDestroy {
}
/**
* Suppression protégée : refus si le Lore contient encore des dossiers
* ou des pages. Protège contre un clic accidentel sur des données
* construites longuement. Logique côté frontend (pas d'appel HTTP
* supplémentaire) car les données sont déjà chargées.
* Suppression en cascade : récupère le détail de ce qui tombera (dossiers,
* pages, templates) et de ce qui sera détaché (campagnes conservées mais
* sans lien vers ce Lore), affiche le récapitulatif dans la confirmation,
* puis délègue au backend (transaction atomique).
*/
deleteLore(): void {
if (!this.lore) return;
if (this.allNodes.length > 0) {
alert(
`Impossible de supprimer "${this.lore.name}" : il contient encore ${this.allNodes.length} dossier(s).\n` +
`Videz le Lore (dossiers et pages) avant de le supprimer.`
);
return;
}
if (!confirm(`Supprimer définitivement le Lore "${this.lore.name}" ?`)) return;
this.loreService.deleteLore(this.lore.id!).subscribe({
next: () => this.router.navigate(['/lore']),
error: () => console.error('Erreur lors de la suppression du Lore')
const lore = this.lore;
this.loreService.getLoreDeletionImpact(lore.id!).subscribe({
next: impact => {
const deleted: string[] = [];
if (impact.folders > 0) deleted.push(`${impact.folders} dossier${impact.folders > 1 ? 's' : ''}`);
if (impact.pages > 0) deleted.push(`${impact.pages} page${impact.pages > 1 ? 's' : ''}`);
if (impact.templates > 0) deleted.push(`${impact.templates} template${impact.templates > 1 ? 's' : ''}`);
const lines = [`Supprimer définitivement le Lore "${lore.name}" ?`];
if (deleted.length) {
lines.push('');
lines.push(`Cette action supprimera aussi : ${deleted.join(', ')}.`);
}
if (impact.detachedCampaigns > 0) {
lines.push('');
lines.push(
`${impact.detachedCampaigns} campagne${impact.detachedCampaigns > 1 ? 's' : ''} ${impact.detachedCampaigns > 1 ? 'seront conservées' : 'sera conservée'} ` +
`mais ${impact.detachedCampaigns > 1 ? 'perdront' : 'perdra'} leur lien vers cet univers.`
);
}
lines.push('');
lines.push('Cette action est irréversible.');
if (!confirm(lines.join('\n'))) return;
this.loreService.deleteLore(lore.id!).subscribe({
next: () => this.router.navigate(['/lore']),
error: () => console.error('Erreur lors de la suppression du Lore')
});
},
error: () => console.error('Impossible de récupérer les dépendances du Lore')
});
}

View File

@@ -12,8 +12,7 @@
<button
type="button"
class="btn-danger"
[disabled]="!canDelete"
[title]="canDelete ? 'Supprimer le dossier' : 'Impossible : le dossier contient des éléments'"
title="Supprimer le dossier et tout son contenu"
(click)="delete()">
Supprimer
</button>
@@ -57,11 +56,6 @@
</div>
</div>
<div class="info-box" *ngIf="!canDelete">
⚠️ Pour supprimer ce dossier, videz-le d'abord : déplacez ou supprimez ses
{{ childFolderCount }} sous-dossier(s) et ses {{ pageCount }} page(s).
</div>
</form>
</div>

View File

@@ -134,16 +134,35 @@ export class LoreNodeEditComponent implements OnInit, OnDestroy {
});
}
get canDelete(): boolean {
return this.childFolderCount === 0 && this.pageCount === 0;
}
/**
* Suppression en cascade : on va chercher le compte exact de sous-dossiers et
* de pages (qui tombent avec le dossier), on l'annonce dans la confirmation,
* puis on délègue au backend — l'atomicité est garantie côté transaction.
*/
delete(): void {
if (!this.canDelete || !this.node) return;
if (!confirm(`Supprimer le dossier "${this.node.name}" ?`)) return;
this.loreService.deleteLoreNode(this.folderId).subscribe({
next: () => this.router.navigate(['/lore', this.loreId]),
error: () => console.error('Erreur lors de la suppression du dossier')
if (!this.node) return;
const node = this.node;
this.loreService.getLoreNodeDeletionImpact(this.folderId).subscribe({
next: impact => {
const parts: string[] = [];
if (impact.folders > 0) parts.push(`${impact.folders} sous-dossier${impact.folders > 1 ? 's' : ''}`);
if (impact.pages > 0) parts.push(`${impact.pages} page${impact.pages > 1 ? 's' : ''}`);
const lines = [`Supprimer le dossier "${node.name}" ?`];
if (parts.length) {
lines.push('');
lines.push(`Cette action supprimera aussi : ${parts.join(', ')}.`);
}
lines.push('');
lines.push('Cette action est irréversible.');
if (!confirm(lines.join('\n'))) return;
this.loreService.deleteLoreNode(this.folderId).subscribe({
next: () => this.router.navigate(['/lore', this.loreId]),
error: () => console.error('Erreur lors de la suppression du dossier')
});
},
error: () => console.error('Impossible de récupérer les dépendances du dossier')
});
}

View File

@@ -3,6 +3,14 @@ import { HttpClient, HttpParams } from '@angular/common/http';
import { Observable } from 'rxjs';
import { Campaign, CampaignCreate, Arc, ArcCreate, Chapter, ChapterCreate, Scene, SceneCreate } from './campaign.model';
/** Compte des entités qui seront supprimées en cascade avec la campagne. */
export interface CampaignDeletionImpact {
arcs: number;
chapters: number;
scenes: number;
characters: number;
}
/**
* Service HTTP pour la gestion des Campagnes.
* Port de sortie vers le Backend Java (Architecture Hexagonale).
@@ -35,6 +43,10 @@ export class CampaignService {
return this.http.delete<void>(`${this.apiUrl}/${id}`);
}
getCampaignDeletionImpact(id: string): Observable<CampaignDeletionImpact> {
return this.http.get<CampaignDeletionImpact>(`${this.apiUrl}/${id}/deletion-impact`);
}
// ========== ARC ==========
getArcs(campaignId: string): Observable<Arc[]> {
return this.http.get<Arc[]>(`http://localhost:8080/api/arcs/campaign/${campaignId}`);

View File

@@ -3,6 +3,23 @@ import { HttpClient, HttpParams } from '@angular/common/http';
import { Observable } from 'rxjs';
import { Lore, LoreCreate, LoreNode, LoreNodeCreate } from './lore.model';
/** Compte des entités qui seront supprimées en cascade avec un dossier. */
export interface LoreNodeDeletionImpact {
/** Sous-dossiers (récursif, sans compter le dossier racine lui-même). */
folders: number;
/** Pages dans l'ensemble du sous-arbre. */
pages: number;
}
/** Compte des entités qui seront supprimées / détachées en cascade avec un Lore. */
export interface LoreDeletionImpact {
folders: number;
pages: number;
templates: number;
/** Campagnes qui perdront leur référence au Lore (mais resteront présentes). */
detachedCampaigns: number;
}
/**
* Service HTTP pour la gestion des Lores.
* Port de sortie vers le Backend Java (Architecture Hexagonale).
@@ -36,6 +53,10 @@ export class LoreService {
return this.http.delete<void>(`${this.apiUrl}/${id}`);
}
getLoreDeletionImpact(id: string): Observable<LoreDeletionImpact> {
return this.http.get<LoreDeletionImpact>(`${this.apiUrl}/${id}/deletion-impact`);
}
getLoreNodes(loreId: string): Observable<LoreNode[]> {
return this.http.get<LoreNode[]>(`${this.nodesUrl}?loreId=${loreId}`);
}
@@ -57,6 +78,10 @@ export class LoreService {
return this.http.delete<void>(`${this.nodesUrl}/${id}`);
}
getLoreNodeDeletionImpact(id: string): Observable<LoreNodeDeletionImpact> {
return this.http.get<LoreNodeDeletionImpact>(`${this.nodesUrl}/${id}/deletion-impact`);
}
searchLores(q: string): Observable<Lore[]> {
const params = new HttpParams().set('q', q);
return this.http.get<Lore[]>(`${this.apiUrl}/search`, { params });