diff --git a/routes/screens/screen_edit.py b/routes/screens/screen_edit.py
index e4bbb426..886b4793 100644
--- a/routes/screens/screen_edit.py
+++ b/routes/screens/screen_edit.py
@@ -22,7 +22,12 @@ def screen_edit(slug, screen_id):
if selected_id:
selected_el = next((e for e in elements if e["id"] == selected_id), None)
if selected_el:
- selected_controls = screens.controls_with_values(slug, selected_el)
+ template_definition_id = None
+ if screen.get("is_template"):
+ owning_type = screens.get_element_type_by_template_screen(slug, screen_id)
+ if owning_type:
+ template_definition_id = owning_type.get("bound_definition_id")
+ selected_controls = screens.controls_with_values(slug, selected_el, template_definition_id)
children_of = {}
for el in elements:
if el.get("parent_id"):
diff --git a/screens/__init__.py b/screens/__init__.py
index 888a4205..e21479e0 100644
--- a/screens/__init__.py
+++ b/screens/__init__.py
@@ -85,6 +85,7 @@ from .elements.rename_element import rename_element
from .elements.element_label import element_label
from .element_types.list_element_types import list_element_types
+from .element_types.get_element_type_by_template_screen import get_element_type_by_template_screen
from .element_types.create_element_type import create_element_type
from .element_types.update_element_type import update_element_type
from .element_types.element_type_usage_count import element_type_usage_count
diff --git a/screens/element_types/get_element_type_by_template_screen.py b/screens/element_types/get_element_type_by_template_screen.py
new file mode 100644
index 00000000..1533cced
--- /dev/null
+++ b/screens/element_types/get_element_type_by_template_screen.py
@@ -0,0 +1,16 @@
+import db
+
+from ..screens_repo.ensure_schema import ensure_schema
+
+
+def get_element_type_by_template_screen(slug, screen_id):
+ """L'entrée du catalogue (voir _element_types) dont l'écran-modèle EST
+ cet écran, s'il y en a une — utile pour retrouver, depuis l'éditeur
+ d'un écran-modèle, l'objet éventuellement lié à cet élément de jeu
+ (bound_definition_id, réglé à la création — voir element_types.html)
+ sans devoir tout lister soi-même."""
+ ensure_schema(slug)
+ conn = db.connect(slug)
+ row = conn.execute("SELECT * FROM _element_types WHERE template_screen_id = ?", (screen_id,)).fetchone()
+ conn.close()
+ return dict(row) if row else None
diff --git a/screens/widgets/controls_with_values.py b/screens/widgets/controls_with_values.py
index f94e4dfc..94ef0093 100644
--- a/screens/widgets/controls_with_values.py
+++ b/screens/widgets/controls_with_values.py
@@ -15,11 +15,21 @@ def _field_label(f):
return f["name"]
-def controls_with_values(slug, el):
+def controls_with_values(slug, el, template_definition_id=None):
"""Pour le panneau de propriétés : la liste des réglages du widget de
cet élément, chacun avec sa valeur actuelle déjà résolue. Le réglage
"définition_id" du Répéteur de données (et de la Jauge) a besoin du jeu
- courant pour lister les objets disponibles — d'où le paramètre slug."""
+ courant pour lister les objets disponibles — d'où le paramètre slug.
+
+ `template_definition_id` : l'objet lié à l'ÉLÉMENT DE JEU en cours
+ d'édition (bound_definition_id, réglé à sa création — voir
+ element_types.html), passé par screen_edit.py seulement quand on
+ édite un écran-modèle qui en a un. Sert de repli pour le champ
+ "Contenu" (voir plus bas) : si CE widget n'a pas sa propre "Donnée
+ liée" réglée, ses {{champ}} font quand même référence à l'objet lié
+ de l'élément de jeu (c'est lui qui fournira les valeurs une fois posé
+ dans un Répéteur), donc le sélecteur de champ doit lister SES champs
+ par défaut plutôt que de rester vide."""
meta = widget_meta(el.get("widget"))
attributes = el.get("attributes") or {}
# Pour la Jauge : quel enregistrement suivre (row_id) et quels champs
@@ -36,6 +46,9 @@ def controls_with_values(slug, el):
# 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"))
+ # Repli pour "content" (voir plus bas) quand ce widget n'a pas sa
+ # propre "Donnée liée" : les champs de l'objet lié à l'élément de jeu.
+ template_bound_options = data_definition_options(slug, template_definition_id) if template_definition_id else None
# Condition de visibilité (voir VISIBILITY_CONDITION_CONTROLS) : elle
# aussi garde SON objet dans un attribut séparé (_visibilite_cond_
# definition_id), jamais partagé avec le Répéteur/la Jauge/Donnée liée.
@@ -67,12 +80,16 @@ def controls_with_values(slug, el):
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["key"] == "content" and attributes.get("_data_definition_id"):
+ 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
# liste déroulante à côté du texte — plutôt que de devoir taper
# "{{nom_du_champ}}" à la main sans savoir quels noms existent
- # (voir insertFieldAtCursor() dans screen_edit.html).
- control["field_options"] = [(f["name"], _field_label(f)) for f in data_binding_options["fields"]]
+ # (voir insertFieldAtCursor() dans screen_edit.html). À défaut,
+ # retombe sur l'objet lié à l'élément de jeu (voir docstring).
+ if attributes.get("_data_definition_id"):
+ control["field_options"] = [(f["name"], _field_label(f)) for f in data_binding_options["fields"]]
+ elif template_bound_options:
+ control["field_options"] = [(f["name"], _field_label(f)) for f in template_bound_options["fields"]]
result.append({**control, "value": _control_value(el, control)})
return result
diff --git a/tests/test_element_type_bound_field_picker.py b/tests/test_element_type_bound_field_picker.py
new file mode 100644
index 00000000..a1870d9a
--- /dev/null
+++ b/tests/test_element_type_bound_field_picker.py
@@ -0,0 +1,76 @@
+"""Sélecteur de champ du panneau de propriétés ("+ Ajouter" à côté de
+"Contenu", voir insertFieldAtCursor() dans screen_edit.html) : doit
+apparaître dès qu'un objet est disponible dans le contexte, pas seulement
+quand le widget a lui-même une "Donnée liée" réglée."""
+import re
+
+import screens
+
+
+def _create_email_object(client, slug):
+ resp = client.post(f"/game/{slug}/objects/new", data={
+ "object_name": "Email", "field_name[]": ["sujet"], "field_type[]": ["texte"],
+ "field_relation[]": [""], "field_required[]": ["0"], "field_min[]": [""], "field_max[]": [""],
+ }, follow_redirects=False)
+ return int(resp.headers["Location"].rstrip("/").split("/")[-1])
+
+
+def test_new_widget_inside_a_bound_element_type_gets_the_field_picker_without_extra_setup(client, game):
+ """Régression : une "Mail card" (élément de jeu créé avec "Objet lié" =
+ Email) doit proposer le sélecteur de champ sur tout nouveau Titre/Texte
+ posé dans son écran-modèle, sans qu'il faille en plus régler "Lier à un
+ objet de données" sur CE widget précis — l'objet lié à l'élément de jeu
+ suffit."""
+ def_id = _create_email_object(client, game)
+ resp = client.post(f"/game/{game}/element-types", data={
+ "name": "Mail card", "icon": "📧", "bound_definition_id": str(def_id),
+ }, follow_redirects=False)
+ assert resp.status_code == 302
+ et = next(t for t in screens.list_element_types(game) if t["name"] == "Mail card")
+ template_screen_id = et["template_screen_id"]
+
+ resp = client.post(f"/game/{game}/screens/{template_screen_id}/elements/add", data={"widget": "titre"}, follow_redirects=False)
+ element_id = int(re.search(r"selected=(\d+)", resp.headers["Location"]).group(1))
+
+ html = client.get(f"/game/{game}/screens/{template_screen_id}/edit?selected={element_id}").data.decode()
+ assert 'id="fieldPicker-content"' in html
+ assert '