Refonte de toute la partie fiche de personnage avec mise en place d'un nouveau bloc de liste d'attribut (pour tout ce qui sera statistiques, compétences etc....)
Passage V0.8.3
This commit is contained in:
@@ -1,7 +1,14 @@
|
||||
import { Component, Input } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { LucideAngularModule, BookOpen } from 'lucide-angular';
|
||||
import { TemplateField } from '../../services/template.model';
|
||||
import { TemplateField, ImageLayout } from '../../services/template.model';
|
||||
|
||||
/** Section rendue dans la vue, dans l'ordre du template. Discriminee par `kind`. */
|
||||
export type RenderedSection =
|
||||
| { kind: 'TEXT'; name: string; value: string }
|
||||
| { kind: 'NUMBER_GROUP'; entries: { label: string; value: string }[] }
|
||||
| { kind: 'KEY_VALUE_LIST'; name: string; entries: { label: string; value: string }[] }
|
||||
| { kind: 'IMAGE'; name: string; ids: string[]; layout: ImageLayout };
|
||||
import { ImageService } from '../../services/image.service';
|
||||
import { ImageGalleryComponent } from '../image-gallery/image-gallery.component';
|
||||
|
||||
@@ -22,6 +29,7 @@ export interface PersonaLike {
|
||||
headerImageId?: string | null;
|
||||
values?: Record<string, string>;
|
||||
imageValues?: Record<string, string[]>;
|
||||
keyValueValues?: Record<string, Record<string, string>>;
|
||||
}
|
||||
|
||||
@Component({
|
||||
@@ -46,30 +54,69 @@ export class PersonaViewComponent {
|
||||
return this.imageService.contentUrl(id);
|
||||
}
|
||||
|
||||
/** Champs TEXT/NUMBER non vides, dans l'ordre du template. */
|
||||
get textFields(): { name: string; value: string; isNumber: boolean }[] {
|
||||
if (!this.persona?.values) return [];
|
||||
return this.templateFields
|
||||
.filter(f => (f.type === 'TEXT' || f.type === 'NUMBER'))
|
||||
.map(f => ({
|
||||
name: f.name,
|
||||
value: this.persona!.values?.[f.name] ?? '',
|
||||
isNumber: f.type === 'NUMBER'
|
||||
}))
|
||||
.filter(x => x.value && x.value.trim().length > 0);
|
||||
/**
|
||||
* Decompose la fiche en (heroBadges, sections) en un seul passage :
|
||||
* - NUMBER consecutifs groupes : 2+ → NUMBER_GROUP en section ; 1 isole → badge hero.
|
||||
* - TEXT / KEY_VALUE_LIST / IMAGE : sections dans l'ordre du template.
|
||||
* Le calcul est fait par rendered() et cache via le get pour eviter les
|
||||
* recalculs multiples par cycle de change detection.
|
||||
*/
|
||||
private rendered(): { heroBadges: { label: string; value: string }[]; sections: RenderedSection[] } {
|
||||
const sections: RenderedSection[] = [];
|
||||
const heroBadges: { label: string; value: string }[] = [];
|
||||
let numberBuffer: { label: string; value: string }[] = [];
|
||||
|
||||
const flushNumberBuffer = () => {
|
||||
if (numberBuffer.length === 1) {
|
||||
heroBadges.push(numberBuffer[0]);
|
||||
} else if (numberBuffer.length > 1) {
|
||||
sections.push({ kind: 'NUMBER_GROUP', entries: numberBuffer });
|
||||
}
|
||||
numberBuffer = [];
|
||||
};
|
||||
|
||||
for (const f of this.templateFields) {
|
||||
if (f.type === 'NUMBER') {
|
||||
const value = this.persona?.values?.[f.name] ?? '';
|
||||
if (value.trim()) numberBuffer.push({ label: f.name, value });
|
||||
continue;
|
||||
}
|
||||
flushNumberBuffer();
|
||||
if (f.type === 'TEXT') {
|
||||
const value = this.persona?.values?.[f.name] ?? '';
|
||||
if (value.trim()) sections.push({ kind: 'TEXT', name: f.name, value });
|
||||
} else if (f.type === 'KEY_VALUE_LIST') {
|
||||
const inner = this.persona?.keyValueValues?.[f.name] ?? {};
|
||||
const labels = f.labels ?? [];
|
||||
const entries = labels.map(label => ({ label, value: inner[label] ?? '' }));
|
||||
if (entries.some(e => e.value.trim())) {
|
||||
sections.push({ kind: 'KEY_VALUE_LIST', name: f.name, entries });
|
||||
}
|
||||
} else if (f.type === 'IMAGE') {
|
||||
const ids = this.persona?.imageValues?.[f.name] ?? [];
|
||||
if (ids.length > 0) {
|
||||
sections.push({ kind: 'IMAGE', name: f.name, ids, layout: f.layout ?? 'GALLERY' });
|
||||
}
|
||||
}
|
||||
}
|
||||
flushNumberBuffer();
|
||||
return { heroBadges, sections };
|
||||
}
|
||||
|
||||
/** Champs IMAGE non vides, dans l'ordre du template. */
|
||||
get imageFields(): { field: TemplateField; ids: string[] }[] {
|
||||
if (!this.persona?.imageValues) return [];
|
||||
return this.templateFields
|
||||
.filter(f => f.type === 'IMAGE')
|
||||
.map(f => ({ field: f, ids: this.persona!.imageValues?.[f.name] ?? [] }))
|
||||
.filter(x => x.ids.length > 0);
|
||||
get heroBadges(): { label: string; value: string }[] {
|
||||
return this.rendered().heroBadges;
|
||||
}
|
||||
|
||||
hasAnyNumber(fields: { isNumber: boolean }[]): boolean {
|
||||
return fields.some(f => f.isNumber);
|
||||
get orderedSections(): RenderedSection[] {
|
||||
return this.rendered().sections;
|
||||
}
|
||||
|
||||
/** Pour la drop cap : seul le 1er TEXT la recoit. */
|
||||
get firstTextSectionName(): string | null {
|
||||
for (const s of this.orderedSections) {
|
||||
if (s.kind === 'TEXT') return s.name;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Premier paragraphe d'un texte (utilise pour la drop cap). */
|
||||
|
||||
Reference in New Issue
Block a user