From 009cf08fde709e2ee28d19420d3fbcd348cff261 Mon Sep 17 00:00:00 2001 From: Moko Consulting Date: Mon, 20 Jul 2026 08:59:52 -0500 Subject: [PATCH 1/3] fix(installer): add MokoMenuRegistry for shared admin menu --- .../admin/src/Installer/MokoMenuRegistry.php | 179 ++++++++++++++++++ 1 file changed, 179 insertions(+) create mode 100644 source/packages/com_mokosuitesight/admin/src/Installer/MokoMenuRegistry.php diff --git a/source/packages/com_mokosuitesight/admin/src/Installer/MokoMenuRegistry.php b/source/packages/com_mokosuitesight/admin/src/Installer/MokoMenuRegistry.php new file mode 100644 index 0000000..f242b2f --- /dev/null +++ b/source/packages/com_mokosuitesight/admin/src/Installer/MokoMenuRegistry.php @@ -0,0 +1,179 @@ + + * @license GPL-3.0-or-later + */ + +declare(strict_types=1); + +namespace Moko\Component\MokoSuiteSight\Administrator\Installer; + +use Joomla\CMS\Factory; +use Joomla\CMS\Log\Log; +use Joomla\Database\DatabaseInterface; + +\defined('_JEXEC') or die; + +/** + * Self-contained MokoSuite admin-menu registry helper. + * + * Each MokoSuite extension calls register()/unregister() from its install script + * to publish its admin menu items into `#__mokosuite_menu`, which the + * mod_mokosuiteclient_menu module reads and renders. The helper is deliberately + * dependency-free and self-healing (it creates the table if missing) so an + * extension can install BEFORE the Client hub that owns the table. + * + * This file is the canonical contract: copy it verbatim into each MokoSuite + * extension, changing only the namespace to match that extension. Do NOT add + * cross-extension dependencies here. + * + * @since 02.59.07 + */ +final class MokoMenuRegistry +{ + /** + * Idempotently (re)register one extension's admin menu items. + * + * Each $items entry is an associative array: + * - title (string, required) language key or literal title + * - link (string) index.php?option=... route ('' for a bare group) + * - icon (string) a VERIFIED icon-* glyph (Atum renders many blank) + * - parent (string) owner element of the group this nests under; + * '' means this row IS a top-level group + * - element (string) component this item routes into (for active-state) + * - access (int) Joomla view access level (default 1) + * - ordering (int) sort within its level (default 0) + * - menu_group (string) reserved sidebar section (default 'suite') + * - params (string|null) reserved JSON + * + * @param string $owner Registering extension element, e.g. 'com_mokosuitehq'. + * @param array $items The menu rows to publish for this owner. + * + * @return void + */ + public static function register(string $owner, array $items): void + { + try { + $db = Factory::getContainer()->get(DatabaseInterface::class); + + if (!self::ensureTable($db)) { + return; + } + + // Delete-then-insert on owner = fully idempotent across reinstall/upgrade. + $db->setQuery( + $db->getQuery(true) + ->delete($db->quoteName('#__mokosuite_menu')) + ->where($db->quoteName('owner') . ' = ' . $db->quote($owner)) + )->execute(); + + foreach ($items as $item) { + $row = (object) [ + 'owner' => $owner, + 'element' => (string) ($item['element'] ?? ''), + 'parent' => (string) ($item['parent'] ?? ''), + 'title' => (string) ($item['title'] ?? ''), + 'link' => (string) ($item['link'] ?? ''), + 'icon' => (string) ($item['icon'] ?? 'icon-puzzle-piece'), + 'menu_group' => (string) ($item['menu_group'] ?? 'suite'), + 'access' => (int) ($item['access'] ?? 1), + 'ordering' => (int) ($item['ordering'] ?? 0), + 'published' => (int) ($item['published'] ?? 1), + 'params' => $item['params'] ?? null, + ]; + + $db->insertObject('#__mokosuite_menu', $row); + } + } catch (\Throwable $e) { + // Best-effort: a registry failure must never block the install. + Log::add( + 'MokoMenuRegistry::register(' . $owner . ') failed: ' . $e->getMessage(), + Log::ERROR, + 'com_mokosuitesight' + ); + } + } + + /** + * Remove all admin menu rows owned by an extension. Call on uninstall. + * + * @param string $owner The registering extension element. + * + * @return void + */ + public static function unregister(string $owner): void + { + try { + $db = Factory::getContainer()->get(DatabaseInterface::class); + + if (!self::tableExists($db)) { + return; + } + + $db->setQuery( + $db->getQuery(true) + ->delete($db->quoteName('#__mokosuite_menu')) + ->where($db->quoteName('owner') . ' = ' . $db->quote($owner)) + )->execute(); + } catch (\Throwable $e) { + Log::add( + 'MokoMenuRegistry::unregister(' . $owner . ') failed: ' . $e->getMessage(), + Log::ERROR, + 'com_mokosuitesight' + ); + } + } + + /** + * Create the registry table if it does not exist yet. This lets a sibling + * extension install before the Client hub that ships the table in its schema. + * + * @param DatabaseInterface $db The database driver. + * + * @return boolean True if the table exists (or was created). + */ + private static function ensureTable(DatabaseInterface $db): bool + { + if (self::tableExists($db)) { + return true; + } + + $db->setQuery( + 'CREATE TABLE IF NOT EXISTS ' . $db->quoteName('#__mokosuite_menu') . ' (' + . '`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,' + . "`owner` VARCHAR(75) NOT NULL DEFAULT ''," + . "`element` VARCHAR(75) NOT NULL DEFAULT ''," + . "`parent` VARCHAR(75) NOT NULL DEFAULT ''," + . "`title` VARCHAR(255) NOT NULL DEFAULT ''," + . "`link` VARCHAR(1024) NOT NULL DEFAULT ''," + . "`icon` VARCHAR(75) NOT NULL DEFAULT 'icon-puzzle-piece'," + . "`menu_group` VARCHAR(50) NOT NULL DEFAULT 'suite'," + . '`access` INT UNSIGNED NOT NULL DEFAULT 1,' + . '`ordering` INT NOT NULL DEFAULT 0,' + . '`published` TINYINT(1) NOT NULL DEFAULT 1,' + . '`params` TEXT NULL,' + . 'PRIMARY KEY (`id`),' + . 'UNIQUE KEY `idx_owner_link` (`owner`, `link`(191)),' + . 'KEY `idx_owner` (`owner`),' + . 'KEY `idx_parent` (`parent`),' + . 'KEY `idx_published_ordering` (`published`, `ordering`)' + . ') ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci' + )->execute(); + + return self::tableExists($db); + } + + /** + * Whether the #__mokosuite_menu table currently exists. + * + * @param DatabaseInterface $db The database driver. + * + * @return boolean + */ + private static function tableExists(DatabaseInterface $db): bool + { + return \in_array($db->replacePrefix('#__mokosuite_menu'), $db->getTableList(), true); + } +} -- 2.52.0 From 4932ea96b3fb4d3e6127f95dfa788a6a7e815179 Mon Sep 17 00:00:00 2001 From: "mokogit-actions[bot]" Date: Mon, 20 Jul 2026 14:00:20 +0000 Subject: [PATCH 2/3] chore(version): pre-release bump to 01.00.39-dev [skip ci] --- .mokogit/workflows/issue-branch.yml | 2 +- CODE_OF_CONDUCT.md | 2 +- GOVERNANCE.md | 2 +- SECURITY.md | 2 +- source/packages/com_mokosuitesight/mokosuitesight.xml | 2 +- source/packages/plg_system_mokosuitesight/mokosuitesight.xml | 2 +- .../packages/plg_webservices_mokosuitesight/mokosuitesight.xml | 2 +- source/pkg_mokosuitesight.xml | 2 +- 8 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.mokogit/workflows/issue-branch.yml b/.mokogit/workflows/issue-branch.yml index f5e19e5..1a093d9 100644 --- a/.mokogit/workflows/issue-branch.yml +++ b/.mokogit/workflows/issue-branch.yml @@ -5,7 +5,7 @@ # FILE INFORMATION # DEFGROUP: MokoGIT.Workflow # INGROUP: MokoCLI.Automation -# VERSION: 01.00.38 +# VERSION: 01.00.39 # BRIEF: Auto-create feature branch when an issue is opened name: "Universal: Issue Branch" diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index 505af45..5955902 100644 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -14,7 +14,7 @@ DEFGROUP: Template-Joomla INGROUP: Template-Joomla.Documentation REPO: https://github.com/mokoconsulting-tech/Template-Joomla/ - VERSION: 01.00.38 + VERSION: 01.00.39 PATH: ./CODE_OF_CONDUCT.md BRIEF: Community expectations and enforcement guidelines NOTE: Adapted with attribution from the Contributor Covenant v2.1 diff --git a/GOVERNANCE.md b/GOVERNANCE.md index 69f9ad4..428db12 100644 --- a/GOVERNANCE.md +++ b/GOVERNANCE.md @@ -19,7 +19,7 @@ DEFGROUP: mokoconsulting-tech.Template-Joomla INGROUP: MokoStandards.Governance REPO: https://github.com/mokoconsulting-tech/Template-Joomla - VERSION: 01.00.38 + VERSION: 01.00.39 PATH: /GOVERNANCE.md BRIEF: Project governance rules, roles, and decision process for Template-Joomla --> diff --git a/SECURITY.md b/SECURITY.md index bd2fdcc..334fc20 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -23,7 +23,7 @@ DEFGROUP: Template-Joomla INGROUP: Template-Joomla.Documentation REPO: https://git.mokoconsulting.tech/MokoConsulting/Template-Joomla PATH: /SECURITY.md -VERSION: 01.00.38 +VERSION: 01.00.39 BRIEF: Security vulnerability reporting and handling policy --> diff --git a/source/packages/com_mokosuitesight/mokosuitesight.xml b/source/packages/com_mokosuitesight/mokosuitesight.xml index cc96832..511e843 100644 --- a/source/packages/com_mokosuitesight/mokosuitesight.xml +++ b/source/packages/com_mokosuitesight/mokosuitesight.xml @@ -5,7 +5,7 @@ 2026-06-23 Copyright (C) 2026 Moko Consulting. GPL-3.0-or-later - 01.00.38 + 01.00.39 Moko\Component\MokoSuiteSight servicessrctmpl diff --git a/source/packages/plg_system_mokosuitesight/mokosuitesight.xml b/source/packages/plg_system_mokosuitesight/mokosuitesight.xml index a3b85cd..fac8bb1 100644 --- a/source/packages/plg_system_mokosuitesight/mokosuitesight.xml +++ b/source/packages/plg_system_mokosuitesight/mokosuitesight.xml @@ -8,7 +8,7 @@ GPL-3.0-or-later hello@mokoconsulting.tech https://mokoconsulting.tech - 01.00.38 + 01.00.39 8.3 PLG_SYSTEM_MOKOSUITESIGHT_DESC Moko\Plugin\System\MokoSuiteSight diff --git a/source/packages/plg_webservices_mokosuitesight/mokosuitesight.xml b/source/packages/plg_webservices_mokosuitesight/mokosuitesight.xml index da583d4..6437646 100644 --- a/source/packages/plg_webservices_mokosuitesight/mokosuitesight.xml +++ b/source/packages/plg_webservices_mokosuitesight/mokosuitesight.xml @@ -3,7 +3,7 @@ Web Services - MokoSuite Insight mokosuitesight Moko Consulting - 01.00.38 + 01.00.39 GPL-3.0-or-later Moko\Plugin\WebServices\MokoSuiteInsight srcservices diff --git a/source/pkg_mokosuitesight.xml b/source/pkg_mokosuitesight.xml index 8939946..8ec4f96 100644 --- a/source/pkg_mokosuitesight.xml +++ b/source/pkg_mokosuitesight.xml @@ -2,7 +2,7 @@ Package - MokoSuite Insight mokosuitesight - 01.00.38 + 01.00.39 2026-06-21 Moko Consulting hello@mokoconsulting.tech -- 2.52.0 From 20f93793c7658d6f8369819257ba8e99b9e5d36f Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Tue, 21 Jul 2026 16:30:38 -0500 Subject: [PATCH 3/3] feat: wire MokoMenuRegistry register() in component script.php Add component-level installer script that calls MokoMenuRegistry::register() on install/update and MokoMenuRegistry::unregister() on uninstall. Menu items use language keys so mod_mokosuiteclient_menu renders translated titles. Also adds declaration to the component manifest. Claude-Session: https://claude.ai/code/session_01CwLGvFJPjoPTp9BEnSjtJf --- .../com_mokosuitesight/mokosuitesight.xml | 1 + source/packages/com_mokosuitesight/script.php | 74 +++++++++++++++++++ 2 files changed, 75 insertions(+) create mode 100644 source/packages/com_mokosuitesight/script.php diff --git a/source/packages/com_mokosuitesight/mokosuitesight.xml b/source/packages/com_mokosuitesight/mokosuitesight.xml index 511e843..eecc2e4 100644 --- a/source/packages/com_mokosuitesight/mokosuitesight.xml +++ b/source/packages/com_mokosuitesight/mokosuitesight.xml @@ -7,6 +7,7 @@ GPL-3.0-or-later 01.00.39 Moko\Component\MokoSuiteSight + script.php servicessrctmpl COM_MOKOSUITESIGHT diff --git a/source/packages/com_mokosuitesight/script.php b/source/packages/com_mokosuitesight/script.php new file mode 100644 index 0000000..9d4f964 --- /dev/null +++ b/source/packages/com_mokosuitesight/script.php @@ -0,0 +1,74 @@ +registerAdminMenu(); + } + } + + public function uninstall(InstallerAdapter $adapter): void + { + $this->unregisterAdminMenu(); + } + + private const MENU_OWNER = 'com_mokosuitesight'; + + private function registerAdminMenu(): void + { + $registry = $this->loadMenuRegistry(); + + if ($registry !== null) + { + $registry::register(self::MENU_OWNER, $this->getMenuItems()); + } + } + + private function unregisterAdminMenu(): void + { + $registry = $this->loadMenuRegistry(); + + if ($registry !== null) + { + $registry::unregister(self::MENU_OWNER); + } + } + + private function loadMenuRegistry(): ?string + { + $class = '\\Moko\\Component\\MokoSuiteSight\\Administrator\\Installer\\MokoMenuRegistry'; + + if (!class_exists($class)) + { + $path = JPATH_ADMINISTRATOR . '/components/com_mokosuitesight/src/Installer/MokoMenuRegistry.php'; + + if (is_file($path)) + { + require_once $path; + } + } + + return class_exists($class) ? $class : null; + } + + private function getMenuItems(): array + { + $owner = self::MENU_OWNER; + + return [ + ['element' => $owner, 'parent' => '', 'title' => 'COM_MOKOSUITESIGHT_SHORT', 'link' => 'index.php?option=com_mokosuitesight', 'icon' => 'icon-eye', 'ordering' => 91], + ]; + } +} -- 2.52.0