diff --git a/screens/widgets/control_groups/data_binding_controls.py b/screens/widgets/control_groups/data_binding_controls.py index 6207c70f..8d8334e1 100644 --- a/screens/widgets/control_groups/data_binding_controls.py +++ b/screens/widgets/control_groups/data_binding_controls.py @@ -13,14 +13,14 @@ from ...flow.constants import CONDITION_OPERATORS 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_text("data_filtre_champ", "Trouver la ligne où ce champ", - "attr:_data_filtre_champ", placeholder="ex. number", 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, ou {{NomObjet.champ}} pour une donnée qui change en cours de partie", - "attr:_data_filtre_valeur", placeholder="ex. 3 ou {{game.current_level}}", default="", group="Donnée liée"), - c_text("data_filtre2_champ", "ET (optionnel) : ce 2ᵉ champ", - "attr:_data_filtre2_champ", placeholder="ex. parcour_id", default="", 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, ou {{NomObjet.champ}}", - "attr:_data_filtre2_valeur", placeholder="ex. 1 ou {{game.current_parcours}}", default="", 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"), ] diff --git a/screens/widgets/controls_with_values.py b/screens/widgets/controls_with_values.py index b2cc63c2..02dc5db7 100644 --- a/screens/widgets/controls_with_values.py +++ b/screens/widgets/controls_with_values.py @@ -21,6 +21,12 @@ def controls_with_values(slug, el): # dans screen_edit.html), ceci ne couvre que le premier affichage/une # navigation vers un élément déjà réglé. jauge_options = data_definition_options(slug, attributes.get("_definition_id")) + # Le Texte/Titre lié à une donnée (voir DATA_BINDING_CONTROLS) garde SON + # objet dans un attribut séparé (_data_definition_id) — jamais le même + # élément que le Répéteur/la Jauge, donc jamais besoin des deux options + # en même temps, mais un calcul dédié quand même pour rester correct si + # ça change un jour. + data_binding_options = data_definition_options(slug, attributes.get("_data_definition_id")) result = [] for control in meta.get("controls", []): control = dict(control) @@ -32,9 +38,11 @@ def controls_with_values(slug, el): control["options"] = [("", "(aucun — utiliser le modèle de texte ci-dessous)")] + [(str(t["id"]), t["name"]) for t in types] if control["key"] == "row_id": control["options"] = [("", "(sélectionner un objet d'abord)")] + [(str(r["id"]), r["label"]) for r in jauge_options["rows"]] - if control["key"] == "champ": + if control["key"] in ("champ", "filtre_champ", "filtre2_champ"): control["options"] = [("", "(sélectionner un objet d'abord)")] + [(f["name"], f["name"]) for f in jauge_options["fields"]] if control["key"] == "champ_nom": control["options"] = [("", "— aucun —")] + [(f["name"], f["name"]) 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"], f["name"]) for f in data_binding_options["fields"]] result.append({**control, "value": _control_value(el, control)}) return result diff --git a/screens/widgets/registry.py b/screens/widgets/registry.py index 07009557..b1b05136 100644 --- a/screens/widgets/registry.py +++ b/screens/widgets/registry.py @@ -240,16 +240,16 @@ WIDGETS = { c_textarea("content", "Modèle d'affichage texte — utilise {{nom_du_champ}}", "content", default="{{nom}}", group="Contenu"), c_color("color", "Couleur du texte", "style:color", "#e8eaf0", group="Texte"), c_slider("font-size", "Taille du texte", "style:font-size", min=10, max=40, default=16, group="Texte"), - c_text("filtre_champ", "Filtrer : n'afficher que les lignes où ce champ", - "attr:_filtre_champ", placeholder="ex. niveau", default="", group="Filtre"), + c_select("filtre_champ", "Filtrer : n'afficher que les lignes où ce champ", + "attr:_filtre_champ", [("", "(sélectionner un objet d'abord)")], default="", group="Filtre"), c_select("filtre_operateur", "...est", "attr:_filtre_operateur", CONDITION_OPERATORS, default="egal", group="Filtre"), - c_text("filtre_valeur", "...cette valeur, ou {{NomObjet.champ}} pour comparer à une donnée qui change en cours de partie", - "attr:_filtre_valeur", placeholder="ex. 3 ou {{Partie.niveau_courant}}", default="", group="Filtre"), - c_text("filtre2_champ", "ET (optionnel) : ce 2ᵉ champ", - "attr:_filtre2_champ", placeholder="ex. parcour_id", default="", group="Filtre"), + c_text("filtre_valeur", "...cette valeur", + "attr:_filtre_valeur", placeholder="ex. 3", default="", group="Filtre"), + c_select("filtre2_champ", "ET (optionnel) : ce 2ᵉ champ", + "attr:_filtre2_champ", [("", "(sélectionner un objet d'abord)")], default="", group="Filtre"), c_select("filtre2_operateur", "...est", "attr:_filtre2_operateur", CONDITION_OPERATORS, default="egal", group="Filtre"), - c_text("filtre2_valeur", "...cette valeur, ou {{NomObjet.champ}}", - "attr:_filtre2_valeur", placeholder="ex. 1 ou {{Partie.parcours_courant}}", default="", group="Filtre"), + c_text("filtre2_valeur", "...cette valeur", + "attr:_filtre2_valeur", placeholder="ex. 1", default="", group="Filtre"), ], }, "jauge": { diff --git a/templates/screen_edit.html b/templates/screen_edit.html index a3dc0050..bb3aff59 100644 --- a/templates/screen_edit.html +++ b/templates/screen_edit.html @@ -380,6 +380,41 @@ + {% elif c.key.endswith('_valeur') %} + +
+
+ +
+
+ +
+ +
{% elif c.type in ('text', 'url') %}
@@ -1429,6 +1464,9 @@ function initBuilderPanel() { bindPropsAutosave(); bindJaugeDefinitionSelect(); + bindDefinitionFieldSelects('field-definition_id', ['field-filtre_champ', 'field-filtre2_champ']); + bindDefinitionFieldSelects('field-data_definition_id', ['field-data_filtre_champ', 'field-data_filtre2_champ']); + initFilterValuePickers(); restoreTreeCollapsedState(); applySelectionHighlight(CURRENT_SELECTED_ID); restoreFloatPanelState('props', 'propsPanelFloat', 'propsPanelReopenBtn'); @@ -1460,6 +1498,103 @@ function bindJaugeDefinitionSelect() { }); } +// ---------- Réglages "champ" d'un filtre (Répéteur/Donnée liée) : listes +// déroulantes peuplées avec les VRAIS champs de l'objet choisi, jamais +// tapées à la main — même principe que bindJaugeDefinitionSelect() ci-dessus, +// généralisé pour s'appliquer à un nombre quelconque de listes "champ" +// rattachées à un même sélecteur d'objet (ici : filtre_champ + filtre2_champ, +// ou data_filtre_champ + data_filtre2_champ). ---------- +function bindDefinitionFieldSelects(defSelId, fieldSelIds) { + var defSel = document.getElementById(defSelId); + if (!defSel) return; + var fieldSels = fieldSelIds.map(function (id) { return document.getElementById(id); }).filter(Boolean); + if (!fieldSels.length) return; + defSel.addEventListener('change', function () { + var info = (DEFINITIONS_DATA || {})[defSel.value] || { fields: [] }; + var opts = '' + + info.fields.map(function (f) { return ''; }).join(''); + fieldSels.forEach(function (sel) { sel.innerHTML = opts; }); + }); +} + +// ---------- Valeur de comparaison d'un filtre : fixe, ou donnée d'un autre +// objet ---------- +// +// La valeur envoyée au serveur reste une simple chaîne "texte" (voir +// _resolve_filter_value côté Python) — soit une valeur fixe, soit une +// référence à un champ d'un autre objet dans un format interne précis. Le +// mode "dynamique" ci-dessous se contente de reconstruire cette chaîne à +// partir de deux listes déroulantes (objet, champ) plutôt que de la faire +// taper à la main. OPEN/CLOSE évitent d'écrire les deux accolades l'une +// contre l'autre dans le source (Jinja les interpréterait comme le DÉBUT +// d'une expression s'il les rencontrait telles quelles). +var _FILTER_REF_OPEN = '{' + '{'; +var _FILTER_REF_CLOSE = '}' + '}'; +var _FILTER_REF_RE = new RegExp('^\\{\\{\\s*([^.{}]+)\\.([^.{}]+)\\s*\\}\\}$'); + +function _filterValueDefinitionId(objName) { + var opt = document.querySelector('.filterValueObjSel option[value="' + CSS.escape(objName) + '"]'); + return opt ? opt.dataset.definitionId : null; +} + +function populateFilterValueFields(fieldSel, defId, selectedName) { + var info = (DEFINITIONS_DATA || {})[defId] || { fields: [] }; + fieldSel.innerHTML = '' + info.fields.map(function (f) { + return ''; + }).join(''); +} + +function updateFilterValueFromDynamic(wrap) { + var objSel = wrap.querySelector('.filterValueObjSel'); + var fieldSel = wrap.querySelector('.filterValueFieldSel'); + var input = wrap.querySelector('.filterValueFixe input'); + input.value = (objSel.value && fieldSel.value) ? (_FILTER_REF_OPEN + objSel.value + '.' + fieldSel.value + _FILTER_REF_CLOSE) : ''; + input.dispatchEvent(new Event('input', { bubbles: true })); +} + +function onFilterValueModeChange(sel) { + var wrap = sel.closest('.filterValueControl'); + var fixe = wrap.querySelector('.filterValueFixe'); + var dyn = wrap.querySelector('.filterValueDynamique'); + if (sel.value === 'dynamique') { + fixe.style.display = 'none'; + dyn.style.display = 'block'; + updateFilterValueFromDynamic(wrap); + } else { + fixe.style.display = 'block'; + dyn.style.display = 'none'; + } +} + +function onFilterValueObjChange(sel) { + var wrap = sel.closest('.filterValueControl'); + populateFilterValueFields(wrap.querySelector('.filterValueFieldSel'), _filterValueDefinitionId(sel.value), ''); + updateFilterValueFromDynamic(wrap); +} + +function onFilterValueFieldChange(sel) { + updateFilterValueFromDynamic(sel.closest('.filterValueControl')); +} + +function initFilterValuePickers() { + document.querySelectorAll('.filterValueControl').forEach(function (wrap) { + var modeSel = wrap.querySelector('.filterValueMode'); + var objSel = wrap.querySelector('.filterValueObjSel'); + var fieldSel = wrap.querySelector('.filterValueFieldSel'); + var current = (wrap.dataset.currentValue || '').trim(); + var m = _FILTER_REF_RE.exec(current); + if (m) { + modeSel.value = 'dynamique'; + wrap.querySelector('.filterValueFixe').style.display = 'none'; + wrap.querySelector('.filterValueDynamique').style.display = 'block'; + objSel.value = m[1].trim(); + populateFilterValueFields(fieldSel, _filterValueDefinitionId(objSel.value), m[2].trim()); + } else { + modeSel.value = 'fixe'; + } + }); +} + // Le retour/avance du navigateur (bouton "précédent") est déjà pris en // charge par pjax.js (navigation globale sans rechargement, voir // static/pjax.js) : il refait un rendu complet de
et rejoue ce