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
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:
co-authored by
Claude Sonnet 5
parent
7e504b7865
commit
d92f75a803
@@ -198,14 +198,17 @@ convention resolve_X/sanitize_X que `quiz_config.py`.
|
||||
**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 — voir
|
||||
`forgeDocRenderScenarioNodeHtml`, static/document/js/document-editor.js).
|
||||
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 côté
|
||||
serveur) 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 (état normal,
|
||||
pas une erreur — contrairement à l'ancien modèle qui exigeait 2 à 4
|
||||
choix par scénario). Un choix est `{"text": str, "target_id": str |
|
||||
None}` — `target_id` à `None` signifie "fin de branche à ce choix".
|
||||
`forgeDocScenarioGraphRenderInspector`, static/document/js/
|
||||
document-editor.js). Chaque nœud est `{"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 côté serveur) vers
|
||||
laquelle un choix d'un AUTRE nœud peut pointer via son `target_id` ;
|
||||
`x`/`y` positionnent le nœud sur le canevas du graphe visuel (voir
|
||||
`forgeDocRenderScenarioTreeModal`) ; un nœud SANS choix est une fin de
|
||||
branche (état normal, pas une erreur — contrairement à l'ancien modèle
|
||||
qui exigeait 2 à 4 choix par scénario). Un choix est `{"text": str,
|
||||
"target_id": str | None}` — `target_id` à `None` signifie "fin de
|
||||
branche à ce choix".
|
||||
|
||||
### `MAX_SCENARIO_CHOICES: int`
|
||||
Borne technique du nombre de choix par nœud (`4`) — évite une UI
|
||||
@@ -230,8 +233,13 @@ est validé indépendamment (voir `_sanitize_scenario`/
|
||||
`_sanitize_scenario_node`/`_sanitize_scenario_choice`, privées) : le
|
||||
`title` doit être non vide, chaque nœud doit avoir un `id` (chaîne non
|
||||
vide) et un `text` non vide une fois `.strip()`-é (sinon le nœud entier
|
||||
est supprimé), et ses choix sont tronqués à `MAX_SCENARIO_CHOICES`. Le
|
||||
scénario entier est supprimé s'il ne reste plus aucun nœud valide après
|
||||
est supprimé), et ses choix sont tronqués à `MAX_SCENARIO_CHOICES`. Un
|
||||
`x`/`y` manquant ou invalide (pas un nombre, ou un booléen — `bool`
|
||||
hérite de `int` en Python) retombe sur un quadrillage en cascade dérivé
|
||||
de 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 sur le graphe visuel. Le scénario entier est supprimé s'il
|
||||
ne reste plus aucun nœud valide après
|
||||
nettoyage (il faut au moins la situation initiale). Une fois l'ensemble
|
||||
des ids valides connu, tout `target_id` qui ne pointe plus vers un nœud
|
||||
existant (nœud invalide/supprimé) est silencieusement remis à `None`
|
||||
|
||||
@@ -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"])
|
||||
|
||||
@@ -130,7 +130,16 @@ regroupement à chaque appel.
|
||||
dans une MODALE dédiée depuis le panneau Propriétés (voir
|
||||
`forgeDocOpenScenarioTreeModal`, static/document/js/document-editor.js)
|
||||
— trop de structure (nœuds + choix + destinations) pour la colonne
|
||||
étroite du panneau Propriétés, contrairement aux autres mini-jeux.
|
||||
étroite du panneau Propriétés, contrairement aux autres mini-jeux. La
|
||||
modale est un VRAI graphe visuel (retour utilisateur du 21/09/2026) :
|
||||
chaque nœud a une position `x`/`y` (voir `scenario_config.py`),
|
||||
affiché comme une carte déplaçable à la souris sur un canevas ; chaque
|
||||
choix relié à un `target_id` est dessiné comme une flèche SVG
|
||||
étiquetée par son texte, jamais un simple menu déroulant. Éditer le
|
||||
texte/les choix d'un nœud se fait dans l'inspecteur (panneau de
|
||||
droite) du nœud sélectionné ; relier un choix se fait en cliquant
|
||||
"Relier" puis le nœud cible sur le graphe (mode connexion, Échap
|
||||
annule).
|
||||
- **Autres mini-jeux** (`zones`) : carte placeholder portant le libellé
|
||||
du type (voir `document_engine/labels/element_kind_labels.py`) —
|
||||
emplacement réservé, formulaire de contenu dédié hors périmètre de
|
||||
|
||||
Reference in New Issue
Block a user