From f436190f909c9e37afa2ed1d727a00dcaaa4b3d9 Mon Sep 17 00:00:00 2001 From: william Date: Sat, 29 Aug 2026 19:34:24 +0200 Subject: [PATCH] =?UTF-8?q?Ajoute=20les=20=C3=A9v=C3=A9nements=20personnal?= =?UTF-8?q?is=C3=A9s=20(=C3=A9diteur=20+=20ex=C3=A9cution)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Deuxième moitié de la fonctionnalité "événements" (voir le commit précédent pour le backend) : un nouvel onglet "📣 Événements" dans screen_edit.html (scène ET modèle, même route) pour créer/modifier/ supprimer un événement (nom, description, "a un paramètre élément" oui/ non) et voir où il est déjà écouté/déclenché ; deux nouveaux types de nœud dans le graphe de logique ; exécution réelle côté client (play.html). - templates/screen_edit.html : - Onglet Événements : barre de création + tableau (patron form/name="..." + attribut `form="eventEditFormN"` déjà utilisé par l'onglet Variables du tableau de bord, pour éditer plusieurs champs d'une ligne sans emboîter un
dans un ). Colonne "Utilisé par" avec liens directs vers les écrans/modèles concernés (list_custom_event_usages, déjà en place côté backend). - Nœud Déclencheur "Sur un événement personnalisé" : un simple sélecteur d'événement (trigger_custom_event_id) — ni élément ni écran, retrouvé par un scan global côté client (comme "À l'affichage de l'écran"). - Nœud Action "Déclencher un événement" : sélecteur d'événement (target_custom_event_id), avec élément/ligne concernés affichés seulement si l'événement a has_element_param (réutilise le sélecteur d'élément existant + "Ligne cliquée" pour la ligne, pas de définition d'objet ici donc pas de liste de lignes fixes possible). - Nœud Action "Modifier un élément" : nouvelle case "Utiliser l'élément transmis par l'événement en cours" (target_element_from_event) — v1, seule cette action l'expose (extensible plus tard sans nouveau changement de schéma). - nodeLabel()/submitNodeForm()/toggleFlowTriggerFields()/ toggleFlowActionFields() étendus en conséquence, CUSTOM_EVENTS_MAP (id -> nom/has_element_param) exposé côté JS pour le rendu des libellés et l'affichage conditionnel des champs paramètre. - templates/play.html : - window.dispatchGameEvent(eventId, elementId, rowId) : scan de gameData.flows (toutes scènes ET modèles à la fois, même principe que findTriggerNode()/runScreenShowTriggers()) pour trouver chaque écouteur, pose window.lastEventParams puis exécute son graphe (runFlowFrom) — au même titre que window.lastClickedRowId pour "Ligne cliquée". - runActionNode : nouvelle branche "declencher_evenement" (résout CLICKED_ROW_ID pour la ligne transmise, comme "Modifier une donnée") ; branche "modifier_element" étendue pour résoudre dynamiquement l'élément depuis window.lastEventParams quand target_element_from_event est actif. - readFieldValue (évaluation de condition) et l'appel serveur de "Modifier une donnée" (routes/flow/flow_node_run_data.py) résolvent désormais aussi EVENT_ROW_ID (-2), au même endroit que CLICKED_ROW_ID (-1) déjà en place. - routes/screens/screen_edit.py : passe custom_events/ custom_event_usages/custom_events_map_json au template (même patron que global_variables déjà threadé pour le sélecteur de variable dans le formulaire de Condition). Nouveaux tests dans tests/test_custom_events.py : forme exacte du payload runtime exposé au JS (types entiers, pas des chaînes — une comparaison stricte "===" échouerait silencieusement sinon), target_element_from_event bien persisté, résolution serveur d' EVENT_ROW_ID depuis le corps de la requête. 210 tests au total. Co-Authored-By: Claude Sonnet 5 --- routes/flow/flow_node_run_data.py | 11 +- routes/screens/screen_edit.py | 8 ++ templates/play.html | 60 ++++++++- templates/screen_edit.html | 214 ++++++++++++++++++++++++++++-- tests/test_custom_events.py | 85 ++++++++++++ 5 files changed, 361 insertions(+), 17 deletions(-) diff --git a/routes/flow/flow_node_run_data.py b/routes/flow/flow_node_run_data.py index 0d362df8..ef233cd9 100644 --- a/routes/flow/flow_node_run_data.py +++ b/routes/flow/flow_node_run_data.py @@ -13,14 +13,19 @@ def flow_node_run_data(slug, node_id): un nœud du graphe de logique plutôt que depuis la table _actions. Si le nœud vise CLICKED_ROW_ID (voir "Ligne cliquée (Répéteur)" dans - screen_edit.html), la vraie ligne n'est connue qu'au moment du clic — - le client (play.html) la transmet dans le corps JSON de cette requête, - et elle remplace ici target_row_id avant d'exécuter l'action.""" + screen_edit.html) ou EVENT_ROW_ID (la ligne transmise par un événement + personnalisé en cours, voir screens/custom_events/), la vraie ligne + n'est connue qu'au moment de l'exécution — le client (play.html) la + transmet dans le corps JSON de cette requête, et elle remplace ici + target_row_id avant d'exécuter l'action.""" node = screens.get_flow_node(slug, node_id) if not node or node.get("node_type") != "action" or node.get("action_type") != "modifier_donnee": return jsonify({"ok": False}), 400 if node.get("target_row_id") == screens.CLICKED_ROW_ID: body = request.get_json(silent=True) or {} node = {**node, "target_row_id": body.get("clicked_row_id")} + elif node.get("target_row_id") == screens.EVENT_ROW_ID: + body = request.get_json(silent=True) or {} + node = {**node, "target_row_id": body.get("event_row_id")} ok = screens.apply_data_action(slug, node) return jsonify({"ok": ok}) diff --git a/routes/screens/screen_edit.py b/routes/screens/screen_edit.py index f0c17600..d134fb25 100644 --- a/routes/screens/screen_edit.py +++ b/routes/screens/screen_edit.py @@ -44,6 +44,12 @@ def screen_edit(slug, screen_id): screens_map = {s["id"]: s["name"] for s in all_screens} animation_clips = screens.list_animation_clips(slug, screen_id) global_variables = db.list_global_variables(slug) + custom_events = db.list_custom_events(slug) + custom_event_usages = screens.list_custom_event_usages(slug) + custom_events_map = { + e["id"]: {"name": e["name"], "has_element_param": bool(e["has_element_param"])} + for e in custom_events + } return render_template( "screen_edit.html", @@ -80,4 +86,6 @@ def screen_edit(slug, screen_id): animate_css_labels_json=json.dumps(screens.ANIMATE_CSS_LABELS), fontawesome_icons=screens.FONTAWESOME_ICONS, global_variables=global_variables, + custom_events=custom_events, custom_event_usages=custom_event_usages, + custom_events_map_json=json.dumps(custom_events_map), ) diff --git a/templates/play.html b/templates/play.html index 43d0ff16..5d14cb78 100644 --- a/templates/play.html +++ b/templates/play.html @@ -788,10 +788,25 @@ bindHoverTexts(); bindHoverTriggers(); return Promise.resolve(); - } else if (node.action_type === 'modifier_element' && node.target_element_id) { - const targetEl = document.querySelector('[data-element-id="' + node.target_element_id + '"]'); + } else if (node.action_type === 'modifier_element' && (node.target_element_id || node.target_element_from_event)) { + // target_element_from_event (voir screens/custom_events/) : résout + // dynamiquement "l'élément transmis par l'événement en cours" + // (window.lastEventParams, posé par dispatchGameEvent() juste avant + // d'exécuter ce graphe) plutôt qu'une cible fixe choisie dans + // l'éditeur — contourne la contrainte de clé étrangère de + // target_element_id, qui empêche d'y stocker un sentinel comme + // EVENT_ROW_ID directement (voir screens/flow/ensure_flow_schema.py). + const resolvedElementId = node.target_element_from_event + ? (window.lastEventParams ? window.lastEventParams.element_id : null) + : node.target_element_id; + const targetEl = resolvedElementId ? document.querySelector('[data-element-id="' + resolvedElementId + '"]') : null; if (targetEl) applyElementProperty(targetEl, node.element_property, node.element_value); return Promise.resolve(); + } else if (node.action_type === 'declencher_evenement' && node.target_custom_event_id) { + let dispatchRowId = node.target_row_id; + if (dispatchRowId === -1) dispatchRowId = window.lastClickedRowId; // CLICKED_ROW_ID + window.dispatchGameEvent(node.target_custom_event_id, node.target_element_id || null, dispatchRowId != null ? dispatchRowId : null); + return Promise.resolve(); } else if (node.action_type === 'activer_onglet' && node.target_element_id) { // 2.3 — panneau à onglets / visibilité mutuellement exclusive : montre // l'élément visé et masque tous ses FRÈRES DIRECTS (même parent_id, @@ -827,12 +842,16 @@ return new Promise(function(resolve) { setTimeout(resolve, ms); }); } else if (node.action_type === 'modifier_donnee') { const runDataUrl = "{{ url_for('flow_node_run_data', slug=game.slug, node_id=999999) }}".replace('999999', node.id); - // clicked_row_id : utilisé côté serveur SEULEMENT si ce nœud vise la - // ligne cliquée (CLICKED_ROW_ID, voir flow_node_run_data.py) — sans - // effet sinon, donc toujours transmis sans condition ici. + // clicked_row_id/event_row_id : utilisés côté serveur SEULEMENT si ce + // nœud vise respectivement CLICKED_ROW_ID ou EVENT_ROW_ID (voir + // flow_node_run_data.py) — sans effet sinon, donc toujours transmis + // sans condition ici. return fetch(runDataUrl, { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ clicked_row_id: window.lastClickedRowId }) + body: JSON.stringify({ + clicked_row_id: window.lastClickedRowId, + event_row_id: window.lastEventParams ? window.lastEventParams.row_id : null, + }) }) .then(r => r.json()) .then(() => refreshRuntimeData()); @@ -855,6 +874,12 @@ // résolue seulement au moment de l'évaluation via le dernier clic // capturé (voir bindClicks() plus haut). if (rowId === -1) rowId = window.lastClickedRowId; + // EVENT_ROW_ID (-2, voir screens/flow/constants.py) : "la ligne de + // Répéteur transmise par l'événement personnalisé en cours" — même + // principe que CLICKED_ROW_ID ci-dessus, résolue via + // window.lastEventParams (posé par dispatchGameEvent()) plutôt que le + // dernier clic capturé. + if (rowId === -2) rowId = window.lastEventParams ? window.lastEventParams.row_id : null; const rows = gameData.data[String(definitionId)] || []; const row = rows.find(r => r.id === rowId); return row ? row[fieldName] : undefined; @@ -1008,6 +1033,29 @@ step(nodeId); } + // Déclenche un événement personnalisé (voir screens/custom_events/, + // action "declencher_evenement") : scan GLOBAL de gameData.flows, même + // principe que findTriggerNode()/runScreenShowTriggers() ci-dessus — un + // écouteur (nœud Déclencheur trigger_event="evenement") peut vivre sur + // N'IMPORTE QUELLE autre scène ou modèle, pas seulement l'écran affiché. + // elementId/rowId (potentiellement null si l'événement n'a pas de + // paramètre) sont posés sur window.lastEventParams AVANT de lancer + // chaque graphe trouvé, pour que les nœuds en aval (ex. "Modifier un + // élément" avec target_element_from_event, voir applyElementProperty + // plus haut/runActionNode plus bas) puissent le résoudre — au même + // titre que window.lastClickedRowId pour "Ligne cliquée (Répéteur)". + window.dispatchGameEvent = function(eventId, elementId, rowId) { + for (const screenIdKey in gameData.flows) { + const flow = gameData.flows[screenIdKey]; + flow.nodes + .filter(function(n){ return n.node_type === 'trigger' && n.trigger_event === 'evenement' && n.trigger_custom_event_id === eventId; }) + .forEach(function(n) { + window.lastEventParams = { element_id: elementId, row_id: rowId }; + runFlowFrom(n.id, screenIdKey); + }); + } + }; + // L'écran affiché au démarrage doit être choisi AVANT le premier // bindClicks() : la recherche du nœud Déclencheur associé à un élément se // fait sur l'écran courant (window.currentScreenId), qui doit donc déjà diff --git a/templates/screen_edit.html b/templates/screen_edit.html index e634c70e..a3139b57 100644 --- a/templates/screen_edit.html +++ b/templates/screen_edit.html @@ -172,6 +172,7 @@ +
@@ -216,6 +217,16 @@ +
@@ -470,6 +510,76 @@
+ + +
+
+

Événements personnalisés

+ +
+ + + + + + + + + {% if custom_events %} + {% for ev in custom_events %} +
+ +
+ {% endfor %} +
+ + + + {% for ev in custom_events %} + + + + + + + + {% endfor %} + +
NomDescriptionParamètreUtilisé par
{{ ev.name }} + + + {% set usages = custom_event_usages.get(ev.id, []) %} + {% if usages %} + {% for u in usages %} + {{ '🔔' if u.role == 'ecoute' else '📣' }} {{ u.screen_name }}{{ ' (modèle)' if u.is_template else '' }}{{ ', ' if not loop.last else '' }} + {% endfor %} + {% else %} + Inutilisé + {% endif %} + + +
+ + +
+
+
+ {% else %} +

Aucun événement pour l'instant — crée le premier ci-dessus, puis pose un nœud "Sur un événement personnalisé" (Déclencheur) ou "Déclencher un événement" (Action) dans la Logique de la scène.

+ {% endif %} +