Correction bug suppression complète coté lore (et suppression dans tout ce qui est campagne de la partie lore liée).

Améliorations ux :
- Bandeau en haut qui reste accessible lors de la création d'un élément (chapitre, page, scène etc...)
- Mise en place d'un surlignage pour voir su quel élément on est positionné
This commit is contained in:
2026-04-23 14:06:50 +02:00
parent 96bc5de942
commit 8efdf5d0e0
33 changed files with 786 additions and 71 deletions

View File

@@ -1,9 +1,13 @@
package com.loremind.application.campaigncontext;
import com.loremind.domain.campaigncontext.Arc;
import com.loremind.domain.campaigncontext.Chapter;
import com.loremind.domain.campaigncontext.ports.ArcRepository;
import com.loremind.domain.campaigncontext.ports.ChapterRepository;
import com.loremind.domain.campaigncontext.ports.SceneRepository;
import org.springframework.beans.BeanUtils;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
import java.util.Optional;
@@ -17,11 +21,20 @@ import java.util.Optional;
public class ArcService {
private final ArcRepository arcRepository;
private final ChapterRepository chapterRepository;
private final SceneRepository sceneRepository;
public ArcService(ArcRepository arcRepository) {
public ArcService(ArcRepository arcRepository,
ChapterRepository chapterRepository,
SceneRepository sceneRepository) {
this.arcRepository = arcRepository;
this.chapterRepository = chapterRepository;
this.sceneRepository = sceneRepository;
}
/** Compte des entités qui seront supprimées en cascade avec l'arc. */
public record DeletionImpact(int chapters, int scenes) {}
public Arc createArc(String name, String description, String campaignId, int order) {
Arc arc = Arc.builder()
.name(name)
@@ -59,7 +72,31 @@ public class ArcService {
return arcRepository.save(arc);
}
/**
* Calcule l'impact d'une suppression en cascade : chapitres + scènes
* qui disparaîtront avec l'arc.
*/
public DeletionImpact getDeletionImpact(String id) {
List<Chapter> chapters = chapterRepository.findByArcId(id);
int sceneTotal = 0;
for (Chapter chapter : chapters) {
sceneTotal += sceneRepository.findByChapterId(chapter.getId()).size();
}
return new DeletionImpact(chapters.size(), sceneTotal);
}
/**
* Supprime l'arc et toutes ses entités dépendantes (chapitres → scènes).
* Transactionnel : atomique.
*/
@Transactional
public void deleteArc(String id) {
for (Chapter chapter : chapterRepository.findByArcId(id)) {
for (var scene : sceneRepository.findByChapterId(chapter.getId())) {
sceneRepository.deleteById(scene.getId());
}
chapterRepository.deleteById(chapter.getId());
}
arcRepository.deleteById(id);
}

View File

@@ -2,8 +2,10 @@ package com.loremind.application.campaigncontext;
import com.loremind.domain.campaigncontext.Chapter;
import com.loremind.domain.campaigncontext.ports.ChapterRepository;
import com.loremind.domain.campaigncontext.ports.SceneRepository;
import org.springframework.beans.BeanUtils;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
import java.util.Optional;
@@ -17,11 +19,16 @@ import java.util.Optional;
public class ChapterService {
private final ChapterRepository chapterRepository;
private final SceneRepository sceneRepository;
public ChapterService(ChapterRepository chapterRepository) {
public ChapterService(ChapterRepository chapterRepository, SceneRepository sceneRepository) {
this.chapterRepository = chapterRepository;
this.sceneRepository = sceneRepository;
}
/** Compte des scènes qui seront supprimées en cascade avec le chapitre. */
public record DeletionImpact(int scenes) {}
public Chapter createChapter(String name, String description, String arcId, int order) {
Chapter chapter = Chapter.builder()
.name(name)
@@ -58,7 +65,17 @@ public class ChapterService {
return chapterRepository.save(chapter);
}
/** Compte des scènes qui tomberont avec le chapitre. */
public DeletionImpact getDeletionImpact(String id) {
return new DeletionImpact(sceneRepository.findByChapterId(id).size());
}
/** Supprime le chapitre et toutes ses scènes. Transactionnel : atomique. */
@Transactional
public void deleteChapter(String id) {
for (var scene : sceneRepository.findByChapterId(id)) {
sceneRepository.deleteById(scene.getId());
}
chapterRepository.deleteById(id);
}

View File

@@ -68,4 +68,12 @@ public class ArcController {
arcService.deleteArc(id);
return ResponseEntity.noContent().build();
}
@GetMapping("/{id}/deletion-impact")
public ResponseEntity<ArcService.DeletionImpact> getDeletionImpact(@PathVariable String id) {
if (!arcService.arcExists(id)) {
return ResponseEntity.notFound().build();
}
return ResponseEntity.ok(arcService.getDeletionImpact(id));
}
}

View File

@@ -68,4 +68,12 @@ public class ChapterController {
chapterService.deleteChapter(id);
return ResponseEntity.noContent().build();
}
@GetMapping("/{id}/deletion-impact")
public ResponseEntity<ChapterService.DeletionImpact> getDeletionImpact(@PathVariable String id) {
if (!chapterService.chapterExists(id)) {
return ResponseEntity.notFound().build();
}
return ResponseEntity.ok(chapterService.getDeletionImpact(id));
}
}

View File

@@ -1,7 +1,11 @@
package com.loremind.application.campaigncontext;
import com.loremind.domain.campaigncontext.Arc;
import com.loremind.domain.campaigncontext.Chapter;
import com.loremind.domain.campaigncontext.Scene;
import com.loremind.domain.campaigncontext.ports.ArcRepository;
import com.loremind.domain.campaigncontext.ports.ChapterRepository;
import com.loremind.domain.campaigncontext.ports.SceneRepository;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
@@ -14,6 +18,7 @@ import java.util.Optional;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.*;
/**
@@ -26,6 +31,10 @@ public class ArcServiceTest {
@Mock
private ArcRepository arcRepository;
@Mock
private ChapterRepository chapterRepository;
@Mock
private SceneRepository sceneRepository;
@InjectMocks
private ArcService arcService;
@@ -159,15 +168,48 @@ public class ArcServiceTest {
}
@Test
void testDeleteArc() {
// Arrange
doNothing().when(arcRepository).deleteById("arc-1");
// Act
void testDeleteArc_EmptyArc() {
// Aucun chapitre : Mockito renvoie List.of() par défaut.
arcService.deleteArc("arc-1");
// Assert
verify(arcRepository, times(1)).deleteById("arc-1");
verify(arcRepository).deleteById("arc-1");
verify(chapterRepository, never()).deleteById(anyString());
verify(sceneRepository, never()).deleteById(anyString());
}
@Test
void testDeleteArc_CascadesChaptersAndScenes() {
Chapter chapter = Chapter.builder().id("chap-1").arcId("arc-1").name("C").build();
Scene s1 = Scene.builder().id("s-1").chapterId("chap-1").name("S1").build();
Scene s2 = Scene.builder().id("s-2").chapterId("chap-1").name("S2").build();
when(chapterRepository.findByArcId("arc-1")).thenReturn(List.of(chapter));
when(sceneRepository.findByChapterId("chap-1")).thenReturn(List.of(s1, s2));
arcService.deleteArc("arc-1");
verify(sceneRepository).deleteById("s-1");
verify(sceneRepository).deleteById("s-2");
verify(chapterRepository).deleteById("chap-1");
verify(arcRepository).deleteById("arc-1");
}
@Test
void testGetDeletionImpact() {
Chapter c1 = Chapter.builder().id("chap-1").arcId("arc-1").name("C1").build();
Chapter c2 = Chapter.builder().id("chap-2").arcId("arc-1").name("C2").build();
Scene s1 = Scene.builder().id("s-1").chapterId("chap-1").name("S1").build();
Scene s2 = Scene.builder().id("s-2").chapterId("chap-2").name("S2").build();
Scene s3 = Scene.builder().id("s-3").chapterId("chap-2").name("S3").build();
when(chapterRepository.findByArcId("arc-1")).thenReturn(List.of(c1, c2));
when(sceneRepository.findByChapterId("chap-1")).thenReturn(List.of(s1));
when(sceneRepository.findByChapterId("chap-2")).thenReturn(List.of(s2, s3));
ArcService.DeletionImpact impact = arcService.getDeletionImpact("arc-1");
assertEquals(2, impact.chapters());
assertEquals(3, impact.scenes());
}
@Test

View File

@@ -1,7 +1,9 @@
package com.loremind.application.campaigncontext;
import com.loremind.domain.campaigncontext.Chapter;
import com.loremind.domain.campaigncontext.Scene;
import com.loremind.domain.campaigncontext.ports.ChapterRepository;
import com.loremind.domain.campaigncontext.ports.SceneRepository;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
@@ -14,6 +16,7 @@ import java.util.Optional;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.*;
/**
@@ -26,6 +29,8 @@ public class ChapterServiceTest {
@Mock
private ChapterRepository chapterRepository;
@Mock
private SceneRepository sceneRepository;
@InjectMocks
private ChapterService chapterService;
@@ -157,15 +162,36 @@ public class ChapterServiceTest {
}
@Test
void testDeleteChapter() {
// Arrange
doNothing().when(chapterRepository).deleteById("chapter-1");
// Act
void testDeleteChapter_EmptyChapter() {
// Aucune scène : Mockito renvoie List.of() par défaut.
chapterService.deleteChapter("chapter-1");
// Assert
verify(chapterRepository, times(1)).deleteById("chapter-1");
verify(chapterRepository).deleteById("chapter-1");
verify(sceneRepository, never()).deleteById(anyString());
}
@Test
void testDeleteChapter_CascadesScenes() {
Scene s1 = Scene.builder().id("s-1").chapterId("chapter-1").name("S1").build();
Scene s2 = Scene.builder().id("s-2").chapterId("chapter-1").name("S2").build();
when(sceneRepository.findByChapterId("chapter-1")).thenReturn(List.of(s1, s2));
chapterService.deleteChapter("chapter-1");
verify(sceneRepository).deleteById("s-1");
verify(sceneRepository).deleteById("s-2");
verify(chapterRepository).deleteById("chapter-1");
}
@Test
void testGetDeletionImpact() {
Scene s1 = Scene.builder().id("s-1").chapterId("chapter-1").name("S1").build();
Scene s2 = Scene.builder().id("s-2").chapterId("chapter-1").name("S2").build();
when(sceneRepository.findByChapterId("chapter-1")).thenReturn(List.of(s1, s2));
ChapterService.DeletionImpact impact = chapterService.getDeletionImpact("chapter-1");
assertEquals(2, impact.scenes());
}
@Test