feat: single MokoSuite menu item with collapsible ecosystem children
Universal: Auto Version Bump / Version Bump (push) Successful in 12s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 20s

Consolidates all Moko components under one top-level "MokoSuite"
sidebar entry. Each component with subviews is a nested collapsible.
Also: Help link keeps target=_blank but hides external-link icon.

Claude-Session: https://claude.ai/code/session_01Jo2JpjCwfHAh2HHRSjczKq
This commit is contained in:
2026-06-27 19:12:42 -05:00
parent 6332405853
commit d65d8faf65
2 changed files with 77 additions and 81 deletions
@@ -2,9 +2,8 @@
/**
* MokoSuiteClient Admin Sidebar Menu
*
* Each installed Moko component gets its own top-level collapsible section.
* com_mokosuitehq is always pinned first. com_mokosuiteclient uses static views
* as children. All other components auto-discover their submenu items.
* Single "MokoSuite" top-level item with all Moko ecosystem components
* as collapsible children underneath.
*/
defined('_JEXEC') or die;
@@ -122,7 +121,6 @@ try
);
$menuItems = $db->loadObjectList() ?: [];
// Load language files for discovered components
$lang = Factory::getLanguage();
$loadedLangs = [];
foreach ($menuItems as $m)
@@ -131,20 +129,16 @@ try
{
$lang->load($m->element . '.sys', JPATH_ADMINISTRATOR);
$lang->load($m->element, JPATH_ADMINISTRATOR);
// Also try component-local language path (Joomla 5/6 pattern)
$compLangPath = JPATH_ADMINISTRATOR . '/components/' . $m->element;
if (is_dir($compLangPath . '/language'))
{
$lang->load($m->element . '.sys', $compLangPath);
$lang->load($m->element, $compLangPath);
}
$loadedLangs[$m->element] = true;
}
}
// Group: level 1 = component parent, level 2 = children
foreach ($menuItems as $m)
{
if ((int) $m->level === 1)
@@ -178,10 +172,7 @@ try
}
}
}
catch (\Throwable $e)
{
// Silent — menu works without auto-discovered components
}
catch (\Throwable $e) {}
// Override com_mokosuiteclient children with static views
if (isset($mokoComponents['com_mokosuiteclient']))
@@ -191,7 +182,6 @@ if (isset($mokoComponents['com_mokosuiteclient']))
}
else
{
// com_mokosuiteclient not in admin menu — add it manually
$mokoComponents['com_mokosuiteclient'] = [
'id' => 0,
'title' => 'MokoSuite',
@@ -209,9 +199,6 @@ $rest = [];
foreach ($mokoComponents as $key => $comp)
{
// Shorten display titles:
// MokoSuiteClient → MokoSuite, MokoSuiteHQ → MokoHQ
// Everything else: MokoSuiteBackup → Backup, MokoSuiteOpenGraph → OpenGraph
if ($key === 'com_mokosuiteclient')
{
$comp['title'] = 'MokoSuite';
@@ -225,35 +212,29 @@ foreach ($mokoComponents as $key => $comp)
$comp['title'] = preg_replace('/^MokoSuite\s*/i', '', $comp['title']);
}
if ($key === 'com_mokosuitehq')
{
$hq = $comp;
}
elseif ($key === 'com_mokosuiteclient')
{
$client = $comp;
}
else
{
$rest[$key] = $comp;
}
if ($key === 'com_mokosuitehq') { $hq = $comp; }
elseif ($key === 'com_mokosuiteclient') { $client = $comp; }
else { $rest[$key] = $comp; }
}
usort($rest, fn($a, $b) => strcasecmp($a['title'], $b['title']));
$sorted = [];
if ($hq !== null)
if ($hq !== null) $sorted[] = $hq;
if ($client !== null) $sorted[] = $client;
foreach ($rest as $comp) $sorted[] = $comp;
// Is ANY Moko component active?
$anyActive = false;
foreach ($sorted as $comp)
{
$sorted[] = $hq;
}
if ($client !== null)
{
$sorted[] = $client;
}
foreach ($rest as $comp)
{
$sorted[] = $comp;
$p = [];
parse_str(parse_url($comp['link'], PHP_URL_QUERY) ?? '', $p);
if (($p['option'] ?? '') === $currentOption) { $anyActive = true; break; }
}
if ($currentOption === 'com_plugins') $anyActive = true;
$iconStyle = 'display:inline-block!important;width:1.25em;text-align:center;margin-inline-end:0.4em;';
?>
<style>
@@ -261,58 +242,64 @@ foreach ($rest as $comp)
</style>
<ul class="nav flex-column main-nav">
<?php foreach ($sorted as $comp): ?>
<?php
$compParsed = [];
parse_str(parse_url($comp['link'], PHP_URL_QUERY) ?? '', $compParsed);
$compOption = $compParsed['option'] ?? '';
$compActive = ($compOption === $currentOption);
// For com_mokosuiteclient static children, also check the plugins filter link
if (!$compActive && $comp['element'] === 'com_mokosuiteclient' && $currentOption === 'com_plugins')
{
$compActive = true;
}
$hasChildren = !empty($comp['children']);
$liClass = 'item mokosuiteclient-ext-item' . ($hasChildren ? ' parent item-level-1' : '') . ($compActive ? ' mm-active' : '');
$aClass = ($hasChildren ? 'has-arrow' : 'no-dropdown') . ($compActive ? ' mm-active' : '');
$childCollapse = 'collapse-level-1 mm-collapse' . ($compActive ? ' mm-show' : '');
?>
<li class="<?php echo $liClass; ?>">
<a class="<?php echo $aClass; ?>" 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>
<li class="item parent item-level-1 mokosuiteclient-ext-item<?php echo $anyActive ? ' mm-active' : ''; ?>">
<a class="has-arrow<?php echo $anyActive ? ' mm-active' : ''; ?>" href="#">
<span class="icon-shield-alt" aria-hidden="true" style="<?php echo $iconStyle; ?>"></span>
<span class="sidebar-item-title">MokoSuite</span>
</a>
<?php if ($hasChildren): ?>
<ul class="<?php echo $childCollapse; ?>" style="padding-inline-start:0.5rem;">
<?php foreach ($comp['children'] as $child): ?>
<ul class="collapse-level-1 mm-collapse<?php echo $anyActive ? ' mm-show' : ''; ?>" style="padding-inline-start:0.5rem;">
<?php foreach ($sorted as $comp): ?>
<?php
$childParsed = [];
parse_str(parse_url($child['link'], PHP_URL_QUERY) ?? '', $childParsed);
$childOption = $childParsed['option'] ?? '';
$childView = $childParsed['view'] ?? '';
$childActive = false;
if ($childOption === $currentOption)
$compParsed = [];
parse_str(parse_url($comp['link'], PHP_URL_QUERY) ?? '', $compParsed);
$compOption = $compParsed['option'] ?? '';
$compActive = ($compOption === $currentOption);
if (!$compActive && $comp['element'] === 'com_mokosuiteclient' && $currentOption === 'com_plugins')
{
$childActive = empty($childView)
? ($currentView === '' || $currentView === 'dashboard')
: ($currentView === $childView);
$compActive = true;
}
$childLiClass = 'item mokosuiteclient-ext-child' . ($childActive ? ' mm-active' : '');
$childAClass = 'no-dropdown' . ($childActive ? ' mm-active' : '');
$hasChildren = !empty($comp['children']);
?>
<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>
<?php if ($hasChildren): ?>
<li class="item parent item-level-2 mokosuiteclient-ext-item<?php echo $compActive ? ' mm-active' : ''; ?>">
<a class="has-arrow<?php echo $compActive ? ' mm-active' : ''; ?>" href="#">
<span class="<?php echo $comp['icon']; ?>" aria-hidden="true" style="<?php echo $iconStyle; ?>"></span>
<span class="sidebar-item-title"><?php echo $comp['title']; ?></span>
</a>
<ul class="collapse-level-2 mm-collapse<?php echo $compActive ? ' mm-show' : ''; ?>" style="padding-inline-start:0.5rem;">
<?php foreach ($comp['children'] as $child): ?>
<?php
$childParsed = [];
parse_str(parse_url($child['link'], PHP_URL_QUERY) ?? '', $childParsed);
$childOption = $childParsed['option'] ?? '';
$childView = $childParsed['view'] ?? '';
$childActive = false;
if ($childOption === $currentOption)
{
$childActive = empty($childView)
? ($currentView === '' || $currentView === 'dashboard')
: ($currentView === $childView);
}
?>
<li class="item mokosuiteclient-ext-child<?php echo $childActive ? ' mm-active' : ''; ?>">
<a class="no-dropdown<?php echo $childActive ? ' mm-active' : ''; ?>" href="<?php echo Route::_($child['link']); ?>"<?php echo $childActive ? ' aria-current="page"' : ''; ?>>
<span class="<?php echo $child['icon']; ?>" aria-hidden="true" style="<?php echo $iconStyle; ?>"></span>
<span class="sidebar-item-title"><?php echo $child['title']; ?></span>
</a>
</li>
<?php endforeach; ?>
</ul>
</li>
<?php else: ?>
<li class="item mokosuiteclient-ext-item<?php echo $compActive ? ' mm-active' : ''; ?>">
<a class="no-dropdown<?php echo $compActive ? ' mm-active' : ''; ?>" href="<?php echo Route::_($comp['link']); ?>"<?php echo $compActive ? ' aria-current="page"' : ''; ?>>
<span class="<?php echo $comp['icon']; ?>" aria-hidden="true" style="<?php echo $iconStyle; ?>"></span>
<span class="sidebar-item-title"><?php echo $comp['title']; ?></span>
</a>
</li>
<?php endif; ?>
<?php endforeach; ?>
</ul>
<?php endif; ?>
</li>
<?php endforeach; ?>
</ul>
@@ -383,12 +383,21 @@ class MokoSuiteClient extends CMSPlugin implements BootableExtensionInterface
var url = " . json_encode($supportUrl) . ";
document.querySelectorAll('a[href*=\"help.joomla.org\"], a[href*=\"docs.joomla.org\"]').forEach(function(link) {
link.href = url;
link.target = '_blank';
link.rel = 'noopener noreferrer';
var extIcon = link.querySelector('.icon-external-link-alt, .icon-external-link, .fa-external-link-alt, .fa-up-right-from-square');
if (extIcon) extIcon.remove();
});
document.querySelectorAll('a[href*=\"dashboard=help\"]').forEach(function(link) {
link.href = url;
link.target = '_blank';
link.rel = 'noopener noreferrer';
var extIcon = link.querySelector('.icon-external-link-alt, .icon-external-link, .fa-external-link-alt, .fa-up-right-from-square');
if (extIcon) extIcon.remove();
});
});
");
$doc->addStyleDeclaration('a[href=\"' . $supportUrl . '\"] .icon-external-link-alt, a[href=\"' . $supportUrl . '\"] .fa-up-right-from-square { display: none !important; }');
}
/**