diff --git a/backend/resources/app/templates/graph-console.tmpl b/backend/resources/app/templates/graph-console.tmpl index e42cb7d86d..894e237d20 100644 --- a/backend/resources/app/templates/graph-console.tmpl +++ b/backend/resources/app/templates/graph-console.tmpl @@ -165,6 +165,11 @@ Graph Console title="collapse every container that holds no changed elements"> fold unchanged + @@ -511,6 +516,15 @@ Graph Console return localStorage.getItem("graph-diff-fold") === "1"; } + // Overview mode: null = off, else collapse containers at depth ≥ value + // (root = depth 0, so e.g. 2 folds the containers hanging from a Page). + function depthFoldValue() { + const raw = localStorage.getItem("graph-depth-fold"); + if (raw == null || raw === "") return null; + const v = parseInt(raw, 10); + return Number.isFinite(v) && v >= 0 ? v : null; + } + // One-shot pulse rings on age-0 marks, placed ~post-render (positions // are sampled once; pulses don't track pan/zoom during their ~1.2 s). function pulseAt(canvasPoint, color, sizePx) { @@ -688,21 +702,36 @@ Graph Console if ((childrenOf[n.id] || []).length && parentOf[n.id]) hasCombo[n.id] = true; }); } - // Diff-fold: derive fold state from the live marks instead of the - // instance — every combo collapses except those on an ancestor path - // of (or being) a changed element, so changes stand out of the - // unchanged mass. Manual double-click fold state is ignored while on. - if (diffFold && withCombos) { + // Derived fold state (overrides manual double-click folds while on): + // overview mode collapses every combo at depth ≥ the limit (all of + // them when only diff-fold is on), then diff-fold re-opens the + // ancestor paths of changed elements — combined, that reads as + // "overview, with changes drilled open". + const depthLimit = depthFoldValue(); + if (withCombos && (diffFold || depthLimit != null)) { + const depthOf = {}; + const depth = function (id) { + if (depthOf[id] != null) return depthOf[id]; + const p = parentOf[id]; + depthOf[id] = p ? depth(p) + 1 : 0; + return depthOf[id]; + }; collapsedIds = new Set(); - Object.keys(hasCombo).forEach(function (id) { collapsedIds.add(comboIdFor(id)); }); - liveMarkedNodeIds().forEach(function (id) { - if (hasCombo[id]) collapsedIds.delete(comboIdFor(id)); - let p = parentOf[id]; - while (p) { - if (hasCombo[p]) collapsedIds.delete(comboIdFor(p)); - p = parentOf[p]; + Object.keys(hasCombo).forEach(function (id) { + if (depthLimit == null || depth(id) >= depthLimit) { + collapsedIds.add(comboIdFor(id)); } }); + if (diffFold) { + liveMarkedNodeIds().forEach(function (id) { + if (hasCombo[id]) collapsedIds.delete(comboIdFor(id)); + let p = parentOf[id]; + while (p) { + if (hasCombo[p]) collapsedIds.delete(comboIdFor(p)); + p = parentOf[p]; + } + }); + } } const combos = []; nodes.forEach(function (n) { @@ -1041,7 +1070,8 @@ Graph Console } if (anywayBtn) anywayBtn.style.display = "none"; graphViewStatus.textContent = graphStatusText(data) + filterNote; - renderGraph(toG6Data(shown, collapsedComboIds(), foldEnabled() || diffFoldEnabled(), + renderGraph(toG6Data(shown, collapsedComboIds(), + foldEnabled() || diffFoldEnabled() || depthFoldValue() != null, LAYOUTS[currentLayoutName()] == null, diffFoldEnabled())); } @@ -1552,6 +1582,16 @@ Graph Console }); } + const depthFoldInput = document.getElementById("graph-depth-fold"); + if (depthFoldInput) { + const v = depthFoldValue(); + depthFoldInput.value = v == null ? "" : String(v); + depthFoldInput.addEventListener("change", function () { + localStorage.setItem("graph-depth-fold", depthFoldInput.value); + renderCurrent(); + }); + } + const layoutSelect = document.getElementById("graph-layout-select"); if (layoutSelect) { Object.keys(LAYOUTS).forEach(function (name) {