diff --git a/ui/src/views/Forecast.jsx b/ui/src/views/Forecast.jsx index f3c6832..7bad8c1 100644 --- a/ui/src/views/Forecast.jsx +++ b/ui/src/views/Forecast.jsx @@ -192,9 +192,7 @@ export default function Forecast({ sources = [], sourceId, versions = [], versio // The column axis has no set_depth() — collapsing it means restoring a shorter // split_by, so the full hierarchy has to be remembered separately to expand again. const splitFullRef = useRef([]) - // set while our own restore is in flight, so the config-update listener can tell // a collapse from the user rearranging split_by themselves - const collapsingRef = useRef(false) const initIdRef = useRef(0) const modifierRef = useRef(false) // the datagrid plugin element, for reading cell coordinates and driving its @@ -670,9 +668,9 @@ export default function Forecast({ sources = [], sourceId, versions = [], versio const { table: _t, ...rest } = cleanLayout(JSON.parse(saved), validCols) const cfg = { ...rest, plugin_config: { ...(rest.plugin_config || {}), edit_mode: 'SELECT_REGION' } } await viewer.restore(cfg) - // split_full outlives split_by: a layout saved while collapsed still knows - // the levels it was collapsed from - adoptSplit(cfg.split_full?.length ? cfg.split_full : cfg.split_by, (cfg.split_by || []).length) + // split_full is the legacy key, from when collapsing truncated split_by + adoptSplit(cfg.split_full?.length ? cfg.split_full : cfg.split_by, + cfg.split_by_depth != null ? cfg.split_by_depth - 1 : null) // restore() has already applied group_by_depth; this only syncs the // toolbar. expand_depth is the legacy key, from when depth was imperative // and had to be stored beside the config rather than in it. @@ -701,22 +699,12 @@ export default function Forecast({ sources = [], sourceId, versions = [], versio if (viewer._pspUpdate) viewer.removeEventListener('perspective-config-update', viewer._pspUpdate) viewer._pspUpdate = async () => { try { - // A split_by change that is not ours is the user rearranging the pivot, and - // it redefines the hierarchy. Ours is a collapse, and must not overwrite it. - if (!collapsingRef.current) { - const live = await viewer.save() - 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: our own collapse, so the full hierarchy stands - } + // split_by is never truncated now, so the live value is always the real + // hierarchy and can be adopted unconditionally -- no need to tell our own + // collapse apart from the user rearranging the pivot. + const live = await viewer.save() + adoptSplit(live.split_by || [], + live.split_by_depth != null ? live.split_by_depth - 1 : null) const cfg = await captureConfig() if (cfg) await persistLayout(vid, cfg) } catch {} @@ -771,46 +759,51 @@ export default function Forecast({ sources = [], sourceId, versions = [], versio // Record the column hierarchy as the uncollapsed truth. Called whenever a layout // arrives or the user rearranges split_by themselves — but never for our own // collapse, which would otherwise overwrite the full list with the short one. - function adoptSplit(full, depth) { - const list = Array.isArray(full) ? full : [] - splitFullRef.current = list - setSplitFull(list) - setSplitDepth(depth == null ? list.length : Math.min(depth, list.length)) + // The column axis as it stands, for rendering the collapse buttons. split_by is + // no longer truncated to collapse, so it *is* the hierarchy -- nothing has to be + // remembered alongside it. + function adoptSplit(list, depth) { + const cols = Array.isArray(list) ? list : [] + splitFullRef.current = cols + setSplitFull(cols) + setSplitDepth(depth == null ? cols.length : Math.min(depth, cols.length)) } // Collapse or expand the column axis to `n` split_by levels. // - // The row axis gets this for free: its GROUP BY ROLLUP view holds every level at - // once and view.set_depth() hides the deeper ones. The column axis has no - // equivalent — there is no split_by_depth in ViewConfig and expand()/collapse() - // take a row index — so collapsing means restoring a truncated split_by, which - // rebuilds the view. Two consequences fall out of that: the row depth has to be - // re-applied afterwards (it lives on the discarded view), and it is whole-axis, - // not per-branch — every column group collapses to the same level together. + // Both axes are now the same shape: a depth in ViewConfig. This used to restore + // a *truncated* split_by instead, because split_by_depth was accepted and then + // dropped by ViewConfig::apply_update -- see ui/vendor's patch. Truncating had + // to be undone to expand again, which is why the full hierarchy needed + // remembering separately (splitFull, persisted as split_full), why our own + // collapse had to be told apart from the user rearranging the pivot + // (collapsingRef and the prefix test), and why the selection was cleared each + // time the axis changed shape. + // + // None of that is needed for a depth. split_by keeps every level, so the + // hierarchy is simply `cfg.split_by`; the depth rides along in saved layouts and + // survives a view rebuild; and the dimensions a slice was cut from are still + // there, so the selection can stay. async function applySplitDepth(n) { const viewer = viewerRef.current - const full = splitFullRef.current - if (!viewer || !full.length) return - const depth = Math.max(0, Math.min(n, full.length)) - collapsingRef.current = true + if (!viewer) return + const { table: _t, ...cfg } = await viewer.save() + const levels = (cfg.split_by || []).length + if (!levels) return + const depth = Math.max(0, Math.min(n, levels)) try { - await viewer.restore({ split_by: full.slice(0, depth) }) + // 1-based, as with group_by_depth: server.cpp does + // ctx2->set_depth(HEADER_COLUMN, column_pivot_depth - 1) + await viewer.restore({ ...cfg, split_by_depth: depth + 1 }) setSplitDepth(depth) - // No need to re-apply the row depth: group_by_depth is part of the config the - // view is rebuilt from, so it comes back with it. } catch (err) { console.error('[applySplitDepth]', err) flash(err.message || String(err), 'error') return - } finally { - collapsingRef.current = false } - // a slice names the split_by dimensions it was cut from, and the highlight is - // keyed on grid coordinates — neither survives a column axis that just changed - setSlices([]) try { - const cfg = await captureConfig() - if (cfg) await persistLayout(versionId, cfg) + const cfg2 = await captureConfig() + if (cfg2) await persistLayout(versionId, cfg2) } catch (err) { console.error('[applySplitDepth persist]', err) } @@ -871,10 +864,10 @@ export default function Forecast({ sources = [], sourceId, versions = [], versio const viewer = viewerRef.current if (!viewer) return null const cfg = await viewer.save() - // group_by_depth is already in cfg, straight from save(). Only split_full is - // ours: it outlives split_by, so a layout saved while collapsed still knows - // the levels it was collapsed from. - return { ...cfg, split_full: splitFullRef.current } + // Both depths are already in cfg, straight from save(). Nothing of ours needs + // to travel beside it any more -- split_full existed only because collapsing + // truncated split_by and the discarded levels had to be remembered. + return cfg } async function persistLayout(vid, cfg) { @@ -932,7 +925,8 @@ export default function Forecast({ sources = [], sourceId, versions = [], versio const cfg = cleanLayout(layout.config, validCols) cfg.plugin_config = { ...(cfg.plugin_config || {}), edit_mode: 'SELECT_REGION' } await viewer.restore(cfg) - adoptSplit(cfg.split_full?.length ? cfg.split_full : cfg.split_by, (cfg.split_by || []).length) + adoptSplit(cfg.split_full?.length ? cfg.split_full : cfg.split_by, + cfg.split_by_depth != null ? cfg.split_by_depth - 1 : null) if (cfg.group_by_depth != null) setExpandDepth(cfg.group_by_depth - 1) else if (cfg.expand_depth != null) await applyDepth(cfg.expand_depth) setActiveLayoutId(layout.id)