Extract demo workflow hook
This commit is contained in:
@@ -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")
|
||||||
|
|
||||||
|
|||||||
@@ -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
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
@@ -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>
|
||||||
|
|||||||
@@ -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,
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user