AP2.3c: Slice-bewusstes Laden und hoisted InitiativeOperationsProvider.
Some checks failed
Test Suite / pytest-backend (push) Waiting to run
Test Suite / lint-backend (push) Waiting to run
Test Suite / compose-smoke (push) Waiting to run
Test Suite / k6 /api/health Baseline (push) Blocked by required conditions
Test Suite / playwright-smoke (push) Blocked by required conditions
Deploy Development / deploy (push) Has been cancelled
Some checks failed
Test Suite / pytest-backend (push) Waiting to run
Test Suite / lint-backend (push) Waiting to run
Test Suite / compose-smoke (push) Waiting to run
Test Suite / k6 /api/health Baseline (push) Blocked by required conditions
Test Suite / playwright-smoke (push) Blocked by required conditions
Deploy Development / deploy (push) Has been cancelled
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
90934453e6
commit
1b55d8558c
|
|
@ -1,6 +1,7 @@
|
||||||
import { Navigate, Route, Routes } from 'react-router-dom'
|
import { Navigate, Route, Routes } from 'react-router-dom'
|
||||||
import { SessionProvider, useSession } from './context/SessionContext.jsx'
|
import { SessionProvider, useSession } from './context/SessionContext.jsx'
|
||||||
import { ProgramScopeProvider } from './context/ProgramScopeContext.jsx'
|
import { ProgramScopeProvider } from './context/ProgramScopeContext.jsx'
|
||||||
|
import { ScopedOperationsBridge } from './layout/ScopedOperationsBridge.jsx'
|
||||||
import { AppLayout } from './layout/AppLayout.jsx'
|
import { AppLayout } from './layout/AppLayout.jsx'
|
||||||
import { AuthPanel } from './pages/AuthPanel.jsx'
|
import { AuthPanel } from './pages/AuthPanel.jsx'
|
||||||
import { ActionDetailPage } from './pages/ActionDetailPage.jsx'
|
import { ActionDetailPage } from './pages/ActionDetailPage.jsx'
|
||||||
|
|
@ -16,6 +17,7 @@ import {
|
||||||
ControlIndexRedirect,
|
ControlIndexRedirect,
|
||||||
MODE_ROUTE_COMPONENTS,
|
MODE_ROUTE_COMPONENTS,
|
||||||
} from './registry/viewRegistry.js'
|
} from './registry/viewRegistry.js'
|
||||||
|
import { OperatingRouteGate } from './registry/OperatingRouteGate.jsx'
|
||||||
import {
|
import {
|
||||||
WorkspaceRedirect,
|
WorkspaceRedirect,
|
||||||
MyActionsRedirect,
|
MyActionsRedirect,
|
||||||
|
|
@ -54,6 +56,7 @@ function AppRoutes() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ProgramScopeProvider>
|
<ProgramScopeProvider>
|
||||||
|
<ScopedOperationsBridge>
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route element={<AppLayout />}>
|
<Route element={<AppLayout />}>
|
||||||
{topLevelViews.map((view) => (
|
{topLevelViews.map((view) => (
|
||||||
|
|
@ -62,27 +65,90 @@ function AppRoutes() {
|
||||||
|
|
||||||
<Route path="/work" element={<WorkLayout />}>
|
<Route path="/work" element={<WorkLayout />}>
|
||||||
<Route index element={<WorkIndexRedirect />} />
|
<Route index element={<WorkIndexRedirect />} />
|
||||||
<Route path="today" element={<MODE_ROUTE_COMPONENTS.workToday />} />
|
<Route
|
||||||
<Route path="sprint" element={<MODE_ROUTE_COMPONENTS.workSprint />} />
|
path="today"
|
||||||
<Route path="mine" element={<MODE_ROUTE_COMPONENTS.workMine />} />
|
element={
|
||||||
|
<OperatingRouteGate>
|
||||||
|
<MODE_ROUTE_COMPONENTS.workToday />
|
||||||
|
</OperatingRouteGate>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
path="sprint"
|
||||||
|
element={
|
||||||
|
<OperatingRouteGate>
|
||||||
|
<MODE_ROUTE_COMPONENTS.workSprint />
|
||||||
|
</OperatingRouteGate>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
path="mine"
|
||||||
|
element={
|
||||||
|
<OperatingRouteGate>
|
||||||
|
<MODE_ROUTE_COMPONENTS.workMine />
|
||||||
|
</OperatingRouteGate>
|
||||||
|
}
|
||||||
|
/>
|
||||||
</Route>
|
</Route>
|
||||||
|
|
||||||
<Route path="/plan" element={<PlanLayout />}>
|
<Route path="/plan" element={<PlanLayout />}>
|
||||||
<Route index element={<PlanIndexRedirect />} />
|
<Route index element={<PlanIndexRedirect />} />
|
||||||
<Route path="portfolio" element={<MODE_ROUTE_COMPONENTS.planPortfolio />} />
|
<Route path="portfolio" element={<MODE_ROUTE_COMPONENTS.planPortfolio />} />
|
||||||
<Route path="profile" element={<MODE_ROUTE_COMPONENTS.planProfile />} />
|
<Route path="profile" element={<MODE_ROUTE_COMPONENTS.planProfile />} />
|
||||||
<Route path="structure" element={<MODE_ROUTE_COMPONENTS.planStructure />} />
|
<Route
|
||||||
<Route path="gates" element={<MODE_ROUTE_COMPONENTS.planGates />} />
|
path="structure"
|
||||||
<Route path="inbox" element={<MODE_ROUTE_COMPONENTS.planInbox />} />
|
element={
|
||||||
<Route path="sprint" element={<MODE_ROUTE_COMPONENTS.planSprint />} />
|
<OperatingRouteGate>
|
||||||
|
<MODE_ROUTE_COMPONENTS.planStructure />
|
||||||
|
</OperatingRouteGate>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
path="gates"
|
||||||
|
element={
|
||||||
|
<OperatingRouteGate>
|
||||||
|
<MODE_ROUTE_COMPONENTS.planGates />
|
||||||
|
</OperatingRouteGate>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
path="inbox"
|
||||||
|
element={
|
||||||
|
<OperatingRouteGate>
|
||||||
|
<MODE_ROUTE_COMPONENTS.planInbox />
|
||||||
|
</OperatingRouteGate>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
path="sprint"
|
||||||
|
element={
|
||||||
|
<OperatingRouteGate>
|
||||||
|
<MODE_ROUTE_COMPONENTS.planSprint />
|
||||||
|
</OperatingRouteGate>
|
||||||
|
}
|
||||||
|
/>
|
||||||
<Route path="work" element={<MODE_ROUTE_COMPONENTS.planWork />} />
|
<Route path="work" element={<MODE_ROUTE_COMPONENTS.planWork />} />
|
||||||
</Route>
|
</Route>
|
||||||
|
|
||||||
<Route path="/control" element={<ControlLayout />}>
|
<Route path="/control" element={<ControlLayout />}>
|
||||||
<Route index element={<ControlIndexRedirect />} />
|
<Route index element={<ControlIndexRedirect />} />
|
||||||
<Route path="status" element={<MODE_ROUTE_COMPONENTS.controlStatus />} />
|
<Route path="status" element={<MODE_ROUTE_COMPONENTS.controlStatus />} />
|
||||||
<Route path="plan-ist" element={<MODE_ROUTE_COMPONENTS.controlPlanIst />} />
|
<Route
|
||||||
<Route path="journey" element={<MODE_ROUTE_COMPONENTS.controlJourney />} />
|
path="plan-ist"
|
||||||
|
element={
|
||||||
|
<OperatingRouteGate>
|
||||||
|
<MODE_ROUTE_COMPONENTS.controlPlanIst />
|
||||||
|
</OperatingRouteGate>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
path="journey"
|
||||||
|
element={
|
||||||
|
<OperatingRouteGate>
|
||||||
|
<MODE_ROUTE_COMPONENTS.controlJourney />
|
||||||
|
</OperatingRouteGate>
|
||||||
|
}
|
||||||
|
/>
|
||||||
</Route>
|
</Route>
|
||||||
|
|
||||||
<Route path="/projects/:projectId" element={<ProjectObjectPage />} />
|
<Route path="/projects/:projectId" element={<ProjectObjectPage />} />
|
||||||
|
|
@ -109,6 +175,7 @@ function AppRoutes() {
|
||||||
<Route path="*" element={<Navigate to="/cockpit" replace />} />
|
<Route path="*" element={<Navigate to="/cockpit" replace />} />
|
||||||
</Route>
|
</Route>
|
||||||
</Routes>
|
</Routes>
|
||||||
|
</ScopedOperationsBridge>
|
||||||
</ProgramScopeProvider>
|
</ProgramScopeProvider>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,9 @@
|
||||||
import { NavLink, useLocation } from 'react-router-dom'
|
import { NavLink, useLocation } from 'react-router-dom'
|
||||||
import { useEffect, useMemo, useState } from 'react'
|
import { useEffect, useMemo, useState } from 'react'
|
||||||
import { listInitiativeProjects } from '../api/projects.js'
|
|
||||||
import { listInitiativeActions } from '../api/initiatives.js'
|
|
||||||
import { listInitiativeBacklog } from '../api/backlog.js'
|
|
||||||
import { getInitiativeExecutionGraphState } from '../api/executionPlan.js'
|
import { getInitiativeExecutionGraphState } from '../api/executionPlan.js'
|
||||||
import { useProgramScope } from '../context/ProgramScopeContext.jsx'
|
import { useProgramScope } from '../context/ProgramScopeContext.jsx'
|
||||||
|
import { useOptionalInitiativeOperations } from '../context/InitiativeOperationsContext.jsx'
|
||||||
|
import { useMethodUiContext } from '../hooks/useMethodUiContext.js'
|
||||||
import { buildProjectsByParent } from '../utils/projectTree.js'
|
import { buildProjectsByParent } from '../utils/projectTree.js'
|
||||||
import { projectPath } from '../utils/routes.js'
|
import { projectPath } from '../utils/routes.js'
|
||||||
import {
|
import {
|
||||||
|
|
@ -12,9 +11,7 @@ import {
|
||||||
resolvePlanOutlineProjectId,
|
resolvePlanOutlineProjectId,
|
||||||
resolvePlanOutlineActionId,
|
resolvePlanOutlineActionId,
|
||||||
} from '../plan/planOutlineNodes.js'
|
} from '../plan/planOutlineNodes.js'
|
||||||
import { resolvePlanOutlineNodes } from '../config/methodUiDefaults.js'
|
import { resolvePlanOutlineNodesFromProfile } from '../registry/resolveOperatingProfile.js'
|
||||||
import { getInitiative } from '../api/initiatives.js'
|
|
||||||
import { getActiveWorkCycle } from '../api/workCycles.js'
|
|
||||||
import { PlanOutlineWorkTree } from './PlanOutlineWorkTree.jsx'
|
import { PlanOutlineWorkTree } from './PlanOutlineWorkTree.jsx'
|
||||||
|
|
||||||
function PlanOutlineProjectTree({ projects, parentId, activeProjectId, depth = 0 }) {
|
function PlanOutlineProjectTree({ projects, parentId, activeProjectId, depth = 0 }) {
|
||||||
|
|
@ -55,15 +52,12 @@ function PlanOutlineProjectTree({ projects, parentId, activeProjectId, depth = 0
|
||||||
export function PlanOutlineNav() {
|
export function PlanOutlineNav() {
|
||||||
const location = useLocation()
|
const location = useLocation()
|
||||||
const { initiativeId, initiativeTitle, hrefWithScope } = useProgramScope()
|
const { initiativeId, initiativeTitle, hrefWithScope } = useProgramScope()
|
||||||
|
const { operatingContext, hasActiveSprint } = useMethodUiContext()
|
||||||
|
const ops = useOptionalInitiativeOperations()
|
||||||
const activeKey = resolvePlanOutlineActiveKey(location.pathname)
|
const activeKey = resolvePlanOutlineActiveKey(location.pathname)
|
||||||
const activeProjectId = resolvePlanOutlineProjectId(location.pathname)
|
const activeProjectId = resolvePlanOutlineProjectId(location.pathname)
|
||||||
const activeActionId = resolvePlanOutlineActionId(location.pathname)
|
const activeActionId = resolvePlanOutlineActionId(location.pathname)
|
||||||
const [counts, setCounts] = useState({ inbox: null, work: null })
|
|
||||||
const [projects, setProjects] = useState([])
|
|
||||||
const [openActions, setOpenActions] = useState([])
|
|
||||||
const [executionGraph, setExecutionGraph] = useState(null)
|
const [executionGraph, setExecutionGraph] = useState(null)
|
||||||
const [archetypeKey, setArchetypeKey] = useState(null)
|
|
||||||
const [hasActiveSprint, setHasActiveSprint] = useState(false)
|
|
||||||
const [structureOpen, setStructureOpen] = useState(
|
const [structureOpen, setStructureOpen] = useState(
|
||||||
activeKey === 'structure' || Boolean(activeProjectId),
|
activeKey === 'structure' || Boolean(activeProjectId),
|
||||||
)
|
)
|
||||||
|
|
@ -71,6 +65,29 @@ export function PlanOutlineNav() {
|
||||||
activeKey === 'work' || Boolean(activeActionId),
|
activeKey === 'work' || Boolean(activeActionId),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const contextReady = Boolean(initiativeId && ops?.initiativeId === initiativeId)
|
||||||
|
const projects = contextReady ? ops.projects : []
|
||||||
|
const backlogItems = contextReady ? ops.backlogItems : []
|
||||||
|
const actions = contextReady ? ops.actions : []
|
||||||
|
|
||||||
|
const openActions = useMemo(
|
||||||
|
() =>
|
||||||
|
actions.filter(
|
||||||
|
(action) => action.status !== 'done' && action.status !== 'discarded',
|
||||||
|
),
|
||||||
|
[actions],
|
||||||
|
)
|
||||||
|
|
||||||
|
const counts = useMemo(
|
||||||
|
() => ({
|
||||||
|
inbox: contextReady
|
||||||
|
? backlogItems.filter((item) => item.status !== 'converted').length
|
||||||
|
: null,
|
||||||
|
work: contextReady ? openActions.length : null,
|
||||||
|
}),
|
||||||
|
[contextReady, backlogItems, openActions],
|
||||||
|
)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setStructureOpen(activeKey === 'structure' || Boolean(activeProjectId))
|
setStructureOpen(activeKey === 'structure' || Boolean(activeProjectId))
|
||||||
}, [activeKey, activeProjectId])
|
}, [activeKey, activeProjectId])
|
||||||
|
|
@ -79,81 +96,27 @@ export function PlanOutlineNav() {
|
||||||
setWorkOpen(activeKey === 'work' || Boolean(activeActionId))
|
setWorkOpen(activeKey === 'work' || Boolean(activeActionId))
|
||||||
}, [activeKey, activeActionId])
|
}, [activeKey, activeActionId])
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!initiativeId) {
|
|
||||||
setArchetypeKey(null)
|
|
||||||
setHasActiveSprint(false)
|
|
||||||
return undefined
|
|
||||||
}
|
|
||||||
let cancelled = false
|
|
||||||
Promise.all([
|
|
||||||
getInitiative(initiativeId),
|
|
||||||
getActiveWorkCycle(initiativeId).catch(() => null),
|
|
||||||
])
|
|
||||||
.then(([initiative, activeCycle]) => {
|
|
||||||
if (cancelled) return
|
|
||||||
setArchetypeKey(initiative?.archetype_key || null)
|
|
||||||
setHasActiveSprint(Boolean(activeCycle?.id))
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
if (!cancelled) {
|
|
||||||
setArchetypeKey(null)
|
|
||||||
setHasActiveSprint(false)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
return () => {
|
|
||||||
cancelled = true
|
|
||||||
}
|
|
||||||
}, [initiativeId])
|
|
||||||
|
|
||||||
const outlineNodes = useMemo(
|
const outlineNodes = useMemo(
|
||||||
() =>
|
() =>
|
||||||
resolvePlanOutlineNodes({
|
resolvePlanOutlineNodesFromProfile({
|
||||||
archetypeKey,
|
operatingContext,
|
||||||
hasActiveSprint,
|
hasActiveSprint,
|
||||||
}),
|
}),
|
||||||
[archetypeKey, hasActiveSprint],
|
[operatingContext, hasActiveSprint],
|
||||||
)
|
)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!initiativeId) {
|
if (!initiativeId) {
|
||||||
setProjects([])
|
setExecutionGraph(null)
|
||||||
return undefined
|
return undefined
|
||||||
}
|
}
|
||||||
let cancelled = false
|
let cancelled = false
|
||||||
listInitiativeProjects(initiativeId)
|
getInitiativeExecutionGraphState(initiativeId)
|
||||||
.then((items) => {
|
.then((graphState) => {
|
||||||
if (!cancelled) setProjects(Array.isArray(items) ? items : [])
|
if (!cancelled) setExecutionGraph(graphState)
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
if (!cancelled) setProjects([])
|
if (!cancelled) setExecutionGraph(null)
|
||||||
})
|
|
||||||
return () => {
|
|
||||||
cancelled = true
|
|
||||||
}
|
|
||||||
}, [initiativeId])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!initiativeId) {
|
|
||||||
setCounts({ inbox: null, work: null })
|
|
||||||
return undefined
|
|
||||||
}
|
|
||||||
let cancelled = false
|
|
||||||
Promise.all([
|
|
||||||
listInitiativeBacklog(initiativeId).catch(() => []),
|
|
||||||
listInitiativeActions(initiativeId).catch(() => []),
|
|
||||||
getInitiativeExecutionGraphState(initiativeId).catch(() => null),
|
|
||||||
]).then(([backlog, actions, graphState]) => {
|
|
||||||
if (cancelled) return
|
|
||||||
const open = actions.filter(
|
|
||||||
(action) => action.status !== 'done' && action.status !== 'discarded',
|
|
||||||
)
|
|
||||||
setOpenActions(open)
|
|
||||||
setExecutionGraph(graphState)
|
|
||||||
setCounts({
|
|
||||||
inbox: backlog.filter((item) => item.status !== 'converted').length,
|
|
||||||
work: open.length,
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
return () => {
|
return () => {
|
||||||
cancelled = true
|
cancelled = true
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ import { useParams } from 'react-router-dom'
|
||||||
import {
|
import {
|
||||||
getInitiative,
|
getInitiative,
|
||||||
getInitiativeSteeringSnapshot,
|
getInitiativeSteeringSnapshot,
|
||||||
|
getInitiativeOperatingContext,
|
||||||
listInitiativeActions,
|
listInitiativeActions,
|
||||||
createInitiativeAction,
|
createInitiativeAction,
|
||||||
updateInitiative,
|
updateInitiative,
|
||||||
|
|
@ -86,6 +87,10 @@ import { useOptionalProgramScope } from './ProgramScopeContext.jsx'
|
||||||
import { collectProjectSubtreeIds } from '../utils/projectTree.js'
|
import { collectProjectSubtreeIds } from '../utils/projectTree.js'
|
||||||
import { filterActionsForWorkCycle } from '../utils/workCycleActions.js'
|
import { filterActionsForWorkCycle } from '../utils/workCycleActions.js'
|
||||||
import { listFeatureParentActions } from '../utils/actionReferences.js'
|
import { listFeatureParentActions } from '../utils/actionReferences.js'
|
||||||
|
import {
|
||||||
|
applySliceResults,
|
||||||
|
loadOperatingSlices,
|
||||||
|
} from '../registry/operatingSliceLoaders.js'
|
||||||
|
|
||||||
const InitiativeOperationsContext = createContext(null)
|
const InitiativeOperationsContext = createContext(null)
|
||||||
|
|
||||||
|
|
@ -114,6 +119,7 @@ export function InitiativeOperationsProvider({ children, initiativeId: initiativ
|
||||||
const [steeringSnapshotLoading, setSteeringSnapshotLoading] = useState(false)
|
const [steeringSnapshotLoading, setSteeringSnapshotLoading] = useState(false)
|
||||||
const [steeringSnapshotError, setSteeringSnapshotError] = useState(null)
|
const [steeringSnapshotError, setSteeringSnapshotError] = useState(null)
|
||||||
const [steeringMethods, setSteeringMethods] = useState([])
|
const [steeringMethods, setSteeringMethods] = useState([])
|
||||||
|
const [operatingContext, setOperatingContext] = useState(null)
|
||||||
const [methodBusy, setMethodBusy] = useState(false)
|
const [methodBusy, setMethodBusy] = useState(false)
|
||||||
const [loading, setLoading] = useState(true)
|
const [loading, setLoading] = useState(true)
|
||||||
const [error, setError] = useState(null)
|
const [error, setError] = useState(null)
|
||||||
|
|
@ -123,62 +129,74 @@ export function InitiativeOperationsProvider({ children, initiativeId: initiativ
|
||||||
const [hideDone, setHideDone] = useState(true)
|
const [hideDone, setHideDone] = useState(true)
|
||||||
const [selectedWorkCycleId, setSelectedWorkCycleId] = useState('')
|
const [selectedWorkCycleId, setSelectedWorkCycleId] = useState('')
|
||||||
|
|
||||||
const load = useCallback(async ({ silent = false } = {}) => {
|
const sliceSetters = useMemo(
|
||||||
|
() => ({
|
||||||
|
setInitiative,
|
||||||
|
setActions,
|
||||||
|
setBlockers,
|
||||||
|
setBacklogItems,
|
||||||
|
setRoadmapItems,
|
||||||
|
setProjects,
|
||||||
|
setEvidenceItems,
|
||||||
|
setDecisions,
|
||||||
|
setReviews,
|
||||||
|
setRecurringItems,
|
||||||
|
setWorkCycles,
|
||||||
|
setActiveWorkCycle,
|
||||||
|
setSteeringMethods,
|
||||||
|
setSteeringSnapshot,
|
||||||
|
}),
|
||||||
|
[],
|
||||||
|
)
|
||||||
|
|
||||||
|
const reloadSlices = useCallback(
|
||||||
|
async (sliceKeys) => {
|
||||||
|
if (!id || !sliceKeys?.length) return
|
||||||
|
const results = await loadOperatingSlices(id, sliceKeys)
|
||||||
|
applySliceResults(results, sliceSetters)
|
||||||
|
},
|
||||||
|
[id, sliceSetters],
|
||||||
|
)
|
||||||
|
|
||||||
|
const load = useCallback(async ({ silent = false, sliceKeys = null } = {}) => {
|
||||||
if (sessionLoading || !id) return
|
if (sessionLoading || !id) return
|
||||||
if (!silent) setLoading(true)
|
if (!silent) setLoading(true)
|
||||||
setError(null)
|
setError(null)
|
||||||
try {
|
try {
|
||||||
const [initData, actionData] = await Promise.all([
|
|
||||||
getInitiative(id),
|
|
||||||
listInitiativeActions(id),
|
|
||||||
])
|
|
||||||
setInitiative(initData)
|
|
||||||
setActions(actionData)
|
|
||||||
|
|
||||||
const loads = []
|
|
||||||
const canReadOm =
|
const canReadOm =
|
||||||
capabilities.has('kairo.initiative.read') ||
|
capabilities.has('kairo.initiative.read') ||
|
||||||
capabilities.has('kairo.blocker.read')
|
capabilities.has('kairo.blocker.read')
|
||||||
if (canReadOm) {
|
|
||||||
loads.push(listInitiativeBlockers(id).then(setBlockers).catch(() => setBlockers([])))
|
let opContext = null
|
||||||
loads.push(listInitiativeBacklog(id).then(setBacklogItems).catch(() => setBacklogItems([])))
|
if (!sliceKeys) {
|
||||||
loads.push(listInitiativeRoadmapItems(id).then(setRoadmapItems).catch(() => setRoadmapItems([])))
|
opContext = await getInitiativeOperatingContext(id).catch(() => null)
|
||||||
loads.push(listInitiativeProjects(id).then(setProjects).catch(() => setProjects([])))
|
setOperatingContext(opContext)
|
||||||
loads.push(listInitiativeEvidence(id).then(setEvidenceItems).catch(() => setEvidenceItems([])))
|
}
|
||||||
loads.push(listInitiativeDecisions(id).then(setDecisions).catch(() => setDecisions([])))
|
|
||||||
loads.push(listInitiativeReviews(id).then(setReviews).catch(() => setReviews([])))
|
const omSlices = canReadOm ? opContext?.data_slices || [] : []
|
||||||
loads.push(listInitiativeRecurring(id).then(setRecurringItems).catch(() => setRecurringItems([])))
|
const keysToLoad = sliceKeys || ['initiative', ...omSlices]
|
||||||
loads.push(listInitiativeWorkCycles(id).then(setWorkCycles).catch(() => setWorkCycles([])))
|
|
||||||
loads.push(
|
if (keysToLoad.includes('steering_snapshot')) {
|
||||||
getActiveWorkCycle(id)
|
|
||||||
.then((item) => setActiveWorkCycle(item || null))
|
|
||||||
.catch(() => setActiveWorkCycle(null))
|
|
||||||
)
|
|
||||||
loads.push(
|
|
||||||
listSteeringMethods().then(setSteeringMethods).catch(() => setSteeringMethods([]))
|
|
||||||
)
|
|
||||||
loads.push(
|
|
||||||
(async () => {
|
|
||||||
setSteeringSnapshotLoading(true)
|
setSteeringSnapshotLoading(true)
|
||||||
setSteeringSnapshotError(null)
|
setSteeringSnapshotError(null)
|
||||||
try {
|
}
|
||||||
setSteeringSnapshot(await getInitiativeSteeringSnapshot(id))
|
|
||||||
} catch (err) {
|
const results = await loadOperatingSlices(id, keysToLoad)
|
||||||
setSteeringSnapshot(null)
|
applySliceResults(results, sliceSetters)
|
||||||
setSteeringSnapshotError(err.message || 'Snapshot fehlgeschlagen')
|
|
||||||
} finally {
|
if (keysToLoad.includes('steering_snapshot') && results.steering_snapshot == null) {
|
||||||
|
setSteeringSnapshotError('Snapshot fehlgeschlagen')
|
||||||
|
}
|
||||||
|
if (keysToLoad.includes('steering_snapshot')) {
|
||||||
setSteeringSnapshotLoading(false)
|
setSteeringSnapshotLoading(false)
|
||||||
}
|
}
|
||||||
})()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
await Promise.all(loads)
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(err.message)
|
setError(err.message)
|
||||||
|
setSteeringSnapshotLoading(false)
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
}
|
}
|
||||||
}, [id, capabilities, sessionLoading])
|
}, [id, capabilities, sessionLoading, sliceSetters])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
load()
|
load()
|
||||||
|
|
@ -380,6 +398,7 @@ export function InitiativeOperationsProvider({ children, initiativeId: initiativ
|
||||||
try {
|
try {
|
||||||
const updated = await updateAction(action.id, { work_cycle_id: targetCycleId })
|
const updated = await updateAction(action.id, { work_cycle_id: targetCycleId })
|
||||||
setActions((prev) => prev.map((a) => (a.id === action.id ? updated : a)))
|
setActions((prev) => prev.map((a) => (a.id === action.id ? updated : a)))
|
||||||
|
await reloadSlices(['work_cycles'])
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(err.message)
|
setError(err.message)
|
||||||
} finally {
|
} finally {
|
||||||
|
|
@ -975,6 +994,8 @@ export function InitiativeOperationsProvider({ children, initiativeId: initiativ
|
||||||
steeringSnapshotLoading,
|
steeringSnapshotLoading,
|
||||||
steeringSnapshotError,
|
steeringSnapshotError,
|
||||||
steeringMethods,
|
steeringMethods,
|
||||||
|
operatingContext,
|
||||||
|
dataSlices: operatingContext?.data_slices || [],
|
||||||
methodBusy,
|
methodBusy,
|
||||||
loading,
|
loading,
|
||||||
error,
|
error,
|
||||||
|
|
@ -993,6 +1014,7 @@ export function InitiativeOperationsProvider({ children, initiativeId: initiativ
|
||||||
actorsUsedFallback: actorsState.usedFallback,
|
actorsUsedFallback: actorsState.usedFallback,
|
||||||
reloadActors: actorsState.reload,
|
reloadActors: actorsState.reload,
|
||||||
reload: load,
|
reload: load,
|
||||||
|
reloadSlices,
|
||||||
handleMethodChange,
|
handleMethodChange,
|
||||||
handleMethodProfileChange,
|
handleMethodProfileChange,
|
||||||
handleCreateAction,
|
handleCreateAction,
|
||||||
|
|
@ -1053,8 +1075,12 @@ export function InitiativeOperationsProvider({ children, initiativeId: initiativ
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function useOptionalInitiativeOperations() {
|
||||||
|
return useContext(InitiativeOperationsContext)
|
||||||
|
}
|
||||||
|
|
||||||
export function useInitiativeOperations() {
|
export function useInitiativeOperations() {
|
||||||
const ctx = useContext(InitiativeOperationsContext)
|
const ctx = useOptionalInitiativeOperations()
|
||||||
if (!ctx) {
|
if (!ctx) {
|
||||||
throw new Error('useInitiativeOperations must be used within InitiativeOperationsProvider')
|
throw new Error('useInitiativeOperations must be used within InitiativeOperationsProvider')
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,6 @@
|
||||||
import { InitiativeOperationsProvider } from '../context/InitiativeOperationsContext.jsx'
|
/**
|
||||||
import { useProgramScope } from '../context/ProgramScopeContext.jsx'
|
* AP2.3c — Scope-Gate ohne eigenen Data-Provider (Provider in ProgramScopeContext).
|
||||||
|
*/
|
||||||
export function ScopedInitiativeProvider({ initiativeId: initiativeIdProp, children }) {
|
export function ScopedInitiativeProvider({ children }) {
|
||||||
const { initiativeId: scopeInitiativeId } = useProgramScope()
|
return children
|
||||||
const initiativeId = initiativeIdProp || scopeInitiativeId
|
|
||||||
|
|
||||||
return (
|
|
||||||
<InitiativeOperationsProvider initiativeId={initiativeId}>
|
|
||||||
{children}
|
|
||||||
</InitiativeOperationsProvider>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
12
frontend/src/layout/ScopedOperationsBridge.jsx
Normal file
12
frontend/src/layout/ScopedOperationsBridge.jsx
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
import { InitiativeOperationsProvider } from '../context/InitiativeOperationsContext.jsx'
|
||||||
|
import { useProgramScope } from '../context/ProgramScopeContext.jsx'
|
||||||
|
|
||||||
|
/** AP2.3c — ein Provider pro Vorhaben-Scope (unter ProgramScope). */
|
||||||
|
export function ScopedOperationsBridge({ children }) {
|
||||||
|
const { initiativeId } = useProgramScope()
|
||||||
|
return (
|
||||||
|
<InitiativeOperationsProvider initiativeId={initiativeId || null}>
|
||||||
|
{children}
|
||||||
|
</InitiativeOperationsProvider>
|
||||||
|
)
|
||||||
|
}
|
||||||
107
frontend/src/registry/operatingSliceLoaders.js
Normal file
107
frontend/src/registry/operatingSliceLoaders.js
Normal file
|
|
@ -0,0 +1,107 @@
|
||||||
|
/**
|
||||||
|
* AP2.3c — Data Slice Registry (lazy OM loads pro Operating Context).
|
||||||
|
*/
|
||||||
|
import {
|
||||||
|
getInitiative,
|
||||||
|
getInitiativeSteeringSnapshot,
|
||||||
|
listInitiativeActions,
|
||||||
|
} from '../api/initiatives.js'
|
||||||
|
import { listInitiativeBlockers } from '../api/blockers.js'
|
||||||
|
import { listInitiativeBacklog } from '../api/backlog.js'
|
||||||
|
import { listInitiativeRoadmapItems } from '../api/roadmap.js'
|
||||||
|
import { listInitiativeProjects } from '../api/projects.js'
|
||||||
|
import { listInitiativeEvidence } from '../api/evidence.js'
|
||||||
|
import { listInitiativeDecisions } from '../api/decisions.js'
|
||||||
|
import { listInitiativeReviews } from '../api/reviews.js'
|
||||||
|
import { listInitiativeRecurring } from '../api/recurring.js'
|
||||||
|
import {
|
||||||
|
listInitiativeWorkCycles,
|
||||||
|
getActiveWorkCycle,
|
||||||
|
} from '../api/workCycles.js'
|
||||||
|
import { listSteeringMethods } from '../api/steering.js'
|
||||||
|
|
||||||
|
/** @type {Record<string, (initiativeId: string) => Promise<unknown>>} */
|
||||||
|
export const SLICE_LOADERS = {
|
||||||
|
initiative: (initiativeId) => getInitiative(initiativeId),
|
||||||
|
actions: (initiativeId) => listInitiativeActions(initiativeId),
|
||||||
|
blockers: (initiativeId) => listInitiativeBlockers(initiativeId),
|
||||||
|
backlog: (initiativeId) => listInitiativeBacklog(initiativeId),
|
||||||
|
roadmap: (initiativeId) => listInitiativeRoadmapItems(initiativeId),
|
||||||
|
projects: (initiativeId) => listInitiativeProjects(initiativeId),
|
||||||
|
evidence: (initiativeId) => listInitiativeEvidence(initiativeId),
|
||||||
|
decisions: (initiativeId) => listInitiativeDecisions(initiativeId),
|
||||||
|
reviews: (initiativeId) => listInitiativeReviews(initiativeId),
|
||||||
|
recurring: (initiativeId) => listInitiativeRecurring(initiativeId),
|
||||||
|
work_cycles: async (initiativeId) => ({
|
||||||
|
workCycles: await listInitiativeWorkCycles(initiativeId),
|
||||||
|
activeWorkCycle: await getActiveWorkCycle(initiativeId).catch(() => null),
|
||||||
|
}),
|
||||||
|
steering_methods: () => listSteeringMethods(),
|
||||||
|
steering_snapshot: (initiativeId) => getInitiativeSteeringSnapshot(initiativeId),
|
||||||
|
}
|
||||||
|
|
||||||
|
const CORE_SLICES = ['actions']
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} initiativeId
|
||||||
|
* @param {string[]} sliceKeys
|
||||||
|
* @returns {Promise<Record<string, unknown>>}
|
||||||
|
*/
|
||||||
|
export async function loadOperatingSlices(initiativeId, sliceKeys) {
|
||||||
|
const keys = new Set(['initiative', 'actions', ...(sliceKeys || [])])
|
||||||
|
const entries = await Promise.all(
|
||||||
|
[...keys].map(async (key) => {
|
||||||
|
const loader = SLICE_LOADERS[key]
|
||||||
|
if (!loader) return [key, null]
|
||||||
|
try {
|
||||||
|
const value = await loader(initiativeId)
|
||||||
|
return [key, value]
|
||||||
|
} catch {
|
||||||
|
return [key, null]
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
return Object.fromEntries(entries)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wendet Slice-Ergebnisse auf InitiativeOperations-State-Setter an.
|
||||||
|
* @param {Record<string, unknown>} slices
|
||||||
|
* @param {object} setters
|
||||||
|
*/
|
||||||
|
export function applySliceResults(slices, setters) {
|
||||||
|
if (slices.initiative != null) setters.setInitiative(slices.initiative)
|
||||||
|
if (Array.isArray(slices.actions)) setters.setActions(slices.actions)
|
||||||
|
if (Array.isArray(slices.blockers)) setters.setBlockers(slices.blockers)
|
||||||
|
if (Array.isArray(slices.backlog)) setters.setBacklogItems(slices.backlog)
|
||||||
|
if (Array.isArray(slices.roadmap)) setters.setRoadmapItems(slices.roadmap)
|
||||||
|
if (Array.isArray(slices.projects)) setters.setProjects(slices.projects)
|
||||||
|
if (Array.isArray(slices.evidence)) setters.setEvidenceItems(slices.evidence)
|
||||||
|
if (Array.isArray(slices.decisions)) setters.setDecisions(slices.decisions)
|
||||||
|
if (Array.isArray(slices.reviews)) setters.setReviews(slices.reviews)
|
||||||
|
if (Array.isArray(slices.recurring)) setters.setRecurringItems(slices.recurring)
|
||||||
|
if (Array.isArray(slices.steering_methods)) setters.setSteeringMethods(slices.steering_methods)
|
||||||
|
|
||||||
|
if (slices.work_cycles && typeof slices.work_cycles === 'object') {
|
||||||
|
const wc = /** @type {{ workCycles?: unknown[], activeWorkCycle?: unknown }} */ (
|
||||||
|
slices.work_cycles
|
||||||
|
)
|
||||||
|
if (Array.isArray(wc.workCycles)) setters.setWorkCycles(wc.workCycles)
|
||||||
|
setters.setActiveWorkCycle(wc.activeWorkCycle || null)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (slices.steering_snapshot != null) {
|
||||||
|
setters.setSteeringSnapshot(slices.steering_snapshot)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} sliceKey
|
||||||
|
* @param {string} initiativeId
|
||||||
|
* @returns {Promise<unknown>}
|
||||||
|
*/
|
||||||
|
export async function reloadOperatingSlice(sliceKey, initiativeId) {
|
||||||
|
const loader = SLICE_LOADERS[sliceKey]
|
||||||
|
if (!loader) return null
|
||||||
|
return loader(initiativeId)
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user