AP1.9b: Scope-Breadcrumb modus-sensitiv für Planen, Ausführen und Kontrolle.
Some checks failed
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) Successful in 42s
Test Suite / pytest-backend (push) Has been cancelled
Some checks failed
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) Successful in 42s
Test Suite / pytest-backend (push) Has been cancelled
Portfolio- und Vorhaben-Klicks bleiben im aktiven Modus; Objekt-Routen leiten auf passende Plan-/Work-Hubs. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
afb07f70b9
commit
aa7e1626ea
|
|
@ -1,3 +1,3 @@
|
|||
APP_VERSION = "0.16.3-ap1.9a"
|
||||
APP_VERSION = "0.16.4-ap1.9b"
|
||||
DB_SCHEMA_VERSION = "014"
|
||||
APP_NAME = "jinkendo-kairo"
|
||||
|
|
|
|||
|
|
@ -1,8 +1,13 @@
|
|||
import { Link } from 'react-router-dom'
|
||||
import { Link, useLocation } from 'react-router-dom'
|
||||
import { useProgramScope } from '../context/ProgramScopeContext.jsx'
|
||||
import { initiativeCatalogPath, scopedPath } from '../utils/routes.js'
|
||||
import { initiativeCatalogPath, projectPath, scopedPath } from '../utils/routes.js'
|
||||
import {
|
||||
resolveInitiativeHub,
|
||||
resolvePortfolioHub,
|
||||
} from '../utils/scopeNavigation.js'
|
||||
|
||||
export function ProgramScopeBar() {
|
||||
const location = useLocation()
|
||||
const {
|
||||
initiativeId,
|
||||
projectId,
|
||||
|
|
@ -12,12 +17,17 @@ export function ProgramScopeBar() {
|
|||
setScope,
|
||||
} = useProgramScope()
|
||||
|
||||
const portfolioTo = resolvePortfolioHub(location.pathname)
|
||||
const initiativeTo = initiativeId
|
||||
? resolveInitiativeHub(location.pathname, initiativeId, scopedPath)
|
||||
: null
|
||||
|
||||
return (
|
||||
<div className="program-scope-bar" aria-label="Programm-Scope">
|
||||
<span className="program-scope-bar__label">Scope</span>
|
||||
<nav className="program-scope-bar__trail">
|
||||
<Link
|
||||
to={scopedPath('/cockpit')}
|
||||
to={portfolioTo}
|
||||
className={
|
||||
'program-scope-bar__crumb' +
|
||||
(!initiativeId ? ' program-scope-bar__crumb--active' : '')
|
||||
|
|
@ -26,13 +36,13 @@ export function ProgramScopeBar() {
|
|||
>
|
||||
Portfolio
|
||||
</Link>
|
||||
{initiativeId && (
|
||||
{initiativeId && initiativeTo && (
|
||||
<>
|
||||
<span className="program-scope-bar__sep" aria-hidden="true">
|
||||
›
|
||||
</span>
|
||||
<Link
|
||||
to={scopedPath('/control/status', { initiativeId })}
|
||||
to={initiativeTo}
|
||||
className={
|
||||
'program-scope-bar__crumb' +
|
||||
(!projectId ? ' program-scope-bar__crumb--active' : '')
|
||||
|
|
@ -48,9 +58,12 @@ export function ProgramScopeBar() {
|
|||
<span className="program-scope-bar__sep" aria-hidden="true">
|
||||
›
|
||||
</span>
|
||||
<span className="program-scope-bar__crumb program-scope-bar__crumb--active">
|
||||
<Link
|
||||
to={projectPath(projectId)}
|
||||
className="program-scope-bar__crumb program-scope-bar__crumb--active"
|
||||
>
|
||||
{projectTitle || 'Projekt'}
|
||||
</span>
|
||||
</Link>
|
||||
</>
|
||||
)}
|
||||
</nav>
|
||||
|
|
|
|||
91
frontend/src/utils/scopeNavigation.js
Normal file
91
frontend/src/utils/scopeNavigation.js
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
/**
|
||||
* Modus-sensitive Scope-Navigation (AP1.9b).
|
||||
* Breadcrumb-Ziele hängen vom aktiven Modus / Pfad ab — nicht immer Kontrolle.
|
||||
*/
|
||||
|
||||
const MODE_DEFAULTS = {
|
||||
cockpit: { portfolioHub: '/cockpit', initiativeHub: '/cockpit' },
|
||||
work: { portfolioHub: '/work/today', initiativeHub: '/work/today' },
|
||||
plan: { portfolioHub: '/plan/structure', initiativeHub: '/plan/structure' },
|
||||
control: { portfolioHub: '/control/status', initiativeHub: '/control/status' },
|
||||
team: { portfolioHub: '/team', initiativeHub: '/team' },
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} pathname
|
||||
* @returns {'cockpit'|'work'|'plan'|'control'|'team'}
|
||||
*/
|
||||
export function resolveAppMode(pathname) {
|
||||
const path = pathname || '/'
|
||||
if (path.startsWith('/actions') || path.startsWith('/work')) return 'work'
|
||||
if (path.startsWith('/projects') || path.startsWith('/gates') || path.startsWith('/plan')) {
|
||||
return 'plan'
|
||||
}
|
||||
if (path.startsWith('/control')) return 'control'
|
||||
if (path.startsWith('/team')) return 'team'
|
||||
if (path.startsWith('/cockpit') || path.startsWith('/workspace')) return 'cockpit'
|
||||
if (path.startsWith('/initiatives')) return 'control'
|
||||
return 'cockpit'
|
||||
}
|
||||
|
||||
/**
|
||||
* Basis-Pfad für Portfolio-Klick (Scope leeren, Modus beibehalten).
|
||||
* @param {string} pathname
|
||||
*/
|
||||
export function resolvePortfolioHub(pathname) {
|
||||
const mode = resolveAppMode(pathname)
|
||||
const defaults = MODE_DEFAULTS[mode]
|
||||
|
||||
if (mode === 'plan' && pathname.startsWith('/plan/')) {
|
||||
const sub = pathname.match(/^(\/plan\/[^/?]+)/)
|
||||
if (sub) return sub[1]
|
||||
}
|
||||
if (mode === 'control' && pathname.startsWith('/control/')) {
|
||||
const sub = pathname.match(/^(\/control\/[^/?]+)/)
|
||||
if (sub) return sub[1]
|
||||
}
|
||||
if (mode === 'work' && pathname.startsWith('/work/')) {
|
||||
const sub = pathname.match(/^(\/work\/[^/?]+)/)
|
||||
if (sub) return sub[1]
|
||||
}
|
||||
|
||||
return defaults.portfolioHub
|
||||
}
|
||||
|
||||
/**
|
||||
* Ziel für Klick auf Vorhaben im Breadcrumb (Scope behält Initiative, Projekt-Filter weg).
|
||||
* @param {string} pathname
|
||||
* @param {string} initiativeId
|
||||
* @param {import('./routes.js').scopedPath} scopedPathFn
|
||||
*/
|
||||
export function resolveInitiativeHub(pathname, initiativeId, scopedPathFn) {
|
||||
const mode = resolveAppMode(pathname)
|
||||
const scope = { initiativeId }
|
||||
|
||||
if (mode === 'plan') {
|
||||
if (pathname.startsWith('/gates/')) {
|
||||
return scopedPathFn('/plan/gates', scope)
|
||||
}
|
||||
if (pathname.startsWith('/projects/')) {
|
||||
return scopedPathFn('/plan/structure', scope)
|
||||
}
|
||||
const sub = pathname.match(/^(\/plan\/[^/?]+)/)
|
||||
return scopedPathFn(sub ? sub[1] : MODE_DEFAULTS.plan.initiativeHub, scope)
|
||||
}
|
||||
|
||||
if (mode === 'work' || pathname.startsWith('/actions/')) {
|
||||
const sub = pathname.match(/^(\/work\/[^/?]+)/)
|
||||
return scopedPathFn(sub ? sub[1] : MODE_DEFAULTS.work.initiativeHub, scope)
|
||||
}
|
||||
|
||||
if (mode === 'control') {
|
||||
const sub = pathname.match(/^(\/control\/[^/?]+)/)
|
||||
return scopedPathFn(sub ? sub[1] : MODE_DEFAULTS.control.initiativeHub, scope)
|
||||
}
|
||||
|
||||
if (mode === 'team') {
|
||||
return scopedPathFn(MODE_DEFAULTS.team.initiativeHub, scope)
|
||||
}
|
||||
|
||||
return scopedPathFn(MODE_DEFAULTS.cockpit.initiativeHub, scope)
|
||||
}
|
||||
46
frontend/src/utils/scopeNavigation.test.js
Normal file
46
frontend/src/utils/scopeNavigation.test.js
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
import { describe, expect, it } from 'vitest'
|
||||
import { scopedPath } from './routes.js'
|
||||
import {
|
||||
resolveAppMode,
|
||||
resolveInitiativeHub,
|
||||
resolvePortfolioHub,
|
||||
} from './scopeNavigation.js'
|
||||
|
||||
describe('scopeNavigation', () => {
|
||||
it('resolves app mode from path', () => {
|
||||
expect(resolveAppMode('/plan/structure')).toBe('plan')
|
||||
expect(resolveAppMode('/plan/gates')).toBe('plan')
|
||||
expect(resolveAppMode('/projects/abc')).toBe('plan')
|
||||
expect(resolveAppMode('/gates/abc')).toBe('plan')
|
||||
expect(resolveAppMode('/work/today')).toBe('work')
|
||||
expect(resolveAppMode('/actions/abc')).toBe('work')
|
||||
expect(resolveAppMode('/control/status')).toBe('control')
|
||||
expect(resolveAppMode('/cockpit')).toBe('cockpit')
|
||||
expect(resolveAppMode('/team')).toBe('team')
|
||||
})
|
||||
|
||||
it('keeps plan sub-route for portfolio hub', () => {
|
||||
expect(resolvePortfolioHub('/plan/gates')).toBe('/plan/gates')
|
||||
expect(resolvePortfolioHub('/plan/inbox')).toBe('/plan/inbox')
|
||||
})
|
||||
|
||||
it('initiative hub stays in plan mode on gates sub-route', () => {
|
||||
const href = resolveInitiativeHub('/plan/gates', 'init-1', scopedPath)
|
||||
expect(href).toBe('/plan/gates?initiative=init-1')
|
||||
})
|
||||
|
||||
it('initiative hub from action object routes to work', () => {
|
||||
const href = resolveInitiativeHub('/actions/act-1', 'init-1', scopedPath)
|
||||
expect(href).toBe('/work/today?initiative=init-1')
|
||||
})
|
||||
|
||||
it('initiative hub from project object routes to plan structure', () => {
|
||||
const href = resolveInitiativeHub('/projects/proj-1', 'init-1', scopedPath)
|
||||
expect(href).toBe('/plan/structure?initiative=init-1')
|
||||
})
|
||||
|
||||
it('control initiative hub uses current sub-route', () => {
|
||||
const href = resolveInitiativeHub('/control/journey', 'init-1', scopedPath)
|
||||
expect(href).toBe('/control/journey?initiative=init-1')
|
||||
})
|
||||
})
|
||||
Loading…
Reference in New Issue
Block a user