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
+82 -2
View File
@@ -487,6 +487,30 @@
</section>
</div>
</div>
<!-- Confirmation de suppression d'un élément — remplace le confirm()
natif du navigateur pour pouvoir avertir, le cas échéant, que des
nœuds de la Logique de la scène seront aussi supprimés (voir
handleDeleteElementSubmit()/element_delete_impact.py) : le
confirm() natif ne pouvait afficher qu'un texte figé au moment du
rendu, jamais une info calculée après coup. -->
<div class="modal" id="deleteConfirmModal">
<div class="modal-background" onclick="closeDeleteConfirmModal()"></div>
<div class="modal-card">
<header class="modal-card-head">
<p class="modal-card-title">🗑️ <span id="deleteConfirmMessage">Supprimer cet élément ?</span></p>
<button class="delete" aria-label="Fermer" onclick="closeDeleteConfirmModal()"></button>
</header>
<section class="modal-card-body">
<p class="hint">Cette action est irréversible.</p>
<p class="hint" id="deleteConfirmFlowWarning" style="display:none; margin-top:10px; color:var(--danger, #e2685f);"></p>
</section>
<footer class="modal-card-foot" style="justify-content:flex-end; gap:8px;">
<button type="button" class="button is-small" onclick="closeDeleteConfirmModal()">Annuler</button>
<button type="button" class="button is-small is-danger" onclick="confirmDeleteElement()">🗑️ Supprimer</button>
</footer>
</div>
</div>
{% if not selected_id %}
<div class="hint" style="padding:20px 0;">Sélectionne un élément (sur l'écran ou dans la liste) pour voir et modifier ses propriétés ici.</div>
{% else %}
@@ -496,7 +520,7 @@
<div class="fieldRow" style="align-items:center; justify-content:space-between;">
<h2 class="subtitle is-6" style="margin:0;">{{ w.get('icon','❔') }} {{ el|elabel }}{% if el.name %} <span class="has-text-grey">— {{ w.get('label', el.widget) }}</span>{% endif %}</h2>
<form method="post" action="{{ url_for('element_delete', slug=game.slug, element_id=el.id) }}"
onsubmit="return confirm('Supprimer cet élément ?');">
data-confirm-message="Supprimer cet élément ?" onsubmit="return handleDeleteElementSubmit(event, this);">
<button type="submit" class="button is-small is-danger">🗑️</button>
</form>
</div>
@@ -524,7 +548,8 @@
<div class="field has-addons" style="margin:0;">
<div class="control is-expanded"><a class="button is-small is-fullwidth" href="{{ url_for('screen_edit', slug=game.slug, screen_id=screen.id, selected=tab.id) }}">📥 Contenu de l'onglet</a></div>
<div class="control">
<form method="post" action="{{ url_for('element_delete', slug=game.slug, element_id=tab.id) }}" onsubmit="return confirm('Supprimer cet onglet et tout son contenu ?');">
<form method="post" action="{{ url_for('element_delete', slug=game.slug, element_id=tab.id) }}"
data-confirm-message="Supprimer cet onglet et tout son contenu ?" onsubmit="return handleDeleteElementSubmit(event, this);">
<button type="submit" class="button is-small is-danger">🗑️</button>
</form>
</div>
@@ -999,6 +1024,61 @@ function filterIconGallery(query) {
// posé) — même liste que la galerie d'ajout ci-dessus, mais choisir une
// icône ici remplace celle de l'élément SÉLECTIONNÉ (champ caché
// "field-icon_class") au lieu d'en créer un nouveau. ----------
// ---------- Confirmation de suppression d'un élément (modale custom, pas
// le confirm() natif du navigateur) — voir deleteConfirmModal plus haut.
// Interroge element_delete_impact.py pour savoir si des nœuds de la
// Logique de la scène seraient aussi supprimés, et l'affiche si oui,
// avant que l'utilisateur confirme quoi que ce soit. ----------
var _pendingDeleteForm = null;
function handleDeleteElementSubmit(event, form) {
if (form.dataset.confirmed === '1') {
// Deuxième passage, après clic sur "Supprimer" dans la modale (voir
// confirmDeleteElement()) : laisse cette fois la soumission suivre son
// cours normalement (pjax.js l'intercepte comme n'importe quel autre
// formulaire, voir static/pjax.js).
form.dataset.confirmed = '';
return true;
}
event.preventDefault();
_pendingDeleteForm = form;
document.getElementById('deleteConfirmMessage').textContent = form.dataset.confirmMessage || 'Supprimer cet élément ?';
var warnEl = document.getElementById('deleteConfirmFlowWarning');
warnEl.style.display = 'none';
warnEl.textContent = '';
document.getElementById('deleteConfirmModal').classList.add('is-active');
var m = form.getAttribute('action').match(/\/elements\/(\d+)\/delete$/);
if (m) {
fetch("/game/{{ game.slug }}/elements/" + m[1] + "/delete-impact")
.then(function (r) { return r.json(); })
.then(function (data) {
if (!data.flow_node_count) return;
var n = data.flow_node_count;
var plural = n > 1 ? 's' : '';
warnEl.textContent = '⚠️ ' + n + ' connexion' + plural + ' de la Logique de la scène (déclencheur/action) utilise' +
(n > 1 ? 'nt' : '') + ' cet élément et sera' + (n > 1 ? 'ont' : '') + ' aussi supprimée' + plural + '.';
warnEl.style.display = 'block';
})
.catch(function () {});
}
return false;
}
function closeDeleteConfirmModal() {
var modal = document.getElementById('deleteConfirmModal');
if (modal) modal.classList.remove('is-active');
_pendingDeleteForm = null;
}
function confirmDeleteElement() {
if (!_pendingDeleteForm) return;
var form = _pendingDeleteForm;
_pendingDeleteForm = null;
closeDeleteConfirmModal();
form.dataset.confirmed = '1';
if (form.requestSubmit) form.requestSubmit(); else form.submit();
}
function openIconPicker() {
var modal = document.getElementById('iconPickerModal');
if (modal) modal.classList.add('is-active');