Donnée liée : conditions ET/OU illimitées + conditions de logique sur une variable globale
Deux fonctionnalités demandées, développées et corrigées dans cet
échange :
1. "Donnée liée" (Texte/Titre) : le réglage à 2 filtres fixes (toujours
combinés en ET) devient une liste de conditions ILLIMITÉE, avec un
choix ET/OU pour les combiner (screens/widgets/controls/c_clause_list.py,
screens/clause_list_codec.py). Rétrocompatible avec les anciens
éléments (_data_filtre_champ/_data_filtre2_champ), convertis à la
volée à la lecture, sans migration. Après un premier essai à la
présentation trop compacte et technique (retour utilisateur : "pas de
champ technique, pas de notation bizarre {{ }}"), la présentation
finale reprend EXACTEMENT l'ancien style (labels "Champ"/"...est"/
"...cette valeur", même sélecteur de valeur fixe/dynamique/variable
déjà existant, jamais la syntaxe brute), simplement répétée par
condition (templates/partials/clause_row.html), avec un bouton
"+ Ajouter une condition" bien visible et une liste scrollable
(static/style.css, .clauseListWrap). Le même moteur (filter_repeater_
rows.py généralisé) profite aussi au Répéteur de données en interne.
2. Nœud Condition de la Logique de la scène : peut désormais tester une
VARIABLE GLOBALE en plus d'un champ d'objet (cond_source/cond_variable/
cond_variable_chemin — screens/flow/ensure_flow_schema.py), sur la
clause principale ET chaque clause supplémentaire (ET/OU). Évalué côté
CLIENT (templates/play.html, evaluateConditionClause), contre un
nouveau gameData.variables exposé par full_game_payload.py — tenu à
jour par refreshRuntimeData() après toute action qui modifie une
variable, sans changement supplémentaire nécessaire. Le panneau de
condition reste utilisable même sans aucun objet défini dans le jeu
(avant, il disparaissait entièrement).
Vérifié : 153 tests pytest (nouveaux : test_data_binding_clause_list.py,
test_condition_variable.py) + logique JS d'évaluation des conditions
vérifiée isolément avec Node (variable scalaire, objet avec chemin
chaîné, tableau par index, variable introuvable, booléen, rétrocompatibilité
legacy) + rendu des deux pages (éditeur/jeu) vérifié sur le vrai projet
"test" en plus des jeux de test.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
7c237d6f1c
commit
8cffbeac68
@@ -1,26 +1,20 @@
|
||||
from ..controls.c_text import c_text
|
||||
from ..controls.c_select import c_select
|
||||
from ..controls.c_clause_list import c_clause_list
|
||||
from ...flow.constants import CONDITION_OPERATORS
|
||||
|
||||
# Permet à un Texte/Titre d'afficher directement la valeur d'un champ d'un
|
||||
# AUTRE objet de données, sans avoir besoin de le poser dans un Répéteur —
|
||||
# utile pour une valeur unique calculée (ex: l'objectif du niveau en cours)
|
||||
# plutôt qu'une liste. La ligne affichée est celle qui correspond aux 1 ou 2
|
||||
# filtres réglés ci-dessous (mêmes réglages, même moteur que ceux du
|
||||
# Répéteur de données — voir filter_repeater_rows.py) ; sans filtre, c'est la
|
||||
# ligne la plus récente de l'objet choisi (convention déjà utilisée par la
|
||||
# Jauge pour un objet "état de partie" qui ne garde qu'une seule ligne).
|
||||
# plutôt qu'une liste. La ligne affichée est celle qui correspond aux
|
||||
# conditions réglées ci-dessous (un nombre quelconque, combinées par ET/OU —
|
||||
# mêmes clauses, même moteur que le Répéteur de données, voir
|
||||
# filter_repeater_rows.py) ; sans condition, c'est la ligne la plus récente
|
||||
# de l'objet choisi (convention déjà utilisée par la Jauge pour un objet
|
||||
# "état de partie" qui ne garde qu'une seule ligne).
|
||||
DATA_BINDING_CONTROLS = [
|
||||
c_select("data_definition_id", "Lier à un objet de données (pour {{champ}} ci-dessus)",
|
||||
"attr:_data_definition_id", [("", "(sélectionner un objet)")], default="", group="Donnée liée"),
|
||||
c_select("data_filtre_champ", "Trouver la ligne où ce champ",
|
||||
"attr:_data_filtre_champ", [("", "(sélectionner un objet d'abord)")], default="", group="Donnée liée"),
|
||||
c_select("data_filtre_operateur", "...est", "attr:_data_filtre_operateur", CONDITION_OPERATORS, default="egal", group="Donnée liée"),
|
||||
c_text("data_filtre_valeur", "...cette valeur",
|
||||
"attr:_data_filtre_valeur", placeholder="ex. 3", default="", group="Donnée liée"),
|
||||
c_select("data_filtre2_champ", "ET (optionnel) : ce 2ᵉ champ",
|
||||
"attr:_data_filtre2_champ", [("", "(sélectionner un objet d'abord)")], default="", group="Donnée liée"),
|
||||
c_select("data_filtre2_operateur", "...est", "attr:_data_filtre2_operateur", CONDITION_OPERATORS, default="egal", group="Donnée liée"),
|
||||
c_text("data_filtre2_valeur", "...cette valeur",
|
||||
"attr:_data_filtre2_valeur", placeholder="ex. 1", default="", group="Donnée liée"),
|
||||
c_clause_list("data_filtres", "Trouver la ligne où...",
|
||||
clauses_attr="_data_filtres_json", combinator_attr="_data_filtre_combinateur",
|
||||
operators=CONDITION_OPERATORS, legacy_prefix="_data_filtre", group="Donnée liée"),
|
||||
]
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import re
|
||||
|
||||
from ..clause_list_codec import decode_clauses
|
||||
|
||||
|
||||
def _control_value(el, control):
|
||||
"""Lit la valeur actuelle d'un réglage à partir de l'élément stocké
|
||||
@@ -7,8 +9,34 @@ def _control_value(el, control):
|
||||
ctype = control["type"]
|
||||
style = el.get("style") or {}
|
||||
|
||||
if ctype == "clause_list":
|
||||
attrs = el.get("attributes") or {}
|
||||
clauses, combinateur = decode_clauses(
|
||||
attrs, control["clauses_attr"], control["combinator_attr"],
|
||||
legacy_prefix=control.get("legacy_prefix"),
|
||||
)
|
||||
return {"clauses": clauses, "combinateur": combinateur}
|
||||
|
||||
if ctype == "preset":
|
||||
for key, _label, props in control["options"]:
|
||||
# Trié par nombre de propriétés NON VIDES exigées, décroissant : une
|
||||
# option comme "Visible normalement" (visibility_control.py), dont
|
||||
# les critères ({"visibility": ""}) ne portent QUE sur "visibility"
|
||||
# (par choix assumé, pour ne jamais toucher "display" — voir son
|
||||
# commentaire), matchait sinon TOUJOURS en premier dès que
|
||||
# "visibility" est absent, MÊME quand "display:none" (posé par
|
||||
# "Masqué", un critère plus spécifique à 1 propriété non vide contre
|
||||
# 0 pour "Visible") est également présent -- le panneau affichait
|
||||
# alors "Visible normalement" sélectionné sur un élément en réalité
|
||||
# masqué, et comme un <select> ne redéclenche un enregistrement que
|
||||
# sur un VRAI changement de valeur, il devenait impossible de le
|
||||
# rendre visible depuis le panneau (choisir l'option déjà affichée
|
||||
# ne fait rien). Les options plus spécifiques (plus de critères non
|
||||
# vides) doivent donc toujours être vérifiées EN PREMIER.
|
||||
options = sorted(
|
||||
control["options"],
|
||||
key=lambda o: -sum(1 for v in o[2].values() if v),
|
||||
)
|
||||
for key, _label, props in options:
|
||||
if all((style.get(k) or "") == (v or "") for k, v in props.items()):
|
||||
return key
|
||||
return control.get("default")
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
def c_clause_list(key, label, clauses_attr, combinator_attr, operators, legacy_prefix=None, group=None):
|
||||
"""Réglage "liste de conditions" — un nombre ILLIMITÉ de clauses
|
||||
(champ/opérateur/valeur), combinées entre elles par ET ou par OU (voir
|
||||
aussi les nœuds Condition du graphe de logique, qui ont le même besoin
|
||||
— screens/flow/constants.py, cond_clauses/cond_combinator — même
|
||||
principe, appliqué ici à la Donnée liée d'un Texte/Titre).
|
||||
|
||||
Volontairement PAS de "target" (contrairement aux autres réglages) :
|
||||
stocké dans DEUX attributs séparés (clauses_attr : JSON de la liste ;
|
||||
combinator_attr : "et"/"ou"), traités par un code dédié à chaque étape
|
||||
(control_value.py, save_element_controls.py, controls_with_values.py,
|
||||
templates/screen_edit.html) plutôt que par le chemin générique "un
|
||||
réglage = une seule valeur = une seule clé de style/attribut"."""
|
||||
return {
|
||||
"type": "clause_list", "key": key, "label": label,
|
||||
"clauses_attr": clauses_attr, "combinator_attr": combinator_attr,
|
||||
"operators": operators, "legacy_prefix": legacy_prefix, "default": None, "group": group,
|
||||
}
|
||||
@@ -78,8 +78,12 @@ def controls_with_values(slug, el, template_definition_id=None):
|
||||
control["options"] = [("", empty_label)] + [(f["name"], _field_label(f)) for f in non_relation_fields]
|
||||
if control["key"] in ("filtre_champ", "filtre2_champ"):
|
||||
control["options"] = [("", "(sélectionner un objet d'abord)")] + [(f["name"], _field_label(f)) for f in jauge_options["fields"]]
|
||||
if control["key"] in ("data_filtre_champ", "data_filtre2_champ"):
|
||||
control["options"] = [("", "(sélectionner un objet d'abord)")] + [(f["name"], _field_label(f)) for f in data_binding_options["fields"]]
|
||||
if control["type"] == "clause_list":
|
||||
# Options du sélecteur "champ" de CHAQUE ligne de condition —
|
||||
# les mêmes champs que data_filtre_champ ci-dessus (même objet
|
||||
# lié, attr:_data_definition_id), exposées une seule fois ici
|
||||
# plutôt que dupliquées par ligne (nombre de lignes variable).
|
||||
control["field_options"] = [(f["name"], _field_label(f)) for f in data_binding_options["fields"]]
|
||||
if control["key"] == "content":
|
||||
# Une fois "Lier à un objet de données" réglé (voir
|
||||
# DATA_BINDING_CONTROLS), propose les champs de CET objet en
|
||||
|
||||
Reference in New Issue
Block a user