70 lines
2.9 KiB
JavaScript
70 lines
2.9 KiB
JavaScript
/**
|
|
* listSelection.js — what a vertical-legend row selects, and the zones a tree forgot.
|
|
*
|
|
* Extracted from ListSelectorLegend.vue so both rules can be tested without mounting a component.
|
|
* Small rules, large consequences: a dashboard filters thousands of rows by the first, and the
|
|
* second decides whether 100+ groups are reachable at all.
|
|
*/
|
|
|
|
/** Every leaf under a node, or the node itself when it is a leaf. */
|
|
export function leavesOf(node) {
|
|
if (!node) return []
|
|
const kids = node.children || node.nodes || []
|
|
if (!kids.length) return [node]
|
|
const out = []
|
|
for (const k of kids) out.push(...leavesOf(k))
|
|
return out
|
|
}
|
|
|
|
/** Every zone id present anywhere in a tree. */
|
|
export function zoneIdsIn(nodes) {
|
|
const out = new Set()
|
|
for (const n of nodes || []) for (const l of leavesOf(n)) if (l && l.id) out.add(String(l.id))
|
|
return out
|
|
}
|
|
|
|
export const UNASSIGNED_ID = '__unassigned__'
|
|
|
|
/**
|
|
* ★ A ZONE THE TREE DOES NOT CONTAIN MUST STILL BE REACHABLE. Four joinable zones are absent from
|
|
* zone-regions.json today — guizhou, xinjiang (100+ groups on its own), bihar, madhya-pradesh — so a
|
|
* legend that renders only the region tree DROPS them, silently, and a reader has no way to select
|
|
* groups that plainly exist. The fix is not to remember: it is to hand this function the full zone
|
|
* set and let it render the difference. A drop then becomes structurally impossible rather than a
|
|
* thing someone has to keep checking.
|
|
*
|
|
* Returns the tree unchanged when nothing is missing, so the bucket never appears without cause.
|
|
*
|
|
* @param {Array} nodes the region-grouped tree
|
|
* @param {Array<string>} allZoneIds every zone the data can join, tree or no tree
|
|
* @param {string} label what to call the bucket (the caller localises it)
|
|
*/
|
|
export function withUnassigned(nodes, allZoneIds, label = 'Unassigned') {
|
|
const present = zoneIdsIn(nodes)
|
|
const missing = (allZoneIds || []).map(String).filter((id) => !present.has(id))
|
|
if (!missing.length) return nodes || []
|
|
return [
|
|
...(nodes || []),
|
|
{
|
|
id: UNASSIGNED_ID,
|
|
label,
|
|
depth: 0,
|
|
// Each missing zone is a selectable leaf, not a lump: a reader wants xinjiang, not "the four
|
|
// zones nobody filed".
|
|
children: missing.map((id) => ({ id, label: id, zoneKey: id })),
|
|
},
|
|
]
|
|
}
|
|
|
|
/**
|
|
* The value the legend emits. gospel's contract: a single SERIALISABLE id, so it survives ?zone=,
|
|
* profile-config and a deep link, and stays usable across their four tab shapes.
|
|
*
|
|
* A leaf emits its zone id; a REGION row emits the region's own id (dx-shepherd: a region selects
|
|
* the union of its zones). The consumer expands a region id to its zones using the zone data it
|
|
* already holds — which is also where the diacritic-folding join lives, so there is no second copy
|
|
* of it here to drift.
|
|
*/
|
|
export function selectedId(node) {
|
|
return node && node.id != null ? String(node.id) : null
|
|
}
|