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
+175
View File
@@ -0,0 +1,175 @@
from __future__ import annotations
from dataclasses import dataclass
from typing import Any, Iterable, Mapping
NAVIGATION_PREFERENCES_KEY = "navigation_preferences"
NAVIGATION_PREFERENCES_CONTRACT_VERSION = "1"
_MAX_ITEMS = 256
@dataclass(frozen=True, slots=True)
class NavigationPreferences:
order: tuple[str, ...] = ()
hidden: tuple[str, ...] = ()
locked: tuple[str, ...] = ()
def as_dict(self) -> dict[str, object]:
return {
"contract_version": NAVIGATION_PREFERENCES_CONTRACT_VERSION,
"order": list(self.order),
"hidden": list(self.hidden),
"locked": list(self.locked),
}
@dataclass(frozen=True, slots=True)
class EffectiveNavigationItem:
id: str
order: int
visible: bool
locked: bool
order_source: str
visibility_source: str
lock_source: str | None = None
def as_dict(self) -> dict[str, object]:
return {
"navigation_id": self.id,
"order": self.order,
"navigation_visible": self.visible,
"navigation_locked": self.locked,
"navigation_order_source": self.order_source,
"navigation_visibility_source": self.visibility_source,
"navigation_lock_source": self.lock_source,
}
def navigation_preferences_from_settings(
settings: object,
) -> NavigationPreferences | None:
if not isinstance(settings, Mapping):
return None
raw = settings.get(NAVIGATION_PREFERENCES_KEY)
if not isinstance(raw, Mapping):
return None
return navigation_preferences_from_mapping(raw)
def navigation_preferences_from_mapping(
raw: Mapping[str, Any],
) -> NavigationPreferences:
return NavigationPreferences(
order=_ids(raw.get("order")),
hidden=_ids(raw.get("hidden")),
locked=_ids(raw.get("locked")),
)
def update_navigation_preferences(
settings: object,
preferences: NavigationPreferences | Mapping[str, Any] | None,
) -> dict[str, Any]:
updated = dict(settings) if isinstance(settings, Mapping) else {}
if preferences is None:
updated.pop(NAVIGATION_PREFERENCES_KEY, None)
else:
raw = preferences.as_dict() if isinstance(preferences, NavigationPreferences) else preferences
updated[NAVIGATION_PREFERENCES_KEY] = navigation_preferences_from_mapping(
raw
).as_dict()
return updated
def resolve_navigation_preferences(
item_ids: Iterable[str],
*,
system: NavigationPreferences | None = None,
tenant: NavigationPreferences | None = None,
user: NavigationPreferences | None = None,
) -> dict[str, EffectiveNavigationItem]:
ordered = list(dict.fromkeys(_clean_id(item_id) for item_id in item_ids))
ordered = [item_id for item_id in ordered if item_id]
available = set(ordered)
order_source = {item_id: "module" for item_id in ordered}
visibility = {item_id: True for item_id in ordered}
visibility_source = {item_id: "module" for item_id in ordered}
locks: dict[str, str] = {}
for source, preferences, may_lock in (
("system", system, True),
("tenant", tenant, True),
("user", user, False),
):
if preferences is None:
continue
requested_order = [item_id for item_id in preferences.order if item_id in available]
if requested_order:
requested = set(requested_order)
ordered = [*requested_order, *(item_id for item_id in ordered if item_id not in requested)]
for item_id in requested_order:
order_source[item_id] = source
requested_hidden = set(preferences.hidden).intersection(available)
for item_id in available:
if item_id in locks:
visibility[item_id] = True
visibility_source[item_id] = locks[item_id]
continue
visibility[item_id] = item_id not in requested_hidden
visibility_source[item_id] = source
if may_lock:
for item_id in preferences.locked:
if item_id not in available:
continue
locks[item_id] = source
visibility[item_id] = True
visibility_source[item_id] = source
return {
item_id: EffectiveNavigationItem(
id=item_id,
order=index,
visible=visibility[item_id],
locked=item_id in locks,
order_source=order_source[item_id],
visibility_source=visibility_source[item_id],
lock_source=locks.get(item_id),
)
for index, item_id in enumerate(ordered)
}
def _ids(value: object) -> tuple[str, ...]:
if not isinstance(value, (list, tuple)):
return ()
cleaned = tuple(
dict.fromkeys(
item_id
for item in value[:_MAX_ITEMS]
if (item_id := _clean_id(item))
)
)
return cleaned
def _clean_id(value: object) -> str:
if not isinstance(value, str):
return ""
clean = value.strip()
if not clean or len(clean) > 255 or any(ord(character) < 32 for character in clean):
return ""
return clean
__all__ = [
"EffectiveNavigationItem",
"NAVIGATION_PREFERENCES_CONTRACT_VERSION",
"NAVIGATION_PREFERENCES_KEY",
"NavigationPreferences",
"navigation_preferences_from_mapping",
"navigation_preferences_from_settings",
"resolve_navigation_preferences",
"update_navigation_preferences",
]