feat: admin menu auto-discovers Moko component submenus from #__menu
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) <noreply@anthropic.com>
This commit is contained in:
@@ -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' : '');
|
||||
?>
|
||||
|
||||
<ul class="nav flex-column main-nav">
|
||||
<li class="<?php echo $parentClass; ?>">
|
||||
<li class="<?php echo $topClass; ?>">
|
||||
<a class="has-arrow" href="#" aria-label="MokoWaaS">
|
||||
<span class="icon-shield-alt" aria-hidden="true"></span>
|
||||
<span class="sidebar-item-title">MokoWaaS</span>
|
||||
</a>
|
||||
<ul class="<?php echo $collapseClass; ?>">
|
||||
<?php foreach ($items as $item): ?>
|
||||
<ul class="<?php echo $topCollapse; ?>">
|
||||
|
||||
<?php // ── MokoWaaS static items ── ?>
|
||||
<?php foreach ($mokowaasItems as $item): ?>
|
||||
<?php
|
||||
$active = false;
|
||||
$parsed = [];
|
||||
@@ -116,6 +127,46 @@ $collapseClass = 'collapse-level-1 mm-collapse' . ($anyActive ? ' mm-show' : '')
|
||||
</a>
|
||||
</li>
|
||||
<?php endforeach; ?>
|
||||
|
||||
<?php // ── Auto-discovered Moko components with submenus ── ?>
|
||||
<?php foreach ($mokoComponents as $comp): ?>
|
||||
<?php
|
||||
$compParsed = [];
|
||||
parse_str(parse_url($comp['link'], PHP_URL_QUERY) ?? '', $compParsed);
|
||||
$compActive = ($compParsed['option'] ?? '') === $currentOption;
|
||||
$hasChildren = !empty($comp['children']);
|
||||
$compLiClass = 'item item-level-2' . ($hasChildren ? ' parent' : '') . ($compActive ? ' mm-active' : '');
|
||||
$compAClass = ($hasChildren ? 'has-arrow' : 'no-dropdown') . ($compActive ? ' mm-active' : '');
|
||||
$childCollapse = 'collapse-level-2 mm-collapse' . ($compActive ? ' mm-show' : '');
|
||||
?>
|
||||
<li class="<?php echo $compLiClass; ?>">
|
||||
<a class="<?php echo $compAClass; ?>" href="<?php echo $hasChildren ? '#' : Route::_($comp['link']); ?>"<?php echo ($compActive && !$hasChildren) ? ' aria-current="page"' : ''; ?>>
|
||||
<span class="<?php echo $comp['icon']; ?>" aria-hidden="true" style="display:inline-block!important;width:1.25em;text-align:center;margin-inline-end:0.4em;"></span>
|
||||
<span class="sidebar-item-title"><?php echo $comp['title']; ?></span>
|
||||
</a>
|
||||
<?php if ($hasChildren): ?>
|
||||
<ul class="<?php echo $childCollapse; ?>">
|
||||
<?php foreach ($comp['children'] as $child): ?>
|
||||
<?php
|
||||
$childParsed = [];
|
||||
parse_str(parse_url($child['link'], PHP_URL_QUERY) ?? '', $childParsed);
|
||||
$childActive = ($childParsed['option'] ?? '') === $currentOption
|
||||
&& ($childParsed['view'] ?? '') === $currentView;
|
||||
$childLiClass = 'item item-level-3' . ($childActive ? ' mm-active' : '');
|
||||
$childAClass = 'no-dropdown' . ($childActive ? ' mm-active' : '');
|
||||
?>
|
||||
<li class="<?php echo $childLiClass; ?>">
|
||||
<a class="<?php echo $childAClass; ?>" href="<?php echo Route::_($child['link']); ?>"<?php echo $childActive ? ' aria-current="page"' : ''; ?>>
|
||||
<span class="<?php echo $child['icon']; ?>" aria-hidden="true" style="display:inline-block!important;width:1.25em;text-align:center;margin-inline-end:0.4em;"></span>
|
||||
<span class="sidebar-item-title"><?php echo $child['title']; ?></span>
|
||||
</a>
|
||||
</li>
|
||||
<?php endforeach; ?>
|
||||
</ul>
|
||||
<?php endif; ?>
|
||||
</li>
|
||||
<?php endforeach; ?>
|
||||
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
Reference in New Issue
Block a user