Compare commits

...

22 Commits

Author SHA1 Message Date
gitea-actions[bot] e4537809a6 chore(version): pre-release bump to 02.52.11-dev [skip ci] 2026-06-29 16:15:14 +00:00
gitea-actions[bot] f555280b81 chore(version): auto-bump patch 02.52.10-dev [skip ci] 2026-06-29 16:15:00 +00:00
jmiller c40ca2e3dc fix: address PR review findings - CSRF response, N+1 query, error handling
Generic: Project CI / Tests (pull_request) Blocked by required conditions
Joomla: Extension CI / Tests (PHP 8.2) (pull_request) Blocked by required conditions
Joomla: Extension CI / Tests (PHP 8.3) (pull_request) Blocked by required conditions
Joomla: Extension CI / PHPStan Analysis (pull_request) Blocked by required conditions
Joomla: Extension CI / Build RC Pre-Release (pull_request) Blocked by required conditions
Universal: PR Check / Build RC Package (pull_request) Blocked by required conditions
Universal: PR Check / Report Issues (pull_request) Blocked by required conditions
Generic: Repo Health / Scripts governance (pull_request) Blocked by required conditions
Generic: Repo Health / Repository health (pull_request) Blocked by required conditions
Generic: Repo Health / Report Issues (pull_request) Blocked by required conditions
Universal: PR Check / Branch Policy (pull_request) Successful in 1s
Joomla: Extension CI / Release Readiness Check (pull_request) Successful in 6s
Universal: PR Check / Validate PR (pull_request) Failing after 7s
Generic: Repo Health / Site Health (pull_request) Has been skipped
Generic: Repo Health / Access control (pull_request) Successful in 1s
Universal: Secret Scanning / Gitleaks Secret Scan (pull_request) Successful in 19s
Universal: Auto Version Bump / Version Bump (push) Successful in 9s
Generic: Project CI / Lint & Validate (pull_request) Successful in 45s
Joomla: Extension CI / Lint & Validate (pull_request) Failing after 46s
Joomla: Metadata Validation / Validate Joomla Metadata (pull_request) Successful in 47s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 21s
- togglePublished: return JSON on CSRF failure instead of die()
- Conditions view: fold group/rule counts into main query as subselects
  instead of N+1 per-item queries
- All 5 toggle-published templates: add .catch() for AJAX error feedback

Claude-Session: https://claude.ai/code/session_01Jo2JpjCwfHAh2HHRSjczKq
2026-06-29 11:14:21 -05:00
gitea-actions[bot] 09a307dc91 chore(version): pre-release bump to 02.52.09-dev [skip ci] 2026-06-29 16:11:29 +00:00
gitea-actions[bot] 2e04bf8e31 chore(version): auto-bump patch 02.52.08-dev [skip ci] 2026-06-29 16:11:14 +00:00
jmiller 7e75a00888 fix: update server URL and add updates.xml
Generic: Project CI / Tests (pull_request) Blocked by required conditions
Joomla: Extension CI / Tests (PHP 8.2) (pull_request) Blocked by required conditions
Joomla: Extension CI / Tests (PHP 8.3) (pull_request) Blocked by required conditions
Joomla: Extension CI / PHPStan Analysis (pull_request) Blocked by required conditions
Joomla: Extension CI / Build RC Pre-Release (pull_request) Blocked by required conditions
Universal: PR Check / Build RC Package (pull_request) Blocked by required conditions
Universal: PR Check / Report Issues (pull_request) Blocked by required conditions
Generic: Repo Health / Scripts governance (pull_request) Blocked by required conditions
Generic: Repo Health / Repository health (pull_request) Blocked by required conditions
Generic: Repo Health / Report Issues (pull_request) Blocked by required conditions
Universal: PR Check / Branch Policy (pull_request) Successful in 1s
Universal: PR Check / Validate PR (pull_request) Failing after 7s
Joomla: Extension CI / Release Readiness Check (pull_request) Successful in 9s
Generic: Repo Health / Site Health (pull_request) Has been skipped
Generic: Repo Health / Access control (pull_request) Successful in 1s
Universal: Auto Version Bump / Version Bump (push) Successful in 14s
Joomla: Metadata Validation / Validate Joomla Metadata (pull_request) Successful in 14s
Universal: Secret Scanning / Gitleaks Secret Scan (pull_request) Successful in 19s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 24s
Generic: Project CI / Lint & Validate (pull_request) Successful in 47s
Joomla: Extension CI / Lint & Validate (pull_request) Failing after 47s
Change package manifest update server from broken generic package URL
to repo raw file path. Add updates.xml to repo root so Joomla update
checker can find the latest stable release.

Claude-Session: https://claude.ai/code/session_01Jo2JpjCwfHAh2HHRSjczKq
2026-06-29 11:11:00 -05:00
gitea-actions[bot] f71083ecaf chore(version): pre-release bump to 02.52.07-dev [skip ci] 2026-06-29 16:03:29 +00:00
gitea-actions[bot] 2a74259bcd chore(version): auto-bump patch 02.52.06-dev [skip ci] 2026-06-29 16:03:17 +00:00
jmiller e5acc19e0c feat: add missing submenu entries and fix menu module icon overrides
Universal: Auto Version Bump / Version Bump (push) Successful in 11s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 19s
- Add 6 submenu items to manifest (Conditions, Snippets, Templates,
  Replacements, Automation, Modules) so views are navigable
- Fix icon overrides for actual element names (com_mokosuite_crm,
  com_mokosuite_erp, com_mokoog, com_mokoshop)
- Add icon mappings for 11 additional MokoSuite components
- Fix item-level CSS classes to match Joomla admin sidebar

Claude-Session: https://claude.ai/code/session_01Jo2JpjCwfHAh2HHRSjczKq
2026-06-29 11:03:04 -05:00
gitea-actions[bot] 5f6b4f9ebf chore(version): pre-release bump to 02.52.05-dev [skip ci] 2026-06-29 15:27:31 +00:00
gitea-actions[bot] 24606b0e44 chore(version): auto-bump patch 02.52.02-dev [skip ci] 2026-06-29 15:27:19 +00:00
jmiller 68a22b200a chore: migrate update server URLs to MokoGitea
Universal: Auto Version Bump / Version Bump (push) Successful in 10s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 18s
2026-06-29 15:22:47 +00:00
gitea-actions[bot] 93287cc2e7 chore(version): pre-release bump to 02.52.01-dev [skip ci]
RC Revert / Rename rc/ back to dev/ (pull_request) Has been skipped
Branch Cleanup / Delete merged branch (pull_request) Failing after 2s
2026-06-29 15:15:50 +00:00
jmiller a9fa2246f8 chore: migrate update server URLs to MokoGitea
Universal: PR Check / Branch Policy (pull_request) Successful in 3s
Universal: PR Check / Validate PR (pull_request) Failing after 8s
Generic: Project CI / Lint & Validate (pull_request) Successful in 16s
Joomla: Metadata Validation / Validate Joomla Metadata (pull_request) Successful in 56s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 28s
Generic: Project CI / Tests (pull_request) Has been cancelled
Universal: PR Check / Build RC Package (pull_request) Has been cancelled
Universal: PR Check / Report Issues (pull_request) Has been cancelled
Replace static updates.xml URL with MokoGitea dynamic update
server endpoint. Remove priority attribute and normalize server name.
2026-06-29 15:13:43 +00:00
gitea-actions[bot] 604550a500 chore(version): pre-release bump to 02.51.06-dev [skip ci] 2026-06-28 19:27:00 +00:00
gitea-actions[bot] fe1410568b chore(version): pre-release bump to 02.51.05-dev [skip ci] 2026-06-28 19:26:40 +00:00
gitea-actions[bot] 912f37f8b2 chore(version): auto-bump patch 02.51.04-dev [skip ci] 2026-06-28 19:26:28 +00:00
jmiller 252ccdfa10 fix: heartbeat button shows proper errors instead of failing silently
Universal: Auto Version Bump / Version Bump (push) Successful in 11s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 20s
- CSRF check returns JSON instead of die() with raw text
- JS parses non-JSON responses gracefully and shows server error
- Visual feedback (check/cross icon) on success/failure
- 3-second icon revert after result

Claude-Session: https://claude.ai/code/session_01Jo2JpjCwfHAh2HHRSjczKq
2026-06-28 14:23:23 -05:00
gitea-actions[bot] d898de5bdc chore(version): pre-release bump to 02.51.03-dev [skip ci] 2026-06-28 18:57:24 +00:00
gitea-actions[bot] dfb0e22912 chore(version): pre-release bump to 02.51.02-dev [skip ci] 2026-06-28 18:56:35 +00:00
gitea-actions[bot] 199d3da05e chore(version): auto-bump patch 02.48.53-dev [skip ci] 2026-06-28 18:56:25 +00:00
jmiller 9417bb60cb feat: add Conditions, Snippets, Replacements, Templates, Modules views
Universal: Auto Version Bump / Version Bump (push) Successful in 10s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 16s
Five new admin views with models, templates, and list UI:
- Conditions: condition sets with group/rule counts and inline publish
- Snippets: reusable text blocks with {snippet alias} syntax
- Replacements: search/replace rules with regex and area badges
- Templates: content templates with category and description
- Modules: advanced module manager with position and client badges
Also adds togglePublished endpoint to DisplayController.

Claude-Session: https://claude.ai/code/session_01Jo2JpjCwfHAh2HHRSjczKq
2026-06-28 13:55:58 -05:00
63 changed files with 1598 additions and 66 deletions
+1 -1
View File
@@ -5,7 +5,7 @@
# FILE INFORMATION
# DEFGROUP: Gitea.Workflow
# INGROUP: moko-platform.Automation
# VERSION: 02.48.52
# VERSION: 02.52.11
# BRIEF: Auto-create feature branch when an issue is opened
name: "Universal: Issue Branch"
+1 -1
View File
@@ -14,7 +14,7 @@
INGROUP: MokoSuiteClient.Documentation
REPO: https://github.com/mokoconsulting-tech/mokosuiteclient
PATH: ./CHANGELOG.md
VERSION: 02.48.52
VERSION: 02.52.11
BRIEF: Version history using `Keep a Changelog`
-->
+1 -1
View File
@@ -14,7 +14,7 @@
DEFGROUP: Joomla.Plugin
INGROUP: MokoSuiteClient.Documentation
REPO: https://github.com/mokoconsulting-tech/mokosuiteclient
VERSION: 02.48.52
VERSION: 02.52.11
PATH: ./CODE_OF_CONDUCT.md
BRIEF: Reference + packaging repo for Moko Consulting Developer GPT Other Default
-->
+1 -1
View File
@@ -19,7 +19,7 @@
DEFGROUP: mokoconsulting-tech.MokoSuiteClientBrand
INGROUP: MokoStandards.Governance
REPO: https://github.com/mokoconsulting-tech/MokoSuiteClientBrand
VERSION: 02.48.52
VERSION: 02.52.11
PATH: /GOVERNANCE.md
BRIEF: Project governance rules, roles, and decision process for MokoSuiteClientBrand
-->
+1 -1
View File
@@ -15,7 +15,7 @@
INGROUP: MokoSuiteClient.Documentation
REPO: https://github.com/mokoconsulting-tech/mokosuiteclient
PATH: ./LICENSE.md
VERSION: 02.48.52
VERSION: 02.52.11
BRIEF: Project license (GPL-3.0-or-later)
-->
GNU GENERAL PUBLIC LICENSE
+1 -1
View File
@@ -9,7 +9,7 @@
DEFGROUP: Joomla.Plugin
INGROUP: MokoSuiteClient
REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoSuiteClient
VERSION: 02.48.52
VERSION: 02.52.11
PATH: /README.md
BRIEF: MokoSuiteClient platform plugin for Joomla
-->
+1 -1
View File
@@ -23,7 +23,7 @@ DEFGROUP: [PROJECT_NAME]
INGROUP: [PROJECT_NAME].Documentation
REPO: [REPOSITORY_URL]
PATH: /SECURITY.md
VERSION: 02.48.52
VERSION: 02.52.11
BRIEF: Security vulnerability reporting and handling policy
-->
+2 -2
View File
@@ -11,13 +11,13 @@
INGROUP: MokoSuiteClient.Build
REPO: https://github.com/mokoconsulting-tech/mokosuiteclient
FILE: build-guide.md
VERSION: 02.48.52
VERSION: 02.52.11
PATH: /docs/guides/
BRIEF: Build and packaging guide for the MokoSuiteClient system plugin
NOTE: Defines environment setup, repository layout, packaging rules, and release preparation
-->
# MokoSuiteClient Build Guide (VERSION: 02.48.52)
# MokoSuiteClient Build Guide (VERSION: 02.52.11)
## 1. Purpose
+2 -2
View File
@@ -10,13 +10,13 @@
DEFGROUP: Joomla.Plugin
INGROUP: MokoSuiteClient.Guides
REPO: https://github.com/mokoconsulting-tech/mokosuiteclient
VERSION: 02.48.52
VERSION: 02.52.11
PATH: /docs/guides/configuration-guide.md
BRIEF: Configuration guide for the MokoSuiteClient system plugin
NOTE: Defines plugin parameters, expected behaviors, and recommended defaults
-->
# MokoSuiteClient Configuration Guide (VERSION: 02.48.52)
# MokoSuiteClient Configuration Guide (VERSION: 02.52.11)
## 1. Objective
+2 -2
View File
@@ -10,13 +10,13 @@
DEFGROUP: Joomla.Plugin
INGROUP: MokoSuiteClient.Guides
REPO: https://github.com/mokoconsulting-tech/mokosuiteclient
VERSION: 02.48.52
VERSION: 02.52.11
PATH: /docs/guides/installation-guide.md
BRIEF: Installation guide for the MokoSuiteClient system plugin
NOTE: First document in the guide set
-->
# MokoSuiteClient Installation Guide (VERSION: 02.48.52)
# MokoSuiteClient Installation Guide (VERSION: 02.52.11)
## Introduction
+2 -2
View File
@@ -10,13 +10,13 @@
DEFGROUP: Joomla.Plugin
INGROUP: MokoSuiteClient.Guides
REPO: https://github.com/mokoconsulting-tech/mokosuiteclient
VERSION: 02.48.52
VERSION: 02.52.11
PATH: /docs/guides/operations-guide.md
BRIEF: Operational guide for administering and managing the MokoSuiteClient system plugin
NOTE: Defines lifecycle, responsibilities, and operational behaviors
-->
# MokoSuiteClient Operations Guide (VERSION: 02.48.52)
# MokoSuiteClient Operations Guide (VERSION: 02.52.11)
## Introduction
+2 -2
View File
@@ -10,13 +10,13 @@
DEFGROUP: Joomla.Plugin
INGROUP: MokoSuiteClient.Guides
REPO: https://github.com/mokoconsulting-tech/mokosuiteclient
VERSION: 02.48.52
VERSION: 02.52.11
PATH: /docs/guides/rollback-and-recovery-guide.md
BRIEF: Rollback and recovery guide for restoring stable operation after plugin related incidents
NOTE: Completes the core guide set for Suite plugin governance
-->
# MokoSuiteClient Rollback and Recovery Guide (VERSION: 02.48.52)
# MokoSuiteClient Rollback and Recovery Guide (VERSION: 02.52.11)
## Introduction
+2 -2
View File
@@ -7,13 +7,13 @@
DEFGROUP: Joomla.Plugin
INGROUP: MokoSuiteClient.Guides
REPO: https://github.com/mokoconsulting-tech/mokosuiteclient
VERSION: 02.48.52
VERSION: 02.52.11
PATH: /docs/guides/testing-guide.md
BRIEF: Testing guide for MokoSuiteClient v02.01.08
NOTE: Covers manual test procedures for language overrides, install/uninstall, and configuration
-->
# MokoSuiteClient Testing Guide (VERSION: 02.48.52)
# MokoSuiteClient Testing Guide (VERSION: 02.52.11)
## 1. Prerequisites
+2 -2
View File
@@ -10,13 +10,13 @@
DEFGROUP: Joomla.Plugin
INGROUP: MokoSuiteClient.Guides
REPO: https://github.com/mokoconsulting-tech/mokosuiteclient
VERSION: 02.48.52
VERSION: 02.52.11
PATH: /docs/guides/troubleshooting-guide.md
BRIEF: Troubleshooting guide for diagnosing and resolving issues related to the MokoSuiteClient plugin
NOTE: Designed for administrators and Suite operations teams
-->
# MokoSuiteClient Troubleshooting Guide (VERSION: 02.48.52)
# MokoSuiteClient Troubleshooting Guide (VERSION: 02.52.11)
## Introduction
+2 -2
View File
@@ -10,13 +10,13 @@
DEFGROUP: Joomla.Plugin
INGROUP: MokoSuiteClient.Guides
REPO: https://github.com/mokoconsulting-tech/mokosuiteclient
VERSION: 02.48.52
VERSION: 02.52.11
PATH: /docs/guides/upgrade-and-versioning-guide.md
BRIEF: Guide for updating, versioning, and maintaining the MokoSuiteClient plugin
NOTE: Defines release flow, version rules, and upgrade validation
-->
# MokoSuiteClient Upgrade and Versioning Guide (VERSION: 02.48.52)
# MokoSuiteClient Upgrade and Versioning Guide (VERSION: 02.52.11)
## Introduction
+2 -2
View File
@@ -10,13 +10,13 @@
DEFGROUP: Joomla.Plugin
INGROUP: MokoSuiteClient.Documentation
REPO: https://github.com/mokoconsulting-tech/mokosuiteclient
VERSION: 02.48.52
VERSION: 02.52.11
PATH: /docs/index.md
BRIEF: Master index of all documentation for the MokoSuiteClient plugin
NOTE: Automatically maintained index for all guide canvases
-->
# MokoSuiteClient Documentation Index (VERSION: 02.48.52)
# MokoSuiteClient Documentation Index (VERSION: 02.52.11)
## Introduction
+2 -2
View File
@@ -11,12 +11,12 @@
INGROUP: MokoSuiteClient
REPO: https://github.com/mokoconsulting-tech/mokosuiteclient
PATH: /docs/plugin-basic.md
VERSION: 02.48.52
VERSION: 02.52.11
BRIEF: Baseline documentation for the MokoSuiteClient system plugin
NOTE: Foundational reference for internal and external stakeholders
-->
# MokoSuiteClient Plugin Overview (VERSION: 02.48.52)
# MokoSuiteClient Plugin Overview (VERSION: 02.52.11)
## Introduction
+1 -1
View File
@@ -10,7 +10,7 @@ DEFGROUP: MokoSuiteClient.Documentation
INGROUP: MokoStandards.Templates
REPO: https://github.com/mokoconsulting-tech/MokoSuiteClient
PATH: /docs/update-server.md
VERSION: 02.48.52
VERSION: 02.52.11
BRIEF: How this extension's Joomla update server file (update.xml) is managed
-->
@@ -17,3 +17,9 @@ COM_MOKOSUITECLIENT_MENU_WAFLOG="WAF Log"
COM_MOKOSUITECLIENT_MENU_DATABASE="Database Tools"
COM_MOKOSUITECLIENT_MENU_CLEANUP="Cache Cleanup"
COM_MOKOSUITECLIENT_MENU_CACHE="Cache Management"
COM_MOKOSUITECLIENT_MENU_CONDITIONS="Conditions"
COM_MOKOSUITECLIENT_MENU_SNIPPETS="Snippets"
COM_MOKOSUITECLIENT_MENU_TEMPLATES="Content Templates"
COM_MOKOSUITECLIENT_MENU_REPLACEMENTS="Replacements"
COM_MOKOSUITECLIENT_MENU_AUTOMATION="Automation"
COM_MOKOSUITECLIENT_MENU_MODULES="Modules"
@@ -36,6 +36,7 @@ class DisplayController extends BaseController
'templates' => 'mokosuiteclient.templates.manage',
'replacements' => 'mokosuiteclient.replacements.manage',
'conditions' => 'mokosuiteclient.conditions.manage',
'modules' => 'core.admin',
];
public function display($cachable = false, $urlparams = [])
@@ -85,7 +86,12 @@ class DisplayController extends BaseController
public function sendHeartbeat()
{
Session::checkToken() or die(Text::_('JINVALID_TOKEN'));
if (!Session::checkToken())
{
$this->jsonResponse(['success' => false, 'message' => 'Session expired — please reload the page.']);
return;
}
try
{
@@ -800,6 +806,65 @@ class DisplayController extends BaseController
$this->jsonResponse($this->getModel('Import')->importAdminTools());
}
// ==================================================================
// Toggle Published
// ==================================================================
public function togglePublished()
{
if (!Session::checkToken())
{
$this->jsonResponse(['success' => false, 'message' => Text::_('JINVALID_TOKEN')]);
return;
}
if (!$this->checkAcl('core.admin'))
{
$this->jsonForbidden();
return;
}
$app = Factory::getApplication();
$table = $app->getInput()->getString('table', '');
$id = $app->getInput()->getInt('id', 0);
$allowed = ['mokosuiteclient_conditions', 'mokosuiteclient_snippets',
'mokosuiteclient_replacements', 'mokosuiteclient_content_templates', 'modules'];
if (!in_array($table, $allowed, true) || $id <= 0)
{
$this->jsonResponse(['success' => false, 'message' => 'Invalid table or ID.']);
return;
}
try
{
$db = Factory::getContainer()->get(\Joomla\Database\DatabaseInterface::class);
$dbTable = '#__' . $table;
$current = (int) $db->setQuery(
$db->getQuery(true)
->select($db->quoteName('published'))
->from($db->quoteName($dbTable))
->where($db->quoteName('id') . ' = ' . $id)
)->loadResult();
$newState = $current ? 0 : 1;
$db->setQuery(
$db->getQuery(true)
->update($db->quoteName($dbTable))
->set($db->quoteName('published') . ' = ' . $newState)
->where($db->quoteName('id') . ' = ' . $id)
)->execute();
$this->jsonResponse(['success' => true, 'published' => $newState]);
}
catch (\Throwable $e)
{
$this->jsonResponse(['success' => false, 'message' => $e->getMessage()]);
}
}
// ==================================================================
// Helpers
// ==================================================================
@@ -0,0 +1,112 @@
<?php
/**
* @package MokoSuiteClient
* @subpackage com_mokosuiteclient
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GNU General Public License version 3 or later; see LICENSE
*
* SPDX-License-Identifier: GPL-3.0-or-later
*/
namespace Moko\Component\MokoSuiteClient\Administrator\Model;
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use Joomla\CMS\MVC\Model\BaseDatabaseModel;
use Joomla\Database\DatabaseInterface;
class ConditionsModel extends BaseDatabaseModel
{
public function getItems(array $filters = [], int $limit = 50, int $offset = 0): array
{
$db = Factory::getContainer()->get(DatabaseInterface::class);
$query = $db->getQuery(true)
->select([
$db->quoteName('c.id'),
$db->quoteName('c.alias'),
$db->quoteName('c.name'),
$db->quoteName('c.description'),
$db->quoteName('c.category'),
$db->quoteName('c.color'),
$db->quoteName('c.match_all'),
$db->quoteName('c.published'),
'(SELECT COUNT(*) FROM ' . $db->quoteName('#__mokosuiteclient_conditions_groups')
. ' WHERE ' . $db->quoteName('condition_id') . ' = ' . $db->quoteName('c.id') . ') AS group_count',
'(SELECT COUNT(*) FROM ' . $db->quoteName('#__mokosuiteclient_conditions_rules', 'r')
. ' INNER JOIN ' . $db->quoteName('#__mokosuiteclient_conditions_groups', 'g')
. ' ON ' . $db->quoteName('g.id') . ' = ' . $db->quoteName('r.group_id')
. ' WHERE ' . $db->quoteName('g.condition_id') . ' = ' . $db->quoteName('c.id') . ') AS rule_count',
])
->from($db->quoteName('#__mokosuiteclient_conditions', 'c'));
if (!empty($filters['search']))
{
$search = $db->quote('%' . $db->escape($filters['search'], true) . '%');
$query->where('(' . $db->quoteName('c.name') . ' LIKE ' . $search
. ' OR ' . $db->quoteName('c.alias') . ' LIKE ' . $search . ')');
}
if ($filters['published'] !== '' && $filters['published'] !== null)
{
$query->where($db->quoteName('c.published') . ' = ' . (int) $filters['published']);
}
$query->order($db->quoteName('c.name') . ' ASC');
$db->setQuery($query, $offset, $limit);
return $db->loadObjectList() ?: [];
}
public function getTotal(array $filters = []): int
{
$db = Factory::getContainer()->get(DatabaseInterface::class);
$query = $db->getQuery(true)
->select('COUNT(*)')
->from($db->quoteName('#__mokosuiteclient_conditions', 'c'));
if (!empty($filters['search']))
{
$search = $db->quote('%' . $db->escape($filters['search'], true) . '%');
$query->where('(' . $db->quoteName('c.name') . ' LIKE ' . $search
. ' OR ' . $db->quoteName('c.alias') . ' LIKE ' . $search . ')');
}
if ($filters['published'] !== '' && $filters['published'] !== null)
{
$query->where($db->quoteName('c.published') . ' = ' . (int) $filters['published']);
}
$db->setQuery($query);
return (int) $db->loadResult();
}
public function getGroupCount(int $conditionId): int
{
$db = Factory::getContainer()->get(DatabaseInterface::class);
$db->setQuery(
$db->getQuery(true)
->select('COUNT(*)')
->from($db->quoteName('#__mokosuiteclient_conditions_groups'))
->where($db->quoteName('condition_id') . ' = ' . $conditionId)
);
return (int) $db->loadResult();
}
public function getRuleCount(int $conditionId): int
{
$db = Factory::getContainer()->get(DatabaseInterface::class);
$db->setQuery(
$db->getQuery(true)
->select('COUNT(*)')
->from($db->quoteName('#__mokosuiteclient_conditions_rules', 'r'))
->join('INNER', $db->quoteName('#__mokosuiteclient_conditions_groups', 'g')
. ' ON ' . $db->quoteName('g.id') . ' = ' . $db->quoteName('r.group_id'))
->where($db->quoteName('g.condition_id') . ' = ' . $conditionId)
);
return (int) $db->loadResult();
}
}
@@ -0,0 +1,93 @@
<?php
/**
* @package MokoSuiteClient
* @subpackage com_mokosuiteclient
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GNU General Public License version 3 or later; see LICENSE
*
* SPDX-License-Identifier: GPL-3.0-or-later
*/
namespace Moko\Component\MokoSuiteClient\Administrator\Model;
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use Joomla\CMS\MVC\Model\BaseDatabaseModel;
use Joomla\Database\DatabaseInterface;
class ModulesModel extends BaseDatabaseModel
{
public function getItems(array $filters = [], int $limit = 50, int $offset = 0): array
{
$db = Factory::getContainer()->get(DatabaseInterface::class);
$query = $db->getQuery(true)
->select([
$db->quoteName('m.id'),
$db->quoteName('m.title'),
$db->quoteName('m.module'),
$db->quoteName('m.position'),
$db->quoteName('m.published'),
$db->quoteName('m.ordering'),
$db->quoteName('m.client_id'),
$db->quoteName('m.access'),
$db->quoteName('m.language'),
])
->from($db->quoteName('#__modules', 'm'));
if (!empty($filters['search']))
{
$search = $db->quote('%' . $db->escape($filters['search'], true) . '%');
$query->where('(' . $db->quoteName('m.title') . ' LIKE ' . $search
. ' OR ' . $db->quoteName('m.module') . ' LIKE ' . $search
. ' OR ' . $db->quoteName('m.position') . ' LIKE ' . $search . ')');
}
if ($filters['published'] !== '' && $filters['published'] !== null)
{
$query->where($db->quoteName('m.published') . ' = ' . (int) $filters['published']);
}
if ($filters['client_id'] !== '' && $filters['client_id'] !== null)
{
$query->where($db->quoteName('m.client_id') . ' = ' . (int) $filters['client_id']);
}
$query->order($db->quoteName('m.client_id') . ' ASC, '
. $db->quoteName('m.position') . ' ASC, '
. $db->quoteName('m.ordering') . ' ASC');
$db->setQuery($query, $offset, $limit);
return $db->loadObjectList() ?: [];
}
public function getTotal(array $filters = []): int
{
$db = Factory::getContainer()->get(DatabaseInterface::class);
$query = $db->getQuery(true)
->select('COUNT(*)')
->from($db->quoteName('#__modules', 'm'));
if (!empty($filters['search']))
{
$search = $db->quote('%' . $db->escape($filters['search'], true) . '%');
$query->where('(' . $db->quoteName('m.title') . ' LIKE ' . $search
. ' OR ' . $db->quoteName('m.module') . ' LIKE ' . $search
. ' OR ' . $db->quoteName('m.position') . ' LIKE ' . $search . ')');
}
if ($filters['published'] !== '' && $filters['published'] !== null)
{
$query->where($db->quoteName('m.published') . ' = ' . (int) $filters['published']);
}
if ($filters['client_id'] !== '' && $filters['client_id'] !== null)
{
$query->where($db->quoteName('m.client_id') . ' = ' . (int) $filters['client_id']);
}
$db->setQuery($query);
return (int) $db->loadResult();
}
}
@@ -0,0 +1,69 @@
<?php
/**
* @package MokoSuiteClient
* @subpackage com_mokosuiteclient
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GNU General Public License version 3 or later; see LICENSE
*
* SPDX-License-Identifier: GPL-3.0-or-later
*/
namespace Moko\Component\MokoSuiteClient\Administrator\Model;
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use Joomla\CMS\MVC\Model\BaseDatabaseModel;
use Joomla\Database\DatabaseInterface;
class ReplacementsModel extends BaseDatabaseModel
{
public function getItems(array $filters = [], int $limit = 50, int $offset = 0): array
{
$db = Factory::getContainer()->get(DatabaseInterface::class);
$query = $db->getQuery(true)
->select('*')
->from($db->quoteName('#__mokosuiteclient_replacements'));
if (!empty($filters['search']))
{
$search = $db->quote('%' . $db->escape($filters['search'], true) . '%');
$query->where('(' . $db->quoteName('name') . ' LIKE ' . $search
. ' OR ' . $db->quoteName('search') . ' LIKE ' . $search . ')');
}
if ($filters['published'] !== '' && $filters['published'] !== null)
{
$query->where($db->quoteName('published') . ' = ' . (int) $filters['published']);
}
$query->order($db->quoteName('ordering') . ' ASC, ' . $db->quoteName('name') . ' ASC');
$db->setQuery($query, $offset, $limit);
return $db->loadObjectList() ?: [];
}
public function getTotal(array $filters = []): int
{
$db = Factory::getContainer()->get(DatabaseInterface::class);
$query = $db->getQuery(true)
->select('COUNT(*)')
->from($db->quoteName('#__mokosuiteclient_replacements'));
if (!empty($filters['search']))
{
$search = $db->quote('%' . $db->escape($filters['search'], true) . '%');
$query->where('(' . $db->quoteName('name') . ' LIKE ' . $search
. ' OR ' . $db->quoteName('search') . ' LIKE ' . $search . ')');
}
if ($filters['published'] !== '' && $filters['published'] !== null)
{
$query->where($db->quoteName('published') . ' = ' . (int) $filters['published']);
}
$db->setQuery($query);
return (int) $db->loadResult();
}
}
@@ -0,0 +1,69 @@
<?php
/**
* @package MokoSuiteClient
* @subpackage com_mokosuiteclient
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GNU General Public License version 3 or later; see LICENSE
*
* SPDX-License-Identifier: GPL-3.0-or-later
*/
namespace Moko\Component\MokoSuiteClient\Administrator\Model;
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use Joomla\CMS\MVC\Model\BaseDatabaseModel;
use Joomla\Database\DatabaseInterface;
class SnippetsModel extends BaseDatabaseModel
{
public function getItems(array $filters = [], int $limit = 50, int $offset = 0): array
{
$db = Factory::getContainer()->get(DatabaseInterface::class);
$query = $db->getQuery(true)
->select('*')
->from($db->quoteName('#__mokosuiteclient_snippets'));
if (!empty($filters['search']))
{
$search = $db->quote('%' . $db->escape($filters['search'], true) . '%');
$query->where('(' . $db->quoteName('name') . ' LIKE ' . $search
. ' OR ' . $db->quoteName('alias') . ' LIKE ' . $search . ')');
}
if ($filters['published'] !== '' && $filters['published'] !== null)
{
$query->where($db->quoteName('published') . ' = ' . (int) $filters['published']);
}
$query->order($db->quoteName('ordering') . ' ASC, ' . $db->quoteName('name') . ' ASC');
$db->setQuery($query, $offset, $limit);
return $db->loadObjectList() ?: [];
}
public function getTotal(array $filters = []): int
{
$db = Factory::getContainer()->get(DatabaseInterface::class);
$query = $db->getQuery(true)
->select('COUNT(*)')
->from($db->quoteName('#__mokosuiteclient_snippets'));
if (!empty($filters['search']))
{
$search = $db->quote('%' . $db->escape($filters['search'], true) . '%');
$query->where('(' . $db->quoteName('name') . ' LIKE ' . $search
. ' OR ' . $db->quoteName('alias') . ' LIKE ' . $search . ')');
}
if ($filters['published'] !== '' && $filters['published'] !== null)
{
$query->where($db->quoteName('published') . ' = ' . (int) $filters['published']);
}
$db->setQuery($query);
return (int) $db->loadResult();
}
}
@@ -0,0 +1,69 @@
<?php
/**
* @package MokoSuiteClient
* @subpackage com_mokosuiteclient
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GNU General Public License version 3 or later; see LICENSE
*
* SPDX-License-Identifier: GPL-3.0-or-later
*/
namespace Moko\Component\MokoSuiteClient\Administrator\Model;
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use Joomla\CMS\MVC\Model\BaseDatabaseModel;
use Joomla\Database\DatabaseInterface;
class TemplatesModel extends BaseDatabaseModel
{
public function getItems(array $filters = [], int $limit = 50, int $offset = 0): array
{
$db = Factory::getContainer()->get(DatabaseInterface::class);
$query = $db->getQuery(true)
->select('*')
->from($db->quoteName('#__mokosuiteclient_content_templates'));
if (!empty($filters['search']))
{
$search = $db->quote('%' . $db->escape($filters['search'], true) . '%');
$query->where('(' . $db->quoteName('name') . ' LIKE ' . $search
. ' OR ' . $db->quoteName('alias') . ' LIKE ' . $search . ')');
}
if ($filters['published'] !== '' && $filters['published'] !== null)
{
$query->where($db->quoteName('published') . ' = ' . (int) $filters['published']);
}
$query->order($db->quoteName('ordering') . ' ASC, ' . $db->quoteName('name') . ' ASC');
$db->setQuery($query, $offset, $limit);
return $db->loadObjectList() ?: [];
}
public function getTotal(array $filters = []): int
{
$db = Factory::getContainer()->get(DatabaseInterface::class);
$query = $db->getQuery(true)
->select('COUNT(*)')
->from($db->quoteName('#__mokosuiteclient_content_templates'));
if (!empty($filters['search']))
{
$search = $db->quote('%' . $db->escape($filters['search'], true) . '%');
$query->where('(' . $db->quoteName('name') . ' LIKE ' . $search
. ' OR ' . $db->quoteName('alias') . ' LIKE ' . $search . ')');
}
if ($filters['published'] !== '' && $filters['published'] !== null)
{
$query->where($db->quoteName('published') . ' = ' . (int) $filters['published']);
}
$db->setQuery($query);
return (int) $db->loadResult();
}
}
@@ -0,0 +1,55 @@
<?php
/**
* @package MokoSuiteClient
* @subpackage com_mokosuiteclient
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GNU General Public License version 3 or later; see LICENSE
*
* SPDX-License-Identifier: GPL-3.0-or-later
*/
namespace Moko\Component\MokoSuiteClient\Administrator\View\Conditions;
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use Joomla\CMS\MVC\View\HtmlView as BaseHtmlView;
use Joomla\CMS\Toolbar\ToolbarHelper;
class HtmlView extends BaseHtmlView
{
protected $items = [];
protected $total = 0;
protected $filters = [];
public function display($tpl = null)
{
$model = new \Moko\Component\MokoSuiteClient\Administrator\Model\ConditionsModel();
$input = Factory::getApplication()->getInput();
$this->filters = [
'search' => $input->getString('filter_search', ''),
'published' => $input->get('filter_published', ''),
];
$page = max(1, $input->getInt('page', 1));
$limit = 50;
$offset = ($page - 1) * $limit;
$this->items = $model->getItems($this->filters, $limit, $offset);
$this->total = $model->getTotal($this->filters);
$this->addToolbar();
$wa = Factory::getApplication()->getDocument()->getWebAssetManager();
$wa->registerAndUseStyle('com_mokosuiteclient.dashboard', 'com_mokosuiteclient/dashboard.css');
parent::display($tpl);
}
protected function addToolbar(): void
{
ToolbarHelper::title('Conditions', 'shuffle');
ToolbarHelper::back('JTOOLBAR_BACK', 'index.php?option=com_mokosuiteclient');
}
}
@@ -0,0 +1,56 @@
<?php
/**
* @package MokoSuiteClient
* @subpackage com_mokosuiteclient
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GNU General Public License version 3 or later; see LICENSE
*
* SPDX-License-Identifier: GPL-3.0-or-later
*/
namespace Moko\Component\MokoSuiteClient\Administrator\View\Modules;
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use Joomla\CMS\MVC\View\HtmlView as BaseHtmlView;
use Joomla\CMS\Toolbar\ToolbarHelper;
class HtmlView extends BaseHtmlView
{
protected $items = [];
protected $total = 0;
protected $filters = [];
public function display($tpl = null)
{
$model = new \Moko\Component\MokoSuiteClient\Administrator\Model\ModulesModel();
$input = Factory::getApplication()->getInput();
$this->filters = [
'search' => $input->getString('filter_search', ''),
'published' => $input->get('filter_published', ''),
'client_id' => $input->get('filter_client', ''),
];
$page = max(1, $input->getInt('page', 1));
$limit = 50;
$offset = ($page - 1) * $limit;
$this->items = $model->getItems($this->filters, $limit, $offset);
$this->total = $model->getTotal($this->filters);
$this->addToolbar();
$wa = Factory::getApplication()->getDocument()->getWebAssetManager();
$wa->registerAndUseStyle('com_mokosuiteclient.dashboard', 'com_mokosuiteclient/dashboard.css');
parent::display($tpl);
}
protected function addToolbar(): void
{
ToolbarHelper::title('Module Manager', 'cube');
ToolbarHelper::back('JTOOLBAR_BACK', 'index.php?option=com_mokosuiteclient');
}
}
@@ -0,0 +1,55 @@
<?php
/**
* @package MokoSuiteClient
* @subpackage com_mokosuiteclient
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GNU General Public License version 3 or later; see LICENSE
*
* SPDX-License-Identifier: GPL-3.0-or-later
*/
namespace Moko\Component\MokoSuiteClient\Administrator\View\Replacements;
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use Joomla\CMS\MVC\View\HtmlView as BaseHtmlView;
use Joomla\CMS\Toolbar\ToolbarHelper;
class HtmlView extends BaseHtmlView
{
protected $items = [];
protected $total = 0;
protected $filters = [];
public function display($tpl = null)
{
$model = new \Moko\Component\MokoSuiteClient\Administrator\Model\ReplacementsModel();
$input = Factory::getApplication()->getInput();
$this->filters = [
'search' => $input->getString('filter_search', ''),
'published' => $input->get('filter_published', ''),
];
$page = max(1, $input->getInt('page', 1));
$limit = 50;
$offset = ($page - 1) * $limit;
$this->items = $model->getItems($this->filters, $limit, $offset);
$this->total = $model->getTotal($this->filters);
$this->addToolbar();
$wa = Factory::getApplication()->getDocument()->getWebAssetManager();
$wa->registerAndUseStyle('com_mokosuiteclient.dashboard', 'com_mokosuiteclient/dashboard.css');
parent::display($tpl);
}
protected function addToolbar(): void
{
ToolbarHelper::title('Replacements', 'right-left');
ToolbarHelper::back('JTOOLBAR_BACK', 'index.php?option=com_mokosuiteclient');
}
}
@@ -0,0 +1,55 @@
<?php
/**
* @package MokoSuiteClient
* @subpackage com_mokosuiteclient
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GNU General Public License version 3 or later; see LICENSE
*
* SPDX-License-Identifier: GPL-3.0-or-later
*/
namespace Moko\Component\MokoSuiteClient\Administrator\View\Snippets;
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use Joomla\CMS\MVC\View\HtmlView as BaseHtmlView;
use Joomla\CMS\Toolbar\ToolbarHelper;
class HtmlView extends BaseHtmlView
{
protected $items = [];
protected $total = 0;
protected $filters = [];
public function display($tpl = null)
{
$model = new \Moko\Component\MokoSuiteClient\Administrator\Model\SnippetsModel();
$input = Factory::getApplication()->getInput();
$this->filters = [
'search' => $input->getString('filter_search', ''),
'published' => $input->get('filter_published', ''),
];
$page = max(1, $input->getInt('page', 1));
$limit = 50;
$offset = ($page - 1) * $limit;
$this->items = $model->getItems($this->filters, $limit, $offset);
$this->total = $model->getTotal($this->filters);
$this->addToolbar();
$wa = Factory::getApplication()->getDocument()->getWebAssetManager();
$wa->registerAndUseStyle('com_mokosuiteclient.dashboard', 'com_mokosuiteclient/dashboard.css');
parent::display($tpl);
}
protected function addToolbar(): void
{
ToolbarHelper::title('Snippets', 'code');
ToolbarHelper::back('JTOOLBAR_BACK', 'index.php?option=com_mokosuiteclient');
}
}
@@ -0,0 +1,55 @@
<?php
/**
* @package MokoSuiteClient
* @subpackage com_mokosuiteclient
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GNU General Public License version 3 or later; see LICENSE
*
* SPDX-License-Identifier: GPL-3.0-or-later
*/
namespace Moko\Component\MokoSuiteClient\Administrator\View\Templates;
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use Joomla\CMS\MVC\View\HtmlView as BaseHtmlView;
use Joomla\CMS\Toolbar\ToolbarHelper;
class HtmlView extends BaseHtmlView
{
protected $items = [];
protected $total = 0;
protected $filters = [];
public function display($tpl = null)
{
$model = new \Moko\Component\MokoSuiteClient\Administrator\Model\TemplatesModel();
$input = Factory::getApplication()->getInput();
$this->filters = [
'search' => $input->getString('filter_search', ''),
'published' => $input->get('filter_published', ''),
];
$page = max(1, $input->getInt('page', 1));
$limit = 50;
$offset = ($page - 1) * $limit;
$this->items = $model->getItems($this->filters, $limit, $offset);
$this->total = $model->getTotal($this->filters);
$this->addToolbar();
$wa = Factory::getApplication()->getDocument()->getWebAssetManager();
$wa->registerAndUseStyle('com_mokosuiteclient.dashboard', 'com_mokosuiteclient/dashboard.css');
parent::display($tpl);
}
protected function addToolbar(): void
{
ToolbarHelper::title('Content Templates', 'file-lines');
ToolbarHelper::back('JTOOLBAR_BACK', 'index.php?option=com_mokosuiteclient');
}
}
@@ -0,0 +1,142 @@
<?php
/**
* @package MokoSuiteClient
* @subpackage com_mokosuiteclient
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GNU General Public License version 3 or later; see LICENSE
*
* SPDX-License-Identifier: GPL-3.0-or-later
*/
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use Joomla\CMS\Router\Route;
use Joomla\CMS\Session\Session;
$items = $this->items;
$total = $this->total;
$filters = $this->filters;
$token = Session::getFormToken();
$input = Factory::getApplication()->getInput();
$page = max(1, $input->getInt('page', 1));
$pages = max(1, ceil($total / 50));
?>
<div id="mokosuiteclient-conditions">
<form method="get" action="<?php echo Route::_('index.php?option=com_mokosuiteclient&view=conditions'); ?>" class="mb-3">
<input type="hidden" name="option" value="com_mokosuiteclient">
<input type="hidden" name="view" value="conditions">
<div class="row g-2 align-items-end">
<div class="col-md-4">
<input type="text" name="filter_search" class="form-control form-control-sm" placeholder="Search by name or alias..." value="<?php echo htmlspecialchars($filters['search'], ENT_QUOTES, 'UTF-8'); ?>">
</div>
<div class="col-md-2">
<select name="filter_published" class="form-select form-select-sm">
<option value="">All States</option>
<option value="1"<?php echo $filters['published'] === '1' ? ' selected' : ''; ?>>Published</option>
<option value="0"<?php echo $filters['published'] === '0' ? ' selected' : ''; ?>>Unpublished</option>
</select>
</div>
<div class="col-auto">
<button type="submit" class="btn btn-sm btn-primary"><span class="icon-search"></span> Filter</button>
<a href="<?php echo Route::_('index.php?option=com_mokosuiteclient&view=conditions'); ?>" class="btn btn-sm btn-outline-secondary">Reset</a>
</div>
</div>
</form>
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<span><span class="icon-shuffle"></span> Conditions</span>
<span class="badge bg-secondary"><?php echo number_format($total); ?> total</span>
</div>
<div class="table-responsive">
<table class="table table-striped table-hover mb-0">
<thead>
<tr>
<th style="width:5%">ID</th>
<th>Name</th>
<th>Alias</th>
<th>Category</th>
<th style="width:8%">Match</th>
<th style="width:8%">Groups</th>
<th style="width:8%">Rules</th>
<th style="width:8%">Status</th>
</tr>
</thead>
<tbody>
<?php if (empty($items)): ?>
<tr><td colspan="8" class="text-center text-muted py-4">No conditions found.</td></tr>
<?php else: ?>
<?php foreach ($items as $item): ?>
<tr>
<td><?php echo (int) $item->id; ?></td>
<td>
<?php if ($item->color): ?>
<span style="display:inline-block;width:12px;height:12px;border-radius:2px;background:<?php echo htmlspecialchars($item->color, ENT_QUOTES, 'UTF-8'); ?>;vertical-align:middle;margin-right:4px;"></span>
<?php endif; ?>
<?php echo htmlspecialchars($item->name, ENT_QUOTES, 'UTF-8'); ?>
</td>
<td><code><?php echo htmlspecialchars($item->alias, ENT_QUOTES, 'UTF-8'); ?></code></td>
<td>
<?php if ($item->category): ?>
<span class="badge bg-info"><?php echo htmlspecialchars($item->category, ENT_QUOTES, 'UTF-8'); ?></span>
<?php endif; ?>
</td>
<td><span class="badge bg-<?php echo $item->match_all ? 'primary' : 'warning text-dark'; ?>"><?php echo $item->match_all ? 'ALL' : 'ANY'; ?></span></td>
<td><?php echo (int) $item->group_count; ?></td>
<td><?php echo (int) $item->rule_count; ?></td>
<td>
<a href="#" class="mokosuite-toggle-published badge bg-<?php echo $item->published ? 'success' : 'danger'; ?>"
data-table="mokosuiteclient_conditions" data-id="<?php echo (int) $item->id; ?>"
data-token="<?php echo $token; ?>">
<?php echo $item->published ? 'Published' : 'Unpublished'; ?>
</a>
</td>
</tr>
<?php endforeach; ?>
<?php endif; ?>
</tbody>
</table>
</div>
</div>
<?php if ($pages > 1): ?>
<nav class="mt-3"><ul class="pagination pagination-sm justify-content-center">
<?php for ($p = 1; $p <= $pages; $p++): ?>
<li class="page-item<?php echo $p === $page ? ' active' : ''; ?>">
<a class="page-link" href="<?php echo Route::_('index.php?option=com_mokosuiteclient&view=conditions&page=' . $p
. ($filters['search'] ? '&filter_search=' . urlencode($filters['search']) : '')
. ($filters['published'] !== '' ? '&filter_published=' . $filters['published'] : '')); ?>"><?php echo $p; ?></a>
</li>
<?php endfor; ?>
</ul></nav>
<?php endif; ?>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
document.querySelectorAll('.mokosuite-toggle-published').forEach(function(el) {
el.addEventListener('click', function(e) {
e.preventDefault();
var table = this.dataset.table, id = this.dataset.id, token = this.dataset.token, badge = this;
var fd = new FormData();
fd.append('table', table);
fd.append('id', id);
fd.append(token, '1');
fetch('index.php?option=com_mokosuiteclient&task=display.togglePublished&format=json', {
method: 'POST', headers: {'X-Requested-With': 'XMLHttpRequest'}, body: fd
}).then(function(r) { return r.json(); }).then(function(d) {
if (d.success) {
var pub = d.published;
badge.className = 'mokosuite-toggle-published badge bg-' + (pub ? 'success' : 'danger');
badge.textContent = pub ? 'Published' : 'Unpublished';
}
}).catch(function() {
badge.textContent = 'Error';
badge.className = 'mokosuite-toggle-published badge bg-warning text-dark';
});
});
});
});
</script>
@@ -0,0 +1,152 @@
<?php
/**
* @package MokoSuiteClient
* @subpackage com_mokosuiteclient
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GNU General Public License version 3 or later; see LICENSE
*
* SPDX-License-Identifier: GPL-3.0-or-later
*/
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use Joomla\CMS\Router\Route;
use Joomla\CMS\Session\Session;
$items = $this->items;
$total = $this->total;
$filters = $this->filters;
$token = Session::getFormToken();
$input = Factory::getApplication()->getInput();
$page = max(1, $input->getInt('page', 1));
$pages = max(1, ceil($total / 50));
$publishedLabels = [1 => 'Published', 0 => 'Unpublished', -2 => 'Trashed'];
$publishedColors = [1 => 'success', 0 => 'danger', -2 => 'dark'];
?>
<div id="mokosuiteclient-modules">
<form method="get" action="<?php echo Route::_('index.php?option=com_mokosuiteclient&view=modules'); ?>" class="mb-3">
<input type="hidden" name="option" value="com_mokosuiteclient">
<input type="hidden" name="view" value="modules">
<div class="row g-2 align-items-end">
<div class="col-md-3">
<input type="text" name="filter_search" class="form-control form-control-sm" placeholder="Search title, type, or position..." value="<?php echo htmlspecialchars($filters['search'], ENT_QUOTES, 'UTF-8'); ?>">
</div>
<div class="col-md-2">
<select name="filter_client" class="form-select form-select-sm">
<option value="">All Clients</option>
<option value="0"<?php echo $filters['client_id'] === '0' ? ' selected' : ''; ?>>Site</option>
<option value="1"<?php echo $filters['client_id'] === '1' ? ' selected' : ''; ?>>Administrator</option>
</select>
</div>
<div class="col-md-2">
<select name="filter_published" class="form-select form-select-sm">
<option value="">All States</option>
<option value="1"<?php echo $filters['published'] === '1' ? ' selected' : ''; ?>>Published</option>
<option value="0"<?php echo $filters['published'] === '0' ? ' selected' : ''; ?>>Unpublished</option>
<option value="-2"<?php echo $filters['published'] === '-2' ? ' selected' : ''; ?>>Trashed</option>
</select>
</div>
<div class="col-auto">
<button type="submit" class="btn btn-sm btn-primary"><span class="icon-search"></span> Filter</button>
<a href="<?php echo Route::_('index.php?option=com_mokosuiteclient&view=modules'); ?>" class="btn btn-sm btn-outline-secondary">Reset</a>
</div>
</div>
</form>
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<span><span class="icon-cube"></span> Module Manager</span>
<span class="badge bg-secondary"><?php echo number_format($total); ?> total</span>
</div>
<div class="table-responsive">
<table class="table table-striped table-hover mb-0">
<thead>
<tr>
<th style="width:5%">ID</th>
<th>Title</th>
<th>Position</th>
<th>Type</th>
<th style="width:8%">Client</th>
<th style="width:8%">Order</th>
<th style="width:8%">Status</th>
</tr>
</thead>
<tbody>
<?php if (empty($items)): ?>
<tr><td colspan="7" class="text-center text-muted py-4">No modules found.</td></tr>
<?php else: ?>
<?php foreach ($items as $item): ?>
<tr>
<td><?php echo (int) $item->id; ?></td>
<td>
<a href="<?php echo Route::_('index.php?option=com_modules&task=module.edit&id=' . (int) $item->id); ?>">
<?php echo htmlspecialchars($item->title, ENT_QUOTES, 'UTF-8'); ?>
</a>
</td>
<td><code><?php echo htmlspecialchars($item->position ?: '(none)', ENT_QUOTES, 'UTF-8'); ?></code></td>
<td><small><?php echo htmlspecialchars($item->module, ENT_QUOTES, 'UTF-8'); ?></small></td>
<td><span class="badge bg-<?php echo $item->client_id ? 'dark' : 'primary'; ?>"><?php echo $item->client_id ? 'Admin' : 'Site'; ?></span></td>
<td><?php echo (int) $item->ordering; ?></td>
<td>
<?php
$pub = (int) $item->published;
$label = $publishedLabels[$pub] ?? 'Unknown';
$color = $publishedColors[$pub] ?? 'secondary';
?>
<a href="#" class="mokosuite-toggle-module badge bg-<?php echo $color; ?>"
data-id="<?php echo (int) $item->id; ?>"
data-token="<?php echo $token; ?>">
<?php echo $label; ?>
</a>
</td>
</tr>
<?php endforeach; ?>
<?php endif; ?>
</tbody>
</table>
</div>
</div>
<?php if ($pages > 1): ?>
<nav class="mt-3"><ul class="pagination pagination-sm justify-content-center">
<?php for ($p = 1; $p <= $pages; $p++): ?>
<li class="page-item<?php echo $p === $page ? ' active' : ''; ?>">
<a class="page-link" href="<?php echo Route::_('index.php?option=com_mokosuiteclient&view=modules&page=' . $p
. ($filters['search'] ? '&filter_search=' . urlencode($filters['search']) : '')
. ($filters['published'] !== '' ? '&filter_published=' . $filters['published'] : '')
. ($filters['client_id'] !== '' ? '&filter_client=' . $filters['client_id'] : '')); ?>"><?php echo $p; ?></a>
</li>
<?php endfor; ?>
</ul></nav>
<?php endif; ?>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
document.querySelectorAll('.mokosuite-toggle-module').forEach(function(el) {
el.addEventListener('click', function(e) {
e.preventDefault();
var id = this.dataset.id, token = this.dataset.token, badge = this;
var fd = new FormData();
fd.append('table', 'modules');
fd.append('id', id);
fd.append(token, '1');
fetch('index.php?option=com_mokosuiteclient&task=display.togglePublished&format=json', {
method: 'POST', headers: {'X-Requested-With': 'XMLHttpRequest'}, body: fd
}).then(function(r) { return r.json(); }).then(function(d) {
if (d.success) {
var pub = d.published;
badge.className = 'mokosuite-toggle-module badge bg-' + (pub ? 'success' : 'danger');
badge.textContent = pub ? 'Published' : 'Unpublished';
}
}).catch(function() {
badge.textContent = 'Error';
badge.className = 'mokosuite-toggle-module badge bg-warning text-dark';
});
});
});
});
</script>
@@ -0,0 +1,142 @@
<?php
/**
* @package MokoSuiteClient
* @subpackage com_mokosuiteclient
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GNU General Public License version 3 or later; see LICENSE
*
* SPDX-License-Identifier: GPL-3.0-or-later
*/
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use Joomla\CMS\Router\Route;
use Joomla\CMS\Session\Session;
$items = $this->items;
$total = $this->total;
$filters = $this->filters;
$token = Session::getFormToken();
$input = Factory::getApplication()->getInput();
$page = max(1, $input->getInt('page', 1));
$pages = max(1, ceil($total / 50));
?>
<div id="mokosuiteclient-replacements">
<form method="get" action="<?php echo Route::_('index.php?option=com_mokosuiteclient&view=replacements'); ?>" class="mb-3">
<input type="hidden" name="option" value="com_mokosuiteclient">
<input type="hidden" name="view" value="replacements">
<div class="row g-2 align-items-end">
<div class="col-md-4">
<input type="text" name="filter_search" class="form-control form-control-sm" placeholder="Search by name or pattern..." value="<?php echo htmlspecialchars($filters['search'], ENT_QUOTES, 'UTF-8'); ?>">
</div>
<div class="col-md-2">
<select name="filter_published" class="form-select form-select-sm">
<option value="">All States</option>
<option value="1"<?php echo $filters['published'] === '1' ? ' selected' : ''; ?>>Published</option>
<option value="0"<?php echo $filters['published'] === '0' ? ' selected' : ''; ?>>Unpublished</option>
</select>
</div>
<div class="col-auto">
<button type="submit" class="btn btn-sm btn-primary"><span class="icon-search"></span> Filter</button>
<a href="<?php echo Route::_('index.php?option=com_mokosuiteclient&view=replacements'); ?>" class="btn btn-sm btn-outline-secondary">Reset</a>
</div>
</div>
</form>
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<span><span class="icon-right-left"></span> Replacements</span>
<span class="badge bg-secondary"><?php echo number_format($total); ?> total</span>
</div>
<div class="table-responsive">
<table class="table table-striped table-hover mb-0">
<thead>
<tr>
<th style="width:5%">ID</th>
<th>Name</th>
<th>Search</th>
<th>Replace</th>
<th style="width:7%">Area</th>
<th style="width:5%">Regex</th>
<th>Category</th>
<th style="width:8%">Status</th>
</tr>
</thead>
<tbody>
<?php if (empty($items)): ?>
<tr><td colspan="8" class="text-center text-muted py-4">No replacement rules found.</td></tr>
<?php else: ?>
<?php foreach ($items as $item): ?>
<tr>
<td><?php echo (int) $item->id; ?></td>
<td>
<?php if ($item->color): ?>
<span style="display:inline-block;width:12px;height:12px;border-radius:2px;background:<?php echo htmlspecialchars($item->color, ENT_QUOTES, 'UTF-8'); ?>;vertical-align:middle;margin-right:4px;"></span>
<?php endif; ?>
<?php echo htmlspecialchars($item->name, ENT_QUOTES, 'UTF-8'); ?>
</td>
<td><code style="font-size:0.8rem"><?php echo htmlspecialchars(mb_strimwidth($item->search, 0, 50, '...'), ENT_QUOTES, 'UTF-8'); ?></code></td>
<td><code style="font-size:0.8rem"><?php echo htmlspecialchars(mb_strimwidth($item->replace_value, 0, 50, '...'), ENT_QUOTES, 'UTF-8'); ?></code></td>
<td><span class="badge bg-secondary"><?php echo htmlspecialchars($item->area, ENT_QUOTES, 'UTF-8'); ?></span></td>
<td><?php echo $item->regex ? '<span class="badge bg-warning text-dark">Yes</span>' : ''; ?></td>
<td>
<?php if ($item->category): ?>
<span class="badge bg-info"><?php echo htmlspecialchars($item->category, ENT_QUOTES, 'UTF-8'); ?></span>
<?php endif; ?>
</td>
<td>
<a href="#" class="mokosuite-toggle-published badge bg-<?php echo $item->published ? 'success' : 'danger'; ?>"
data-table="mokosuiteclient_replacements" data-id="<?php echo (int) $item->id; ?>"
data-token="<?php echo $token; ?>">
<?php echo $item->published ? 'Published' : 'Unpublished'; ?>
</a>
</td>
</tr>
<?php endforeach; ?>
<?php endif; ?>
</tbody>
</table>
</div>
</div>
<?php if ($pages > 1): ?>
<nav class="mt-3"><ul class="pagination pagination-sm justify-content-center">
<?php for ($p = 1; $p <= $pages; $p++): ?>
<li class="page-item<?php echo $p === $page ? ' active' : ''; ?>">
<a class="page-link" href="<?php echo Route::_('index.php?option=com_mokosuiteclient&view=replacements&page=' . $p
. ($filters['search'] ? '&filter_search=' . urlencode($filters['search']) : '')
. ($filters['published'] !== '' ? '&filter_published=' . $filters['published'] : '')); ?>"><?php echo $p; ?></a>
</li>
<?php endfor; ?>
</ul></nav>
<?php endif; ?>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
document.querySelectorAll('.mokosuite-toggle-published').forEach(function(el) {
el.addEventListener('click', function(e) {
e.preventDefault();
var table = this.dataset.table, id = this.dataset.id, token = this.dataset.token, badge = this;
var fd = new FormData();
fd.append('table', table);
fd.append('id', id);
fd.append(token, '1');
fetch('index.php?option=com_mokosuiteclient&task=display.togglePublished&format=json', {
method: 'POST', headers: {'X-Requested-With': 'XMLHttpRequest'}, body: fd
}).then(function(r) { return r.json(); }).then(function(d) {
if (d.success) {
var pub = d.published;
badge.className = 'mokosuite-toggle-published badge bg-' + (pub ? 'success' : 'danger');
badge.textContent = pub ? 'Published' : 'Unpublished';
}
}).catch(function() {
badge.textContent = 'Error';
badge.className = 'mokosuite-toggle-published badge bg-warning text-dark';
});
});
});
});
</script>
@@ -0,0 +1,141 @@
<?php
/**
* @package MokoSuiteClient
* @subpackage com_mokosuiteclient
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GNU General Public License version 3 or later; see LICENSE
*
* SPDX-License-Identifier: GPL-3.0-or-later
*/
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use Joomla\CMS\Router\Route;
use Joomla\CMS\Session\Session;
$items = $this->items;
$total = $this->total;
$filters = $this->filters;
$token = Session::getFormToken();
$input = Factory::getApplication()->getInput();
$page = max(1, $input->getInt('page', 1));
$pages = max(1, ceil($total / 50));
?>
<div id="mokosuiteclient-snippets">
<form method="get" action="<?php echo Route::_('index.php?option=com_mokosuiteclient&view=snippets'); ?>" class="mb-3">
<input type="hidden" name="option" value="com_mokosuiteclient">
<input type="hidden" name="view" value="snippets">
<div class="row g-2 align-items-end">
<div class="col-md-4">
<input type="text" name="filter_search" class="form-control form-control-sm" placeholder="Search by name or alias..." value="<?php echo htmlspecialchars($filters['search'], ENT_QUOTES, 'UTF-8'); ?>">
</div>
<div class="col-md-2">
<select name="filter_published" class="form-select form-select-sm">
<option value="">All States</option>
<option value="1"<?php echo $filters['published'] === '1' ? ' selected' : ''; ?>>Published</option>
<option value="0"<?php echo $filters['published'] === '0' ? ' selected' : ''; ?>>Unpublished</option>
</select>
</div>
<div class="col-auto">
<button type="submit" class="btn btn-sm btn-primary"><span class="icon-search"></span> Filter</button>
<a href="<?php echo Route::_('index.php?option=com_mokosuiteclient&view=snippets'); ?>" class="btn btn-sm btn-outline-secondary">Reset</a>
</div>
</div>
</form>
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<span><span class="icon-code"></span> Snippets</span>
<span class="badge bg-secondary"><?php echo number_format($total); ?> total</span>
</div>
<div class="table-responsive">
<table class="table table-striped table-hover mb-0">
<thead>
<tr>
<th style="width:5%">ID</th>
<th>Name</th>
<th>Alias</th>
<th>Category</th>
<th style="width:8%">Order</th>
<th style="width:8%">Status</th>
</tr>
</thead>
<tbody>
<?php if (empty($items)): ?>
<tr><td colspan="6" class="text-center text-muted py-4">No snippets found.</td></tr>
<?php else: ?>
<?php foreach ($items as $item): ?>
<tr>
<td><?php echo (int) $item->id; ?></td>
<td>
<?php if ($item->color): ?>
<span style="display:inline-block;width:12px;height:12px;border-radius:2px;background:<?php echo htmlspecialchars($item->color, ENT_QUOTES, 'UTF-8'); ?>;vertical-align:middle;margin-right:4px;"></span>
<?php endif; ?>
<?php echo htmlspecialchars($item->name, ENT_QUOTES, 'UTF-8'); ?>
<?php if ($item->description): ?>
<br><small class="text-muted"><?php echo htmlspecialchars(mb_strimwidth($item->description, 0, 80, '...'), ENT_QUOTES, 'UTF-8'); ?></small>
<?php endif; ?>
</td>
<td><code>{snippet <?php echo htmlspecialchars($item->alias, ENT_QUOTES, 'UTF-8'); ?>}</code></td>
<td>
<?php if ($item->category): ?>
<span class="badge bg-info"><?php echo htmlspecialchars($item->category, ENT_QUOTES, 'UTF-8'); ?></span>
<?php endif; ?>
</td>
<td><?php echo (int) $item->ordering; ?></td>
<td>
<a href="#" class="mokosuite-toggle-published badge bg-<?php echo $item->published ? 'success' : 'danger'; ?>"
data-table="mokosuiteclient_snippets" data-id="<?php echo (int) $item->id; ?>"
data-token="<?php echo $token; ?>">
<?php echo $item->published ? 'Published' : 'Unpublished'; ?>
</a>
</td>
</tr>
<?php endforeach; ?>
<?php endif; ?>
</tbody>
</table>
</div>
</div>
<?php if ($pages > 1): ?>
<nav class="mt-3"><ul class="pagination pagination-sm justify-content-center">
<?php for ($p = 1; $p <= $pages; $p++): ?>
<li class="page-item<?php echo $p === $page ? ' active' : ''; ?>">
<a class="page-link" href="<?php echo Route::_('index.php?option=com_mokosuiteclient&view=snippets&page=' . $p
. ($filters['search'] ? '&filter_search=' . urlencode($filters['search']) : '')
. ($filters['published'] !== '' ? '&filter_published=' . $filters['published'] : '')); ?>"><?php echo $p; ?></a>
</li>
<?php endfor; ?>
</ul></nav>
<?php endif; ?>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
document.querySelectorAll('.mokosuite-toggle-published').forEach(function(el) {
el.addEventListener('click', function(e) {
e.preventDefault();
var table = this.dataset.table, id = this.dataset.id, token = this.dataset.token, badge = this;
var fd = new FormData();
fd.append('table', table);
fd.append('id', id);
fd.append(token, '1');
fetch('index.php?option=com_mokosuiteclient&task=display.togglePublished&format=json', {
method: 'POST', headers: {'X-Requested-With': 'XMLHttpRequest'}, body: fd
}).then(function(r) { return r.json(); }).then(function(d) {
if (d.success) {
var pub = d.published;
badge.className = 'mokosuite-toggle-published badge bg-' + (pub ? 'success' : 'danger');
badge.textContent = pub ? 'Published' : 'Unpublished';
}
}).catch(function() {
badge.textContent = 'Error';
badge.className = 'mokosuite-toggle-published badge bg-warning text-dark';
});
});
});
});
</script>
@@ -0,0 +1,141 @@
<?php
/**
* @package MokoSuiteClient
* @subpackage com_mokosuiteclient
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GNU General Public License version 3 or later; see LICENSE
*
* SPDX-License-Identifier: GPL-3.0-or-later
*/
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use Joomla\CMS\Router\Route;
use Joomla\CMS\Session\Session;
$items = $this->items;
$total = $this->total;
$filters = $this->filters;
$token = Session::getFormToken();
$input = Factory::getApplication()->getInput();
$page = max(1, $input->getInt('page', 1));
$pages = max(1, ceil($total / 50));
?>
<div id="mokosuiteclient-templates">
<form method="get" action="<?php echo Route::_('index.php?option=com_mokosuiteclient&view=templates'); ?>" class="mb-3">
<input type="hidden" name="option" value="com_mokosuiteclient">
<input type="hidden" name="view" value="templates">
<div class="row g-2 align-items-end">
<div class="col-md-4">
<input type="text" name="filter_search" class="form-control form-control-sm" placeholder="Search by name or alias..." value="<?php echo htmlspecialchars($filters['search'], ENT_QUOTES, 'UTF-8'); ?>">
</div>
<div class="col-md-2">
<select name="filter_published" class="form-select form-select-sm">
<option value="">All States</option>
<option value="1"<?php echo $filters['published'] === '1' ? ' selected' : ''; ?>>Published</option>
<option value="0"<?php echo $filters['published'] === '0' ? ' selected' : ''; ?>>Unpublished</option>
</select>
</div>
<div class="col-auto">
<button type="submit" class="btn btn-sm btn-primary"><span class="icon-search"></span> Filter</button>
<a href="<?php echo Route::_('index.php?option=com_mokosuiteclient&view=templates'); ?>" class="btn btn-sm btn-outline-secondary">Reset</a>
</div>
</div>
</form>
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<span><span class="icon-file-alt"></span> Content Templates</span>
<span class="badge bg-secondary"><?php echo number_format($total); ?> total</span>
</div>
<div class="table-responsive">
<table class="table table-striped table-hover mb-0">
<thead>
<tr>
<th style="width:5%">ID</th>
<th>Name</th>
<th>Alias</th>
<th>Category</th>
<th style="width:8%">Order</th>
<th style="width:8%">Status</th>
</tr>
</thead>
<tbody>
<?php if (empty($items)): ?>
<tr><td colspan="6" class="text-center text-muted py-4">No content templates found.</td></tr>
<?php else: ?>
<?php foreach ($items as $item): ?>
<tr>
<td><?php echo (int) $item->id; ?></td>
<td>
<?php if ($item->color): ?>
<span style="display:inline-block;width:12px;height:12px;border-radius:2px;background:<?php echo htmlspecialchars($item->color, ENT_QUOTES, 'UTF-8'); ?>;vertical-align:middle;margin-right:4px;"></span>
<?php endif; ?>
<?php echo htmlspecialchars($item->name, ENT_QUOTES, 'UTF-8'); ?>
<?php if ($item->description): ?>
<br><small class="text-muted"><?php echo htmlspecialchars(mb_strimwidth($item->description, 0, 80, '...'), ENT_QUOTES, 'UTF-8'); ?></small>
<?php endif; ?>
</td>
<td><code><?php echo htmlspecialchars($item->alias, ENT_QUOTES, 'UTF-8'); ?></code></td>
<td>
<?php if ($item->category): ?>
<span class="badge bg-info"><?php echo htmlspecialchars($item->category, ENT_QUOTES, 'UTF-8'); ?></span>
<?php endif; ?>
</td>
<td><?php echo (int) $item->ordering; ?></td>
<td>
<a href="#" class="mokosuite-toggle-published badge bg-<?php echo $item->published ? 'success' : 'danger'; ?>"
data-table="mokosuiteclient_content_templates" data-id="<?php echo (int) $item->id; ?>"
data-token="<?php echo $token; ?>">
<?php echo $item->published ? 'Published' : 'Unpublished'; ?>
</a>
</td>
</tr>
<?php endforeach; ?>
<?php endif; ?>
</tbody>
</table>
</div>
</div>
<?php if ($pages > 1): ?>
<nav class="mt-3"><ul class="pagination pagination-sm justify-content-center">
<?php for ($p = 1; $p <= $pages; $p++): ?>
<li class="page-item<?php echo $p === $page ? ' active' : ''; ?>">
<a class="page-link" href="<?php echo Route::_('index.php?option=com_mokosuiteclient&view=templates&page=' . $p
. ($filters['search'] ? '&filter_search=' . urlencode($filters['search']) : '')
. ($filters['published'] !== '' ? '&filter_published=' . $filters['published'] : '')); ?>"><?php echo $p; ?></a>
</li>
<?php endfor; ?>
</ul></nav>
<?php endif; ?>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
document.querySelectorAll('.mokosuite-toggle-published').forEach(function(el) {
el.addEventListener('click', function(e) {
e.preventDefault();
var table = this.dataset.table, id = this.dataset.id, token = this.dataset.token, badge = this;
var fd = new FormData();
fd.append('table', table);
fd.append('id', id);
fd.append(token, '1');
fetch('index.php?option=com_mokosuiteclient&task=display.togglePublished&format=json', {
method: 'POST', headers: {'X-Requested-With': 'XMLHttpRequest'}, body: fd
}).then(function(r) { return r.json(); }).then(function(d) {
if (d.success) {
var pub = d.published;
badge.className = 'mokosuite-toggle-published badge bg-' + (pub ? 'success' : 'danger');
badge.textContent = pub ? 'Published' : 'Unpublished';
}
}).catch(function() {
badge.textContent = 'Error';
badge.className = 'mokosuite-toggle-published badge bg-warning text-dark';
});
});
});
});
</script>
@@ -126,20 +126,31 @@ document.addEventListener('DOMContentLoaded', function () {
fd.append(token, '1');
fetch(url, {method: 'POST', body: fd, headers: {'X-Requested-With': 'XMLHttpRequest'}})
.then(function (r) { return r.json(); })
.then(function (r) {
return r.text().then(function (text) {
try { return JSON.parse(text); }
catch (e) { return {success: false, message: 'Server error: ' + text.substring(0, 200)}; }
});
})
.then(function (d) {
var msg = d.message || (d.success ? 'Heartbeat sent to HQ.' : 'Heartbeat failed.');
if (d.success) {
Joomla.renderMessages({message: [d.message || 'Heartbeat sent to HQ.']});
if (icon) { icon.className = 'icon-check'; icon.style.color = '#198754'; }
Joomla.renderMessages({message: [msg]});
} else {
Joomla.renderMessages({error: [d.message || 'Heartbeat failed.']});
if (icon) { icon.className = 'icon-times'; icon.style.color = '#dc3545'; }
Joomla.renderMessages({error: [msg]});
}
})
.catch(function () {
Joomla.renderMessages({error: ['Network error sending heartbeat.']});
.catch(function (err) {
if (icon) { icon.className = 'icon-times'; icon.style.color = '#dc3545'; }
Joomla.renderMessages({error: ['Heartbeat failed: ' + (err.message || 'network error')]});
})
.finally(function () {
btn.disabled = false;
if (icon) icon.className = 'icon-upload';
setTimeout(function () {
if (icon) { icon.className = 'icon-upload'; icon.style.color = ''; }
}, 3000);
});
});
}
@@ -20,7 +20,7 @@
<license>GPL-3.0-or-later</license>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
<authorUrl>https://mokoconsulting.tech</authorUrl>
<version>02.48.52</version>
<version>02.52.11</version>
<description>MokoSuiteClient admin dashboard and REST API. Provides a control panel for managing MokoSuiteClient feature plugins, site health monitoring, and remote management endpoints.</description>
<namespace path="src">Moko\Component\MokoSuiteClient</namespace>
@@ -47,6 +47,12 @@
<menu link="option=com_mokosuiteclient&amp;view=waflog" img="class:shield-alt">COM_MOKOSUITECLIENT_MENU_WAFLOG</menu>
<menu link="option=com_mokosuiteclient&amp;view=database" img="class:database">COM_MOKOSUITECLIENT_MENU_DATABASE</menu>
<menu link="option=com_mokosuiteclient&amp;view=cleanup" img="class:trash">COM_MOKOSUITECLIENT_MENU_CLEANUP</menu>
<menu link="option=com_mokosuiteclient&amp;view=conditions" img="class:shuffle">COM_MOKOSUITECLIENT_MENU_CONDITIONS</menu>
<menu link="option=com_mokosuiteclient&amp;view=snippets" img="class:code">COM_MOKOSUITECLIENT_MENU_SNIPPETS</menu>
<menu link="option=com_mokosuiteclient&amp;view=templates" img="class:file-alt">COM_MOKOSUITECLIENT_MENU_TEMPLATES</menu>
<menu link="option=com_mokosuiteclient&amp;view=replacements" img="class:exchange-alt">COM_MOKOSUITECLIENT_MENU_REPLACEMENTS</menu>
<menu link="option=com_mokosuiteclient&amp;view=automation" img="class:random">COM_MOKOSUITECLIENT_MENU_AUTOMATION</menu>
<menu link="option=com_mokosuiteclient&amp;view=modules" img="class:th-large">COM_MOKOSUITECLIENT_MENU_MODULES</menu>
<menu link="option=com_plugins&amp;filter[folder]=system&amp;filter[search]=mokosuiteclient" img="class:power-off">COM_MOKOSUITECLIENT_MENU_PLUGINS</menu>
<menu link="option=com_installer&amp;view=update" img="class:refresh">COM_MOKOSUITECLIENT_MENU_UPDATES</menu>
<menu link="option=com_checkin" img="class:check-square">COM_MOKOSUITECLIENT_MENU_CHECKIN</menu>
@@ -7,7 +7,7 @@
<license>GPL-3.0-or-later</license>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
<authorUrl>https://mokoconsulting.tech</authorUrl>
<version>02.48.52</version>
<version>02.52.11</version>
<description>MOD_MOKOSUITECLIENT_CACHE_DESC</description>
<namespace path="src">Moko\Module\MokoSuiteClientCache</namespace>
@@ -7,7 +7,7 @@
<license>GPL-3.0-or-later</license>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
<authorUrl>https://mokoconsulting.tech</authorUrl>
<version>02.48.52</version>
<version>02.52.11</version>
<description>MOD_MOKOSUITECLIENT_CATEGORIES_DESC</description>
<namespace path="src">Moko\Module\MokoSuiteClientCategories</namespace>
@@ -7,7 +7,7 @@
<license>GPL-3.0-or-later</license>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
<authorUrl>https://mokoconsulting.tech</authorUrl>
<version>02.48.52</version>
<version>02.52.11</version>
<description>MOD_MOKOSUITECLIENT_CPANEL_DESC</description>
<namespace path="src">Moko\Module\MokoSuiteClientCpanel</namespace>
@@ -7,7 +7,7 @@
<license>GPL-3.0-or-later</license>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
<authorUrl>https://mokoconsulting.tech</authorUrl>
<version>02.48.52</version>
<version>02.52.11</version>
<description>MokoSuiteClient admin sidebar menu — renders a dedicated MokoSuiteClient section in the admin menu before Joomla's default menu.</description>
<namespace path="src">Moko\Module\MokoSuiteClientMenu</namespace>
@@ -28,6 +28,7 @@ $allViews = [
['icon' => 'fa-solid fa-file-lines', 'title' => 'Templates', 'link' => 'index.php?option=com_mokosuiteclient&view=templates', 'acl' => 'mokosuiteclient.templates.manage'],
['icon' => 'fa-solid fa-right-left', 'title' => 'Replacements', 'link' => 'index.php?option=com_mokosuiteclient&view=replacements','acl' => 'mokosuiteclient.replacements.manage'],
['icon' => 'fa-solid fa-shuffle', 'title' => 'Conditions', 'link' => 'index.php?option=com_mokosuiteclient&view=conditions', 'acl' => 'mokosuiteclient.conditions.manage'],
['icon' => 'icon-cube', 'title' => 'Modules', 'link' => 'index.php?option=com_mokosuiteclient&view=modules', 'acl' => 'core.admin'],
['icon' => 'icon-database', 'title' => 'Database Tools', 'link' => 'index.php?option=com_mokosuiteclient&view=database', 'acl' => 'core.admin'],
['icon' => 'icon-trash', 'title' => 'Cache Cleanup', 'link' => 'index.php?option=com_mokosuiteclient&view=cleanup', 'acl' => 'mokosuiteclient.cache'],
['icon' => 'icon-power-off', 'title' => 'Feature Plugins', 'link' => 'index.php?option=com_plugins&filter[folder]=system&filter[search]=mokosuiteclient', 'acl' => 'core.admin'],
@@ -42,9 +43,10 @@ $iconOverrides = [
'com_mokosuiteclient' => 'icon-shield-alt',
'com_mokosuitehq' => 'icon-tachometer-alt',
'com_mokosuitebackup' => 'icon-archive',
'com_mokosuitecrm' => 'icon-address-book',
'com_mokosuiteerp' => 'icon-briefcase',
'com_mokosuite_crm' => 'icon-address-book',
'com_mokosuite_erp' => 'icon-briefcase',
'com_mokosuiteshop' => 'icon-shopping-cart',
'com_mokoshop' => 'icon-shopping-cart',
'com_mokosuitepos' => 'icon-calculator',
'com_mokosuitemrp' => 'icon-cogs',
'com_mokosuitehrm' => 'icon-id-badge',
@@ -56,8 +58,24 @@ $iconOverrides = [
'com_mokosuiteforms' => 'icon-list-alt',
'com_mokosuitecommunity' => 'icon-comments',
'com_mokosuitecross' => 'icon-share-alt',
'com_mokoog' => 'icon-globe',
'com_mokosuiteopengraph' => 'icon-globe',
'com_mokosuitestorelocator' => 'icon-map-marker-alt',
'com_mokosuiteanalytics' => 'icon-chart-line',
'com_mokosuitesecurity' => 'icon-lock',
'com_mokosuitenotify' => 'icon-bell',
'com_mokosuiteworkflow' => 'icon-random',
'com_mokosuiteai' => 'icon-magic',
'com_mokosuiteauto' => 'icon-car',
'com_mokosuitebeauty' => 'icon-spa',
'com_mokosuiteconstruction' => 'icon-hard-hat',
'com_mokosuiteeditor' => 'icon-edit',
'com_mokosuiteevent' => 'icon-calendar',
'com_mokosuiteinsight' => 'icon-lightbulb',
'com_mokosuitelibrary' => 'icon-book',
'com_mokosuiterealty' => 'icon-home',
'com_mokosuitesupport' => 'icon-life-ring',
'com_mokosuitetaxi' => 'icon-taxi',
];
$childIconMap = [
@@ -262,7 +280,7 @@ $iconStyle = 'display:inline-block!important;width:1.25em;text-align:center;marg
$hasChildren = !empty($comp['children']);
?>
<?php if ($hasChildren): ?>
<li class="item parent item-level-2 mokosuiteclient-ext-item<?php echo $compActive ? ' mm-active' : ''; ?>">
<li class="item parent item-level-1 mokosuiteclient-ext-item<?php echo $compActive ? ' mm-active' : ''; ?>">
<a class="has-arrow<?php echo $compActive ? ' mm-active' : ''; ?>" href="#">
<span class="<?php echo htmlspecialchars($comp['icon'], ENT_QUOTES, 'UTF-8'); ?>" aria-hidden="true" style="<?php echo $iconStyle; ?>"></span>
<span class="sidebar-item-title"><?php echo htmlspecialchars($comp['title'], ENT_QUOTES, 'UTF-8'); ?></span>
@@ -282,7 +300,7 @@ $iconStyle = 'display:inline-block!important;width:1.25em;text-align:center;marg
: ($currentView === $childView);
}
?>
<li class="item mokosuiteclient-ext-child<?php echo $childActive ? ' mm-active' : ''; ?>">
<li class="item item-level-2 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 htmlspecialchars($child['icon'], ENT_QUOTES, 'UTF-8'); ?>" aria-hidden="true" style="<?php echo $iconStyle; ?>"></span>
<span class="sidebar-item-title"><?php echo htmlspecialchars($child['title'], ENT_QUOTES, 'UTF-8'); ?></span>
@@ -292,7 +310,7 @@ $iconStyle = 'display:inline-block!important;width:1.25em;text-align:center;marg
</ul>
</li>
<?php else: ?>
<li class="item mokosuiteclient-ext-item<?php echo $compActive ? ' mm-active' : ''; ?>">
<li class="item item-level-1 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 htmlspecialchars($comp['icon'], ENT_QUOTES, 'UTF-8'); ?>" aria-hidden="true" style="<?php echo $iconStyle; ?>"></span>
<span class="sidebar-item-title"><?php echo htmlspecialchars($comp['title'], ENT_QUOTES, 'UTF-8'); ?></span>
@@ -22,7 +22,7 @@
* DEFGROUP: Joomla.Plugin
* INGROUP: MokoSuiteClient
* REPO: https://github.com/mokoconsulting-tech/mokosuiteclient
* VERSION: 02.48.52
* VERSION: 02.52.11
* PATH: /src/Extension/MokoSuiteClient.php
* NOTE: Core system plugin for MokoSuiteClient admin tools suite
*/
@@ -8,7 +8,7 @@
* FILE INFORMATION
* DEFGROUP: Joomla.Plugin
* INGROUP: MokoSuiteClient
* VERSION: 02.48.52
* VERSION: 02.52.11
* PATH: /src/Field/ArticlesField.php
* BRIEF: List field that populates with published Joomla articles
*/
@@ -8,7 +8,7 @@
* FILE INFORMATION
* DEFGROUP: Joomla.Plugin
* INGROUP: MokoSuiteClient
* VERSION: 02.48.52
* VERSION: 02.52.11
* PATH: /src/Field/CopyableTokenField.php
* BRIEF: Read-only token field with a copy-to-clipboard button
*/
@@ -30,7 +30,7 @@
<license>GNU General Public License version 3 or later; see LICENSE.md</license>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
<authorUrl>https://mokoconsulting.tech</authorUrl>
<version>02.48.52</version>
<version>02.52.11</version>
<description>MokoSuiteClient core system plugin — coordinates feature plugins, heartbeat, health checks, and admin customizations.</description>
<namespace path=".">Moko\Plugin\System\MokoSuiteClient</namespace>
<scriptfile>script.php</scriptfile>
@@ -22,7 +22,7 @@
* DEFGROUP: Joomla.Plugin
* INGROUP: MokoSuiteClient
* REPO: https://github.com/mokoconsulting-tech/mokosuiteclient
* VERSION: 02.48.52
* VERSION: 02.52.11
* PATH: /src/script.php
* BRIEF: Installation script for MokoSuiteClient plugin
* NOTE: Handles installation, update, and uninstallation tasks including language override deployment
@@ -22,7 +22,7 @@
* DEFGROUP: Joomla.Plugin
* INGROUP: MokoSuiteClient
* REPO: https://github.com/mokoconsulting-tech/mokosuiteclient
* VERSION: 02.48.52
* VERSION: 02.52.11
* PATH: /src/services/provider.php
* BRIEF: Service provider for dependency injection in Joomla 5.x
* NOTE: Registers the plugin with Joomla's DI container
@@ -8,7 +8,7 @@
<license>GPL-3.0-or-later</license>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
<authorUrl>https://mokoconsulting.tech</authorUrl>
<version>02.48.52</version>
<version>02.52.11</version>
<description>PLG_SYSTEM_MOKOSUITECLIENT_BACKUP_DESC</description>
<namespace path="src">Moko\Plugin\System\MokoSuiteClientBackup</namespace>
@@ -8,7 +8,7 @@
<license>GPL-3.0-or-later</license>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
<authorUrl>https://mokoconsulting.tech</authorUrl>
<version>02.48.52</version>
<version>02.52.11</version>
<description>PLG_SYSTEM_MOKOSUITECLIENT_DBIP_DESC</description>
<namespace path="src">Moko\Plugin\System\MokoSuiteClientDBIP</namespace>
@@ -8,7 +8,7 @@
<license>GPL-3.0-or-later</license>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
<authorUrl>https://mokoconsulting.tech</authorUrl>
<version>02.48.52</version>
<version>02.52.11</version>
<description>PLG_SYSTEM_MOKOSUITECLIENT_DEVTOOLS_DESC</description>
<namespace path="src">Moko\Plugin\System\MokoSuiteClientDevTools</namespace>
@@ -8,7 +8,7 @@
<license>GPL-3.0-or-later</license>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
<authorUrl>https://mokoconsulting.tech</authorUrl>
<version>02.48.52</version>
<version>02.52.11</version>
<description>PLG_SYSTEM_MOKOSUITECLIENT_FIREWALL_DESC</description>
<namespace path="src">Moko\Plugin\System\MokoSuiteClientFirewall</namespace>
@@ -8,7 +8,7 @@
<license>GPL-3.0-or-later</license>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
<authorUrl>https://mokoconsulting.tech</authorUrl>
<version>02.48.52</version>
<version>02.52.11</version>
<description>PLG_SYSTEM_MOKOSUITECLIENT_LICENSE_DESC</description>
<namespace path="src">Moko\Plugin\System\MokoSuiteClientLicense</namespace>
<files><folder>src</folder><folder>services</folder><folder>language</folder></files>
@@ -8,7 +8,7 @@
<license>GPL-3.0-or-later</license>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
<authorUrl>https://mokoconsulting.tech</authorUrl>
<version>02.48.52</version>
<version>02.52.11</version>
<description>PLG_SYSTEM_MOKOSUITECLIENT_OFFLINE_DESC</description>
<namespace path="src">Moko\Plugin\System\MokoSuiteClientOffline</namespace>
@@ -8,7 +8,7 @@
<license>GPL-3.0-or-later</license>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
<authorUrl>https://mokoconsulting.tech</authorUrl>
<version>02.48.52</version>
<version>02.52.11</version>
<description>PLG_SYSTEM_MOKOSUITECLIENT_TENANT_DESC</description>
<namespace path="src">Moko\Plugin\System\MokoSuiteClientTenant</namespace>
@@ -12,7 +12,7 @@
<license>GNU General Public License version 3 or later; see LICENSE</license>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
<authorUrl>https://mokoconsulting.tech</authorUrl>
<version>02.48.52</version>
<version>02.52.11</version>
<description>PLG_TASK_MOKOSUITECLIENTDEMO_DESC</description>
<namespace path="src">Moko\Plugin\Task\MokoSuiteClientDemo</namespace>
@@ -10,7 +10,7 @@
* INGROUP: MokoSuiteClient
* REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoSuiteClient
* PATH: /src/packages/plg_system_mokosuiteclient/Service/DemoResetService.php
* VERSION: 02.48.52
* VERSION: 02.52.11
* BRIEF: Content-only snapshot/restore for demo site reset
*/
@@ -12,7 +12,7 @@
<license>GNU General Public License version 3 or later; see LICENSE</license>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
<authorUrl>https://mokoconsulting.tech</authorUrl>
<version>02.48.52</version>
<version>02.52.11</version>
<description>PLG_TASK_MOKOSUITECLIENTSYNC_DESC</description>
<namespace path="src">Moko\Plugin\Task\MokoSuiteClientSync</namespace>
@@ -10,7 +10,7 @@
* INGROUP: MokoSuiteClient
* REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoSuiteClient
* PATH: /src/packages/plg_system_mokosuiteclient/Service/ContentSyncReceiver.php
* VERSION: 02.48.52
* VERSION: 02.52.11
* BRIEF: Receiver-side content sync — applies incoming payload to local DB
*/
@@ -10,7 +10,7 @@
* INGROUP: MokoSuiteClient
* REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoSuiteClient
* PATH: /src/packages/plg_system_mokosuiteclient/Service/ContentSyncService.php
* VERSION: 02.48.52
* VERSION: 02.52.11
* BRIEF: Sender-side content sync — builds payload and pushes to remote sites
*/
@@ -7,7 +7,7 @@
<license>GPL-3.0-or-later</license>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
<authorUrl>https://mokoconsulting.tech</authorUrl>
<version>02.48.52</version>
<version>02.52.11</version>
<description>Joomla Web Services API routes for MokoSuiteClient site management — health checks, cache, updates, backups, and site info.</description>
<namespace path="src">Moko\Plugin\WebServices\MokoSuiteClient</namespace>
<files>
+2 -2
View File
@@ -2,7 +2,7 @@
<extension type="package" method="upgrade">
<name>Package - MokoSuiteClient</name>
<packagename>mokosuiteclient</packagename>
<version>02.48.52</version>
<version>02.52.11</version>
<creationDate>2026-06-02</creationDate>
<author>Moko Consulting</author>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
@@ -34,6 +34,6 @@
</files>
<updateservers>
<server type="extension" priority="1" name="Package - MokoSuiteClient">https://git.mokoconsulting.tech/MokoConsulting/MokoSuiteClient/updates.xml</server>
<server type="extension" name="MokoSuiteClient Updates">https://git.mokoconsulting.tech/MokoConsulting/MokoSuiteClient/raw/branch/main/updates.xml</server>
</updateservers>
</extension>
+20
View File
@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="UTF-8"?>
<updates>
<update>
<name>Package - MokoSuiteClient</name>
<element>pkg_mokosuiteclient</element>
<type>package</type>
<version>02.52.04</version>
<infourl title="Package - MokoSuiteClient">https://mokoconsulting.tech/support/mokosuiteclient</infourl>
<downloads>
<downloadurl type="full" format="zip">https://git.mokoconsulting.tech/MokoConsulting/MokoSuiteClient/releases/download/stable/pkg_mokosuiteclient-02.52.04.zip</downloadurl>
</downloads>
<tags>
<tag>stable</tag>
</tags>
<targetplatform name="joomla" version="6..*"></targetplatform>
<php_minimum>8.2</php_minimum>
<client>site</client>
<description>MokoSuiteClient - Joomla client-facing tracker and identity layer for the MokoSuite platform.</description>
</update>
</updates>