Migration Angular 20 -> 21 : fin de la migration securite (0 vulnerabilite npm)
- ng update @angular/core@21 @angular/cli@21 (corrige GHSA-jrmj-c5cx-3cw6, XSS SVG) - Migration automatique des templates vers le control flow natif (@if/@for, 138 fichiers) - Correction des 5 `track` invalides generes par la migration (trackBy a 1 argument -> track $index) + suppression des fonctions trackBy mortes - TypeScript 5.9, zone.js 0.15 ; npm audit : 0 vulnerabilite Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -1,85 +1,92 @@
|
||||
<div class="folder-view" *ngIf="node">
|
||||
|
||||
<!-- Fil d'Ariane : Lore → ancêtres → dossier courant -->
|
||||
<nav class="breadcrumb" aria-label="Fil d'Ariane">
|
||||
<button type="button" class="crumb" (click)="navigateToLoreRoot()" *ngIf="lore">
|
||||
{{ lore.name }}
|
||||
</button>
|
||||
<ng-container *ngFor="let ancestor of ancestors">
|
||||
@if (node) {
|
||||
<div class="folder-view">
|
||||
<!-- Fil d'Ariane : Lore → ancêtres → dossier courant -->
|
||||
<nav class="breadcrumb" aria-label="Fil d'Ariane">
|
||||
@if (lore) {
|
||||
<button type="button" class="crumb" (click)="navigateToLoreRoot()">
|
||||
{{ lore.name }}
|
||||
</button>
|
||||
}
|
||||
@for (ancestor of ancestors; track ancestor) {
|
||||
<lucide-icon [img]="ChevronRight" [size]="12" class="crumb-sep"></lucide-icon>
|
||||
<button type="button" class="crumb" (click)="navigateToSubfolder(ancestor.id!)">
|
||||
{{ ancestor.name }}
|
||||
</button>
|
||||
}
|
||||
<lucide-icon [img]="ChevronRight" [size]="12" class="crumb-sep"></lucide-icon>
|
||||
<button type="button" class="crumb" (click)="navigateToSubfolder(ancestor.id!)">
|
||||
{{ ancestor.name }}
|
||||
</button>
|
||||
</ng-container>
|
||||
<lucide-icon [img]="ChevronRight" [size]="12" class="crumb-sep"></lucide-icon>
|
||||
<span class="crumb current">{{ node.name }}</span>
|
||||
</nav>
|
||||
|
||||
<!-- Header : icône + nom + actions -->
|
||||
<div class="detail-header">
|
||||
<div class="header-texts">
|
||||
<h1>
|
||||
<lucide-icon [img]="folderIcon" [size]="24" class="title-icon"></lucide-icon>
|
||||
{{ node.name }}
|
||||
</h1>
|
||||
<p class="description">
|
||||
{{ subfolders.length }} sous-dossier(s) · {{ pages.length }} page(s)
|
||||
</p>
|
||||
</div>
|
||||
<div class="header-actions">
|
||||
<button type="button" class="btn-secondary" (click)="navigateToEdit()" title="Modifier le dossier">
|
||||
<lucide-icon [img]="Pencil" [size]="14"></lucide-icon>
|
||||
Modifier
|
||||
</button>
|
||||
<button type="button" class="btn-danger" (click)="delete()" title="Supprimer le dossier et tout son contenu">
|
||||
<lucide-icon [img]="Trash2" [size]="14"></lucide-icon>
|
||||
Supprimer
|
||||
</button>
|
||||
<span class="crumb current">{{ node.name }}</span>
|
||||
</nav>
|
||||
<!-- Header : icône + nom + actions -->
|
||||
<div class="detail-header">
|
||||
<div class="header-texts">
|
||||
<h1>
|
||||
<lucide-icon [img]="folderIcon" [size]="24" class="title-icon"></lucide-icon>
|
||||
{{ node.name }}
|
||||
</h1>
|
||||
<p class="description">
|
||||
{{ subfolders.length }} sous-dossier(s) · {{ pages.length }} page(s)
|
||||
</p>
|
||||
</div>
|
||||
<div class="header-actions">
|
||||
<button type="button" class="btn-secondary" (click)="navigateToEdit()" title="Modifier le dossier">
|
||||
<lucide-icon [img]="Pencil" [size]="14"></lucide-icon>
|
||||
Modifier
|
||||
</button>
|
||||
<button type="button" class="btn-danger" (click)="delete()" title="Supprimer le dossier et tout son contenu">
|
||||
<lucide-icon [img]="Trash2" [size]="14"></lucide-icon>
|
||||
Supprimer
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Sous-dossiers -->
|
||||
<section class="detail-section">
|
||||
<div class="section-header">
|
||||
<h2>Sous-dossiers</h2>
|
||||
<button class="btn-add" (click)="navigateToCreateSubfolder()">
|
||||
<lucide-icon [img]="Plus" [size]="14"></lucide-icon>
|
||||
Nouveau sous-dossier
|
||||
</button>
|
||||
</div>
|
||||
@if (subfolders.length > 0) {
|
||||
<div class="items-grid">
|
||||
@for (sub of subfolders; track sub) {
|
||||
<div class="node-card" (click)="navigateToSubfolder(sub.id!)">
|
||||
<lucide-icon [img]="Folder" [size]="24" class="node-icon"></lucide-icon>
|
||||
<span class="node-name">{{ sub.name }}</span>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
@if (subfolders.length === 0) {
|
||||
<div class="empty-state">
|
||||
<p>Aucun sous-dossier.</p>
|
||||
</div>
|
||||
}
|
||||
</section>
|
||||
<!-- Pages -->
|
||||
<section class="detail-section">
|
||||
<div class="section-header">
|
||||
<h2>Pages</h2>
|
||||
<button class="btn-add" (click)="navigateToCreatePage()">
|
||||
<lucide-icon [img]="Plus" [size]="14"></lucide-icon>
|
||||
Nouvelle page
|
||||
</button>
|
||||
</div>
|
||||
@if (pages.length > 0) {
|
||||
<div class="items-grid">
|
||||
@for (page of pages; track page) {
|
||||
<div class="node-card" (click)="navigateToPage(page.id!)">
|
||||
<lucide-icon [img]="FileText" [size]="24" class="node-icon"></lucide-icon>
|
||||
<span class="node-name">{{ page.title }}</span>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
@if (pages.length === 0) {
|
||||
<div class="empty-state">
|
||||
<p>Aucune page dans ce dossier.</p>
|
||||
</div>
|
||||
}
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<!-- Sous-dossiers -->
|
||||
<section class="detail-section">
|
||||
<div class="section-header">
|
||||
<h2>Sous-dossiers</h2>
|
||||
<button class="btn-add" (click)="navigateToCreateSubfolder()">
|
||||
<lucide-icon [img]="Plus" [size]="14"></lucide-icon>
|
||||
Nouveau sous-dossier
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="items-grid" *ngIf="subfolders.length > 0">
|
||||
<div class="node-card" *ngFor="let sub of subfolders" (click)="navigateToSubfolder(sub.id!)">
|
||||
<lucide-icon [img]="Folder" [size]="24" class="node-icon"></lucide-icon>
|
||||
<span class="node-name">{{ sub.name }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="empty-state" *ngIf="subfolders.length === 0">
|
||||
<p>Aucun sous-dossier.</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Pages -->
|
||||
<section class="detail-section">
|
||||
<div class="section-header">
|
||||
<h2>Pages</h2>
|
||||
<button class="btn-add" (click)="navigateToCreatePage()">
|
||||
<lucide-icon [img]="Plus" [size]="14"></lucide-icon>
|
||||
Nouvelle page
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="items-grid" *ngIf="pages.length > 0">
|
||||
<div class="node-card" *ngFor="let page of pages" (click)="navigateToPage(page.id!)">
|
||||
<lucide-icon [img]="FileText" [size]="24" class="node-icon"></lucide-icon>
|
||||
<span class="node-name">{{ page.title }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="empty-state" *ngIf="pages.length === 0">
|
||||
<p>Aucune page dans ce dossier.</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Component, OnDestroy, OnInit } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { forkJoin } from 'rxjs';
|
||||
import { LucideAngularModule, LucideIconData, Folder, FileText, Pencil, Trash2, Plus, ChevronRight } from 'lucide-angular';
|
||||
@@ -24,7 +24,7 @@ import { ConfirmDialogService } from '../../shared/confirm-dialog/confirm-dialog
|
||||
*/
|
||||
@Component({
|
||||
selector: 'app-folder-view',
|
||||
imports: [CommonModule, LucideAngularModule],
|
||||
imports: [LucideAngularModule],
|
||||
templateUrl: './folder-view.component.html',
|
||||
styleUrls: ['./folder-view.component.scss']
|
||||
})
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { Component, EventEmitter, Output } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
|
||||
import { ReactiveFormsModule, FormBuilder, FormGroup, Validators } from '@angular/forms';
|
||||
import { LucideAngularModule, BookCopy, X } from 'lucide-angular';
|
||||
|
||||
@Component({
|
||||
selector: 'app-lore-create',
|
||||
imports: [CommonModule, ReactiveFormsModule, LucideAngularModule],
|
||||
imports: [ReactiveFormsModule, LucideAngularModule],
|
||||
templateUrl: './lore-create.component.html',
|
||||
styleUrls: ['./lore-create.component.scss']
|
||||
})
|
||||
|
||||
@@ -1,70 +1,78 @@
|
||||
<div class="lore-detail" *ngIf="lore">
|
||||
|
||||
<!-- ============ Header : mode lecture ============ -->
|
||||
<div class="detail-header" *ngIf="!editing">
|
||||
<div class="header-texts">
|
||||
<h1>{{ lore.name }}</h1>
|
||||
<p class="description">{{ lore.description }}</p>
|
||||
</div>
|
||||
<div class="header-actions">
|
||||
<button type="button" class="btn-secondary" (click)="startEdit()" title="Modifier le Lore">
|
||||
<lucide-icon [img]="Pencil" [size]="14"></lucide-icon>
|
||||
Modifier
|
||||
</button>
|
||||
<button type="button" class="btn-danger" (click)="deleteLore()" title="Supprimer le Lore">
|
||||
<lucide-icon [img]="Trash2" [size]="14"></lucide-icon>
|
||||
Supprimer
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ============ Header : mode édition inline ============ -->
|
||||
<div class="detail-header edit-mode" *ngIf="editing">
|
||||
<div class="field">
|
||||
<label for="lore-detail-edit-name">Nom</label>
|
||||
<input id="lore-detail-edit-name" type="text" [(ngModel)]="editName" name="editName" required />
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="lore-detail-edit-description">Description</label>
|
||||
<textarea id="lore-detail-edit-description" [(ngModel)]="editDescription" name="editDescription" rows="3"></textarea>
|
||||
</div>
|
||||
<div class="header-actions">
|
||||
<button type="button" class="btn-primary" (click)="saveEdit()" [disabled]="!editName.trim()">
|
||||
Sauvegarder
|
||||
</button>
|
||||
<button type="button" class="btn-secondary" (click)="cancelEdit()">
|
||||
Annuler
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ============ Grille des dossiers racine ============ -->
|
||||
<section class="detail-section nodes-section" *ngIf="!editing">
|
||||
<div class="section-header">
|
||||
<h2>Dossiers</h2>
|
||||
<button class="btn-add" (click)="navigateToCreateNode()">
|
||||
<lucide-icon [img]="Plus" [size]="14"></lucide-icon>
|
||||
Nouveau dossier
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- rootNodes : uniquement les dossiers racine (pas les sous-dossiers,
|
||||
qui sont visibles dans l'arbre de la sidebar). -->
|
||||
<div class="nodes-grid" *ngIf="rootNodes.length > 0">
|
||||
<div class="node-card" *ngFor="let node of rootNodes" (click)="navigateToFolder(node.id!)">
|
||||
<lucide-icon [img]="Folder" [size]="24" class="node-icon"></lucide-icon>
|
||||
<span class="node-name">{{ node.name }}</span>
|
||||
@if (lore) {
|
||||
<div class="lore-detail">
|
||||
<!-- ============ Header : mode lecture ============ -->
|
||||
@if (!editing) {
|
||||
<div class="detail-header">
|
||||
<div class="header-texts">
|
||||
<h1>{{ lore.name }}</h1>
|
||||
<p class="description">{{ lore.description }}</p>
|
||||
</div>
|
||||
<div class="header-actions">
|
||||
<button type="button" class="btn-secondary" (click)="startEdit()" title="Modifier le Lore">
|
||||
<lucide-icon [img]="Pencil" [size]="14"></lucide-icon>
|
||||
Modifier
|
||||
</button>
|
||||
<button type="button" class="btn-danger" (click)="deleteLore()" title="Supprimer le Lore">
|
||||
<lucide-icon [img]="Trash2" [size]="14"></lucide-icon>
|
||||
Supprimer
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="empty-state" *ngIf="rootNodes.length === 0">
|
||||
<lucide-icon [img]="Folder" [size]="40" class="empty-icon"></lucide-icon>
|
||||
<p>Aucun dossier pour le moment.</p>
|
||||
<button class="btn-add-first" (click)="navigateToCreateNode()">
|
||||
<lucide-icon [img]="Plus" [size]="14"></lucide-icon>
|
||||
Créer votre premier dossier
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
</div>
|
||||
}
|
||||
<!-- ============ Header : mode édition inline ============ -->
|
||||
@if (editing) {
|
||||
<div class="detail-header edit-mode">
|
||||
<div class="field">
|
||||
<label for="lore-detail-edit-name">Nom</label>
|
||||
<input id="lore-detail-edit-name" type="text" [(ngModel)]="editName" name="editName" required />
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="lore-detail-edit-description">Description</label>
|
||||
<textarea id="lore-detail-edit-description" [(ngModel)]="editDescription" name="editDescription" rows="3"></textarea>
|
||||
</div>
|
||||
<div class="header-actions">
|
||||
<button type="button" class="btn-primary" (click)="saveEdit()" [disabled]="!editName.trim()">
|
||||
Sauvegarder
|
||||
</button>
|
||||
<button type="button" class="btn-secondary" (click)="cancelEdit()">
|
||||
Annuler
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
<!-- ============ Grille des dossiers racine ============ -->
|
||||
@if (!editing) {
|
||||
<section class="detail-section nodes-section">
|
||||
<div class="section-header">
|
||||
<h2>Dossiers</h2>
|
||||
<button class="btn-add" (click)="navigateToCreateNode()">
|
||||
<lucide-icon [img]="Plus" [size]="14"></lucide-icon>
|
||||
Nouveau dossier
|
||||
</button>
|
||||
</div>
|
||||
<!-- rootNodes : uniquement les dossiers racine (pas les sous-dossiers,
|
||||
qui sont visibles dans l'arbre de la sidebar). -->
|
||||
@if (rootNodes.length > 0) {
|
||||
<div class="nodes-grid">
|
||||
@for (node of rootNodes; track node) {
|
||||
<div class="node-card" (click)="navigateToFolder(node.id!)">
|
||||
<lucide-icon [img]="Folder" [size]="24" class="node-icon"></lucide-icon>
|
||||
<span class="node-name">{{ node.name }}</span>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
@if (rootNodes.length === 0) {
|
||||
<div class="empty-state">
|
||||
<lucide-icon [img]="Folder" [size]="40" class="empty-icon"></lucide-icon>
|
||||
<p>Aucun dossier pour le moment.</p>
|
||||
<button class="btn-add-first" (click)="navigateToCreateNode()">
|
||||
<lucide-icon [img]="Plus" [size]="14"></lucide-icon>
|
||||
Créer votre premier dossier
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
</section>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Component, OnInit, OnDestroy } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { LucideAngularModule, Folder, Plus, Pencil, Trash2 } from 'lucide-angular';
|
||||
@@ -14,7 +14,7 @@ import { ConfirmDialogService } from '../../shared/confirm-dialog/confirm-dialog
|
||||
|
||||
@Component({
|
||||
selector: 'app-lore-detail',
|
||||
imports: [CommonModule, FormsModule, LucideAngularModule],
|
||||
imports: [FormsModule, LucideAngularModule],
|
||||
templateUrl: './lore-detail.component.html',
|
||||
styleUrls: ['./lore-detail.component.scss']
|
||||
})
|
||||
|
||||
@@ -14,14 +14,16 @@
|
||||
formControlName="name"
|
||||
placeholder="Ex: Personnages, Créatures..."
|
||||
[class.invalid]="form.get('name')?.invalid && form.get('name')?.touched"
|
||||
/>
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label>Dossier parent <span class="optional">(optionnel)</span></label>
|
||||
<select formControlName="parentId">
|
||||
<option value="">— Racine du Lore —</option>
|
||||
<option *ngFor="let parent of availableParents" [value]="parent.id">{{ parent.name }}</option>
|
||||
@for (parent of availableParents; track parent) {
|
||||
<option [value]="parent.id">{{ parent.name }}</option>
|
||||
}
|
||||
</select>
|
||||
<p class="hint">Laissez vide pour créer un dossier à la racine du lore</p>
|
||||
</div>
|
||||
@@ -29,14 +31,15 @@
|
||||
<div class="field">
|
||||
<label>Icône</label>
|
||||
<div class="icon-grid">
|
||||
<button
|
||||
type="button"
|
||||
class="icon-btn"
|
||||
*ngFor="let option of iconOptions"
|
||||
[class.selected]="selectedIcon === option.key"
|
||||
(click)="selectIcon(option.key)">
|
||||
<lucide-icon [img]="option.icon" [size]="18"></lucide-icon>
|
||||
</button>
|
||||
@for (option of iconOptions; track option) {
|
||||
<button
|
||||
type="button"
|
||||
class="icon-btn"
|
||||
[class.selected]="selectedIcon === option.key"
|
||||
(click)="selectIcon(option.key)">
|
||||
<lucide-icon [img]="option.icon" [size]="18"></lucide-icon>
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Component, OnInit, OnDestroy } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
|
||||
import { ReactiveFormsModule, FormBuilder, FormGroup, Validators } from '@angular/forms';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { LucideAngularModule, LucideIconData } from 'lucide-angular';
|
||||
@@ -14,7 +14,7 @@ import { LORE_ICON_OPTIONS, IconOption, resolveIcon } from '../lore-icons';
|
||||
|
||||
@Component({
|
||||
selector: 'app-lore-node-create',
|
||||
imports: [CommonModule, ReactiveFormsModule, LucideAngularModule],
|
||||
imports: [ReactiveFormsModule, LucideAngularModule],
|
||||
templateUrl: './lore-node-create.component.html',
|
||||
styleUrls: ['./lore-node-create.component.scss']
|
||||
})
|
||||
|
||||
@@ -1,54 +1,52 @@
|
||||
<div class="page" *ngIf="node">
|
||||
|
||||
<header class="page-header">
|
||||
<div>
|
||||
<h1>Éditer le dossier</h1>
|
||||
<p class="subtitle">
|
||||
{{ childFolderCount }} sous-dossier(s) · {{ pageCount }} page(s)
|
||||
</p>
|
||||
</div>
|
||||
<div class="header-actions">
|
||||
<button type="button" class="btn-secondary" (click)="cancel()">Annuler</button>
|
||||
<button
|
||||
type="submit"
|
||||
class="btn-primary"
|
||||
[disabled]="form.invalid"
|
||||
(click)="save()">
|
||||
Sauvegarder
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<form [formGroup]="form" class="edit-form">
|
||||
|
||||
<div class="field">
|
||||
<label>Nom du dossier *</label>
|
||||
<input type="text" formControlName="name" />
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label>Dossier parent <span class="optional">(optionnel)</span></label>
|
||||
<select formControlName="parentId">
|
||||
<option value="">— Racine du Lore —</option>
|
||||
<option *ngFor="let parent of availableParents" [value]="parent.id">{{ parent.name }}</option>
|
||||
</select>
|
||||
<p class="hint">Vous ne pouvez pas choisir un sous-dossier du dossier courant (cycle interdit)</p>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label>Icône</label>
|
||||
<div class="icon-grid">
|
||||
@if (node) {
|
||||
<div class="page">
|
||||
<header class="page-header">
|
||||
<div>
|
||||
<h1>Éditer le dossier</h1>
|
||||
<p class="subtitle">
|
||||
{{ childFolderCount }} sous-dossier(s) · {{ pageCount }} page(s)
|
||||
</p>
|
||||
</div>
|
||||
<div class="header-actions">
|
||||
<button type="button" class="btn-secondary" (click)="cancel()">Annuler</button>
|
||||
<button
|
||||
type="button"
|
||||
class="icon-btn"
|
||||
*ngFor="let option of iconOptions"
|
||||
[class.selected]="selectedIcon === option.key"
|
||||
(click)="selectIcon(option.key)">
|
||||
<lucide-icon [img]="option.icon" [size]="18"></lucide-icon>
|
||||
type="submit"
|
||||
class="btn-primary"
|
||||
[disabled]="form.invalid"
|
||||
(click)="save()">
|
||||
Sauvegarder
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
|
||||
</div>
|
||||
</header>
|
||||
<form [formGroup]="form" class="edit-form">
|
||||
<div class="field">
|
||||
<label>Nom du dossier *</label>
|
||||
<input type="text" formControlName="name" />
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>Dossier parent <span class="optional">(optionnel)</span></label>
|
||||
<select formControlName="parentId">
|
||||
<option value="">— Racine du Lore —</option>
|
||||
@for (parent of availableParents; track parent) {
|
||||
<option [value]="parent.id">{{ parent.name }}</option>
|
||||
}
|
||||
</select>
|
||||
<p class="hint">Vous ne pouvez pas choisir un sous-dossier du dossier courant (cycle interdit)</p>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>Icône</label>
|
||||
<div class="icon-grid">
|
||||
@for (option of iconOptions; track option) {
|
||||
<button
|
||||
type="button"
|
||||
class="icon-btn"
|
||||
[class.selected]="selectedIcon === option.key"
|
||||
(click)="selectIcon(option.key)">
|
||||
<lucide-icon [img]="option.icon" [size]="18"></lucide-icon>
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Component, OnInit, OnDestroy } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
|
||||
import { ReactiveFormsModule, FormBuilder, FormGroup, Validators } from '@angular/forms';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { forkJoin } from 'rxjs';
|
||||
@@ -32,7 +32,7 @@ import { LORE_ICON_OPTIONS, IconOption } from '../lore-icons';
|
||||
*/
|
||||
@Component({
|
||||
selector: 'app-lore-node-edit',
|
||||
imports: [CommonModule, ReactiveFormsModule, LucideAngularModule],
|
||||
imports: [ReactiveFormsModule, LucideAngularModule],
|
||||
templateUrl: './lore-node-edit.component.html',
|
||||
styleUrls: ['./lore-node-edit.component.scss']
|
||||
})
|
||||
|
||||
@@ -8,18 +8,20 @@
|
||||
|
||||
<div class="lore-grid">
|
||||
|
||||
<div class="lore-card" *ngFor="let lore of lores" (click)="navigateToDetail(lore.id!)">
|
||||
<div class="card-header">
|
||||
<lucide-icon [img]="Folder" [size]="20" class="card-icon"></lucide-icon>
|
||||
<span class="card-date">Il y a 2h</span>
|
||||
@for (lore of lores; track lore) {
|
||||
<div class="lore-card" (click)="navigateToDetail(lore.id!)">
|
||||
<div class="card-header">
|
||||
<lucide-icon [img]="Folder" [size]="20" class="card-icon"></lucide-icon>
|
||||
<span class="card-date">Il y a 2h</span>
|
||||
</div>
|
||||
<h2>{{ lore.name }}</h2>
|
||||
<p class="card-description">{{ lore.description }}</p>
|
||||
<div class="card-stats">
|
||||
<span>📄 {{ lore.pageCount || 0 }} pages</span>
|
||||
<span>🌳 {{ lore.nodeCount || 0 }} dossiers</span>
|
||||
</div>
|
||||
</div>
|
||||
<h2>{{ lore.name }}</h2>
|
||||
<p class="card-description">{{ lore.description }}</p>
|
||||
<div class="card-stats">
|
||||
<span>📄 {{ lore.pageCount || 0 }} pages</span>
|
||||
<span>🌳 {{ lore.nodeCount || 0 }} dossiers</span>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="lore-card card-new" (click)="openCreateModal()">
|
||||
<div class="new-icon">
|
||||
@@ -35,8 +37,9 @@
|
||||
|
||||
</div>
|
||||
|
||||
<app-lore-create
|
||||
*ngIf="showCreateModal"
|
||||
(close)="onModalClose()"
|
||||
(created)="onLoreCreated($event)">
|
||||
</app-lore-create>
|
||||
@if (showCreateModal) {
|
||||
<app-lore-create
|
||||
(close)="onModalClose()"
|
||||
(created)="onLoreCreated($event)">
|
||||
</app-lore-create>
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
|
||||
import { Router } from '@angular/router';
|
||||
import { LucideAngularModule, BookOpen, Folder, Plus } from 'lucide-angular';
|
||||
import { LoreService } from '../services/lore.service';
|
||||
@@ -9,7 +9,7 @@ import { LoreCreateComponent } from './lore-create/lore-create.component';
|
||||
|
||||
@Component({
|
||||
selector: 'app-lore',
|
||||
imports: [CommonModule, LucideAngularModule, LoreCreateComponent],
|
||||
imports: [LucideAngularModule, LoreCreateComponent],
|
||||
templateUrl: './lore.component.html',
|
||||
styleUrls: ['./lore.component.scss']
|
||||
})
|
||||
|
||||
@@ -17,64 +17,66 @@
|
||||
<div class="field">
|
||||
<label>Template *</label>
|
||||
|
||||
<div class="templates-grid" *ngIf="templates.length; else emptyTemplates">
|
||||
<button
|
||||
type="button"
|
||||
class="template-card"
|
||||
*ngFor="let t of templates"
|
||||
[class.selected]="selectedTemplateId === t.id"
|
||||
(click)="selectTemplate(t)">
|
||||
<div class="template-card-head">
|
||||
<lucide-icon [img]="FileText" [size]="16"></lucide-icon>
|
||||
<span class="template-name">{{ t.name }}</span>
|
||||
</div>
|
||||
<p class="template-description">{{ t.description || '—' }}</p>
|
||||
</button>
|
||||
|
||||
<!-- Carte "+" : sauvegarde le brouillon et part creer un nouveau template ;
|
||||
template-create renverra ici via le mecanisme returnTo. -->
|
||||
<a
|
||||
class="template-card template-card-create"
|
||||
[routerLink]="['/lore', loreId, 'templates', 'create']"
|
||||
[queryParams]="{ returnTo: 'page-create' }"
|
||||
(click)="saveDraft()"
|
||||
title="Créer un nouveau template pour ce Lore">
|
||||
<div class="template-card-head">
|
||||
<lucide-icon [img]="Plus" [size]="16"></lucide-icon>
|
||||
<span class="template-name">Créer un template</span>
|
||||
</div>
|
||||
<p class="template-description">
|
||||
Vous reviendrez ici automatiquement, votre saisie sera conservée.
|
||||
</p>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<ng-template #emptyTemplates>
|
||||
@if (templates.length) {
|
||||
<div class="templates-grid">
|
||||
@for (t of templates; track t) {
|
||||
<button
|
||||
type="button"
|
||||
class="template-card"
|
||||
[class.selected]="selectedTemplateId === t.id"
|
||||
(click)="selectTemplate(t)">
|
||||
<div class="template-card-head">
|
||||
<lucide-icon [img]="FileText" [size]="16"></lucide-icon>
|
||||
<span class="template-name">{{ t.name }}</span>
|
||||
</div>
|
||||
<p class="template-description">{{ t.description || '—' }}</p>
|
||||
</button>
|
||||
}
|
||||
<!-- Carte "+" : sauvegarde le brouillon et part creer un nouveau template ;
|
||||
template-create renverra ici via le mecanisme returnTo. -->
|
||||
<a
|
||||
class="template-card template-card-create"
|
||||
[routerLink]="['/lore', loreId, 'templates', 'create']"
|
||||
[queryParams]="{ returnTo: 'page-create' }"
|
||||
(click)="saveDraft()"
|
||||
title="Créer un nouveau template pour ce Lore">
|
||||
<div class="template-card-head">
|
||||
<lucide-icon [img]="Plus" [size]="16"></lucide-icon>
|
||||
<span class="template-name">Créer un template</span>
|
||||
</div>
|
||||
<p class="template-description">
|
||||
Vous reviendrez ici automatiquement, votre saisie sera conservée.
|
||||
</p>
|
||||
</a>
|
||||
</div>
|
||||
} @else {
|
||||
<p class="empty-hint">
|
||||
Aucun template défini pour ce Lore.
|
||||
<a [routerLink]="['/lore', loreId, 'templates', 'create']" [queryParams]="{ returnTo: 'page-create' }" (click)="saveDraft()">Créer un template</a> d'abord.
|
||||
</p>
|
||||
</ng-template>
|
||||
}
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Dossier de destination -->
|
||||
<div class="field">
|
||||
<label for="page-node">Dossier de destination *</label>
|
||||
|
||||
<ng-container *ngIf="nodes.length; else emptyFolders">
|
||||
@if (nodes.length) {
|
||||
<select id="page-node" formControlName="nodeId">
|
||||
<option value="" disabled>Sélectionnez un dossier</option>
|
||||
<option *ngFor="let node of nodes" [value]="node.id">{{ node.name }}</option>
|
||||
@for (node of nodes; track node) {
|
||||
<option [value]="node.id">{{ node.name }}</option>
|
||||
}
|
||||
</select>
|
||||
<p class="hint">La page sera créée dans ce dossier</p>
|
||||
</ng-container>
|
||||
|
||||
<ng-template #emptyFolders>
|
||||
} @else {
|
||||
<p class="empty-hint">
|
||||
Aucun dossier dans ce Lore.
|
||||
<a [routerLink]="['/lore', loreId, 'nodes', 'create']" [queryParams]="{ returnTo: 'page-create' }" (click)="saveDraft()">Créer un dossier</a> d'abord.
|
||||
</p>
|
||||
</ng-template>
|
||||
}
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Aide contextuelle -->
|
||||
@@ -84,13 +86,15 @@
|
||||
</div>
|
||||
|
||||
<!-- Erreur wizard (parsing <values> ou échec HTTP) -->
|
||||
<div class="wizard-error" *ngIf="wizardError" role="alert">{{ wizardError }}</div>
|
||||
@if (wizardError) {
|
||||
<div class="wizard-error" role="alert">{{ wizardError }}</div>
|
||||
}
|
||||
|
||||
<!-- Actions -->
|
||||
<div class="actions-row">
|
||||
<button type="button" class="btn-secondary" (click)="cancel()">Annuler</button>
|
||||
<button type="button" class="btn-ai" (click)="openWizard()" [disabled]="!canSubmit"
|
||||
title="Ouvrir l'assistant IA pour pré-remplir les champs">
|
||||
title="Ouvrir l'assistant IA pour pré-remplir les champs">
|
||||
<lucide-icon [img]="Sparkles" [size]="14"></lucide-icon>
|
||||
Créer avec l'IA
|
||||
</button>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Component, OnInit, OnDestroy } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
|
||||
import { ReactiveFormsModule, FormBuilder, FormGroup, Validators } from '@angular/forms';
|
||||
import { ActivatedRoute, Router, RouterModule } from '@angular/router';
|
||||
import { LucideAngularModule, FileText, Sparkles, Plus } from 'lucide-angular';
|
||||
@@ -26,7 +26,7 @@ import { AiChatDrawerComponent, ChatPrimaryAction } from '../../shared/ai-chat-d
|
||||
*/
|
||||
@Component({
|
||||
selector: 'app-page-create',
|
||||
imports: [CommonModule, ReactiveFormsModule, RouterModule, LucideAngularModule, AiChatDrawerComponent],
|
||||
imports: [ReactiveFormsModule, RouterModule, LucideAngularModule, AiChatDrawerComponent],
|
||||
templateUrl: './page-create.component.html',
|
||||
styleUrls: ['./page-create.component.scss']
|
||||
})
|
||||
|
||||
@@ -1,116 +1,114 @@
|
||||
<div class="page" *ngIf="page">
|
||||
|
||||
<app-breadcrumb [items]="breadcrumbItems"></app-breadcrumb>
|
||||
|
||||
<header class="page-header">
|
||||
<div>
|
||||
<h1>{{ page.title }}</h1>
|
||||
<p class="subtitle">{{ template?.name || 'Page' }}</p>
|
||||
</div>
|
||||
<div class="header-actions">
|
||||
<button type="button" class="btn-ai"
|
||||
(click)="toggleChat()"
|
||||
[disabled]="aiLoading"
|
||||
[class.active]="chatOpen"
|
||||
title="Ouvrir l'Assistant IA (chat ou remplissage automatique)">
|
||||
<lucide-icon [img]="Sparkles" [size]="14"></lucide-icon>
|
||||
{{ aiLoading ? 'Génération…' : 'Assistant IA' }}
|
||||
</button>
|
||||
<button type="button" class="btn-secondary" [routerLink]="['/lore', loreId, 'pages', pageId]">Annuler</button>
|
||||
<button type="button" class="btn-danger" (click)="delete()">Supprimer</button>
|
||||
<button type="button" class="btn-primary" (click)="save()" [disabled]="!title.trim()">
|
||||
Sauvegarder
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div *ngIf="aiError" class="ai-error-banner" role="alert">
|
||||
<span>{{ aiError }}</span>
|
||||
<button type="button" class="ai-error-dismiss" (click)="aiError = null" aria-label="Fermer">×</button>
|
||||
@if (page) {
|
||||
<div class="page">
|
||||
<app-breadcrumb [items]="breadcrumbItems"></app-breadcrumb>
|
||||
<header class="page-header">
|
||||
<div>
|
||||
<h1>{{ page.title }}</h1>
|
||||
<p class="subtitle">{{ template?.name || 'Page' }}</p>
|
||||
</div>
|
||||
<div class="header-actions">
|
||||
<button type="button" class="btn-ai"
|
||||
(click)="toggleChat()"
|
||||
[disabled]="aiLoading"
|
||||
[class.active]="chatOpen"
|
||||
title="Ouvrir l'Assistant IA (chat ou remplissage automatique)">
|
||||
<lucide-icon [img]="Sparkles" [size]="14"></lucide-icon>
|
||||
{{ aiLoading ? 'Génération…' : 'Assistant IA' }}
|
||||
</button>
|
||||
<button type="button" class="btn-secondary" [routerLink]="['/lore', loreId, 'pages', pageId]">Annuler</button>
|
||||
<button type="button" class="btn-danger" (click)="delete()">Supprimer</button>
|
||||
<button type="button" class="btn-primary" (click)="save()" [disabled]="!title.trim()">
|
||||
Sauvegarder
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
@if (aiError) {
|
||||
<div class="ai-error-banner" role="alert">
|
||||
<span>{{ aiError }}</span>
|
||||
<button type="button" class="ai-error-dismiss" (click)="aiError = null" aria-label="Fermer">×</button>
|
||||
</div>
|
||||
}
|
||||
<form class="edit-form">
|
||||
<!-- Identité ----------------------------------------------------- -->
|
||||
<div class="field">
|
||||
<label>Nom</label>
|
||||
<input type="text" [(ngModel)]="title" name="title" required />
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>Dossier</label>
|
||||
<select [(ngModel)]="nodeId" name="nodeId">
|
||||
@for (node of nodes; track node) {
|
||||
<option [value]="node.id">{{ node.name }}</option>
|
||||
}
|
||||
</select>
|
||||
<p class="hint">Déplacez cette page dans un autre dossier</p>
|
||||
</div>
|
||||
<!-- Champs dynamiques du template -------------------------------- -->
|
||||
@if (template?.fields?.length) {
|
||||
<h2 class="section-title">Champs</h2>
|
||||
@for (field of template!.fields; track field) {
|
||||
<!-- Champ TEXT : textarea editable -->
|
||||
@if (field.type === 'TEXT') {
|
||||
<div class="field">
|
||||
<label>{{ field.name }}</label>
|
||||
<textarea
|
||||
[(ngModel)]="values[field.name]"
|
||||
[name]="'value_' + field.name"
|
||||
rows="4"
|
||||
[placeholder]="'Valeur pour ' + field.name + '...'">
|
||||
</textarea>
|
||||
</div>
|
||||
}
|
||||
<!-- Champ IMAGE : galerie editable. -->
|
||||
@if (field.type === 'IMAGE') {
|
||||
<div class="field">
|
||||
<label>{{ field.name }}</label>
|
||||
<app-image-gallery
|
||||
[imageIds]="imageValues[field.name] || []"
|
||||
[editable]="true"
|
||||
[layout]="field.layout ?? 'GALLERY'"
|
||||
(imageIdsChange)="imageValues[field.name] = $event">
|
||||
</app-image-gallery>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
}
|
||||
<!-- Tags --------------------------------------------------------- -->
|
||||
<h2 class="section-title">Tags</h2>
|
||||
<div class="field">
|
||||
<app-chips-input
|
||||
[value]="tags"
|
||||
(valueChange)="tags = $event"
|
||||
placeholder="Ajouter un tag (Entrée pour valider)...">
|
||||
</app-chips-input>
|
||||
<p class="hint">Mots-clés libres pour classer et retrouver cette page</p>
|
||||
</div>
|
||||
<!-- Liens vers d'autres pages ----------------------------------- -->
|
||||
<h2 class="section-title">Pages liées</h2>
|
||||
<div class="field">
|
||||
<app-lore-link-picker
|
||||
[value]="relatedPageIds"
|
||||
[availablePages]="allPages"
|
||||
[excludePageId]="pageId"
|
||||
[loreId]="loreId"
|
||||
(valueChange)="relatedPageIds = $event">
|
||||
</app-lore-link-picker>
|
||||
<p class="hint">Cliquez sur un lien pour ouvrir la page associée</p>
|
||||
</div>
|
||||
<!-- Notes privées ----------------------------------------------- -->
|
||||
<h2 class="section-title">Notes privées</h2>
|
||||
<div class="field">
|
||||
<textarea
|
||||
[(ngModel)]="notes"
|
||||
name="notes"
|
||||
rows="4"
|
||||
placeholder="Notes personnelles (non exportées vers FoundryVTT)">
|
||||
</textarea>
|
||||
<p class="hint">Visibles uniquement par vous. Utiles pour préparer vos sessions.</p>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<form class="edit-form">
|
||||
|
||||
<!-- Identité ----------------------------------------------------- -->
|
||||
<div class="field">
|
||||
<label>Nom</label>
|
||||
<input type="text" [(ngModel)]="title" name="title" required />
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label>Dossier</label>
|
||||
<select [(ngModel)]="nodeId" name="nodeId">
|
||||
<option *ngFor="let node of nodes" [value]="node.id">{{ node.name }}</option>
|
||||
</select>
|
||||
<p class="hint">Déplacez cette page dans un autre dossier</p>
|
||||
</div>
|
||||
|
||||
<!-- Champs dynamiques du template -------------------------------- -->
|
||||
<ng-container *ngIf="template?.fields?.length">
|
||||
<h2 class="section-title">Champs</h2>
|
||||
<ng-container *ngFor="let field of template!.fields">
|
||||
<!-- Champ TEXT : textarea editable -->
|
||||
<div class="field" *ngIf="field.type === 'TEXT'">
|
||||
<label>{{ field.name }}</label>
|
||||
<textarea
|
||||
[(ngModel)]="values[field.name]"
|
||||
[name]="'value_' + field.name"
|
||||
rows="4"
|
||||
[placeholder]="'Valeur pour ' + field.name + '...'">
|
||||
</textarea>
|
||||
</div>
|
||||
<!-- Champ IMAGE : galerie editable. -->
|
||||
<div class="field" *ngIf="field.type === 'IMAGE'">
|
||||
<label>{{ field.name }}</label>
|
||||
<app-image-gallery
|
||||
[imageIds]="imageValues[field.name] || []"
|
||||
[editable]="true"
|
||||
[layout]="field.layout ?? 'GALLERY'"
|
||||
(imageIdsChange)="imageValues[field.name] = $event">
|
||||
</app-image-gallery>
|
||||
</div>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
|
||||
<!-- Tags --------------------------------------------------------- -->
|
||||
<h2 class="section-title">Tags</h2>
|
||||
<div class="field">
|
||||
<app-chips-input
|
||||
[value]="tags"
|
||||
(valueChange)="tags = $event"
|
||||
placeholder="Ajouter un tag (Entrée pour valider)...">
|
||||
</app-chips-input>
|
||||
<p class="hint">Mots-clés libres pour classer et retrouver cette page</p>
|
||||
</div>
|
||||
|
||||
<!-- Liens vers d'autres pages ----------------------------------- -->
|
||||
<h2 class="section-title">Pages liées</h2>
|
||||
<div class="field">
|
||||
<app-lore-link-picker
|
||||
[value]="relatedPageIds"
|
||||
[availablePages]="allPages"
|
||||
[excludePageId]="pageId"
|
||||
[loreId]="loreId"
|
||||
(valueChange)="relatedPageIds = $event">
|
||||
</app-lore-link-picker>
|
||||
<p class="hint">Cliquez sur un lien pour ouvrir la page associée</p>
|
||||
</div>
|
||||
|
||||
<!-- Notes privées ----------------------------------------------- -->
|
||||
<h2 class="section-title">Notes privées</h2>
|
||||
<div class="field">
|
||||
<textarea
|
||||
[(ngModel)]="notes"
|
||||
name="notes"
|
||||
rows="4"
|
||||
placeholder="Notes personnelles (non exportées vers FoundryVTT)">
|
||||
</textarea>
|
||||
<p class="hint">Visibles uniquement par vous. Utiles pour préparer vos sessions.</p>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- Drawer chat IA (hors du .page pour pouvoir couvrir le viewport côté droit) -->
|
||||
<app-ai-chat-drawer
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Component, OnInit, OnDestroy } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { ActivatedRoute, Router, RouterLink } from '@angular/router';
|
||||
import { forkJoin } from 'rxjs';
|
||||
@@ -36,7 +36,7 @@ import { ConfirmDialogService } from '../../shared/confirm-dialog/confirm-dialog
|
||||
*/
|
||||
@Component({
|
||||
selector: 'app-page-edit',
|
||||
imports: [CommonModule, FormsModule, RouterLink, LucideAngularModule, ChipsInputComponent, LoreLinkPickerComponent, BreadcrumbComponent, AiChatDrawerComponent, ImageGalleryComponent],
|
||||
imports: [FormsModule, RouterLink, LucideAngularModule, ChipsInputComponent, LoreLinkPickerComponent, BreadcrumbComponent, AiChatDrawerComponent, ImageGalleryComponent],
|
||||
templateUrl: './page-edit.component.html',
|
||||
styleUrls: ['./page-edit.component.scss']
|
||||
})
|
||||
|
||||
@@ -1,72 +1,81 @@
|
||||
<div class="view-page" *ngIf="page">
|
||||
|
||||
<app-breadcrumb [items]="breadcrumbItems"></app-breadcrumb>
|
||||
|
||||
<header class="view-header">
|
||||
<div>
|
||||
<h1>{{ page.title }}</h1>
|
||||
<p class="view-subtitle">{{ template?.name || 'Page' }}</p>
|
||||
</div>
|
||||
<div class="view-actions">
|
||||
<button type="button" class="btn-primary" (click)="editMode()">
|
||||
<lucide-icon [img]="Pencil" [size]="14"></lucide-icon>
|
||||
Modifier
|
||||
</button>
|
||||
<button type="button" class="btn-danger" (click)="deletePage()" title="Supprimer la page">
|
||||
<lucide-icon [img]="Trash2" [size]="14"></lucide-icon>
|
||||
Supprimer
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- Champs dynamiques du template (seuls les champs TEXT sont rendus ici ;
|
||||
le support complet des champs IMAGE arrive a l'etape 5). -->
|
||||
<ng-container *ngIf="template?.fields?.length">
|
||||
<ng-container *ngFor="let field of template!.fields">
|
||||
<section class="view-section" *ngIf="field.type === 'TEXT'">
|
||||
<h2 class="view-section-title">{{ field.name }}</h2>
|
||||
<p class="view-section-body" *ngIf="valueOf(field.name); else emptyField">{{ valueOf(field.name) }}</p>
|
||||
<ng-template #emptyField>
|
||||
<p class="view-section-empty">Non renseigné</p>
|
||||
</ng-template>
|
||||
@if (page) {
|
||||
<div class="view-page">
|
||||
<app-breadcrumb [items]="breadcrumbItems"></app-breadcrumb>
|
||||
<header class="view-header">
|
||||
<div>
|
||||
<h1>{{ page.title }}</h1>
|
||||
<p class="view-subtitle">{{ template?.name || 'Page' }}</p>
|
||||
</div>
|
||||
<div class="view-actions">
|
||||
<button type="button" class="btn-primary" (click)="editMode()">
|
||||
<lucide-icon [img]="Pencil" [size]="14"></lucide-icon>
|
||||
Modifier
|
||||
</button>
|
||||
<button type="button" class="btn-danger" (click)="deletePage()" title="Supprimer la page">
|
||||
<lucide-icon [img]="Trash2" [size]="14"></lucide-icon>
|
||||
Supprimer
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
<!-- Champs dynamiques du template (seuls les champs TEXT sont rendus ici ;
|
||||
le support complet des champs IMAGE arrive a l'etape 5). -->
|
||||
@if (template?.fields?.length) {
|
||||
@for (field of template!.fields; track field) {
|
||||
@if (field.type === 'TEXT') {
|
||||
<section class="view-section">
|
||||
<h2 class="view-section-title">{{ field.name }}</h2>
|
||||
@if (valueOf(field.name)) {
|
||||
<p class="view-section-body">{{ valueOf(field.name) }}</p>
|
||||
} @else {
|
||||
<p class="view-section-empty">Non renseigné</p>
|
||||
}
|
||||
</section>
|
||||
}
|
||||
@if (field.type === 'IMAGE') {
|
||||
<section class="view-section">
|
||||
<h2 class="view-section-title">{{ field.name }}</h2>
|
||||
<app-image-gallery
|
||||
[imageIds]="imageIdsOf(field.name)"
|
||||
[layout]="field.layout ?? 'GALLERY'">
|
||||
</app-image-gallery>
|
||||
</section>
|
||||
}
|
||||
}
|
||||
}
|
||||
<!-- Tags -->
|
||||
@if ((page.tags?.length ?? 0) > 0) {
|
||||
<section class="view-section">
|
||||
<h2 class="view-section-title">Tags</h2>
|
||||
<div class="view-chips">
|
||||
@for (tag of page.tags; track tag) {
|
||||
<span class="view-chip view-chip--tag">{{ tag }}</span>
|
||||
}
|
||||
</div>
|
||||
</section>
|
||||
<section class="view-section" *ngIf="field.type === 'IMAGE'">
|
||||
<h2 class="view-section-title">{{ field.name }}</h2>
|
||||
<app-image-gallery
|
||||
[imageIds]="imageIdsOf(field.name)"
|
||||
[layout]="field.layout ?? 'GALLERY'">
|
||||
</app-image-gallery>
|
||||
}
|
||||
<!-- Pages liées -->
|
||||
@if ((page.relatedPageIds?.length ?? 0) > 0) {
|
||||
<section class="view-section">
|
||||
<h2 class="view-section-title">Pages liées</h2>
|
||||
<div class="view-chips">
|
||||
@for (relId of page.relatedPageIds; track relId) {
|
||||
<a class="view-chip"
|
||||
[routerLink]="['/lore', loreId, 'pages', relId]">
|
||||
{{ titleOfRelated(relId) }}
|
||||
</a>
|
||||
}
|
||||
</div>
|
||||
</section>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
|
||||
<!-- Tags -->
|
||||
<section class="view-section" *ngIf="(page.tags?.length ?? 0) > 0">
|
||||
<h2 class="view-section-title">Tags</h2>
|
||||
<div class="view-chips">
|
||||
<span class="view-chip view-chip--tag" *ngFor="let tag of page.tags">{{ tag }}</span>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Pages liées -->
|
||||
<section class="view-section" *ngIf="(page.relatedPageIds?.length ?? 0) > 0">
|
||||
<h2 class="view-section-title">Pages liées</h2>
|
||||
<div class="view-chips">
|
||||
<a class="view-chip"
|
||||
*ngFor="let relId of page.relatedPageIds"
|
||||
[routerLink]="['/lore', loreId, 'pages', relId]">
|
||||
{{ titleOfRelated(relId) }}
|
||||
</a>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Notes privées MJ -->
|
||||
<section class="view-section view-section--private" *ngIf="page.notes?.trim()">
|
||||
<h2 class="view-section-title">
|
||||
<span class="view-section-icon">🔒</span>
|
||||
Notes privées
|
||||
</h2>
|
||||
<p class="view-section-body">{{ page.notes }}</p>
|
||||
</section>
|
||||
|
||||
</div>
|
||||
}
|
||||
<!-- Notes privées MJ -->
|
||||
@if (page.notes?.trim()) {
|
||||
<section class="view-section view-section--private">
|
||||
<h2 class="view-section-title">
|
||||
<span class="view-section-icon">🔒</span>
|
||||
Notes privées
|
||||
</h2>
|
||||
<p class="view-section-body">{{ page.notes }}</p>
|
||||
</section>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Component, OnInit, OnDestroy } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
|
||||
import { ActivatedRoute, Router, RouterModule } from '@angular/router';
|
||||
import { forkJoin } from 'rxjs';
|
||||
import { LucideAngularModule, Pencil, Trash2 } from 'lucide-angular';
|
||||
@@ -28,7 +28,7 @@ import { ConfirmDialogService } from '../../shared/confirm-dialog/confirm-dialog
|
||||
*/
|
||||
@Component({
|
||||
selector: 'app-page-view',
|
||||
imports: [CommonModule, RouterModule, LucideAngularModule, BreadcrumbComponent, ImageGalleryComponent],
|
||||
imports: [RouterModule, LucideAngularModule, BreadcrumbComponent, ImageGalleryComponent],
|
||||
templateUrl: './page-view.component.html',
|
||||
styleUrls: ['./page-view.component.scss']
|
||||
})
|
||||
|
||||
@@ -23,22 +23,23 @@
|
||||
<div class="field">
|
||||
<label for="template-default-node">Dossier par défaut *</label>
|
||||
|
||||
<ng-container *ngIf="nodes.length; else emptyFolders">
|
||||
@if (nodes.length) {
|
||||
<select id="template-default-node" formControlName="defaultNodeId">
|
||||
<option value="" disabled>Sélectionnez un dossier</option>
|
||||
<option *ngFor="let node of nodes" [value]="node.id">{{ node.name }}</option>
|
||||
@for (node of nodes; track node) {
|
||||
<option [value]="node.id">{{ node.name }}</option>
|
||||
}
|
||||
</select>
|
||||
<p class="hint">Les pages créées avec ce template seront placées dans ce dossier</p>
|
||||
</ng-container>
|
||||
|
||||
<ng-template #emptyFolders>
|
||||
} @else {
|
||||
<p class="empty-hint">
|
||||
Aucun dossier dans ce Lore.
|
||||
<a [routerLink]="['/lore', loreId, 'nodes', 'create']"
|
||||
[queryParams]="{ returnTo: nodeCreateReturnTo }"
|
||||
(click)="saveDraft()">Créer un dossier</a> d'abord.
|
||||
[queryParams]="{ returnTo: nodeCreateReturnTo }"
|
||||
(click)="saveDraft()">Créer un dossier</a> d'abord.
|
||||
</p>
|
||||
</ng-template>
|
||||
}
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
@@ -49,47 +50,51 @@
|
||||
<label class="section-label">Champs du template *</label>
|
||||
|
||||
<ul class="fields-list">
|
||||
<li class="field-row" *ngFor="let f of fields; let i = index; let first = first; let last = last">
|
||||
<div class="reorder-stack">
|
||||
<button type="button" class="btn-icon btn-reorder"
|
||||
(click)="moveField(i, -1)"
|
||||
[disabled]="first"
|
||||
aria-label="Monter d'un cran" title="Monter">
|
||||
<lucide-icon [img]="ChevronUp" [size]="12"></lucide-icon>
|
||||
@for (f of fields; track f; let i = $index; let first = $first; let last = $last) {
|
||||
<li class="field-row">
|
||||
<div class="reorder-stack">
|
||||
<button type="button" class="btn-icon btn-reorder"
|
||||
(click)="moveField(i, -1)"
|
||||
[disabled]="first"
|
||||
aria-label="Monter d'un cran" title="Monter">
|
||||
<lucide-icon [img]="ChevronUp" [size]="12"></lucide-icon>
|
||||
</button>
|
||||
<button type="button" class="btn-icon btn-reorder"
|
||||
(click)="moveField(i, 1)"
|
||||
[disabled]="last"
|
||||
aria-label="Descendre d'un cran" title="Descendre">
|
||||
<lucide-icon [img]="ChevronDown" [size]="12"></lucide-icon>
|
||||
</button>
|
||||
</div>
|
||||
<span class="field-chip" [class.field-chip-image]="f.type === 'IMAGE'">
|
||||
<lucide-icon [img]="f.type === 'IMAGE' ? ImageIcon : Type" [size]="12"></lucide-icon>
|
||||
{{ f.name }}
|
||||
</span>
|
||||
<button type="button"
|
||||
class="btn-icon btn-type-toggle"
|
||||
(click)="toggleFieldType(i)"
|
||||
[attr.aria-label]="'Basculer vers ' + (f.type === 'TEXT' ? 'Image' : 'Texte')"
|
||||
[title]="f.type === 'TEXT' ? 'Transformer en champ Image' : 'Transformer en champ Texte'">
|
||||
{{ f.type === 'TEXT' ? 'Texte' : 'Image' }}
|
||||
</button>
|
||||
<button type="button" class="btn-icon btn-reorder"
|
||||
(click)="moveField(i, 1)"
|
||||
[disabled]="last"
|
||||
aria-label="Descendre d'un cran" title="Descendre">
|
||||
<lucide-icon [img]="ChevronDown" [size]="12"></lucide-icon>
|
||||
@if (f.type === 'IMAGE') {
|
||||
<select
|
||||
class="layout-select"
|
||||
[ngModel]="f.layout ?? 'GALLERY'"
|
||||
[ngModelOptions]="{ standalone: true }"
|
||||
(ngModelChange)="setFieldLayout(i, $event)"
|
||||
title="Mise en page des images">
|
||||
<option value="GALLERY">Grille</option>
|
||||
<option value="HERO">Heros</option>
|
||||
<option value="MASONRY">Mosaique</option>
|
||||
<option value="CAROUSEL">Carrousel</option>
|
||||
</select>
|
||||
}
|
||||
<button type="button" class="btn-icon" (click)="removeField(i)" aria-label="Supprimer">
|
||||
<lucide-icon [img]="Trash2" [size]="14"></lucide-icon>
|
||||
</button>
|
||||
</div>
|
||||
<span class="field-chip" [class.field-chip-image]="f.type === 'IMAGE'">
|
||||
<lucide-icon [img]="f.type === 'IMAGE' ? ImageIcon : Type" [size]="12"></lucide-icon>
|
||||
{{ f.name }}
|
||||
</span>
|
||||
<button type="button"
|
||||
class="btn-icon btn-type-toggle"
|
||||
(click)="toggleFieldType(i)"
|
||||
[attr.aria-label]="'Basculer vers ' + (f.type === 'TEXT' ? 'Image' : 'Texte')"
|
||||
[title]="f.type === 'TEXT' ? 'Transformer en champ Image' : 'Transformer en champ Texte'">
|
||||
{{ f.type === 'TEXT' ? 'Texte' : 'Image' }}
|
||||
</button>
|
||||
<select *ngIf="f.type === 'IMAGE'"
|
||||
class="layout-select"
|
||||
[ngModel]="f.layout ?? 'GALLERY'"
|
||||
[ngModelOptions]="{ standalone: true }"
|
||||
(ngModelChange)="setFieldLayout(i, $event)"
|
||||
title="Mise en page des images">
|
||||
<option value="GALLERY">Grille</option>
|
||||
<option value="HERO">Heros</option>
|
||||
<option value="MASONRY">Mosaique</option>
|
||||
<option value="CAROUSEL">Carrousel</option>
|
||||
</select>
|
||||
<button type="button" class="btn-icon" (click)="removeField(i)" aria-label="Supprimer">
|
||||
<lucide-icon [img]="Trash2" [size]="14"></lucide-icon>
|
||||
</button>
|
||||
</li>
|
||||
</li>
|
||||
}
|
||||
</ul>
|
||||
|
||||
<div class="field-row add-row">
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
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';
|
||||
@@ -20,7 +20,7 @@ import { popReturnTo } from '../return-stack.helper';
|
||||
*/
|
||||
@Component({
|
||||
selector: 'app-template-create',
|
||||
imports: [CommonModule, FormsModule, ReactiveFormsModule, RouterModule, LucideAngularModule],
|
||||
imports: [FormsModule, ReactiveFormsModule, RouterModule, LucideAngularModule],
|
||||
templateUrl: './template-create.component.html',
|
||||
styleUrls: ['./template-create.component.scss']
|
||||
})
|
||||
|
||||
@@ -1,117 +1,110 @@
|
||||
<div class="page" *ngIf="template">
|
||||
|
||||
<header class="page-header">
|
||||
<div>
|
||||
<h1>{{ template.name }}</h1>
|
||||
<p class="subtitle">Template</p>
|
||||
</div>
|
||||
<div class="header-actions">
|
||||
<button type="button" class="btn-danger" (click)="delete()">Supprimer</button>
|
||||
<button type="button" class="btn-primary" (click)="save()" [disabled]="form.invalid">Sauvegarder</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<form [formGroup]="form" class="template-form">
|
||||
|
||||
<!-- Colonne gauche ---------------------------------------------- -->
|
||||
<div class="col-left">
|
||||
|
||||
<div class="field">
|
||||
<label for="template-edit-name">Nom</label>
|
||||
<input id="template-edit-name" type="text" formControlName="name" />
|
||||
@if (template) {
|
||||
<div class="page">
|
||||
<header class="page-header">
|
||||
<div>
|
||||
<h1>{{ template.name }}</h1>
|
||||
<p class="subtitle">Template</p>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label for="template-edit-default-node">Dossier par défaut</label>
|
||||
<select id="template-edit-default-node" formControlName="defaultNodeId">
|
||||
<option value="">-- Aucun --</option>
|
||||
<option *ngFor="let node of nodes" [value]="node.id">{{ node.name }}</option>
|
||||
</select>
|
||||
<p class="hint">Les pages créées avec ce template seront placées dans ce dossier par défaut</p>
|
||||
<div class="header-actions">
|
||||
<button type="button" class="btn-danger" (click)="delete()">Supprimer</button>
|
||||
<button type="button" class="btn-primary" (click)="save()" [disabled]="form.invalid">Sauvegarder</button>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label for="template-edit-description">Description</label>
|
||||
<textarea id="template-edit-description" formControlName="description" rows="6"></textarea>
|
||||
</header>
|
||||
<form [formGroup]="form" class="template-form">
|
||||
<!-- Colonne gauche ---------------------------------------------- -->
|
||||
<div class="col-left">
|
||||
<div class="field">
|
||||
<label for="template-edit-name">Nom</label>
|
||||
<input id="template-edit-name" type="text" formControlName="name" />
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="template-edit-default-node">Dossier par défaut</label>
|
||||
<select id="template-edit-default-node" formControlName="defaultNodeId">
|
||||
<option value="">-- Aucun --</option>
|
||||
@for (node of nodes; track node) {
|
||||
<option [value]="node.id">{{ node.name }}</option>
|
||||
}
|
||||
</select>
|
||||
<p class="hint">Les pages créées avec ce template seront placées dans ce dossier par défaut</p>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="template-edit-description">Description</label>
|
||||
<textarea id="template-edit-description" formControlName="description" rows="6"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Colonne droite --------------------------------------------- -->
|
||||
<div class="col-right">
|
||||
|
||||
<label class="section-label">Champs du template</label>
|
||||
|
||||
<ul class="fields-list">
|
||||
<li class="field-row" *ngFor="let f of fields; let i = index; let first = first; let last = last">
|
||||
<div class="reorder-stack">
|
||||
<button type="button" class="btn-icon-ghost btn-reorder"
|
||||
(click)="moveField(i, -1)"
|
||||
[disabled]="first"
|
||||
aria-label="Monter d'un cran" title="Monter">
|
||||
<lucide-icon [img]="ChevronUp" [size]="12"></lucide-icon>
|
||||
</button>
|
||||
<button type="button" class="btn-icon-ghost btn-reorder"
|
||||
(click)="moveField(i, 1)"
|
||||
[disabled]="last"
|
||||
aria-label="Descendre d'un cran" title="Descendre">
|
||||
<lucide-icon [img]="ChevronDown" [size]="12"></lucide-icon>
|
||||
</button>
|
||||
</div>
|
||||
<span class="field-chip"
|
||||
<!-- Colonne droite --------------------------------------------- -->
|
||||
<div class="col-right">
|
||||
<label class="section-label">Champs du template</label>
|
||||
<ul class="fields-list">
|
||||
@for (f of fields; track f; let i = $index; let first = $first; let last = $last) {
|
||||
<li class="field-row">
|
||||
<div class="reorder-stack">
|
||||
<button type="button" class="btn-icon-ghost btn-reorder"
|
||||
(click)="moveField(i, -1)"
|
||||
[disabled]="first"
|
||||
aria-label="Monter d'un cran" title="Monter">
|
||||
<lucide-icon [img]="ChevronUp" [size]="12"></lucide-icon>
|
||||
</button>
|
||||
<button type="button" class="btn-icon-ghost btn-reorder"
|
||||
(click)="moveField(i, 1)"
|
||||
[disabled]="last"
|
||||
aria-label="Descendre d'un cran" title="Descendre">
|
||||
<lucide-icon [img]="ChevronDown" [size]="12"></lucide-icon>
|
||||
</button>
|
||||
</div>
|
||||
<span class="field-chip"
|
||||
[class.field-chip-image]="f.type === 'IMAGE'"
|
||||
[class.field-chip-existing]="f.type !== 'IMAGE' && isExistingField(f)"
|
||||
[class.field-chip-new]="f.type !== 'IMAGE' && !isExistingField(f)">
|
||||
<lucide-icon [img]="f.type === 'IMAGE' ? ImageIcon : Type" [size]="12"></lucide-icon>
|
||||
{{ f.name }}
|
||||
</span>
|
||||
<button type="button"
|
||||
class="btn-icon-ghost btn-type-toggle"
|
||||
(click)="toggleFieldType(i)"
|
||||
[title]="f.type === 'TEXT' ? 'Transformer en champ Image' : 'Transformer en champ Texte'">
|
||||
{{ f.type === 'TEXT' ? 'Texte' : 'Image' }}
|
||||
</button>
|
||||
<select *ngIf="f.type === 'IMAGE'"
|
||||
<lucide-icon [img]="f.type === 'IMAGE' ? ImageIcon : Type" [size]="12"></lucide-icon>
|
||||
{{ f.name }}
|
||||
</span>
|
||||
<button type="button"
|
||||
class="btn-icon-ghost btn-type-toggle"
|
||||
(click)="toggleFieldType(i)"
|
||||
[title]="f.type === 'TEXT' ? 'Transformer en champ Image' : 'Transformer en champ Texte'">
|
||||
{{ f.type === 'TEXT' ? 'Texte' : 'Image' }}
|
||||
</button>
|
||||
@if (f.type === 'IMAGE') {
|
||||
<select
|
||||
class="layout-select"
|
||||
[ngModel]="f.layout ?? 'GALLERY'"
|
||||
[ngModelOptions]="{ standalone: true }"
|
||||
(ngModelChange)="setFieldLayout(i, $event)"
|
||||
title="Mise en page des images">
|
||||
<option value="GALLERY">Grille</option>
|
||||
<option value="HERO">Heros</option>
|
||||
<option value="MASONRY">Mosaique</option>
|
||||
<option value="CAROUSEL">Carrousel</option>
|
||||
<option value="GALLERY">Grille</option>
|
||||
<option value="HERO">Heros</option>
|
||||
<option value="MASONRY">Mosaique</option>
|
||||
<option value="CAROUSEL">Carrousel</option>
|
||||
</select>
|
||||
}
|
||||
<button type="button" class="btn-icon-danger" (click)="removeField(i)" aria-label="Supprimer">
|
||||
<lucide-icon [img]="Trash2" [size]="14"></lucide-icon>
|
||||
</button>
|
||||
</li>
|
||||
}
|
||||
</ul>
|
||||
<div class="field-row add-row">
|
||||
<input
|
||||
type="text"
|
||||
[(ngModel)]="newFieldName"
|
||||
[ngModelOptions]="{ standalone: true }"
|
||||
placeholder="+ Ajouter un champ"
|
||||
(keydown.enter)="$event.preventDefault(); addField()" />
|
||||
<select
|
||||
class="type-select"
|
||||
[(ngModel)]="newFieldType"
|
||||
[ngModelOptions]="{ standalone: true }"
|
||||
aria-label="Type du champ">
|
||||
<option value="TEXT">Texte</option>
|
||||
<option value="IMAGE">Image</option>
|
||||
</select>
|
||||
<button type="button" class="btn-icon-danger" (click)="removeField(i)" aria-label="Supprimer">
|
||||
<lucide-icon [img]="Trash2" [size]="14"></lucide-icon>
|
||||
<button type="button" class="btn-add" (click)="addField()" title="Ajouter le champ">
|
||||
<lucide-icon [img]="Plus" [size]="14"></lucide-icon>
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div class="field-row add-row">
|
||||
<input
|
||||
type="text"
|
||||
[(ngModel)]="newFieldName"
|
||||
[ngModelOptions]="{ standalone: true }"
|
||||
placeholder="+ Ajouter un champ"
|
||||
(keydown.enter)="$event.preventDefault(); addField()" />
|
||||
<select
|
||||
class="type-select"
|
||||
[(ngModel)]="newFieldType"
|
||||
[ngModelOptions]="{ standalone: true }"
|
||||
aria-label="Type du champ">
|
||||
<option value="TEXT">Texte</option>
|
||||
<option value="IMAGE">Image</option>
|
||||
</select>
|
||||
<button type="button" class="btn-add" (click)="addField()" title="Ajouter le champ">
|
||||
<lucide-icon [img]="Plus" [size]="14"></lucide-icon>
|
||||
</button>
|
||||
</div>
|
||||
<p class="hint">Texte = zone editable + generable par l'IA. Image = galerie d'illustrations.</p>
|
||||
</div>
|
||||
|
||||
<p class="hint">Texte = zone editable + generable par l'IA. Image = galerie d'illustrations.</p>
|
||||
|
||||
</div>
|
||||
|
||||
</form>
|
||||
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
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';
|
||||
@@ -21,7 +21,7 @@ import { ConfirmDialogService } from '../../shared/confirm-dialog/confirm-dialog
|
||||
*/
|
||||
@Component({
|
||||
selector: 'app-template-edit',
|
||||
imports: [CommonModule, FormsModule, ReactiveFormsModule, LucideAngularModule],
|
||||
imports: [FormsModule, ReactiveFormsModule, LucideAngularModule],
|
||||
templateUrl: './template-edit.component.html',
|
||||
styleUrls: ['./template-edit.component.scss']
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user