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
200 lines
7.3 KiB
TypeScript
200 lines
7.3 KiB
TypeScript
import { Component, OnInit, OnDestroy } from '@angular/core';
|
|
import { CommonModule } from '@angular/common';
|
|
import { FormsModule, ReactiveFormsModule, FormBuilder, FormGroup, Validators } from '@angular/forms';
|
|
import { ActivatedRoute, Router } from '@angular/router';
|
|
import { forkJoin, Subject } from 'rxjs';
|
|
import { switchMap, takeUntil } from 'rxjs/operators';
|
|
import { LucideAngularModule, Plus, Trash2, Type, Image as ImageIcon, ChevronUp, ChevronDown } from 'lucide-angular';
|
|
import { LoreService } from '../../services/lore.service';
|
|
import { TemplateService } from '../../services/template.service';
|
|
import { PageService } from '../../services/page.service';
|
|
import { LayoutService } from '../../services/layout.service';
|
|
import { PageTitleService } from '../../services/page-title.service';
|
|
import { LoreNode } from '../../services/lore.model';
|
|
import { FieldType, ImageLayout, Template, TemplateField } from '../../services/template.model';
|
|
import { loadLoreSidebarData, buildLoreSidebarConfig } from '../lore-sidebar.helper';
|
|
import { ConfirmDialogService } from '../../shared/confirm-dialog/confirm-dialog.service';
|
|
|
|
/**
|
|
* Écran d'édition d'un Template existant.
|
|
* Mêmes champs que la création + bouton Supprimer.
|
|
*/
|
|
@Component({
|
|
selector: 'app-template-edit',
|
|
standalone: true,
|
|
imports: [CommonModule, FormsModule, ReactiveFormsModule, LucideAngularModule],
|
|
templateUrl: './template-edit.component.html',
|
|
styleUrls: ['./template-edit.component.scss']
|
|
})
|
|
export class TemplateEditComponent implements OnInit, OnDestroy {
|
|
readonly Plus = Plus;
|
|
readonly Trash2 = Trash2;
|
|
readonly Type = Type;
|
|
readonly ImageIcon = ImageIcon;
|
|
readonly ChevronUp = ChevronUp;
|
|
readonly ChevronDown = ChevronDown;
|
|
|
|
form: FormGroup;
|
|
loreId = '';
|
|
templateId = '';
|
|
template: Template | null = null;
|
|
nodes: LoreNode[] = [];
|
|
fields: TemplateField[] = [];
|
|
newFieldName = '';
|
|
newFieldType: FieldType = 'TEXT';
|
|
/**
|
|
* Noms des champs chargés depuis le backend — utilisés pour discriminer
|
|
* visuellement les champs existants (orange) des champs ajoutés dans cette
|
|
* session d'édition (vert). Non muté ensuite.
|
|
*/
|
|
private originalFieldNames = new Set<string>();
|
|
|
|
private destroy$ = new Subject<void>();
|
|
|
|
/** True si le champ est présent depuis le chargement du template. */
|
|
isExistingField(field: TemplateField): boolean {
|
|
return this.originalFieldNames.has(field.name);
|
|
}
|
|
|
|
constructor(
|
|
private fb: FormBuilder,
|
|
private route: ActivatedRoute,
|
|
private router: Router,
|
|
private loreService: LoreService,
|
|
private templateService: TemplateService,
|
|
private pageService: PageService,
|
|
private layoutService: LayoutService,
|
|
private pageTitleService: PageTitleService,
|
|
private confirmDialog: ConfirmDialogService
|
|
) {
|
|
this.form = this.fb.group({
|
|
name: ['', Validators.required],
|
|
description: [''],
|
|
defaultNodeId: ['']
|
|
});
|
|
}
|
|
|
|
ngOnInit(): void {
|
|
// switchMap pour annuler le chargement precedent si l'utilisateur change
|
|
// de template avant la fin de la requete (Angular reutilise l'instance du
|
|
// composant entre /templates/T1 et /templates/T2, donc ngOnInit ne refire
|
|
// pas et il faut reagir aux changements de params nous-memes).
|
|
this.route.paramMap.pipe(
|
|
switchMap(params => {
|
|
this.loreId = params.get('loreId')!;
|
|
this.templateId = params.get('templateId')!;
|
|
return forkJoin({
|
|
sidebar: loadLoreSidebarData(this.loreId, this.loreService, this.templateService, this.pageService),
|
|
template: this.templateService.getById(this.templateId)
|
|
});
|
|
}),
|
|
takeUntil(this.destroy$)
|
|
).subscribe(({ sidebar, template }) => {
|
|
this.nodes = sidebar.nodes;
|
|
this.layoutService.show(buildLoreSidebarConfig(sidebar));
|
|
this.hydrate(template);
|
|
});
|
|
}
|
|
|
|
private hydrate(template: Template): void {
|
|
this.template = template;
|
|
// Copie defensive + normalisation du type (defaut TEXT si inconnu/manquant,
|
|
// utile pour les templates legacy cote frontend meme si le backend le fait aussi).
|
|
this.fields = (template.fields ?? []).map(f => {
|
|
const type: FieldType = f.type === 'IMAGE' ? 'IMAGE' : 'TEXT';
|
|
return type === 'IMAGE'
|
|
? { name: f.name, type, layout: f.layout ?? 'GALLERY' }
|
|
: { name: f.name, type };
|
|
});
|
|
this.originalFieldNames = new Set(this.fields.map(f => f.name));
|
|
this.form.patchValue({
|
|
name: template.name,
|
|
description: template.description,
|
|
defaultNodeId: template.defaultNodeId ?? ''
|
|
});
|
|
this.pageTitleService.set(template.name);
|
|
}
|
|
|
|
addField(): void {
|
|
const name = this.newFieldName.trim();
|
|
if (!name) return;
|
|
if (this.fields.some(f => f.name === name)) return;
|
|
const newField: TemplateField = this.newFieldType === 'IMAGE'
|
|
? { name, type: 'IMAGE', layout: 'GALLERY' }
|
|
: { name, type: 'TEXT' };
|
|
this.fields = [...this.fields, newField];
|
|
this.newFieldName = '';
|
|
}
|
|
|
|
removeField(index: number): void {
|
|
this.fields = this.fields.filter((_, i) => i !== index);
|
|
}
|
|
|
|
/** Deplace un champ d'un cran vers le haut ou le bas. No-op aux bords. */
|
|
moveField(index: number, direction: -1 | 1): void {
|
|
const target = index + direction;
|
|
if (target < 0 || target >= this.fields.length) return;
|
|
const next = [...this.fields];
|
|
[next[index], next[target]] = [next[target], next[index]];
|
|
this.fields = next;
|
|
}
|
|
|
|
/** Bascule le type d'un champ (TEXT <-> IMAGE). */
|
|
toggleFieldType(index: number): void {
|
|
const field = this.fields[index];
|
|
if (!field) return;
|
|
const nextType: FieldType = field.type === 'TEXT' ? 'IMAGE' : 'TEXT';
|
|
this.fields = this.fields.map((f, i) => {
|
|
if (i !== index) return f;
|
|
return nextType === 'IMAGE'
|
|
? { name: f.name, type: 'IMAGE', layout: f.layout ?? 'GALLERY' }
|
|
: { name: f.name, type: 'TEXT' };
|
|
});
|
|
}
|
|
|
|
/** Met a jour le layout d'un champ IMAGE. */
|
|
setFieldLayout(index: number, layout: ImageLayout): void {
|
|
this.fields = this.fields.map((f, i) =>
|
|
i === index && f.type === 'IMAGE' ? { ...f, layout } : f
|
|
);
|
|
}
|
|
|
|
save(): void {
|
|
if (this.form.invalid || !this.template) return;
|
|
const raw = this.form.value;
|
|
this.templateService.update(this.templateId, {
|
|
...this.template,
|
|
name: raw.name,
|
|
description: raw.description,
|
|
defaultNodeId: raw.defaultNodeId || null,
|
|
fields: this.fields
|
|
}).subscribe({
|
|
next: () => this.router.navigate(['/lore', this.loreId]),
|
|
error: () => console.error('Erreur lors de la sauvegarde du template')
|
|
});
|
|
}
|
|
|
|
delete(): void {
|
|
this.confirmDialog.confirm({
|
|
title: 'Supprimer le template',
|
|
message: `Supprimer le template "${this.template?.name}" ?`,
|
|
confirmLabel: 'Supprimer',
|
|
variant: 'danger'
|
|
}).then(ok => {
|
|
if (!ok) return;
|
|
this.templateService.delete(this.templateId).subscribe({
|
|
next: () => this.router.navigate(['/lore', this.loreId]),
|
|
error: () => console.error('Erreur lors de la suppression du template')
|
|
});
|
|
});
|
|
}
|
|
|
|
ngOnDestroy(): void {
|
|
this.destroy$.next();
|
|
this.destroy$.complete();
|
|
// hide() volontairement retire : la sidebar reste prise en charge par le
|
|
// composant suivant (sous-route ou detail parent) afin d'eviter qu'elle
|
|
// disparaisse lors des navigations internes a la section.
|
|
}
|
|
}
|