Extract demo workflow hook
GeoIntel CI / docs-smoke (push) Has been cancelled
GeoIntel CI / contract-smoke (push) Has been cancelled

This commit is contained in:
Codex
2026-06-17 07:52:14 +02:00
parent 0c135b420d
commit 6d655a1f1a
8 changed files with 165 additions and 44 deletions
+7
View File
@@ -7,6 +7,13 @@
# Changelog # Changelog
## Sprint 41 Demo workflow hook decomposition (2026-06-17)
- Moved offline demo workflow orchestration from `App.tsx` into `frontend/src/hooks/useDemoWorkflow.ts`.
- The hook keeps the existing cross-module selection behavior for project, candidate/reference datasets, map AOI, QA/QC, detection, segmentation and export refresh state.
- Extended frontend orchestration regression tests so `demoApi` stays out of `App.tsx`.
- No API contracts, migrations, product features, provider fetching or AI behavior were introduced.
## Sprint 40 Project workspace hook decomposition (2026-06-17) ## Sprint 40 Project workspace hook decomposition (2026-06-17)
- Moved project listing/creation, area creation and project-scoped area/dataset loading from `App.tsx` into `frontend/src/hooks/useProjectWorkspace.ts`. - Moved project listing/creation, area creation and project-scoped area/dataset loading from `App.tsx` into `frontend/src/hooks/useProjectWorkspace.ts`.
@@ -22,8 +22,8 @@ def test_demo_workflow_browser_smoke_script_checks_connected_v1_state() -> None:
def test_frontend_demo_action_loads_candidate_dataset_details_for_map_layer() -> None: def test_frontend_demo_action_loads_candidate_dataset_details_for_map_layer() -> None:
app = (ROOT / "frontend" / "src" / "App.tsx").read_text(encoding="utf-8") demo_hook = (ROOT / "frontend" / "src" / "hooks" / "useDemoWorkflow.ts").read_text(encoding="utf-8")
assert "const candidateDataset = projectData?.datasets.find" in app assert "const candidateDataset = projectData?.datasets.find" in demo_hook
assert "dataset.id === result.candidate_dataset_id" in app assert "dataset.id === result.candidate_dataset_id" in demo_hook
assert "await loadDatasetDetails(result.project_id, candidateDataset)" in app assert "await loadDatasetDetails(result.project_id, candidateDataset)" in demo_hook
@@ -10,20 +10,37 @@ def test_app_uses_shared_orchestration_hooks() -> None:
app = (ROOT / "frontend" / "src" / "App.tsx").read_text(encoding="utf-8") app = (ROOT / "frontend" / "src" / "App.tsx").read_text(encoding="utf-8")
assert "useProjectWorkspace" in app assert "useProjectWorkspace" in app
assert "useDemoWorkflow" in app
assert "useProviderCapabilities" in app assert "useProviderCapabilities" in app
assert "useChangeDetectionWorkflow" in app assert "useChangeDetectionWorkflow" in app
assert "useMapWorkspaceState" in app assert "useMapWorkspaceState" in app
assert "from './hooks/useProjectWorkspace'" in app assert "from './hooks/useProjectWorkspace'" in app
assert "from './hooks/useDemoWorkflow'" in app
assert "from './hooks/useProviderCapabilities'" in app assert "from './hooks/useProviderCapabilities'" in app
assert "from './hooks/useChangeDetectionWorkflow'" in app assert "from './hooks/useChangeDetectionWorkflow'" in app
assert "from './hooks/useMapWorkspaceState'" in app assert "from './hooks/useMapWorkspaceState'" in app
assert "projectsApi" not in app assert "projectsApi" not in app
assert "areasApi" not in app assert "areasApi" not in app
assert "datasetsApi" not in app assert "datasetsApi" not in app
assert "demoApi" not in app
assert "analysisApi" not in app assert "analysisApi" not in app
assert "externalApi" not in app assert "externalApi" not in app
def test_demo_workflow_hook_owns_demo_api_and_cross_module_selection() -> None:
hook = (ROOT / "frontend" / "src" / "hooks" / "useDemoWorkflow.ts").read_text(encoding="utf-8")
assert "demoApi.seedWorkflow" in hook
assert "loadDemoWorkflow" in hook
assert "setSelectedProjectId(result.project_id)" in hook
assert "setSelectedDatasetId(result.candidate_dataset_id)" in hook
assert "setSelectedMapAreaId(result.area_id)" in hook
assert "setQaCandidateDatasetId(result.candidate_dataset_id)" in hook
assert "setDetectionReferenceDatasetId(result.reference_dataset_id)" in hook
assert "setSegmentationReferenceDatasetId(result.reference_dataset_id)" in hook
assert "loadDatasetDetails(result.project_id, candidateDataset)" in hook
def test_project_workspace_hook_owns_project_area_dataset_loading() -> None: def test_project_workspace_hook_owns_project_area_dataset_loading() -> None:
hook = (ROOT / "frontend" / "src" / "hooks" / "useProjectWorkspace.ts").read_text(encoding="utf-8") hook = (ROOT / "frontend" / "src" / "hooks" / "useProjectWorkspace.ts").read_text(encoding="utf-8")
+26
View File
@@ -1,3 +1,29 @@
## Sprint 41 Demo workflow hook decomposition (2026-06-17)
Changed:
- Moved offline demo workflow orchestration from `frontend/src/App.tsx` into `frontend/src/hooks/useDemoWorkflow.ts`.
- Preserved the existing backend fixture seed flow and all cross-module selection updates for project, candidate/reference datasets, map AOI, QA/QC, detection, segmentation and exports.
- Extended static frontend orchestration tests so `demoApi.seedWorkflow` is owned by the new hook and not `App.tsx`.
- Updated frontend README, changelog and TODO status.
Validation:
- `cd backend && python -m pytest tests/test_sprint21_demo_workflow_smoke.py tests/test_sprint39_frontend_orchestration_hooks.py -q` passed: 9 tests.
- `python -m compileall backend/app` passed.
- `cd backend && python -m pytest -W error::DeprecationWarning` passed: 191 tests.
- `bash scripts/run_readiness_check.sh` passed.
- `cd frontend && npm run typecheck` passed.
- `cd frontend && npm run build` passed.
- `cd backend && python -m alembic heads` passed: `202606120900 (head)`.
- `cd backend && python -m alembic upgrade head --sql` passed.
- `bash -n scripts/live_migration_smoke.sh` passed.
Limitations:
- No UX behavior, API contracts, migrations, provider fetching or AI behavior changed.
- `App.tsx` still has a UTF-8 BOM and a stale React import cleanup opportunity; TypeScript build is unaffected.
Next recommended pass:
- Commit, deploy to Tower and run browser/runtime smoke, then do a final `App.tsx` size/import cleanup pass.
## Sprint 40 Project workspace hook decomposition (2026-06-17) ## Sprint 40 Project workspace hook decomposition (2026-06-17)
Changed: Changed:
+2 -1
View File
@@ -53,7 +53,8 @@ This file now starts with the current implementation status. Older preparation/b
- [x] Export preview component decomposition and HTML report download-only UX hardening. - [x] Export preview component decomposition and HTML report download-only UX hardening.
- [x] Provider, change-detection and map-workspace orchestration hook decomposition. - [x] Provider, change-detection and map-workspace orchestration hook decomposition.
- [x] Project/area/dataset cross-load orchestration hook decomposition. - [x] Project/area/dataset cross-load orchestration hook decomposition.
- [ ] Further demo workflow orchestration decomposition. - [x] Demo workflow orchestration hook decomposition.
- [ ] Final `App.tsx` import/encoding cleanup and size audit.
## Sprint 8 status ## Sprint 8 status
+1
View File
@@ -215,6 +215,7 @@ React + TypeScript + MapLibre foundation for project/area/dataset workflow.
- Derived MapLibre workbench state lives in `src/hooks/useMapWorkspaceState.ts`. - Derived MapLibre workbench state lives in `src/hooks/useMapWorkspaceState.ts`.
- Project, area and dataset-list loading lives in `src/hooks/useProjectWorkspace.ts`. - Project, area and dataset-list loading lives in `src/hooks/useProjectWorkspace.ts`.
- Area fallback selection now lives with the owning workflow hooks: clip-area selection in `useDatasetWorkflow.ts` and map-area selection in `useMapWorkspaceState.ts`. - Area fallback selection now lives with the owning workflow hooks: clip-area selection in `useDatasetWorkflow.ts` and map-area selection in `useMapWorkspaceState.ts`.
- Offline demo workflow orchestration lives in `src/hooks/useDemoWorkflow.ts`, because it coordinates project, dataset, map, QA/QC, detection, segmentation and export state after the backend fixture seed.
## Raster dependency visibility ## Raster dependency visibility
+23 -39
View File
@@ -1,6 +1,5 @@
import { FormEvent, useEffect, useMemo, useState } from 'react' import { FormEvent, useEffect, useMemo, useState } from 'react'
import './styles/app.css' import './styles/app.css'
import { demoApi } from './services/api'
import { ChangeDetectionPanel } from './components/analysis/ChangeDetectionPanel' import { ChangeDetectionPanel } from './components/analysis/ChangeDetectionPanel'
import { DatasetDetailPanel } from './components/datasets/DatasetDetailPanel' import { DatasetDetailPanel } from './components/datasets/DatasetDetailPanel'
import { DatasetPanel } from './components/datasets/DatasetPanel' import { DatasetPanel } from './components/datasets/DatasetPanel'
@@ -16,6 +15,7 @@ import type { DatasetCreateResponse } from './types'
import { ProviderPanel } from './components/providers/ProviderPanel' import { ProviderPanel } from './components/providers/ProviderPanel'
import { SegmentationLab } from './components/segmentation/SegmentationLab' import { SegmentationLab } from './components/segmentation/SegmentationLab'
import { useChangeDetectionWorkflow } from './hooks/useChangeDetectionWorkflow' import { useChangeDetectionWorkflow } from './hooks/useChangeDetectionWorkflow'
import { useDemoWorkflow } from './hooks/useDemoWorkflow'
import { useDetectionWorkflow } from './hooks/useDetectionWorkflow' import { useDetectionWorkflow } from './hooks/useDetectionWorkflow'
import { useDatasetWorkflow } from './hooks/useDatasetWorkflow' import { useDatasetWorkflow } from './hooks/useDatasetWorkflow'
import { useExportWorkflow } from './hooks/useExportWorkflow' import { useExportWorkflow } from './hooks/useExportWorkflow'
@@ -24,16 +24,12 @@ import { useProviderCapabilities } from './hooks/useProviderCapabilities'
import { useProjectWorkspace } from './hooks/useProjectWorkspace' import { useProjectWorkspace } from './hooks/useProjectWorkspace'
import { useQualityWorkflow } from './hooks/useQualityWorkflow' import { useQualityWorkflow } from './hooks/useQualityWorkflow'
import { useSegmentationWorkflow } from './hooks/useSegmentationWorkflow' import { useSegmentationWorkflow } from './hooks/useSegmentationWorkflow'
import { formatError } from './lib/formatError'
function isVectorDatasetType(datasetType: string): boolean { function isVectorDatasetType(datasetType: string): boolean {
return datasetType === 'vector' || datasetType === 'geojson' return datasetType === 'vector' || datasetType === 'geojson'
} }
function App(): JSX.Element { function App(): JSX.Element {
const [loadingDemoWorkflow, setLoadingDemoWorkflow] = useState(false)
const [demoWorkflowMessage, setDemoWorkflowMessage] = useState<string | null>(null)
const { const {
projects, projects,
selectedProject, selectedProject,
@@ -324,6 +320,28 @@ function App(): JSX.Element {
datasetContent, datasetContent,
selectedDataset, selectedDataset,
}) })
const {
loadingDemoWorkflow,
demoWorkflowMessage,
loadDemoWorkflow,
} = useDemoWorkflow({
loadProjects,
loadProjectData,
loadDatasetDetails,
loadDetectionRuns,
loadSegmentationRuns,
loadQualityChecks,
loadExports,
setSelectedProjectId,
setSelectedDatasetId,
setSelectedMapAreaId,
setQaCandidateDatasetId,
setQaReferenceDatasetId,
setQaAreaId,
setDetectionReferenceDatasetId,
setSegmentationReferenceDatasetId,
setErrorMessage,
})
useEffect(() => { useEffect(() => {
loadProjects().catch(() => null) loadProjects().catch(() => null)
@@ -356,40 +374,6 @@ function App(): JSX.Element {
loadSegmentationResults().catch(() => null) loadSegmentationResults().catch(() => null)
}, [selectedSegmentationRunId, segmentationClassFilter, segmentationMinConfidenceFilter]) }, [selectedSegmentationRunId, segmentationClassFilter, segmentationMinConfidenceFilter])
const loadDemoWorkflow = async () => {
setLoadingDemoWorkflow(true)
setDemoWorkflowMessage(null)
setErrorMessage(null)
try {
const result = await demoApi.seedWorkflow()
setSelectedProjectId(result.project_id)
setSelectedDatasetId(result.candidate_dataset_id)
setSelectedMapAreaId(result.area_id)
setQaCandidateDatasetId(result.candidate_dataset_id)
setQaReferenceDatasetId(result.reference_dataset_id)
setQaAreaId(result.area_id)
setDetectionReferenceDatasetId(result.reference_dataset_id)
setSegmentationReferenceDatasetId(result.reference_dataset_id)
setDemoWorkflowMessage(result.message)
await loadProjects()
const [projectData] = await Promise.all([
loadProjectData(result.project_id),
loadDetectionRuns(result.project_id),
loadSegmentationRuns(result.project_id),
loadQualityChecks(result.project_id),
loadExports(result.project_id),
])
const candidateDataset = projectData?.datasets.find((dataset) => dataset.id === result.candidate_dataset_id)
if (candidateDataset) {
await loadDatasetDetails(result.project_id, candidateDataset)
}
} catch (error) {
setErrorMessage(formatError(error, 'Failed to load demo workflow'))
} finally {
setLoadingDemoWorkflow(false)
}
}
return ( return (
<div className="app-shell"> <div className="app-shell">
<header> <header>
+85
View File
@@ -0,0 +1,85 @@
import { useState } from 'react'
import { demoApi } from '../services/api'
import type { DatasetCreateResponse, QualityCheckRead } from '../types'
import { formatError } from '../lib/formatError'
interface DemoWorkflowOptions {
loadProjects: () => Promise<void>
loadProjectData: (projectId: string) => Promise<{ datasets: DatasetCreateResponse[] } | null>
loadDatasetDetails: (projectId: string, dataset: DatasetCreateResponse) => Promise<void>
loadDetectionRuns: (projectId?: string | null) => Promise<void>
loadSegmentationRuns: (projectId?: string | null) => Promise<void>
loadQualityChecks: (projectId?: string | null) => Promise<QualityCheckRead[] | void>
loadExports: (projectId?: string | null) => Promise<void>
setSelectedProjectId: (projectId: string) => void
setSelectedDatasetId: (datasetId: string | null) => void
setSelectedMapAreaId: (areaId: string) => void
setQaCandidateDatasetId: (datasetId: string) => void
setQaReferenceDatasetId: (datasetId: string) => void
setQaAreaId: (areaId: string) => void
setDetectionReferenceDatasetId: (datasetId: string) => void
setSegmentationReferenceDatasetId: (datasetId: string) => void
setErrorMessage: (message: string | null) => void
}
export function useDemoWorkflow({
loadProjects,
loadProjectData,
loadDatasetDetails,
loadDetectionRuns,
loadSegmentationRuns,
loadQualityChecks,
loadExports,
setSelectedProjectId,
setSelectedDatasetId,
setSelectedMapAreaId,
setQaCandidateDatasetId,
setQaReferenceDatasetId,
setQaAreaId,
setDetectionReferenceDatasetId,
setSegmentationReferenceDatasetId,
setErrorMessage,
}: DemoWorkflowOptions) {
const [loadingDemoWorkflow, setLoadingDemoWorkflow] = useState(false)
const [demoWorkflowMessage, setDemoWorkflowMessage] = useState<string | null>(null)
const loadDemoWorkflow = async () => {
setLoadingDemoWorkflow(true)
setDemoWorkflowMessage(null)
setErrorMessage(null)
try {
const result = await demoApi.seedWorkflow()
setSelectedProjectId(result.project_id)
setSelectedDatasetId(result.candidate_dataset_id)
setSelectedMapAreaId(result.area_id)
setQaCandidateDatasetId(result.candidate_dataset_id)
setQaReferenceDatasetId(result.reference_dataset_id)
setQaAreaId(result.area_id)
setDetectionReferenceDatasetId(result.reference_dataset_id)
setSegmentationReferenceDatasetId(result.reference_dataset_id)
setDemoWorkflowMessage(result.message)
await loadProjects()
const [projectData] = await Promise.all([
loadProjectData(result.project_id),
loadDetectionRuns(result.project_id),
loadSegmentationRuns(result.project_id),
loadQualityChecks(result.project_id),
loadExports(result.project_id),
])
const candidateDataset = projectData?.datasets.find((dataset) => dataset.id === result.candidate_dataset_id)
if (candidateDataset) {
await loadDatasetDetails(result.project_id, candidateDataset)
}
} catch (error) {
setErrorMessage(formatError(error, 'Failed to load demo workflow'))
} finally {
setLoadingDemoWorkflow(false)
}
}
return {
loadingDemoWorkflow,
demoWorkflowMessage,
loadDemoWorkflow,
}
}