Coverage for src/lexigram/admin/navigation/assembler.py: 0%
42 statements
« prev ^ index » next coverage.py v7.15.4, created at 2026-08-24 23:18 +0800
« prev ^ index » next coverage.py v7.15.4, created at 2026-08-24 23:18 +0800
1"""Navigation assembler — builds unified sidebar from contributors and resources."""
3from __future__ import annotations
5from collections.abc import Sequence
6from typing import Any
8from lexigram.admin.contributors.registry import ContributorRegistry
9from lexigram.contracts.admin.types import NavigationContribution
12class NavigationAssembler:
13 """Merges navigation from contributors and resources into grouped sidebar.
15 Groups are keyed by the ``group`` field of ``NavigationContribution``.
16 Items within each group are sorted by ``order``.
17 """
19 def __init__(
20 self,
21 contributor_registry: ContributorRegistry,
22 resource_items: Sequence[NavigationContribution] | None = None,
23 ) -> None:
24 self._registry = contributor_registry
25 self._resource_items = list(resource_items or [])
27 async def build(self) -> dict[str, list[NavigationContribution]]:
28 """Build grouped navigation from all sources.
30 Returns a dict keyed by group name, with items sorted by order.
31 """
32 groups: dict[str, list[NavigationContribution]] = {}
34 for item in self._resource_items:
35 groups.setdefault(item.group, []).append(item)
37 for contributor in self._registry.get_all():
38 for item in contributor.get_navigation_items():
39 groups.setdefault(item.group, []).append(item)
41 for group_name, items in groups.items():
42 groups[group_name] = sorted(items, key=lambda n: n.order)
44 return groups
47def contributions_to_flat_nav(
48 groups: dict[str, list[NavigationContribution]],
49) -> list[dict[str, Any]]:
50 """Convert grouped NavigationContribution objects to AdminShell-compatible flat dicts.
52 Args:
53 groups: Output from ``NavigationAssembler.build()`` — dict keyed by group name.
55 Returns:
56 Flat list of dicts suitable for ``AdminShell._prepare_navigation()``.
57 Group headers have ``is_group=True``. Items include label, href, icon,
58 and optionally permission and badge.
59 """
60 result: list[dict[str, Any]] = []
62 top_items: list[dict[str, Any]] = []
63 grouped: dict[str, list[dict[str, Any]]] = {}
65 for group_name, items in groups.items():
66 if not items:
67 continue
68 for item in items:
69 nav_dict: dict[str, Any] = {
70 "label": item.label,
71 "href": item.url,
72 "icon": item.icon,
73 }
74 if item.permission:
75 nav_dict["permission"] = item.permission
76 if item.badge_endpoint:
77 nav_dict["badge"] = item.badge_endpoint
79 if not group_name:
80 top_items.append(nav_dict)
81 else:
82 grouped.setdefault(group_name, []).append(nav_dict)
84 result.extend(top_items)
86 for group_name in sorted(grouped.keys()):
87 group_items = grouped[group_name]
88 result.append({"is_group": True, "label": group_name.replace("_", " ").title()})
89 result.extend(group_items)
91 return result
94__all__ = ["NavigationAssembler", "contributions_to_flat_nav"]