Extract project workspace 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:31:28 +02:00
parent ea70a77c6f
commit c0216823f2
9 changed files with 257 additions and 132 deletions
+26 -130
View File
@@ -1,8 +1,5 @@
import { FormEvent, useEffect, useMemo, useState } from 'react'
import './styles/app.css'
import { areasApi } from './services/api/areas'
import { datasetsApi } from './services/api/datasets'
import { projectsApi } from './services/api/projects'
import { demoApi } from './services/api'
import { ChangeDetectionPanel } from './components/analysis/ChangeDetectionPanel'
import { DatasetDetailPanel } from './components/datasets/DatasetDetailPanel'
@@ -15,15 +12,7 @@ import { AreaPanel } from './components/project/AreaPanel'
import { ProjectPanel } from './components/project/ProjectPanel'
import { QualityResultsPanel } from './components/quality/QualityResultsPanel'
import { WorkbenchStatusStrip } from './components/WorkbenchStatusStrip'
import type {
DatasetCreateResponse,
DatasetListResponse,
ProjectRead,
ProjectCreate,
AreaCreate,
AreaListResponse,
AreaRead,
} from './types'
import type { DatasetCreateResponse } from './types'
import { ProviderPanel } from './components/providers/ProviderPanel'
import { SegmentationLab } from './components/segmentation/SegmentationLab'
import { useChangeDetectionWorkflow } from './hooks/useChangeDetectionWorkflow'
@@ -32,6 +21,7 @@ import { useDatasetWorkflow } from './hooks/useDatasetWorkflow'
import { useExportWorkflow } from './hooks/useExportWorkflow'
import { useMapWorkspaceState } from './hooks/useMapWorkspaceState'
import { useProviderCapabilities } from './hooks/useProviderCapabilities'
import { useProjectWorkspace } from './hooks/useProjectWorkspace'
import { useQualityWorkflow } from './hooks/useQualityWorkflow'
import { useSegmentationWorkflow } from './hooks/useSegmentationWorkflow'
import { formatError } from './lib/formatError'
@@ -41,32 +31,31 @@ function isVectorDatasetType(datasetType: string): boolean {
}
function App(): JSX.Element {
const [projects, setProjects] = useState<ProjectRead[]>([])
const [selectedProjectId, setSelectedProjectId] = useState<string | null>(null)
const [areas, setAreas] = useState<AreaRead[]>([])
const [datasets, setDatasets] = useState<DatasetCreateResponse[]>([])
const [loadingProjects, setLoadingProjects] = useState(false)
const [loadingDemoWorkflow, setLoadingDemoWorkflow] = useState(false)
const [demoWorkflowMessage, setDemoWorkflowMessage] = useState<string | null>(null)
const [loadingAreas, setLoadingAreas] = useState(false)
const [loadingDatasets, setLoadingDatasets] = useState(false)
const [errorMessage, setErrorMessage] = useState<string | null>(null)
const [projectForm, setProjectForm] = useState<ProjectCreate>({
name: '',
description: '',
region: 'Kempen',
})
const [areaForm, setAreaForm] = useState({
name: 'Demo AOI',
geometry:
'{"type":"MultiPolygon","coordinates":[[[[4.35,51.28],[4.55,51.28],[4.55,51.46],[4.35,51.46],[4.35,51.28]]]]}',
crs: 'EPSG:4326',
})
const selectedProject = useMemo(
() => projects.find((project) => project.id === selectedProjectId) ?? null,
[projects, selectedProjectId],
)
const {
projects,
selectedProject,
selectedProjectId,
areas,
datasets,
loadingProjects,
loadingAreas,
loadingDatasets,
errorMessage,
projectForm,
areaForm,
loadProjects,
loadProjectData,
createProject,
createArea,
resetProjectData,
setSelectedProjectId,
setErrorMessage,
setProjectForm,
setAreaForm,
} = useProjectWorkspace()
const {
selectedDatasetId,
selectedDataset,
@@ -128,6 +117,7 @@ function App(): JSX.Element {
setDatasetForm,
} = useDatasetWorkflow({
selectedProjectId,
areas,
datasets,
loadProjectData,
setErrorMessage,
@@ -334,52 +324,6 @@ function App(): JSX.Element {
datasetContent,
selectedDataset,
})
const loadProjects = async () => {
setLoadingProjects(true)
setErrorMessage(null)
try {
const response = await projectsApi.list()
setProjects(response.items)
if (!selectedProjectId && response.items.length > 0) {
setSelectedProjectId(response.items[0].id)
}
} catch (error) {
setErrorMessage(error instanceof Error ? error.message : 'Failed to load projects')
} finally {
setLoadingProjects(false)
}
}
async function loadProjectData(projectId: string) {
setLoadingAreas(true)
setLoadingDatasets(true)
setErrorMessage(null)
try {
const [areaResponse, datasetResponse]: [AreaListResponse, DatasetListResponse] = await Promise.all([
areasApi.list(projectId),
datasetsApi.list(projectId),
])
setAreas(areaResponse.items)
setDatasets(datasetResponse.items)
if (areaResponse.items.length === 0) {
setSelectedClipAreaId('')
} else if (!selectedClipAreaId || !areaResponse.items.some((area) => area.id === selectedClipAreaId)) {
setSelectedClipAreaId(areaResponse.items[0].id)
}
if (areaResponse.items.length === 0) {
setSelectedMapAreaId('')
} else if (!selectedMapAreaId || !areaResponse.items.some((area) => area.id === selectedMapAreaId)) {
setSelectedMapAreaId(areaResponse.items[0].id)
}
return { areas: areaResponse.items, datasets: datasetResponse.items }
} catch (error) {
setErrorMessage(error instanceof Error ? error.message : 'Failed to load project data')
return null
} finally {
setLoadingAreas(false)
setLoadingDatasets(false)
}
}
useEffect(() => {
loadProjects().catch(() => null)
@@ -390,10 +334,8 @@ function App(): JSX.Element {
useEffect(() => {
if (!selectedProjectId) {
setAreas([])
setDatasets([])
resetProjectData()
resetDatasetForProject()
setSelectedMapAreaId('')
resetDetectionForProject()
resetSegmentationForProject()
resetExportsForProject()
@@ -414,25 +356,6 @@ function App(): JSX.Element {
loadSegmentationResults().catch(() => null)
}, [selectedSegmentationRunId, segmentationClassFilter, segmentationMinConfidenceFilter])
const createProject = async (event: FormEvent) => {
event.preventDefault()
if (!projectForm.name.trim()) {
setErrorMessage('Project name is required')
return
}
try {
await projectsApi.create({
name: projectForm.name.trim(),
description: projectForm.description?.trim() || undefined,
region: projectForm.region?.trim() || 'Kempen',
})
setProjectForm((previous) => ({ ...previous, name: '', description: '' }))
await loadProjects()
} catch (error) {
setErrorMessage(error instanceof Error ? error.message : 'Failed to create project')
}
}
const loadDemoWorkflow = async () => {
setLoadingDemoWorkflow(true)
setDemoWorkflowMessage(null)
@@ -467,33 +390,6 @@ function App(): JSX.Element {
}
}
const createArea = async (event: FormEvent) => {
event.preventDefault()
if (!selectedProjectId) {
setErrorMessage('Select a project first')
return
}
let geometry: AreaCreate['geometry']
try {
geometry = JSON.parse(areaForm.geometry) as AreaCreate['geometry']
} catch {
setErrorMessage('Invalid GeoJSON geometry JSON')
return
}
try {
const createdArea = await areasApi.create(selectedProjectId, {
name: areaForm.name,
crs: areaForm.crs,
geometry,
})
setSelectedMapAreaId(createdArea.id)
await loadProjectData(selectedProjectId)
setAreaForm((previous) => ({ ...previous, name: '' }))
} catch (error) {
setErrorMessage(error instanceof Error ? error.message : 'Failed to create area')
}
}
return (
<div className="app-shell">
<header>