diff --git a/CHANGELOG.md b/CHANGELOG.md index 05145e09..edb76728 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ - Added a central operator scope registry with current municipality names and NIS codes, including Mol `13025` and Nijlen `12026`. - Added an explicit, idempotent VRBG scope provisioner that creates one regional boundary, 28 member boundaries, one regional Area, 28 municipality Areas and canonical source datasets through the existing API. - Added a compact Mol/Kempen region selector to the map-first explorer and made its heading and full-area action scope-neutral. +- Made region switches clear all project-bound state, ignore stale project-data responses and prefer the regional boundary/AOI, preventing Mol context from leaking into the Kempen workbench. - Kept thematic regional ingestion separate from the boundary pass so large GRB/WCS sources can be partitioned and validated without hidden startup fetches or truncated datasets. ## Sprint 188 Official modern Mol land-use series (2026-07-14) diff --git a/backend/tests/test_sprint189_kempen_scope.py b/backend/tests/test_sprint189_kempen_scope.py index 0dc94458..02163639 100644 --- a/backend/tests/test_sprint189_kempen_scope.py +++ b/backend/tests/test_sprint189_kempen_scope.py @@ -132,3 +132,17 @@ def test_kempen_scope_operator_is_packaged_and_exposed_in_map_flow() -> None: assert 'aria-label="Regio"' in workspace assert "projects={projects}" in app assert "onSelectProject={selectProject}" in app + + +def test_project_switches_reset_scoped_state_and_prefer_the_regional_context() -> None: + bootstrap = (ROOT / "frontend/src/hooks/useWorkbenchBootstrap.ts").read_text(encoding="utf-8") + project_workspace = (ROOT / "frontend/src/hooks/useProjectWorkspace.ts").read_text(encoding="utf-8") + map_state = (ROOT / "frontend/src/hooks/useMapWorkspaceState.ts").read_text(encoding="utf-8") + dataset_workflow = (ROOT / "frontend/src/hooks/useDatasetWorkflow.ts").read_text(encoding="utf-8") + + selected_project_branch = bootstrap.split("if (!selectedProjectId)", maxsplit=1)[1] + assert "resetProjectData()" in selected_project_branch + assert "resetDatasetForProject()" in selected_project_branch + assert "projectDataRequestSequence" in project_workspace + assert "vervoerregio|operationele grens" in map_state + assert "datasets.find(isOperationalScopeBoundaryDataset)" in dataset_workflow diff --git a/frontend/src/hooks/useDatasetWorkflow.ts b/frontend/src/hooks/useDatasetWorkflow.ts index b229df30..6b53a441 100644 --- a/frontend/src/hooks/useDatasetWorkflow.ts +++ b/frontend/src/hooks/useDatasetWorkflow.ts @@ -30,6 +30,14 @@ function toRasterMetadata(metadata: Record | null | undefined): return metadata as unknown as RasterMetadataResponse } +function isOperationalScopeBoundaryDataset(dataset: DatasetCreateResponse): boolean { + return ( + dataset.status === 'ready' && + (dataset.dataset_type === 'vector' || dataset.dataset_type === 'geojson') && + dataset.source_metadata?.layer_type === 'regional_boundary' + ) +} + function extractRasterTileManifestPath(job: JobRead | null | undefined): string { const manifest = toRasterTileHandoff(job) return manifest?.manifest_path ?? '' @@ -131,6 +139,7 @@ export function useDatasetWorkflow({ } // Auto-open the first usable dataset so Data, Map and Exports start with real context. const defaultDataset = + datasets.find(isOperationalScopeBoundaryDataset) ?? datasets.find(isPrimaryFocusMunicipalityBoundaryDataset) ?? datasets.find((dataset) => isVectorDatasetType(dataset.dataset_type) && dataset.status === 'ready') ?? datasets.find((dataset) => dataset.status === 'ready') ?? diff --git a/frontend/src/hooks/useMapWorkspaceState.ts b/frontend/src/hooks/useMapWorkspaceState.ts index cc85d75e..431c6b4d 100644 --- a/frontend/src/hooks/useMapWorkspaceState.ts +++ b/frontend/src/hooks/useMapWorkspaceState.ts @@ -31,9 +31,9 @@ export function useMapWorkspaceState({ if (areas.length === 0) { setSelectedMapAreaId('') } else if (!selectedMapAreaId || !areas.some((area) => area.id === selectedMapAreaId)) { - const municipalityArea = areas.find((area) => /gemeente mol|municipality/i.test(area.name)) - if (municipalityArea) { - setSelectedMapAreaId(municipalityArea.id) + const operationalScopeArea = areas.find((area) => /vervoerregio|operationele grens/i.test(area.name)) + if (operationalScopeArea) { + setSelectedMapAreaId(operationalScopeArea.id) } else { const largestArea = [...areas].sort((left, right) => (right.area_m2 ?? 0) - (left.area_m2 ?? 0))[0] if (largestArea?.area_m2) { diff --git a/frontend/src/hooks/useProjectWorkspace.ts b/frontend/src/hooks/useProjectWorkspace.ts index 28a134a4..1e509f64 100644 --- a/frontend/src/hooks/useProjectWorkspace.ts +++ b/frontend/src/hooks/useProjectWorkspace.ts @@ -1,4 +1,4 @@ -import { FormEvent, useMemo, useState } from 'react' +import { FormEvent, useMemo, useRef, useState } from 'react' import { PRIMARY_FOCUS_AREA_GEOJSON, PRIMARY_FOCUS_AREA_NAME, @@ -46,6 +46,7 @@ export function useProjectWorkspace() { const [loadingAreas, setLoadingAreas] = useState(false) const [loadingDatasets, setLoadingDatasets] = useState(false) const [errorMessage, setErrorMessage] = useState(null) + const projectDataRequestSequence = useRef(0) const [projectForm, setProjectForm] = useState({ name: '', @@ -145,20 +146,28 @@ export function useProjectWorkspace() { } const loadProjectData = async (projectId: string) => { + const requestId = ++projectDataRequestSequence.current setLoadingAreas(true) setLoadingDatasets(true) setErrorMessage(null) try { const projectData = await fetchProjectData(projectId) + if (requestId !== projectDataRequestSequence.current) { + return null + } setAreas(projectData.areas) setDatasets(projectData.datasets) return projectData } catch (error) { - setErrorMessage(error instanceof Error ? error.message : 'Failed to load project data') + if (requestId === projectDataRequestSequence.current) { + setErrorMessage(error instanceof Error ? error.message : 'Failed to load project data') + } return null } finally { - setLoadingAreas(false) - setLoadingDatasets(false) + if (requestId === projectDataRequestSequence.current) { + setLoadingAreas(false) + setLoadingDatasets(false) + } } } @@ -209,6 +218,7 @@ export function useProjectWorkspace() { } const resetProjectData = () => { + projectDataRequestSequence.current += 1 setAreas([]) setDatasets([]) } diff --git a/frontend/src/hooks/useWorkbenchBootstrap.ts b/frontend/src/hooks/useWorkbenchBootstrap.ts index 9502b07d..f8423f51 100644 --- a/frontend/src/hooks/useWorkbenchBootstrap.ts +++ b/frontend/src/hooks/useWorkbenchBootstrap.ts @@ -70,6 +70,11 @@ export function useWorkbenchBootstrap({ resetExportsForProject() return } + resetProjectData() + resetDatasetForProject() + resetDetectionForProject() + resetSegmentationForProject() + resetExportsForProject() loadProjectData(selectedProjectId).catch(() => null) loadDetectionRuns(selectedProjectId).catch(() => null) loadSegmentationRuns(selectedProjectId).catch(() => null)