AP2.3d: Route Gating via OperatingRouteGate und Slice-basierte Seitenlogik.
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
1b55d8558c
commit
11e36e674e
|
|
@ -7,6 +7,7 @@ export function InitiativeInboxPage() {
|
||||||
initiative,
|
initiative,
|
||||||
initiativeId,
|
initiativeId,
|
||||||
backlogItems,
|
backlogItems,
|
||||||
|
dataSlices,
|
||||||
roadmapItems,
|
roadmapItems,
|
||||||
featureParentActions,
|
featureParentActions,
|
||||||
workCycles,
|
workCycles,
|
||||||
|
|
@ -31,8 +32,6 @@ export function InitiativeInboxPage() {
|
||||||
return <LoadingState message="Lade Eingang …" />
|
return <LoadingState message="Lade Eingang …" />
|
||||||
}
|
}
|
||||||
|
|
||||||
const isProductArchetype = initiative?.archetype_key === 'initiative.product'
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{error && <p className="error">{error}</p>}
|
{error && <p className="error">{error}</p>}
|
||||||
|
|
@ -43,7 +42,7 @@ export function InitiativeInboxPage() {
|
||||||
featureActions={featureParentActions}
|
featureActions={featureParentActions}
|
||||||
workCycles={workCycles}
|
workCycles={workCycles}
|
||||||
activeWorkCycle={activeWorkCycle}
|
activeWorkCycle={activeWorkCycle}
|
||||||
sprintPlanningEnabled={isProductArchetype}
|
sprintPlanningEnabled={dataSlices.includes('work_cycles')}
|
||||||
canManage={capabilities.has('kairo.backlog.manage')}
|
canManage={capabilities.has('kairo.backlog.manage')}
|
||||||
onCreate={handleCreateBacklog}
|
onCreate={handleCreateBacklog}
|
||||||
onUpdate={handleUpdateBacklog}
|
onUpdate={handleUpdateBacklog}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { useMemo } from 'react'
|
import { useEffect, useMemo } from 'react'
|
||||||
import { Link } from 'react-router-dom'
|
import { Link } from 'react-router-dom'
|
||||||
import { RequireInitiativeScope } from '../../components/RequireInitiativeScope.jsx'
|
import { RequireInitiativeScope } from '../../components/RequireInitiativeScope.jsx'
|
||||||
import { ScopedInitiativeProvider } from '../../layout/ScopedInitiativeProvider.jsx'
|
import { ScopedInitiativeProvider } from '../../layout/ScopedInitiativeProvider.jsx'
|
||||||
|
|
@ -41,11 +41,23 @@ function WorkSprintInner() {
|
||||||
handleMoveActionToSprint,
|
handleMoveActionToSprint,
|
||||||
handleUnplanAction,
|
handleUnplanAction,
|
||||||
initiative,
|
initiative,
|
||||||
|
operatingContext,
|
||||||
|
dataSlices,
|
||||||
steeringSnapshot,
|
steeringSnapshot,
|
||||||
steeringSnapshotLoading,
|
steeringSnapshotLoading,
|
||||||
visibleActions,
|
visibleActions,
|
||||||
|
reloadSlices,
|
||||||
} = ops
|
} = ops
|
||||||
|
|
||||||
|
const supportsWorkCycles = dataSlices.includes('work_cycles')
|
||||||
|
const isContinuousProduct = operatingContext?.method_key === 'continuous_product'
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (initiativeId && !steeringSnapshot && !steeringSnapshotLoading) {
|
||||||
|
reloadSlices(['steering_snapshot'])
|
||||||
|
}
|
||||||
|
}, [initiativeId, steeringSnapshot, steeringSnapshotLoading, reloadSlices])
|
||||||
|
|
||||||
const defaultWorkCycleId = activeWorkCycle?.id || ''
|
const defaultWorkCycleId = activeWorkCycle?.id || ''
|
||||||
|
|
||||||
const createAction = useMemo(
|
const createAction = useMemo(
|
||||||
|
|
@ -62,7 +74,7 @@ function WorkSprintInner() {
|
||||||
return <LoadingState message="Lade Sprint-Backlog …" />
|
return <LoadingState message="Lade Sprint-Backlog …" />
|
||||||
}
|
}
|
||||||
|
|
||||||
const isProductArchetype = initiative?.archetype_key === 'initiative.product'
|
const isProductArchetype = isContinuousProduct && supportsWorkCycles
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|
@ -187,8 +199,8 @@ function WorkSprintInner() {
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{!activeWorkCycle && !isProductArchetype && (
|
{!activeWorkCycle && !supportsWorkCycles && (
|
||||||
<EmptyState message="Sprint-Ausführung ist für Product-Vorhaben — wähle ein passendes Vorhaben im Scope." />
|
<EmptyState message="Sprint-Ausführung ist für diesen Vorhabentyp nicht verfügbar." />
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
|
|
||||||
30
frontend/src/registry/OperatingRouteGate.jsx
Normal file
30
frontend/src/registry/OperatingRouteGate.jsx
Normal file
|
|
@ -0,0 +1,30 @@
|
||||||
|
import { Navigate, useLocation } from 'react-router-dom'
|
||||||
|
import { useProgramScope } from '../context/ProgramScopeContext.jsx'
|
||||||
|
import { useMethodUiContext } from '../hooks/useMethodUiContext.js'
|
||||||
|
import {
|
||||||
|
isRouteAllowedForProfile,
|
||||||
|
resolveRedirectForDisallowedRoute,
|
||||||
|
} from './resolveOperatingProfile.js'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AP2.3d — Redirect wenn Route für Archetyp/Slice unpassend.
|
||||||
|
*/
|
||||||
|
export function OperatingRouteGate({ children }) {
|
||||||
|
const location = useLocation()
|
||||||
|
const { hrefWithScope, initiativeId } = useProgramScope()
|
||||||
|
const { operatingContext, loading } = useMethodUiContext()
|
||||||
|
|
||||||
|
if (!initiativeId || loading) {
|
||||||
|
return children
|
||||||
|
}
|
||||||
|
|
||||||
|
const input = { operatingContext }
|
||||||
|
const path = location.pathname
|
||||||
|
|
||||||
|
if (isRouteAllowedForProfile(path, input)) {
|
||||||
|
return children
|
||||||
|
}
|
||||||
|
|
||||||
|
const redirect = resolveRedirectForDisallowedRoute(path, input)
|
||||||
|
return <Navigate to={hrefWithScope(redirect)} replace />
|
||||||
|
}
|
||||||
|
|
@ -144,6 +144,38 @@ export const MODE_ROUTE_COMPONENTS = {
|
||||||
controlJourney: ControlJourneyPage,
|
controlJourney: ControlJourneyPage,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Modus-Routen mit Slice-Gating (AP2.3d). */
|
||||||
|
export const MODE_ROUTES = [
|
||||||
|
{ path: 'today', componentKey: 'workToday', requiredSlices: ['actions'] },
|
||||||
|
{ path: 'mine', componentKey: 'workMine', requiredSlices: ['actions'] },
|
||||||
|
{
|
||||||
|
path: 'sprint',
|
||||||
|
componentKey: 'workSprint',
|
||||||
|
requiredSlices: ['work_cycles'],
|
||||||
|
routePath: '/work/sprint',
|
||||||
|
},
|
||||||
|
{ path: 'portfolio', componentKey: 'planPortfolio', requiredSlices: [] },
|
||||||
|
{ path: 'profile', componentKey: 'planProfile', requiredSlices: [] },
|
||||||
|
{ path: 'structure', componentKey: 'planStructure', requiredSlices: ['projects'] },
|
||||||
|
{ path: 'gates', componentKey: 'planGates', requiredSlices: ['roadmap'] },
|
||||||
|
{ path: 'inbox', componentKey: 'planInbox', requiredSlices: ['backlog'] },
|
||||||
|
{
|
||||||
|
path: 'sprint',
|
||||||
|
componentKey: 'planSprint',
|
||||||
|
requiredSlices: ['work_cycles'],
|
||||||
|
routePath: '/plan/sprint',
|
||||||
|
},
|
||||||
|
{ path: 'work', componentKey: 'planWork', requiredSlices: ['actions'] },
|
||||||
|
{ path: 'status', componentKey: 'controlStatus', requiredSlices: [] },
|
||||||
|
{ path: 'plan-ist', componentKey: 'controlPlanIst', requiredSlices: ['roadmap'] },
|
||||||
|
{
|
||||||
|
path: 'journey',
|
||||||
|
componentKey: 'controlJourney',
|
||||||
|
requiredSlices: ['roadmap'],
|
||||||
|
routePath: '/control/journey',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
export {
|
export {
|
||||||
WorkLayout,
|
WorkLayout,
|
||||||
WorkIndexRedirect,
|
WorkIndexRedirect,
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user