Corrige le filtre "élément le plus spécifique" (logique inversée)

Les deux commits précédents (ciblage des éléments imbriqués dans
applyOpenRowBindings() et refreshRuntimeData()) n'avaient AUCUN effet
visible, confirmé par l'utilisateur après redémarrage du serveur — cause
trouvée : leur filtre "ne garder que les éléments les plus spécifiques"
vérifiait l'inverse de ce qu'il fallait.

Un CONTENEUR contient toujours le HTML de ses descendants dans son
propre rendered_html — donc un ancêtre "a le marqueur/placeholder" quasi
systématiquement dès qu'un descendant l'a. Le filtre précédent excluait
un élément candidat si un de ses ANCÊTRES était candidat — ce qui, vu ce
qui précède, ne gardait quasiment jamais que l'ancêtre RACINE de
l'écran, reproduisant exactement le bug d'origine (tout l'écran
régénéré) que ces commits visaient à corriger.

Fix : inversion du sens du filtre — un candidat est désormais exclu si
l'un de ses PROPRES DESCENDANTS est aussi candidat (le descendant sera
déjà régénéré individuellement, inutile de régénérer aussi son
ancêtre). Vérifié par une simulation Node.js reproduisant la structure
réelle de l'écran de test (Répéteur niché sous 2 conteneurs, "mail
content" sous 2 autres) : la nouvelle logique cible bien uniquement le
Répéteur et "mail content", plus jamais le conteneur racine de l'écran.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
william
2026-08-26 13:04:01 +02:00
co-authored by Claude Sonnet 5
parent f4a7a7340e
commit 5e4e226794
+32 -17
View File
@@ -351,26 +351,33 @@
// rechargement de page (flash à vide) à chaque clic sur une ligne. // rechargement de page (flash à vide) à chaque clic sur une ligne.
// //
// Parmi les éléments qui ont un {{champ}} non résolu, on ne garde que // Parmi les éléments qui ont un {{champ}} non résolu, on ne garde que
// les plus "hauts" dans l'arbre : si un ancêtre ET un de ses // les plus SPÉCIFIQUES (les plus profonds) : si un élément ET l'un de
// descendants sont tous deux candidats, seul l'ancêtre est régénéré // ses DESCENDANTS sont tous deux candidats, seul le descendant est
// (son rendered_html inclut déjà ce descendant) — sans ce filtre, le // régénéré (rendered_html d'un conteneur inclut TOUJOURS celui de ses
// descendant serait régénéré deux fois, la 2e fois à partir du DOM déjà // descendants, donc un conteneur "candidat" ne l'est souvent que parce
// à jour de la 1re passe. // qu'un descendant à lui l'est — le garder en plus regénérerait
// inutilement tout son sous-arbre, à l'exact opposé du but recherché).
// ATTENTION : la version précédente de ce filtre vérifiait l'inverse
// (exclure un élément si un ANCÊTRE est candidat) — comme un ancêtre
// est presque toujours "candidat" dès qu'un descendant l'est (son HTML
// le contient), ce filtre ne gardait en pratique QUE l'ancêtre racine
// de l'écran, annulant complètement l'effet recherché par ce correctif.
const byId = {}; const byId = {};
screenData.elements.forEach(function(e){ byId[e.id] = e; }); screenData.elements.forEach(function(e){ byId[e.id] = e; });
function hasUnresolvedPlaceholder(e) { function hasUnresolvedPlaceholder(e) {
return !!(e.rendered_html && e.rendered_html.indexOf(PLACEHOLDER_OPEN) !== -1); return !!(e.rendered_html && e.rendered_html.indexOf(PLACEHOLDER_OPEN) !== -1);
} }
function hasCandidateAncestor(e) { function isDescendantOf(e, ancestorId) {
let p = e.parent_id ? byId[e.parent_id] : null; let p = e.parent_id ? byId[e.parent_id] : null;
while (p) { while (p) {
if (hasUnresolvedPlaceholder(p)) return true; if (p.id === ancestorId) return true;
p = p.parent_id ? byId[p.parent_id] : null; p = p.parent_id ? byId[p.parent_id] : null;
} }
return false; return false;
} }
const targets = screenData.elements.filter(function(e) { const candidates = screenData.elements.filter(hasUnresolvedPlaceholder);
return hasUnresolvedPlaceholder(e) && !hasCandidateAncestor(e); const targets = candidates.filter(function(e) {
return !candidates.some(function(other) { return other.id !== e.id && isDescendantOf(other, e.id); });
}); });
targets.forEach(function(el) { targets.forEach(function(el) {
@@ -480,25 +487,33 @@
// qui portait le marqueur (le Répéteur imbriqué à l'intérieur) // qui portait le marqueur (le Répéteur imbriqué à l'intérieur)
// et se retrouvait régénéré en entier à sa place — potentiellement // et se retrouvait régénéré en entier à sa place — potentiellement
// l'écran entier (jauges, onglets... compris) s'il n'y avait // l'écran entier (jauges, onglets... compris) s'il n'y avait
// qu'un seul gros conteneur racine. Comme pour // qu'un seul gros conteneur racine.
// applyOpenRowBindings(), on ne garde que les éléments marqués //
// les plus "hauts" dans l'arbre, pour ne jamais régénérer un même // Parmi les éléments marqués, on ne garde que les plus
// nœud deux fois. // SPÉCIFIQUES (les plus profonds) : un ancêtre est presque
// toujours "marqué" lui aussi dès qu'un descendant l'est, puisque
// son rendered_html contient celui de ses descendants — le
// garder EN PLUS régénérerait tout son sous-arbre pour rien.
// (Une version antérieure de ce filtre vérifiait l'inverse —
// exclure un élément si un ANCÊTRE est marqué — ce qui ne
// gardait en pratique que l'ancêtre racine de l'écran et
// annulait complètement l'effet recherché.)
const byId = {}; const byId = {};
screenData.elements.forEach(function(e){ byId[e.id] = e; }); screenData.elements.forEach(function(e){ byId[e.id] = e; });
function hasMarker(e) { function hasMarker(e) {
return !!(e.rendered_html && (e.rendered_html.indexOf('repeaterItem') !== -1 || e.rendered_html.indexOf('jaugeBar') !== -1 || e.rendered_html.indexOf('visibilityGated') !== -1)); return !!(e.rendered_html && (e.rendered_html.indexOf('repeaterItem') !== -1 || e.rendered_html.indexOf('jaugeBar') !== -1 || e.rendered_html.indexOf('visibilityGated') !== -1));
} }
function hasMarkedAncestor(e) { function isDescendantOf(e, ancestorId) {
let p = e.parent_id ? byId[e.parent_id] : null; let p = e.parent_id ? byId[e.parent_id] : null;
while (p) { while (p) {
if (hasMarker(p)) return true; if (p.id === ancestorId) return true;
p = p.parent_id ? byId[p.parent_id] : null; p = p.parent_id ? byId[p.parent_id] : null;
} }
return false; return false;
} }
screenData.elements.filter(function(e) { const markedCandidates = screenData.elements.filter(hasMarker);
return hasMarker(e) && !hasMarkedAncestor(e); markedCandidates.filter(function(e) {
return !markedCandidates.some(function(other) { return other.id !== e.id && isDescendantOf(other, e.id); });
}).forEach(function(el) { }).forEach(function(el) {
const target = screenDiv.querySelector('[data-element-id="' + el.id + '"]'); const target = screenDiv.querySelector('[data-element-id="' + el.id + '"]');
if (!target) return; if (!target) return;