From 1c8625f8283deb397a0fba3411bd3e6ee8a236e8 Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Thu, 4 Jun 2026 17:34:39 -0500 Subject: [PATCH] feat: admin menu auto-discovers Moko component submenus from #__menu MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Queries #__menu for all com_moko* components (except com_mokowaas). Renders each as a collapsible parent with its submenu items nested at level 3. Icons and titles loaded from the DB menu records. Uses Text::_() for language key translation. MokoJoomBackup, MokoJoomCommunity, MokoJoomCross, etc. automatically appear with their full submenu when installed — zero config needed. Authored-by: Moko Consulting Co-Authored-By: Claude Opus 4.6 (1M context) --- .../mod_mokowaas_menu/tmpl/default.php | 129 ++++++++++++------ 1 file changed, 90 insertions(+), 39 deletions(-) diff --git a/src/packages/mod_mokowaas_menu/tmpl/default.php b/src/packages/mod_mokowaas_menu/tmpl/default.php index 779461d0..dc5be777 100644 --- a/src/packages/mod_mokowaas_menu/tmpl/default.php +++ b/src/packages/mod_mokowaas_menu/tmpl/default.php @@ -2,14 +2,15 @@ /** * MokoWaaS Admin Sidebar Menu * - * Uses native Joomla admin menu classes (MetisMenu). Static MokoWaaS - * views are listed first, then installed Moko extensions are auto-discovered - * from #__extensions so they appear automatically when installed. + * Renders MokoWaaS static views first, then auto-discovers installed + * Moko components from #__menu and renders their submenu items as + * nested MetisMenu collapsible sections. */ defined('_JEXEC') or die; use Joomla\CMS\Factory; +use Joomla\CMS\Language\Text; use Joomla\CMS\Router\Route; $app = Factory::getApplication(); @@ -17,7 +18,7 @@ $currentOption = $app->getInput()->get('option', ''); $currentView = $app->getInput()->get('view', ''); // ── Static MokoWaaS views ──────────────────────────────────────────── -$items = [ +$mokowaasItems = [ ['icon' => 'icon-cogs', 'title' => 'Dashboard', 'link' => 'index.php?option=com_mokowaas'], ['icon' => 'fa-solid fa-handshake-angle', 'title' => 'Helpdesk', 'link' => 'index.php?option=com_mokowaas&view=tickets'], ['icon' => 'icon-puzzle-piece', 'title' => 'Extensions', 'link' => 'index.php?option=com_mokowaas&view=extensions'], @@ -29,73 +30,83 @@ $items = [ ['icon' => 'icon-power-off', 'title' => 'Feature Plugins', 'link' => 'index.php?option=com_plugins&filter[folder]=system&filter[search]=mokowaas'], ]; -// ── Auto-discover installed Moko extensions ────────────────────────── -// Map element → menu entry. Only shown when the extension is installed. -$mokoExtensions = [ - 'com_mokojoombackup' => ['icon' => 'fa-solid fa-box-archive', 'title' => 'MokoJoomBackup', 'link' => 'index.php?option=com_mokojoombackup'], - 'com_mokojoomtos' => ['icon' => 'icon-file-contract', 'title' => 'MokoJoomTOS', 'link' => 'index.php?option=com_mokojoomtos'], - 'com_comprofiler' => ['icon' => 'icon-users', 'title' => 'MokoJoomCommunity', 'link' => 'index.php?option=com_comprofiler'], - 'com_dpcalendar' => ['icon' => 'icon-calendar', 'title' => 'MokoJoomCalendar', 'link' => 'index.php?option=com_dpcalendar'], - 'com_joomgallery' => ['icon' => 'icon-images', 'title' => 'MokoJoomGallery', 'link' => 'index.php?option=com_joomgallery'], - 'com_mokojoomcross' => ['icon' => 'fa-solid fa-arrow-right-arrow-left', 'title' => 'MokoJoomCross', 'link' => 'index.php?option=com_mokojoomcross'], - 'com_ats' => ['icon' => 'fa-solid fa-headset', 'title' => 'Akeeba Ticket System','link' => 'index.php?option=com_ats'], - 'com_akeebabackup' => ['icon' => 'fa-solid fa-hard-drive', 'title' => 'Akeeba Backup', 'link' => 'index.php?option=com_akeebabackup'], -]; +// ── Auto-discover Moko component menus from #__menu ────────────────── +$mokoComponents = []; try { $db = Factory::getContainer()->get(\Joomla\Database\DatabaseInterface::class); - $elements = array_keys($mokoExtensions); - $quoted = array_map([$db, 'quote'], $elements); + // Find all Moko component menu items (exclude com_mokowaas — handled above) $db->setQuery( - $db->getQuery(true) - ->select($db->quoteName('element')) - ->from($db->quoteName('#__extensions')) - ->where($db->quoteName('type') . ' = ' . $db->quote('component')) - ->where($db->quoteName('element') . ' IN (' . implode(',', $quoted) . ')') - ->where($db->quoteName('enabled') . ' = 1') + "SELECT m.id, m.title, m.link, m.level, m.parent_id, m.img, e.element" + . " FROM " . $db->quoteName('#__menu') . " m" + . " LEFT JOIN " . $db->quoteName('#__extensions') . " e ON m.component_id = e.extension_id" + . " WHERE m.client_id = 1 AND m.level >= 1 AND m.published = 1" + . " AND e.element LIKE 'com_moko%'" + . " AND e.element != 'com_mokowaas'" + . " AND e.enabled = 1" + . " ORDER BY e.element, m.level, m.lft" ); - $installed = $db->loadColumn() ?: []; + $menuItems = $db->loadObjectList() ?: []; - foreach ($installed as $element) + // Group: level 1 = component parent, level 2 = children + foreach ($menuItems as $m) { - if (isset($mokoExtensions[$element])) + if ((int) $m->level === 1) { - $items[] = $mokoExtensions[$element]; + $mokoComponents[$m->element] = [ + 'id' => $m->id, + 'title' => Text::_($m->title), + 'link' => $m->link, + 'icon' => str_replace('class:', 'icon-', $m->img ?: 'class:puzzle-piece'), + 'element' => $m->element, + 'children' => [], + ]; + } + elseif ((int) $m->level === 2 && isset($mokoComponents[$m->element])) + { + $mokoComponents[$m->element]['children'][] = [ + 'title' => Text::_($m->title), + 'link' => $m->link, + 'icon' => str_replace('class:', 'icon-', $m->img ?: 'class:cog'), + ]; } } } catch (\Throwable $e) { - // Silent — menu still works with static items only + // Silent — menu works without auto-discovered components } // ── Determine active state ─────────────────────────────────────────── -$anyActive = false; -foreach ($items as $item) +$mokowaasActive = ($currentOption === 'com_mokowaas'); +$anyMokoActive = $mokowaasActive; + +foreach ($mokoComponents as $comp) { $parsed = []; - parse_str(parse_url($item['link'], PHP_URL_QUERY) ?? '', $parsed); + parse_str(parse_url($comp['link'], PHP_URL_QUERY) ?? '', $parsed); if (($parsed['option'] ?? '') === $currentOption) { - $anyActive = true; - break; + $anyMokoActive = true; } } -$parentClass = 'item parent item-level-1' . ($anyActive ? ' mm-active' : ''); -$collapseClass = 'collapse-level-1 mm-collapse' . ($anyActive ? ' mm-show' : ''); +$topClass = 'item parent item-level-1' . ($anyMokoActive ? ' mm-active' : ''); +$topCollapse = 'collapse-level-1 mm-collapse' . ($anyMokoActive ? ' mm-show' : ''); ?>