feat(map): add reference layers and stabilize workspace controls
This commit is contained in:
@@ -32,6 +32,13 @@ import {
|
||||
type CameraSurveySelection,
|
||||
} from "./mapCameraPresets.js";
|
||||
import { buildMapSubjectCardModel, DEFAULT_MAP_SUBJECT_DETAIL_PROFILE } from "./mapSubjectCard.mjs";
|
||||
import {
|
||||
ensureMapReferencePresentationProfiles,
|
||||
initialMapReferenceLayers,
|
||||
isMapReferencePresentationProfile,
|
||||
type MapReferenceLayer,
|
||||
} from "./mapReferenceStations.js";
|
||||
import { useMapReferenceRuntime } from "./useMapReferenceRuntime.js";
|
||||
const CesiumMapRenderer = lazy(() => import("./CesiumMapRenderer.js").then((module) => ({ default: module.CesiumMapRenderer })));
|
||||
|
||||
type PreviewFeatures = { inspector?: boolean; toolbar?: boolean; assistant?: boolean };
|
||||
@@ -171,6 +178,8 @@ export type MapPageLayout = {
|
||||
subjectDetailProfiles: MapSubjectDetailProfile[];
|
||||
dataProductBindings: MapDataProductBinding[];
|
||||
subjectStates: MapSubjectState[];
|
||||
referenceLayers: MapReferenceLayer[];
|
||||
inspectorOpenSections: string[];
|
||||
savedAt?: string;
|
||||
};
|
||||
|
||||
@@ -247,10 +256,12 @@ export function createDefaultMapPageLayout(expanded = false): MapPageLayout {
|
||||
mapHeight: expanded ? 620 : 470,
|
||||
camera: { ...fallbackMapCamera },
|
||||
pinBindings: [],
|
||||
presentationProfiles: [],
|
||||
presentationProfiles: ensureMapReferencePresentationProfiles([]),
|
||||
subjectDetailProfiles: [structuredClone(DEFAULT_MAP_SUBJECT_DETAIL_PROFILE) as MapSubjectDetailProfile],
|
||||
dataProductBindings: [],
|
||||
subjectStates: [],
|
||||
referenceLayers: initialMapReferenceLayers(),
|
||||
inspectorOpenSections: ["map-base"],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -354,7 +365,11 @@ export const MapFixturePreview = forwardRef<MapFixturePreviewHandle, {
|
||||
const [subjectCardZIndex, setSubjectCardZIndex] = useState(140);
|
||||
const [subjectCardActive, setSubjectCardActive] = useState(false);
|
||||
const [subjectCardTabId, setSubjectCardTabId] = useState("overview");
|
||||
const [expandedFacetRows, setExpandedFacetRows] = useState<Record<string, boolean>>({});
|
||||
const [inspectorOpen, setInspectorOpen] = useState(false);
|
||||
const [inspectorOpenSections, setInspectorOpenSections] = useState<string[]>(() => (
|
||||
initialLayout?.inspectorOpenSections ?? ["map-base"]
|
||||
));
|
||||
const [layersOpen, setLayersOpen] = useState(false);
|
||||
const [layersWindowRect, setLayersWindowRect] = useState<WorkspaceWindowRect>(defaultLayersWindowRect);
|
||||
const [layersWindowMaximized, setLayersWindowMaximized] = useState(false);
|
||||
@@ -388,7 +403,7 @@ export const MapFixturePreview = forwardRef<MapFixturePreviewHandle, {
|
||||
// Presentation profiles are application/page-owned, versioned map.style_profile
|
||||
// values. A human camera/settings save must preserve profiles provisioned by MCP.
|
||||
const [presentationProfiles, setPresentationProfiles] = useState<MapPresentationProfile[]>(() => (
|
||||
normalizeClientMapPresentationProfiles(initialLayout?.presentationProfiles ?? [])
|
||||
ensureMapReferencePresentationProfiles(normalizeClientMapPresentationProfiles(initialLayout?.presentationProfiles ?? []))
|
||||
));
|
||||
const [subjectDetailProfiles] = useState<MapSubjectDetailProfile[]>(() => (
|
||||
initialLayout?.subjectDetailProfiles?.length
|
||||
@@ -399,6 +414,9 @@ export const MapFixturePreview = forwardRef<MapFixturePreviewHandle, {
|
||||
// not belong to the visual inspector. Preserve them verbatim when a human
|
||||
// edits camera or presentation settings and saves the page layout.
|
||||
const [dataProductBindings] = useState<MapDataProductBinding[]>(() => initialLayout?.dataProductBindings ?? []);
|
||||
const [referenceLayers, setReferenceLayers] = useState<MapReferenceLayer[]>(() => (
|
||||
initialMapReferenceLayers(initialLayout?.referenceLayers)
|
||||
));
|
||||
const [subjectStates, setSubjectStates] = useState<Record<string, MapSubjectState>>(() => (
|
||||
initialSubjectState(initialLayout?.dataProductBindings ?? [], initialLayout?.subjectStates)
|
||||
));
|
||||
@@ -415,10 +433,23 @@ export const MapFixturePreview = forwardRef<MapFixturePreviewHandle, {
|
||||
bindings: dataProductBindings,
|
||||
enabled: Boolean(applicationId && pageId),
|
||||
});
|
||||
const referenceRuntimeBindings = useMapReferenceRuntime(referenceLayers, mapCamera, true);
|
||||
const referencePresentationFilters = useMemo<MapPresentationFilters>(() => Object.fromEntries(
|
||||
referenceLayers.map((layer) => [layer.id, { visible: layer.visible, facets: {} }]),
|
||||
), [referenceLayers]);
|
||||
const rendererPresentationFilters = useMemo<MapPresentationFilters>(() => ({
|
||||
...presentationFilters,
|
||||
...referencePresentationFilters,
|
||||
}), [presentationFilters, referencePresentationFilters]);
|
||||
const primaryBindingIds = useMemo(() => new Set(
|
||||
dataProductBindings.filter((binding) => !binding.joinToBindingId).map((binding) => binding.id),
|
||||
), [dataProductBindings]);
|
||||
const primaryRuntimeBindings = useMemo(() => (
|
||||
runtimeBindings.filter((binding) => primaryBindingIds.has(binding.bindingId))
|
||||
), [primaryBindingIds, runtimeBindings]);
|
||||
const selectable = useMemo(() => (
|
||||
runtimeBindings.flatMap((binding) => {
|
||||
primaryRuntimeBindings.flatMap((binding) => {
|
||||
const bindingConfig = dataProductBindings.find((candidate) => candidate.id === binding.bindingId);
|
||||
if (bindingConfig?.joinToBindingId) return [];
|
||||
const facts = [...binding.facts];
|
||||
const primaryProfile = mapPresentationProfileForFact(
|
||||
presentationProfiles,
|
||||
@@ -440,8 +471,9 @@ export const MapFixturePreview = forwardRef<MapFixturePreviewHandle, {
|
||||
};
|
||||
});
|
||||
})
|
||||
), [dataProductBindings, presentationProfiles, runtimeBindings]);
|
||||
), [dataProductBindings, presentationProfiles, primaryRuntimeBindings]);
|
||||
const presentationSummaries = useMemo(() => [...dataProductBindings]
|
||||
.filter((binding) => !binding.joinToBindingId)
|
||||
.sort((left, right) => (left.order ?? 0) - (right.order ?? 0) || left.id.localeCompare(right.id))
|
||||
.flatMap((bindingConfig) => {
|
||||
const binding = runtimeBindings.find((candidate) => candidate.bindingId === bindingConfig.id);
|
||||
@@ -457,7 +489,18 @@ export const MapFixturePreview = forwardRef<MapFixturePreviewHandle, {
|
||||
counts: mapPresentationFacetCounts(facts, profile),
|
||||
}];
|
||||
}), [dataProductBindings, presentationProfiles, runtimeBindings]);
|
||||
const filteredTargets = useMemo(() => runtimeBindings.flatMap((binding) => {
|
||||
const referenceObjectSummaries = useMemo(() => referenceLayers.flatMap((layer) => {
|
||||
const profile = presentationProfiles.find((candidate) => candidate.id === layer.presentationProfileId);
|
||||
if (!profile) return [];
|
||||
const runtime = referenceRuntimeBindings.find((candidate) => candidate.bindingId === layer.id);
|
||||
return [{
|
||||
layer,
|
||||
displayName: profile.title,
|
||||
total: runtime?.facts.length ?? 0,
|
||||
}];
|
||||
}), [presentationProfiles, referenceLayers, referenceRuntimeBindings]);
|
||||
const objectLayerCount = presentationSummaries.length + referenceObjectSummaries.length;
|
||||
const filteredTargets = useMemo(() => primaryRuntimeBindings.flatMap((binding) => {
|
||||
const bindingConfig = dataProductBindings.find((candidate) => candidate.id === binding.bindingId);
|
||||
return binding.facts.flatMap((fact) => {
|
||||
const profile = mapPresentationProfileForFact(
|
||||
@@ -475,7 +518,7 @@ export const MapFixturePreview = forwardRef<MapFixturePreviewHandle, {
|
||||
renderable: mapRuntimeFactIsRenderable(fact, profile),
|
||||
}];
|
||||
});
|
||||
}).sort((left, right) => left.title.localeCompare(right.title, "ru")), [dataProductBindings, presentationFilters, presentationProfiles, runtimeBindings]);
|
||||
}).sort((left, right) => left.title.localeCompare(right.title, "ru")), [dataProductBindings, presentationFilters, presentationProfiles, primaryRuntimeBindings]);
|
||||
const visibleTargetEntityIds = useMemo(() => (
|
||||
filteredTargets.filter((target) => target.renderable).map((target) => target.entityId)
|
||||
), [filteredTargets]);
|
||||
@@ -714,6 +757,8 @@ export const MapFixturePreview = forwardRef<MapFixturePreviewHandle, {
|
||||
presentationProfiles,
|
||||
subjectDetailProfiles,
|
||||
dataProductBindings,
|
||||
referenceLayers,
|
||||
inspectorOpenSections,
|
||||
subjectStates: dataProductBindings.map((binding) => subjectStates[binding.id] ?? {
|
||||
bindingId: binding.id,
|
||||
visible: true,
|
||||
@@ -726,7 +771,7 @@ export const MapFixturePreview = forwardRef<MapFixturePreviewHandle, {
|
||||
: state;
|
||||
}),
|
||||
}),
|
||||
}), [dataProductBindings, mapCamera, mapHeight, mapSettings, pinBindings, presentationProfiles, presentationSummaries, subjectDetailProfiles, subjectStates]);
|
||||
}), [dataProductBindings, inspectorOpenSections, mapCamera, mapHeight, mapSettings, pinBindings, presentationProfiles, presentationSummaries, referenceLayers, subjectDetailProfiles, subjectStates]);
|
||||
|
||||
const updateSubjectState = (bindingId: string, update: (state: MapSubjectState) => MapSubjectState) => {
|
||||
setSubjectStates((current) => {
|
||||
@@ -811,7 +856,11 @@ export const MapFixturePreview = forwardRef<MapFixturePreviewHandle, {
|
||||
const binding = dataProductBindings.find((candidate) => candidate.id === entity?.bindingId);
|
||||
const profile = subjectDetailProfiles.find((candidate) => candidate.id === binding?.subjectDetailProfileId)
|
||||
?? subjectDetailProfiles.find((candidate) => candidate.semanticTypes.includes(entity?.fact.semanticType ?? ""));
|
||||
setSubjectCardTabId(profile?.defaultTabId ?? "overview");
|
||||
setSubjectCardTabId((current) => (
|
||||
profile?.tabs.some((tab) => tab.id === current)
|
||||
? current
|
||||
: (profile?.defaultTabId ?? "overview")
|
||||
));
|
||||
setSubjectCardOpen(true);
|
||||
setSubjectCardActive(true);
|
||||
setLayersWindowActive(false);
|
||||
@@ -819,6 +868,11 @@ export const MapFixturePreview = forwardRef<MapFixturePreviewHandle, {
|
||||
setSubjectCardZIndex((current) => Math.max(current, layersWindowZIndex, ...Object.values(subjectStates).map((state) => state.window.zIndex)) + 1);
|
||||
}, [dataProductBindings, layersWindowZIndex, selectable, subjectDetailProfiles, subjectStates]);
|
||||
|
||||
const handleSelectAndFocus = useCallback((entityId: string) => {
|
||||
handleSelect(entityId);
|
||||
mapRendererRef.current?.focusRuntimeEntity(entityId);
|
||||
}, [handleSelect]);
|
||||
|
||||
const rememberGatewayHealth = useCallback((health: MapGatewayHealth) => {
|
||||
gatewayHealthRef.current = health;
|
||||
setGatewayHealth(health);
|
||||
@@ -1010,14 +1064,26 @@ export const MapFixturePreview = forwardRef<MapFixturePreviewHandle, {
|
||||
<RangeControl label="Детализация" value={mapSettings.buildingsDetail} min={4} max={32} formatValue={(value) => `SSE ${value}`} onChange={(buildingsDetail) => updateMapSettings({ buildingsDetail })} />
|
||||
</>,
|
||||
},
|
||||
...presentationProfiles.flatMap((profile) => [
|
||||
...presentationProfiles.flatMap((profile) => {
|
||||
const referenceProfile = isMapReferencePresentationProfile(profile);
|
||||
const referenceLayer = referenceLayers.find((layer) => layer.presentationProfileId === profile.id);
|
||||
return [
|
||||
{
|
||||
id: `map-target-${profile.id}`,
|
||||
label: profile.target.variant === "surface-fill" ? "HGeoZone" : "Таргет",
|
||||
label: referenceProfile ? profile.title : profile.target.variant === "surface-fill" ? "HGeoZone" : "Таргет",
|
||||
description: profile.target.variant === "surface-fill" ? `проекция · ${profile.title}` : profile.title,
|
||||
group: profile.target.variant === "surface-fill" ? "Слои" : "Таргеты",
|
||||
group: referenceProfile ? "Станции" : profile.target.variant === "surface-fill" ? "Слои" : "Таргеты",
|
||||
content: <>
|
||||
<small className="catalog-map-inspector__note">Профиль принадлежит этой странице Application и управляется тем же provider-neutral MCP-контрактом. Исходный API в настройках отсутствует.</small>
|
||||
{referenceLayer ? (
|
||||
<Checker
|
||||
checked={referenceLayer.visible}
|
||||
label={`Показывать слой «${profile.title}»`}
|
||||
onChange={(visible) => setReferenceLayers((current) => current.map((layer) => (
|
||||
layer.id === referenceLayer.id ? { ...layer, visible } : layer
|
||||
)))}
|
||||
/>
|
||||
) : null}
|
||||
{profile.target.variant === "surface-fill" && <>
|
||||
<ControlRow label="Тип слоя"><strong>HGeoZone · ground projection</strong></ControlRow>
|
||||
{profile.styles.map((style) => {
|
||||
@@ -1054,14 +1120,9 @@ export const MapFixturePreview = forwardRef<MapFixturePreviewHandle, {
|
||||
<RangeControl label={profile.target.variant === "surface-fill" ? "Скрывать HGeoZone выше" : "Скрывать таргет выше"} value={profile.target.hideCameraHeightMeters} min={1_000} max={500_000} step={1_000} formatValue={(value) => `${Math.round(value / 1_000)} км`} onChange={(hideCameraHeightMeters) => updatePresentationProfile(profile.id, (current) => ({ ...current, target: { ...current.target, hideCameraHeightMeters } }))} />
|
||||
<ControlRow label="Фон плашки"><ColorField label="Цвет фона подписи" value={profile.label.backgroundColor} onChange={(backgroundColor) => updatePresentationProfile(profile.id, (current) => ({ ...current, label: { ...current.label, backgroundColor } }))} /></ControlRow>
|
||||
<RangeControl label="Прозрачность плашки" value={Math.round(profile.label.backgroundOpacity * 100)} min={0} max={100} step={1} formatValue={(value) => `${value}%`} onChange={(value) => updatePresentationProfile(profile.id, (current) => ({ ...current, label: { ...current.label, backgroundOpacity: value / 100 } }))} />
|
||||
{profile.target.variant === "elevated-spike" && <>
|
||||
<ControlRow label="Обводка таргета"><ColorField label="Цвет обводки таргета" value={profile.target.outlineColor} onChange={(outlineColor) => updatePresentationProfile(profile.id, (current) => ({ ...current, target: { ...current.target, outlineColor } }))} /></ControlRow>
|
||||
<RangeControl label="Прозрачность обводки" value={Math.round(profile.target.outlineOpacity * 100)} min={0} max={100} step={1} formatValue={(value) => `${value}%`} onChange={(value) => updatePresentationProfile(profile.id, (current) => ({ ...current, target: { ...current.target, outlineOpacity: value / 100 } }))} />
|
||||
<RangeControl label="Толщина обводки" value={profile.target.outlineWidthPx} min={0} max={8} step={0.5} formatValue={(value) => `${value} px`} onChange={(outlineWidthPx) => updatePresentationProfile(profile.id, (current) => ({ ...current, target: { ...current.target, outlineWidthPx } }))} />
|
||||
</>}
|
||||
</>,
|
||||
},
|
||||
...(profile.target.variant === "surface-fill" ? [] : [{
|
||||
...(profile.target.variant === "surface-fill" || referenceProfile ? [] : [{
|
||||
id: `map-state-classes-${profile.id}`,
|
||||
label: "Классы состояния",
|
||||
description: "нормализованные фасеты онтологии",
|
||||
@@ -1078,7 +1139,8 @@ export const MapFixturePreview = forwardRef<MapFixturePreviewHandle, {
|
||||
})}
|
||||
</>,
|
||||
}]),
|
||||
]),
|
||||
];
|
||||
}),
|
||||
{
|
||||
id: "map-grid",
|
||||
label: "Сетка и LOD",
|
||||
@@ -1239,9 +1301,9 @@ export const MapFixturePreview = forwardRef<MapFixturePreviewHandle, {
|
||||
onSpiralStateChange={handleSpiralStateChange}
|
||||
initialCamera={mapCamera ?? undefined}
|
||||
presentation={presentation}
|
||||
runtimeBindings={runtimeBindings}
|
||||
runtimeBindings={[...primaryRuntimeBindings, ...referenceRuntimeBindings]}
|
||||
presentationProfiles={presentationProfiles}
|
||||
presentationFilters={presentationFilters}
|
||||
presentationFilters={rendererPresentationFilters}
|
||||
/>
|
||||
</Suspense>
|
||||
|
||||
@@ -1308,7 +1370,7 @@ export const MapFixturePreview = forwardRef<MapFixturePreviewHandle, {
|
||||
<div className="catalog-map-fixture__objects-menu-list">
|
||||
<div className="catalog-map-fixture__objects-menu-head">
|
||||
<strong>Объекты</strong>
|
||||
<small>{presentationSummaries.length} {presentationSummaries.length === 1 ? "группа" : "групп"}</small>
|
||||
<small>{objectLayerCount} {objectLayerCount === 1 ? "группа" : "групп"}</small>
|
||||
</div>
|
||||
{presentationSummaries.map((summary) => {
|
||||
const state = subjectStates[summary.bindingId];
|
||||
@@ -1324,30 +1386,45 @@ export const MapFixturePreview = forwardRef<MapFixturePreviewHandle, {
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
role="menuitemcheckbox"
|
||||
aria-checked={visible}
|
||||
role={hasControls ? "menuitem" : "menuitemcheckbox"}
|
||||
aria-checked={hasControls ? undefined : visible}
|
||||
className="catalog-map-fixture__objects-menu-toggle"
|
||||
onClick={() => toggleSubjectVisibility(summary.bindingId)}
|
||||
onClick={() => {
|
||||
if (hasControls) {
|
||||
openSubjectWindow(summary.bindingId);
|
||||
close();
|
||||
return;
|
||||
}
|
||||
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();
|
||||
}}
|
||||
><Icon name="settings" /></IconButton>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
{!presentationSummaries.length ? <small className="catalog-map-fixture__objects-menu-empty">Нет подключённых объектов.</small> : null}
|
||||
{referenceObjectSummaries.map(({ layer, displayName, total }) => (
|
||||
<div
|
||||
className="catalog-map-fixture__objects-menu-item"
|
||||
key={layer.id}
|
||||
data-visible={layer.visible || undefined}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
role="menuitemcheckbox"
|
||||
aria-checked={layer.visible}
|
||||
className="catalog-map-fixture__objects-menu-toggle"
|
||||
onClick={() => setReferenceLayers((current) => current.map((candidate) => (
|
||||
candidate.id === layer.id ? { ...candidate, visible: !candidate.visible } : candidate
|
||||
)))}
|
||||
>
|
||||
<span>{displayName}</span>
|
||||
<small>{layer.visible ? `на карте: ${total}` : `слой скрыт · ${total} объектов`}</small>
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
{!objectLayerCount ? <small className="catalog-map-fixture__objects-menu-empty">Нет подключённых объектов.</small> : null}
|
||||
</div>
|
||||
)}
|
||||
</Dropdown>
|
||||
@@ -1382,6 +1459,7 @@ export const MapFixturePreview = forwardRef<MapFixturePreviewHandle, {
|
||||
zIndex={state.window.zIndex}
|
||||
minWidth={240}
|
||||
minHeight={220}
|
||||
autoHeight
|
||||
className="catalog-map-fixture__subject-window catalog-map-fixture__map-glass-window"
|
||||
>
|
||||
<div className="catalog-map-fixture__target-filters">
|
||||
@@ -1390,17 +1468,63 @@ export const MapFixturePreview = forwardRef<MapFixturePreviewHandle, {
|
||||
{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 rowId = `${summary.bindingId}:${facet.field}:${item.value}`;
|
||||
const expanded = Boolean(expandedFacetRows[rowId]);
|
||||
const matchingEntities = selectable.filter((entity) => (
|
||||
entity.bindingId === summary.bindingId
|
||||
&& mapFactMatchesFilters(
|
||||
entity.fact,
|
||||
summary.profile,
|
||||
{
|
||||
[summary.bindingId]: {
|
||||
visible: true,
|
||||
facets: { [facet.field]: [item.value] },
|
||||
},
|
||||
},
|
||||
summary.bindingId,
|
||||
)
|
||||
));
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
key={`${facet.field}:${item.value}`}
|
||||
aria-pressed={active}
|
||||
data-active={active || undefined}
|
||||
disabled={!facet.filterable}
|
||||
onClick={() => togglePresentationFilter(summary.bindingId, facet.field, item.value)}
|
||||
>
|
||||
{item.label} <span>{summary.counts[facet.field]?.[item.value] ?? 0}</span>
|
||||
</button>
|
||||
<div className="catalog-map-fixture__target-filter-branch" key={`${facet.field}:${item.value}`}>
|
||||
<div className="catalog-map-fixture__target-filter-row" data-active={active || undefined}>
|
||||
<button
|
||||
type="button"
|
||||
className="catalog-map-fixture__target-filter-body"
|
||||
aria-pressed={active}
|
||||
disabled={!facet.filterable}
|
||||
onClick={() => togglePresentationFilter(summary.bindingId, facet.field, item.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>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="catalog-map-fixture__target-filter-expander"
|
||||
aria-label={`${expanded ? "Свернуть" : "Развернуть"} ${item.label}`}
|
||||
aria-expanded={expanded}
|
||||
onClick={() => setExpandedFacetRows((current) => ({ ...current, [rowId]: !current[rowId] }))}
|
||||
>
|
||||
<Icon name="chevron-right" size={14} />
|
||||
</button>
|
||||
</div>
|
||||
{expanded ? (
|
||||
<div className="catalog-map-fixture__target-filter-children">
|
||||
{matchingEntities.map((entity) => (
|
||||
<button
|
||||
type="button"
|
||||
className="catalog-map-fixture__target-filter-entity"
|
||||
key={entity.id}
|
||||
data-selected={entity.id === selectedId || undefined}
|
||||
onClick={() => handleSelectAndFocus(entity.id)}
|
||||
>
|
||||
<span>{entity.title}</span>
|
||||
{entity.status ? <small>{entity.status}</small> : null}
|
||||
</button>
|
||||
))}
|
||||
{!matchingEntities.length ? <small>Нет объектов в группе.</small> : null}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
})
|
||||
))}
|
||||
@@ -1501,7 +1625,12 @@ export const MapFixturePreview = forwardRef<MapFixturePreviewHandle, {
|
||||
className="catalog-map-fixture__map-settings-window"
|
||||
onClose={() => setInspectorOpen(false)}
|
||||
>
|
||||
<Inspector sections={inspectorSections} defaultOpen={["map-base"]} singleOpen />
|
||||
<Inspector
|
||||
sections={inspectorSections}
|
||||
openSections={inspectorOpenSections}
|
||||
singleOpen
|
||||
onOpenSectionsChange={setInspectorOpenSections}
|
||||
/>
|
||||
</Window>
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user