From b8fc4c5ba1ae828af4dd21cfe6f774f67d92bde2 Mon Sep 17 00:00:00 2001 From: Codex Date: Wed, 22 Jul 2026 10:33:58 +0300 Subject: [PATCH] fix(foundry): make map objects true layer toggles --- apps/catalog/src/MapFixturePreview.tsx | 56 ++++++++++++++++++++------ apps/catalog/src/styles.css | 46 ++++++++++++++++----- package.json | 3 +- scripts/map-object-layers.test.mjs | 30 ++++++++++++++ 4 files changed, 111 insertions(+), 24 deletions(-) create mode 100644 scripts/map-object-layers.test.mjs diff --git a/apps/catalog/src/MapFixturePreview.tsx b/apps/catalog/src/MapFixturePreview.tsx index 390b539..d30fcb1 100644 --- a/apps/catalog/src/MapFixturePreview.tsx +++ b/apps/catalog/src/MapFixturePreview.tsx @@ -268,6 +268,10 @@ function initialSubjectState(bindings: MapDataProductBinding[], saved: MapSubjec })) as Record; } +function hasSubjectWindowControls(profile: MapPresentationProfile) { + return profile.facets.some((facet) => facet.counter || facet.filterable); +} + const logarithmicControlValue = (value: number) => Math.log10(Math.max(Number.MIN_VALUE, value)); const valueFromLogarithmicControl = (value: number) => Math.max(1, Math.round(10 ** value)); const formatMetricDistance = (value: number) => value >= 1000 @@ -628,9 +632,14 @@ export const MapFixturePreview = forwardRef { + const summary = presentationSummaries.find((candidate) => candidate.bindingId === state.bindingId); + return summary && !hasSubjectWindowControls(summary.profile) + ? { ...state, window: { ...state.window, open: false } } + : state; }), }), - }), [dataProductBindings, mapCamera, mapHeight, mapSettings, pinBindings, presentationProfiles, subjectStates]); + }), [dataProductBindings, mapCamera, mapHeight, mapSettings, pinBindings, presentationProfiles, presentationSummaries, subjectStates]); const updateSubjectState = (bindingId: string, update: (state: MapSubjectState) => MapSubjectState) => { setSubjectStates((current) => { @@ -656,6 +665,10 @@ export const MapFixturePreview = forwardRef { + updateSubjectState(bindingId, (state) => ({ ...state, visible: !state.visible })); + }; + const openSubjectWindow = (bindingId: string) => { const nextZIndex = Math.max(20, layersWindowZIndex, ...Object.values(subjectStates).map((state) => state.window.zIndex)) + 1; updateSubjectState(bindingId, (state) => ({ @@ -1201,21 +1214,38 @@ export const MapFixturePreview = forwardRef { const state = subjectStates[summary.bindingId]; const visibleCount = filteredTargets.filter((target) => target.bindingId === summary.bindingId && target.renderable).length; + const visible = state?.visible !== false; + const hasControls = hasSubjectWindowControls(summary.profile); return ( - + + {hasControls ? ( + { + openSubjectWindow(summary.bindingId); + close(); + }} + > + ) : null} + ); })} {!presentationSummaries.length ? Нет подключённых объектов. : null} @@ -1229,7 +1259,7 @@ export const MapFixturePreview = forwardRef { const state = subjectStates[summary.bindingId]; - if (!state?.window.open) return null; + if (!state?.window.open || !hasSubjectWindowControls(summary.profile)) return null; const visibleCount = filteredTargets.filter((target) => target.bindingId === summary.bindingId && target.renderable).length; return ( span { +.catalog-map-fixture__objects-menu-toggle > span { overflow: hidden; font-size: var(--nodedc-font-size-sm); font-weight: 760; @@ -770,6 +791,11 @@ textarea { white-space: nowrap; } +.catalog-map-fixture__objects-menu-item > .nodedc-icon-button { + width: 2.25rem; + height: 2.25rem; +} + .catalog-map-fixture__objects-menu-empty { padding: 0.72rem; } diff --git a/package.json b/package.json index 576eab3..d6e82cf 100644 --- a/package.json +++ b/package.json @@ -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/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", - "check": "npm run build:packages && npm run typecheck --workspaces --if-present && npm run validate:registry && npm run test:inspector-select && npm run test:hgeozone-projection", + "check": "npm run build:packages && npm run typecheck --workspaces --if-present && npm run validate:registry && npm run test:inspector-select && npm run test:hgeozone-projection && npm run test:map-object-layers", "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", @@ -23,6 +23,7 @@ "test:map-animation": "node --test scripts/map-spiral.test.mjs scripts/map-camera-presets.test.mjs", "test:map-filters": "node --test scripts/map-presentation-filters.test.mjs", "test:hgeozone-projection": "node --test scripts/hgeozone-projection.test.mjs", + "test:map-object-layers": "node --test scripts/map-object-layers.test.mjs", "test:map-cache-contract": "node --test scripts/map-cache-resource-contract.test.mjs", "test:inspector-select": "node --test scripts/inspector-select-contract.test.mjs" }, diff --git a/scripts/map-object-layers.test.mjs b/scripts/map-object-layers.test.mjs new file mode 100644 index 0000000..3cd50d3 --- /dev/null +++ b/scripts/map-object-layers.test.mjs @@ -0,0 +1,30 @@ +import assert from "node:assert/strict"; +import { readFile } from "node:fs/promises"; +import test from "node:test"; + +test("Objects menu treats each binding as a persisted visibility layer", async () => { + const preview = await readFile(new URL("../apps/catalog/src/MapFixturePreview.tsx", import.meta.url), "utf8"); + + assert.match(preview, /const toggleSubjectVisibility = \(bindingId: string\)/); + assert.match(preview, /role="menuitemcheckbox"/); + assert.match(preview, /aria-checked=\{visible\}/); + assert.match(preview, /onClick=\{\(\) => toggleSubjectVisibility\(summary\.bindingId\)\}/); + assert.match(preview, /visible: !state\.visible/); + assert.match(preview, /слой скрыт · \$\{summary\.total\} объектов/); +}); + +test("a subject window exists only when the presentation profile exposes controls", async () => { + const preview = await readFile(new URL("../apps/catalog/src/MapFixturePreview.tsx", import.meta.url), "utf8"); + + assert.match(preview, /function hasSubjectWindowControls\(profile: MapPresentationProfile\)/); + assert.match(preview, /\{hasControls \? \(/); + assert.match(preview, /if \(!state\?\.window\.open \|\| !hasSubjectWindowControls\(summary\.profile\)\) return null/); + assert.match(preview, /window: \{ \.\.\.state\.window, open: false \}/); +}); + +test("hidden and visible layers have an explicit visual state", async () => { + const styles = await readFile(new URL("../apps/catalog/src/styles.css", import.meta.url), "utf8"); + + assert.match(styles, /\.catalog-map-fixture__objects-menu-item\[data-visible\]/); + assert.match(styles, /\.catalog-map-fixture__objects-menu-item:not\(\[data-visible\]\)/); +});