feat(core): add layered side rail preferences

This commit is contained in:
2026-08-20 01:47:04 +02:00
parent 51bf14f376
commit cf16a7b27a
13 changed files with 763 additions and 19 deletions
+122 -11
View File
@@ -7,6 +7,12 @@ from govoplan_core.admin.models import SystemSettings
from govoplan_core.admin.settings import SYSTEM_SETTINGS_ID
from govoplan_core.auth import ApiPrincipal, get_api_principal, require_any_scope
from govoplan_core.core.maintenance import saved_maintenance_mode
from govoplan_core.core.navigation import (
EffectiveNavigationItem,
navigation_preferences_from_mapping,
navigation_preferences_from_settings,
resolve_navigation_preferences,
)
from govoplan_core.core.module_entitlements import (
module_entitlement_payload,
tenant_module_entitlement_state,
@@ -87,21 +93,116 @@ def _effective_manifest_state(
)
def _nav_item_payload(item: NavItem, module_id: str | None = None) -> dict[str, object]:
def _effective_navigation_state(
principal: ApiPrincipal,
manifests: tuple[ModuleManifest, ...],
) -> dict[str, dict[str, EffectiveNavigationItem]]:
default_items: list[tuple[int, str, str]] = []
for manifest in manifests:
frontend_items = (
manifest.frontend.nav_items
if manifest.frontend is not None and manifest.frontend.nav_items
else manifest.nav_items
)
for item in frontend_items:
navigation_id = item.surface_id or navigation_view_surface_id(
manifest.id, item.path
)
default_items.append((item.order, item.label, navigation_id))
default_ids = [
item_id
for _order, _label, item_id in sorted(
default_items, key=lambda item: (item[0], item[1], item[2])
)
]
principal_ref = getattr(principal, "principal", None)
tenant_id = getattr(principal_ref, "tenant_id", None)
system_preferences = None
tenant_preferences = None
if tenant_id is not None:
try:
with get_database().session() as session:
system_item = session.get(SystemSettings, SYSTEM_SETTINGS_ID)
tenant = session.get(Tenant, tenant_id)
system_preferences = navigation_preferences_from_settings(
system_item.settings if system_item is not None else {}
)
tenant_preferences = navigation_preferences_from_settings(
tenant.settings if tenant is not None else {}
)
except (RuntimeError, SQLAlchemyError) as exc:
raise HTTPException(
status_code=503,
detail="Navigation preferences could not be resolved.",
) from exc
user_settings = getattr(getattr(principal, "user", None), "settings", {})
user_ui = user_settings.get("ui") if isinstance(user_settings, dict) else {}
user_navigation = user_ui.get("navigation") if isinstance(user_ui, dict) else None
user_preferences = (
navigation_preferences_from_mapping(user_navigation)
if isinstance(user_navigation, dict)
else None
)
return {
"module": resolve_navigation_preferences(default_ids),
"system": resolve_navigation_preferences(
default_ids,
system=system_preferences,
),
"tenant": resolve_navigation_preferences(
default_ids,
system=system_preferences,
tenant=tenant_preferences,
),
"user": resolve_navigation_preferences(
default_ids,
system=system_preferences,
tenant=tenant_preferences,
user=user_preferences,
),
}
def _nav_item_payload(
item: NavItem,
module_id: str | None = None,
navigation: dict[str, dict[str, EffectiveNavigationItem]] | None = None,
) -> dict[str, object]:
navigation_id = (
item.surface_id or navigation_view_surface_id(module_id, item.path)
if module_id
else item.surface_id
)
effective = (
navigation.get("user", {}).get(navigation_id)
if navigation_id and navigation
else None
)
payload = {
"path": item.path,
"label": item.label,
"icon": item.icon,
"section": item.section,
"required_all": list(item.required_all),
"required_any": list(item.required_any),
"order": item.order,
"surface_id": (
item.surface_id or navigation_view_surface_id(module_id, item.path)
if module_id
else item.surface_id
),
"order": effective.order if effective is not None else item.order,
"surface_id": navigation_id,
}
if effective is not None:
payload.update(effective.as_dict())
payload["navigation_layers"] = {
scope: {
"order": scoped.order,
"visible": scoped.visible,
"locked": scoped.locked,
"lock_source": scoped.lock_source,
}
for scope in ("module", "system", "tenant")
if (scoped := navigation.get(scope, {}).get(navigation_id)) is not None
}
return payload
def _frontend_route_payload(route: FrontendRoute, module_id: str | None = None) -> dict[str, object]:
@@ -241,7 +342,10 @@ def _documentation_help_contexts(manifest: ModuleManifest) -> list[dict[str, obj
return contexts
def _frontend_payload(manifest: ModuleManifest) -> dict[str, object] | None:
def _frontend_payload(
manifest: ModuleManifest,
navigation: dict[str, dict[str, EffectiveNavigationItem]] | None = None,
) -> dict[str, object] | None:
frontend = manifest.frontend
if frontend is None:
return None
@@ -257,7 +361,10 @@ def _frontend_payload(manifest: ModuleManifest) -> dict[str, object] | None:
"public_routes": [
_public_frontend_route_payload(route) for route in frontend.public_routes
],
"nav": [_nav_item_payload(item, manifest.id) for item in frontend.nav_items],
"nav": [
_nav_item_payload(item, manifest.id, navigation)
for item in frontend.nav_items
],
"settings_routes": [_frontend_route_payload(route, manifest.id) for route in frontend.settings_routes],
"view_surface_contract_version": VIEW_SURFACE_CONTRACT_VERSION,
"view_surfaces": _frontend_view_surfaces(manifest),
@@ -323,6 +430,7 @@ def create_platform_router(settings: object | None = None) -> APIRouter:
request,
principal,
)
navigation_state = _effective_navigation_state(principal, manifests)
principal_ref = getattr(principal, "principal", None)
tenant_id = getattr(principal_ref, "tenant_id", None)
return {
@@ -351,8 +459,11 @@ def create_platform_router(settings: object | None = None) -> APIRouter:
if key != "declarations"
},
"help_contexts": _documentation_help_contexts(manifest),
"nav": [_nav_item_payload(item, manifest.id) for item in manifest.nav_items],
"frontend": _frontend_payload(manifest),
"nav": [
_nav_item_payload(item, manifest.id, navigation_state)
for item in manifest.nav_items
],
"frontend": _frontend_payload(manifest, navigation_state),
}
for manifest in manifests
],