Nettoyage post-revue : factorisation, code mort et fiabilité (reorder/lore/foundry/PDF)
All checks were successful
Build & Push Images / build (brain) (push) Successful in 1m27s
Build & Push Images / build (web) (push) Successful in 1m50s
Build & Push Images / build (core) (push) Successful in 3m29s
Build & Push Images / build-switcher (push) Successful in 18s

Corrections fonctionnelles
- order initialisé à la création des pages et dossiers de lore (nextOrderFor) :
  un nouvel élément se place désormais en dernier de sa fratrie au lieu de 0
- storePortrait : type MIME canonique dérivé de l'extension du fichier
  (un data URL "image/jpg" n'est plus rejeté silencieusement)
- takeUntilDestroyed ajouté sur les abonnements paramMap de arc-view,
  folder-view et campaign-detail (fuites mémoire)

Factorisation (suppression de duplication)
- shared/folder-grouping.util.ts : groupByFolder + byOrder + byFolderName
  mutualisés entre npc-list, enemy-list, campaign-detail et la sidebar
  (folderChildren). Le tri des dossiers est désormais cohérent entre la
  sidebar et les vues cartes (insensible casse/accents partout)
- DataSyncService.onChange()/persist() remplacent le câblage
  changed$ + reorder recopié dans 5 vues
- campaign-detail : loadCampaignBundle()/applyCampaignBundle() éliminent
  le forkJoin dupliqué entre ngOnInit et reload
- ReorderSupport (domain/shared) : squelette générique remplaçant les
  8 boucles de réordonnancement copiées dans les services
- FoundryExportService : GameSystem résolu une seule fois dans buildBundle
- module Foundry : walkScalars mutualise la récursion de flattenStats et
  flattenStructure (comportements préservés) ; esc() de l'importer aligné
  sur foundry.utils.escapeHTML

Suppression de code mort
- characterService/characters retirés de loadCampaignTreeData et de ses
  16 appelants (arguments, injections, imports et littéraux CampaignTreeData)
- CSS orphelin : .tree-row.cdk-drop-list-receiving et .btn-back
- BottomPanel.initiallyOpen (jamais lu) retiré de l'interface et de l'appelant
- commentaire trompeur du PDF corrigé

Tests
- mise à jour de 3 tests obsolètes de campaign-tree.helper.spec qui
  vérifiaient encore l'ancien comportement (tri alphabétique, non-classés
  en enfants directs) → alignés sur le modèle actuel (tri par order +
  pseudo-dossier "Sans dossier")
This commit is contained in:
2026-06-26 09:10:20 +02:00
parent 3896c5b4cb
commit a9242ba1e1
94 changed files with 2234 additions and 546 deletions

View File

@@ -9,7 +9,7 @@ const t = { instant: (k: string) => k } as unknown as TranslateService;
function data(partial: Partial<CampaignTreeData> = {}): CampaignTreeData {
return {
arcs: [], chaptersByArc: {}, scenesByChapter: {},
characters: [], npcs: [], randomTables: [], enemies: [],
npcs: [], randomTables: [], enemies: [],
...partial,
};
}
@@ -23,15 +23,15 @@ describe('buildCampaignTree', () => {
]);
});
it('trie les arcs en ordre NUMÉRIQUE naturel (1, 2, 10)', () => {
it('trie les arcs par ordre manuel (champ order, réagencé au glisser-déposer)', () => {
const arcs = [
{ id: 'a', name: '10. Final' },
{ id: 'b', name: '2. Voyage' },
{ id: 'c', name: '1. Intro' },
{ id: 'a', name: 'Final', order: 2 },
{ id: 'b', name: 'Voyage', order: 1 },
{ id: 'c', name: 'Intro', order: 0 },
];
const tree = buildCampaignTree('c1', data({ arcs: arcs as never }), t);
const arcLabels = tree.filter((n) => n.id?.startsWith('arc-')).map((n) => n.label);
expect(arcLabels).toEqual(['1. Intro', '2. Voyage', '10. Final']);
expect(arcLabels).toEqual(['Intro', 'Voyage', 'Final']);
});
it('imbrique chapitres et scènes et marque les chapitres à prérequis', () => {
@@ -47,7 +47,7 @@ describe('buildCampaignTree', () => {
expect(chapter.children![0].id).toBe('scene-s1');
});
it('regroupe les PNJ par dossier et laisse les non classés à la racine', () => {
it('regroupe les PNJ par dossier et place les non classés dans un pseudo-dossier', () => {
const npcs = [
{ id: 'n1', name: 'Alice', folder: 'Ville' },
{ id: 'n2', name: 'Bob', folder: 'Ville' },
@@ -58,15 +58,19 @@ describe('buildCampaignTree', () => {
expect(npcsRoot.meta).toBe('3');
const folder = npcsRoot.children!.find((c) => c.id === 'npc-folder-Ville')!;
expect(folder.children!.map((c) => c.label)).toEqual(['Alice', 'Bob']);
expect(npcsRoot.children!.some((c) => c.id === 'npc-n3')).toBe(true);
// Les non classés vont dans un pseudo-dossier « Sans dossier » (et non en enfants
// directs) : tous les dossiers restent FRÈRES → glisser-déposer inter-dossiers fiable.
const none = npcsRoot.children!.find((c) => c.id === 'npc-folder-__none__')!;
expect(none.children!.some((c) => c.id === 'npc-n3')).toBe(true);
});
it("affiche le niveau de l'ennemi en méta", () => {
const tree = buildCampaignTree('c', data({
enemies: [{ id: 'e1', name: 'Gobelin', level: 3 }] as never,
enemies: [{ id: 'e1', name: 'Gobelin', level: 3, folder: 'Cave' }] as never,
}), t);
const enemiesRoot = tree.find((n) => n.id === 'enemies-root')!;
expect(enemiesRoot.children![0].meta).toBe('Niv. 3');
const folder = enemiesRoot.children!.find((c) => c.id === 'enemy-folder-Cave')!;
expect(folder.children![0].meta).toBe('Niv. 3');
});
it('libelle « nouvelle quête » dans un arc HUB (vs « nouveau chapitre »)', () => {