Remplace le confirm() natif par une modale custom qui avertit des suppressions en cascade

Depuis le dernier correctif, supprimer un élément supprime aussi en
silence tout nœud de la Logique de la scène qui le référence (déclencheur/
action, voir delete_element.py) - nécessaire pour éviter le plantage
"FOREIGN KEY constraint failed", mais l'utilisateur n'était jamais prévenu
qu'un bout de sa logique disparaissait en même temps.

Nouvelle route GET .../elements/<id>/delete-impact (element_delete_
impact.py) : calcule, sans rien supprimer, combien de nœuds de la Logique
de la scène référencent cet élément OU l'un de ses descendants (partage
element_descendant_ids.py avec delete_element.py, pour rester exactement
cohérent avec ce qui sera réellement supprimé).

Les deux boutons "supprimer" de screen_edit.html (élément sélectionné, et
onglet d'un widget Onglets) ouvrent maintenant une modale custom
(deleteConfirmModal, même famille que le sélecteur d'icônes) au lieu du
confirm() natif du navigateur : elle interroge cette route juste après
ouverture et affiche un avertissement dédié si le nombre remonté est non
nul, avant que l'utilisateur ne confirme quoi que ce soit - impossible à
faire avec confirm(), dont le texte est figé au moment du rendu de la
page. La confirmation soumet ensuite le formulaire normalement (via
requestSubmit(), intercepté par pjax.js comme n'importe quel autre
formulaire).

Ajoute deux tests pour la nouvelle route (impact nul, impact non nul sans
rien supprimer).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
william
2026-08-25 16:27:34 +02:00
co-authored by Claude Sonnet 5
parent 1e86077e45
commit 6894c5fc95
8 changed files with 181 additions and 28 deletions
+1
View File
@@ -80,6 +80,7 @@ from .elements.reorder_element import reorder_element
from .elements.move_element_to_container import move_element_to_container
from .elements.save_element_controls import save_element_controls
from .elements.delete_element import delete_element
from .elements.flow_nodes_referencing_element import flow_nodes_referencing_element
from .elements.duplicate_element import duplicate_element
from .elements.rename_element import rename_element
from .elements.element_label import element_label
+5 -26
View File
@@ -1,30 +1,7 @@
import db
from ..flow.ensure_flow_schema import ensure_flow_schema
def _collect_ids(conn, screen_id, root_id):
"""Rassemble l'id de cet élément et de TOUS ses descendants — ceux
qu'une suppression en cascade (parent_id ON DELETE CASCADE, voir
ensure_schema.py) va de toute façon supprimer automatiquement en même
temps, sans passer par ce fichier. Nécessaire pour nettoyer AUSSI les
références de la Logique de la scène vers un simple DESCENDANT, pas
seulement vers l'élément explicitement supprimé."""
rows = conn.execute(
"SELECT id, parent_id FROM _screen_elements WHERE screen_id = ?", (screen_id,)
).fetchall()
children_of = {}
for r in rows:
if r["parent_id"]:
children_of.setdefault(r["parent_id"], []).append(r["id"])
ids = [root_id]
stack = [root_id]
while stack:
current = stack.pop()
for child_id in children_of.get(current, []):
ids.append(child_id)
stack.append(child_id)
return ids
from .element_descendant_ids import element_descendant_ids
def delete_element(slug, element_id):
@@ -34,7 +11,7 @@ def delete_element(slug, element_id):
if not row:
conn.close()
return
ids = _collect_ids(conn, row["screen_id"], element_id)
ids = element_descendant_ids(conn, row["screen_id"], element_id)
placeholders = ",".join("?" * len(ids))
# Un nœud de la Logique de la scène (déclencheur "clic sur cet
# élément"/action "Modifier cet élément"...) ou une ancienne action du
@@ -45,7 +22,9 @@ def delete_element(slug, element_id):
# élément ne doit pas pouvoir être supprimé "par erreur" en cascade
# depuis un nœud de logique qu'on modifie). Sans ce nettoyage préalable,
# PRAGMA foreign_keys=ON (voir db/connection.py) fait échouer la
# suppression elle-même avec "FOREIGN KEY constraint failed".
# suppression elle-même avec "FOREIGN KEY constraint failed". Voir
# element_delete_impact.py pour prévenir l'utilisateur AVANT qu'il
# confirme, plutôt que de supprimer ces nœuds en silence.
conn.execute(
f"DELETE FROM _flow_nodes WHERE trigger_element_id IN ({placeholders}) OR target_element_id IN ({placeholders})",
ids + ids,
@@ -0,0 +1,23 @@
def element_descendant_ids(conn, screen_id, root_id):
"""Id de cet élément et de TOUS ses descendants — ceux qu'une
suppression en cascade (parent_id ON DELETE CASCADE, voir
ensure_schema.py) supprime automatiquement en même temps que lui.
Partagé entre delete_element.py (nettoyage des références de la
Logique de la scène avant suppression) et flow_nodes_referencing_
element.py (pour prévenir l'utilisateur AVANT qu'il confirme la
suppression, voir routes/elements/element_delete_impact.py)."""
rows = conn.execute(
"SELECT id, parent_id FROM _screen_elements WHERE screen_id = ?", (screen_id,)
).fetchall()
children_of = {}
for r in rows:
if r["parent_id"]:
children_of.setdefault(r["parent_id"], []).append(r["id"])
ids = [root_id]
stack = [root_id]
while stack:
current = stack.pop()
for child_id in children_of.get(current, []):
ids.append(child_id)
stack.append(child_id)
return ids
@@ -0,0 +1,28 @@
import db
from ..flow.ensure_flow_schema import ensure_flow_schema
from .element_descendant_ids import element_descendant_ids
def flow_nodes_referencing_element(slug, element_id):
"""Nombre de nœuds de la Logique de la scène (déclencheur "clic sur cet
élément", action "Modifier cet élément"/"Activer cet onglet"...) qui
référencent cet élément OU L'UN DE SES DESCENDANTS — donc qui seraient
supprimés SI on supprimait cet élément (voir delete_element.py). Utilisé
pour prévenir l'utilisateur AVANT qu'il confirme la suppression (voir
la modale de confirmation dans screen_edit.html), plutôt que de les
supprimer en silence."""
ensure_flow_schema(slug)
conn = db.connect(slug)
row = conn.execute("SELECT screen_id FROM _screen_elements WHERE id = ?", (element_id,)).fetchone()
if not row:
conn.close()
return 0
ids = element_descendant_ids(conn, row["screen_id"], element_id)
placeholders = ",".join("?" * len(ids))
count = conn.execute(
f"SELECT COUNT(*) AS c FROM _flow_nodes WHERE trigger_element_id IN ({placeholders}) OR target_element_id IN ({placeholders})",
ids + ids,
).fetchone()["c"]
conn.close()
return count