fix(ui): Plan-Navigation nicht mehr durch leere data_slices blockieren
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 44s
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 44s
Test Suite / pytest-backend (push) Has been cancelled
Archetyp-UI-Profil und Methoden-Schnittmenge werden korrekt gemerged; Frontend faellt bei leerem data_slices auf ui_profile.dataSlices zurueck, damit Eingang/Sprint/Gates erreichbar bleiben.
This commit is contained in:
parent
fe1edb84f8
commit
11b7128204
|
|
@ -47,16 +47,29 @@ def _load_ui_profile_from_db(archetype_key: str) -> Optional[dict[str, Any]]:
|
|||
conn.close()
|
||||
|
||||
|
||||
def _resolve_ui_profile(archetype_key: str) -> dict[str, Any]:
|
||||
db_profile = _load_ui_profile_from_db(archetype_key)
|
||||
if db_profile:
|
||||
return db_profile
|
||||
def _seed_ui_profile(archetype_key: str) -> dict[str, Any]:
|
||||
seed_profile = get_ui_profile_json(archetype_key)
|
||||
if seed_profile:
|
||||
return seed_profile
|
||||
return resolve_ui_profile(archetype_key)
|
||||
|
||||
|
||||
def _resolve_ui_profile(archetype_key: str) -> dict[str, Any]:
|
||||
seed_profile = _seed_ui_profile(archetype_key)
|
||||
db_profile = _load_ui_profile_from_db(archetype_key)
|
||||
if not db_profile:
|
||||
return seed_profile
|
||||
|
||||
merged = {**seed_profile, **db_profile}
|
||||
if not (merged.get("omCapabilities") or merged.get("dataSlices")):
|
||||
seed_slices = seed_profile.get("dataSlices") or []
|
||||
merged["dataSlices"] = list(seed_slices)
|
||||
merged["omCapabilities"] = list(
|
||||
seed_profile.get("omCapabilities") or seed_slices
|
||||
)
|
||||
return merged
|
||||
|
||||
|
||||
def _resolve_om_capabilities(ui_profile: dict[str, Any]) -> frozenset[str]:
|
||||
caps = ui_profile.get("omCapabilities") or ui_profile.get("dataSlices") or []
|
||||
return frozenset(caps)
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from auth import AUTH_HEADER
|
||||
from services import operating_context as operating_context_service
|
||||
from tests.factories import provision_user_in_tenant
|
||||
from tests.test_initiatives_actions import _auth, _create_initiative, _login
|
||||
|
||||
|
|
@ -182,3 +183,19 @@ def test_operating_context_not_found(client):
|
|||
headers=_auth(token),
|
||||
)
|
||||
assert res.status_code == 404
|
||||
|
||||
|
||||
def test_resolve_ui_profile_merges_seed_slices_when_db_partial(monkeypatch):
|
||||
monkeypatch.setattr(
|
||||
operating_context_service,
|
||||
"_load_ui_profile_from_db",
|
||||
lambda _key: {
|
||||
"planOutlineKeys": ["profile", "inbox", "sprint", "gates"],
|
||||
"planDefaultRoute": "/plan/inbox",
|
||||
},
|
||||
)
|
||||
profile = operating_context_service._resolve_ui_profile("initiative.product")
|
||||
assert profile["planOutlineKeys"] == ["profile", "inbox", "sprint", "gates"]
|
||||
assert "backlog" in profile["dataSlices"]
|
||||
assert "work_cycles" in profile["dataSlices"]
|
||||
assert profile["omCapabilities"]
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import {
|
|||
useState,
|
||||
} from 'react'
|
||||
import { useParams } from 'react-router-dom'
|
||||
import { resolveEffectiveDataSlices } from '../registry/resolveOperatingProfile.js'
|
||||
import {
|
||||
getInitiative,
|
||||
getInitiativeSteeringSnapshot,
|
||||
|
|
@ -190,7 +191,7 @@ export function InitiativeOperationsProvider({ children, initiativeId: initiativ
|
|||
setOperatingContext(opContext)
|
||||
}
|
||||
|
||||
const omSlices = canReadOm ? opContext?.data_slices || [] : []
|
||||
const omSlices = canReadOm ? resolveEffectiveDataSlices(opContext) : []
|
||||
const keysToLoad = sliceKeys || ['initiative', ...omSlices]
|
||||
|
||||
if (keysToLoad.includes('steering_snapshot')) {
|
||||
|
|
@ -1030,7 +1031,7 @@ export function InitiativeOperationsProvider({ children, initiativeId: initiativ
|
|||
steeringSnapshotError,
|
||||
steeringMethods,
|
||||
operatingContext,
|
||||
dataSlices: operatingContext?.data_slices || [],
|
||||
dataSlices: resolveEffectiveDataSlices(operatingContext),
|
||||
uiFeatures,
|
||||
steeringElements,
|
||||
operatingProfile,
|
||||
|
|
|
|||
|
|
@ -48,6 +48,23 @@ const EMPTY_PROFILE = {
|
|||
steeringElements: [],
|
||||
}
|
||||
|
||||
/**
|
||||
* API data_slices haben Vorrang; leeres Array gilt als „fehlend“ (JS-Truthy-Falle).
|
||||
* @param {OperatingContextResponse | null | undefined} context
|
||||
* @returns {string[]}
|
||||
*/
|
||||
export function resolveEffectiveDataSlices(context) {
|
||||
const apiSlices = context?.data_slices
|
||||
if (Array.isArray(apiSlices) && apiSlices.length > 0) {
|
||||
return apiSlices
|
||||
}
|
||||
const profileSlices = context?.ui_profile?.dataSlices
|
||||
if (Array.isArray(profileSlices) && profileSlices.length > 0) {
|
||||
return profileSlices
|
||||
}
|
||||
return Array.isArray(apiSlices) ? apiSlices : []
|
||||
}
|
||||
|
||||
function resolveUiFeatures(context, profileFeatures) {
|
||||
if (context?.ui_features && Object.keys(context.ui_features).length) {
|
||||
return { ...(context.ui_features || {}) }
|
||||
|
|
@ -85,7 +102,7 @@ export function resolveOperatingProfile(context, options = {}) {
|
|||
profile.controlDefaultRoute || EMPTY_PROFILE.controlDefaultRoute,
|
||||
planOutlineKeys: profile.planOutlineKeys ?? null,
|
||||
workNavKeys: profile.workNavKeys ?? null,
|
||||
dataSlices: context.data_slices || profile.dataSlices || [],
|
||||
dataSlices: resolveEffectiveDataSlices(context),
|
||||
uiFeatures: resolveUiFeatures(context, profile.uiFeatures),
|
||||
steeringElements: resolveSteeringElements(context),
|
||||
}
|
||||
|
|
@ -98,7 +115,7 @@ export function resolveOperatingProfile(context, options = {}) {
|
|||
})
|
||||
return {
|
||||
...fallback,
|
||||
dataSlices: context?.data_slices || [],
|
||||
dataSlices: resolveEffectiveDataSlices(context),
|
||||
uiFeatures: resolveUiFeatures(context, {}),
|
||||
steeringElements: resolveSteeringElements(context),
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { describe, expect, it } from 'vitest'
|
||||
import {
|
||||
isRouteAllowedForProfile,
|
||||
resolveEffectiveDataSlices,
|
||||
resolveOperatingProfile,
|
||||
resolveOperatingProfileFromInput,
|
||||
resolveRedirectForDisallowedRoute,
|
||||
|
|
@ -83,6 +84,26 @@ describe('resolveOperatingProfile', () => {
|
|||
expect(isRouteAllowedForProfile('/plan/sprint', input)).toBe(true)
|
||||
})
|
||||
|
||||
it('leeres data_slices fällt auf ui_profile.dataSlices zurück', () => {
|
||||
const context = {
|
||||
...PRODUCT_CONTEXT,
|
||||
data_slices: [],
|
||||
ui_profile: {
|
||||
...PRODUCT_CONTEXT.ui_profile,
|
||||
dataSlices: ['backlog', 'roadmap', 'work_cycles', 'actions'],
|
||||
},
|
||||
}
|
||||
expect(resolveEffectiveDataSlices(context)).toEqual([
|
||||
'backlog',
|
||||
'roadmap',
|
||||
'work_cycles',
|
||||
'actions',
|
||||
])
|
||||
expect(isRouteAllowedForProfile('/plan/inbox', { operatingContext: context })).toBe(true)
|
||||
expect(isRouteAllowedForProfile('/plan/gates', { operatingContext: context })).toBe(true)
|
||||
expect(isRouteAllowedForProfile('/plan/sprint', { operatingContext: context })).toBe(true)
|
||||
})
|
||||
|
||||
it('liefert uiFeatures aus Methoden-Vertrag (API)', () => {
|
||||
const profile = resolveOperatingProfile(PRODUCT_CONTEXT)
|
||||
expect(profile.uiFeatures.continuousProductWorkMode).toBe(true)
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user