Files
LoreMind/web/src/app/lore/page-edit/page-edit.component.html
IETM_FIXE\ietm6 42c4b53ced
Some checks failed
Qualité & Sécurité / MegaLinter (PMD · Ruff · Bandit · gitleaks · hadolint) (push) Successful in 1m9s
Build & Push Images / build (brain) (push) Successful in 1m10s
Qualité & Sécurité / Trivy (CVE dépendances Maven / pip / npm) (push) Failing after 2m36s
Build & Push Images / build (web) (push) Successful in 1m48s
Build & Push Images / build-switcher (push) Successful in 20s
Build & Push Images / build (core) (push) Successful in 3m9s
Qualité & Sécurité / Web (ESLint · angular-eslint + sonarjs) (push) Successful in 38s
Stack qualité CI (MegaLinter, Trivy, ESLint) et purge du backlog lint web
CI (Gitea Actions) :
- Nouveau workflow quality.yml (séparé de ci.yml, non bloquant pour la release) :
  MegaLinter v9 (PMD, Ruff, Bandit, gitleaks, hadolint), ng lint (web) et
  Trivy (CVE des dépendances Maven/pip/npm), sur main + beta + PR
- Trivy : préchargement du cache Maven avant le scan — sans ça Maven Central
  rate-limite l'IP du runner (429) en résolvant les BOMs du parent Spring Boot
- upload-artifact v4 → v3 : l'API artifacts v4 n'est pas supportée par Gitea
  (corrige aussi l'upload du rapport Playwright de e2e.yml, cassé depuis toujours)
- Ruleset PMD projet (java-pmd-ruleset.xml, auto-détecté par MegaLinter) orienté
  bugs réels, sans le style Lombok-hostile du défaut : 11 337 → 65 findings ;
  PMD en mode rapport (JAVA_PMD_DISABLE_ERRORS) le temps de purger ce backlog

Web :
- angular-eslint 21 + eslint-plugin-sonarjs : les règles Sonar vivent désormais
  dans ESLint (config web/eslint.config.js, réglages justifiés en commentaire)
- tsconfig : skipLibCheck + types:[] + node_modules ré-exclu — répare les builds
  desktop Windows/Linux cassés par le conflit @types/eslint-scope vs ESLint 9
- Purge du backlog lint : 114 erreurs → 0 sur 66 fichiers (ngOnDestroy vides
  supprimés, any typés avec les vrais DTOs, outputs (close) → (closed), regex
  super-linéaires désamorcées, ternaires/fonctions imbriqués dépliés, intention
  documentée sur les catch/error volontairement vides)
- Bug latent corrigé au passage : license.service.disconnect() émettait
  undefined au lieu de true en cas de succès (masqué par un double cast)
2026-07-07 01:05:00 +02:00

201 lines
8.8 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
@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 || ('pageEdit.pageFallback' | translate) }}</p>
</div>
<div class="header-actions">
<button type="button" class="btn-ai"
(click)="toggleChat()"
[disabled]="aiLoading"
[class.active]="chatOpen"
[title]="'pageEdit.aiTitle' | translate">
<lucide-icon [img]="Sparkles" [size]="14"></lucide-icon>
{{ (aiLoading ? 'pageEdit.generating' : 'pageEdit.aiAssistant') | translate }}
</button>
<button type="button" class="btn-secondary" [routerLink]="['/lore', loreId, 'pages', pageId]">{{ 'common.cancel' | translate }}</button>
<button type="button" class="btn-danger" (click)="delete()">{{ 'common.delete' | translate }}</button>
<button type="button" class="btn-primary" (click)="save()" [disabled]="!title.trim()">
{{ 'common.save' | translate }}
</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" [attr.aria-label]="'common.close' | translate">×</button>
</div>
}
<form class="edit-form">
<!-- Identité ----------------------------------------------------- -->
<div class="field">
<label>{{ 'common.name' | translate }}</label>
<input type="text" [(ngModel)]="title" name="title" required />
</div>
<div class="field">
<label>{{ 'pageEdit.folder' | translate }}</label>
<select [(ngModel)]="nodeId" name="nodeId">
@for (node of nodes; track node) {
<option [value]="node.id">{{ node.name }}</option>
}
</select>
<p class="hint">{{ 'pageEdit.folderHint' | translate }}</p>
</div>
<!-- Champs dynamiques du template. Rendus en grille 12 colonnes quand le
template porte une mise en page (hasLayout) ; sinon empilés (rendu
historique). Les blocs suivent l'ordre (ligne, colonne) du template. -->
@if (template?.fields?.length) {
<h2 class="section-title">{{ 'pageEdit.fields' | translate }}</h2>
<div class="edit-fields" [class.edit-fields--grid]="hasLayout">
@for (field of orderedFields; track field) {
<!-- Champ TEXT : textarea editable -->
@if (field.type === 'TEXT') {
<div class="field"
[style.grid-column]="hasLayout ? gridColumn(field) : null">
<label>{{ field.name }}</label>
<textarea
[(ngModel)]="values[keyOf(field)]"
[name]="'value_' + keyOf(field)"
rows="4"
[placeholder]="'pageEdit.valuePlaceholder' | translate:{ field: field.name }">
</textarea>
</div>
}
<!-- Champ IMAGE : galerie editable. -->
@if (field.type === 'IMAGE') {
<div class="field"
[style.grid-column]="hasLayout ? gridColumn(field) : null">
<label>{{ field.name }}</label>
<app-image-block
[editable]="true"
[imageIds]="imageValues[keyOf(field)] || []"
(imageIdsChange)="imageValues[keyOf(field)] = $event"
[framing]="imageFraming[keyOf(field)] || {}"
(framingChange)="imageFraming[keyOf(field)] = $event">
</app-image-block>
</div>
}
<!-- Champ KEY_VALUE_LIST : liste libellé/valeur (labels figés par le template). -->
@if (field.type === 'KEY_VALUE_LIST') {
<div class="field"
[style.grid-column]="hasLayout ? gridColumn(field) : null">
<label>{{ field.name }}</label>
<div class="kv-grid">
@for (lbl of field.labels ?? []; track $index) {
<div class="kv-cell">
<span class="kv-label">{{ lbl }}</span>
<input
type="text"
[(ngModel)]="keyValueValues[keyOf(field)][lbl]"
[name]="'kv_' + keyOf(field) + '_' + lbl"
placeholder="—" />
</div>
}
@if (!(field.labels ?? []).length) {
<p class="kv-empty">{{ 'pageEdit.noLabels' | translate }}</p>
}
</div>
</div>
}
<!-- Champ TABLE : colonnes figées par le template, lignes libres. -->
@if (field.type === 'TABLE') {
<div class="field"
[style.grid-column]="hasLayout ? gridColumn(field) : null">
<label>{{ field.name }}</label>
@if ((field.labels ?? []).length) {
<div class="table-edit-wrap">
<table class="table-edit">
<thead>
<tr>
@for (col of field.labels; track $index) {
<th>{{ col }}</th>
}
<th class="table-edit-actions-col"></th>
</tr>
</thead>
<tbody>
@for (row of tableRows(keyOf(field)); track $index; let ri = $index) {
<tr>
@for (col of field.labels; track $index) {
<td>
<input
type="text"
[(ngModel)]="row[col]"
[name]="'tbl_' + keyOf(field) + '_' + ri + '_' + col"
[placeholder]="col" />
</td>
}
<td class="table-edit-actions-col">
<button type="button" class="btn-row-delete"
(click)="removeTableRow(keyOf(field), ri)"
[attr.aria-label]="'pageEdit.deleteRowN' | translate:{ n: (ri + 1) }" [title]="'pageEdit.deleteRow' | translate">
<lucide-icon [img]="Trash2" [size]="13"></lucide-icon>
</button>
</td>
</tr>
}
</tbody>
</table>
<button type="button" class="btn-row-add" (click)="addTableRow(keyOf(field), field.labels)">
<lucide-icon [img]="Plus" [size]="13"></lucide-icon>
{{ 'pageEdit.addRow' | translate }}
</button>
</div>
} @else {
<p class="kv-empty">{{ 'pageEdit.noColumns' | translate }}</p>
}
</div>
}
}
</div>
}
<!-- Tags --------------------------------------------------------- -->
<h2 class="section-title">{{ 'pageEdit.tags' | translate }}</h2>
<div class="field">
<app-chips-input
[value]="tags"
(valueChange)="tags = $event"
[placeholder]="'pageEdit.tagsPlaceholder' | translate">
</app-chips-input>
<p class="hint">{{ 'pageEdit.tagsHint' | translate }}</p>
</div>
<!-- Liens vers d'autres pages ----------------------------------- -->
<h2 class="section-title">{{ 'pageEdit.relatedPages' | translate }}</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">{{ 'pageEdit.relatedPagesHint' | translate }}</p>
</div>
<!-- Notes privées ----------------------------------------------- -->
<h2 class="section-title">{{ 'pageEdit.privateNotes' | translate }}</h2>
<div class="field">
<textarea
[(ngModel)]="notes"
name="notes"
rows="4"
[placeholder]="'pageEdit.privateNotesPlaceholder' | translate">
</textarea>
<p class="hint">{{ 'pageEdit.privateNotesHint' | translate }}</p>
</div>
</form>
</div>
}
<!-- Drawer chat IA (hors du .page pour pouvoir couvrir le viewport côté droit) -->
<app-ai-chat-drawer
[loreId]="loreId"
[pageId]="pageId"
[isOpen]="chatOpen"
[quickSuggestions]="chatQuickSuggestions"
[primaryAction]="chatPrimaryAction"
(closed)="chatOpen = false"
(primaryActionClick)="onChatFillRequested()">
</app-ai-chat-drawer>