From 1f89a323d5912ac37a81f57cc194f40b433eadba Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Thu, 4 Jun 2026 14:26:47 -0500 Subject: [PATCH] feat: auto-discover installed Moko extensions in admin sidebar menu Static MokoWaaS views listed first, then installed Moko components auto-discovered from #__extensions. Supported: MokoJoomBackup, MokoJoomCommunity, MokoJoomCalendar, MokoJoomGallery, MokoJoomCross, Akeeba Backup, Akeeba Ticket System. New extensions appear automatically. Authored-by: Moko Consulting Co-Authored-By: Claude Opus 4.6 (1M context) --- .../mod_mokowaas_menu/tmpl/default.php | 93 +++++++++++++++---- 1 file changed, 74 insertions(+), 19 deletions(-) diff --git a/src/packages/mod_mokowaas_menu/tmpl/default.php b/src/packages/mod_mokowaas_menu/tmpl/default.php index 94da8597..779461d0 100644 --- a/src/packages/mod_mokowaas_menu/tmpl/default.php +++ b/src/packages/mod_mokowaas_menu/tmpl/default.php @@ -2,34 +2,89 @@ /** * MokoWaaS Admin Sidebar Menu * - * Uses native Joomla admin menu classes (MetisMenu) so it renders - * identically to Joomla's own sidebar menu items. + * 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. */ defined('_JEXEC') or die; -use Joomla\CMS\Language\Text; +use Joomla\CMS\Factory; use Joomla\CMS\Router\Route; -$items = [ - ['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'], - ['icon' => 'fa-solid fa-file-code', 'title' => '.htaccess Maker', 'link' => 'index.php?option=com_mokowaas&view=htaccess'], - ['icon' => 'icon-lock', 'title' => 'Privacy Guard', 'link' => 'index.php?option=com_mokowaas&view=privacy'], - ['icon' => 'icon-shield-alt', 'title' => 'WAF Log', 'link' => 'index.php?option=com_mokowaas&view=waflog'], - ['icon' => 'icon-database', 'title' => 'Database Tools', 'link' => 'index.php?option=com_mokowaas&view=database'], - ['icon' => 'icon-trash', 'title' => 'Cache Cleanup', 'link' => 'index.php?option=com_mokowaas&view=cleanup'], - ['icon' => 'icon-power-off', 'title' => 'Feature Plugins', 'link' => 'index.php?option=com_plugins&filter[folder]=system&filter[search]=mokowaas'], -]; - -$app = \Joomla\CMS\Factory::getApplication(); +$app = Factory::getApplication(); $currentOption = $app->getInput()->get('option', ''); $currentView = $app->getInput()->get('view', ''); -// Determine if any child is active (auto-expand) -$anyActive = ($currentOption === 'com_mokowaas'); -$parentClass = 'item parent item-level-1' . ($anyActive ? ' mm-active' : ''); +// ── Static MokoWaaS views ──────────────────────────────────────────── +$items = [ + ['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'], + ['icon' => 'fa-solid fa-file-code', 'title' => '.htaccess Maker', 'link' => 'index.php?option=com_mokowaas&view=htaccess'], + ['icon' => 'icon-lock', 'title' => 'Privacy Guard', 'link' => 'index.php?option=com_mokowaas&view=privacy'], + ['icon' => 'icon-shield-alt', 'title' => 'WAF Log', 'link' => 'index.php?option=com_mokowaas&view=waflog'], + ['icon' => 'icon-database', 'title' => 'Database Tools', 'link' => 'index.php?option=com_mokowaas&view=database'], + ['icon' => 'icon-trash', 'title' => 'Cache Cleanup', 'link' => 'index.php?option=com_mokowaas&view=cleanup'], + ['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'], +]; + +try +{ + $db = Factory::getContainer()->get(\Joomla\Database\DatabaseInterface::class); + $elements = array_keys($mokoExtensions); + $quoted = array_map([$db, 'quote'], $elements); + + $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') + ); + $installed = $db->loadColumn() ?: []; + + foreach ($installed as $element) + { + if (isset($mokoExtensions[$element])) + { + $items[] = $mokoExtensions[$element]; + } + } +} +catch (\Throwable $e) +{ + // Silent — menu still works with static items only +} + +// ── Determine active state ─────────────────────────────────────────── +$anyActive = false; +foreach ($items as $item) +{ + $parsed = []; + parse_str(parse_url($item['link'], PHP_URL_QUERY) ?? '', $parsed); + if (($parsed['option'] ?? '') === $currentOption) + { + $anyActive = true; + break; + } +} + +$parentClass = 'item parent item-level-1' . ($anyActive ? ' mm-active' : ''); $collapseClass = 'collapse-level-1 mm-collapse' . ($anyActive ? ' mm-show' : ''); ?>