AP2.3b: Frontend Profile Resolver — Operating Context statt methodUiDefaults.
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
4ad908bfbf
commit
90934453e6
|
|
@ -12,6 +12,10 @@ export function getInitiativeSteeringSnapshot(id) {
|
||||||
return apiFetch(`/api/initiatives/${id}/steering-snapshot`)
|
return apiFetch(`/api/initiatives/${id}/steering-snapshot`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getInitiativeOperatingContext(id) {
|
||||||
|
return apiFetch(`/api/initiatives/${id}/operating-context`)
|
||||||
|
}
|
||||||
|
|
||||||
export function createInitiative(payload) {
|
export function createInitiative(payload) {
|
||||||
return apiFetch('/api/initiatives', {
|
return apiFetch('/api/initiatives', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { NavLink, useLocation } from 'react-router-dom'
|
import { NavLink, useLocation } from 'react-router-dom'
|
||||||
import { useProgramScope } from '../context/ProgramScopeContext.jsx'
|
import { useProgramScope } from '../context/ProgramScopeContext.jsx'
|
||||||
import { useMethodUiContext } from '../hooks/useMethodUiContext.js'
|
import { useMethodUiContext } from '../hooks/useMethodUiContext.js'
|
||||||
import { resolveActiveProcessStepKey } from '../config/methodUiDefaults.js'
|
import { resolveActiveProcessStepKey } from '../registry/resolveOperatingProfile.js'
|
||||||
|
|
||||||
export function MethodProcessBar() {
|
export function MethodProcessBar() {
|
||||||
const location = useLocation()
|
const location = useLocation()
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
/**
|
/**
|
||||||
* AP1.9d — Methoden-Default-UI: Archetyp/Methode steuert Prozessleiste,
|
* AP1.9d — Methoden-Default-UI (DEPRECATED AP2.3b — Fallback bis Resolver live).
|
||||||
* Modus-Defaults und sichtbare Nav-Knoten (Anti-Todo-Wand).
|
* Bevorzugt: GET /api/initiatives/:id/operating-context + resolveOperatingProfile.js
|
||||||
*
|
*
|
||||||
* @typedef {{ key: string, to: string, label: string, mode: 'plan'|'work'|'control' }} ProcessStep
|
* @typedef {{ key: string, to: string, label: string, mode: 'plan'|'work'|'control' }} ProcessStep
|
||||||
* @typedef {{
|
* @typedef {{
|
||||||
|
|
|
||||||
|
|
@ -1,24 +1,21 @@
|
||||||
import { useEffect, useMemo, useState } from 'react'
|
import { useEffect, useMemo, useState } from 'react'
|
||||||
import { getInitiative } from '../api/initiatives.js'
|
import { getInitiativeOperatingContext } from '../api/initiatives.js'
|
||||||
import { getActiveWorkCycle } from '../api/workCycles.js'
|
import { getActiveWorkCycle } from '../api/workCycles.js'
|
||||||
import { getInitiativeSteeringSnapshot } from '../api/initiatives.js'
|
|
||||||
import { useProgramScope } from '../context/ProgramScopeContext.jsx'
|
import { useProgramScope } from '../context/ProgramScopeContext.jsx'
|
||||||
import { resolveMethodUiDefaults } from '../config/methodUiDefaults.js'
|
import { resolveOperatingProfileFromInput } from '../registry/resolveOperatingProfile.js'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Lädt Archetyp/Methode für methodengebundene UI (AP1.9d).
|
* Lädt Operating Context für methodengebundene UI (AP2.3b).
|
||||||
*/
|
*/
|
||||||
export function useMethodUiContext() {
|
export function useMethodUiContext() {
|
||||||
const { initiativeId } = useProgramScope()
|
const { initiativeId } = useProgramScope()
|
||||||
const [archetypeKey, setArchetypeKey] = useState(null)
|
const [operatingContext, setOperatingContext] = useState(null)
|
||||||
const [methodKey, setMethodKey] = useState(null)
|
|
||||||
const [hasActiveSprint, setHasActiveSprint] = useState(false)
|
const [hasActiveSprint, setHasActiveSprint] = useState(false)
|
||||||
const [loading, setLoading] = useState(false)
|
const [loading, setLoading] = useState(false)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!initiativeId) {
|
if (!initiativeId) {
|
||||||
setArchetypeKey(null)
|
setOperatingContext(null)
|
||||||
setMethodKey(null)
|
|
||||||
setHasActiveSprint(false)
|
setHasActiveSprint(false)
|
||||||
return undefined
|
return undefined
|
||||||
}
|
}
|
||||||
|
|
@ -27,20 +24,17 @@ export function useMethodUiContext() {
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
|
|
||||||
Promise.all([
|
Promise.all([
|
||||||
getInitiative(initiativeId),
|
getInitiativeOperatingContext(initiativeId).catch(() => null),
|
||||||
getActiveWorkCycle(initiativeId).catch(() => null),
|
getActiveWorkCycle(initiativeId).catch(() => null),
|
||||||
getInitiativeSteeringSnapshot(initiativeId).catch(() => null),
|
|
||||||
])
|
])
|
||||||
.then(([initiative, activeCycle, snapshot]) => {
|
.then(([context, activeCycle]) => {
|
||||||
if (cancelled) return
|
if (cancelled) return
|
||||||
setArchetypeKey(initiative?.archetype_key || null)
|
setOperatingContext(context)
|
||||||
setMethodKey(snapshot?.method_key || null)
|
|
||||||
setHasActiveSprint(Boolean(activeCycle?.id))
|
setHasActiveSprint(Boolean(activeCycle?.id))
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
if (!cancelled) {
|
if (!cancelled) {
|
||||||
setArchetypeKey(null)
|
setOperatingContext(null)
|
||||||
setMethodKey(null)
|
|
||||||
setHasActiveSprint(false)
|
setHasActiveSprint(false)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
@ -53,19 +47,25 @@ export function useMethodUiContext() {
|
||||||
}
|
}
|
||||||
}, [initiativeId])
|
}, [initiativeId])
|
||||||
|
|
||||||
const input = useMemo(
|
const profileInput = useMemo(
|
||||||
() => ({ archetypeKey, methodKey, hasActiveSprint }),
|
() => ({ operatingContext, hasActiveSprint }),
|
||||||
[archetypeKey, methodKey, hasActiveSprint],
|
[operatingContext, hasActiveSprint],
|
||||||
)
|
)
|
||||||
|
|
||||||
const defaults = useMemo(() => resolveMethodUiDefaults(input), [input])
|
const profile = useMemo(
|
||||||
|
() => resolveOperatingProfileFromInput(profileInput),
|
||||||
|
[profileInput],
|
||||||
|
)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
initiativeId,
|
initiativeId,
|
||||||
archetypeKey,
|
archetypeKey: operatingContext?.archetype_key || null,
|
||||||
methodKey,
|
methodKey: operatingContext?.method_key || null,
|
||||||
|
methodProfileKey: operatingContext?.method_profile_key || null,
|
||||||
|
operatingContext,
|
||||||
hasActiveSprint,
|
hasActiveSprint,
|
||||||
loading,
|
loading,
|
||||||
...defaults,
|
dataSlices: profile.dataSlices,
|
||||||
|
...profile,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
import { Navigate } from 'react-router-dom'
|
|
||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
|
import { Navigate } from 'react-router-dom'
|
||||||
import { useProgramScope } from '../../context/ProgramScopeContext.jsx'
|
import { useProgramScope } from '../../context/ProgramScopeContext.jsx'
|
||||||
import { getInitiative } from '../../api/initiatives.js'
|
import { getInitiativeOperatingContext } from '../../api/initiatives.js'
|
||||||
import { getActiveWorkCycle } from '../../api/workCycles.js'
|
import { getActiveWorkCycle } from '../../api/workCycles.js'
|
||||||
import { resolveMethodUiDefaults } from '../../config/methodUiDefaults.js'
|
import { resolveOperatingProfileFromInput } from '../../registry/resolveOperatingProfile.js'
|
||||||
|
|
||||||
/** Portfolio ohne Scope → Übersicht; mit Vorhaben → methoden-Default (AP1.9d). */
|
/** Portfolio ohne Scope → Übersicht; mit Vorhaben → methoden-Default (AP1.9d). */
|
||||||
export function PlanIndexRedirect() {
|
export function PlanIndexRedirect() {
|
||||||
|
|
@ -17,13 +17,13 @@ export function PlanIndexRedirect() {
|
||||||
}
|
}
|
||||||
let cancelled = false
|
let cancelled = false
|
||||||
Promise.all([
|
Promise.all([
|
||||||
getInitiative(initiativeId),
|
getInitiativeOperatingContext(initiativeId),
|
||||||
getActiveWorkCycle(initiativeId).catch(() => null),
|
getActiveWorkCycle(initiativeId).catch(() => null),
|
||||||
])
|
])
|
||||||
.then(([initiative, activeCycle]) => {
|
.then(([context, activeCycle]) => {
|
||||||
if (cancelled) return
|
if (cancelled) return
|
||||||
const defaults = resolveMethodUiDefaults({
|
const defaults = resolveOperatingProfileFromInput({
|
||||||
archetypeKey: initiative?.archetype_key,
|
operatingContext: context,
|
||||||
hasActiveSprint: Boolean(activeCycle?.id),
|
hasActiveSprint: Boolean(activeCycle?.id),
|
||||||
})
|
})
|
||||||
setTarget(hrefWithScope(defaults.planDefaultRoute))
|
setTarget(hrefWithScope(defaults.planDefaultRoute))
|
||||||
|
|
|
||||||
170
frontend/src/registry/resolveOperatingProfile.js
Normal file
170
frontend/src/registry/resolveOperatingProfile.js
Normal file
|
|
@ -0,0 +1,170 @@
|
||||||
|
/**
|
||||||
|
* AP2.3b — Operating Profile Resolver (API-first, methodUiDefaults als Fallback).
|
||||||
|
*/
|
||||||
|
import { PLAN_OUTLINE_NODES } from '../plan/planOutlineNodes.js'
|
||||||
|
import { WORK_NAV_ITEMS } from '../config/workNav.js'
|
||||||
|
import {
|
||||||
|
resolveMethodUiDefaults,
|
||||||
|
resolveActiveProcessStepKey as resolveActiveProcessStepKeyFallback,
|
||||||
|
} from '../config/methodUiDefaults.js'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {import('../config/methodUiDefaults.js').ProcessStep} ProcessStep
|
||||||
|
* @typedef {import('../config/methodUiDefaults.js').MethodUiDefaults} MethodUiDefaults
|
||||||
|
* @typedef {{
|
||||||
|
* initiative_id?: string,
|
||||||
|
* archetype_key?: string | null,
|
||||||
|
* method_key?: string | null,
|
||||||
|
* method_profile_key?: string | null,
|
||||||
|
* ui_profile?: Partial<MethodUiDefaults> | null,
|
||||||
|
* data_slices?: string[],
|
||||||
|
* method_capabilities?: object,
|
||||||
|
* }} OperatingContextResponse
|
||||||
|
* @typedef {{
|
||||||
|
* archetypeKey?: string | null,
|
||||||
|
* methodKey?: string | null,
|
||||||
|
* hasActiveSprint?: boolean,
|
||||||
|
* operatingContext?: OperatingContextResponse | null,
|
||||||
|
* }} ResolveOperatingProfileInput
|
||||||
|
*/
|
||||||
|
|
||||||
|
const EMPTY_PROFILE = {
|
||||||
|
processSteps: [],
|
||||||
|
planDefaultRoute: '/plan/profile',
|
||||||
|
workDefaultRoute: '/work/today',
|
||||||
|
controlDefaultRoute: '/control/status',
|
||||||
|
planOutlineKeys: null,
|
||||||
|
workNavKeys: null,
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {OperatingContextResponse | null | undefined} context
|
||||||
|
* @param {{ hasActiveSprint?: boolean }} [options]
|
||||||
|
* @returns {MethodUiDefaults & { dataSlices: string[] }}
|
||||||
|
*/
|
||||||
|
export function resolveOperatingProfile(context, options = {}) {
|
||||||
|
if (context?.ui_profile) {
|
||||||
|
const profile = context.ui_profile
|
||||||
|
let workDefaultRoute = profile.workDefaultRoute || EMPTY_PROFILE.workDefaultRoute
|
||||||
|
if (
|
||||||
|
context.archetype_key === 'initiative.product' &&
|
||||||
|
!options.hasActiveSprint
|
||||||
|
) {
|
||||||
|
workDefaultRoute = '/work/today'
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
processSteps: profile.processSteps || [],
|
||||||
|
planDefaultRoute: profile.planDefaultRoute || EMPTY_PROFILE.planDefaultRoute,
|
||||||
|
workDefaultRoute,
|
||||||
|
controlDefaultRoute:
|
||||||
|
profile.controlDefaultRoute || EMPTY_PROFILE.controlDefaultRoute,
|
||||||
|
planOutlineKeys: profile.planOutlineKeys ?? null,
|
||||||
|
workNavKeys: profile.workNavKeys ?? null,
|
||||||
|
dataSlices: context.data_slices || profile.dataSlices || [],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const fallback = resolveMethodUiDefaults({
|
||||||
|
archetypeKey: context?.archetype_key || null,
|
||||||
|
methodKey: context?.method_key || null,
|
||||||
|
hasActiveSprint: options.hasActiveSprint,
|
||||||
|
})
|
||||||
|
return {
|
||||||
|
...fallback,
|
||||||
|
dataSlices: context?.data_slices || [],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {ResolveOperatingProfileInput} input
|
||||||
|
* @returns {MethodUiDefaults & { dataSlices: string[] }}
|
||||||
|
*/
|
||||||
|
export function resolveOperatingProfileFromInput(input = {}) {
|
||||||
|
if (input.operatingContext) {
|
||||||
|
return resolveOperatingProfile(input.operatingContext, {
|
||||||
|
hasActiveSprint: input.hasActiveSprint,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
const fallback = resolveMethodUiDefaults({
|
||||||
|
archetypeKey: input.archetypeKey || null,
|
||||||
|
methodKey: input.methodKey || null,
|
||||||
|
hasActiveSprint: input.hasActiveSprint,
|
||||||
|
})
|
||||||
|
return { ...fallback, dataSlices: [] }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {ResolveOperatingProfileInput} input
|
||||||
|
* @returns {typeof PLAN_OUTLINE_NODES}
|
||||||
|
*/
|
||||||
|
export function resolvePlanOutlineNodesFromProfile(input = {}) {
|
||||||
|
const { planOutlineKeys } = resolveOperatingProfileFromInput(input)
|
||||||
|
if (!planOutlineKeys) return PLAN_OUTLINE_NODES
|
||||||
|
const allowed = new Set(planOutlineKeys)
|
||||||
|
return PLAN_OUTLINE_NODES.filter((node) => allowed.has(node.key))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {ResolveOperatingProfileInput} input
|
||||||
|
* @returns {typeof WORK_NAV_ITEMS}
|
||||||
|
*/
|
||||||
|
export function resolveWorkNavItemsFromProfile(input = {}) {
|
||||||
|
const { workNavKeys } = resolveOperatingProfileFromInput(input)
|
||||||
|
if (!workNavKeys) return WORK_NAV_ITEMS
|
||||||
|
const order = new Map(workNavKeys.map((key, index) => [key, index]))
|
||||||
|
return WORK_NAV_ITEMS.filter((item) => order.has(item.key)).sort(
|
||||||
|
(a, b) => (order.get(a.key) ?? 0) - (order.get(b.key) ?? 0),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} pathname
|
||||||
|
* @param {ProcessStep[]} steps
|
||||||
|
*/
|
||||||
|
export function resolveActiveProcessStepKey(pathname, steps) {
|
||||||
|
return resolveActiveProcessStepKeyFallback(pathname, steps)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} routePath
|
||||||
|
* @param {ResolveOperatingProfileInput} input
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
export function isRouteAllowedForProfile(routePath, input = {}) {
|
||||||
|
const profile = resolveOperatingProfileFromInput(input)
|
||||||
|
const normalized = (routePath || '').replace(/\/$/, '')
|
||||||
|
|
||||||
|
if (normalized.startsWith('/plan/sprint') || normalized.startsWith('/work/sprint')) {
|
||||||
|
return profile.dataSlices.includes('work_cycles')
|
||||||
|
}
|
||||||
|
if (normalized.startsWith('/plan/structure')) {
|
||||||
|
return profile.dataSlices.includes('projects')
|
||||||
|
}
|
||||||
|
if (normalized.startsWith('/plan/inbox')) {
|
||||||
|
return profile.dataSlices.includes('backlog')
|
||||||
|
}
|
||||||
|
if (normalized.startsWith('/control/journey')) {
|
||||||
|
const steps = profile.processSteps || []
|
||||||
|
return steps.some((s) => s.key === 'journey')
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} routePath
|
||||||
|
* @param {ResolveOperatingProfileInput} input
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
export function resolveRedirectForDisallowedRoute(routePath, input = {}) {
|
||||||
|
const profile = resolveOperatingProfileFromInput(input)
|
||||||
|
const normalized = (routePath || '').replace(/\/$/, '')
|
||||||
|
|
||||||
|
if (normalized.startsWith('/work/')) {
|
||||||
|
return profile.workDefaultRoute
|
||||||
|
}
|
||||||
|
if (normalized.startsWith('/control/')) {
|
||||||
|
return profile.controlDefaultRoute
|
||||||
|
}
|
||||||
|
return profile.planDefaultRoute
|
||||||
|
}
|
||||||
76
frontend/src/registry/resolveOperatingProfile.test.js
Normal file
76
frontend/src/registry/resolveOperatingProfile.test.js
Normal file
|
|
@ -0,0 +1,76 @@
|
||||||
|
import { describe, expect, it } from 'vitest'
|
||||||
|
import {
|
||||||
|
isRouteAllowedForProfile,
|
||||||
|
resolveOperatingProfile,
|
||||||
|
resolveOperatingProfileFromInput,
|
||||||
|
resolveRedirectForDisallowedRoute,
|
||||||
|
} from './resolveOperatingProfile.js'
|
||||||
|
|
||||||
|
const PRODUCT_CONTEXT = {
|
||||||
|
initiative_id: 'test-id',
|
||||||
|
archetype_key: 'initiative.product',
|
||||||
|
method_key: 'continuous_product',
|
||||||
|
ui_profile: {
|
||||||
|
processSteps: [
|
||||||
|
{ key: 'inbox', to: '/plan/inbox', label: 'Eingang', mode: 'plan' },
|
||||||
|
{ key: 'sprint-plan', to: '/plan/sprint', label: 'Sprint planen', mode: 'plan' },
|
||||||
|
],
|
||||||
|
planDefaultRoute: '/plan/inbox',
|
||||||
|
workDefaultRoute: '/work/sprint',
|
||||||
|
controlDefaultRoute: '/control/status',
|
||||||
|
planOutlineKeys: ['profile', 'inbox', 'sprint', 'gates'],
|
||||||
|
workNavKeys: ['sprint', 'today', 'mine'],
|
||||||
|
},
|
||||||
|
data_slices: ['backlog', 'actions', 'roadmap', 'work_cycles', 'projects'],
|
||||||
|
}
|
||||||
|
|
||||||
|
const SUPPORT_QUEUE_CONTEXT = {
|
||||||
|
archetype_key: 'initiative.support_queue',
|
||||||
|
method_key: 'queue_pull',
|
||||||
|
ui_profile: {
|
||||||
|
processSteps: [],
|
||||||
|
planDefaultRoute: '/plan/inbox',
|
||||||
|
workDefaultRoute: '/work/today',
|
||||||
|
controlDefaultRoute: '/control/status',
|
||||||
|
planOutlineKeys: ['profile', 'inbox', 'work'],
|
||||||
|
workNavKeys: ['today', 'mine'],
|
||||||
|
},
|
||||||
|
data_slices: ['backlog', 'actions', 'blockers'],
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('resolveOperatingProfile', () => {
|
||||||
|
it('liefert UI-Profil aus Operating Context', () => {
|
||||||
|
const profile = resolveOperatingProfile(PRODUCT_CONTEXT)
|
||||||
|
expect(profile.planDefaultRoute).toBe('/plan/inbox')
|
||||||
|
expect(profile.processSteps).toHaveLength(2)
|
||||||
|
expect(profile.dataSlices).toContain('work_cycles')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Product ohne aktiven Sprint → work/today', () => {
|
||||||
|
const profile = resolveOperatingProfile(PRODUCT_CONTEXT, { hasActiveSprint: false })
|
||||||
|
expect(profile.workDefaultRoute).toBe('/work/today')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Product mit aktivem Sprint → work/sprint', () => {
|
||||||
|
const profile = resolveOperatingProfile(PRODUCT_CONTEXT, { hasActiveSprint: true })
|
||||||
|
expect(profile.workDefaultRoute).toBe('/work/sprint')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Fallback ohne Operating Context', () => {
|
||||||
|
const profile = resolveOperatingProfileFromInput({
|
||||||
|
archetypeKey: 'initiative.linear_project',
|
||||||
|
})
|
||||||
|
expect(profile.planDefaultRoute).toBe('/plan/gates')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('support_queue erlaubt kein /plan/sprint', () => {
|
||||||
|
const input = { operatingContext: SUPPORT_QUEUE_CONTEXT }
|
||||||
|
expect(isRouteAllowedForProfile('/plan/sprint', input)).toBe(false)
|
||||||
|
expect(resolveRedirectForDisallowedRoute('/plan/sprint', input)).toBe('/plan/inbox')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('product erlaubt /plan/sprint', () => {
|
||||||
|
const input = { operatingContext: PRODUCT_CONTEXT }
|
||||||
|
expect(isRouteAllowedForProfile('/plan/sprint', input)).toBe(true)
|
||||||
|
})
|
||||||
|
})
|
||||||
Loading…
Reference in New Issue
Block a user