- Lorsqu'on part de zéro : la création de dossier / page / template ce fait de manière plus fluide à la création d'un lore (par exemple création de page sans template et dossier : parcours facilité) - Ajout d'un bouton "+" dans le header templates - Harmonisation création / modification template Correction de tests unitaires
207 lines
7.5 KiB
TypeScript
207 lines
7.5 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, RouterModule } from '@angular/router';
|
|
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 { LoreNode } from '../../services/lore.model';
|
|
import { FieldType, ImageLayout, TemplateField } from '../../services/template.model';
|
|
import { loadLoreSidebarData, buildLoreSidebarConfig } from '../lore-sidebar.helper';
|
|
import { popReturnTo } from '../return-stack.helper';
|
|
|
|
/**
|
|
* Écran de création d'un Template (gabarit de Page).
|
|
* - Champs principaux : nom, description, noeud par défaut.
|
|
* - Liste dynamique de "champs du template" (ex: "Nom", "Description", "Personnalité").
|
|
* Le user peut ajouter/retirer n'importe lequel — tous sont égaux.
|
|
*/
|
|
@Component({
|
|
selector: 'app-template-create',
|
|
standalone: true,
|
|
imports: [CommonModule, FormsModule, ReactiveFormsModule, RouterModule, LucideAngularModule],
|
|
templateUrl: './template-create.component.html',
|
|
styleUrls: ['./template-create.component.scss']
|
|
})
|
|
export class TemplateCreateComponent implements OnInit, OnDestroy {
|
|
readonly Plus = Plus;
|
|
readonly Trash2 = Trash2;
|
|
readonly Type = Type;
|
|
readonly ImageIcon = ImageIcon;
|
|
readonly ChevronUp = ChevronUp;
|
|
readonly ChevronDown = ChevronDown;
|
|
|
|
form: FormGroup;
|
|
loreId = '';
|
|
nodes: LoreNode[] = [];
|
|
/**
|
|
* Champs dynamiques actuellement definis. Chaque champ a un type discriminant
|
|
* (TEXT ou IMAGE) qui pilote son rendu sur les pages.
|
|
*/
|
|
fields: TemplateField[] = [
|
|
{ name: 'Nom', type: 'TEXT' },
|
|
{ name: 'Description', type: 'TEXT' }
|
|
];
|
|
/** Valeur courante de l'input d'ajout de champ (non binding direct pour reset facile). */
|
|
newFieldName = '';
|
|
/** Type choisi pour le prochain champ a ajouter. */
|
|
newFieldType: FieldType = 'TEXT';
|
|
|
|
constructor(
|
|
private fb: FormBuilder,
|
|
private route: ActivatedRoute,
|
|
private router: Router,
|
|
private loreService: LoreService,
|
|
private templateService: TemplateService,
|
|
private pageService: PageService,
|
|
private layoutService: LayoutService
|
|
) {
|
|
this.form = this.fb.group({
|
|
name: ['', Validators.required],
|
|
description: [''],
|
|
defaultNodeId: ['', Validators.required]
|
|
});
|
|
}
|
|
|
|
ngOnInit(): void {
|
|
this.loreId = this.route.snapshot.paramMap.get('loreId')!;
|
|
loadLoreSidebarData(this.loreId, this.loreService, this.templateService, this.pageService).subscribe(data => {
|
|
this.nodes = data.nodes;
|
|
this.layoutService.show(buildLoreSidebarConfig(data));
|
|
this.restoreDraft();
|
|
});
|
|
}
|
|
|
|
/** Clé sessionStorage pour le brouillon de template — scopée au lore. */
|
|
private get draftKey(): string {
|
|
return `template-create-draft:${this.loreId}`;
|
|
}
|
|
|
|
/**
|
|
* Sauvegarde le formulaire courant avant un détour (création de dossier).
|
|
* defaultNodeId volontairement omis : il référence potentiellement un dossier
|
|
* qui n'existe pas encore.
|
|
*/
|
|
saveDraft(): void {
|
|
const draft = {
|
|
name: this.form.value.name ?? '',
|
|
description: this.form.value.description ?? '',
|
|
fields: this.fields
|
|
};
|
|
try {
|
|
sessionStorage.setItem(this.draftKey, JSON.stringify(draft));
|
|
} catch { /* storage indisponible : on ignore */ }
|
|
}
|
|
|
|
private restoreDraft(): void {
|
|
let raw: string | null = null;
|
|
try { raw = sessionStorage.getItem(this.draftKey); } catch { return; }
|
|
if (!raw) return;
|
|
sessionStorage.removeItem(this.draftKey);
|
|
try {
|
|
const draft = JSON.parse(raw) as { name?: string; description?: string; fields?: TemplateField[] };
|
|
if (draft.name) this.form.patchValue({ name: draft.name });
|
|
if (draft.description) this.form.patchValue({ description: draft.description });
|
|
if (Array.isArray(draft.fields) && draft.fields.length) this.fields = draft.fields;
|
|
} catch { /* JSON corrompu : on ignore */ }
|
|
}
|
|
|
|
/**
|
|
* Construit le `returnTo` à passer à l'écran de création de dossier :
|
|
* on empile 'template-create' par-dessus la pile courante, pour que node-create
|
|
* revienne ici puis remonte à l'écran d'origine le cas échéant.
|
|
*/
|
|
get nodeCreateReturnTo(): string {
|
|
const current = this.route.snapshot.queryParamMap.get('returnTo');
|
|
return current ? `template-create,${current}` : 'template-create';
|
|
}
|
|
|
|
addField(): void {
|
|
const name = this.newFieldName.trim();
|
|
if (!name) return;
|
|
// Unicite par nom (on ignore le type pour eviter des collisions d'affichage).
|
|
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 = '';
|
|
// Le type reste sur la derniere valeur choisie : pratique pour enchainer
|
|
// plusieurs champs du meme type.
|
|
}
|
|
|
|
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 existant (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
|
|
);
|
|
}
|
|
|
|
submit(): void {
|
|
if (this.form.invalid) return;
|
|
const raw = this.form.value;
|
|
this.templateService.create({
|
|
loreId: this.loreId,
|
|
name: raw.name,
|
|
description: raw.description,
|
|
defaultNodeId: raw.defaultNodeId,
|
|
fields: this.fields
|
|
}).subscribe({
|
|
next: () => this.navigateBack(),
|
|
error: () => console.error('Erreur lors de la création du template')
|
|
});
|
|
}
|
|
|
|
cancel(): void {
|
|
this.navigateBack();
|
|
}
|
|
|
|
/**
|
|
* Redirige vers l'écran d'origine en dépilant le premier élément du query-param
|
|
* `returnTo` (pile de retours séparés par des virgules, ex : `page-create` ou
|
|
* `template-create,page-create`). Sinon retombe sur la page détail du Lore.
|
|
*/
|
|
private navigateBack(): void {
|
|
const { next, rest } = popReturnTo(this.route.snapshot.queryParamMap.get('returnTo'));
|
|
if (next === 'page-create') {
|
|
this.router.navigate(['/lore', this.loreId, 'pages', 'create'], {
|
|
queryParams: rest ? { returnTo: rest } : {}
|
|
});
|
|
return;
|
|
}
|
|
this.router.navigate(['/lore', this.loreId]);
|
|
}
|
|
|
|
ngOnDestroy(): void {
|
|
this.layoutService.hide();
|
|
}
|
|
}
|