Stack qualité CI (MegaLinter, Trivy, ESLint) et purge du backlog lint web
Some checks failed
Qualité & Sécurité / MegaLinter (PMD · Ruff · Bandit · gitleaks · hadolint) (push) Successful in 1m9s
Build & Push Images / build (brain) (push) Successful in 1m10s
Qualité & Sécurité / Trivy (CVE dépendances Maven / pip / npm) (push) Failing after 2m36s
Build & Push Images / build (web) (push) Successful in 1m48s
Build & Push Images / build-switcher (push) Successful in 20s
Build & Push Images / build (core) (push) Successful in 3m9s
Qualité & Sécurité / Web (ESLint · angular-eslint + sonarjs) (push) Successful in 38s
Some checks failed
Qualité & Sécurité / MegaLinter (PMD · Ruff · Bandit · gitleaks · hadolint) (push) Successful in 1m9s
Build & Push Images / build (brain) (push) Successful in 1m10s
Qualité & Sécurité / Trivy (CVE dépendances Maven / pip / npm) (push) Failing after 2m36s
Build & Push Images / build (web) (push) Successful in 1m48s
Build & Push Images / build-switcher (push) Successful in 20s
Build & Push Images / build (core) (push) Successful in 3m9s
Qualité & Sécurité / Web (ESLint · angular-eslint + sonarjs) (push) Successful in 38s
CI (Gitea Actions) : - Nouveau workflow quality.yml (séparé de ci.yml, non bloquant pour la release) : MegaLinter v9 (PMD, Ruff, Bandit, gitleaks, hadolint), ng lint (web) et Trivy (CVE des dépendances Maven/pip/npm), sur main + beta + PR - Trivy : préchargement du cache Maven avant le scan — sans ça Maven Central rate-limite l'IP du runner (429) en résolvant les BOMs du parent Spring Boot - upload-artifact v4 → v3 : l'API artifacts v4 n'est pas supportée par Gitea (corrige aussi l'upload du rapport Playwright de e2e.yml, cassé depuis toujours) - Ruleset PMD projet (java-pmd-ruleset.xml, auto-détecté par MegaLinter) orienté bugs réels, sans le style Lombok-hostile du défaut : 11 337 → 65 findings ; PMD en mode rapport (JAVA_PMD_DISABLE_ERRORS) le temps de purger ce backlog Web : - angular-eslint 21 + eslint-plugin-sonarjs : les règles Sonar vivent désormais dans ESLint (config web/eslint.config.js, réglages justifiés en commentaire) - tsconfig : skipLibCheck + types:[] + node_modules ré-exclu — répare les builds desktop Windows/Linux cassés par le conflit @types/eslint-scope vs ESLint 9 - Purge du backlog lint : 114 erreurs → 0 sur 66 fichiers (ngOnDestroy vides supprimés, any typés avec les vrais DTOs, outputs (close) → (closed), regex super-linéaires désamorcées, ternaires/fonctions imbriqués dépliés, intention documentée sur les catch/error volontairement vides) - Bug latent corrigé au passage : license.service.disconnect() émettait undefined au lieu de true en cas de succès (masqué par un double cast)
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { Component, OnInit, OnDestroy } from '@angular/core';
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
|
||||
import { ReactiveFormsModule, FormBuilder, FormGroup, Validators } from '@angular/forms';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
@@ -25,7 +25,7 @@ import { CAMPAIGN_ICON_OPTIONS } from '../../campaign-icons';
|
||||
templateUrl: './arc-create.component.html',
|
||||
styleUrls: ['./arc-create.component.scss']
|
||||
})
|
||||
export class ArcCreateComponent implements OnInit, OnDestroy {
|
||||
export class ArcCreateComponent implements OnInit {
|
||||
readonly BookOpen = BookOpen;
|
||||
readonly campaignIconOptions = CAMPAIGN_ICON_OPTIONS;
|
||||
|
||||
@@ -88,11 +88,4 @@ export class ArcCreateComponent implements OnInit, OnDestroy {
|
||||
cancel(): void {
|
||||
this.router.navigate(['/campaigns', this.campaignId]);
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
// Volontairement vide : la sidebar reste prise en charge par le composant
|
||||
// suivant (autre sous-route ou le composant detail parent) qui appellera
|
||||
// show(). Eviter d'appeler hide() ici previent le clignotement / la
|
||||
// disparition de la sidebar lors des navigations internes a la section.
|
||||
}
|
||||
}
|
||||
|
||||
@@ -183,5 +183,5 @@
|
||||
[isOpen]="chatOpen"
|
||||
[welcomeMessage]="'arcEdit.chatWelcome' | translate"
|
||||
[quickSuggestions]="chatQuickSuggestions"
|
||||
(close)="chatOpen = false">
|
||||
(closed)="chatOpen = false">
|
||||
</app-ai-chat-drawer>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Component, OnInit, OnDestroy, DestroyRef } from '@angular/core';
|
||||
import { Component, OnInit, DestroyRef } from '@angular/core';
|
||||
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
||||
|
||||
import { ReactiveFormsModule, FormBuilder, FormGroup, Validators } from '@angular/forms';
|
||||
@@ -42,7 +42,7 @@ import { FieldProposal } from '../../../services/entity-assist.model';
|
||||
templateUrl: './arc-edit.component.html',
|
||||
styleUrls: ['./arc-edit.component.scss']
|
||||
})
|
||||
export class ArcEditComponent implements OnInit, OnDestroy {
|
||||
export class ArcEditComponent implements OnInit {
|
||||
readonly Trash2 = Trash2;
|
||||
readonly Sparkles = Sparkles;
|
||||
readonly campaignIconOptions = CAMPAIGN_ICON_OPTIONS;
|
||||
@@ -221,11 +221,4 @@ export class ArcEditComponent implements OnInit, OnDestroy {
|
||||
cancel(): void {
|
||||
this.router.navigate(['/campaigns', this.campaignId, 'arcs', this.arcId]);
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
// Volontairement vide : la sidebar reste prise en charge par le composant
|
||||
// suivant (autre sous-route ou le composant detail parent) qui appellera
|
||||
// show(). Eviter d'appeler hide() ici previent le clignotement / la
|
||||
// disparition de la sidebar lors des navigations internes a la section.
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Component, OnInit, OnDestroy, DestroyRef } from '@angular/core';
|
||||
import { Component, OnInit, DestroyRef } from '@angular/core';
|
||||
|
||||
import { ActivatedRoute, Router, RouterModule } from '@angular/router';
|
||||
import { forkJoin, of } from 'rxjs';
|
||||
@@ -35,7 +35,7 @@ import { ConfirmDialogService } from '../../../shared/confirm-dialog/confirm-dia
|
||||
templateUrl: './arc-view.component.html',
|
||||
styleUrls: ['./arc-view.component.scss']
|
||||
})
|
||||
export class ArcViewComponent implements OnInit, OnDestroy {
|
||||
export class ArcViewComponent implements OnInit {
|
||||
readonly Pencil = Pencil;
|
||||
readonly Trash2 = Trash2;
|
||||
readonly Plus = Plus;
|
||||
@@ -162,8 +162,4 @@ export class ArcViewComponent implements OnInit, OnDestroy {
|
||||
error: () => console.error('Impossible de récupérer les dépendances de l\'arc')
|
||||
});
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
// Volontairement vide : la sidebar reste prise en charge par le composant suivant.
|
||||
}
|
||||
}
|
||||
|
||||
@@ -92,7 +92,7 @@ export class CampaignGraphComponent implements OnInit, OnDestroy {
|
||||
private readonly DRAG_THRESHOLD = 4;
|
||||
|
||||
// Adjacence (ids de nœuds) — sert à recalculer les arêtes après un drag.
|
||||
private adjacency: Array<{ key: string; kind: NodeKind; a: string; b: string }> = [];
|
||||
private adjacency: { key: string; kind: NodeKind; a: string; b: string }[] = [];
|
||||
|
||||
// Disposition personnalisée : positions sauvegardées sur la campagne, sauvegarde
|
||||
// différée après un drag (évite un PUT par pixel déplacé).
|
||||
@@ -174,7 +174,10 @@ export class CampaignGraphComponent implements OnInit, OnDestroy {
|
||||
if (this.draggingId) return; // pas de changement de focus en plein drag
|
||||
this.hoveredId = node.id;
|
||||
this.hoverNeighbors = new Set(
|
||||
this.adjacency.flatMap(e => e.a === node.id ? [e.b] : e.b === node.id ? [e.a] : []));
|
||||
this.adjacency.flatMap(e => {
|
||||
if (e.a === node.id) return [e.b];
|
||||
return e.b === node.id ? [e.a] : [];
|
||||
}));
|
||||
}
|
||||
|
||||
onNodeLeave(): void {
|
||||
@@ -230,7 +233,7 @@ export class CampaignGraphComponent implements OnInit, OnDestroy {
|
||||
for (const [arcId, chapters] of Object.entries(treeData.chaptersByArc)) {
|
||||
for (const ch of chapters) arcOfChapter.set(ch.id!, arcId);
|
||||
}
|
||||
const linkedScenes: Array<{ scene: Scene; chapterId: string }> = [];
|
||||
const linkedScenes: { scene: Scene; chapterId: string }[] = [];
|
||||
if (!this.hiddenKinds.has('scene')) {
|
||||
for (const [chapterId, scenes] of Object.entries(treeData.scenesByChapter)) {
|
||||
if (!arcOfChapter.has(chapterId)) continue;
|
||||
@@ -268,7 +271,7 @@ export class CampaignGraphComponent implements OnInit, OnDestroy {
|
||||
|
||||
// Arêtes. Les liens page↔page sont dé-dupliqués par paire non-orientée
|
||||
// (A→B et B→A = un seul trait) ; les liens entité→page portent le type de l'entité.
|
||||
const adjacency: Array<{ key: string; kind: NodeKind; a: string; b: string }> = [];
|
||||
const adjacency: { key: string; kind: NodeKind; a: string; b: string }[] = [];
|
||||
const seenPairs = new Set<string>();
|
||||
for (const p of pages) {
|
||||
for (const targetId of p.relatedPageIds ?? []) {
|
||||
@@ -530,14 +533,16 @@ export class CampaignGraphComponent implements OnInit, OnDestroy {
|
||||
if (Math.abs(dx) >= needX || Math.abs(dy) >= needY) continue;
|
||||
const overlapX = needX - Math.abs(dx);
|
||||
const overlapY = needY - Math.abs(dy);
|
||||
// Départage déterministe quand les centres coïncident (dx/dy = 0).
|
||||
const tieBreak = i % 2 === 0 ? 1 : -1;
|
||||
if (overlapX / needX < overlapY / needY) {
|
||||
const shift = overlapX / 2 + 0.5;
|
||||
const sign = dx !== 0 ? Math.sign(dx) : (i % 2 === 0 ? 1 : -1);
|
||||
const sign = dx !== 0 ? Math.sign(dx) : tieBreak;
|
||||
a.x -= sign * shift;
|
||||
b.x += sign * shift;
|
||||
} else {
|
||||
const shift = overlapY / 2 + 0.5;
|
||||
const sign = dy !== 0 ? Math.sign(dy) : (i % 2 === 0 ? 1 : -1);
|
||||
const sign = dy !== 0 ? Math.sign(dy) : tieBreak;
|
||||
a.y -= sign * shift;
|
||||
b.y += sign * shift;
|
||||
}
|
||||
|
||||
@@ -82,7 +82,10 @@ export function buildCampaignTree(
|
||||
// Clé TYPE|ID obligatoire : chaque table a sa propre séquence IDENTITY (une quête
|
||||
// id 6 et une scène id 6 coexistent) — sans le type, un manque de quête allumerait
|
||||
// à tort la pastille d'une scène/PNJ portant le même numéro.
|
||||
const sevRank = (s: ReadinessSeverity): number => (s === 'BLOCKING' ? 2 : s === 'RECOMMENDED' ? 1 : 0);
|
||||
const sevRank = (s: ReadinessSeverity): number => {
|
||||
if (s === 'BLOCKING') return 2;
|
||||
return s === 'RECOMMENDED' ? 1 : 0;
|
||||
};
|
||||
const gapsByKey = new Map<string, ReadinessGap[]>();
|
||||
for (const g of readinessGaps) {
|
||||
const key = `${g.entityType}|${g.entityId}`;
|
||||
|
||||
@@ -69,7 +69,7 @@
|
||||
[placeholder]="'campaignCreate.gameSystemNamePlaceholder' | translate"
|
||||
(keydown.enter)="$event.preventDefault(); submitCreateGameSystem()"
|
||||
(keydown.escape)="cancelCreateGameSystem()"
|
||||
autofocus
|
||||
|
||||
/>
|
||||
<div class="inline-create-actions">
|
||||
<button type="button" class="btn-inline-primary"
|
||||
|
||||
@@ -29,7 +29,7 @@ export interface CampaignCreatePayload {
|
||||
styleUrls: ['./campaign-create.component.scss']
|
||||
})
|
||||
export class CampaignCreateComponent implements OnInit {
|
||||
@Output() close = new EventEmitter<void>();
|
||||
@Output() closed = new EventEmitter<void>();
|
||||
@Output() created = new EventEmitter<CampaignCreatePayload>();
|
||||
|
||||
readonly BookCopy = BookCopy;
|
||||
@@ -131,6 +131,6 @@ export class CampaignCreateComponent implements OnInit {
|
||||
}
|
||||
|
||||
onCancel(): void {
|
||||
this.close.emit();
|
||||
this.closed.emit();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -106,7 +106,7 @@
|
||||
[placeholder]="'campaignDetail.gameSystemNamePlaceholder' | translate"
|
||||
(keydown.enter)="$event.preventDefault(); submitCreateGameSystem()"
|
||||
(keydown.escape)="cancelCreateGameSystem()"
|
||||
autofocus
|
||||
|
||||
/>
|
||||
<div class="inline-create-actions">
|
||||
<button type="button" class="btn-inline-primary"
|
||||
|
||||
@@ -52,6 +52,19 @@ interface NpcNode { name: string; description: string; selected: boolean; existi
|
||||
* Flux : upload → progression streamée → arbre éditable (revue) → création.
|
||||
* Rien n'est créé tant que l'utilisateur n'a pas validé « Créer dans la campagne ».
|
||||
*/
|
||||
|
||||
/** Nettoie les pièces (rooms) d'une scène pour le payload de création. */
|
||||
function cleanRooms(rooms: { name: string; description: string; enemies: string; loot: string }[]) {
|
||||
return rooms
|
||||
.filter(r => r.name.trim())
|
||||
.map(r => ({
|
||||
name: r.name.trim(),
|
||||
description: r.description.trim(),
|
||||
enemies: r.enemies.trim(),
|
||||
loot: r.loot.trim()
|
||||
}));
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'app-campaign-import',
|
||||
imports: [FormsModule, LucideAngularModule, TranslatePipe, FileDropDirective],
|
||||
@@ -401,14 +414,7 @@ export class CampaignImportComponent implements OnInit {
|
||||
playerNarration: s.playerNarration.trim(),
|
||||
gmNotes: s.gmNotes.trim(),
|
||||
existingId: s.existingId ?? null,
|
||||
rooms: s.rooms
|
||||
.filter(r => r.name.trim())
|
||||
.map(r => ({
|
||||
name: r.name.trim(),
|
||||
description: r.description.trim(),
|
||||
enemies: r.enemies.trim(),
|
||||
loot: r.loot.trim()
|
||||
}))
|
||||
rooms: cleanRooms(s.rooms)
|
||||
}))
|
||||
}))
|
||||
})),
|
||||
|
||||
@@ -39,7 +39,7 @@
|
||||
|
||||
@if (showCreateModal) {
|
||||
<app-campaign-create
|
||||
(close)="onModalClose()"
|
||||
(closed)="onModalClose()"
|
||||
(created)="onCampaignCreated($event)">
|
||||
</app-campaign-create>
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Component, OnInit, OnDestroy } from '@angular/core';
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
|
||||
import { ReactiveFormsModule, FormBuilder, FormGroup, Validators } from '@angular/forms';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
@@ -24,7 +24,7 @@ import { CAMPAIGN_ICON_OPTIONS } from '../../campaign-icons';
|
||||
templateUrl: './chapter-create.component.html',
|
||||
styleUrls: ['./chapter-create.component.scss']
|
||||
})
|
||||
export class ChapterCreateComponent implements OnInit, OnDestroy {
|
||||
export class ChapterCreateComponent implements OnInit {
|
||||
readonly campaignIconOptions = CAMPAIGN_ICON_OPTIONS;
|
||||
selectedIcon: string | null = null;
|
||||
|
||||
@@ -88,11 +88,4 @@ export class ChapterCreateComponent implements OnInit, OnDestroy {
|
||||
cancel(): void {
|
||||
this.router.navigate(['/campaigns', this.campaignId]);
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
// Volontairement vide : la sidebar reste prise en charge par le composant
|
||||
// suivant (autre sous-route ou le composant detail parent) qui appellera
|
||||
// show(). Eviter d'appeler hide() ici previent le clignotement / la
|
||||
// disparition de la sidebar lors des navigations internes a la section.
|
||||
}
|
||||
}
|
||||
|
||||
@@ -151,5 +151,5 @@
|
||||
[isOpen]="chatOpen"
|
||||
[welcomeMessage]="'chapterEdit.chatWelcome' | translate"
|
||||
[quickSuggestions]="chatQuickSuggestions"
|
||||
(close)="chatOpen = false">
|
||||
(closed)="chatOpen = false">
|
||||
</app-ai-chat-drawer>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Component, OnInit, OnDestroy, DestroyRef } from '@angular/core';
|
||||
import { Component, OnInit, DestroyRef } from '@angular/core';
|
||||
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
||||
|
||||
import { ReactiveFormsModule, FormBuilder, FormGroup, Validators } from '@angular/forms';
|
||||
@@ -52,7 +52,7 @@ import { SceneDraftPanelComponent } from '../../../shared/scene-draft-panel/scen
|
||||
templateUrl: './chapter-edit.component.html',
|
||||
styleUrls: ['./chapter-edit.component.scss']
|
||||
})
|
||||
export class ChapterEditComponent implements OnInit, OnDestroy {
|
||||
export class ChapterEditComponent implements OnInit {
|
||||
readonly Trash2 = Trash2;
|
||||
readonly Sparkles = Sparkles;
|
||||
readonly campaignIconOptions = CAMPAIGN_ICON_OPTIONS;
|
||||
@@ -220,6 +220,4 @@ export class ChapterEditComponent implements OnInit, OnDestroy {
|
||||
cancel(): void {
|
||||
this.router.navigate(['/campaigns', this.campaignId, 'arcs', this.arcId, 'chapters', this.chapterId]);
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Component, OnInit, OnDestroy, ElementRef, ViewChild, HostListener, DestroyRef } from '@angular/core';
|
||||
import { Component, OnInit, ElementRef, ViewChild, HostListener, DestroyRef } from '@angular/core';
|
||||
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
||||
|
||||
import { FormsModule } from '@angular/forms';
|
||||
@@ -29,7 +29,7 @@ interface GraphEdge { key: string; label: string; kind: LinkType; x1: number; y1
|
||||
templateUrl: './chapter-graph.component.html',
|
||||
styleUrls: ['./chapter-graph.component.scss']
|
||||
})
|
||||
export class ChapterGraphComponent implements OnInit, OnDestroy {
|
||||
export class ChapterGraphComponent implements OnInit {
|
||||
readonly ArrowLeft = ArrowLeft;
|
||||
readonly Plus = Plus;
|
||||
readonly ZoomIn = ZoomIn;
|
||||
@@ -507,7 +507,7 @@ export class ChapterGraphComponent implements OnInit, OnDestroy {
|
||||
scene.graphX = node.x;
|
||||
scene.graphY = node.y;
|
||||
this.campaignService.updateScene(nodeId, { ...scene, order: scene.order ?? 0 })
|
||||
.subscribe({ error: () => {} });
|
||||
.subscribe({ error: () => { /* best-effort : erreur ignorée volontairement */ } });
|
||||
}
|
||||
|
||||
private truncate(text: string): string {
|
||||
@@ -548,7 +548,7 @@ export class ChapterGraphComponent implements OnInit, OnDestroy {
|
||||
};
|
||||
this.campaignService.createScene(payload).subscribe({
|
||||
next: () => this.load(),
|
||||
error: () => {}
|
||||
error: () => { /* best-effort : erreur ignorée volontairement */ }
|
||||
});
|
||||
}
|
||||
|
||||
@@ -592,7 +592,7 @@ export class ChapterGraphComponent implements OnInit, OnDestroy {
|
||||
node.name = name;
|
||||
node.displayName = this.truncate(name);
|
||||
scene.name = name;
|
||||
this.campaignService.updateScene(id, { ...scene, order: scene.order ?? 0 }).subscribe({ error: () => {} });
|
||||
this.campaignService.updateScene(id, { ...scene, order: scene.order ?? 0 }).subscribe({ error: () => { /* best-effort : erreur ignorée volontairement */ } });
|
||||
}
|
||||
|
||||
cancelRename(): void {
|
||||
@@ -635,7 +635,7 @@ export class ChapterGraphComponent implements OnInit, OnDestroy {
|
||||
if (existing.some(b => b.targetSceneId === toId)) return;
|
||||
const next: SceneBranch[] = [...existing, { label: '', targetSceneId: toId, condition: '', kind: 'EXIT' }];
|
||||
this.campaignService.updateScene(fromId, { ...scene, order: scene.order ?? 0, branches: next })
|
||||
.subscribe({ next: () => this.load(), error: () => {} });
|
||||
.subscribe({ next: () => this.load(), error: () => { /* best-effort : erreur ignorée volontairement */ } });
|
||||
}
|
||||
|
||||
// ─────────────── Sélection / édition / suppression d'un lien ───────────────
|
||||
@@ -698,7 +698,7 @@ export class ChapterGraphComponent implements OnInit, OnDestroy {
|
||||
? { ...b, label: this.editEdgeLabel.trim(), condition: this.editEdgeCondition.trim(), kind: this.editEdgeKind }
|
||||
: b);
|
||||
this.campaignService.updateScene(scene.id!, { ...scene, order: scene.order ?? 0, branches })
|
||||
.subscribe({ next: () => { this.selectedEdgeKey = null; this.load(); }, error: () => {} });
|
||||
.subscribe({ next: () => { this.selectedEdgeKey = null; this.load(); }, error: () => { /* best-effort : erreur ignorée volontairement */ } });
|
||||
}
|
||||
|
||||
/** Supprime (sépare) le lien sélectionné. */
|
||||
@@ -709,10 +709,6 @@ export class ChapterGraphComponent implements OnInit, OnDestroy {
|
||||
const { scene, branchIndex } = found;
|
||||
const branches = (scene.branches ?? []).filter((_, i) => i !== branchIndex);
|
||||
this.campaignService.updateScene(scene.id!, { ...scene, order: scene.order ?? 0, branches })
|
||||
.subscribe({ next: () => { this.selectedEdgeKey = null; this.load(); }, error: () => {} });
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
// Volontairement vide : la sidebar reste prise en charge par le composant suivant.
|
||||
.subscribe({ next: () => { this.selectedEdgeKey = null; this.load(); }, error: () => { /* best-effort : erreur ignorée volontairement */ } });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Component, OnInit, OnDestroy, DestroyRef } from '@angular/core';
|
||||
import { Component, OnInit, DestroyRef } from '@angular/core';
|
||||
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
||||
|
||||
import { ActivatedRoute, Router, RouterModule } from '@angular/router';
|
||||
@@ -30,7 +30,7 @@ import { ConfirmDialogService } from '../../../shared/confirm-dialog/confirm-dia
|
||||
templateUrl: './chapter-view.component.html',
|
||||
styleUrls: ['./chapter-view.component.scss']
|
||||
})
|
||||
export class ChapterViewComponent implements OnInit, OnDestroy {
|
||||
export class ChapterViewComponent implements OnInit {
|
||||
readonly Pencil = Pencil;
|
||||
readonly Network = Network;
|
||||
readonly Trash2 = Trash2;
|
||||
@@ -147,11 +147,4 @@ export class ChapterViewComponent implements OnInit, OnDestroy {
|
||||
error: () => console.error('Impossible de récupérer les dépendances du chapitre')
|
||||
});
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
// Volontairement vide : la sidebar reste prise en charge par le composant
|
||||
// suivant (autre sous-route ou le composant detail parent) qui appellera
|
||||
// show(). Eviter d'appeler hide() ici previent le clignotement / la
|
||||
// disparition de la sidebar lors des navigations internes a la section.
|
||||
}
|
||||
}
|
||||
|
||||
@@ -100,6 +100,6 @@
|
||||
[isOpen]="chatOpen"
|
||||
[welcomeMessage]="'characterEdit.chatWelcome' | translate"
|
||||
[quickSuggestions]="chatQuickSuggestions"
|
||||
(close)="chatOpen = false">
|
||||
(closed)="chatOpen = false">
|
||||
</app-ai-chat-drawer>
|
||||
}
|
||||
|
||||
@@ -26,6 +26,6 @@
|
||||
entityType="character"
|
||||
[entityId]="characterId"
|
||||
[isOpen]="chatOpen"
|
||||
(close)="chatOpen = false">
|
||||
(closed)="chatOpen = false">
|
||||
</app-ai-chat-drawer>
|
||||
}
|
||||
|
||||
@@ -57,7 +57,11 @@ export class ItemCatalogViewComponent implements OnInit {
|
||||
map.get(cat)!.push(it);
|
||||
}
|
||||
return [...map.entries()]
|
||||
.sort(([a], [b]) => (a === '—' ? 1 : b === '—' ? -1 : a.localeCompare(b, 'fr')))
|
||||
.sort(([a], [b]) => {
|
||||
if (a === '—') return 1; // catégorie "sans catégorie" toujours en dernier
|
||||
if (b === '—') return -1;
|
||||
return a.localeCompare(b, 'fr');
|
||||
})
|
||||
.map(([category, items]) => ({ category, items }));
|
||||
}
|
||||
|
||||
|
||||
@@ -131,6 +131,6 @@
|
||||
[isOpen]="chatOpen"
|
||||
[welcomeMessage]="'npcEdit.chatWelcome' | translate"
|
||||
[quickSuggestions]="chatQuickSuggestions"
|
||||
(close)="chatOpen = false">
|
||||
(closed)="chatOpen = false">
|
||||
</app-ai-chat-drawer>
|
||||
}
|
||||
|
||||
@@ -43,6 +43,6 @@
|
||||
entityType="npc"
|
||||
[entityId]="npcId"
|
||||
[isOpen]="chatOpen"
|
||||
(close)="chatOpen = false">
|
||||
(closed)="chatOpen = false">
|
||||
</app-ai-chat-drawer>
|
||||
}
|
||||
|
||||
@@ -15,6 +15,11 @@ import { Page } from '../../../services/page.model';
|
||||
import { PersonaViewComponent } from '../../../shared/persona-view/persona-view.component';
|
||||
import { AiChatDrawerComponent } from '../../../shared/ai-chat-drawer/ai-chat-drawer.component';
|
||||
|
||||
/** Indexe des pages par id (les pages venant du serveur ont toujours un id). */
|
||||
function indexPagesById(pages: Page[]): Map<string, Page> {
|
||||
return new Map(pages.map(p => [p.id!, p]));
|
||||
}
|
||||
|
||||
/**
|
||||
* Vue lecture seule "WorldAnvil" d'une fiche PNJ.
|
||||
* Route : /campaigns/:campaignId/npcs/:npcId
|
||||
@@ -89,7 +94,7 @@ export class NpcViewComponent implements OnInit, OnDestroy {
|
||||
if (camp.loreId) {
|
||||
this.loreId = camp.loreId;
|
||||
this.pageService.getByLoreId(camp.loreId).subscribe(pages => {
|
||||
this.lorePagesById = new Map(pages.map(p => [p.id!, p]));
|
||||
this.lorePagesById = indexPagesById(pages);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Component, OnInit, OnDestroy, DestroyRef } from '@angular/core';
|
||||
import { Component, OnInit, DestroyRef } from '@angular/core';
|
||||
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
||||
|
||||
import { ActivatedRoute, Router, RouterModule } from '@angular/router';
|
||||
@@ -34,7 +34,7 @@ import { SessionPrepPanelComponent } from '../../../shared/session-prep-panel/se
|
||||
templateUrl: './playthrough-detail.component.html',
|
||||
styleUrls: ['./playthrough-detail.component.scss']
|
||||
})
|
||||
export class PlaythroughDetailComponent implements OnInit, OnDestroy {
|
||||
export class PlaythroughDetailComponent implements OnInit {
|
||||
readonly ArrowLeft = ArrowLeft;
|
||||
readonly Play = Play;
|
||||
readonly Flag = Flag;
|
||||
@@ -155,6 +155,4 @@ export class PlaythroughDetailComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Component, OnInit, OnDestroy, DestroyRef } from '@angular/core';
|
||||
import { Component, OnInit, DestroyRef } from '@angular/core';
|
||||
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
||||
|
||||
import { ActivatedRoute, Router, RouterModule } from '@angular/router';
|
||||
@@ -26,7 +26,7 @@ import { PlaythroughFlagsManagerComponent } from '../../../shared/playthrough-fl
|
||||
templateUrl: './playthrough-flags-page.component.html',
|
||||
styleUrls: ['./playthrough-flags-page.component.scss']
|
||||
})
|
||||
export class PlaythroughFlagsPageComponent implements OnInit, OnDestroy {
|
||||
export class PlaythroughFlagsPageComponent implements OnInit {
|
||||
readonly ArrowLeft = ArrowLeft;
|
||||
|
||||
campaignId = '';
|
||||
@@ -75,6 +75,4 @@ export class PlaythroughFlagsPageComponent implements OnInit, OnDestroy {
|
||||
back(): void {
|
||||
this.router.navigate(['/campaigns', this.campaignId]);
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Component, OnInit, OnDestroy, DestroyRef } from '@angular/core';
|
||||
import { Component, OnInit, DestroyRef } from '@angular/core';
|
||||
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
||||
import { ReactiveFormsModule, FormBuilder, FormGroup, Validators } from '@angular/forms';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
@@ -41,7 +41,7 @@ import { ConfirmDialogService } from '../../../shared/confirm-dialog/confirm-dia
|
||||
templateUrl: './quest-edit.component.html',
|
||||
styleUrls: ['./quest-edit.component.scss']
|
||||
})
|
||||
export class QuestEditComponent implements OnInit, OnDestroy {
|
||||
export class QuestEditComponent implements OnInit {
|
||||
readonly Trash2 = Trash2;
|
||||
readonly campaignIconOptions = CAMPAIGN_ICON_OPTIONS;
|
||||
selectedIcon: string | null = null;
|
||||
@@ -232,6 +232,4 @@ export class QuestEditComponent implements OnInit, OnDestroy {
|
||||
cancel(): void {
|
||||
this.router.navigate(['/campaigns', this.campaignId, 'quests']);
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Component, OnInit, OnDestroy, DestroyRef } from '@angular/core';
|
||||
import { Component, OnInit, DestroyRef } from '@angular/core';
|
||||
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
||||
import { ActivatedRoute, Router, RouterModule } from '@angular/router';
|
||||
import { forkJoin, of } from 'rxjs';
|
||||
@@ -33,7 +33,7 @@ import { ConfirmDialogService } from '../../../shared/confirm-dialog/confirm-dia
|
||||
templateUrl: './quest-view.component.html',
|
||||
styleUrls: ['./quest-view.component.scss']
|
||||
})
|
||||
export class QuestViewComponent implements OnInit, OnDestroy {
|
||||
export class QuestViewComponent implements OnInit {
|
||||
readonly Pencil = Pencil;
|
||||
readonly Trash2 = Trash2;
|
||||
readonly BookOpen = BookOpen;
|
||||
@@ -199,8 +199,4 @@ export class QuestViewComponent implements OnInit, OnDestroy {
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
// Volontairement vide : la sidebar reste prise en charge par le composant suivant.
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Component, OnInit, OnDestroy } from '@angular/core';
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
|
||||
import { ReactiveFormsModule, FormBuilder, FormGroup, Validators } from '@angular/forms';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
@@ -24,7 +24,7 @@ import { CAMPAIGN_ICON_OPTIONS } from '../../campaign-icons';
|
||||
templateUrl: './scene-create.component.html',
|
||||
styleUrls: ['./scene-create.component.scss']
|
||||
})
|
||||
export class SceneCreateComponent implements OnInit, OnDestroy {
|
||||
export class SceneCreateComponent implements OnInit {
|
||||
readonly campaignIconOptions = CAMPAIGN_ICON_OPTIONS;
|
||||
selectedIcon: string | null = null;
|
||||
|
||||
@@ -90,11 +90,4 @@ export class SceneCreateComponent implements OnInit, OnDestroy {
|
||||
cancel(): void {
|
||||
this.router.navigate(['/campaigns', this.campaignId]);
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
// Volontairement vide : la sidebar reste prise en charge par le composant
|
||||
// suivant (autre sous-route ou le composant detail parent) qui appellera
|
||||
// show(). Eviter d'appeler hide() ici previent le clignotement / la
|
||||
// disparition de la sidebar lors des navigations internes a la section.
|
||||
}
|
||||
}
|
||||
|
||||
@@ -385,5 +385,5 @@
|
||||
[isOpen]="chatOpen"
|
||||
[welcomeMessage]="'sceneEdit.chatWelcome' | translate"
|
||||
[quickSuggestions]="chatQuickSuggestions"
|
||||
(close)="chatOpen = false">
|
||||
(closed)="chatOpen = false">
|
||||
</app-ai-chat-drawer>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Component, OnInit, OnDestroy, DestroyRef } from '@angular/core';
|
||||
import { Component, OnInit, DestroyRef } from '@angular/core';
|
||||
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
||||
|
||||
import { ReactiveFormsModule, FormBuilder, FormGroup, Validators } from '@angular/forms';
|
||||
@@ -67,7 +67,7 @@ interface BattlemapEdit {
|
||||
templateUrl: './scene-edit.component.html',
|
||||
styleUrls: ['./scene-edit.component.scss']
|
||||
})
|
||||
export class SceneEditComponent implements OnInit, OnDestroy {
|
||||
export class SceneEditComponent implements OnInit {
|
||||
readonly Trash2 = Trash2;
|
||||
readonly Sparkles = Sparkles;
|
||||
readonly campaignIconOptions = CAMPAIGN_ICON_OPTIONS;
|
||||
@@ -251,7 +251,7 @@ export class SceneEditComponent implements OnInit, OnDestroy {
|
||||
};
|
||||
if (entry.mediaFileId) {
|
||||
this.storedFileService.getById(entry.mediaFileId)
|
||||
.subscribe({ next: f => entry.mediaName = f.filename, error: () => {} });
|
||||
.subscribe({ next: f => entry.mediaName = f.filename, error: () => { /* best-effort : erreur ignorée volontairement */ } });
|
||||
}
|
||||
if (entry.dataFileId) {
|
||||
this.storedFileService.getById(entry.dataFileId).subscribe({
|
||||
@@ -260,7 +260,7 @@ export class SceneEditComponent implements OnInit, OnDestroy {
|
||||
// Deduit la source : un .dd2vtt/.uvtt => DungeonDraft.
|
||||
if (/\.(dd2vtt|uvtt)$/i.test(f.filename)) entry.source = 'DUNGEONDRAFT';
|
||||
},
|
||||
error: () => {}
|
||||
error: () => { /* best-effort : erreur ignorée volontairement */ }
|
||||
});
|
||||
}
|
||||
return entry;
|
||||
@@ -480,12 +480,14 @@ export class SceneEditComponent implements OnInit, OnDestroy {
|
||||
onDd2vttSelected(bm: BattlemapEdit, event: Event): void {
|
||||
const input = event.target as HTMLInputElement;
|
||||
const file = input.files?.[0];
|
||||
if (file) void this.uploadDd2vtt(bm, file);
|
||||
// Fire-and-forget : uploadDd2vtt gère lui-même ses erreurs.
|
||||
if (file) this.uploadDd2vtt(bm, file);
|
||||
input.value = '';
|
||||
}
|
||||
|
||||
onDd2vttDropped(bm: BattlemapEdit, files: File[]): void {
|
||||
if (files[0]) void this.uploadDd2vtt(bm, files[0]);
|
||||
// Fire-and-forget : uploadDd2vtt gère lui-même ses erreurs.
|
||||
if (files[0]) this.uploadDd2vtt(bm, files[0]);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -552,13 +554,7 @@ export class SceneEditComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
private extForType(type: string): string {
|
||||
return type === 'image/jpeg' ? '.jpg' : type === 'image/webp' ? '.webp' : '.png';
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
// Volontairement vide : la sidebar reste prise en charge par le composant
|
||||
// suivant (autre sous-route ou le composant detail parent) qui appellera
|
||||
// show(). Eviter d'appeler hide() ici previent le clignotement / la
|
||||
// disparition de la sidebar lors des navigations internes a la section.
|
||||
if (type === 'image/jpeg') return '.jpg';
|
||||
return type === 'image/webp' ? '.webp' : '.png';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Component, OnInit, OnDestroy, DestroyRef } from '@angular/core';
|
||||
import { Component, OnInit, DestroyRef } from '@angular/core';
|
||||
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
||||
|
||||
import { ActivatedRoute, Router, RouterModule } from '@angular/router';
|
||||
@@ -31,7 +31,7 @@ import { ConfirmDialogService } from '../../../shared/confirm-dialog/confirm-dia
|
||||
templateUrl: './scene-view.component.html',
|
||||
styleUrls: ['./scene-view.component.scss']
|
||||
})
|
||||
export class SceneViewComponent implements OnInit, OnDestroy {
|
||||
export class SceneViewComponent implements OnInit {
|
||||
readonly Pencil = Pencil;
|
||||
readonly Trash2 = Trash2;
|
||||
readonly resolveCampaignIcon = resolveCampaignIcon;
|
||||
@@ -160,11 +160,4 @@ export class SceneViewComponent implements OnInit, OnDestroy {
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
// Volontairement vide : la sidebar reste prise en charge par le composant
|
||||
// suivant (autre sous-route ou le composant detail parent) qui appellera
|
||||
// show(). Eviter d'appeler hide() ici previent le clignotement / la
|
||||
// disparition de la sidebar lors des navigations internes a la section.
|
||||
}
|
||||
}
|
||||
|
||||
@@ -361,7 +361,10 @@ export class GameSystemEditComponent implements OnInit {
|
||||
};
|
||||
|
||||
for (const line of lines) {
|
||||
const match = line.match(/^##\s+(.+?)\s*$/);
|
||||
// `\S` force un début de titre non-blanc : frontière déterministe entre
|
||||
// `\s+` et la capture (l'ancien `.+?`+`\s*$` backtrackait). Le trim()
|
||||
// en aval retire les espaces de fin.
|
||||
const match = line.match(/^##\s+(\S.*)$/);
|
||||
if (match) {
|
||||
flush();
|
||||
current = { title: match[1].trim(), content: '', collapsed: false };
|
||||
|
||||
@@ -154,10 +154,14 @@ export class BlockGridBuilderComponent implements OnDestroy {
|
||||
|
||||
addLabel(block: TemplateField): void { block.labels = [...(block.labels ?? []), '']; this.emit(); }
|
||||
updateLabel(block: TemplateField, i: number, value: string): void {
|
||||
if (!block.labels) return; block.labels[i] = value; this.emit();
|
||||
if (!block.labels) return;
|
||||
block.labels[i] = value;
|
||||
this.emit();
|
||||
}
|
||||
removeLabel(block: TemplateField, i: number): void {
|
||||
if (!block.labels) return; block.labels = block.labels.filter((_, k) => k !== i); this.emit();
|
||||
if (!block.labels) return;
|
||||
block.labels = block.labels.filter((_, k) => k !== i);
|
||||
this.emit();
|
||||
}
|
||||
|
||||
// --- Glisser : création / déplacement / redimensionnement ---------------
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Component, OnDestroy, OnInit, DestroyRef } from '@angular/core';
|
||||
import { Component, OnInit, DestroyRef } from '@angular/core';
|
||||
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { forkJoin } from 'rxjs';
|
||||
@@ -33,7 +33,7 @@ import { ConfirmDialogService } from '../../shared/confirm-dialog/confirm-dialog
|
||||
templateUrl: './folder-view.component.html',
|
||||
styleUrls: ['./folder-view.component.scss']
|
||||
})
|
||||
export class FolderViewComponent implements OnInit, OnDestroy {
|
||||
export class FolderViewComponent implements OnInit {
|
||||
readonly Folder = Folder;
|
||||
readonly FileText = FileText;
|
||||
readonly Pencil = Pencil;
|
||||
@@ -237,11 +237,4 @@ export class FolderViewComponent implements OnInit, OnDestroy {
|
||||
error: () => console.error('Impossible de récupérer les dépendances du dossier')
|
||||
});
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
// Volontairement vide : la sidebar reste prise en charge par le composant
|
||||
// suivant (autre sous-route ou le composant detail parent) qui appellera
|
||||
// show(). Eviter d'appeler hide() ici previent le clignotement / la
|
||||
// disparition de la sidebar lors des navigations internes a la section.
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ import { TranslatePipe } from '@ngx-translate/core';
|
||||
styleUrls: ['./lore-create.component.scss']
|
||||
})
|
||||
export class LoreCreateComponent {
|
||||
@Output() close = new EventEmitter<void>();
|
||||
@Output() closed = new EventEmitter<void>();
|
||||
@Output() created = new EventEmitter<{ name: string; description: string }>();
|
||||
|
||||
readonly BookCopy = BookCopy;
|
||||
@@ -32,6 +32,6 @@ export class LoreCreateComponent {
|
||||
}
|
||||
|
||||
onCancel(): void {
|
||||
this.close.emit();
|
||||
this.closed.emit();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Component, OnInit, OnDestroy } from '@angular/core';
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
|
||||
import { ReactiveFormsModule, FormBuilder, FormGroup, Validators } from '@angular/forms';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
@@ -11,7 +11,7 @@ import { LayoutService } from '../../services/layout.service';
|
||||
import { LoreNode } from '../../services/lore.model';
|
||||
import { loadLoreSidebarData, buildLoreSidebarConfig } from '../lore-sidebar.helper';
|
||||
import { popReturnTo } from '../return-stack.helper';
|
||||
import { LORE_ICON_OPTIONS, IconOption, resolveIcon } from '../lore-icons';
|
||||
import { LORE_ICON_OPTIONS, IconOption } from '../lore-icons';
|
||||
|
||||
@Component({
|
||||
selector: 'app-lore-node-create',
|
||||
@@ -19,7 +19,7 @@ import { LORE_ICON_OPTIONS, IconOption, resolveIcon } from '../lore-icons';
|
||||
templateUrl: './lore-node-create.component.html',
|
||||
styleUrls: ['./lore-node-create.component.scss']
|
||||
})
|
||||
export class LoreNodeCreateComponent implements OnInit, OnDestroy {
|
||||
export class LoreNodeCreateComponent implements OnInit {
|
||||
|
||||
readonly iconOptions: IconOption[] = LORE_ICON_OPTIONS;
|
||||
|
||||
@@ -109,11 +109,4 @@ export class LoreNodeCreateComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
this.router.navigate(['/lore', this.loreId]);
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
// Volontairement vide : la sidebar reste prise en charge par le composant
|
||||
// suivant (autre sous-route ou le composant detail parent) qui appellera
|
||||
// show(). Eviter d'appeler hide() ici previent le clignotement / la
|
||||
// disparition de la sidebar lors des navigations internes a la section.
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Component, OnInit, OnDestroy, DestroyRef } from '@angular/core';
|
||||
import { Component, OnInit, DestroyRef } from '@angular/core';
|
||||
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
||||
|
||||
import { ReactiveFormsModule, FormBuilder, FormGroup, Validators } from '@angular/forms';
|
||||
@@ -38,7 +38,7 @@ import { LORE_ICON_OPTIONS, IconOption } from '../lore-icons';
|
||||
templateUrl: './lore-node-edit.component.html',
|
||||
styleUrls: ['./lore-node-edit.component.scss']
|
||||
})
|
||||
export class LoreNodeEditComponent implements OnInit, OnDestroy {
|
||||
export class LoreNodeEditComponent implements OnInit {
|
||||
|
||||
readonly iconOptions: IconOption[] = LORE_ICON_OPTIONS;
|
||||
|
||||
@@ -147,11 +147,4 @@ export class LoreNodeEditComponent implements OnInit, OnDestroy {
|
||||
if (!key) return null;
|
||||
return this.iconOptions.find(o => o.key === key)?.icon ?? null;
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
// Volontairement vide : la sidebar reste prise en charge par le composant
|
||||
// suivant (autre sous-route ou le composant detail parent) qui appellera
|
||||
// show(). Eviter d'appeler hide() ici previent le clignotement / la
|
||||
// disparition de la sidebar lors des navigations internes a la section.
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,7 +39,7 @@
|
||||
|
||||
@if (showCreateModal) {
|
||||
<app-lore-create
|
||||
(close)="onModalClose()"
|
||||
(closed)="onModalClose()"
|
||||
(created)="onLoreCreated($event)">
|
||||
</app-lore-create>
|
||||
}
|
||||
|
||||
@@ -111,7 +111,7 @@
|
||||
[systemPromptAddon]="wizardSystemPrompt"
|
||||
[quickSuggestions]="wizardSuggestions"
|
||||
[primaryAction]="wizardPrimaryAction"
|
||||
(close)="closeWizard()"
|
||||
(closed)="closeWizard()"
|
||||
(assistantReply)="onWizardReply($event)"
|
||||
(primaryActionClick)="applyWizardAndCreate()">
|
||||
</app-ai-chat-drawer>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Component, OnInit, OnDestroy, DestroyRef } from '@angular/core';
|
||||
import { Component, OnInit, DestroyRef } from '@angular/core';
|
||||
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
||||
|
||||
import { ReactiveFormsModule, FormBuilder, FormGroup, Validators } from '@angular/forms';
|
||||
@@ -32,7 +32,7 @@ import { AiChatDrawerComponent, ChatPrimaryAction } from '../../shared/ai-chat-d
|
||||
templateUrl: './page-create.component.html',
|
||||
styleUrls: ['./page-create.component.scss']
|
||||
})
|
||||
export class PageCreateComponent implements OnInit, OnDestroy {
|
||||
export class PageCreateComponent implements OnInit {
|
||||
readonly FileText = FileText;
|
||||
readonly Sparkles = Sparkles;
|
||||
readonly Plus = Plus;
|
||||
@@ -165,7 +165,7 @@ export class PageCreateComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
private restoreDraft(): void {
|
||||
let raw: string | null = null;
|
||||
let raw: string | null;
|
||||
try { raw = sessionStorage.getItem(this.draftKey); } catch { return; }
|
||||
if (!raw) return;
|
||||
sessionStorage.removeItem(this.draftKey);
|
||||
@@ -328,7 +328,9 @@ Les clés du JSON doivent correspondre EXACTEMENT aux noms de champs indiqués.
|
||||
* Retourne null si absent ou JSON invalide.
|
||||
*/
|
||||
private extractValuesBlock(reply: string): Record<string, string> | null {
|
||||
const match = reply.match(/<values>\s*([\s\S]*?)\s*<\/values>/i);
|
||||
// Pas de \s* autour de la capture (backtracking quadratique) :
|
||||
// JSON.parse tolère nativement les blancs en tête/queue.
|
||||
const match = reply.match(/<values>([\s\S]*?)<\/values>/i);
|
||||
if (!match) return null;
|
||||
try {
|
||||
const parsed = JSON.parse(match[1]) as Record<string, unknown>;
|
||||
@@ -342,11 +344,4 @@ Les clés du JSON doivent correspondre EXACTEMENT aux noms de champs indiqués.
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
// Volontairement vide : la sidebar reste prise en charge par le composant
|
||||
// suivant (autre sous-route ou le composant detail parent) qui appellera
|
||||
// show(). Eviter d'appeler hide() ici previent le clignotement / la
|
||||
// disparition de la sidebar lors des navigations internes a la section.
|
||||
}
|
||||
}
|
||||
|
||||
@@ -195,6 +195,6 @@
|
||||
[isOpen]="chatOpen"
|
||||
[quickSuggestions]="chatQuickSuggestions"
|
||||
[primaryAction]="chatPrimaryAction"
|
||||
(close)="chatOpen = false"
|
||||
(closed)="chatOpen = false"
|
||||
(primaryActionClick)="onChatFillRequested()">
|
||||
</app-ai-chat-drawer>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Component, OnInit, OnDestroy, DestroyRef } from '@angular/core';
|
||||
import { Component, OnInit, DestroyRef } from '@angular/core';
|
||||
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
||||
|
||||
import { FormsModule } from '@angular/forms';
|
||||
@@ -44,7 +44,7 @@ import { ConfirmDialogService } from '../../shared/confirm-dialog/confirm-dialog
|
||||
templateUrl: './page-edit.component.html',
|
||||
styleUrls: ['./page-edit.component.scss']
|
||||
})
|
||||
export class PageEditComponent implements OnInit, OnDestroy {
|
||||
export class PageEditComponent implements OnInit {
|
||||
readonly Sparkles = Sparkles;
|
||||
readonly Plus = Plus;
|
||||
readonly Trash2 = Trash2;
|
||||
@@ -84,7 +84,7 @@ export class PageEditComponent implements OnInit, OnDestroy {
|
||||
/** Valeurs des champs KEY_VALUE_LIST (liste clé/valeur) : fieldName → (label → valeur). */
|
||||
keyValueValues: Record<string, Record<string, string>> = {};
|
||||
/** Valeurs des champs TABLE : fieldName → lignes (colonne → cellule). */
|
||||
tableValues: Record<string, Array<Record<string, string>>> = {};
|
||||
tableValues: Record<string, Record<string, string>[]> = {};
|
||||
/** Étiquettes libres (Phase 5B). */
|
||||
tags: string[] = [];
|
||||
/** IDs des pages liées (Phase 5B). */
|
||||
@@ -200,7 +200,7 @@ export class PageEditComponent implements OnInit, OnDestroy {
|
||||
const imageBase: Record<string, string[]> = {};
|
||||
const framingBase: Record<string, Record<string, ImageFraming>> = {};
|
||||
const kvBase: Record<string, Record<string, string>> = {};
|
||||
const tableBase: Record<string, Array<Record<string, string>>> = {};
|
||||
const tableBase: Record<string, Record<string, string>[]> = {};
|
||||
// Les valeurs sont rangées par clé STABLE (id) ; on relit d'abord par clé,
|
||||
// puis par nom (pages dont les valeurs étaient encore rangées par nom — elles
|
||||
// sont ainsi migrées vers la clé id à la prochaine sauvegarde).
|
||||
@@ -259,7 +259,9 @@ export class PageEditComponent implements OnInit, OnDestroy {
|
||||
addTableRow(fieldName: string, columns: string[] | null | undefined): void {
|
||||
const row: Record<string, string> = {};
|
||||
for (const col of columns ?? []) row[col] = '';
|
||||
(this.tableValues[fieldName] ??= []).push(row);
|
||||
const rows = this.tableValues[fieldName] ?? [];
|
||||
rows.push(row);
|
||||
this.tableValues[fieldName] = rows;
|
||||
}
|
||||
|
||||
removeTableRow(fieldName: string, rowIndex: number): void {
|
||||
@@ -267,7 +269,7 @@ export class PageEditComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
/** Lignes du tableau d'un champ — toujours un tableau (jamais undefined) pour le `@for`. */
|
||||
tableRows(fieldName: string): Array<Record<string, string>> {
|
||||
tableRows(fieldName: string): Record<string, string>[] {
|
||||
return this.tableValues[fieldName] ?? [];
|
||||
}
|
||||
|
||||
@@ -343,11 +345,4 @@ export class PageEditComponent implements OnInit, OnDestroy {
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
// Volontairement vide : la sidebar reste prise en charge par le composant
|
||||
// suivant (autre sous-route ou le composant detail parent) qui appellera
|
||||
// show(). Eviter d'appeler hide() ici previent le clignotement / la
|
||||
// disparition de la sidebar lors des navigations internes a la section.
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Component, OnInit, OnDestroy, DestroyRef } from '@angular/core';
|
||||
import { Component, OnInit, DestroyRef } from '@angular/core';
|
||||
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
||||
|
||||
import { ActivatedRoute, Router, RouterModule } from '@angular/router';
|
||||
@@ -36,7 +36,7 @@ import { ConfirmDialogService } from '../../shared/confirm-dialog/confirm-dialog
|
||||
templateUrl: './page-view.component.html',
|
||||
styleUrls: ['./page-view.component.scss']
|
||||
})
|
||||
export class PageViewComponent implements OnInit, OnDestroy {
|
||||
export class PageViewComponent implements OnInit {
|
||||
readonly Pencil = Pencil;
|
||||
readonly Trash2 = Trash2;
|
||||
|
||||
@@ -155,7 +155,7 @@ export class PageViewComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
/** Lignes d'un champ TABLE (liste vide si jamais rempli). */
|
||||
tableRowsOf(field: TemplateField): Array<Record<string, string>> {
|
||||
tableRowsOf(field: TemplateField): Record<string, string>[] {
|
||||
const v = this.page?.tableValues;
|
||||
return v?.[blockKey(field)] ?? v?.[field.name] ?? [];
|
||||
}
|
||||
@@ -196,11 +196,4 @@ export class PageViewComponent implements OnInit, OnDestroy {
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
// Volontairement vide : la sidebar reste prise en charge par le composant
|
||||
// suivant (autre sous-route ou le composant detail parent) qui appellera
|
||||
// show(). Eviter d'appeler hide() ici previent le clignotement / la
|
||||
// disparition de la sidebar lors des navigations internes a la section.
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Component, OnInit, OnDestroy } from '@angular/core';
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
|
||||
import { ReactiveFormsModule, FormBuilder, FormGroup, Validators } from '@angular/forms';
|
||||
import { ActivatedRoute, Router, RouterModule } from '@angular/router';
|
||||
@@ -24,7 +24,7 @@ import { popReturnTo } from '../return-stack.helper';
|
||||
templateUrl: './template-create.component.html',
|
||||
styleUrls: ['./template-create.component.scss']
|
||||
})
|
||||
export class TemplateCreateComponent implements OnInit, OnDestroy {
|
||||
export class TemplateCreateComponent implements OnInit {
|
||||
form: FormGroup;
|
||||
loreId = '';
|
||||
nodes: LoreNode[] = [];
|
||||
@@ -85,7 +85,7 @@ export class TemplateCreateComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
private restoreDraft(): void {
|
||||
let raw: string | null = null;
|
||||
let raw: string | null;
|
||||
try { raw = sessionStorage.getItem(this.draftKey); } catch { return; }
|
||||
if (!raw) return;
|
||||
sessionStorage.removeItem(this.draftKey);
|
||||
@@ -146,11 +146,4 @@ export class TemplateCreateComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
this.router.navigate(['/lore', this.loreId]);
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
// Volontairement vide : la sidebar reste prise en charge par le composant
|
||||
// suivant (autre sous-route ou le composant detail parent) qui appellera
|
||||
// show(). Eviter d'appeler hide() ici previent le clignotement / la
|
||||
// disparition de la sidebar lors des navigations internes a la section.
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { Observable, catchError, of } from 'rxjs';
|
||||
import { Observable, catchError, map, of } from 'rxjs';
|
||||
|
||||
/**
|
||||
* Reflet de LicenseStatus (enum cote backend).
|
||||
@@ -44,13 +44,13 @@ export interface BetaStatusDTO {
|
||||
enabled: boolean;
|
||||
updateAvailable: boolean;
|
||||
anyUnknown: boolean;
|
||||
images: Array<{
|
||||
images: {
|
||||
image: string;
|
||||
localVersion: string | null;
|
||||
remoteVersion: string | null;
|
||||
status: 'UP_TO_DATE' | 'UPDATE_AVAILABLE' | 'UNKNOWN';
|
||||
updateAvailable: boolean;
|
||||
}>;
|
||||
}[];
|
||||
checkedAt: string;
|
||||
disabledReason: string | null;
|
||||
}
|
||||
@@ -86,10 +86,13 @@ export class LicenseService {
|
||||
|
||||
disconnect(): Observable<boolean> {
|
||||
return this.http.delete<void>(this.apiUrl, this.authOptions).pipe(
|
||||
// Convertit en boolean : true = succes, false = erreur
|
||||
// (catchError plus bas masque les detail HTTP)
|
||||
catchError(() => of(false as any))
|
||||
) as unknown as Observable<boolean>;
|
||||
// Convertit en boolean : true = succes, false = erreur.
|
||||
// (Avant : le cast `as unknown as` masquait l'absence de map() — le
|
||||
// succes emettait `undefined`, donc falsy. Consommateur actuel ignore
|
||||
// la valeur, mais le type est desormais honnete.)
|
||||
map(() => true),
|
||||
catchError(() => of(false))
|
||||
);
|
||||
}
|
||||
|
||||
refresh(): Observable<LicenseStatusDTO | null> {
|
||||
|
||||
@@ -48,7 +48,9 @@ export interface NotebookAction {
|
||||
resolution?: string;
|
||||
}
|
||||
|
||||
const ACTION_RE = /```loremind-action\s*([\s\S]*?)```/g;
|
||||
// Pas de \s* avant la capture (backtracking quadratique) : la capture est
|
||||
// trim()ée avant JSON.parse de toute façon.
|
||||
const ACTION_RE = /```loremind-action([\s\S]*?)```/g;
|
||||
const VALID_TYPES = new Set(['npc', 'scene', 'chapter', 'arc', 'table']);
|
||||
|
||||
/**
|
||||
|
||||
@@ -6,6 +6,13 @@ import { Notebook, NotebookArchive, NotebookDetail, NotebookSource, NotebookChat
|
||||
import { LanguageService } from './language.service';
|
||||
import { parseSseStream } from '../shared/sse.util';
|
||||
|
||||
/** Normalise les sources renvoyées par le Brain (événement SSE `sources`). */
|
||||
function mapSseSources(sources: { source_id?: string; page?: number | null; score?: number }[] | undefined) {
|
||||
return (sources ?? []).map(s => ({
|
||||
sourceId: s.source_id ?? '', page: s.page ?? null, score: s.score ?? 0
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Service des notebooks (atelier RAG) : CRUD, upload/indexation de sources,
|
||||
* et chat ancré STREAMÉ (SSE via fetch, comme ai-chat.service).
|
||||
@@ -96,12 +103,7 @@ export class NotebookService {
|
||||
} else if (event === 'sources') {
|
||||
try {
|
||||
const o = JSON.parse(data) as { sources?: { source_id?: string; page?: number | null; score?: number }[] };
|
||||
emit({
|
||||
type: 'sources',
|
||||
sources: (o.sources ?? []).map(s => ({
|
||||
sourceId: s.source_id ?? '', page: s.page ?? null, score: s.score ?? 0
|
||||
}))
|
||||
});
|
||||
emit({ type: 'sources', sources: mapSseSources(o.sources) });
|
||||
} catch { /* ignore */ }
|
||||
} else if (event === 'progress') {
|
||||
try {
|
||||
|
||||
@@ -40,7 +40,7 @@ export interface Page {
|
||||
* Pour chaque champ TABLE (colonnes figees au template, lignes libres) :
|
||||
* fieldName → lignes ordonnees, chaque ligne = colonne → cellule.
|
||||
*/
|
||||
tableValues?: Record<string, Array<Record<string, string>>>;
|
||||
tableValues?: Record<string, Record<string, string>[]>;
|
||||
notes?: string | null;
|
||||
tags?: string[];
|
||||
relatedPageIds?: string[];
|
||||
|
||||
@@ -46,9 +46,10 @@ export class VersionCheckerService {
|
||||
*/
|
||||
start(): void {
|
||||
if (this.timer !== null) return;
|
||||
void this.fetchInitial();
|
||||
// Fire-and-forget : fetchInitial/poll gèrent leurs erreurs en interne.
|
||||
this.fetchInitial();
|
||||
this.timer = setInterval(
|
||||
() => void this.poll(),
|
||||
() => { this.poll(); },
|
||||
VersionCheckerService.POLL_INTERVAL_MS,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
name="editName"
|
||||
(keydown.enter)="saveRename()"
|
||||
(keydown.escape)="cancelRename()"
|
||||
autofocus />
|
||||
/>
|
||||
<button type="button" class="btn-icon" (click)="saveRename()" [disabled]="!editName.trim()" [title]="'common.validate' | translate">
|
||||
<lucide-icon [img]="Check" [size]="14"></lucide-icon>
|
||||
</button>
|
||||
|
||||
@@ -281,11 +281,14 @@ export class SessionDetailComponent implements OnInit, OnDestroy {
|
||||
if (!this.session) return;
|
||||
const session = this.session;
|
||||
const entryCount = this.entries.length;
|
||||
const entriesDetail = entryCount === 0
|
||||
? this.translate.instant('sessionDetail.deleteConfirm.noEntries')
|
||||
: entryCount === 1
|
||||
? this.translate.instant('sessionDetail.deleteConfirm.entriesOne')
|
||||
: this.translate.instant('sessionDetail.deleteConfirm.entriesMany', { n: entryCount });
|
||||
let entriesDetail: string;
|
||||
if (entryCount === 0) {
|
||||
entriesDetail = this.translate.instant('sessionDetail.deleteConfirm.noEntries');
|
||||
} else if (entryCount === 1) {
|
||||
entriesDetail = this.translate.instant('sessionDetail.deleteConfirm.entriesOne');
|
||||
} else {
|
||||
entriesDetail = this.translate.instant('sessionDetail.deleteConfirm.entriesMany', { n: entryCount });
|
||||
}
|
||||
const details = [
|
||||
entriesDetail,
|
||||
this.translate.instant('sessionDetail.deleteConfirm.irreversible')
|
||||
|
||||
@@ -58,7 +58,9 @@ export class SessionDicePanelComponent {
|
||||
}
|
||||
const sumRolls = rolls.reduce((s, n) => s + n, 0);
|
||||
const total = sumRolls + this.modifier;
|
||||
const modPart = this.modifier === 0 ? '' : (this.modifier > 0 ? `+${this.modifier}` : `${this.modifier}`);
|
||||
let modPart = '';
|
||||
if (this.modifier > 0) modPart = `+${this.modifier}`;
|
||||
else if (this.modifier < 0) modPart = `${this.modifier}`;
|
||||
const notation = `${safeCount}d${this.selectedFace}${modPart}`;
|
||||
const detailsPart = rolls.length > 1 ? ` [${rolls.join(', ')}]` : '';
|
||||
const summary = `🎲 ${notation}${detailsPart} = ${total}`;
|
||||
|
||||
@@ -52,7 +52,11 @@ export class SessionItemCatalogsPanelComponent implements OnInit {
|
||||
map.get(cat)!.push(it);
|
||||
}
|
||||
return [...map.entries()]
|
||||
.sort(([a], [b]) => (a === '—' ? 1 : b === '—' ? -1 : a.localeCompare(b, 'fr')))
|
||||
.sort(([a], [b]) => {
|
||||
if (a === '—') return 1; // catégorie "sans catégorie" toujours en dernier
|
||||
if (b === '—') return -1;
|
||||
return a.localeCompare(b, 'fr');
|
||||
})
|
||||
.map(([category, items]) => ({ category, items }));
|
||||
}
|
||||
|
||||
|
||||
@@ -209,9 +209,10 @@ export class OllamaModelManagerComponent implements OnDestroy {
|
||||
});
|
||||
}
|
||||
|
||||
private extractError(err: any, fallback: string): string {
|
||||
if (err?.error?.detail) return String(err.error.detail);
|
||||
if (err?.message) return err.message;
|
||||
private extractError(err: unknown, fallback: string): string {
|
||||
const e = err as { error?: { detail?: unknown }; message?: string } | null;
|
||||
if (e?.error?.detail) return String(e.error.detail);
|
||||
if (e?.message) return e.message;
|
||||
return fallback;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,7 +56,7 @@ export class SettingsComponent implements OnInit {
|
||||
/** Catalogue Gemini (dynamique si cle configuree, repli statique sinon). */
|
||||
geminiModels: GeminiModel[] = [];
|
||||
/** Fournisseur 1min.ai actuellement selectionne (filtre la liste des modeles). */
|
||||
oneminProvider: string = '';
|
||||
oneminProvider = '';
|
||||
|
||||
loadingModels = false;
|
||||
saving = false;
|
||||
@@ -419,9 +419,10 @@ export class SettingsComponent implements OnInit {
|
||||
});
|
||||
}
|
||||
|
||||
private extractError(err: any, fallback: string): string {
|
||||
if (err?.error?.detail) return String(err.error.detail);
|
||||
if (err?.message) return err.message;
|
||||
private extractError(err: unknown, fallback: string): string {
|
||||
const e = err as { error?: { detail?: unknown }; message?: string } | null;
|
||||
if (e?.error?.detail) return String(e.error.detail);
|
||||
if (e?.message) return e.message;
|
||||
return fallback;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -130,11 +130,12 @@ export class UpdatesSectionComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
this.licenseError = '';
|
||||
this.licenseService.install(jwt).subscribe((res) => {
|
||||
if ((res as any)?.error) {
|
||||
this.licenseError = (res as any).error;
|
||||
// install() renvoie une union typée : le garde `in` suffit à discriminer.
|
||||
if ('error' in res) {
|
||||
this.licenseError = res.error;
|
||||
return;
|
||||
}
|
||||
this.licenseStatus = res as LicenseStatusDTO;
|
||||
this.licenseStatus = res;
|
||||
this.licenseJwtInput = '';
|
||||
this.licenseSuccess = this.translate.instant('updatesSection.patreonConnectedSuccess');
|
||||
if (this.licenseStatus.betaChannelEnabled) {
|
||||
|
||||
@@ -80,7 +80,7 @@
|
||||
(keyup.enter)="submitRenameTitle()"
|
||||
(keyup.escape)="cancelRenameTitle()"
|
||||
(blur)="submitRenameTitle()"
|
||||
autofocus />
|
||||
/>
|
||||
}
|
||||
} @else {
|
||||
<h2 class="header-title">{{ 'aiChatDrawer.title' | translate }}</h2>
|
||||
|
||||
@@ -88,7 +88,7 @@ export class AiChatDrawerComponent implements OnInit, OnChanges, OnDestroy {
|
||||
/** Persistance activee ? false = mode wizard ephemere. */
|
||||
@Input() persistent = true;
|
||||
|
||||
@Output() close = new EventEmitter<void>();
|
||||
@Output() closed = new EventEmitter<void>();
|
||||
@Output() primaryActionClick = new EventEmitter<void>();
|
||||
@Output() assistantReply = new EventEmitter<string>();
|
||||
|
||||
@@ -180,7 +180,7 @@ export class AiChatDrawerComponent implements OnInit, OnChanges, OnDestroy {
|
||||
this.isWide = !this.isWide;
|
||||
try {
|
||||
localStorage.setItem(this.LS_WIDE, this.isWide ? '1' : '0');
|
||||
} catch {}
|
||||
} catch { /* localStorage indisponible : ignoré */ }
|
||||
}
|
||||
|
||||
/** Debut du drag : enregistre la position de depart + abonne listeners globaux. */
|
||||
@@ -216,7 +216,7 @@ export class AiChatDrawerComponent implements OnInit, OnChanges, OnDestroy {
|
||||
if (this.customWidth !== null) {
|
||||
try {
|
||||
localStorage.setItem(this.LS_WIDTH, String(this.customWidth));
|
||||
} catch {}
|
||||
} catch { /* localStorage indisponible : ignoré */ }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -231,7 +231,7 @@ export class AiChatDrawerComponent implements OnInit, OnChanges, OnDestroy {
|
||||
}
|
||||
}
|
||||
this.isWide = localStorage.getItem(this.LS_WIDE) === '1';
|
||||
} catch {}
|
||||
} catch { /* localStorage indisponible : ignoré */ }
|
||||
}
|
||||
|
||||
ngOnChanges(changes: SimpleChanges): void {
|
||||
@@ -374,7 +374,7 @@ export class AiChatDrawerComponent implements OnInit, OnChanges, OnDestroy {
|
||||
|
||||
onClose(): void {
|
||||
this.abortStream();
|
||||
this.close.emit();
|
||||
this.closed.emit();
|
||||
}
|
||||
|
||||
send(): void {
|
||||
@@ -450,7 +450,7 @@ export class AiChatDrawerComponent implements OnInit, OnChanges, OnDestroy {
|
||||
this.scrollToBottom();
|
||||
|
||||
if (convId) {
|
||||
this.conversationService.appendMessage(convId, 'user', text).subscribe({ error: () => {} });
|
||||
this.conversationService.appendMessage(convId, 'user', text).subscribe({ error: () => { /* best-effort : erreur ignorée volontairement */ } });
|
||||
}
|
||||
|
||||
this.streamSub = this.buildStream().subscribe({
|
||||
@@ -477,7 +477,7 @@ export class AiChatDrawerComponent implements OnInit, OnChanges, OnDestroy {
|
||||
next: () => {
|
||||
if (wasEmpty) this.triggerAutoTitle(convId);
|
||||
},
|
||||
error: () => {},
|
||||
error: () => { /* best-effort : erreur ignorée volontairement */ },
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -505,7 +505,7 @@ export class AiChatDrawerComponent implements OnInit, OnChanges, OnDestroy {
|
||||
c.id === convId ? { ...c, title } : c,
|
||||
);
|
||||
},
|
||||
error: () => {},
|
||||
error: () => { /* best-effort : erreur ignorée volontairement */ },
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ import { TranslatePipe } from '@ngx-translate/core';
|
||||
*/
|
||||
export interface BreadcrumbItem {
|
||||
label: string;
|
||||
route?: string | any[];
|
||||
route?: string | unknown[];
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -26,7 +26,9 @@ export class DiceUtils {
|
||||
|
||||
/** Parse une formule simple `[N]dM`. Renvoie null si invalide. */
|
||||
static parse(formula: string | null | undefined): ParsedDice | null {
|
||||
const m = /^\s*(\d*)\s*[dD]\s*(\d+)\s*$/.exec(formula ?? '');
|
||||
// trim() préalable au lieu de \s* aux extrémités : `\s*(\d*)\s*` était
|
||||
// ambigu (quadratique) quand \d* est vide. Même langage accepté.
|
||||
const m = /^(\d*)\s*[dD]\s*(\d+)$/.exec((formula ?? '').trim());
|
||||
if (!m) return null;
|
||||
const count = m[1] ? parseInt(m[1], 10) : 1;
|
||||
const faces = parseInt(m[2], 10);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Component, Input } from '@angular/core';
|
||||
import { Component, Input, OnInit } from '@angular/core';
|
||||
|
||||
import { LucideAngularModule, ChevronDown, ChevronUp } from 'lucide-angular';
|
||||
|
||||
@@ -17,7 +17,7 @@ import { LucideAngularModule, ChevronDown, ChevronUp } from 'lucide-angular';
|
||||
templateUrl: './expandable-section.component.html',
|
||||
styleUrls: ['./expandable-section.component.scss']
|
||||
})
|
||||
export class ExpandableSectionComponent {
|
||||
export class ExpandableSectionComponent implements OnInit {
|
||||
readonly ChevronDown = ChevronDown;
|
||||
readonly ChevronUp = ChevronUp;
|
||||
|
||||
|
||||
@@ -12,24 +12,34 @@ import { PageService } from '../../services/page.service';
|
||||
import { TemplateService } from '../../services/template.service';
|
||||
import { CampaignService } from '../../services/campaign.service';
|
||||
import { NpcService } from '../../services/npc.service';
|
||||
import { CharacterService } from '../../services/character.service';
|
||||
import { CharacterService, CharacterSearchResult } from '../../services/character.service';
|
||||
import { RandomTableService } from '../../services/random-table.service';
|
||||
import { ItemCatalogService } from '../../services/item-catalog.service';
|
||||
import { EnemyService } from '../../services/enemy.service';
|
||||
import { Lore, LoreNode } from '../../services/lore.model';
|
||||
import { Template } from '../../services/template.model';
|
||||
import { Page } from '../../services/page.model';
|
||||
import { Campaign } from '../../services/campaign.model';
|
||||
import { Npc } from '../../services/npc.model';
|
||||
import { RandomTable } from '../../services/random-table.model';
|
||||
import { ItemCatalog } from '../../services/item-catalog.model';
|
||||
import { Enemy } from '../../services/enemy.model';
|
||||
|
||||
type ResultKind =
|
||||
| 'lore' | 'node' | 'template' | 'page' | 'campaign'
|
||||
| 'npc' | 'character' | 'random-table' | 'item-catalog' | 'enemy';
|
||||
|
||||
interface SearchResult {
|
||||
id: string;
|
||||
/** Optionnel dans les DTOs (objets pas encore persistés) mais toujours
|
||||
* présent sur des résultats de recherche serveur. */
|
||||
id: string | undefined;
|
||||
kind: ResultKind;
|
||||
title: string;
|
||||
subtitle: string;
|
||||
/** Tag affiché sous le titre (ex: "Lore", "Dossier", "Template", "Page"). */
|
||||
tag: string;
|
||||
/** Route Angular (array pour router.navigate). */
|
||||
route: any[];
|
||||
route: (string | undefined)[];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -140,8 +150,8 @@ export class GlobalSearchComponent implements OnInit, OnDestroy {
|
||||
* noeuds/templates, et enfin les racines (campagnes, lores).
|
||||
*/
|
||||
private buildResults(r: {
|
||||
lores: any[]; nodes: any[]; templates: any[]; pages: any[]; campaigns: any[];
|
||||
npcs: any[]; characters: any[]; tables: any[]; catalogs: any[]; enemies: any[];
|
||||
lores: Lore[]; nodes: LoreNode[]; templates: Template[]; pages: Page[]; campaigns: Campaign[];
|
||||
npcs: Npc[]; characters: CharacterSearchResult[]; tables: RandomTable[]; catalogs: ItemCatalog[]; enemies: Enemy[];
|
||||
}): SearchResult[] {
|
||||
const { lores, nodes, templates, pages, campaigns, npcs, characters, tables, catalogs, enemies } = r;
|
||||
const pageResults: SearchResult[] = pages.map(p => ({
|
||||
|
||||
@@ -130,7 +130,7 @@ export class ImageBlockComponent implements OnDestroy {
|
||||
const id = this.currentId;
|
||||
if (!id) return;
|
||||
// Best-effort côté serveur (pas d'orpheline) ; on n'attend pas la réponse.
|
||||
this.imageService.delete(id).subscribe({ error: () => {} });
|
||||
this.imageService.delete(id).subscribe({ error: () => { /* best-effort : erreur ignorée volontairement */ } });
|
||||
const ids = this.imageIds.filter(i => i !== id);
|
||||
if (this.framing?.[id]) {
|
||||
const next = { ...this.framing };
|
||||
|
||||
@@ -96,7 +96,7 @@ export class ImageGalleryComponent {
|
||||
event.stopPropagation(); // Evite d'ouvrir le lightbox en cliquant sur X.
|
||||
// On supprime aussi cote serveur pour ne pas laisser d'image orpheline.
|
||||
// Best-effort : on n'attend pas le retour pour emettre la nouvelle liste.
|
||||
this.imageService.delete(id).subscribe({ error: () => {} });
|
||||
this.imageService.delete(id).subscribe({ error: () => { /* best-effort : erreur ignorée volontairement */ } });
|
||||
this.imageIdsChange.emit(this.imageIds.filter(i => i !== id));
|
||||
}
|
||||
|
||||
|
||||
@@ -18,6 +18,10 @@ export class MarkdownPipe implements PipeTransform {
|
||||
if (!value) return '';
|
||||
const html = marked.parse(value, { async: false, gfm: true, breaks: true }) as string;
|
||||
const clean = DOMPurify.sanitize(html);
|
||||
// Revue sécurité : le bypass est sûr ICI car le HTML vient d'être passé
|
||||
// par DOMPurify juste au-dessus (le sanitizer Angular, moins permissif,
|
||||
// casserait le rendu markdown). Ne jamais bypasser sans DOMPurify amont.
|
||||
// eslint-disable-next-line sonarjs/no-angular-bypass-sanitization
|
||||
return this.sanitizer.bypassSecurityTrustHtml(clean);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,8 +36,9 @@
|
||||
</button>
|
||||
}
|
||||
|
||||
<!-- Mode normal : spacer + outils -->
|
||||
@if (!(layoutConfig$ | async)) {
|
||||
<!-- Mode normal : spacer + outils. `=== null` (et pas `!`) : l'async pipe
|
||||
émet null tant que rien n'est reçu, la négation brute serait ambiguë. -->
|
||||
@if ((layoutConfig$ | async) === null) {
|
||||
<div class="spacer"></div>
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user