fix(site-finder): MiniMap не клипает layer-controls — рыночные слои доступны (#1218)
All checks were successful
CI / changes (push) Successful in 7s
CI / backend-tests (push) Has been skipped
CI / changes (pull_request) Successful in 9s
CI / backend-tests (pull_request) Has been skipped
CI / frontend-tests (push) Successful in 49s
CI / frontend-tests (pull_request) Successful in 48s
All checks were successful
CI / changes (push) Successful in 7s
CI / backend-tests (push) Has been skipped
CI / changes (pull_request) Successful in 9s
CI / backend-tests (pull_request) Has been skipped
CI / frontend-tests (push) Successful in 49s
CI / frontend-tests (pull_request) Successful in 48s
Section 1 mini-map (/site-finder/analysis/[cad] — единственная карта
страницы) оборачивал SiteMap в height:320 + overflow:hidden. SiteMap
рендерит карту фиксированной высотой 420px + ПОД ней (flow) кнопку
"Добавить POI", легенду и CpLayerControlPanel с тумблерами
competitors/pipeline/risk/opportunity/redlines. Нижние 100px карты и
ВСЕ контролы обрезались clip-обёрткой → недостижимы кликом.
visibleMarketLayers init = {"competitors"} — единственный способ
включить остальные слои (#999 zone-of-risk, #958 §12.1-13 opportunity
parcels / red lines) — те самые скрытые чекбоксы → слои физически
невозможно было включить на странице анализа, хотя данные пробрасывались
props'ами ради них.
Patch (Вариант A из issue):
- SiteMap.tsx: новый prop mapHeight?: number (default 420 — backward-
compat для /site-finder/[cad]).
- MiniMap.tsx: снято height:320 + overflow:hidden; передаём mapHeight=280
(компактнее но не клипает); только width-constraint (maxWidth 400)
на обёртке.
2 новых vitest: mapHeight=280 пробрасывается в SiteMap (mocked stub),
clipping-стили отсутствуют. 82/82 frontend тестов зелёные. tsc + lint clean.
Closes #1218
This commit is contained in:
parent
8f3e461959
commit
ab45cacfbf
3 changed files with 100 additions and 18 deletions
|
|
@ -126,6 +126,11 @@ interface Props {
|
||||||
// §12.1-13 (#958) — opportunity-ЗУ + красные линии застройки (geom_wkt).
|
// §12.1-13 (#958) — opportunity-ЗУ + красные линии застройки (geom_wkt).
|
||||||
opportunityParcels?: OpportunityParcel[];
|
opportunityParcels?: OpportunityParcel[];
|
||||||
redLines?: RedLine[];
|
redLines?: RedLine[];
|
||||||
|
// #1218 — высота тайла карты (px). Default 420 — режим обычной странички
|
||||||
|
// /site-finder/[cad]. MiniMap на /analysis передаёт меньше (≈280), чтобы
|
||||||
|
// карта была компактнее. На высоту НЕ влияет на flow-контролы ниже
|
||||||
|
// (POI add button, легенда, CpLayerControlPanel) — они идут потоком.
|
||||||
|
mapHeight?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ключи переключаемых рыночных слоёв (#999 + §12.1-13 #958).
|
// Ключи переключаемых рыночных слоёв (#999 + §12.1-13 #958).
|
||||||
|
|
@ -167,6 +172,7 @@ export function SiteMap({
|
||||||
riskZones,
|
riskZones,
|
||||||
opportunityParcels,
|
opportunityParcels,
|
||||||
redLines,
|
redLines,
|
||||||
|
mapHeight = 420,
|
||||||
}: Props) {
|
}: Props) {
|
||||||
// Fix Leaflet default icon paths broken by webpack bundler
|
// Fix Leaflet default icon paths broken by webpack bundler
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -310,7 +316,7 @@ export function SiteMap({
|
||||||
border: "1px solid #e5e7eb",
|
border: "1px solid #e5e7eb",
|
||||||
borderRadius: 12,
|
borderRadius: 12,
|
||||||
overflow: "hidden",
|
overflow: "hidden",
|
||||||
height: 420,
|
height: mapHeight,
|
||||||
cursor: addMode ? "crosshair" : "grab",
|
cursor: addMode ? "crosshair" : "grab",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,15 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* MiniMap — compact Leaflet map (400×320) showing parcel polygon + top POI.
|
* MiniMap — compact Leaflet map (≈400px wide) showing parcel polygon + top POI.
|
||||||
* Dynamic import of react-leaflet (ssr:false) to avoid SSR breakage.
|
* Dynamic import of react-leaflet (ssr:false) to avoid SSR breakage.
|
||||||
* Wraps SiteMap with fixed dimensions for Section 1 layout.
|
* Wraps SiteMap with constrained width for Section 1 layout.
|
||||||
|
*
|
||||||
|
* #1218: ранее обёртка имела `height: 320 + overflow: hidden` — это клипало
|
||||||
|
* legend + CpLayerControlPanel, рендерящиеся flow-div ПОД картой в SiteMap,
|
||||||
|
* и делало недостижимыми переключатели рыночных слоёв (Будущие проекты,
|
||||||
|
* Зоны риска, Перспективные ЗУ, Красные линии). Фикс: задаём только ширину,
|
||||||
|
* высота тайла карты — через prop `mapHeight` SiteMap; контент течёт вниз.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import dynamic from "next/dynamic";
|
import dynamic from "next/dynamic";
|
||||||
|
|
@ -11,6 +17,10 @@ import type { ParcelAnalyzeResponse } from "@/lib/site-finder-api";
|
||||||
import type { ParcelAnalysis } from "@/types/site-finder";
|
import type { ParcelAnalysis } from "@/types/site-finder";
|
||||||
import type { Geometry } from "geojson";
|
import type { Geometry } from "geojson";
|
||||||
|
|
||||||
|
// MiniMap высота тайла карты — компромисс между плотностью Section 1 и
|
||||||
|
// читаемостью. Layer-controls/легенда вне этого размера (flow ниже).
|
||||||
|
const MINIMAP_HEIGHT = 280;
|
||||||
|
|
||||||
// Lazy-mount to avoid SSR breakage (Leaflet uses window)
|
// Lazy-mount to avoid SSR breakage (Leaflet uses window)
|
||||||
const SiteMap = dynamic(
|
const SiteMap = dynamic(
|
||||||
() =>
|
() =>
|
||||||
|
|
@ -22,7 +32,7 @@ const SiteMap = dynamic(
|
||||||
loading: () => (
|
loading: () => (
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
height: 320,
|
height: MINIMAP_HEIGHT,
|
||||||
background: "var(--bg-card-alt)",
|
background: "var(--bg-card-alt)",
|
||||||
borderRadius: 12,
|
borderRadius: 12,
|
||||||
border: "1px solid var(--border-card)",
|
border: "1px solid var(--border-card)",
|
||||||
|
|
@ -75,28 +85,27 @@ function toSiteMapData(data: ParcelAnalyzeResponse): ParcelAnalysis {
|
||||||
export function MiniMap({ data }: Props) {
|
export function MiniMap({ data }: Props) {
|
||||||
const adapted = toSiteMapData(data);
|
const adapted = toSiteMapData(data);
|
||||||
|
|
||||||
|
// #1218: НЕ задаём height + overflow:hidden на обёртке. SiteMap сам
|
||||||
|
// ограничивает высоту карты-тайла (mapHeight), а легенда + layer-controls
|
||||||
|
// (Будущие проекты / Зоны риска / Перспективные ЗУ / Красные линии) лежат
|
||||||
|
// под картой потоком и должны быть видны/кликабельны.
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
width: "100%",
|
width: "100%",
|
||||||
maxWidth: 400,
|
maxWidth: 400,
|
||||||
height: 320,
|
|
||||||
borderRadius: 12,
|
|
||||||
overflow: "hidden",
|
|
||||||
border: "1px solid var(--border-card)",
|
|
||||||
flexShrink: 0,
|
flexShrink: 0,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div style={{ height: 320 }}>
|
<SiteMap
|
||||||
<SiteMap
|
data={adapted}
|
||||||
data={adapted}
|
competitors={data.competitors ?? []}
|
||||||
competitors={data.competitors ?? []}
|
pipelineObjects={data.pipeline_24mo?.top_objects ?? []}
|
||||||
pipelineObjects={data.pipeline_24mo?.top_objects ?? []}
|
riskZones={data.nspd_risk_zones ?? []}
|
||||||
riskZones={data.nspd_risk_zones ?? []}
|
opportunityParcels={data.nspd_opportunity_parcels ?? []}
|
||||||
opportunityParcels={data.nspd_opportunity_parcels ?? []}
|
redLines={data.nspd_red_lines ?? []}
|
||||||
redLines={data.nspd_red_lines ?? []}
|
mapHeight={MINIMAP_HEIGHT}
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,67 @@
|
||||||
|
import { render, screen, waitFor } from "@testing-library/react";
|
||||||
|
import { describe, expect, it, vi } from "vitest";
|
||||||
|
|
||||||
|
import { MiniMap } from "../MiniMap";
|
||||||
|
import type { ParcelAnalyzeResponse } from "@/lib/site-finder-api";
|
||||||
|
|
||||||
|
// Stub SiteMap so we can assert the props MiniMap passes through (mapHeight
|
||||||
|
// in particular — #1218: ранее MiniMap клипал layer-панель/легенду через
|
||||||
|
// `overflow: hidden + height: 320`; теперь высота карты задаётся через prop).
|
||||||
|
// Stub does NOT use `unknown` — we capture a typed snapshot of the relevant
|
||||||
|
// props (mapHeight) for the assertion.
|
||||||
|
|
||||||
|
interface CapturedProps {
|
||||||
|
mapHeight?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const captured: { last: CapturedProps | null } = { last: null };
|
||||||
|
|
||||||
|
vi.mock("@/components/site-finder/SiteMap", () => ({
|
||||||
|
SiteMap: (props: CapturedProps) => {
|
||||||
|
captured.last = { mapHeight: props.mapHeight };
|
||||||
|
return <div data-testid="sitemap-stub">map(h={props.mapHeight ?? "?"})</div>;
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Minimal ParcelAnalyzeResponse — only fields MiniMap touches (toSiteMapData).
|
||||||
|
const FIXTURE: ParcelAnalyzeResponse = {
|
||||||
|
cad_num: "66:41:0000000:1",
|
||||||
|
source: "cad_building",
|
||||||
|
geom_geojson: null,
|
||||||
|
district: null,
|
||||||
|
score: 0,
|
||||||
|
score_breakdown: {},
|
||||||
|
poi_count: 0,
|
||||||
|
competitors: [],
|
||||||
|
pipeline_24mo: null,
|
||||||
|
nspd_risk_zones: [],
|
||||||
|
nspd_opportunity_parcels: [],
|
||||||
|
nspd_red_lines: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
describe("MiniMap (#1218)", () => {
|
||||||
|
it("passes mapHeight=280 to SiteMap (compact tile, controls flow below)", async () => {
|
||||||
|
render(<MiniMap data={FIXTURE} />);
|
||||||
|
// SiteMap is dynamically imported (ssr:false) — wait until our stub renders.
|
||||||
|
await waitFor(() =>
|
||||||
|
expect(screen.getByTestId("sitemap-stub")).toBeInTheDocument(),
|
||||||
|
);
|
||||||
|
expect(captured.last?.mapHeight).toBe(280);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("wrapper does NOT clip overflow — layer controls must remain visible", async () => {
|
||||||
|
const { container } = render(<MiniMap data={FIXTURE} />);
|
||||||
|
await waitFor(() =>
|
||||||
|
expect(screen.getByTestId("sitemap-stub")).toBeInTheDocument(),
|
||||||
|
);
|
||||||
|
// The outer wrapper is the first element child — SiteMap stub lives inside.
|
||||||
|
const wrapper = container.firstElementChild as HTMLElement | null;
|
||||||
|
expect(wrapper).not.toBeNull();
|
||||||
|
// No fixed height (must be empty so flow children — legend + control panel —
|
||||||
|
// are not clipped to 320px), and no overflow: hidden.
|
||||||
|
expect(wrapper?.style.height).toBe("");
|
||||||
|
expect(wrapper?.style.overflow).toBe("");
|
||||||
|
// Width constraint preserved (column slot is 400px in Section 1 grid).
|
||||||
|
expect(wrapper?.style.maxWidth).toBe("400px");
|
||||||
|
});
|
||||||
|
});
|
||||||
Loading…
Add table
Reference in a new issue