Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 86836ad81c |
@@ -41,7 +41,7 @@ from app.infrastructure.onemin_adapter import OneMinAiLLMProvider
|
|||||||
app = FastAPI(
|
app = FastAPI(
|
||||||
title="LoreMind Brain",
|
title="LoreMind Brain",
|
||||||
description="Backend IA pour la génération de contenu narratif.",
|
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>
|
<groupId>com.loremind</groupId>
|
||||||
<artifactId>loremind-core</artifactId>
|
<artifactId>loremind-core</artifactId>
|
||||||
<version>0.8.1</version>
|
<version>0.8.3</version>
|
||||||
<name>LoreMind Core</name>
|
<name>LoreMind Core</name>
|
||||||
<description>Backend Core - Architecture Hexagonale</description>
|
<description>Backend Core - Architecture Hexagonale</description>
|
||||||
|
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ public class CharacterService {
|
|||||||
String headerImageId,
|
String headerImageId,
|
||||||
Map<String, String> values,
|
Map<String, String> values,
|
||||||
Map<String, List<String>> imageValues,
|
Map<String, List<String>> imageValues,
|
||||||
|
Map<String, Map<String, String>> keyValueValues,
|
||||||
String campaignId,
|
String campaignId,
|
||||||
Integer order
|
Integer order
|
||||||
) {}
|
) {}
|
||||||
@@ -46,6 +47,7 @@ public class CharacterService {
|
|||||||
.headerImageId(data.headerImageId())
|
.headerImageId(data.headerImageId())
|
||||||
.values(data.values() != null ? new HashMap<>(data.values()) : new HashMap<>())
|
.values(data.values() != null ? new HashMap<>(data.values()) : new HashMap<>())
|
||||||
.imageValues(data.imageValues() != null ? new HashMap<>(data.imageValues()) : 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())
|
.campaignId(data.campaignId())
|
||||||
.order(order)
|
.order(order)
|
||||||
.build();
|
.build();
|
||||||
@@ -68,6 +70,7 @@ public class CharacterService {
|
|||||||
existing.setHeaderImageId(data.headerImageId());
|
existing.setHeaderImageId(data.headerImageId());
|
||||||
existing.setValues(data.values() != null ? new HashMap<>(data.values()) : new HashMap<>());
|
existing.setValues(data.values() != null ? new HashMap<>(data.values()) : new HashMap<>());
|
||||||
existing.setImageValues(data.imageValues() != null ? new HashMap<>(data.imageValues()) : 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) {
|
if (data.order() != null) {
|
||||||
existing.setOrder(data.order());
|
existing.setOrder(data.order());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ public class NpcService {
|
|||||||
String headerImageId,
|
String headerImageId,
|
||||||
Map<String, String> values,
|
Map<String, String> values,
|
||||||
Map<String, List<String>> imageValues,
|
Map<String, List<String>> imageValues,
|
||||||
|
Map<String, Map<String, String>> keyValueValues,
|
||||||
String campaignId,
|
String campaignId,
|
||||||
Integer order
|
Integer order
|
||||||
) {}
|
) {}
|
||||||
@@ -41,6 +42,7 @@ public class NpcService {
|
|||||||
.headerImageId(data.headerImageId())
|
.headerImageId(data.headerImageId())
|
||||||
.values(data.values() != null ? new HashMap<>(data.values()) : new HashMap<>())
|
.values(data.values() != null ? new HashMap<>(data.values()) : new HashMap<>())
|
||||||
.imageValues(data.imageValues() != null ? new HashMap<>(data.imageValues()) : 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())
|
.campaignId(data.campaignId())
|
||||||
.order(order)
|
.order(order)
|
||||||
.build();
|
.build();
|
||||||
@@ -63,6 +65,7 @@ public class NpcService {
|
|||||||
existing.setHeaderImageId(data.headerImageId());
|
existing.setHeaderImageId(data.headerImageId());
|
||||||
existing.setValues(data.values() != null ? new HashMap<>(data.values()) : new HashMap<>());
|
existing.setValues(data.values() != null ? new HashMap<>(data.values()) : new HashMap<>());
|
||||||
existing.setImageValues(data.imageValues() != null ? new HashMap<>(data.imageValues()) : 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) {
|
if (data.order() != null) {
|
||||||
existing.setOrder(data.order());
|
existing.setOrder(data.order());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -51,6 +51,14 @@ public class Character {
|
|||||||
*/
|
*/
|
||||||
private Map<String, List<String>> imageValues;
|
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. */
|
/** Référence vers la Campaign parente. */
|
||||||
private String campaignId;
|
private String campaignId;
|
||||||
|
|
||||||
@@ -70,4 +78,9 @@ public class Character {
|
|||||||
if (imageValues == null) imageValues = new HashMap<>();
|
if (imageValues == null) imageValues = new HashMap<>();
|
||||||
return imageValues;
|
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. */
|
/** Valeurs IMAGE du template PNJ (listes d'IDs ordonnees par champ). Jamais null. */
|
||||||
private Map<String, List<String>> imageValues;
|
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). */
|
/** Référence vers la Campaign parente (cross-aggregate via ID). */
|
||||||
private String campaignId;
|
private String campaignId;
|
||||||
|
|
||||||
@@ -58,4 +61,9 @@ public class Npc {
|
|||||||
if (imageValues == null) imageValues = new HashMap<>();
|
if (imageValues == null) imageValues = new HashMap<>();
|
||||||
return imageValues;
|
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>)
|
* - TEXT : valeur textuelle libre (Map<String, String>)
|
||||||
* - IMAGE : galerie d'images, liste d'IDs (Map<String, List<String>>)
|
* - IMAGE : galerie d'images, liste d'IDs (Map<String, List<String>>)
|
||||||
* - NUMBER : valeur numerique stockee en texte (parsee a l'usage)
|
* - 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>
|
* <p>
|
||||||
* Extension future possible : RICH_TEXT, DATE, BOOLEAN, KEY_VALUE_LIST, REFERENCE...
|
* Extension future possible : RICH_TEXT, DATE, BOOLEAN, REFERENCE...
|
||||||
*/
|
*/
|
||||||
public enum FieldType {
|
public enum FieldType {
|
||||||
TEXT,
|
TEXT,
|
||||||
IMAGE,
|
IMAGE,
|
||||||
NUMBER
|
NUMBER,
|
||||||
|
KEY_VALUE_LIST
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ import lombok.Builder;
|
|||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
import lombok.NoArgsConstructor;
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Value Object d'un champ de Template (kernel partage).
|
* Value Object d'un champ de Template (kernel partage).
|
||||||
* <p>
|
* <p>
|
||||||
@@ -27,29 +29,45 @@ public class TemplateField {
|
|||||||
private FieldType type;
|
private FieldType type;
|
||||||
/** Variante de rendu pour les champs IMAGE. Null = GALLERY. */
|
/** Variante de rendu pour les champs IMAGE. Null = GALLERY. */
|
||||||
private ImageLayout layout;
|
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) {
|
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). */
|
/** Raccourci : construit un champ de type TEXT (cas le plus courant). */
|
||||||
public static TemplateField text(String name) {
|
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. */
|
/** Raccourci : construit un champ de type IMAGE avec layout GALLERY. */
|
||||||
public static TemplateField image(String name) {
|
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. */
|
/** Raccourci : construit un champ IMAGE avec un layout specifique. */
|
||||||
public static TemplateField image(String name, ImageLayout layout) {
|
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. */
|
/** Raccourci : construit un champ de type NUMBER. */
|
||||||
public static TemplateField number(String name) {
|
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("Niveau"),
|
||||||
TemplateField.number("PV max"),
|
TemplateField.number("PV max"),
|
||||||
TemplateField.number("CA"),
|
TemplateField.number("CA"),
|
||||||
TemplateField.number("FOR"),
|
TemplateField.keyValueList("Caracteristiques",
|
||||||
TemplateField.number("DEX"),
|
List.of("FOR", "DEX", "CON", "INT", "SAG", "CHA")),
|
||||||
TemplateField.number("CON"),
|
|
||||||
TemplateField.number("INT"),
|
|
||||||
TemplateField.number("SAG"),
|
|
||||||
TemplateField.number("CHA"),
|
|
||||||
TemplateField.text("Competences"),
|
TemplateField.text("Competences"),
|
||||||
TemplateField.text("Equipement"),
|
TemplateField.text("Equipement"),
|
||||||
TemplateField.text("Sorts"),
|
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()) {
|
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.
|
// 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.StringListMapJsonConverter;
|
||||||
import com.loremind.infrastructure.persistence.converter.StringMapJsonConverter;
|
import com.loremind.infrastructure.persistence.converter.StringMapJsonConverter;
|
||||||
|
import com.loremind.infrastructure.persistence.converter.StringMapMapJsonConverter;
|
||||||
import jakarta.persistence.*;
|
import jakarta.persistence.*;
|
||||||
import lombok.AllArgsConstructor;
|
import lombok.AllArgsConstructor;
|
||||||
import lombok.Builder;
|
import lombok.Builder;
|
||||||
@@ -53,6 +54,11 @@ public class CharacterJpaEntity {
|
|||||||
@Column(name = "image_values", columnDefinition = "TEXT")
|
@Column(name = "image_values", columnDefinition = "TEXT")
|
||||||
private Map<String, List<String>> imageValues;
|
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)
|
@Column(name = "campaign_id", nullable = false)
|
||||||
private Long campaignId;
|
private Long campaignId;
|
||||||
|
|
||||||
@@ -71,6 +77,7 @@ public class CharacterJpaEntity {
|
|||||||
updatedAt = LocalDateTime.now();
|
updatedAt = LocalDateTime.now();
|
||||||
if (values == null) values = new HashMap<>();
|
if (values == null) values = new HashMap<>();
|
||||||
if (imageValues == null) imageValues = new HashMap<>();
|
if (imageValues == null) imageValues = new HashMap<>();
|
||||||
|
if (keyValueValues == null) keyValueValues = new HashMap<>();
|
||||||
}
|
}
|
||||||
|
|
||||||
@PreUpdate
|
@PreUpdate
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package com.loremind.infrastructure.persistence.entity;
|
|||||||
|
|
||||||
import com.loremind.infrastructure.persistence.converter.StringListMapJsonConverter;
|
import com.loremind.infrastructure.persistence.converter.StringListMapJsonConverter;
|
||||||
import com.loremind.infrastructure.persistence.converter.StringMapJsonConverter;
|
import com.loremind.infrastructure.persistence.converter.StringMapJsonConverter;
|
||||||
|
import com.loremind.infrastructure.persistence.converter.StringMapMapJsonConverter;
|
||||||
import jakarta.persistence.*;
|
import jakarta.persistence.*;
|
||||||
import lombok.AllArgsConstructor;
|
import lombok.AllArgsConstructor;
|
||||||
import lombok.Builder;
|
import lombok.Builder;
|
||||||
@@ -46,6 +47,10 @@ public class NpcJpaEntity {
|
|||||||
@Column(name = "image_values", columnDefinition = "TEXT")
|
@Column(name = "image_values", columnDefinition = "TEXT")
|
||||||
private Map<String, List<String>> imageValues;
|
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)
|
@Column(name = "campaign_id", nullable = false)
|
||||||
private Long campaignId;
|
private Long campaignId;
|
||||||
|
|
||||||
@@ -64,6 +69,7 @@ public class NpcJpaEntity {
|
|||||||
updatedAt = LocalDateTime.now();
|
updatedAt = LocalDateTime.now();
|
||||||
if (values == null) values = new HashMap<>();
|
if (values == null) values = new HashMap<>();
|
||||||
if (imageValues == null) imageValues = new HashMap<>();
|
if (imageValues == null) imageValues = new HashMap<>();
|
||||||
|
if (keyValueValues == null) keyValueValues = new HashMap<>();
|
||||||
}
|
}
|
||||||
|
|
||||||
@PreUpdate
|
@PreUpdate
|
||||||
|
|||||||
@@ -57,6 +57,7 @@ public class PostgresCharacterRepository implements CharacterRepository {
|
|||||||
.headerImageId(e.getHeaderImageId())
|
.headerImageId(e.getHeaderImageId())
|
||||||
.values(e.getValues() != null ? new HashMap<>(e.getValues()) : new HashMap<>())
|
.values(e.getValues() != null ? new HashMap<>(e.getValues()) : new HashMap<>())
|
||||||
.imageValues(e.getImageValues() != null ? new HashMap<>(e.getImageValues()) : 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())
|
.campaignId(e.getCampaignId().toString())
|
||||||
.order(e.getOrder())
|
.order(e.getOrder())
|
||||||
.createdAt(e.getCreatedAt())
|
.createdAt(e.getCreatedAt())
|
||||||
@@ -73,6 +74,7 @@ public class PostgresCharacterRepository implements CharacterRepository {
|
|||||||
.headerImageId(c.getHeaderImageId())
|
.headerImageId(c.getHeaderImageId())
|
||||||
.values(c.getValues() != null ? new HashMap<>(c.getValues()) : new HashMap<>())
|
.values(c.getValues() != null ? new HashMap<>(c.getValues()) : new HashMap<>())
|
||||||
.imageValues(c.getImageValues() != null ? new HashMap<>(c.getImageValues()) : 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()))
|
.campaignId(Long.parseLong(c.getCampaignId()))
|
||||||
.order(c.getOrder())
|
.order(c.getOrder())
|
||||||
.createdAt(c.getCreatedAt())
|
.createdAt(c.getCreatedAt())
|
||||||
|
|||||||
@@ -57,6 +57,7 @@ public class PostgresNpcRepository implements NpcRepository {
|
|||||||
.headerImageId(e.getHeaderImageId())
|
.headerImageId(e.getHeaderImageId())
|
||||||
.values(e.getValues() != null ? new HashMap<>(e.getValues()) : new HashMap<>())
|
.values(e.getValues() != null ? new HashMap<>(e.getValues()) : new HashMap<>())
|
||||||
.imageValues(e.getImageValues() != null ? new HashMap<>(e.getImageValues()) : 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())
|
.campaignId(e.getCampaignId().toString())
|
||||||
.order(e.getOrder())
|
.order(e.getOrder())
|
||||||
.createdAt(e.getCreatedAt())
|
.createdAt(e.getCreatedAt())
|
||||||
@@ -73,6 +74,7 @@ public class PostgresNpcRepository implements NpcRepository {
|
|||||||
.headerImageId(n.getHeaderImageId())
|
.headerImageId(n.getHeaderImageId())
|
||||||
.values(n.getValues() != null ? new HashMap<>(n.getValues()) : new HashMap<>())
|
.values(n.getValues() != null ? new HashMap<>(n.getValues()) : new HashMap<>())
|
||||||
.imageValues(n.getImageValues() != null ? new HashMap<>(n.getImageValues()) : 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()))
|
.campaignId(Long.parseLong(n.getCampaignId()))
|
||||||
.order(n.getOrder())
|
.order(n.getOrder())
|
||||||
.createdAt(n.getCreatedAt())
|
.createdAt(n.getCreatedAt())
|
||||||
|
|||||||
@@ -62,6 +62,7 @@ public class CharacterController {
|
|||||||
dto.getHeaderImageId(),
|
dto.getHeaderImageId(),
|
||||||
dto.getValues(),
|
dto.getValues(),
|
||||||
dto.getImageValues(),
|
dto.getImageValues(),
|
||||||
|
dto.getKeyValueValues(),
|
||||||
dto.getCampaignId(),
|
dto.getCampaignId(),
|
||||||
order
|
order
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -62,6 +62,7 @@ public class NpcController {
|
|||||||
dto.getHeaderImageId(),
|
dto.getHeaderImageId(),
|
||||||
dto.getValues(),
|
dto.getValues(),
|
||||||
dto.getImageValues(),
|
dto.getImageValues(),
|
||||||
|
dto.getKeyValueValues(),
|
||||||
dto.getCampaignId(),
|
dto.getCampaignId(),
|
||||||
order
|
order
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ public class CharacterDTO {
|
|||||||
private String headerImageId;
|
private String headerImageId;
|
||||||
private Map<String, String> values = new HashMap<>();
|
private Map<String, String> values = new HashMap<>();
|
||||||
private Map<String, List<String>> imageValues = new HashMap<>();
|
private Map<String, List<String>> imageValues = new HashMap<>();
|
||||||
|
private Map<String, Map<String, String>> keyValueValues = new HashMap<>();
|
||||||
private String campaignId;
|
private String campaignId;
|
||||||
private int order;
|
private int order;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ public class NpcDTO {
|
|||||||
private String headerImageId;
|
private String headerImageId;
|
||||||
private Map<String, String> values = new HashMap<>();
|
private Map<String, String> values = new HashMap<>();
|
||||||
private Map<String, List<String>> imageValues = new HashMap<>();
|
private Map<String, List<String>> imageValues = new HashMap<>();
|
||||||
|
private Map<String, Map<String, String>> keyValueValues = new HashMap<>();
|
||||||
private String campaignId;
|
private String campaignId;
|
||||||
private int order;
|
private int order;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ import lombok.AllArgsConstructor;
|
|||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
import lombok.NoArgsConstructor;
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* DTO pour un champ de Template.
|
* DTO pour un champ de Template.
|
||||||
* <p>
|
* <p>
|
||||||
@@ -17,13 +19,20 @@ import lombok.NoArgsConstructor;
|
|||||||
@AllArgsConstructor
|
@AllArgsConstructor
|
||||||
public class TemplateFieldDTO {
|
public class TemplateFieldDTO {
|
||||||
private String name;
|
private String name;
|
||||||
/** "TEXT" ou "IMAGE" (string pour serialisation JSON transparente). */
|
/** "TEXT" | "IMAGE" | "NUMBER" | "KEY_VALUE_LIST". */
|
||||||
private String type;
|
private String type;
|
||||||
/** "GALLERY" | "HERO" | "MASONRY" | "CAROUSEL", null si type=TEXT. */
|
/** "GALLERY" | "HERO" | "MASONRY" | "CAROUSEL", uniquement pour IMAGE. */
|
||||||
private String layout;
|
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) {
|
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.setHeaderImageId(c.getHeaderImageId());
|
||||||
dto.setValues(c.getValues() != null ? new HashMap<>(c.getValues()) : new HashMap<>());
|
dto.setValues(c.getValues() != null ? new HashMap<>(c.getValues()) : new HashMap<>());
|
||||||
dto.setImageValues(c.getImageValues() != null ? new HashMap<>(c.getImageValues()) : 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.setCampaignId(c.getCampaignId());
|
||||||
dto.setOrder(c.getOrder());
|
dto.setOrder(c.getOrder());
|
||||||
return dto;
|
return dto;
|
||||||
@@ -32,6 +33,7 @@ public class CharacterMapper {
|
|||||||
.headerImageId(dto.getHeaderImageId())
|
.headerImageId(dto.getHeaderImageId())
|
||||||
.values(dto.getValues() != null ? new HashMap<>(dto.getValues()) : new HashMap<>())
|
.values(dto.getValues() != null ? new HashMap<>(dto.getValues()) : new HashMap<>())
|
||||||
.imageValues(dto.getImageValues() != null ? new HashMap<>(dto.getImageValues()) : 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())
|
.campaignId(dto.getCampaignId())
|
||||||
.order(dto.getOrder())
|
.order(dto.getOrder())
|
||||||
.build();
|
.build();
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ public class NpcMapper {
|
|||||||
dto.setHeaderImageId(n.getHeaderImageId());
|
dto.setHeaderImageId(n.getHeaderImageId());
|
||||||
dto.setValues(n.getValues() != null ? new HashMap<>(n.getValues()) : new HashMap<>());
|
dto.setValues(n.getValues() != null ? new HashMap<>(n.getValues()) : new HashMap<>());
|
||||||
dto.setImageValues(n.getImageValues() != null ? new HashMap<>(n.getImageValues()) : 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.setCampaignId(n.getCampaignId());
|
||||||
dto.setOrder(n.getOrder());
|
dto.setOrder(n.getOrder());
|
||||||
return dto;
|
return dto;
|
||||||
@@ -32,6 +33,7 @@ public class NpcMapper {
|
|||||||
.headerImageId(dto.getHeaderImageId())
|
.headerImageId(dto.getHeaderImageId())
|
||||||
.values(dto.getValues() != null ? new HashMap<>(dto.getValues()) : new HashMap<>())
|
.values(dto.getValues() != null ? new HashMap<>(dto.getValues()) : new HashMap<>())
|
||||||
.imageValues(dto.getImageValues() != null ? new HashMap<>(dto.getImageValues()) : 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())
|
.campaignId(dto.getCampaignId())
|
||||||
.order(dto.getOrder())
|
.order(dto.getOrder())
|
||||||
.build();
|
.build();
|
||||||
|
|||||||
@@ -6,14 +6,16 @@ import com.loremind.domain.shared.template.TemplateField;
|
|||||||
import com.loremind.infrastructure.web.dto.shared.TemplateFieldDTO;
|
import com.loremind.infrastructure.web.dto.shared.TemplateFieldDTO;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Mapper pour convertir entre {@link TemplateField} (domaine) et
|
* Mapper pour convertir entre {@link TemplateField} (domaine) et
|
||||||
* {@link TemplateFieldDTO} (wire).
|
* {@link TemplateFieldDTO} (wire).
|
||||||
* <p>
|
* <p>
|
||||||
* Tolerance : un type inconnu recu du client est interprete comme TEXT
|
* Tolerance : un type inconnu recu du client est interprete comme TEXT.
|
||||||
* (plus safe que de rejeter la requete et d'interrompre la sauvegarde).
|
|
||||||
* Un layout inconnu ou absent sur un champ IMAGE est interprete comme GALLERY.
|
* 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
|
@Component
|
||||||
public class TemplateFieldMapper {
|
public class TemplateFieldMapper {
|
||||||
@@ -26,7 +28,11 @@ public class TemplateFieldMapper {
|
|||||||
ImageLayout layout = field.getLayout() != null ? field.getLayout() : ImageLayout.GALLERY;
|
ImageLayout layout = field.getLayout() != null ? field.getLayout() : ImageLayout.GALLERY;
|
||||||
layoutStr = layout.name();
|
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) {
|
public TemplateField toDomain(TemplateFieldDTO dto) {
|
||||||
@@ -47,6 +53,10 @@ public class TemplateFieldMapper {
|
|||||||
layout = ImageLayout.GALLERY;
|
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(
|
Npc result = npcService.createNpc(
|
||||||
new NpcService.NpcData("Borin le forgeron", null, null,
|
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);
|
assertNotNull(result);
|
||||||
ArgumentCaptor<Npc> captor = ArgumentCaptor.forClass(Npc.class);
|
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.findByCampaignId("camp-1")).thenReturn(List.of(a, b));
|
||||||
when(npcRepository.save(any(Npc.class))).thenReturn(testNpc);
|
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);
|
ArgumentCaptor<Npc> captor = ArgumentCaptor.forClass(Npc.class);
|
||||||
verify(npcRepository).save(captor.capture());
|
verify(npcRepository).save(captor.capture());
|
||||||
@@ -79,7 +79,7 @@ public class NpcServiceTest {
|
|||||||
when(npcRepository.findByCampaignId("camp-1")).thenReturn(List.of());
|
when(npcRepository.findByCampaignId("camp-1")).thenReturn(List.of());
|
||||||
when(npcRepository.save(any(Npc.class))).thenReturn(testNpc);
|
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);
|
ArgumentCaptor<Npc> captor = ArgumentCaptor.forClass(Npc.class);
|
||||||
verify(npcRepository).save(captor.capture());
|
verify(npcRepository).save(captor.capture());
|
||||||
@@ -124,7 +124,7 @@ public class NpcServiceTest {
|
|||||||
|
|
||||||
Npc result = npcService.updateNpc("npc-1",
|
Npc result = npcService.updateNpc("npc-1",
|
||||||
new NpcService.NpcData("Borin renommé", null, null,
|
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("Borin renommé", result.getName());
|
||||||
assertEquals("v2", result.getValues().get("Notes"));
|
assertEquals("v2", result.getValues().get("Notes"));
|
||||||
@@ -138,7 +138,7 @@ public class NpcServiceTest {
|
|||||||
|
|
||||||
Npc result = npcService.updateNpc("npc-1",
|
Npc result = npcService.updateNpc("npc-1",
|
||||||
new NpcService.NpcData("Borin", null, null,
|
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é
|
// testNpc avait order=1 → préservé
|
||||||
assertEquals(1, result.getOrder());
|
assertEquals(1, result.getOrder());
|
||||||
@@ -150,7 +150,7 @@ public class NpcServiceTest {
|
|||||||
|
|
||||||
IllegalArgumentException ex = assertThrows(IllegalArgumentException.class,
|
IllegalArgumentException ex = assertThrows(IllegalArgumentException.class,
|
||||||
() -> npcService.updateNpc("missing",
|
() -> 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"));
|
assertTrue(ex.getMessage().contains("missing"));
|
||||||
verify(npcRepository, never()).save(any());
|
verify(npcRepository, never()).save(any());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,7 +40,7 @@
|
|||||||
Auteur : ietm64
|
Auteur : ietm64
|
||||||
Licence : AGPL-3.0
|
Licence : AGPL-3.0
|
||||||
Projet : LoreMindMJ - assistant pour Maitres de Jeu de JDR
|
Projet : LoreMindMJ - assistant pour Maitres de Jeu de JDR
|
||||||
Version : 0.8.1
|
Version : 0.8.3
|
||||||
|
|
||||||
.LINK
|
.LINK
|
||||||
https://github.com/IGMLcreation/LoreMind
|
https://github.com/IGMLcreation/LoreMind
|
||||||
|
|||||||
4
web/package-lock.json
generated
4
web/package-lock.json
generated
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "loremind-web",
|
"name": "loremind-web",
|
||||||
"version": "0.8.1",
|
"version": "0.8.3",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "loremind-web",
|
"name": "loremind-web",
|
||||||
"version": "0.8.1",
|
"version": "0.8.3",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@angular/animations": "^17.0.0",
|
"@angular/animations": "^17.0.0",
|
||||||
"@angular/common": "^17.0.0",
|
"@angular/common": "^17.0.0",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "loremind-web",
|
"name": "loremind-web",
|
||||||
"version": "0.8.1",
|
"version": "0.8.3",
|
||||||
"description": "LoreMind Frontend - Angular",
|
"description": "LoreMind Frontend - Angular",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"ng": "ng",
|
"ng": "ng",
|
||||||
|
|||||||
@@ -61,8 +61,10 @@
|
|||||||
[fields]="templateFields"
|
[fields]="templateFields"
|
||||||
[values]="values"
|
[values]="values"
|
||||||
[imageValues]="imageValues"
|
[imageValues]="imageValues"
|
||||||
|
[keyValueValues]="keyValueValues"
|
||||||
(valuesChange)="values = $event"
|
(valuesChange)="values = $event"
|
||||||
(imageValuesChange)="imageValues = $event">
|
(imageValuesChange)="imageValues = $event"
|
||||||
|
(keyValueValuesChange)="keyValueValues = $event">
|
||||||
</app-dynamic-fields-form>
|
</app-dynamic-fields-form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -53,6 +53,7 @@ export class CharacterEditComponent implements OnInit {
|
|||||||
headerImageId: string | null = null;
|
headerImageId: string | null = null;
|
||||||
values: Record<string, string> = {};
|
values: Record<string, string> = {};
|
||||||
imageValues: Record<string, string[]> = {};
|
imageValues: Record<string, string[]> = {};
|
||||||
|
keyValueValues: Record<string, Record<string, string>> = {};
|
||||||
templateFields: TemplateField[] = [];
|
templateFields: TemplateField[] = [];
|
||||||
private order = 0;
|
private order = 0;
|
||||||
|
|
||||||
@@ -81,6 +82,7 @@ export class CharacterEditComponent implements OnInit {
|
|||||||
this.headerImageId = c.headerImageId ?? null;
|
this.headerImageId = c.headerImageId ?? null;
|
||||||
this.values = c.values ?? {};
|
this.values = c.values ?? {};
|
||||||
this.imageValues = c.imageValues ?? {};
|
this.imageValues = c.imageValues ?? {};
|
||||||
|
this.keyValueValues = c.keyValueValues ?? {};
|
||||||
this.order = c.order ?? 0;
|
this.order = c.order ?? 0;
|
||||||
},
|
},
|
||||||
error: () => this.back()
|
error: () => this.back()
|
||||||
@@ -112,6 +114,7 @@ export class CharacterEditComponent implements OnInit {
|
|||||||
headerImageId: this.headerImageId,
|
headerImageId: this.headerImageId,
|
||||||
values: this.values,
|
values: this.values,
|
||||||
imageValues: this.imageValues,
|
imageValues: this.imageValues,
|
||||||
|
keyValueValues: this.keyValueValues,
|
||||||
campaignId: this.campaignId
|
campaignId: this.campaignId
|
||||||
};
|
};
|
||||||
const req = this.characterId
|
const req = this.characterId
|
||||||
|
|||||||
@@ -61,8 +61,10 @@
|
|||||||
[fields]="templateFields"
|
[fields]="templateFields"
|
||||||
[values]="values"
|
[values]="values"
|
||||||
[imageValues]="imageValues"
|
[imageValues]="imageValues"
|
||||||
|
[keyValueValues]="keyValueValues"
|
||||||
(valuesChange)="values = $event"
|
(valuesChange)="values = $event"
|
||||||
(imageValuesChange)="imageValues = $event">
|
(imageValuesChange)="imageValues = $event"
|
||||||
|
(keyValueValuesChange)="keyValueValues = $event">
|
||||||
</app-dynamic-fields-form>
|
</app-dynamic-fields-form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ export class NpcEditComponent implements OnInit {
|
|||||||
headerImageId: string | null = null;
|
headerImageId: string | null = null;
|
||||||
values: Record<string, string> = {};
|
values: Record<string, string> = {};
|
||||||
imageValues: Record<string, string[]> = {};
|
imageValues: Record<string, string[]> = {};
|
||||||
|
keyValueValues: Record<string, Record<string, string>> = {};
|
||||||
templateFields: TemplateField[] = [];
|
templateFields: TemplateField[] = [];
|
||||||
private order = 0;
|
private order = 0;
|
||||||
|
|
||||||
@@ -76,6 +77,7 @@ export class NpcEditComponent implements OnInit {
|
|||||||
this.headerImageId = n.headerImageId ?? null;
|
this.headerImageId = n.headerImageId ?? null;
|
||||||
this.values = n.values ?? {};
|
this.values = n.values ?? {};
|
||||||
this.imageValues = n.imageValues ?? {};
|
this.imageValues = n.imageValues ?? {};
|
||||||
|
this.keyValueValues = n.keyValueValues ?? {};
|
||||||
this.order = n.order ?? 0;
|
this.order = n.order ?? 0;
|
||||||
},
|
},
|
||||||
error: () => this.back()
|
error: () => this.back()
|
||||||
@@ -107,6 +109,7 @@ export class NpcEditComponent implements OnInit {
|
|||||||
headerImageId: this.headerImageId,
|
headerImageId: this.headerImageId,
|
||||||
values: this.values,
|
values: this.values,
|
||||||
imageValues: this.imageValues,
|
imageValues: this.imageValues,
|
||||||
|
keyValueValues: this.keyValueValues,
|
||||||
campaignId: this.campaignId
|
campaignId: this.campaignId
|
||||||
};
|
};
|
||||||
const req = this.npcId
|
const req = this.npcId
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ export interface Character {
|
|||||||
headerImageId?: string | null;
|
headerImageId?: string | null;
|
||||||
values?: Record<string, string>;
|
values?: Record<string, string>;
|
||||||
imageValues?: Record<string, string[]>;
|
imageValues?: Record<string, string[]>;
|
||||||
|
/** Champs KEY_VALUE_LIST : fieldName -> label -> value. */
|
||||||
|
keyValueValues?: Record<string, Record<string, string>>;
|
||||||
campaignId: string;
|
campaignId: string;
|
||||||
order?: number;
|
order?: number;
|
||||||
}
|
}
|
||||||
@@ -23,5 +25,6 @@ export interface CharacterCreate {
|
|||||||
headerImageId?: string | null;
|
headerImageId?: string | null;
|
||||||
values?: Record<string, string>;
|
values?: Record<string, string>;
|
||||||
imageValues?: Record<string, string[]>;
|
imageValues?: Record<string, string[]>;
|
||||||
|
keyValueValues?: Record<string, Record<string, string>>;
|
||||||
campaignId: string;
|
campaignId: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ export interface Npc {
|
|||||||
headerImageId?: string | null;
|
headerImageId?: string | null;
|
||||||
values?: Record<string, string>;
|
values?: Record<string, string>;
|
||||||
imageValues?: Record<string, string[]>;
|
imageValues?: Record<string, string[]>;
|
||||||
|
keyValueValues?: Record<string, Record<string, string>>;
|
||||||
campaignId: string;
|
campaignId: string;
|
||||||
order?: number;
|
order?: number;
|
||||||
}
|
}
|
||||||
@@ -19,5 +20,6 @@ export interface NpcCreate {
|
|||||||
headerImageId?: string | null;
|
headerImageId?: string | null;
|
||||||
values?: Record<string, string>;
|
values?: Record<string, string>;
|
||||||
imageValues?: Record<string, string[]>;
|
imageValues?: Record<string, string[]>;
|
||||||
|
keyValueValues?: Record<string, Record<string, string>>;
|
||||||
campaignId: string;
|
campaignId: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,8 +5,9 @@
|
|||||||
* - 'TEXT' : champ textuel libre (rendu en textarea)
|
* - 'TEXT' : champ textuel libre (rendu en textarea)
|
||||||
* - 'IMAGE' : galerie d'images (rendu en app-image-gallery)
|
* - 'IMAGE' : galerie d'images (rendu en app-image-gallery)
|
||||||
* - 'NUMBER' : valeur numerique (rendu en input number)
|
* - '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
|
* Variante de rendu pour un champ IMAGE. Miroir de
|
||||||
@@ -27,6 +28,8 @@ export interface TemplateField {
|
|||||||
type: FieldType;
|
type: FieldType;
|
||||||
/** Uniquement pour type='IMAGE'. Absent/null = 'GALLERY'. */
|
/** Uniquement pour type='IMAGE'. Absent/null = 'GALLERY'. */
|
||||||
layout?: ImageLayout | null;
|
layout?: ImageLayout | null;
|
||||||
|
/** Labels predefinis pour KEY_VALUE_LIST (ordre significatif). */
|
||||||
|
labels?: string[] | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Template {
|
export interface Template {
|
||||||
|
|||||||
@@ -28,6 +28,23 @@
|
|||||||
[imageIds]="imagesFor(f)"
|
[imageIds]="imagesFor(f)"
|
||||||
(imageIdsChange)="onImageIdsChange(f, $event)">
|
(imageIdsChange)="onImageIdsChange(f, $event)">
|
||||||
</app-image-gallery>
|
</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>
|
</ng-container>
|
||||||
</div>
|
</div>
|
||||||
</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 {
|
.dff-empty {
|
||||||
padding: 24px;
|
padding: 24px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
|||||||
@@ -26,9 +26,11 @@ export class DynamicFieldsFormComponent {
|
|||||||
@Input() fields: TemplateField[] = [];
|
@Input() fields: TemplateField[] = [];
|
||||||
@Input() values: Record<string, string> = {};
|
@Input() values: Record<string, string> = {};
|
||||||
@Input() imageValues: Record<string, string[]> = {};
|
@Input() imageValues: Record<string, string[]> = {};
|
||||||
|
@Input() keyValueValues: Record<string, Record<string, string>> = {};
|
||||||
|
|
||||||
@Output() valuesChange = new EventEmitter<Record<string, string>>();
|
@Output() valuesChange = new EventEmitter<Record<string, string>>();
|
||||||
@Output() imageValuesChange = 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 {
|
onTextChange(field: TemplateField, value: string): void {
|
||||||
this.values = { ...this.values, [field.name]: value };
|
this.values = { ...this.values, [field.name]: value };
|
||||||
@@ -44,5 +46,18 @@ export class DynamicFieldsFormComponent {
|
|||||||
return this.imageValues[field.name] ?? [];
|
return this.imageValues[field.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;
|
trackByName = (_: number, f: TemplateField) => f.name;
|
||||||
|
trackByLabel = (_: number, l: string) => l;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,44 +15,92 @@
|
|||||||
<div class="pv-title-block">
|
<div class="pv-title-block">
|
||||||
<h1 class="pv-name">{{ persona.name }}</h1>
|
<h1 class="pv-name">{{ persona.name }}</h1>
|
||||||
<p *ngIf="subtitle" class="pv-subtitle">{{ subtitle }}</p>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Stats numeriques en bandeau si presentes -->
|
<!-- Sections rendues dans l'ordre du template -->
|
||||||
<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 -->
|
|
||||||
<div class="pv-sections">
|
<div class="pv-sections">
|
||||||
<ng-container *ngFor="let f of textFields; let first = first">
|
<ng-container *ngFor="let s of orderedSections">
|
||||||
<section *ngIf="!f.isNumber" class="pv-section">
|
|
||||||
<h2 class="pv-section-title">{{ f.name }}</h2>
|
<!-- TEXT -->
|
||||||
|
<section *ngIf="s.kind === 'TEXT'" class="pv-section">
|
||||||
|
<h2 class="pv-section-title">{{ s.name }}</h2>
|
||||||
<div class="pv-section-body">
|
<div class="pv-section-body">
|
||||||
<p [class.with-dropcap]="first" class="pv-paragraph">
|
<p [class.with-dropcap]="s.name === firstTextSectionName" class="pv-paragraph">
|
||||||
{{ firstParagraph(f.value) }}
|
{{ firstParagraph(s.value) }}
|
||||||
</p>
|
</p>
|
||||||
<p *ngIf="restAfterFirstParagraph(f.value)" class="pv-paragraph">
|
<p *ngIf="restAfterFirstParagraph(s.value)" class="pv-paragraph">
|
||||||
{{ restAfterFirstParagraph(f.value) }}
|
{{ restAfterFirstParagraph(s.value) }}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</ng-container>
|
|
||||||
|
|
||||||
<!-- Galeries d'images templates -->
|
<!-- NUMBER_GROUP : NUMBER consecutifs - table si <=6, sinon liste 2 cols -->
|
||||||
<section *ngFor="let img of imageFields" class="pv-section pv-section-images">
|
<section *ngIf="s.kind === 'NUMBER_GROUP'" class="pv-section pv-section-number">
|
||||||
<h2 class="pv-section-title">{{ img.field.name }}</h2>
|
<ng-container *ngIf="s.entries.length <= 6; else numGroupList">
|
||||||
<app-image-gallery [imageIds]="img.ids" [layout]="img.field.layout || 'GALLERY'" [editable]="false">
|
<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>
|
</app-image-gallery>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
<!-- Etat vide -->
|
<!-- 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>
|
<lucide-icon [img]="BookOpen" [size]="32"></lucide-icon>
|
||||||
<p>Cette fiche est encore vide.</p>
|
<p>Cette fiche est encore vide.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -92,41 +92,145 @@
|
|||||||
letter-spacing: 0.05em;
|
letter-spacing: 0.05em;
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Bandeau de stats (NUMBER) ---------------------------------------------
|
// Badges compacts pour les NUMBER isoles (Niveau, etc.) — evite la grosse card.
|
||||||
|
.pv-hero-badges {
|
||||||
.pv-stat-band {
|
margin-top: 12px;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
gap: 16px;
|
gap: 8px;
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.pv-stat {
|
.pv-hero-badge {
|
||||||
display: none;
|
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 {
|
.pv-hero-badge-label {
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
min-width: 60px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pv-stat-label {
|
|
||||||
font-size: 0.7rem;
|
font-size: 0.7rem;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
letter-spacing: 0.08em;
|
letter-spacing: 0.1em;
|
||||||
color: #9ca3af;
|
color: #9ca3af;
|
||||||
}
|
}
|
||||||
|
.pv-hero-badge-value {
|
||||||
.pv-stat-value {
|
font-family: 'Cinzel', 'EB Garamond', Georgia, serif;
|
||||||
font-size: 1.25rem;
|
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
color: #f3f4f6;
|
font-size: 1.05rem;
|
||||||
font-family: 'Cinzel', Georgia, serif;
|
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 {
|
.pv-section-title {
|
||||||
font-family: 'Cinzel', 'EB Garamond', Georgia, serif;
|
font-family: 'Cinzel', 'EB Garamond', Georgia, serif;
|
||||||
font-size: 1.4rem;
|
font-size: 1.4rem;
|
||||||
|
|||||||
@@ -1,7 +1,14 @@
|
|||||||
import { Component, Input } from '@angular/core';
|
import { Component, Input } from '@angular/core';
|
||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { LucideAngularModule, BookOpen } from 'lucide-angular';
|
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 { ImageService } from '../../services/image.service';
|
||||||
import { ImageGalleryComponent } from '../image-gallery/image-gallery.component';
|
import { ImageGalleryComponent } from '../image-gallery/image-gallery.component';
|
||||||
|
|
||||||
@@ -22,6 +29,7 @@ export interface PersonaLike {
|
|||||||
headerImageId?: string | null;
|
headerImageId?: string | null;
|
||||||
values?: Record<string, string>;
|
values?: Record<string, string>;
|
||||||
imageValues?: Record<string, string[]>;
|
imageValues?: Record<string, string[]>;
|
||||||
|
keyValueValues?: Record<string, Record<string, string>>;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
@@ -46,30 +54,69 @@ export class PersonaViewComponent {
|
|||||||
return this.imageService.contentUrl(id);
|
return this.imageService.contentUrl(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Champs TEXT/NUMBER non vides, dans l'ordre du template. */
|
/**
|
||||||
get textFields(): { name: string; value: string; isNumber: boolean }[] {
|
* Decompose la fiche en (heroBadges, sections) en un seul passage :
|
||||||
if (!this.persona?.values) return [];
|
* - NUMBER consecutifs groupes : 2+ → NUMBER_GROUP en section ; 1 isole → badge hero.
|
||||||
return this.templateFields
|
* - TEXT / KEY_VALUE_LIST / IMAGE : sections dans l'ordre du template.
|
||||||
.filter(f => (f.type === 'TEXT' || f.type === 'NUMBER'))
|
* Le calcul est fait par rendered() et cache via le get pour eviter les
|
||||||
.map(f => ({
|
* recalculs multiples par cycle de change detection.
|
||||||
name: f.name,
|
*/
|
||||||
value: this.persona!.values?.[f.name] ?? '',
|
private rendered(): { heroBadges: { label: string; value: string }[]; sections: RenderedSection[] } {
|
||||||
isNumber: f.type === 'NUMBER'
|
const sections: RenderedSection[] = [];
|
||||||
}))
|
const heroBadges: { label: string; value: string }[] = [];
|
||||||
.filter(x => x.value && x.value.trim().length > 0);
|
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 heroBadges(): { label: string; value: string }[] {
|
||||||
get imageFields(): { field: TemplateField; ids: string[] }[] {
|
return this.rendered().heroBadges;
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
hasAnyNumber(fields: { isNumber: boolean }[]): boolean {
|
get orderedSections(): RenderedSection[] {
|
||||||
return fields.some(f => f.isNumber);
|
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). */
|
/** Premier paragraphe d'un texte (utilise pour la drop cap). */
|
||||||
|
|||||||
@@ -5,7 +5,8 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="tfe-list">
|
<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">
|
<div class="tfe-row-controls">
|
||||||
<button type="button" class="btn-arrow" (click)="moveUp(i)" [disabled]="i === 0" title="Monter">
|
<button type="button" class="btn-arrow" (click)="moveUp(i)" [disabled]="i === 0" title="Monter">
|
||||||
<lucide-icon [img]="ArrowUp" [size]="14"></lucide-icon>
|
<lucide-icon [img]="ArrowUp" [size]="14"></lucide-icon>
|
||||||
@@ -46,6 +47,40 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</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">
|
<div *ngIf="fields.length === 0" class="tfe-empty">
|
||||||
Aucun champ pour l'instant — ajoutez-en avec les boutons ci-dessous.
|
Aucun champ pour l'instant — ajoutez-en avec les boutons ci-dessous.
|
||||||
</div>
|
</div>
|
||||||
@@ -75,5 +110,9 @@
|
|||||||
<lucide-icon [img]="ImageIcon" [size]="12"></lucide-icon>
|
<lucide-icon [img]="ImageIcon" [size]="12"></lucide-icon>
|
||||||
Image(s)
|
Image(s)
|
||||||
</button>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -137,3 +137,88 @@
|
|||||||
.chip-custom {
|
.chip-custom {
|
||||||
border-style: dashed;
|
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 { Component, EventEmitter, Input, Output } from '@angular/core';
|
||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { FormsModule } from '@angular/forms';
|
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';
|
import { TemplateField, FieldType, ImageLayout } from '../../services/template.model';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -29,6 +29,8 @@ export class TemplateFieldsEditorComponent {
|
|||||||
readonly Type = Type;
|
readonly Type = Type;
|
||||||
readonly ImageIcon = ImageIcon;
|
readonly ImageIcon = ImageIcon;
|
||||||
readonly Hash = Hash;
|
readonly Hash = Hash;
|
||||||
|
readonly ListOrdered = ListOrdered;
|
||||||
|
readonly X = X;
|
||||||
|
|
||||||
/** Liste des champs (binding parent). */
|
/** Liste des champs (binding parent). */
|
||||||
@Input() fields: TemplateField[] = [];
|
@Input() fields: TemplateField[] = [];
|
||||||
@@ -47,7 +49,8 @@ export class TemplateFieldsEditorComponent {
|
|||||||
readonly typeOptions: { value: FieldType; label: string }[] = [
|
readonly typeOptions: { value: FieldType; label: string }[] = [
|
||||||
{ value: 'TEXT', label: 'Texte' },
|
{ value: 'TEXT', label: 'Texte' },
|
||||||
{ value: 'NUMBER', label: 'Nombre' },
|
{ 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 }[] = [
|
readonly layoutOptions: { value: ImageLayout; label: string }[] = [
|
||||||
@@ -75,9 +78,53 @@ export class TemplateFieldsEditorComponent {
|
|||||||
|
|
||||||
addBlank(type: FieldType): void {
|
addBlank(type: FieldType): void {
|
||||||
const layout: ImageLayout | null = type === 'IMAGE' ? 'GALLERY' : null;
|
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 {
|
remove(index: number): void {
|
||||||
const next = [...this.fields];
|
const next = [...this.fields];
|
||||||
next.splice(index, 1);
|
next.splice(index, 1);
|
||||||
@@ -100,10 +147,11 @@ export class TemplateFieldsEditorComponent {
|
|||||||
|
|
||||||
/** Notifie les changements internes (input/select sur un champ existant). */
|
/** Notifie les changements internes (input/select sur un champ existant). */
|
||||||
onFieldChanged(): void {
|
onFieldChanged(): void {
|
||||||
// Quand le type passe a IMAGE, layout = GALLERY ; sinon null.
|
|
||||||
for (const f of this.fields) {
|
for (const f of this.fields) {
|
||||||
if (f.type === 'IMAGE' && !f.layout) f.layout = 'GALLERY';
|
if (f.type === 'IMAGE' && !f.layout) f.layout = 'GALLERY';
|
||||||
if (f.type !== 'IMAGE') f.layout = null;
|
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]);
|
this.emit([...this.fields]);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user