Ajout de tests playwright et correction de tests non passant (pour les tests ajoutés : partie game system ).
Some checks failed
Build & Push Images / build (brain) (push) Has been cancelled
Build & Push Images / build (core) (push) Has been cancelled
Build & Push Images / build (web) (push) Has been cancelled

Correction de plusieurs anomalies : problème de switch entre 2 templates (par exemple si on était sur un template 1 et qu'on voulait passer directement au 2, ce dernier ne chargeait pas) ;
correction du soucis d'apparition de la sidebar à gauche qui disparaissait sans explication ; problème de redirection : lorsqu'on terminait de créer un PJ / PNJ ; on arrivait sur l'accueil de la campagne au lieu de voir le résultat de la création.
Problème de redirection également lors du clique sur un PNJ / PJ sur le coté : on arrivait sur l'édition au lieu de la présentation. Correction de la première lettre stylisée : tout est au même style comme ça plus de probleme de lecture.

Nouveautées : stylisation des modales (notamment suppression, warning.....) avec en prime l'ajout d'un warning lors du changement de système pour avertir que les fiches persos ne sont pas conservées.
Ajout d'une option pour créer un game system directement à la création d'une campagne afin de faciliter la mise en place de cette dernière.
Ajout d'un bouton pour créer un nouveau template directement lorsqu'on créer une page : ça permet de créer un template et de revenir sur la page qu'on était en train de créer sans perdre le titre.

Passage en bêta 0.8.4
This commit is contained in:
2026-05-19 13:37:22 +02:00
parent e346bb4b85
commit ae9455c244
70 changed files with 1908 additions and 495 deletions

View File

@@ -50,14 +50,50 @@
<div class="field">
<label for="campaign-game-system">Système de JDR</label>
<select id="campaign-game-system" formControlName="gameSystemId">
<select *ngIf="!creatingGameSystem" id="campaign-game-system" formControlName="gameSystemId">
<option value="">— Aucun (campagne générique) —</option>
<option *ngFor="let gs of availableGameSystems" [value]="gs.id">{{ gs.name }}</option>
<option [value]="CREATE_GAMESYSTEM_SENTINEL">+ Créer un nouveau système…</option>
</select>
<p class="hint">
<!-- Mode creation inline : remplace temporairement le select. -->
<div *ngIf="creatingGameSystem" class="inline-create">
<input
type="text"
[(ngModel)]="newGameSystemName"
[ngModelOptions]="{ standalone: true }"
placeholder="Nom du nouveau système (ex: D&D 5e, Nimble, Maison)"
(keydown.enter)="$event.preventDefault(); submitCreateGameSystem()"
(keydown.escape)="cancelCreateGameSystem()"
autofocus
/>
<div class="inline-create-actions">
<button type="button" class="btn-inline-primary"
[disabled]="!newGameSystemName.trim() || creatingGameSystemInFlight"
(click)="submitCreateGameSystem()">
<lucide-icon [img]="Check" [size]="14"></lucide-icon>
Créer
</button>
<button type="button" class="btn-inline-secondary" (click)="cancelCreateGameSystem()">
Annuler
</button>
</div>
<p class="hint">
Création rapide — vous pourrez ajouter les règles, les templates de fiches PJ/PNJ
et le reste depuis la section "Systèmes" plus tard.
</p>
</div>
<p *ngIf="!creatingGameSystem" class="hint">
Optionnel. Si défini, l'IA injectera les règles du système (classes, combat, lore...)
dans ses suggestions pour respecter les mécaniques du JDR.
</p>
<p *ngIf="!creatingGameSystem" class="hint hint-warning">
⚠️ Le système de jeu choisi détermine aussi le <strong>template des fiches de PJ et PNJ</strong>.
Le changer plus tard rendra les champs des fiches existantes invisibles
(les données restent stockées mais ne s'afficheront qu'en revenant à l'ancien système).
Choisissez bien dès le départ si possible.
</p>
</div>
<div class="info-box">

View File

@@ -87,6 +87,81 @@ form {
input[type="number"] { width: 120px; }
}
.inline-create {
display: flex;
flex-direction: column;
gap: 0.5rem;
padding: 0.875rem;
background: #1a2233;
border: 1px solid #2d3748;
border-left: 3px solid #6c63ff;
border-radius: 8px;
input {
width: 100%;
background: #1f2937;
border: 1px solid #374151;
border-radius: 6px;
padding: 0.6rem 0.875rem;
color: white;
font-size: 0.9rem;
outline: none;
transition: border-color 0.2s;
&::placeholder { color: #4b5563; }
&:focus { border-color: #6c63ff; }
}
}
.inline-create-actions {
display: flex;
gap: 0.5rem;
}
.btn-inline-primary {
display: flex;
align-items: center;
gap: 0.375rem;
padding: 0.5rem 0.875rem;
background: #6c63ff;
color: white;
border: none;
border-radius: 6px;
font-size: 0.85rem;
font-weight: 600;
cursor: pointer;
transition: background 0.2s;
&:hover:not(:disabled) { background: #5b52e0; }
&:disabled { opacity: 0.4; cursor: not-allowed; }
}
.btn-inline-secondary {
padding: 0.5rem 0.875rem;
background: transparent;
color: #9ca3af;
border: 1px solid #374151;
border-radius: 6px;
font-size: 0.85rem;
cursor: pointer;
transition: background 0.2s, color 0.2s;
&:hover { background: #1f2937; color: white; }
}
.hint-warning {
margin-top: 0.5rem;
background: rgba(234, 179, 8, 0.08);
border-left: 3px solid #eab308;
border-radius: 4px;
padding: 0.625rem 0.875rem;
color: #fbbf24;
font-size: 0.8rem;
line-height: 1.5;
strong { color: #fde68a; }
}
.info-box {
background: #1f2937;
border-radius: 8px;

View File

@@ -1,7 +1,8 @@
import { Component, EventEmitter, OnInit, Output } from '@angular/core';
import { CommonModule } from '@angular/common';
import { ReactiveFormsModule, FormBuilder, FormGroup, Validators } from '@angular/forms';
import { LucideAngularModule, BookCopy, X } from 'lucide-angular';
import { FormsModule } from '@angular/forms';
import { LucideAngularModule, BookCopy, X, Plus, Check } from 'lucide-angular';
import { LoreService } from '../../../services/lore.service';
import { Lore } from '../../../services/lore.model';
import { GameSystemService } from '../../../services/game-system.service';
@@ -22,7 +23,7 @@ export interface CampaignCreatePayload {
@Component({
selector: 'app-campaign-create',
standalone: true,
imports: [CommonModule, ReactiveFormsModule, LucideAngularModule],
imports: [CommonModule, ReactiveFormsModule, FormsModule, LucideAngularModule],
templateUrl: './campaign-create.component.html',
styleUrls: ['./campaign-create.component.scss']
})
@@ -32,6 +33,11 @@ export class CampaignCreateComponent implements OnInit {
readonly BookCopy = BookCopy;
readonly X = X;
readonly Plus = Plus;
readonly Check = Check;
/** Valeur sentinelle de l'option "Creer un systeme" dans le <select>. */
readonly CREATE_GAMESYSTEM_SENTINEL = '__create__';
form: FormGroup;
/** Lores disponibles pour association. Chargés à l'ouverture de la modal. */
@@ -39,6 +45,11 @@ export class CampaignCreateComponent implements OnInit {
/** GameSystems disponibles pour association. */
availableGameSystems: GameSystem[] = [];
/** Mode creation inline d'un GameSystem depuis le dropdown. */
creatingGameSystem = false;
newGameSystemName = '';
creatingGameSystemInFlight = false;
constructor(
private fb: FormBuilder,
private loreService: LoreService,
@@ -62,6 +73,47 @@ export class CampaignCreateComponent implements OnInit {
next: (gs) => this.availableGameSystems = gs,
error: () => this.availableGameSystems = []
});
// Detecte la selection de l'option sentinelle "Creer un systeme" et bascule
// en mode creation inline. On reinitialise immediatement le control a ''
// pour que la sentinelle ne reste pas en valeur reelle du form.
this.form.get('gameSystemId')?.valueChanges.subscribe(value => {
if (value === this.CREATE_GAMESYSTEM_SENTINEL) {
this.form.get('gameSystemId')?.setValue('', { emitEvent: false });
this.startCreateGameSystem();
}
});
}
startCreateGameSystem(): void {
this.creatingGameSystem = true;
this.newGameSystemName = '';
}
cancelCreateGameSystem(): void {
this.creatingGameSystem = false;
this.newGameSystemName = '';
}
submitCreateGameSystem(): void {
const name = this.newGameSystemName.trim();
if (!name || this.creatingGameSystemInFlight) return;
this.creatingGameSystemInFlight = true;
this.gameSystemService.create({ name, isPublic: false }).subscribe({
next: (created) => {
this.creatingGameSystemInFlight = false;
this.availableGameSystems = [...this.availableGameSystems, created];
if (created.id) {
this.form.get('gameSystemId')?.setValue(created.id);
}
this.creatingGameSystem = false;
this.newGameSystemName = '';
},
error: () => {
this.creatingGameSystemInFlight = false;
console.error('Erreur lors de la creation du systeme de jeu');
}
});
}
submit(): void {

View File

@@ -55,10 +55,37 @@
</div>
<div class="field">
<label>Système de JDR</label>
<select [(ngModel)]="editGameSystemId" name="editGameSystemId">
<select *ngIf="!creatingGameSystem"
[(ngModel)]="editGameSystemId"
name="editGameSystemId"
(ngModelChange)="onEditGameSystemChange($event)">
<option value="">— Aucun (générique) —</option>
<option *ngFor="let gs of availableGameSystems" [value]="gs.id">{{ gs.name }}</option>
<option [value]="CREATE_GAMESYSTEM_SENTINEL">+ Créer un nouveau système…</option>
</select>
<div *ngIf="creatingGameSystem" class="inline-create">
<input
type="text"
[(ngModel)]="newGameSystemName"
name="newGameSystemName"
placeholder="Nom du nouveau système (ex: D&D 5e, Nimble, Maison)"
(keydown.enter)="$event.preventDefault(); submitCreateGameSystem()"
(keydown.escape)="cancelCreateGameSystem()"
autofocus
/>
<div class="inline-create-actions">
<button type="button" class="btn-inline-primary"
[disabled]="!newGameSystemName.trim() || creatingGameSystemInFlight"
(click)="submitCreateGameSystem()">
<lucide-icon [img]="Check" [size]="14"></lucide-icon>
Créer
</button>
<button type="button" class="btn-inline-secondary" (click)="cancelCreateGameSystem()">
Annuler
</button>
</div>
</div>
</div>
<div class="header-actions">
<button type="button" class="btn-primary" (click)="saveEdit()" [disabled]="!editName.trim()">

View File

@@ -122,6 +122,64 @@
textarea { resize: vertical; }
}
.inline-create {
display: flex;
flex-direction: column;
gap: 0.5rem;
padding: 0.875rem;
background: #0f172a;
border: 1px solid #1f2937;
border-left: 3px solid #6c63ff;
border-radius: 8px;
input {
width: 100%;
background: #0a1320;
border: 1px solid #1f2937;
border-radius: 6px;
padding: 0.5rem 0.75rem;
color: white;
font-size: 0.9rem;
outline: none;
&::placeholder { color: #4b5563; }
&:focus { border-color: #6c63ff; }
}
.inline-create-actions { display: flex; gap: 0.5rem; }
.btn-inline-primary {
display: flex;
align-items: center;
gap: 0.375rem;
padding: 0.45rem 0.875rem;
background: #6c63ff;
color: white;
border: none;
border-radius: 6px;
font-size: 0.85rem;
font-weight: 600;
cursor: pointer;
transition: background 0.2s;
&:hover:not(:disabled) { background: #5b52e0; }
&:disabled { opacity: 0.4; cursor: not-allowed; }
}
.btn-inline-secondary {
padding: 0.45rem 0.875rem;
background: transparent;
color: #9ca3af;
border: 1px solid #374151;
border-radius: 6px;
font-size: 0.85rem;
cursor: pointer;
transition: background 0.2s, color 0.2s;
&:hover { background: #1f2937; color: white; }
}
}
.header-actions { justify-content: flex-end; }
}
}

View File

@@ -2,7 +2,7 @@ import { Component, OnInit, OnDestroy } from '@angular/core';
import { CommonModule } from '@angular/common';
import { ActivatedRoute } from '@angular/router';
import { FormsModule } from '@angular/forms';
import { LucideAngularModule, Swords, Plus, Globe, Pencil, Trash2, User, Dices, Drama } from 'lucide-angular';
import { LucideAngularModule, Swords, Plus, Globe, Pencil, Trash2, User, Dices, Drama, Check } from 'lucide-angular';
import { Router, RouterLink } from '@angular/router';
import { forkJoin, of } from 'rxjs';
import { catchError, switchMap, filter, map } from 'rxjs/operators';
@@ -14,11 +14,12 @@ import { CharacterService } from '../../../services/character.service';
import { NpcService } from '../../../services/npc.service';
import { Character } from '../../../services/character.model';
import { Npc } from '../../../services/npc.model';
import { LayoutService, GlobalItem } from '../../../services/layout.service';
import { LayoutService } from '../../../services/layout.service';
import { PageTitleService } from '../../../services/page-title.service';
import { Campaign, Arc } from '../../../services/campaign.model';
import { Lore } from '../../../services/lore.model';
import { loadCampaignTreeData, buildCampaignTree, CampaignTreeData } from '../../campaign-tree.helper';
import { loadCampaignTreeData, buildCampaignSidebarConfig, CampaignTreeData } from '../../campaign-tree.helper';
import { ConfirmDialogService } from '../../../shared/confirm-dialog/confirm-dialog.service';
@Component({
selector: 'app-campaign-detail',
@@ -36,6 +37,7 @@ export class CampaignDetailComponent implements OnInit, OnDestroy {
readonly User = User;
readonly Dices = Dices;
readonly Drama = Drama;
readonly Check = Check;
campaign: Campaign | null = null;
arcs: Arc[] = [];
@@ -61,6 +63,13 @@ export class CampaignDetailComponent implements OnInit, OnDestroy {
editLoreId = '';
editGameSystemId = '';
/** Valeur sentinelle de l'option "Creer un systeme" dans le <select>. */
readonly CREATE_GAMESYSTEM_SENTINEL = '__create__';
/** Mode creation inline d'un GameSystem depuis le dropdown d'edition. */
creatingGameSystem = false;
newGameSystemName = '';
creatingGameSystemInFlight = false;
constructor(
private route: ActivatedRoute,
private router: Router,
@@ -70,7 +79,8 @@ export class CampaignDetailComponent implements OnInit, OnDestroy {
private characterService: CharacterService,
private npcService: NpcService,
private layoutService: LayoutService,
private pageTitleService: PageTitleService
private pageTitleService: PageTitleService,
private confirmDialog: ConfirmDialogService
) {}
ngOnInit(): void {
@@ -241,24 +251,7 @@ export class CampaignDetailComponent implements OnInit, OnDestroy {
}
private showLayout(allCampaigns: Campaign[], data: CampaignTreeData): void {
const campaignId = this.campaign!.id!;
const globalItems: GlobalItem[] = allCampaigns.map(c => ({
id: c.id!,
name: c.name,
route: `/campaigns/${c.id}`
}));
this.layoutService.show({
title: this.campaign!.name,
items: buildCampaignTree(campaignId, data),
footerLabel: 'Toutes les campagnes',
createActions: [
{ id: 'create-arc', label: '+ Nouvel arc', variant: 'primary', route: `/campaigns/${campaignId}/arcs/create` }
],
globalItems,
globalBackLabel: 'Toutes les campagnes',
globalBackRoute: '/campaigns'
});
this.layoutService.show(buildCampaignSidebarConfig(this.campaign!, allCampaigns, data, this.campaign!.id!));
}
// ─────────────── Édition / suppression de la Campagne ───────────────
@@ -283,16 +276,83 @@ export class CampaignDetailComponent implements OnInit, OnDestroy {
cancelEdit(): void {
this.editing = false;
this.creatingGameSystem = false;
this.newGameSystemName = '';
}
/** Detecte la selection de l'option sentinelle dans le <select> GameSystem. */
onEditGameSystemChange(value: string): void {
if (value === this.CREATE_GAMESYSTEM_SENTINEL) {
this.editGameSystemId = '';
this.startCreateGameSystem();
}
}
startCreateGameSystem(): void {
this.creatingGameSystem = true;
this.newGameSystemName = '';
}
cancelCreateGameSystem(): void {
this.creatingGameSystem = false;
this.newGameSystemName = '';
}
submitCreateGameSystem(): void {
const name = this.newGameSystemName.trim();
if (!name || this.creatingGameSystemInFlight) return;
this.creatingGameSystemInFlight = true;
this.gameSystemService.create({ name, isPublic: false }).subscribe({
next: (created) => {
this.creatingGameSystemInFlight = false;
this.availableGameSystems = [...this.availableGameSystems, created];
if (created.id) {
this.editGameSystemId = created.id;
}
this.creatingGameSystem = false;
this.newGameSystemName = '';
},
error: () => {
this.creatingGameSystemInFlight = false;
console.error('Erreur lors de la creation du systeme de jeu');
}
});
}
saveEdit(): void {
if (!this.campaign || !this.editName.trim()) return;
const newGameSystemId = this.editGameSystemId ? this.editGameSystemId : null;
const currentGameSystemId = this.campaign.gameSystemId ?? null;
const gameSystemChanged = newGameSystemId !== currentGameSystemId;
const hasSheets = this.characters.length > 0 || this.npcs.length > 0;
if (gameSystemChanged && hasSheets) {
const count = this.characters.length + this.npcs.length;
this.confirmDialog.confirm({
title: 'Changer le systeme de jeu ?',
message:
`Vous etes sur le point de changer le systeme de jeu de cette campagne. ` +
`Cela change egalement le template des fiches de PJ et PNJ.`,
details: [
`${count} fiche(s) existante(s) sont liees au template du systeme actuel.`,
`Leurs champs ne s'afficheront plus avec le nouveau systeme.`,
`Les donnees restent stockees : revenir a l'ancien systeme les rendra a nouveau visibles.`
],
confirmLabel: 'Changer quand meme',
variant: 'warning'
}).then(ok => { if (ok) this.persistEdit(newGameSystemId); });
return;
}
this.persistEdit(newGameSystemId);
}
private persistEdit(newGameSystemId: string | null): void {
if (!this.campaign) return;
this.campaignService.updateCampaign(this.campaign.id!, {
name: this.editName.trim(),
description: this.editDescription,
playerCount: this.campaign.playerCount ?? 0,
loreId: this.editLoreId ? this.editLoreId : null,
gameSystemId: this.editGameSystemId ? this.editGameSystemId : null
gameSystemId: newGameSystemId
}).subscribe({
next: (updated) => {
this.campaign = updated;
@@ -321,18 +381,22 @@ export class CampaignDetailComponent implements OnInit, OnDestroy {
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.');
const details: string[] = [];
if (parts.length) details.push(`Sera aussi supprime : ${parts.join(', ')}.`);
details.push('Cette action est irreversible.');
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')
this.confirmDialog.confirm({
title: 'Supprimer la campagne ?',
message: `Supprimer definitivement la campagne "${campaign.name}" ?`,
details,
confirmLabel: 'Supprimer',
variant: 'danger'
}).then(ok => {
if (!ok) 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')