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"
|
DB_SCHEMA_VERSION = "014"
|
||||||
APP_NAME = "jinkendo-kairo"
|
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 { 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() {
|
export function ProgramScopeBar() {
|
||||||
|
const location = useLocation()
|
||||||
const {
|
const {
|
||||||
initiativeId,
|
initiativeId,
|
||||||
projectId,
|
projectId,
|
||||||
|
|
@ -12,12 +17,17 @@ export function ProgramScopeBar() {
|
||||||
setScope,
|
setScope,
|
||||||
} = useProgramScope()
|
} = useProgramScope()
|
||||||
|
|
||||||
|
const portfolioTo = resolvePortfolioHub(location.pathname)
|
||||||
|
const initiativeTo = initiativeId
|
||||||
|
? resolveInitiativeHub(location.pathname, initiativeId, scopedPath)
|
||||||
|
: null
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="program-scope-bar" aria-label="Programm-Scope">
|
<div className="program-scope-bar" aria-label="Programm-Scope">
|
||||||
<span className="program-scope-bar__label">Scope</span>
|
<span className="program-scope-bar__label">Scope</span>
|
||||||
<nav className="program-scope-bar__trail">
|
<nav className="program-scope-bar__trail">
|
||||||
<Link
|
<Link
|
||||||
to={scopedPath('/cockpit')}
|
to={portfolioTo}
|
||||||
className={
|
className={
|
||||||
'program-scope-bar__crumb' +
|
'program-scope-bar__crumb' +
|
||||||
(!initiativeId ? ' program-scope-bar__crumb--active' : '')
|
(!initiativeId ? ' program-scope-bar__crumb--active' : '')
|
||||||
|
|
@ -26,13 +36,13 @@ export function ProgramScopeBar() {
|
||||||
>
|
>
|
||||||
Portfolio
|
Portfolio
|
||||||
</Link>
|
</Link>
|
||||||
{initiativeId && (
|
{initiativeId && initiativeTo && (
|
||||||
<>
|
<>
|
||||||
<span className="program-scope-bar__sep" aria-hidden="true">
|
<span className="program-scope-bar__sep" aria-hidden="true">
|
||||||
›
|
›
|
||||||
</span>
|
</span>
|
||||||
<Link
|
<Link
|
||||||
to={scopedPath('/control/status', { initiativeId })}
|
to={initiativeTo}
|
||||||
className={
|
className={
|
||||||
'program-scope-bar__crumb' +
|
'program-scope-bar__crumb' +
|
||||||
(!projectId ? ' program-scope-bar__crumb--active' : '')
|
(!projectId ? ' program-scope-bar__crumb--active' : '')
|
||||||
|
|
@ -48,9 +58,12 @@ export function ProgramScopeBar() {
|
||||||
<span className="program-scope-bar__sep" aria-hidden="true">
|
<span className="program-scope-bar__sep" aria-hidden="true">
|
||||||
›
|
›
|
||||||
</span>
|
</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'}
|
{projectTitle || 'Projekt'}
|
||||||
</span>
|
</Link>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</nav>
|
</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