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
+40 -10
View File
@@ -16,16 +16,27 @@ jamais, et renvoie toujours un dict complet.
Structure d'un scénario : {"title": str, "nodes": [...]}. `nodes[0]` est
TOUJOURS la situation initiale (racine de l'arbre, jamais supprimable
depuis le panneau Propriétés). Chaque nœud est
{"id": str, "text": str, "choices": [...]} — `id` est une référence
STABLE (générée côté client, jamais recalculée ici) vers laquelle un
choix d'un AUTRE nœud peut pointer via son `target_id` ; un nœud sans
choix est une fin de branche. Un choix est {"text": str,
"target_id": str | None} — `target_id` à None signifie "fin de branche
à ce choix" (aucun nœud suivant)."""
{"id": str, "text": str, "x": float, "y": float, "choices": [...]} —
`id` est une référence STABLE (générée côté client, jamais recalculée
ici) vers laquelle un choix d'un AUTRE nœud peut pointer via son
`target_id` ; un nœud sans choix est une fin de branche. `x`/`y`
positionnent le nœud sur le canevas du graphe visuel (voir
document_engine/rendering/render_document_element.py::
_render_scenario_player et forgeDocRenderScenarioGraph, static/document/
js/document-editor.js) — une position manquante/invalide retombe sur un
quadrillage en cascade calculé depuis l'INDEX du nœud dans la liste
(_scenario_node_fallback_position), jamais (0, 0) pour tous les nœuds,
ce qui les empilerait exactement au même endroit. Un choix est
{"text": str, "target_id": str | None} — `target_id` à None signifie
"fin de branche à ce choix" (aucun nœud suivant)."""
from typing import Any
MAX_SCENARIO_CHOICES = 4
_NODE_GRID_COLUMNS = 4
_NODE_GRID_STEP_X = 220
_NODE_GRID_STEP_Y = 160
_NODE_GRID_MARGIN = 40
DEFAULT_SCENARIO_CONFIG: dict[str, Any] = {
"theme_color": "#ff5f2e",
@@ -48,7 +59,22 @@ def _sanitize_scenario_choice(raw: Any) -> dict[str, Any] | None:
return {"text": text, "target_id": target_id if isinstance(target_id, str) and target_id else None}
def _sanitize_scenario_node(raw: Any) -> dict[str, Any] | None:
def _scenario_node_fallback_position(index: int) -> tuple[float, float]:
"""Position par défaut d'un nœud dont x/y est manquant/invalide — un
quadrillage en cascade dérivé de son INDEX dans la liste, jamais
(0, 0) pour tous les nœuds (qui les empilerait exactement au même
endroit, rendant le graphe visuel illisible à la première ouverture
d'un scénario créé avant l'ajout de x/y au modèle, ou d'un nœud
ajouté par un client qui n'enverrait pas encore de position)."""
col = index % _NODE_GRID_COLUMNS
row = index // _NODE_GRID_COLUMNS
return (
_NODE_GRID_MARGIN + col * _NODE_GRID_STEP_X,
_NODE_GRID_MARGIN + row * _NODE_GRID_STEP_Y,
)
def _sanitize_scenario_node(raw: Any, index: int) -> dict[str, Any] | None:
"""None si le nœud est invalide (id absent, ou texte vide) — même
convention que les autres sanitize_X de ce module. Un nœud SANS choix
est parfaitement valide (fin de branche), contrairement à l'ancien
@@ -67,7 +93,11 @@ def _sanitize_scenario_node(raw: Any) -> dict[str, Any] | None:
if isinstance(raw_choices, list):
choices = [c for c in (_sanitize_scenario_choice(item) for item in raw_choices) if c is not None]
choices = choices[:MAX_SCENARIO_CHOICES]
return {"id": node_id, "text": text, "choices": choices}
fallback_x, fallback_y = _scenario_node_fallback_position(index)
raw_x, raw_y = raw.get("x"), raw.get("y")
x = raw_x if isinstance(raw_x, (int, float)) and not isinstance(raw_x, bool) else fallback_x
y = raw_y if isinstance(raw_y, (int, float)) and not isinstance(raw_y, bool) else fallback_y
return {"id": node_id, "text": text, "x": x, "y": y, "choices": choices}
def _sanitize_scenario(raw: Any) -> dict[str, Any] | None:
@@ -87,10 +117,10 @@ def _sanitize_scenario(raw: Any) -> dict[str, Any] | None:
raw_nodes = raw.get("nodes")
if not isinstance(raw_nodes, list):
return None
nodes = []
nodes: list[dict[str, Any]] = []
seen_ids: set[str] = set()
for item in raw_nodes:
node = _sanitize_scenario_node(item)
node = _sanitize_scenario_node(item, len(nodes))
if node is None or node["id"] in seen_ids:
continue
seen_ids.add(node["id"])