Remplace la modale liste+menus deroulants du Scenario par un vrai graphe visuel
Build and deploy / test-python (push) Successful in 7m32s
Build and deploy / test-js (push) Successful in 49s
Build and deploy / lint-python (push) Successful in 5m21s
Build and deploy / lint-js (push) Failing after 1m12s
Build and deploy / build-and-push (push) Skipped
Build and deploy / deploy (push) Skipped
Build and deploy / sonarqube (push) Failing after 3m53s

Retour utilisateur : "passons a un veritable graphe visuel". Chaque
nœud gagne une position x/y (document_engine/labels/scenario_config.py
: un x/y manquant/invalide retombe sur un quadrillage en cascade derive
de l'index du nœud, jamais (0, 0) pour tous les nœuds qui les
empilerait au meme endroit).

Cote editeur (static/document/js/document-editor.js) : les nœuds sont
des cartes deplacables a la souris sur un canevas (glisser-deposer reel,
meme principe que le glisser des formes libres), les choix relies a une
cible sont dessines comme des fleches SVG etiquetees par leur texte
(jamais un menu deroulant). Editer le texte/les choix d'un nœud se fait
dans un panneau inspecteur (colonne de droite) pour le nœud
selectionne ; relier un choix se fait en cliquant "Relier" puis le nœud
cible sur le graphe (mode connexion, Echap annule sans fermer la
modale). La modale generique (.docModal*) est agrandie specifiquement
pour ce graphe (jusqu'a 1180px) sans toucher sa taille par defaut.

Verifie via simulation DOM reelle (jsdom) : rendu des nœuds/positions,
glisser-deposer avec persistance au relachement, traces des fleches SVG
+ etiquettes, workflow complet du mode connexion, suppression d'un nœud
avec reparation des references pendantes, et les 3 façons de fermer la
modale (bouton/fond/Echap) y compris l'annulation du mode connexion
sans fermer.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
william
2026-09-21 09:03:47 +02:00
co-authored by Claude Sonnet 5
parent 7e504b7865
commit d92f75a803
6 changed files with 777 additions and 159 deletions
+63 -3
View File
@@ -1,7 +1,7 @@
"""Modèle de données du mini-jeu Scénario
(document_engine/labels/scenario_config.py) — sans Flask, teste
directement sanitize_scenario_config (arbre de décision), et le rendu
du plateau."""
directement sanitize_scenario_config (arbre de décision, graphe visuel
avec positions x/y), et le rendu du plateau."""
from typing import Any
@@ -22,12 +22,14 @@ def test_sanitize_scenario_config_keeps_a_valid_tree_as_is() -> None:
{
"id": "n1",
"text": "Un collègue vous demande son mot de passe.",
"x": 40,
"y": 60,
"choices": [
{"text": "Le lui donner", "target_id": "n2"},
{"text": "Refuser", "target_id": None},
],
},
{"id": "n2", "text": "Violation de sécurité.", "choices": []},
{"id": "n2", "text": "Violation de sécurité.", "x": 300, "y": 60, "choices": []},
],
}
],
@@ -37,6 +39,64 @@ def test_sanitize_scenario_config_keeps_a_valid_tree_as_is() -> None:
assert config["scenarios"] == raw["scenarios"]
def test_sanitize_scenario_config_keeps_explicit_xy_position() -> None:
raw = {
"scenarios": [
{"title": "Titre", "nodes": [{"id": "n1", "text": "Situation", "x": 500, "y": 300, "choices": []}]}
]
}
config = doc_engine.sanitize_scenario_config(raw)
node = config["scenarios"][0]["nodes"][0]
assert (node["x"], node["y"]) == (500, 300)
def test_sanitize_scenario_config_falls_back_to_a_cascading_grid_when_xy_is_missing() -> None:
"""Jamais (0, 0) pour tous les nœuds (qui les empilerait exactement au
même endroit sur le graphe visuel) : une position par défaut dérivée
de l'INDEX du nœud dans la liste."""
raw = {
"scenarios": [
{
"title": "Titre",
"nodes": [
{"id": "n1", "text": "A", "choices": []},
{"id": "n2", "text": "B", "choices": []},
],
}
]
}
config = doc_engine.sanitize_scenario_config(raw)
nodes = config["scenarios"][0]["nodes"]
assert (nodes[0]["x"], nodes[0]["y"]) != (nodes[1]["x"], nodes[1]["y"])
def test_sanitize_scenario_config_falls_back_to_grid_when_xy_is_not_a_number() -> None:
raw = {
"scenarios": [
{"title": "Titre", "nodes": [{"id": "n1", "text": "Situation", "x": "beaucoup", "y": None, "choices": []}]}
]
}
config = doc_engine.sanitize_scenario_config(raw)
node = config["scenarios"][0]["nodes"][0]
assert isinstance(node["x"], (int, float))
assert isinstance(node["y"], (int, float))
def test_sanitize_scenario_config_rejects_a_boolean_as_xy() -> None:
"""True/False sont des int en Python (bool hérite de int) — même
garde-fou que pour correct_index dans l'ancien modèle : un JSON
malformé pourrait glisser `true` là où une coordonnée est attendue."""
raw = {
"scenarios": [
{"title": "Titre", "nodes": [{"id": "n1", "text": "Situation", "x": True, "y": False, "choices": []}]}
]
}
config = doc_engine.sanitize_scenario_config(raw)
node = config["scenarios"][0]["nodes"][0]
assert node["x"] is not True
assert node["y"] is not False
def test_sanitize_scenario_config_drops_a_scenario_with_empty_title() -> None:
raw = {"scenarios": [{"title": " ", "nodes": [{"id": "n1", "text": "Situation", "choices": []}]}]}
assert doc_engine.sanitize_scenario_config(raw)["scenarios"] == []