Mise en place de la possibilité de supprimer des lores / campagnes d'un seul coup
This commit is contained in:
@@ -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')
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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')
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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')
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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}`);
|
||||
|
||||
@@ -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 });
|
||||
|
||||
Reference in New Issue
Block a user