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>;
|
})) 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 logarithmicControlValue = (value: number) => Math.log10(Math.max(Number.MIN_VALUE, value));
|
||||||
const valueFromLogarithmicControl = (value: number) => Math.max(1, Math.round(10 ** value));
|
const valueFromLogarithmicControl = (value: number) => Math.max(1, Math.round(10 ** value));
|
||||||
const formatMetricDistance = (value: number) => value >= 1000
|
const formatMetricDistance = (value: number) => value >= 1000
|
||||||
|
|
@ -628,9 +632,14 @@ export const MapFixturePreview = forwardRef<MapFixturePreviewHandle, {
|
||||||
visible: true,
|
visible: true,
|
||||||
filters: {},
|
filters: {},
|
||||||
window: defaultSubjectWindowState(0),
|
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) => {
|
const updateSubjectState = (bindingId: string, update: (state: MapSubjectState) => MapSubjectState) => {
|
||||||
setSubjectStates((current) => {
|
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 openSubjectWindow = (bindingId: string) => {
|
||||||
const nextZIndex = Math.max(20, layersWindowZIndex, ...Object.values(subjectStates).map((state) => state.window.zIndex)) + 1;
|
const nextZIndex = Math.max(20, layersWindowZIndex, ...Object.values(subjectStates).map((state) => state.window.zIndex)) + 1;
|
||||||
updateSubjectState(bindingId, (state) => ({
|
updateSubjectState(bindingId, (state) => ({
|
||||||
|
|
@ -1201,21 +1214,38 @@ export const MapFixturePreview = forwardRef<MapFixturePreviewHandle, {
|
||||||
{presentationSummaries.map((summary) => {
|
{presentationSummaries.map((summary) => {
|
||||||
const state = subjectStates[summary.bindingId];
|
const state = subjectStates[summary.bindingId];
|
||||||
const visibleCount = filteredTargets.filter((target) => target.bindingId === summary.bindingId && target.renderable).length;
|
const visibleCount = filteredTargets.filter((target) => target.bindingId === summary.bindingId && target.renderable).length;
|
||||||
|
const visible = state?.visible !== false;
|
||||||
|
const hasControls = hasSubjectWindowControls(summary.profile);
|
||||||
return (
|
return (
|
||||||
<button
|
<div
|
||||||
type="button"
|
|
||||||
role="menuitem"
|
|
||||||
className="catalog-map-fixture__objects-menu-item"
|
className="catalog-map-fixture__objects-menu-item"
|
||||||
key={summary.bindingId}
|
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={() => {
|
onClick={() => {
|
||||||
openSubjectWindow(summary.bindingId);
|
openSubjectWindow(summary.bindingId);
|
||||||
close();
|
close();
|
||||||
}}
|
}}
|
||||||
>
|
><Icon name="settings" /></IconButton>
|
||||||
<span>{summary.displayName}</span>
|
) : null}
|
||||||
<small>{state?.visible === false || visibleCount === 0 ? "на карте: 0" : `на карте: ${visibleCount}`}{state?.window.open ? " · окно открыто" : ""}</small>
|
</div>
|
||||||
</button>
|
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
{!presentationSummaries.length ? <small className="catalog-map-fixture__objects-menu-empty">Нет подключённых объектов.</small> : null}
|
{!presentationSummaries.length ? <small className="catalog-map-fixture__objects-menu-empty">Нет подключённых объектов.</small> : null}
|
||||||
|
|
@ -1229,7 +1259,7 @@ export const MapFixturePreview = forwardRef<MapFixturePreviewHandle, {
|
||||||
|
|
||||||
{presentationSummaries.map((summary) => {
|
{presentationSummaries.map((summary) => {
|
||||||
const state = subjectStates[summary.bindingId];
|
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;
|
const visibleCount = filteredTargets.filter((target) => target.bindingId === summary.bindingId && target.renderable).length;
|
||||||
return (
|
return (
|
||||||
<WorkspaceWindow
|
<WorkspaceWindow
|
||||||
|
|
|
||||||
|
|
@ -722,7 +722,7 @@ textarea {
|
||||||
|
|
||||||
.catalog-map-fixture__objects-menu-list,
|
.catalog-map-fixture__objects-menu-list,
|
||||||
.catalog-map-fixture__objects-menu-head,
|
.catalog-map-fixture__objects-menu-head,
|
||||||
.catalog-map-fixture__objects-menu-item {
|
.catalog-map-fixture__objects-menu-toggle {
|
||||||
display: grid;
|
display: grid;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -736,7 +736,7 @@ textarea {
|
||||||
}
|
}
|
||||||
|
|
||||||
.catalog-map-fixture__objects-menu-head small,
|
.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 {
|
.catalog-map-fixture__objects-menu-empty {
|
||||||
color: var(--nodedc-map-glass-text-muted);
|
color: var(--nodedc-map-glass-text-muted);
|
||||||
font-size: var(--nodedc-font-size-xs);
|
font-size: var(--nodedc-font-size-xs);
|
||||||
|
|
@ -745,24 +745,45 @@ textarea {
|
||||||
.catalog-map-fixture__objects-menu-item {
|
.catalog-map-fixture__objects-menu-item {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
gap: 0.16rem;
|
display: grid;
|
||||||
border: 0;
|
grid-template-columns: minmax(0, 1fr) auto;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.28rem;
|
||||||
border-radius: 0.78rem;
|
border-radius: 0.78rem;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
padding: 0.64rem 0.7rem;
|
padding: 0.18rem;
|
||||||
color: var(--nodedc-map-glass-text);
|
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;
|
font: inherit;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.catalog-map-fixture__objects-menu-item:hover,
|
.catalog-map-fixture__objects-menu-toggle:focus-visible {
|
||||||
.catalog-map-fixture__objects-menu-item:focus-visible,
|
outline: 2px solid var(--nodedc-map-glass-text);
|
||||||
.catalog-map-fixture__objects-menu-item[data-open] {
|
outline-offset: -2px;
|
||||||
background: rgb(255 255 255 / 0.16);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.catalog-map-fixture__objects-menu-item > span {
|
.catalog-map-fixture__objects-menu-toggle > span {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
font-size: var(--nodedc-font-size-sm);
|
font-size: var(--nodedc-font-size-sm);
|
||||||
font-weight: 760;
|
font-weight: 760;
|
||||||
|
|
@ -770,6 +791,11 @@ textarea {
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.catalog-map-fixture__objects-menu-item > .nodedc-icon-button {
|
||||||
|
width: 2.25rem;
|
||||||
|
height: 2.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
.catalog-map-fixture__objects-menu-empty {
|
.catalog-map-fixture__objects-menu-empty {
|
||||||
padding: 0.72rem;
|
padding: 0.72rem;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@
|
||||||
"scripts": {
|
"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": "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",
|
"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",
|
"dev": "npm run build:packages && npm run dev --workspace @nodedc/ui-catalog",
|
||||||
"serve": "node server/catalog-server.mjs",
|
"serve": "node server/catalog-server.mjs",
|
||||||
"validate:registry": "node scripts/validate-registry.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-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:map-filters": "node --test scripts/map-presentation-filters.test.mjs",
|
||||||
"test:hgeozone-projection": "node --test scripts/hgeozone-projection.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:map-cache-contract": "node --test scripts/map-cache-resource-contract.test.mjs",
|
||||||
"test:inspector-select": "node --test scripts/inspector-select-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