fix(map): canonicalize facet visibility and compact header

This commit is contained in:
Codex
2026-08-09 15:48:23 +03:00
parent 71c1528b83
commit bfa3af896b
8 changed files with 213 additions and 91 deletions
+42 -28
View File
@@ -105,42 +105,56 @@ export function mapPresentationBindingIsAll(bindingId: string, filters: MapPrese
}
/**
* Apply one interactive facet-chip transition without collapsing the storage
* contract. A missing field means that facet is not part of the current union,
* while one explicitly empty field represents the intentional match-nothing
* view after the final selected chip is switched off.
* A missing facet is the compact canonical representation of every configured
* value being enabled. An explicit array is the exact enabled subset; an empty
* array therefore remains an intentional match-nothing state.
*/
export function toggleMapPresentationFacetSelection(
export function mapPresentationFacetValueIsEnabled(
facets: Record<string, string[]>,
field: string,
value: string,
) {
const selected = facets[field];
if (!selected?.includes(value)) {
// A saved empty view can contain one or more empty facet arrays. They are
// a global zero-match sentinel, not active constraints, so selecting the
// first chip must replace them instead of leaving a hidden empty array that
// would continue to suppress every fact.
const activeFacets = Object.fromEntries(
Object.entries(facets).filter(([, values]) => values.length > 0),
);
return { ...activeFacets, [field]: [...(activeFacets[field] ?? []), value] };
}
return selected === undefined || selected.includes(value);
}
const nextSelected = selected.filter((item) => item !== value);
if (nextSelected.length > 0) return { ...facets, [field]: nextSelected };
export function normalizeMapPresentationFacetSelections(
facets: Record<string, string[]>,
profile: MapPresentationProfile,
) {
return Object.fromEntries(profile.facets.flatMap((facet) => {
const selected = facets[facet.field];
if (selected === undefined) return [];
const availableValues = [...new Set(facet.values.map((item) => item.value))];
const enabledValues = availableValues.filter((value) => selected.includes(value));
// Legacy layouts could persist every value explicitly. Canonicalize that
// shape to an unconstrained facet so scoped facets (for example movement
// on online subjects) cannot accidentally suppress unrelated subjects.
return availableValues.length > 0 && enabledValues.length === availableValues.length
? []
: [[facet.field, enabledValues] as const];
}));
}
const { [field]: _removed, ...remaining } = facets;
const activeFacets = Object.fromEntries(
Object.entries(remaining).filter(([, values]) => values.length > 0),
);
if (Object.keys(activeFacets).length > 0) return activeFacets;
// Never collapse the last interactive deselection to `{}`: the renderer
// correctly interprets `{}` as unconstrained/all. Retain an explicit empty
// facet so the zero-selection state remains zero matches through any toggle
// cycle and through a later Application Save.
return { [field]: [] };
export function toggleMapPresentationFacetSelection(
facets: Record<string, string[]>,
field: string,
value: string,
availableValues: string[],
) {
const values = [...new Set(availableValues)];
if (!values.includes(value)) return facets;
const selected = facets[field];
const enabled = new Set(selected === undefined
? values
: values.filter((item) => selected.includes(item)));
if (enabled.has(value)) enabled.delete(value);
else enabled.add(value);
const nextEnabled = values.filter((item) => enabled.has(item));
const next = { ...facets };
if (nextEnabled.length === values.length) delete next[field];
else next[field] = nextEnabled;
return next;
}
export function mapPresentationProfileForFact(