Collapse the column axis with split_by_depth, like the row axis
Column collapse did not survive flipping away from the browser and back, because it was the one piece of state still held outside the config: applySplitDepth restored a *truncated* split_by, and a rebuilt view took its split_by from the config while the levels it had dropped lived only in splitFull. split_by_depth is the config-level equivalent, and it works now that apply_update applies it -- server.cpp does ctx2->set_depth(HEADER_COLUMN, column_pivot_depth - 1), so it is 1-based exactly like group_by_depth. Everything that existed to support truncation goes with it: - splitFull no longer has to outlive split_by, because split_by keeps every level. It is just the live axis, for rendering the buttons. - split_full is no longer persisted beside the config. Still read on load, for layouts saved by the old scheme. - collapsingRef and the prefix test are gone. They existed to tell our own collapse from the user rearranging the pivot, which only mattered because a collapse looked like a shorter split_by. - The selection survives a collapse. It was cleared because slices name the split_by dimensions they were cut from and the axis was changing shape; with a depth those dimensions are all still there. CLAUDE.md's §Column hierarchy describes the old mechanism throughout, and is now wrong; updating it separately. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
251692a3f9
commit
607dac221d
@ -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) {
|
||||
// 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()
|
||||
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
|
||||
}
|
||||
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)
|
||||
|
||||
Loading…
Reference in New Issue
Block a user