From 83d91789c11c6fe2e9be7acae4e0f589b7893475 Mon Sep 17 00:00:00 2001 From: fs Date: Thu, 26 Mar 2026 08:16:58 +0100 Subject: [PATCH] feat(sidebar): render module-contributed admin nav items via sidebar.admin_nav_item slot The sidebar template now reads sidebar.admin_nav_item UI slots from modules and merges them into the matching admin nav group (by group key). Module permissions are resolved via layoutAuth which already includes module UI abilities from ModuleRegistry::getModuleUiAbilities(). This restores audit nav items (API audit, System audit, Import logs, User lifecycle logs) in the Logs group when the audit module is active. Co-Authored-By: Claude Opus 4.6 (1M context) --- templates/partials/app-main-aside.phtml | 34 +++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/templates/partials/app-main-aside.phtml b/templates/partials/app-main-aside.phtml index 4000694..a45279b 100644 --- a/templates/partials/app-main-aside.phtml +++ b/templates/partials/app-main-aside.phtml @@ -169,6 +169,39 @@ $adminNavGroups = [ ], ]; +// ── Merge module-contributed sidebar nav items into admin groups ── +if ($moduleNavItemSlots) { + // Index groups by key for fast lookup + $groupIndex = []; + foreach ($adminNavGroups as $idx => $group) { + $groupIndex[$group['key'] ?? ''] = $idx; + } + // Sort module items by order, then append to matching group + usort($moduleNavItemSlots, static fn ($a, $b) => ($a['order'] ?? 100) <=> ($b['order'] ?? 100)); + foreach ($moduleNavItemSlots as $slot) { + if (!is_array($slot)) { continue; } + $targetGroup = trim((string) ($slot['group'] ?? '')); + $slotPath = trim((string) ($slot['path'] ?? '')); + $slotLabel = trim((string) ($slot['label'] ?? '')); + $slotPermission = trim((string) ($slot['permission'] ?? '')); + if ($targetGroup === '' || $slotPath === '' || $slotLabel === '') { continue; } + // Check permission via layoutAuth (module permissions are merged into layout capabilities) + $slotVisible = $slotPermission === '' || !empty($layoutAuth[$slotPermission]); + if (!isset($groupIndex[$targetGroup])) { continue; } + $gIdx = $groupIndex[$targetGroup]; + $adminNavGroups[$gIdx]['items'][] = [ + 'label' => t($slotLabel), + 'path' => $slotPath, + 'active' => navActive($slotPath, true), + 'visible' => $slotVisible, + 'withTenant' => false, + ]; + if ($slotVisible) { + $adminNavGroups[$gIdx]['visible'] = true; + } + } +} + // Render helper for grouped admin aside navigation $renderAdminNavGroup = static function (array $group, string $tenantQueryParam): void { if (empty($group['visible'])) { @@ -233,6 +266,7 @@ $csrfToken = (string) ($layoutNav['csrfToken'] ?? ''); $moduleSlots = is_array($layoutNav['moduleSlots'] ?? null) ? $layoutNav['moduleSlots'] : []; $modulePanelSlots = is_array($moduleSlots['aside.tab_panel'] ?? null) ? $moduleSlots['aside.tab_panel'] : []; $moduleSearchSlots = is_array($moduleSlots['search.resource_item'] ?? null) ? $moduleSlots['search.resource_item'] : []; +$moduleNavItemSlots = is_array($moduleSlots['sidebar.admin_nav_item'] ?? null) ? $moduleSlots['sidebar.admin_nav_item'] : []; ?>