fix(foundry): make map objects true layer toggles
This commit is contained in:
parent
ca26702047
commit
b8fc4c5ba1
|
|
@ -268,6 +268,10 @@ function initialSubjectState(bindings: MapDataProductBinding[], saved: MapSubjec
|
|||
})) as Record<string, MapSubjectState>;
|
||||
}
|
||||
|
||||
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<MapFixturePreviewHandle, {
|
|||
visible: true,
|
||||
filters: {},
|
||||
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 } }
|
||||
: 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<MapFixturePreviewHandle, {
|
|||
});
|
||||
};
|
||||
|
||||
const toggleSubjectVisibility = (bindingId: string) => {
|
||||
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<MapFixturePreviewHandle, {
|
|||
{presentationSummaries.map((summary) => {
|
||||
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 (
|
||||
<button
|
||||
type="button"
|
||||
role="menuitem"
|
||||
<div
|
||||
className="catalog-map-fixture__objects-menu-item"
|
||||
key={summary.bindingId}
|
||||
data-open={state?.window.open || undefined}
|
||||
data-visible={visible || undefined}
|
||||
data-open={hasControls && state?.window.open || undefined}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
role="menuitemcheckbox"
|
||||
aria-checked={visible}
|
||||
className="catalog-map-fixture__objects-menu-toggle"
|
||||
onClick={() => toggleSubjectVisibility(summary.bindingId)}
|
||||
>
|
||||
<span>{summary.displayName}</span>
|
||||
<small>{visible ? `на карте: ${visibleCount}` : `слой скрыт · ${summary.total} объектов`}</small>
|
||||
</button>
|
||||
{hasControls ? (
|
||||
<IconButton
|
||||
label={`Фильтры и счётчики: ${summary.displayName}`}
|
||||
role="menuitem"
|
||||
aria-pressed={state?.window.open || false}
|
||||
data-active={state?.window.open || undefined}
|
||||
onClick={() => {
|
||||
openSubjectWindow(summary.bindingId);
|
||||
close();
|
||||
}}
|
||||
>
|
||||
<span>{summary.displayName}</span>
|
||||
<small>{state?.visible === false || visibleCount === 0 ? "на карте: 0" : `на карте: ${visibleCount}`}{state?.window.open ? " · окно открыто" : ""}</small>
|
||||
</button>
|
||||
><Icon name="settings" /></IconButton>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
{!presentationSummaries.length ? <small className="catalog-map-fixture__objects-menu-empty">Нет подключённых объектов.</small> : null}
|
||||
|
|
@ -1229,7 +1259,7 @@ export const MapFixturePreview = forwardRef<MapFixturePreviewHandle, {
|
|||
|
||||
{presentationSummaries.map((summary) => {
|
||||
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 (
|
||||
<WorkspaceWindow
|
||||
|
|
|
|||
|
|
@ -722,7 +722,7 @@ textarea {
|
|||
|
||||
.catalog-map-fixture__objects-menu-list,
|
||||
.catalog-map-fixture__objects-menu-head,
|
||||
.catalog-map-fixture__objects-menu-item {
|
||||
.catalog-map-fixture__objects-menu-toggle {
|
||||
display: grid;
|
||||
}
|
||||
|
||||
|
|
@ -736,7 +736,7 @@ textarea {
|
|||
}
|
||||
|
||||
.catalog-map-fixture__objects-menu-head small,
|
||||
.catalog-map-fixture__objects-menu-item small,
|
||||
.catalog-map-fixture__objects-menu-toggle small,
|
||||
.catalog-map-fixture__objects-menu-empty {
|
||||
color: var(--nodedc-map-glass-text-muted);
|
||||
font-size: var(--nodedc-font-size-xs);
|
||||
|
|
@ -745,24 +745,45 @@ textarea {
|
|||
.catalog-map-fixture__objects-menu-item {
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
gap: 0.16rem;
|
||||
border: 0;
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) auto;
|
||||
align-items: center;
|
||||
gap: 0.28rem;
|
||||
border-radius: 0.78rem;
|
||||
background: transparent;
|
||||
padding: 0.64rem 0.7rem;
|
||||
padding: 0.18rem;
|
||||
color: var(--nodedc-map-glass-text);
|
||||
}
|
||||
|
||||
.catalog-map-fixture__objects-menu-item:hover,
|
||||
.catalog-map-fixture__objects-menu-item[data-open],
|
||||
.catalog-map-fixture__objects-menu-item[data-visible] {
|
||||
background: rgb(255 255 255 / 0.16);
|
||||
}
|
||||
|
||||
.catalog-map-fixture__objects-menu-item:not([data-visible]) {
|
||||
opacity: 0.62;
|
||||
}
|
||||
|
||||
.catalog-map-fixture__objects-menu-toggle {
|
||||
min-width: 0;
|
||||
gap: 0.16rem;
|
||||
border: 0;
|
||||
border-radius: 0.64rem;
|
||||
background: transparent;
|
||||
padding: 0.48rem 0.52rem;
|
||||
color: inherit;
|
||||
font: inherit;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.catalog-map-fixture__objects-menu-item:hover,
|
||||
.catalog-map-fixture__objects-menu-item:focus-visible,
|
||||
.catalog-map-fixture__objects-menu-item[data-open] {
|
||||
background: rgb(255 255 255 / 0.16);
|
||||
.catalog-map-fixture__objects-menu-toggle:focus-visible {
|
||||
outline: 2px solid var(--nodedc-map-glass-text);
|
||||
outline-offset: -2px;
|
||||
}
|
||||
|
||||
.catalog-map-fixture__objects-menu-item > 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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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\]\)/);
|
||||
});
|
||||
Loading…
Reference in New Issue