fix(map): canonicalize facet visibility and compact header
This commit is contained in:
@@ -2089,7 +2089,12 @@ export function CatalogApp() {
|
||||
description={activeApplicationPage ? `${activeApplication.metadata.name} · ${activeApplicationPage.template.id}@${activeApplicationPage.template.version}` : `/${activeApplication.metadata.slug} · manifest ${activeApplication.schemaVersion}`}
|
||||
expanded={panelExpanded}
|
||||
onExpandedChange={workspace.setContentExpanded}
|
||||
headerTools={activeApplicationPage?.template.id === "map" ? <div ref={setMapHeaderActionsHost} className="catalog-map-header-actions-host" /> : undefined}
|
||||
headerTools={(
|
||||
<div className="catalog-application-header-tools">
|
||||
<SegmentedControl label="Режим модуля" value={applicationMode} items={[{ value: "edit", label: "Настройка" }, { value: "preview", label: "Предпросмотр" }]} onChange={setApplicationMode} />
|
||||
{activeApplicationPage?.template.id === "map" ? <div ref={setMapHeaderActionsHost} className="catalog-map-header-actions-host" /> : null}
|
||||
</div>
|
||||
)}
|
||||
utilityActions={[{
|
||||
label: applicationSaveState === "saving" ? "Сохранение…" : applicationSaveState === "error" ? "Повторить сохранение" : "Сохранить",
|
||||
icon: "save",
|
||||
@@ -2103,9 +2108,6 @@ export function CatalogApp() {
|
||||
onClose={workspace.closeView}
|
||||
>
|
||||
<div className="catalog-panel-content">
|
||||
<div className="catalog-module-mode">
|
||||
<SegmentedControl label="Режим модуля" value={applicationMode} items={[{ value: "edit", label: "Настройка" }, { value: "preview", label: "Предпросмотр" }]} onChange={setApplicationMode} />
|
||||
</div>
|
||||
{activeApplicationPage ? renderApplicationPage() : renderApplicationDraft()}
|
||||
</div>
|
||||
</ApplicationPanel>
|
||||
|
||||
@@ -17,10 +17,12 @@ import type { MapRuntimeFact } from "./useMapDataProductRuntime.js";
|
||||
import {
|
||||
compareMapRuntimeFacts,
|
||||
mapFactMatchesFilters,
|
||||
mapPresentationFacetValueIsEnabled,
|
||||
mapPresentationFacetCounts,
|
||||
mapPresentationProfileForFact,
|
||||
mapRuntimeDisplayLabel,
|
||||
mapRuntimeFactIsRenderable,
|
||||
normalizeMapPresentationFacetSelections,
|
||||
normalizeClientMapPresentationProfiles,
|
||||
resolveMapPresentationClass,
|
||||
toggleMapPresentationFacetSelection,
|
||||
@@ -665,11 +667,23 @@ const defaultSubjectCardRect: WorkspaceWindowRect = {
|
||||
height: 520,
|
||||
};
|
||||
|
||||
function initialSubjectState(bindings: MapDataProductBinding[], saved: MapSubjectState[] | undefined) {
|
||||
function initialSubjectState(
|
||||
bindings: MapDataProductBinding[],
|
||||
saved: MapSubjectState[] | undefined,
|
||||
profiles: MapPresentationProfile[],
|
||||
) {
|
||||
const savedByBinding = new Map((saved ?? []).map((state) => [state.bindingId, state]));
|
||||
return Object.fromEntries(bindings.map((binding, index) => {
|
||||
const state = savedByBinding.get(binding.id);
|
||||
return [binding.id, state ?? {
|
||||
const profile = mapPresentationProfileForFact(
|
||||
profiles,
|
||||
binding.presentationProfileId,
|
||||
binding.semanticTypes[0] ?? "",
|
||||
);
|
||||
return [binding.id, state ? {
|
||||
...state,
|
||||
filters: profile ? normalizeMapPresentationFacetSelections(state.filters, profile) : state.filters,
|
||||
} : {
|
||||
bindingId: binding.id,
|
||||
visible: true,
|
||||
filters: {},
|
||||
@@ -796,14 +810,22 @@ export const MapFixturePreview = forwardRef<MapFixturePreviewHandle, {
|
||||
initialMapReferenceLayers(initialLayout?.referenceLayers)
|
||||
));
|
||||
const [subjectStates, setSubjectStates] = useState<Record<string, MapSubjectState>>(() => (
|
||||
initialSubjectState(initialLayout?.dataProductBindings ?? [], initialLayout?.subjectStates)
|
||||
initialSubjectState(initialLayout?.dataProductBindings ?? [], initialLayout?.subjectStates, presentationProfiles)
|
||||
));
|
||||
const presentationFilters = useMemo<MapPresentationFilters>(() => Object.fromEntries(
|
||||
Object.entries(subjectStates).map(([bindingId, state]) => [bindingId, {
|
||||
visible: state.visible,
|
||||
facets: state.filters,
|
||||
}]),
|
||||
), [subjectStates]);
|
||||
Object.entries(subjectStates).map(([bindingId, state]) => {
|
||||
const binding = dataProductBindings.find((candidate) => candidate.id === bindingId);
|
||||
const profile = mapPresentationProfileForFact(
|
||||
presentationProfiles,
|
||||
binding?.presentationProfileId,
|
||||
binding?.semanticTypes[0] ?? "",
|
||||
);
|
||||
return [bindingId, {
|
||||
visible: state.visible,
|
||||
facets: profile ? normalizeMapPresentationFacetSelections(state.filters, profile) : state.filters,
|
||||
}];
|
||||
}),
|
||||
), [dataProductBindings, presentationProfiles, subjectStates]);
|
||||
const runtimeBindings = useMapDataProductRuntime({
|
||||
applicationId,
|
||||
pageId,
|
||||
@@ -1339,9 +1361,12 @@ export const MapFixturePreview = forwardRef<MapFixturePreviewHandle, {
|
||||
window: defaultSubjectWindowState(0),
|
||||
}).map((state) => {
|
||||
const summary = presentationSummaries.find((candidate) => candidate.bindingId === state.bindingId);
|
||||
return summary && !hasSubjectWindowControls(summary.profile)
|
||||
? { ...state, window: { ...state.window, open: false } }
|
||||
const normalizedState = summary
|
||||
? { ...state, filters: normalizeMapPresentationFacetSelections(state.filters, summary.profile) }
|
||||
: state;
|
||||
return summary && !hasSubjectWindowControls(summary.profile)
|
||||
? { ...normalizedState, window: { ...normalizedState.window, open: false } }
|
||||
: normalizedState;
|
||||
}),
|
||||
}),
|
||||
}), [dataProductBindings, inspectorOpenSections, mapCamera, mapHeight, mapSettings, pinBindings, presentationProfiles, presentationSummaries, referenceLayers, subjectDetailProfiles, subjectStates]);
|
||||
@@ -1359,13 +1384,13 @@ export const MapFixturePreview = forwardRef<MapFixturePreviewHandle, {
|
||||
});
|
||||
}, [dataProductBindings]);
|
||||
|
||||
const togglePresentationFilter = (bindingId: string, field: string, value: string) => {
|
||||
const togglePresentationFilter = (bindingId: string, field: string, value: string, availableValues: string[]) => {
|
||||
updateSubjectState(bindingId, (state) => {
|
||||
const filters = state.visible ? state.filters : {};
|
||||
return {
|
||||
...state,
|
||||
visible: true,
|
||||
filters: toggleMapPresentationFacetSelection(filters, field, value),
|
||||
filters: toggleMapPresentationFacetSelection(filters, field, value, availableValues),
|
||||
};
|
||||
});
|
||||
};
|
||||
@@ -2619,7 +2644,7 @@ export const MapFixturePreview = forwardRef<MapFixturePreviewHandle, {
|
||||
<div className="catalog-map-fixture__target-filter-list">
|
||||
{summary.profile.facets.filter((facet) => facet.counter || facet.filterable).flatMap((facet) => (
|
||||
facet.values.map((item) => {
|
||||
const active = state.filters[facet.field]?.includes(item.value) ?? false;
|
||||
const active = mapPresentationFacetValueIsEnabled(state.filters, facet.field, item.value);
|
||||
const rowId = `${summary.bindingId}:${facet.field}:${item.value}`;
|
||||
const expanded = Boolean(expandedFacetRows[rowId]);
|
||||
const matchingEntities = selectable.filter((entity) => (
|
||||
@@ -2644,7 +2669,12 @@ export const MapFixturePreview = forwardRef<MapFixturePreviewHandle, {
|
||||
className="catalog-map-fixture__target-filter-body"
|
||||
aria-pressed={active}
|
||||
disabled={!facet.filterable}
|
||||
onClick={() => togglePresentationFilter(summary.bindingId, facet.field, item.value)}
|
||||
onClick={() => togglePresentationFilter(
|
||||
summary.bindingId,
|
||||
facet.field,
|
||||
item.value,
|
||||
facet.values.map((value) => value.value),
|
||||
)}
|
||||
>
|
||||
<span className="catalog-map-fixture__target-filter-label">{item.label}</span>
|
||||
<span className="catalog-map-fixture__target-filter-count">{summary.counts[facet.field]?.[item.value] ?? 0}</span>
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -377,9 +377,10 @@ textarea {
|
||||
padding-right: 1.25rem;
|
||||
}
|
||||
|
||||
.catalog-module-mode {
|
||||
.catalog-application-header-tools {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
gap: 0.45rem;
|
||||
}
|
||||
|
||||
.catalog-module-preview {
|
||||
|
||||
+1
-1
@@ -11,7 +11,7 @@
|
||||
"scripts": {
|
||||
"build": "npm run build --workspace @nodedc/ui-core && npm run build --workspace @nodedc/ui-dom && npm run build --workspace @nodedc/ui-react && npm run build --workspace @nodedc/page-patterns && npm run build --workspace @nodedc/map-cesium-react && npm run build --workspace @nodedc/ui-catalog",
|
||||
"build:packages": "npm run build --workspace @nodedc/ui-core && npm run build --workspace @nodedc/ui-dom && npm run build --workspace @nodedc/ui-react && npm run build --workspace @nodedc/page-patterns && npm run build --workspace @nodedc/map-cesium-react",
|
||||
"check": "npm run build:packages && npm run typecheck --workspaces --if-present && npm run validate:registry && npm run test:floating-position && npm run test:inspector-select && npm run test:range-control && npm run test:hgeozone-projection && npm run test:map-grid-lod && npm run test:map-object-layers && npm run test:map-inspector-overlay-state && npm run test:map-reference-stations && npm run test:map-search && npm run test:map-subject-card && npm run test:map-subject-detail-profile",
|
||||
"check": "npm run build:packages && npm run typecheck --workspaces --if-present && npm run validate:registry && npm run test:floating-position && npm run test:inspector-select && npm run test:range-control && npm run test:hgeozone-projection && npm run test:map-grid-lod && npm run test:map-filters && npm run test:map-object-layers && npm run test:map-inspector-overlay-state && npm run test:map-reference-stations && npm run test:map-search && npm run test:map-subject-card && npm run test:map-subject-detail-profile",
|
||||
"dev": "npm run build:packages && npm run dev --workspace @nodedc/ui-catalog",
|
||||
"serve": "node server/catalog-server.mjs",
|
||||
"validate:registry": "node scripts/validate-registry.mjs",
|
||||
|
||||
@@ -61,6 +61,8 @@ test("Map settings use a canonical trailing push panel and ApplicationPanel head
|
||||
assert.doesNotMatch(preview, /settingsPanelPinned|onPointerDownCapture/);
|
||||
assert.match(catalog, /endPanelOpen=\{mapSettingsPanelOpen\}/);
|
||||
assert.match(catalog, /headerTools=\{activePageTemplate\.id === "map"[\s\S]*?setMapHeaderActionsHost/);
|
||||
assert.match(catalog, /className="catalog-application-header-tools"[\s\S]*?label="Режим модуля"[\s\S]*?setMapHeaderActionsHost/);
|
||||
assert.doesNotMatch(catalog, /className="catalog-module-mode"/);
|
||||
assert.match(styles, /data-content-expanded="true"\]\[data-end-panel-open="true"\][\s\S]*?right: calc\(/);
|
||||
assert.match(styles, /@keyframes nodedc-application-side-panel-in[\s\S]*?translateX\(1\.6rem\)/);
|
||||
assert.match(catalogStyles, /\.catalog-map-header-action\.nodedc-icon-button[\s\S]*?--nodedc-panel-action-bg/);
|
||||
|
||||
@@ -29,6 +29,17 @@ test("hidden and visible layers have an explicit visual state", async () => {
|
||||
assert.match(styles, /\.catalog-map-fixture__objects-menu-item:not\(\[data-visible\]\)/);
|
||||
});
|
||||
|
||||
test("facet controls and renderer consume the same canonical enabled-value state", async () => {
|
||||
const preview = await readFile(new URL("../apps/catalog/src/MapFixturePreview.tsx", import.meta.url), "utf8");
|
||||
|
||||
assert.match(preview, /initialSubjectState\(initialLayout\?\.dataProductBindings[\s\S]*?presentationProfiles\)/);
|
||||
assert.match(preview, /facets: profile \? normalizeMapPresentationFacetSelections\(state\.filters, profile\) : state\.filters/);
|
||||
assert.match(preview, /filters: normalizeMapPresentationFacetSelections\(state\.filters, summary\.profile\)/);
|
||||
assert.match(preview, /const active = mapPresentationFacetValueIsEnabled\(state\.filters, facet\.field, item\.value\)/);
|
||||
assert.match(preview, /toggleMapPresentationFacetSelection\(filters, field, value, availableValues\)/);
|
||||
assert.match(preview, /facet\.values\.map\(\(value\) => value\.value\)/);
|
||||
});
|
||||
|
||||
test("joined detail aspects do not become independent map layers", async () => {
|
||||
const preview = await readFile(new URL("../apps/catalog/src/MapFixturePreview.tsx", import.meta.url), "utf8");
|
||||
|
||||
|
||||
@@ -12,6 +12,8 @@ const {
|
||||
mapFactMatchesFilters,
|
||||
mapPresentationBindingIsAll,
|
||||
mapPresentationFacetCounts,
|
||||
mapPresentationFacetValueIsEnabled,
|
||||
normalizeMapPresentationFacetSelections,
|
||||
toggleMapPresentationFacetSelection,
|
||||
} = await import(moduleUrl);
|
||||
|
||||
@@ -35,6 +37,8 @@ const onlineMoving = {
|
||||
test("missing subject state is explicit all for that binding", () => {
|
||||
assert.equal(mapPresentationBindingIsAll("fleet", {}), true);
|
||||
assert.equal(mapFactMatchesFilters(onlineMoving, profile, {}, "fleet"), true);
|
||||
assert.equal(mapPresentationFacetValueIsEnabled({}, "signal_state", "active"), true);
|
||||
assert.equal(mapPresentationFacetValueIsEnabled({}, "signal_state", "inactive"), true);
|
||||
});
|
||||
|
||||
test("a selected chip restricts only its own facet", () => {
|
||||
@@ -49,62 +53,120 @@ test("deselecting the last chip remains empty and never normalizes to all", () =
|
||||
assert.equal(mapFactMatchesFilters(onlineMoving, profile, filters, "fleet"), false);
|
||||
});
|
||||
|
||||
test("interactive deselect of the last chip preserves the zero-match state", () => {
|
||||
const selected = toggleMapPresentationFacetSelection({}, "signal_state", "active");
|
||||
assert.deepEqual(selected, { signal_state: ["active"] });
|
||||
test("toggling from implicit all disables the clicked value instead of selecting only it", () => {
|
||||
const facets = toggleMapPresentationFacetSelection(
|
||||
{},
|
||||
"signal_state",
|
||||
"active",
|
||||
["active", "inactive"],
|
||||
);
|
||||
|
||||
const empty = toggleMapPresentationFacetSelection(selected, "signal_state", "active");
|
||||
const filters = { fleet: { visible: true, facets: empty } };
|
||||
assert.deepEqual(empty, { signal_state: [] });
|
||||
assert.equal(mapPresentationBindingIsAll("fleet", filters), false);
|
||||
assert.equal(mapFactMatchesFilters(onlineMoving, profile, filters, "fleet"), false);
|
||||
});
|
||||
|
||||
test("the first chip selected after a saved empty view removes every empty sentinel", () => {
|
||||
const selected = toggleMapPresentationFacetSelection({
|
||||
signal_state: [],
|
||||
movement_state: [],
|
||||
}, "signal_state", "active");
|
||||
|
||||
assert.deepEqual(selected, { signal_state: ["active"] });
|
||||
assert.deepEqual(facets, { signal_state: ["inactive"] });
|
||||
assert.equal(mapPresentationFacetValueIsEnabled(facets, "signal_state", "active"), false);
|
||||
assert.equal(mapPresentationFacetValueIsEnabled(facets, "signal_state", "inactive"), true);
|
||||
assert.equal(mapFactMatchesFilters(onlineMoving, profile, {
|
||||
fleet: { visible: true, facets: selected },
|
||||
}, "fleet"), true);
|
||||
fleet: { visible: true, facets },
|
||||
}, "fleet"), false);
|
||||
});
|
||||
|
||||
test("a complete interactive toggle cycle returns to zero matches instead of all", () => {
|
||||
let facets = { signal_state: [], movement_state: [] };
|
||||
const toggle = (field, value) => {
|
||||
facets = toggleMapPresentationFacetSelection(facets, field, value);
|
||||
};
|
||||
test("turning off the last enabled value is explicit zero and can be re-enabled locally", () => {
|
||||
const none = toggleMapPresentationFacetSelection(
|
||||
{ signal_state: ["inactive"] },
|
||||
"signal_state",
|
||||
"inactive",
|
||||
["active", "inactive"],
|
||||
);
|
||||
assert.deepEqual(none, { signal_state: [] });
|
||||
assert.equal(mapFactMatchesFilters(onlineMoving, profile, {
|
||||
fleet: { visible: true, facets: none },
|
||||
}, "fleet"), false);
|
||||
|
||||
toggle("signal_state", "active");
|
||||
toggle("signal_state", "inactive");
|
||||
toggle("movement_state", "moving");
|
||||
toggle("movement_state", "stopped");
|
||||
assert.deepEqual(facets, {
|
||||
const active = toggleMapPresentationFacetSelection(
|
||||
{ ...none, movement_state: [] },
|
||||
"signal_state",
|
||||
"active",
|
||||
["active", "inactive"],
|
||||
);
|
||||
assert.deepEqual(active, { signal_state: ["active"], movement_state: [] });
|
||||
assert.equal(mapPresentationFacetValueIsEnabled(active, "signal_state", "active"), true);
|
||||
assert.equal(mapPresentationFacetValueIsEnabled(active, "movement_state", "moving"), false);
|
||||
});
|
||||
|
||||
test("enabling the final disabled value canonicalizes that facet back to implicit all", () => {
|
||||
const facets = toggleMapPresentationFacetSelection(
|
||||
{ signal_state: ["active"] },
|
||||
"signal_state",
|
||||
"inactive",
|
||||
["active", "inactive"],
|
||||
);
|
||||
|
||||
assert.deepEqual(facets, {});
|
||||
assert.equal(mapPresentationBindingIsAll("fleet", { fleet: { visible: true, facets } }), true);
|
||||
assert.equal(mapPresentationFacetValueIsEnabled(facets, "signal_state", "active"), true);
|
||||
assert.equal(mapPresentationFacetValueIsEnabled(facets, "signal_state", "inactive"), true);
|
||||
});
|
||||
|
||||
test("legacy visually-all facets cannot keep suppressing offline subjects", () => {
|
||||
const legacyFacets = {
|
||||
signal_state: ["active", "inactive"],
|
||||
movement_state: ["moving", "stopped"],
|
||||
});
|
||||
};
|
||||
const offlineStopped = {
|
||||
attributes: { signal_state: "inactive", movement_state: "stopped" },
|
||||
};
|
||||
|
||||
toggle("signal_state", "active");
|
||||
toggle("signal_state", "inactive");
|
||||
toggle("movement_state", "moving");
|
||||
toggle("movement_state", "stopped");
|
||||
assert.equal(mapPresentationFacetValueIsEnabled(legacyFacets, "signal_state", "inactive"), true);
|
||||
assert.equal(mapPresentationFacetValueIsEnabled(legacyFacets, "movement_state", "stopped"), true);
|
||||
assert.equal(mapFactMatchesFilters(
|
||||
offlineStopped,
|
||||
profile,
|
||||
{ fleet: { visible: true, facets: legacyFacets } },
|
||||
"fleet",
|
||||
), false);
|
||||
|
||||
const filters = { fleet: { visible: true, facets } };
|
||||
assert.deepEqual(facets, { movement_state: [] });
|
||||
assert.equal(mapPresentationBindingIsAll("fleet", filters), false);
|
||||
assert.equal(mapFactMatchesFilters(onlineMoving, profile, filters, "fleet"), false);
|
||||
const facets = normalizeMapPresentationFacetSelections(legacyFacets, profile);
|
||||
|
||||
assert.deepEqual(facets, {});
|
||||
assert.equal(mapFactMatchesFilters(
|
||||
offlineStopped,
|
||||
profile,
|
||||
{ fleet: { visible: true, facets } },
|
||||
"fleet",
|
||||
), true);
|
||||
});
|
||||
|
||||
test("interactive deselect preserves other values and facet constraints", () => {
|
||||
test("a full toggle cycle has distinct and recoverable all and none states", () => {
|
||||
let facets = {};
|
||||
const toggle = (value) => {
|
||||
facets = toggleMapPresentationFacetSelection(
|
||||
facets,
|
||||
"signal_state",
|
||||
value,
|
||||
["active", "inactive"],
|
||||
);
|
||||
};
|
||||
|
||||
toggle("active");
|
||||
assert.deepEqual(facets, { signal_state: ["inactive"] });
|
||||
toggle("inactive");
|
||||
assert.deepEqual(facets, { signal_state: [] });
|
||||
toggle("active");
|
||||
assert.deepEqual(facets, { signal_state: ["active"] });
|
||||
toggle("inactive");
|
||||
assert.deepEqual(facets, {});
|
||||
});
|
||||
|
||||
test("interactive toggle preserves independent facet constraints", () => {
|
||||
const facets = {
|
||||
signal_state: ["active", "inactive"],
|
||||
signal_state: ["active"],
|
||||
movement_state: ["moving"],
|
||||
};
|
||||
assert.deepEqual(toggleMapPresentationFacetSelection(facets, "signal_state", "active"), {
|
||||
signal_state: ["inactive"],
|
||||
assert.deepEqual(toggleMapPresentationFacetSelection(
|
||||
facets,
|
||||
"signal_state",
|
||||
"inactive",
|
||||
["active", "inactive"],
|
||||
), {
|
||||
movement_state: ["moving"],
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user