Préserver l'état visuel transitoire lors du rafraîchissement des données

Cause réelle du bug rapporté ("clic sur une ligne = comme un rechargement,
impossible de cliquer sur une deuxième ligne") : applyOpenRowBindings()
regénère entièrement le sous-arbre d'un élément contenant des {{champ}}
(ex. "mail content") à partir de son HTML D'ORIGINE, tel que rendu par le
serveur — donc avec son style PAR DÉFAUT (ici "invisible" via le réglage
Disposition > Visibilité). Cette fonction est appelée après CHAQUE
rafraîchissement de données (refreshRuntimeData), y compris pour un
changement de donnée sans rapport avec ce panneau.

Or une action "Modifier un élément → Rendre visible" ne modifie JAMAIS la
base : c'est un changement DOM transitoire (style.visibility = ''). Quand
un clic sur une ligne de Répéteur déclenche EN PARALLÈLE "Ouvrir la ligne
cliquée" + "Rendre visible" + "Modifier une donnée", la branche
"Modifier une donnée" est asynchrone (aller-retour serveur) et termine
après les deux autres, synchrones. Son refreshRuntimeData() qui suit
regénère alors "mail content" depuis son état par défaut, écrasant le
"Rendre visible" qui venait tout juste d'être posé — le panneau redevient
invisible. Un second clic sur le MÊME mail "corrige" l'affichage car la
donnée est déjà à jour, donc la Condition ne redéclenche plus l'action de
modification, plus de refresh, plus d'écrasement ; mais ouvrir un AUTRE
mail reproduisait le même écrasement.

Fix : avant de remplacer wrapper.innerHTML, sauvegarder le style inline,
la classe et les data-* de chaque élément du sous-arbre, puis les
réappliquer juste après la regénération — la résolution des {{champ}}
reste correcte (c'est le but premier de la fonction) sans plus annuler
les changements posés par une action "Modifier un élément" au même clic.

Les trois actions du graphe (ouvrir la ligne, rendre visible, modifier la
donnée) restent connectées telles quelles, sans aucun retrait.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
william
2026-08-25 18:17:35 +02:00
co-authored by Claude Sonnet 5
parent 96d017c21f
commit 438e561b45
+28
View File
@@ -342,6 +342,26 @@
screenData.elements.filter(function(e){ return !e.parent_id; }).forEach(function(el) { screenData.elements.filter(function(e){ return !e.parent_id; }).forEach(function(el) {
const wrapper = screenDiv.querySelector('.playElement[data-el-id="' + el.id + '"] .playElementInner'); const wrapper = screenDiv.querySelector('.playElement[data-el-id="' + el.id + '"] .playElementInner');
if (!wrapper || !el.rendered_html || el.rendered_html.indexOf(PLACEHOLDER_OPEN) === -1) return; if (!wrapper || !el.rendered_html || el.rendered_html.indexOf(PLACEHOLDER_OPEN) === -1) return;
// Avant de regénérer ce sous-arbre depuis le HTML D'ORIGINE (nécessaire
// pour ne pas laisser les valeurs d'un mail précédent affichées, voir
// plus haut), sauvegarde l'état visuel TRANSITOIRE posé par une action
// "Modifier un élément" (jamais enregistré en base — voir
// applyElementProperty) sur cet élément ou l'un de ses descendants,
// pour le réappliquer après. Sans ça, un panneau rendu visible par
// cette action (ex. "mail content") redevenait invisible à CHAQUE
// rafraîchissement de données, y compris pour un changement de donnée
// sans rapport avec lui — puisque wrapper.innerHTML = html ci-dessous
// recrée entièrement ses nœuds à partir de leur état PAR DÉFAUT.
const savedState = {};
wrapper.querySelectorAll('[data-element-id]').forEach(function(node) {
savedState[node.dataset.elementId] = {
style: node.getAttribute('style') || '',
className: node.className,
dataset: Object.assign({}, node.dataset),
};
});
let html = el.rendered_html; let html = el.rendered_html;
Object.keys(row).forEach(function(fieldName) { Object.keys(row).forEach(function(fieldName) {
if (fieldName === 'id') return; if (fieldName === 'id') return;
@@ -351,6 +371,14 @@
html = html.split(PLACEHOLDER_OPEN + fieldName + PLACEHOLDER_CLOSE).join(tmp.innerHTML); html = html.split(PLACEHOLDER_OPEN + fieldName + PLACEHOLDER_CLOSE).join(tmp.innerHTML);
}); });
wrapper.innerHTML = html; wrapper.innerHTML = html;
wrapper.querySelectorAll('[data-element-id]').forEach(function(node) {
const saved = savedState[node.dataset.elementId];
if (!saved) return;
node.setAttribute('style', saved.style);
node.className = saved.className;
Object.assign(node.dataset, saved.dataset);
});
}); });
} }