diff --git a/ui/src/views/Forecast.jsx b/ui/src/views/Forecast.jsx index 2a7d9f9..384f784 100644 --- a/ui/src/views/Forecast.jsx +++ b/ui/src/views/Forecast.jsx @@ -346,14 +346,14 @@ export default function Forecast({ sources = [], sourceId, versions = [], versio let settle = null const onObserved = (ev) => { const d = ev.detail || {} - // Losing intersection, or collapsing to zero size, is the element going - // away: record it, because that is when the view gets discarded. - const leaving = d.entries?.some(e => - e.isIntersecting === false || (e.width === 0 && e.height === 0)) + // Only intersection counts as going away. A zero size is a transient of + // layout -- opening Perspective's own settings sidebar collapses the + // datagrid for a frame -- and treating it as a departure meant the return + // re-applied depth, so adjusting the layout snapped the tree back. + const leaving = d.entries?.some(e => e.isIntersecting === false) if (leaving) { wentAwayRef.current = true; return dbg(`observer ${d.kind} -> viewer went away`) } - // A resize with a non-zero size is an ordinary reflow. It is not a - // restoration, so it must not re-apply anything. + // Everything else is a reflow. Not a restoration, so it restores nothing. if (!wentAwayRef.current) return clearTimeout(settle) @@ -875,7 +875,17 @@ export default function Forecast({ sources = [], sourceId, versions = [], versio // it redefines the hierarchy. Ours is a collapse, and must not overwrite it. if (!collapsingRef.current) { const live = await viewer.save() - adoptSplit(live.split_by || [], (live.split_by || []).length) + const next = live.split_by || [] + const full = splitFullRef.current || [] + // This event fires for anything in the config -- opening the settings + // sidebar included -- not only for split_by. While collapsed, the live + // split_by is a prefix of the full hierarchy, and adopting it would + // record the truncation as the hierarchy and make the deeper levels + // unreachable. A genuine rearrangement is not a prefix of what we hold. + const isCollapsedPrefix = + next.length < full.length && next.every((c, i) => c === full[i]) + if (!isCollapsedPrefix) adoptSplit(next, next.length) + else dbg(`config-update: split_by is our collapse (${next.length}/${full.length}), keeping the full hierarchy`) } const cfg = await captureConfig() if (cfg) await persistLayout(vid, cfg)