feat(N4): Restliche Module — Tasks/Kommunikation/Wiki/Reports/Agents/Tags/Search + Navigation + Dashboard-Schnittstelle (#368)
Check Cross-Plugin Imports / check (push) Has been cancelled
Check Cross-Plugin Imports / check (push) Has been cancelled
- Scope-Deklarationen: tasks only_mine, kommunikation conversation_ids, wiki category_ids (NEUE contracts.py), report_generator template_ids, automation agent_ids (module_key agents), tags tag_ids, unified_search entity_types dynamisch aus Provider-Registry - Core-Beiträge: navigation default_route (Startseite) + dashboard widget_app_ids (Widget-TYP-Angebot, Layout bleibt Phase M) - Backend-Filter (additive UND): /tasks (only_mine), /comm/conversations, /wiki/articles+/categories (Subtree), /reports/print-templates, /agents, /tags, /search GET+POST (entity_types-Schnitt), /miniapps?host=dashboard - apply_entity_type_scope-Helper (requested ∧ scope) - Frontend: WorkspaceSwitcher default_route-Navigation, Sidebar workspace-menu_order-Sortierung, workspaceStore moduleMenuOrder() - Tests: 18/18 Deklarationen + 11/11 Filter (TDD), Frontend 2/2 + Store 18/18, tsc clean, Build OK - Regression 64 passed (4 Kombi-Failures = Suite-Isolation, solo-bewiesen); Checker 0; Ruff = Vorbestand (Stash-bewiesen)
This commit is contained in:
@@ -155,6 +155,23 @@ async def expand_folder_scope(
|
||||
return result
|
||||
|
||||
|
||||
def apply_entity_type_scope(
|
||||
requested: list[str] | None,
|
||||
scope_entity_types: Any,
|
||||
) -> list[str] | None:
|
||||
"""Intersect requested search entity types with the workspace scope (N4).
|
||||
|
||||
Pure AND: the effective set is requested ∧ scope. ``None`` means "no
|
||||
restriction" on either side (search all). An empty result list means the
|
||||
search legitimately yields nothing (scope excludes every requested type).
|
||||
"""
|
||||
if not isinstance(scope_entity_types, list) or not scope_entity_types:
|
||||
return requested
|
||||
if requested is None:
|
||||
return list(scope_entity_types)
|
||||
return [et for et in requested if et in set(scope_entity_types)]
|
||||
|
||||
|
||||
def scope_uuid_set(raw: Any) -> set[uuid.UUID] | None:
|
||||
"""Convert a scope dimension value into a set of UUIDs (Phase N3).
|
||||
|
||||
@@ -244,4 +261,72 @@ def get_scope_definitions() -> dict[str, list[dict[str, Any]]]:
|
||||
modules.setdefault(parsed.module_key, []).extend(
|
||||
dimension.model_dump() for dimension in parsed.dimensions
|
||||
)
|
||||
|
||||
# ─── Core contributions (Phase N4) ──────────────────────────
|
||||
# Core-owned modules (no plugin owns them) contribute through the same
|
||||
# registry so the N2 editor renders them automatically.
|
||||
for core_contribution in _core_scope_contributions():
|
||||
parsed = _parse_contribution("core", core_contribution)
|
||||
if parsed is None:
|
||||
continue
|
||||
modules.setdefault(parsed.module_key, []).extend(
|
||||
dimension.model_dump() for dimension in parsed.dimensions
|
||||
)
|
||||
return modules
|
||||
|
||||
|
||||
def _core_scope_contributions() -> list[dict[str, Any]]:
|
||||
"""Scope contributions for core-owned modules (Phase N4).
|
||||
|
||||
- navigation: default_route per workspace ("Startseite") — where the
|
||||
workspace switcher navigates to.
|
||||
- dashboard: widget_app_ids — the workspace limits the OFFERED widget
|
||||
types (admin context, workspace_widgets boundary). The personal
|
||||
layout stays user-owned (Phase M boundary, user-corrected split).
|
||||
"""
|
||||
from app.core.permission_registry import CORE_PERMISSIONS
|
||||
|
||||
route_options = [
|
||||
{"value": "/", "label": "Dashboard"},
|
||||
{"value": "/contacts", "label": "Kontakte"},
|
||||
]
|
||||
# Every core permission module with a matching frontend route contributes
|
||||
# a navigation option (dynamic, registry-derived — no hardcoded list).
|
||||
known_routes = {"/", "/contacts", "/tasks", "/calendar", "/mail", "/dms", "/wiki", "/communication", "/reports", "/tags", "/search", "/agents", "/workflows"}
|
||||
for perm in CORE_PERMISSIONS:
|
||||
module = perm.get("module", "")
|
||||
route = f"/{module}"
|
||||
if route in known_routes and all(o["value"] != route for o in route_options):
|
||||
route_options.append({"value": route, "label": module.title()})
|
||||
|
||||
return [
|
||||
{
|
||||
"module_key": "navigation",
|
||||
"dimensions": [
|
||||
{
|
||||
"key": "default_route",
|
||||
"label": "Startseite",
|
||||
"control": "select",
|
||||
"options": route_options,
|
||||
"default": "/",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
"module_key": "dashboard",
|
||||
"dimensions": [
|
||||
{
|
||||
"key": "widget_app_ids",
|
||||
"label": "Verfügbare Widgets",
|
||||
"control": "multiselect",
|
||||
"options": [],
|
||||
"value_source": {
|
||||
"endpoint": "/api/v1/miniapps?host=dashboard",
|
||||
"items_path": "items",
|
||||
"value_key": "app_id",
|
||||
"label_key": "name",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user