Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 86836ad81c |
@@ -41,7 +41,7 @@ from app.infrastructure.onemin_adapter import OneMinAiLLMProvider
|
||||
app = FastAPI(
|
||||
title="LoreMind Brain",
|
||||
description="Backend IA pour la génération de contenu narratif.",
|
||||
version="0.6.6",
|
||||
version="0.8.3",
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
|
||||
<groupId>com.loremind</groupId>
|
||||
<artifactId>loremind-core</artifactId>
|
||||
<version>0.8.1</version>
|
||||
<version>0.8.3</version>
|
||||
<name>LoreMind Core</name>
|
||||
<description>Backend Core - Architecture Hexagonale</description>
|
||||
|
||||
|
||||
@@ -32,6 +32,7 @@ public class CharacterService {
|
||||
String headerImageId,
|
||||
Map<String, String> values,
|
||||
Map<String, List<String>> imageValues,
|
||||
Map<String, Map<String, String>> keyValueValues,
|
||||
String campaignId,
|
||||
Integer order
|
||||
) {}
|
||||
@@ -46,6 +47,7 @@ public class CharacterService {
|
||||
.headerImageId(data.headerImageId())
|
||||
.values(data.values() != null ? new HashMap<>(data.values()) : new HashMap<>())
|
||||
.imageValues(data.imageValues() != null ? new HashMap<>(data.imageValues()) : new HashMap<>())
|
||||
.keyValueValues(data.keyValueValues() != null ? new HashMap<>(data.keyValueValues()) : new HashMap<>())
|
||||
.campaignId(data.campaignId())
|
||||
.order(order)
|
||||
.build();
|
||||
@@ -68,6 +70,7 @@ public class CharacterService {
|
||||
existing.setHeaderImageId(data.headerImageId());
|
||||
existing.setValues(data.values() != null ? new HashMap<>(data.values()) : new HashMap<>());
|
||||
existing.setImageValues(data.imageValues() != null ? new HashMap<>(data.imageValues()) : new HashMap<>());
|
||||
existing.setKeyValueValues(data.keyValueValues() != null ? new HashMap<>(data.keyValueValues()) : new HashMap<>());
|
||||
if (data.order() != null) {
|
||||
existing.setOrder(data.order());
|
||||
}
|
||||
|
||||
@@ -27,6 +27,7 @@ public class NpcService {
|
||||
String headerImageId,
|
||||
Map<String, String> values,
|
||||
Map<String, List<String>> imageValues,
|
||||
Map<String, Map<String, String>> keyValueValues,
|
||||
String campaignId,
|
||||
Integer order
|
||||
) {}
|
||||
@@ -41,6 +42,7 @@ public class NpcService {
|
||||
.headerImageId(data.headerImageId())
|
||||
.values(data.values() != null ? new HashMap<>(data.values()) : new HashMap<>())
|
||||
.imageValues(data.imageValues() != null ? new HashMap<>(data.imageValues()) : new HashMap<>())
|
||||
.keyValueValues(data.keyValueValues() != null ? new HashMap<>(data.keyValueValues()) : new HashMap<>())
|
||||
.campaignId(data.campaignId())
|
||||
.order(order)
|
||||
.build();
|
||||
@@ -63,6 +65,7 @@ public class NpcService {
|
||||
existing.setHeaderImageId(data.headerImageId());
|
||||
existing.setValues(data.values() != null ? new HashMap<>(data.values()) : new HashMap<>());
|
||||
existing.setImageValues(data.imageValues() != null ? new HashMap<>(data.imageValues()) : new HashMap<>());
|
||||
existing.setKeyValueValues(data.keyValueValues() != null ? new HashMap<>(data.keyValueValues()) : new HashMap<>());
|
||||
if (data.order() != null) {
|
||||
existing.setOrder(data.order());
|
||||
}
|
||||
|
||||
@@ -51,6 +51,14 @@ public class Character {
|
||||
*/
|
||||
private Map<String, List<String>> imageValues;
|
||||
|
||||
/**
|
||||
* Valeurs des champs KEY_VALUE_LIST du template PJ. Cle externe = nom du
|
||||
* champ template (ex: "Caracteristiques"), cle interne = label predefini
|
||||
* dans le template (ex: "FOR"), valeur = valeur saisie (ex: "16").
|
||||
* Les labels suivent l'ordre defini dans TemplateField.labels.
|
||||
*/
|
||||
private Map<String, Map<String, String>> keyValueValues;
|
||||
|
||||
/** Référence vers la Campaign parente. */
|
||||
private String campaignId;
|
||||
|
||||
@@ -70,4 +78,9 @@ public class Character {
|
||||
if (imageValues == null) imageValues = new HashMap<>();
|
||||
return imageValues;
|
||||
}
|
||||
|
||||
public Map<String, Map<String, String>> getKeyValueValues() {
|
||||
if (keyValueValues == null) keyValueValues = new HashMap<>();
|
||||
return keyValueValues;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,6 +40,9 @@ public class Npc {
|
||||
/** Valeurs IMAGE du template PNJ (listes d'IDs ordonnees par champ). Jamais null. */
|
||||
private Map<String, List<String>> imageValues;
|
||||
|
||||
/** Valeurs KEY_VALUE_LIST : fieldName -> label -> value. Jamais null. */
|
||||
private Map<String, Map<String, String>> keyValueValues;
|
||||
|
||||
/** Référence vers la Campaign parente (cross-aggregate via ID). */
|
||||
private String campaignId;
|
||||
|
||||
@@ -58,4 +61,9 @@ public class Npc {
|
||||
if (imageValues == null) imageValues = new HashMap<>();
|
||||
return imageValues;
|
||||
}
|
||||
|
||||
public Map<String, Map<String, String>> getKeyValueValues() {
|
||||
if (keyValueValues == null) keyValueValues = new HashMap<>();
|
||||
return keyValueValues;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,11 +6,15 @@ package com.loremind.domain.shared.template;
|
||||
* - TEXT : valeur textuelle libre (Map<String, String>)
|
||||
* - IMAGE : galerie d'images, liste d'IDs (Map<String, List<String>>)
|
||||
* - NUMBER : valeur numerique stockee en texte (parsee a l'usage)
|
||||
* - KEY_VALUE_LIST : liste de paires {label, value} avec labels figes au template
|
||||
* (Map<String, Map<String, String>> : fieldName -> label -> value).
|
||||
* Usage : stat blocks, listes de competences, traits.
|
||||
* <p>
|
||||
* Extension future possible : RICH_TEXT, DATE, BOOLEAN, KEY_VALUE_LIST, REFERENCE...
|
||||
* Extension future possible : RICH_TEXT, DATE, BOOLEAN, REFERENCE...
|
||||
*/
|
||||
public enum FieldType {
|
||||
TEXT,
|
||||
IMAGE,
|
||||
NUMBER
|
||||
NUMBER,
|
||||
KEY_VALUE_LIST
|
||||
}
|
||||
|
||||
@@ -5,6 +5,8 @@ import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Value Object d'un champ de Template (kernel partage).
|
||||
* <p>
|
||||
@@ -27,29 +29,45 @@ public class TemplateField {
|
||||
private FieldType type;
|
||||
/** Variante de rendu pour les champs IMAGE. Null = GALLERY. */
|
||||
private ImageLayout layout;
|
||||
/**
|
||||
* Labels predefinis pour les champs KEY_VALUE_LIST (ordre significatif).
|
||||
* Ex: ["FOR","DEX","CON","INT","SAG","CHA"] pour un champ "Caracteristiques".
|
||||
* Null/vide pour les autres types.
|
||||
*/
|
||||
private List<String> labels;
|
||||
|
||||
/** Constructeur de retrocompat : type seul, layout=null. */
|
||||
/** Constructeur de retrocompat : type seul, layout/labels=null. */
|
||||
public TemplateField(String name, FieldType type) {
|
||||
this(name, type, null);
|
||||
this(name, type, null, null);
|
||||
}
|
||||
|
||||
/** Constructeur de retrocompat : type + layout, labels=null. */
|
||||
public TemplateField(String name, FieldType type, ImageLayout layout) {
|
||||
this(name, type, layout, null);
|
||||
}
|
||||
|
||||
/** Raccourci : construit un champ de type TEXT (cas le plus courant). */
|
||||
public static TemplateField text(String name) {
|
||||
return new TemplateField(name, FieldType.TEXT, null);
|
||||
return new TemplateField(name, FieldType.TEXT, null, null);
|
||||
}
|
||||
|
||||
/** Raccourci : construit un champ de type IMAGE avec layout GALLERY. */
|
||||
public static TemplateField image(String name) {
|
||||
return new TemplateField(name, FieldType.IMAGE, ImageLayout.GALLERY);
|
||||
return new TemplateField(name, FieldType.IMAGE, ImageLayout.GALLERY, null);
|
||||
}
|
||||
|
||||
/** Raccourci : construit un champ IMAGE avec un layout specifique. */
|
||||
public static TemplateField image(String name, ImageLayout layout) {
|
||||
return new TemplateField(name, FieldType.IMAGE, layout);
|
||||
return new TemplateField(name, FieldType.IMAGE, layout, null);
|
||||
}
|
||||
|
||||
/** Raccourci : construit un champ de type NUMBER. */
|
||||
public static TemplateField number(String name) {
|
||||
return new TemplateField(name, FieldType.NUMBER, null);
|
||||
return new TemplateField(name, FieldType.NUMBER, null, null);
|
||||
}
|
||||
|
||||
/** Raccourci : construit un champ KEY_VALUE_LIST avec labels predefinis. */
|
||||
public static TemplateField keyValueList(String name, List<String> labels) {
|
||||
return new TemplateField(name, FieldType.KEY_VALUE_LIST, null, labels);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -152,12 +152,8 @@ public class GameSystemSeeder {
|
||||
TemplateField.number("Niveau"),
|
||||
TemplateField.number("PV max"),
|
||||
TemplateField.number("CA"),
|
||||
TemplateField.number("FOR"),
|
||||
TemplateField.number("DEX"),
|
||||
TemplateField.number("CON"),
|
||||
TemplateField.number("INT"),
|
||||
TemplateField.number("SAG"),
|
||||
TemplateField.number("CHA"),
|
||||
TemplateField.keyValueList("Caracteristiques",
|
||||
List.of("FOR", "DEX", "CON", "INT", "SAG", "CHA")),
|
||||
TemplateField.text("Competences"),
|
||||
TemplateField.text("Equipement"),
|
||||
TemplateField.text("Sorts"),
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
package com.loremind.infrastructure.persistence.converter;
|
||||
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import jakarta.persistence.AttributeConverter;
|
||||
import jakarta.persistence.Converter;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Convertit une Map<String, Map<String, String>> en JSON et inversement.
|
||||
* <p>
|
||||
* Utilise pour Character/Npc.keyValueValues : pour chaque champ KEY_VALUE_LIST
|
||||
* du template, stocke une map label -> value. Exemple :
|
||||
* {"Caracteristiques": {"FOR":"16","DEX":"12","CON":"14"}}
|
||||
* <p>
|
||||
* Adaptateur technique pur : le domaine ignore ce converter.
|
||||
*/
|
||||
@Converter
|
||||
public class StringMapMapJsonConverter
|
||||
implements AttributeConverter<Map<String, Map<String, String>>, String> {
|
||||
|
||||
private static final ObjectMapper MAPPER = new ObjectMapper();
|
||||
private static final TypeReference<Map<String, Map<String, String>>> TYPE_REF =
|
||||
new TypeReference<>() {};
|
||||
|
||||
@Override
|
||||
public String convertToDatabaseColumn(Map<String, Map<String, String>> attribute) {
|
||||
if (attribute == null || attribute.isEmpty()) return "{}";
|
||||
try {
|
||||
return MAPPER.writeValueAsString(attribute);
|
||||
} catch (Exception e) {
|
||||
throw new IllegalStateException(
|
||||
"Erreur serialisation Map<String, Map<String,String>> -> JSON", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Map<String, String>> convertToEntityAttribute(String dbData) {
|
||||
if (dbData == null || dbData.isBlank()) return Collections.emptyMap();
|
||||
try {
|
||||
return MAPPER.readValue(dbData, TYPE_REF);
|
||||
} catch (Exception e) {
|
||||
throw new IllegalStateException(
|
||||
"Erreur deserialisation JSON -> Map<String, Map<String,String>>", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -85,8 +85,18 @@ public class TemplateFieldListJsonConverter
|
||||
}
|
||||
}
|
||||
}
|
||||
List<String> labels = null;
|
||||
if (type == FieldType.KEY_VALUE_LIST) {
|
||||
JsonNode labelsNode = item.path("labels");
|
||||
if (labelsNode.isArray()) {
|
||||
labels = new ArrayList<>();
|
||||
for (JsonNode label : labelsNode) {
|
||||
if (label.isTextual()) labels.add(label.asText());
|
||||
}
|
||||
}
|
||||
}
|
||||
if (name != null && !name.isBlank()) {
|
||||
result.add(new TemplateField(name, type, layout));
|
||||
result.add(new TemplateField(name, type, layout, labels));
|
||||
}
|
||||
}
|
||||
// Autres types de noeuds (nombre, booleen...) : ignores silencieusement.
|
||||
|
||||
@@ -2,6 +2,7 @@ package com.loremind.infrastructure.persistence.entity;
|
||||
|
||||
import com.loremind.infrastructure.persistence.converter.StringListMapJsonConverter;
|
||||
import com.loremind.infrastructure.persistence.converter.StringMapJsonConverter;
|
||||
import com.loremind.infrastructure.persistence.converter.StringMapMapJsonConverter;
|
||||
import jakarta.persistence.*;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
@@ -53,6 +54,11 @@ public class CharacterJpaEntity {
|
||||
@Column(name = "image_values", columnDefinition = "TEXT")
|
||||
private Map<String, List<String>> imageValues;
|
||||
|
||||
/** Valeurs KEY_VALUE_LIST serialisees JSON (fieldName -> label -> value). */
|
||||
@Convert(converter = StringMapMapJsonConverter.class)
|
||||
@Column(name = "key_value_values", columnDefinition = "TEXT")
|
||||
private Map<String, Map<String, String>> keyValueValues;
|
||||
|
||||
@Column(name = "campaign_id", nullable = false)
|
||||
private Long campaignId;
|
||||
|
||||
@@ -71,6 +77,7 @@ public class CharacterJpaEntity {
|
||||
updatedAt = LocalDateTime.now();
|
||||
if (values == null) values = new HashMap<>();
|
||||
if (imageValues == null) imageValues = new HashMap<>();
|
||||
if (keyValueValues == null) keyValueValues = new HashMap<>();
|
||||
}
|
||||
|
||||
@PreUpdate
|
||||
|
||||
@@ -2,6 +2,7 @@ package com.loremind.infrastructure.persistence.entity;
|
||||
|
||||
import com.loremind.infrastructure.persistence.converter.StringListMapJsonConverter;
|
||||
import com.loremind.infrastructure.persistence.converter.StringMapJsonConverter;
|
||||
import com.loremind.infrastructure.persistence.converter.StringMapMapJsonConverter;
|
||||
import jakarta.persistence.*;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
@@ -46,6 +47,10 @@ public class NpcJpaEntity {
|
||||
@Column(name = "image_values", columnDefinition = "TEXT")
|
||||
private Map<String, List<String>> imageValues;
|
||||
|
||||
@Convert(converter = StringMapMapJsonConverter.class)
|
||||
@Column(name = "key_value_values", columnDefinition = "TEXT")
|
||||
private Map<String, Map<String, String>> keyValueValues;
|
||||
|
||||
@Column(name = "campaign_id", nullable = false)
|
||||
private Long campaignId;
|
||||
|
||||
@@ -64,6 +69,7 @@ public class NpcJpaEntity {
|
||||
updatedAt = LocalDateTime.now();
|
||||
if (values == null) values = new HashMap<>();
|
||||
if (imageValues == null) imageValues = new HashMap<>();
|
||||
if (keyValueValues == null) keyValueValues = new HashMap<>();
|
||||
}
|
||||
|
||||
@PreUpdate
|
||||
|
||||
@@ -57,6 +57,7 @@ public class PostgresCharacterRepository implements CharacterRepository {
|
||||
.headerImageId(e.getHeaderImageId())
|
||||
.values(e.getValues() != null ? new HashMap<>(e.getValues()) : new HashMap<>())
|
||||
.imageValues(e.getImageValues() != null ? new HashMap<>(e.getImageValues()) : new HashMap<>())
|
||||
.keyValueValues(e.getKeyValueValues() != null ? new HashMap<>(e.getKeyValueValues()) : new HashMap<>())
|
||||
.campaignId(e.getCampaignId().toString())
|
||||
.order(e.getOrder())
|
||||
.createdAt(e.getCreatedAt())
|
||||
@@ -73,6 +74,7 @@ public class PostgresCharacterRepository implements CharacterRepository {
|
||||
.headerImageId(c.getHeaderImageId())
|
||||
.values(c.getValues() != null ? new HashMap<>(c.getValues()) : new HashMap<>())
|
||||
.imageValues(c.getImageValues() != null ? new HashMap<>(c.getImageValues()) : new HashMap<>())
|
||||
.keyValueValues(c.getKeyValueValues() != null ? new HashMap<>(c.getKeyValueValues()) : new HashMap<>())
|
||||
.campaignId(Long.parseLong(c.getCampaignId()))
|
||||
.order(c.getOrder())
|
||||
.createdAt(c.getCreatedAt())
|
||||
|
||||
@@ -57,6 +57,7 @@ public class PostgresNpcRepository implements NpcRepository {
|
||||
.headerImageId(e.getHeaderImageId())
|
||||
.values(e.getValues() != null ? new HashMap<>(e.getValues()) : new HashMap<>())
|
||||
.imageValues(e.getImageValues() != null ? new HashMap<>(e.getImageValues()) : new HashMap<>())
|
||||
.keyValueValues(e.getKeyValueValues() != null ? new HashMap<>(e.getKeyValueValues()) : new HashMap<>())
|
||||
.campaignId(e.getCampaignId().toString())
|
||||
.order(e.getOrder())
|
||||
.createdAt(e.getCreatedAt())
|
||||
@@ -73,6 +74,7 @@ public class PostgresNpcRepository implements NpcRepository {
|
||||
.headerImageId(n.getHeaderImageId())
|
||||
.values(n.getValues() != null ? new HashMap<>(n.getValues()) : new HashMap<>())
|
||||
.imageValues(n.getImageValues() != null ? new HashMap<>(n.getImageValues()) : new HashMap<>())
|
||||
.keyValueValues(n.getKeyValueValues() != null ? new HashMap<>(n.getKeyValueValues()) : new HashMap<>())
|
||||
.campaignId(Long.parseLong(n.getCampaignId()))
|
||||
.order(n.getOrder())
|
||||
.createdAt(n.getCreatedAt())
|
||||
|
||||
@@ -62,6 +62,7 @@ public class CharacterController {
|
||||
dto.getHeaderImageId(),
|
||||
dto.getValues(),
|
||||
dto.getImageValues(),
|
||||
dto.getKeyValueValues(),
|
||||
dto.getCampaignId(),
|
||||
order
|
||||
);
|
||||
|
||||
@@ -62,6 +62,7 @@ public class NpcController {
|
||||
dto.getHeaderImageId(),
|
||||
dto.getValues(),
|
||||
dto.getImageValues(),
|
||||
dto.getKeyValueValues(),
|
||||
dto.getCampaignId(),
|
||||
order
|
||||
);
|
||||
|
||||
@@ -21,6 +21,7 @@ public class CharacterDTO {
|
||||
private String headerImageId;
|
||||
private Map<String, String> values = new HashMap<>();
|
||||
private Map<String, List<String>> imageValues = new HashMap<>();
|
||||
private Map<String, Map<String, String>> keyValueValues = new HashMap<>();
|
||||
private String campaignId;
|
||||
private int order;
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ public class NpcDTO {
|
||||
private String headerImageId;
|
||||
private Map<String, String> values = new HashMap<>();
|
||||
private Map<String, List<String>> imageValues = new HashMap<>();
|
||||
private Map<String, Map<String, String>> keyValueValues = new HashMap<>();
|
||||
private String campaignId;
|
||||
private int order;
|
||||
}
|
||||
|
||||
@@ -4,6 +4,8 @@ import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* DTO pour un champ de Template.
|
||||
* <p>
|
||||
@@ -17,13 +19,20 @@ import lombok.NoArgsConstructor;
|
||||
@AllArgsConstructor
|
||||
public class TemplateFieldDTO {
|
||||
private String name;
|
||||
/** "TEXT" ou "IMAGE" (string pour serialisation JSON transparente). */
|
||||
/** "TEXT" | "IMAGE" | "NUMBER" | "KEY_VALUE_LIST". */
|
||||
private String type;
|
||||
/** "GALLERY" | "HERO" | "MASONRY" | "CAROUSEL", null si type=TEXT. */
|
||||
/** "GALLERY" | "HERO" | "MASONRY" | "CAROUSEL", uniquement pour IMAGE. */
|
||||
private String layout;
|
||||
/** Labels predefinis pour KEY_VALUE_LIST (ordre significatif). */
|
||||
private List<String> labels;
|
||||
|
||||
/** Retrocompat : constructeur sans layout. */
|
||||
/** Retrocompat : constructeur sans labels. */
|
||||
public TemplateFieldDTO(String name, String type, String layout) {
|
||||
this(name, type, layout, null);
|
||||
}
|
||||
|
||||
/** Retrocompat : constructeur sans layout ni labels. */
|
||||
public TemplateFieldDTO(String name, String type) {
|
||||
this(name, type, null);
|
||||
this(name, type, null, null);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ public class CharacterMapper {
|
||||
dto.setHeaderImageId(c.getHeaderImageId());
|
||||
dto.setValues(c.getValues() != null ? new HashMap<>(c.getValues()) : new HashMap<>());
|
||||
dto.setImageValues(c.getImageValues() != null ? new HashMap<>(c.getImageValues()) : new HashMap<>());
|
||||
dto.setKeyValueValues(c.getKeyValueValues() != null ? new HashMap<>(c.getKeyValueValues()) : new HashMap<>());
|
||||
dto.setCampaignId(c.getCampaignId());
|
||||
dto.setOrder(c.getOrder());
|
||||
return dto;
|
||||
@@ -32,6 +33,7 @@ public class CharacterMapper {
|
||||
.headerImageId(dto.getHeaderImageId())
|
||||
.values(dto.getValues() != null ? new HashMap<>(dto.getValues()) : new HashMap<>())
|
||||
.imageValues(dto.getImageValues() != null ? new HashMap<>(dto.getImageValues()) : new HashMap<>())
|
||||
.keyValueValues(dto.getKeyValueValues() != null ? new HashMap<>(dto.getKeyValueValues()) : new HashMap<>())
|
||||
.campaignId(dto.getCampaignId())
|
||||
.order(dto.getOrder())
|
||||
.build();
|
||||
|
||||
@@ -18,6 +18,7 @@ public class NpcMapper {
|
||||
dto.setHeaderImageId(n.getHeaderImageId());
|
||||
dto.setValues(n.getValues() != null ? new HashMap<>(n.getValues()) : new HashMap<>());
|
||||
dto.setImageValues(n.getImageValues() != null ? new HashMap<>(n.getImageValues()) : new HashMap<>());
|
||||
dto.setKeyValueValues(n.getKeyValueValues() != null ? new HashMap<>(n.getKeyValueValues()) : new HashMap<>());
|
||||
dto.setCampaignId(n.getCampaignId());
|
||||
dto.setOrder(n.getOrder());
|
||||
return dto;
|
||||
@@ -32,6 +33,7 @@ public class NpcMapper {
|
||||
.headerImageId(dto.getHeaderImageId())
|
||||
.values(dto.getValues() != null ? new HashMap<>(dto.getValues()) : new HashMap<>())
|
||||
.imageValues(dto.getImageValues() != null ? new HashMap<>(dto.getImageValues()) : new HashMap<>())
|
||||
.keyValueValues(dto.getKeyValueValues() != null ? new HashMap<>(dto.getKeyValueValues()) : new HashMap<>())
|
||||
.campaignId(dto.getCampaignId())
|
||||
.order(dto.getOrder())
|
||||
.build();
|
||||
|
||||
@@ -6,14 +6,16 @@ import com.loremind.domain.shared.template.TemplateField;
|
||||
import com.loremind.infrastructure.web.dto.shared.TemplateFieldDTO;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Mapper pour convertir entre {@link TemplateField} (domaine) et
|
||||
* {@link TemplateFieldDTO} (wire).
|
||||
* <p>
|
||||
* Tolerance : un type inconnu recu du client est interprete comme TEXT
|
||||
* (plus safe que de rejeter la requete et d'interrompre la sauvegarde).
|
||||
* Tolerance : un type inconnu recu du client est interprete comme TEXT.
|
||||
* Un layout inconnu ou absent sur un champ IMAGE est interprete comme GALLERY.
|
||||
* Le layout est force a null pour les champs TEXT.
|
||||
* Layout/labels forces a null pour les types qui ne les utilisent pas.
|
||||
*/
|
||||
@Component
|
||||
public class TemplateFieldMapper {
|
||||
@@ -26,7 +28,11 @@ public class TemplateFieldMapper {
|
||||
ImageLayout layout = field.getLayout() != null ? field.getLayout() : ImageLayout.GALLERY;
|
||||
layoutStr = layout.name();
|
||||
}
|
||||
return new TemplateFieldDTO(field.getName(), typeStr, layoutStr);
|
||||
List<String> labels = null;
|
||||
if (field.getType() == FieldType.KEY_VALUE_LIST && field.getLabels() != null) {
|
||||
labels = new ArrayList<>(field.getLabels());
|
||||
}
|
||||
return new TemplateFieldDTO(field.getName(), typeStr, layoutStr, labels);
|
||||
}
|
||||
|
||||
public TemplateField toDomain(TemplateFieldDTO dto) {
|
||||
@@ -47,6 +53,10 @@ public class TemplateFieldMapper {
|
||||
layout = ImageLayout.GALLERY;
|
||||
}
|
||||
}
|
||||
return new TemplateField(dto.getName(), type, layout);
|
||||
List<String> labels = null;
|
||||
if (type == FieldType.KEY_VALUE_LIST && dto.getLabels() != null) {
|
||||
labels = new ArrayList<>(dto.getLabels());
|
||||
}
|
||||
return new TemplateField(dto.getName(), type, layout, labels);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,7 +51,7 @@ public class NpcServiceTest {
|
||||
|
||||
Npc result = npcService.createNpc(
|
||||
new NpcService.NpcData("Borin le forgeron", null, null,
|
||||
Map.of("Notes", "Borin"), null, "camp-1", 5));
|
||||
Map.of("Notes", "Borin"), null, null, "camp-1", 5));
|
||||
|
||||
assertNotNull(result);
|
||||
ArgumentCaptor<Npc> captor = ArgumentCaptor.forClass(Npc.class);
|
||||
@@ -67,7 +67,7 @@ public class NpcServiceTest {
|
||||
when(npcRepository.findByCampaignId("camp-1")).thenReturn(List.of(a, b));
|
||||
when(npcRepository.save(any(Npc.class))).thenReturn(testNpc);
|
||||
|
||||
npcService.createNpc(new NpcService.NpcData("Nouveau", null, null, null, null, "camp-1", null));
|
||||
npcService.createNpc(new NpcService.NpcData("Nouveau", null, null, null, null, null, "camp-1", null));
|
||||
|
||||
ArgumentCaptor<Npc> captor = ArgumentCaptor.forClass(Npc.class);
|
||||
verify(npcRepository).save(captor.capture());
|
||||
@@ -79,7 +79,7 @@ public class NpcServiceTest {
|
||||
when(npcRepository.findByCampaignId("camp-1")).thenReturn(List.of());
|
||||
when(npcRepository.save(any(Npc.class))).thenReturn(testNpc);
|
||||
|
||||
npcService.createNpc(new NpcService.NpcData("Premier", null, null, null, null, "camp-1", null));
|
||||
npcService.createNpc(new NpcService.NpcData("Premier", null, null, null, null, null, "camp-1", null));
|
||||
|
||||
ArgumentCaptor<Npc> captor = ArgumentCaptor.forClass(Npc.class);
|
||||
verify(npcRepository).save(captor.capture());
|
||||
@@ -124,7 +124,7 @@ public class NpcServiceTest {
|
||||
|
||||
Npc result = npcService.updateNpc("npc-1",
|
||||
new NpcService.NpcData("Borin renommé", null, null,
|
||||
Map.of("Notes", "v2"), null, "camp-1", 7));
|
||||
Map.of("Notes", "v2"), null, null, "camp-1", 7));
|
||||
|
||||
assertEquals("Borin renommé", result.getName());
|
||||
assertEquals("v2", result.getValues().get("Notes"));
|
||||
@@ -138,7 +138,7 @@ public class NpcServiceTest {
|
||||
|
||||
Npc result = npcService.updateNpc("npc-1",
|
||||
new NpcService.NpcData("Borin", null, null,
|
||||
Map.of("Notes", "txt"), null, "camp-1", null));
|
||||
Map.of("Notes", "txt"), null, null, "camp-1", null));
|
||||
|
||||
// testNpc avait order=1 → préservé
|
||||
assertEquals(1, result.getOrder());
|
||||
@@ -150,7 +150,7 @@ public class NpcServiceTest {
|
||||
|
||||
IllegalArgumentException ex = assertThrows(IllegalArgumentException.class,
|
||||
() -> npcService.updateNpc("missing",
|
||||
new NpcService.NpcData("x", null, null, null, null, "camp-1", null)));
|
||||
new NpcService.NpcData("x", null, null, null, null, null, "camp-1", null)));
|
||||
assertTrue(ex.getMessage().contains("missing"));
|
||||
verify(npcRepository, never()).save(any());
|
||||
}
|
||||
|
||||
@@ -40,7 +40,7 @@
|
||||
Auteur : ietm64
|
||||
Licence : AGPL-3.0
|
||||
Projet : LoreMindMJ - assistant pour Maitres de Jeu de JDR
|
||||
Version : 0.8.1
|
||||
Version : 0.8.3
|
||||
|
||||
.LINK
|
||||
https://github.com/IGMLcreation/LoreMind
|
||||
|
||||
4
web/package-lock.json
generated
4
web/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "loremind-web",
|
||||
"version": "0.8.1",
|
||||
"version": "0.8.3",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "loremind-web",
|
||||
"version": "0.8.1",
|
||||
"version": "0.8.3",
|
||||
"dependencies": {
|
||||
"@angular/animations": "^17.0.0",
|
||||
"@angular/common": "^17.0.0",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "loremind-web",
|
||||
"version": "0.8.1",
|
||||
"version": "0.8.3",
|
||||
"description": "LoreMind Frontend - Angular",
|
||||
"scripts": {
|
||||
"ng": "ng",
|
||||
|
||||
@@ -61,8 +61,10 @@
|
||||
[fields]="templateFields"
|
||||
[values]="values"
|
||||
[imageValues]="imageValues"
|
||||
[keyValueValues]="keyValueValues"
|
||||
(valuesChange)="values = $event"
|
||||
(imageValuesChange)="imageValues = $event">
|
||||
(imageValuesChange)="imageValues = $event"
|
||||
(keyValueValuesChange)="keyValueValues = $event">
|
||||
</app-dynamic-fields-form>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -53,6 +53,7 @@ export class CharacterEditComponent implements OnInit {
|
||||
headerImageId: string | null = null;
|
||||
values: Record<string, string> = {};
|
||||
imageValues: Record<string, string[]> = {};
|
||||
keyValueValues: Record<string, Record<string, string>> = {};
|
||||
templateFields: TemplateField[] = [];
|
||||
private order = 0;
|
||||
|
||||
@@ -81,6 +82,7 @@ export class CharacterEditComponent implements OnInit {
|
||||
this.headerImageId = c.headerImageId ?? null;
|
||||
this.values = c.values ?? {};
|
||||
this.imageValues = c.imageValues ?? {};
|
||||
this.keyValueValues = c.keyValueValues ?? {};
|
||||
this.order = c.order ?? 0;
|
||||
},
|
||||
error: () => this.back()
|
||||
@@ -112,6 +114,7 @@ export class CharacterEditComponent implements OnInit {
|
||||
headerImageId: this.headerImageId,
|
||||
values: this.values,
|
||||
imageValues: this.imageValues,
|
||||
keyValueValues: this.keyValueValues,
|
||||
campaignId: this.campaignId
|
||||
};
|
||||
const req = this.characterId
|
||||
|
||||
@@ -61,8 +61,10 @@
|
||||
[fields]="templateFields"
|
||||
[values]="values"
|
||||
[imageValues]="imageValues"
|
||||
[keyValueValues]="keyValueValues"
|
||||
(valuesChange)="values = $event"
|
||||
(imageValuesChange)="imageValues = $event">
|
||||
(imageValuesChange)="imageValues = $event"
|
||||
(keyValueValuesChange)="keyValueValues = $event">
|
||||
</app-dynamic-fields-form>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -48,6 +48,7 @@ export class NpcEditComponent implements OnInit {
|
||||
headerImageId: string | null = null;
|
||||
values: Record<string, string> = {};
|
||||
imageValues: Record<string, string[]> = {};
|
||||
keyValueValues: Record<string, Record<string, string>> = {};
|
||||
templateFields: TemplateField[] = [];
|
||||
private order = 0;
|
||||
|
||||
@@ -76,6 +77,7 @@ export class NpcEditComponent implements OnInit {
|
||||
this.headerImageId = n.headerImageId ?? null;
|
||||
this.values = n.values ?? {};
|
||||
this.imageValues = n.imageValues ?? {};
|
||||
this.keyValueValues = n.keyValueValues ?? {};
|
||||
this.order = n.order ?? 0;
|
||||
},
|
||||
error: () => this.back()
|
||||
@@ -107,6 +109,7 @@ export class NpcEditComponent implements OnInit {
|
||||
headerImageId: this.headerImageId,
|
||||
values: this.values,
|
||||
imageValues: this.imageValues,
|
||||
keyValueValues: this.keyValueValues,
|
||||
campaignId: this.campaignId
|
||||
};
|
||||
const req = this.npcId
|
||||
|
||||
@@ -13,6 +13,8 @@ export interface Character {
|
||||
headerImageId?: string | null;
|
||||
values?: Record<string, string>;
|
||||
imageValues?: Record<string, string[]>;
|
||||
/** Champs KEY_VALUE_LIST : fieldName -> label -> value. */
|
||||
keyValueValues?: Record<string, Record<string, string>>;
|
||||
campaignId: string;
|
||||
order?: number;
|
||||
}
|
||||
@@ -23,5 +25,6 @@ export interface CharacterCreate {
|
||||
headerImageId?: string | null;
|
||||
values?: Record<string, string>;
|
||||
imageValues?: Record<string, string[]>;
|
||||
keyValueValues?: Record<string, Record<string, string>>;
|
||||
campaignId: string;
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ export interface Npc {
|
||||
headerImageId?: string | null;
|
||||
values?: Record<string, string>;
|
||||
imageValues?: Record<string, string[]>;
|
||||
keyValueValues?: Record<string, Record<string, string>>;
|
||||
campaignId: string;
|
||||
order?: number;
|
||||
}
|
||||
@@ -19,5 +20,6 @@ export interface NpcCreate {
|
||||
headerImageId?: string | null;
|
||||
values?: Record<string, string>;
|
||||
imageValues?: Record<string, string[]>;
|
||||
keyValueValues?: Record<string, Record<string, string>>;
|
||||
campaignId: string;
|
||||
}
|
||||
|
||||
@@ -5,8 +5,9 @@
|
||||
* - 'TEXT' : champ textuel libre (rendu en textarea)
|
||||
* - 'IMAGE' : galerie d'images (rendu en app-image-gallery)
|
||||
* - 'NUMBER' : valeur numerique (rendu en input number)
|
||||
* - 'KEY_VALUE_LIST' : liste de paires {label, value} avec labels figes au template
|
||||
*/
|
||||
export type FieldType = 'TEXT' | 'IMAGE' | 'NUMBER';
|
||||
export type FieldType = 'TEXT' | 'IMAGE' | 'NUMBER' | 'KEY_VALUE_LIST';
|
||||
|
||||
/**
|
||||
* Variante de rendu pour un champ IMAGE. Miroir de
|
||||
@@ -27,6 +28,8 @@ export interface TemplateField {
|
||||
type: FieldType;
|
||||
/** Uniquement pour type='IMAGE'. Absent/null = 'GALLERY'. */
|
||||
layout?: ImageLayout | null;
|
||||
/** Labels predefinis pour KEY_VALUE_LIST (ordre significatif). */
|
||||
labels?: string[] | null;
|
||||
}
|
||||
|
||||
export interface Template {
|
||||
|
||||
@@ -28,6 +28,23 @@
|
||||
[imageIds]="imagesFor(f)"
|
||||
(imageIdsChange)="onImageIdsChange(f, $event)">
|
||||
</app-image-gallery>
|
||||
|
||||
<!-- KEY_VALUE_LIST : grille d'inputs avec labels figes du template -->
|
||||
<div *ngSwitchCase="'KEY_VALUE_LIST'" class="dff-kv-grid">
|
||||
<div class="dff-kv-cell" *ngFor="let lbl of f.labels; trackBy: trackByLabel">
|
||||
<span class="dff-kv-label">{{ lbl }}</span>
|
||||
<input
|
||||
type="text"
|
||||
[ngModel]="kvValue(f, lbl)"
|
||||
(ngModelChange)="onKvChange(f, lbl, $event)"
|
||||
[name]="'kv-' + f.name + '-' + lbl"
|
||||
placeholder="—"
|
||||
/>
|
||||
</div>
|
||||
<div *ngIf="!f.labels?.length" class="dff-kv-empty">
|
||||
Aucun label defini dans le template pour ce champ.
|
||||
</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -40,6 +40,50 @@
|
||||
}
|
||||
}
|
||||
|
||||
.dff-kv-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(110px, 1fr));
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.dff-kv-cell {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 3px;
|
||||
padding: 8px 6px;
|
||||
background: rgba(255, 255, 255, 0.03);
|
||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||
border-radius: 4px;
|
||||
|
||||
.dff-kv-label {
|
||||
font-size: 0.7rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.08em;
|
||||
color: var(--color-text-muted, #9ca3af);
|
||||
}
|
||||
|
||||
input {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
padding: 4px 6px;
|
||||
background: rgba(0, 0, 0, 0.25);
|
||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||
border-radius: 3px;
|
||||
color: var(--color-text, #fff);
|
||||
font-family: 'Cinzel', 'EB Garamond', Georgia, serif;
|
||||
font-weight: 700;
|
||||
font-size: 1.05rem;
|
||||
}
|
||||
}
|
||||
|
||||
.dff-kv-empty {
|
||||
padding: 8px;
|
||||
font-size: 0.8rem;
|
||||
color: var(--color-text-muted, #888);
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.dff-empty {
|
||||
padding: 24px;
|
||||
text-align: center;
|
||||
|
||||
@@ -26,9 +26,11 @@ export class DynamicFieldsFormComponent {
|
||||
@Input() fields: TemplateField[] = [];
|
||||
@Input() values: Record<string, string> = {};
|
||||
@Input() imageValues: Record<string, string[]> = {};
|
||||
@Input() keyValueValues: Record<string, Record<string, string>> = {};
|
||||
|
||||
@Output() valuesChange = new EventEmitter<Record<string, string>>();
|
||||
@Output() imageValuesChange = new EventEmitter<Record<string, string[]>>();
|
||||
@Output() keyValueValuesChange = new EventEmitter<Record<string, Record<string, string>>>();
|
||||
|
||||
onTextChange(field: TemplateField, value: string): void {
|
||||
this.values = { ...this.values, [field.name]: value };
|
||||
@@ -44,5 +46,18 @@ export class DynamicFieldsFormComponent {
|
||||
return this.imageValues[field.name] ?? [];
|
||||
}
|
||||
|
||||
trackByName = (_: number, f: TemplateField) => f.name;
|
||||
/** Valeur d'un label particulier dans un champ KEY_VALUE_LIST. */
|
||||
kvValue(field: TemplateField, label: string): string {
|
||||
return this.keyValueValues?.[field.name]?.[label] ?? '';
|
||||
}
|
||||
|
||||
/** Met a jour la valeur d'un label dans un champ KEY_VALUE_LIST. */
|
||||
onKvChange(field: TemplateField, label: string, value: string): void {
|
||||
const inner = { ...(this.keyValueValues[field.name] ?? {}), [label]: value };
|
||||
this.keyValueValues = { ...this.keyValueValues, [field.name]: inner };
|
||||
this.keyValueValuesChange.emit(this.keyValueValues);
|
||||
}
|
||||
|
||||
trackByName = (_: number, f: TemplateField) => f.name;
|
||||
trackByLabel = (_: number, l: string) => l;
|
||||
}
|
||||
|
||||
@@ -15,44 +15,92 @@
|
||||
<div class="pv-title-block">
|
||||
<h1 class="pv-name">{{ persona.name }}</h1>
|
||||
<p *ngIf="subtitle" class="pv-subtitle">{{ subtitle }}</p>
|
||||
|
||||
<!-- Badges des NUMBER isoles (rendu compact, evite la grosse card pour 1 valeur) -->
|
||||
<div *ngIf="heroBadges.length" class="pv-hero-badges">
|
||||
<span *ngFor="let b of heroBadges" class="pv-hero-badge">
|
||||
<span class="pv-hero-badge-label">{{ b.label }}</span>
|
||||
<span class="pv-hero-badge-value">{{ b.value }}</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Stats numeriques en bandeau si presentes -->
|
||||
<div *ngIf="textFields.length && hasAnyNumber(textFields)" class="pv-stat-band">
|
||||
<div *ngFor="let f of textFields" class="pv-stat" [class.pv-stat-number]="f.isNumber">
|
||||
<ng-container *ngIf="f.isNumber">
|
||||
<span class="pv-stat-label">{{ f.name }}</span>
|
||||
<span class="pv-stat-value">{{ f.value }}</span>
|
||||
</ng-container>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Sections texte -->
|
||||
<!-- Sections rendues dans l'ordre du template -->
|
||||
<div class="pv-sections">
|
||||
<ng-container *ngFor="let f of textFields; let first = first">
|
||||
<section *ngIf="!f.isNumber" class="pv-section">
|
||||
<h2 class="pv-section-title">{{ f.name }}</h2>
|
||||
<ng-container *ngFor="let s of orderedSections">
|
||||
|
||||
<!-- TEXT -->
|
||||
<section *ngIf="s.kind === 'TEXT'" class="pv-section">
|
||||
<h2 class="pv-section-title">{{ s.name }}</h2>
|
||||
<div class="pv-section-body">
|
||||
<p [class.with-dropcap]="first" class="pv-paragraph">
|
||||
{{ firstParagraph(f.value) }}
|
||||
<p [class.with-dropcap]="s.name === firstTextSectionName" class="pv-paragraph">
|
||||
{{ firstParagraph(s.value) }}
|
||||
</p>
|
||||
<p *ngIf="restAfterFirstParagraph(f.value)" class="pv-paragraph">
|
||||
{{ restAfterFirstParagraph(f.value) }}
|
||||
<p *ngIf="restAfterFirstParagraph(s.value)" class="pv-paragraph">
|
||||
{{ restAfterFirstParagraph(s.value) }}
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
</ng-container>
|
||||
|
||||
<!-- Galeries d'images templates -->
|
||||
<section *ngFor="let img of imageFields" class="pv-section pv-section-images">
|
||||
<h2 class="pv-section-title">{{ img.field.name }}</h2>
|
||||
<app-image-gallery [imageIds]="img.ids" [layout]="img.field.layout || 'GALLERY'" [editable]="false">
|
||||
<!-- NUMBER_GROUP : NUMBER consecutifs - table si <=6, sinon liste 2 cols -->
|
||||
<section *ngIf="s.kind === 'NUMBER_GROUP'" class="pv-section pv-section-number">
|
||||
<ng-container *ngIf="s.entries.length <= 6; else numGroupList">
|
||||
<div class="pv-kv-table" [style.--cols]="s.entries.length">
|
||||
<div class="pv-kv-row pv-kv-row-labels">
|
||||
<div class="pv-kv-cell" *ngFor="let e of s.entries">{{ e.label }}</div>
|
||||
</div>
|
||||
<div class="pv-kv-row pv-kv-row-values">
|
||||
<div class="pv-kv-cell" *ngFor="let e of s.entries">{{ e.value }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
<ng-template #numGroupList>
|
||||
<div class="pv-kv-list">
|
||||
<div class="pv-kv-list-row" *ngFor="let e of s.entries">
|
||||
<span class="pv-kv-list-label">{{ e.label }}</span>
|
||||
<span class="pv-kv-list-dots"></span>
|
||||
<span class="pv-kv-list-value">{{ e.value }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</ng-template>
|
||||
</section>
|
||||
|
||||
<!-- KEY_VALUE_LIST : table style Foundry si <=6, sinon liste 2 cols (skills) -->
|
||||
<section *ngIf="s.kind === 'KEY_VALUE_LIST'" class="pv-section pv-section-kv">
|
||||
<h2 class="pv-section-title">{{ s.name }}</h2>
|
||||
<ng-container *ngIf="s.entries.length <= 6; else kvList">
|
||||
<div class="pv-kv-table" [style.--cols]="s.entries.length">
|
||||
<div class="pv-kv-row pv-kv-row-labels">
|
||||
<div class="pv-kv-cell" *ngFor="let e of s.entries">{{ e.label }}</div>
|
||||
</div>
|
||||
<div class="pv-kv-row pv-kv-row-values">
|
||||
<div class="pv-kv-cell" *ngFor="let e of s.entries">{{ e.value || '—' }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
<ng-template #kvList>
|
||||
<div class="pv-kv-list">
|
||||
<div class="pv-kv-list-row" *ngFor="let e of s.entries">
|
||||
<span class="pv-kv-list-label">{{ e.label }}</span>
|
||||
<span class="pv-kv-list-dots"></span>
|
||||
<span class="pv-kv-list-value">{{ e.value || '—' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</ng-template>
|
||||
</section>
|
||||
|
||||
<!-- IMAGE : galerie -->
|
||||
<section *ngIf="s.kind === 'IMAGE'" class="pv-section pv-section-images">
|
||||
<h2 class="pv-section-title">{{ s.name }}</h2>
|
||||
<app-image-gallery [imageIds]="s.ids" [layout]="s.layout" [editable]="false">
|
||||
</app-image-gallery>
|
||||
</section>
|
||||
|
||||
</ng-container>
|
||||
|
||||
<!-- Etat vide -->
|
||||
<div *ngIf="textFields.length === 0 && imageFields.length === 0" class="pv-empty">
|
||||
<div *ngIf="orderedSections.length === 0" class="pv-empty">
|
||||
<lucide-icon [img]="BookOpen" [size]="32"></lucide-icon>
|
||||
<p>Cette fiche est encore vide.</p>
|
||||
</div>
|
||||
|
||||
@@ -92,41 +92,145 @@
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
// --- Bandeau de stats (NUMBER) ---------------------------------------------
|
||||
|
||||
.pv-stat-band {
|
||||
// Badges compacts pour les NUMBER isoles (Niveau, etc.) — evite la grosse card.
|
||||
.pv-hero-badges {
|
||||
margin-top: 12px;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 16px;
|
||||
padding: 12px 32px;
|
||||
margin: 16px 32px 0;
|
||||
background: rgba(255, 255, 255, 0.04);
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.08);
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.pv-stat {
|
||||
display: none;
|
||||
.pv-hero-badge {
|
||||
display: inline-flex;
|
||||
align-items: baseline;
|
||||
gap: 6px;
|
||||
padding: 4px 12px;
|
||||
background: rgba(209, 168, 120, 0.08);
|
||||
border: 1px solid rgba(209, 168, 120, 0.3);
|
||||
border-radius: 100px;
|
||||
|
||||
&.pv-stat-number {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
min-width: 60px;
|
||||
}
|
||||
|
||||
.pv-stat-label {
|
||||
.pv-hero-badge-label {
|
||||
font-size: 0.7rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.08em;
|
||||
letter-spacing: 0.1em;
|
||||
color: #9ca3af;
|
||||
}
|
||||
|
||||
.pv-stat-value {
|
||||
font-size: 1.25rem;
|
||||
.pv-hero-badge-value {
|
||||
font-family: 'Cinzel', 'EB Garamond', Georgia, serif;
|
||||
font-weight: 700;
|
||||
color: #f3f4f6;
|
||||
font-family: 'Cinzel', Georgia, serif;
|
||||
font-size: 1.05rem;
|
||||
color: #d1a878;
|
||||
}
|
||||
}
|
||||
|
||||
// --- Table compacte (NUMBER_GROUP / KEY_VALUE_LIST) ------------------------
|
||||
// Style Foundry : 2 rangees (labels uppercase / valeurs serif), separateurs
|
||||
// fins, sans gros ornements. --cols est la variable CSS pour le nombre
|
||||
// d'entrees, posee inline dans le HTML.
|
||||
|
||||
.pv-kv-table {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
background: rgba(0, 0, 0, 0.18);
|
||||
}
|
||||
|
||||
.pv-kv-row {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(var(--cols, 4), minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.pv-kv-row-labels {
|
||||
background: rgba(255, 255, 255, 0.03);
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
|
||||
|
||||
.pv-kv-cell {
|
||||
font-size: 0.7rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.1em;
|
||||
color: #9ca3af;
|
||||
padding: 6px 4px;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
.pv-kv-row-values {
|
||||
.pv-kv-cell {
|
||||
font-family: 'Cinzel', 'EB Garamond', Georgia, serif;
|
||||
font-size: 1.15rem;
|
||||
font-weight: 700;
|
||||
color: #d1a878;
|
||||
padding: 8px 4px;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
.pv-kv-cell {
|
||||
border-right: 1px solid rgba(255, 255, 255, 0.04);
|
||||
|
||||
&:last-child {
|
||||
border-right: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
// Sur mobile : si plus de 4 colonnes, on retombe en wrap (sacrifice de la
|
||||
// structure tableau pour eviter le scroll horizontal sur les stats blocks).
|
||||
.pv-kv-row {
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
// --- Liste 2 colonnes (KV avec >6 entrees, type "skills" Foundry) ---------
|
||||
|
||||
.pv-kv-list {
|
||||
column-count: 2;
|
||||
column-gap: 32px;
|
||||
padding: 8px 12px;
|
||||
background: rgba(0, 0, 0, 0.18);
|
||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.pv-kv-list-row {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 6px;
|
||||
padding: 4px 0;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.04);
|
||||
break-inside: avoid; // empeche un row de se couper entre 2 colonnes
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.pv-kv-list-label {
|
||||
font-size: 0.85rem;
|
||||
color: #d6d8de;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
// Pointilles entre label et valeur (style fiche papier)
|
||||
.pv-kv-list-dots {
|
||||
flex: 1;
|
||||
border-bottom: 1px dotted rgba(255, 255, 255, 0.12);
|
||||
transform: translateY(-3px);
|
||||
}
|
||||
|
||||
.pv-kv-list-value {
|
||||
font-family: 'Cinzel', 'EB Garamond', Georgia, serif;
|
||||
font-weight: 700;
|
||||
font-size: 1rem;
|
||||
color: #d1a878;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.pv-kv-list {
|
||||
column-count: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -144,6 +248,13 @@
|
||||
}
|
||||
}
|
||||
|
||||
.pv-section-kv,
|
||||
.pv-section-number {
|
||||
.pv-kv-table {
|
||||
margin-top: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.pv-section-title {
|
||||
font-family: 'Cinzel', 'EB Garamond', Georgia, serif;
|
||||
font-size: 1.4rem;
|
||||
|
||||
@@ -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). */
|
||||
|
||||
@@ -5,7 +5,8 @@
|
||||
</div>
|
||||
|
||||
<div class="tfe-list">
|
||||
<div class="tfe-row" *ngFor="let f of fields; let i = index" [class.invalid]="isDuplicate(f, i) || !f.name.trim()">
|
||||
<div class="tfe-item" *ngFor="let f of fields; let i = index">
|
||||
<div class="tfe-row" [class.invalid]="isDuplicate(f, i) || !f.name.trim()">
|
||||
<div class="tfe-row-controls">
|
||||
<button type="button" class="btn-arrow" (click)="moveUp(i)" [disabled]="i === 0" title="Monter">
|
||||
<lucide-icon [img]="ArrowUp" [size]="14"></lucide-icon>
|
||||
@@ -46,6 +47,40 @@
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Sous-editeur des labels pour KEY_VALUE_LIST -->
|
||||
<div class="tfe-labels" *ngIf="f.type === 'KEY_VALUE_LIST'">
|
||||
<div class="tfe-labels-header">
|
||||
<lucide-icon [img]="ListOrdered" [size]="12"></lucide-icon>
|
||||
<span>Labels (cles fixes pour toutes les fiches)</span>
|
||||
</div>
|
||||
<div class="tfe-labels-list">
|
||||
<div class="tfe-label-row" *ngFor="let lbl of f.labels; let li = index; trackBy: trackByIndex">
|
||||
<button type="button" class="btn-arrow-mini" (click)="moveLabelUp(f, li)" [disabled]="li === 0" title="Monter">
|
||||
<lucide-icon [img]="ArrowUp" [size]="11"></lucide-icon>
|
||||
</button>
|
||||
<button type="button" class="btn-arrow-mini" (click)="moveLabelDown(f, li)" [disabled]="li === (f.labels?.length || 0) - 1" title="Descendre">
|
||||
<lucide-icon [img]="ArrowDown" [size]="11"></lucide-icon>
|
||||
</button>
|
||||
<input
|
||||
type="text"
|
||||
class="tfe-label-input"
|
||||
[ngModel]="lbl"
|
||||
(ngModelChange)="updateLabelAt(f, li, $event)"
|
||||
[name]="'lbl-' + i + '-' + li"
|
||||
placeholder="Ex: FOR, DEX..."
|
||||
/>
|
||||
<button type="button" class="btn-remove-mini" (click)="removeLabel(f, li)" title="Retirer ce label">
|
||||
<lucide-icon [img]="X" [size]="11"></lucide-icon>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<button type="button" class="chip chip-mini" (click)="addLabel(f)">
|
||||
<lucide-icon [img]="Plus" [size]="11"></lucide-icon>
|
||||
Ajouter un label
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div *ngIf="fields.length === 0" class="tfe-empty">
|
||||
Aucun champ pour l'instant — ajoutez-en avec les boutons ci-dessous.
|
||||
</div>
|
||||
@@ -75,5 +110,9 @@
|
||||
<lucide-icon [img]="ImageIcon" [size]="12"></lucide-icon>
|
||||
Image(s)
|
||||
</button>
|
||||
<button type="button" class="chip chip-custom" (click)="addBlank('KEY_VALUE_LIST')">
|
||||
<lucide-icon [img]="ListOrdered" [size]="12"></lucide-icon>
|
||||
Liste cle/valeur
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -137,3 +137,88 @@
|
||||
.chip-custom {
|
||||
border-style: dashed;
|
||||
}
|
||||
|
||||
.chip-mini {
|
||||
padding: 3px 8px;
|
||||
font-size: 0.72rem;
|
||||
align-self: flex-start;
|
||||
}
|
||||
|
||||
// --- Sous-editeur labels (KEY_VALUE_LIST) ---
|
||||
|
||||
.tfe-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.tfe-labels {
|
||||
margin-left: 32px;
|
||||
padding: 8px 10px;
|
||||
background: rgba(0, 0, 0, 0.15);
|
||||
border: 1px dashed rgba(255, 255, 255, 0.08);
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.tfe-labels-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 0.75rem;
|
||||
color: var(--color-text-muted, #aaa);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
.tfe-labels-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 3px;
|
||||
}
|
||||
|
||||
.tfe-label-row {
|
||||
display: grid;
|
||||
grid-template-columns: auto auto 1fr auto;
|
||||
gap: 4px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.btn-arrow-mini,
|
||||
.btn-remove-mini {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 2px;
|
||||
background: transparent;
|
||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||
border-radius: 3px;
|
||||
color: var(--color-text-muted, #888);
|
||||
cursor: pointer;
|
||||
transition: all 100ms;
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
background: rgba(255, 255, 255, 0.06);
|
||||
color: var(--color-text, #fff);
|
||||
}
|
||||
&:disabled {
|
||||
opacity: 0.3;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
|
||||
.btn-remove-mini:hover {
|
||||
border-color: rgba(255, 100, 100, 0.4);
|
||||
color: rgba(255, 100, 100, 0.9);
|
||||
}
|
||||
|
||||
.tfe-label-input {
|
||||
padding: 4px 6px;
|
||||
background: rgba(0, 0, 0, 0.25);
|
||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||
border-radius: 3px;
|
||||
color: var(--color-text, #fff);
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Component, EventEmitter, Input, Output } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { LucideAngularModule, Plus, Trash2, ArrowUp, ArrowDown, Type, Image as ImageIcon, Hash } from 'lucide-angular';
|
||||
import { LucideAngularModule, Plus, Trash2, ArrowUp, ArrowDown, Type, Image as ImageIcon, Hash, ListOrdered, X } from 'lucide-angular';
|
||||
import { TemplateField, FieldType, ImageLayout } from '../../services/template.model';
|
||||
|
||||
/**
|
||||
@@ -29,6 +29,8 @@ export class TemplateFieldsEditorComponent {
|
||||
readonly Type = Type;
|
||||
readonly ImageIcon = ImageIcon;
|
||||
readonly Hash = Hash;
|
||||
readonly ListOrdered = ListOrdered;
|
||||
readonly X = X;
|
||||
|
||||
/** Liste des champs (binding parent). */
|
||||
@Input() fields: TemplateField[] = [];
|
||||
@@ -47,7 +49,8 @@ export class TemplateFieldsEditorComponent {
|
||||
readonly typeOptions: { value: FieldType; label: string }[] = [
|
||||
{ value: 'TEXT', label: 'Texte' },
|
||||
{ value: 'NUMBER', label: 'Nombre' },
|
||||
{ value: 'IMAGE', label: 'Image(s)' }
|
||||
{ value: 'IMAGE', label: 'Image(s)' },
|
||||
{ value: 'KEY_VALUE_LIST', label: 'Liste cle/valeur' }
|
||||
];
|
||||
|
||||
readonly layoutOptions: { value: ImageLayout; label: string }[] = [
|
||||
@@ -75,9 +78,53 @@ export class TemplateFieldsEditorComponent {
|
||||
|
||||
addBlank(type: FieldType): void {
|
||||
const layout: ImageLayout | null = type === 'IMAGE' ? 'GALLERY' : null;
|
||||
this.emit([...this.fields, { name: '', type, layout }]);
|
||||
const labels: string[] | null = type === 'KEY_VALUE_LIST' ? [] : null;
|
||||
this.emit([...this.fields, { name: '', type, layout, labels }]);
|
||||
}
|
||||
|
||||
// --- Sous-editeur labels (KEY_VALUE_LIST) ---
|
||||
|
||||
addLabel(field: TemplateField): void {
|
||||
const labels = field.labels ? [...field.labels] : [];
|
||||
labels.push('');
|
||||
field.labels = labels;
|
||||
this.onFieldChanged();
|
||||
}
|
||||
|
||||
removeLabel(field: TemplateField, index: number): void {
|
||||
if (!field.labels) return;
|
||||
const labels = [...field.labels];
|
||||
labels.splice(index, 1);
|
||||
field.labels = labels;
|
||||
this.onFieldChanged();
|
||||
}
|
||||
|
||||
updateLabelAt(field: TemplateField, index: number, value: string): void {
|
||||
if (!field.labels) return;
|
||||
const labels = [...field.labels];
|
||||
labels[index] = value;
|
||||
field.labels = labels;
|
||||
this.onFieldChanged();
|
||||
}
|
||||
|
||||
moveLabelUp(field: TemplateField, index: number): void {
|
||||
if (!field.labels || index <= 0) return;
|
||||
const labels = [...field.labels];
|
||||
[labels[index - 1], labels[index]] = [labels[index], labels[index - 1]];
|
||||
field.labels = labels;
|
||||
this.onFieldChanged();
|
||||
}
|
||||
|
||||
moveLabelDown(field: TemplateField, index: number): void {
|
||||
if (!field.labels || index >= field.labels.length - 1) return;
|
||||
const labels = [...field.labels];
|
||||
[labels[index], labels[index + 1]] = [labels[index + 1], labels[index]];
|
||||
field.labels = labels;
|
||||
this.onFieldChanged();
|
||||
}
|
||||
|
||||
trackByIndex = (i: number) => i;
|
||||
|
||||
remove(index: number): void {
|
||||
const next = [...this.fields];
|
||||
next.splice(index, 1);
|
||||
@@ -100,10 +147,11 @@ export class TemplateFieldsEditorComponent {
|
||||
|
||||
/** Notifie les changements internes (input/select sur un champ existant). */
|
||||
onFieldChanged(): void {
|
||||
// Quand le type passe a IMAGE, layout = GALLERY ; sinon null.
|
||||
for (const f of this.fields) {
|
||||
if (f.type === 'IMAGE' && !f.layout) f.layout = 'GALLERY';
|
||||
if (f.type !== 'IMAGE') f.layout = null;
|
||||
if (f.type === 'KEY_VALUE_LIST' && !f.labels) f.labels = [];
|
||||
if (f.type !== 'KEY_VALUE_LIST') f.labels = null;
|
||||
}
|
||||
this.emit([...this.fields]);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user