feat(demo): multi-select list for tables, formatted next-reset with timezone
Generic: Repo Health / Site Health (push) Has been cancelled
Generic: Repo Health / Access control (push) Has been cancelled
Universal: Auto Version Bump / Version Bump (push) Has been cancelled
Update Server / Update Server (push) Has been cancelled
Generic: Repo Health / Release configuration (push) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled

- SnapshotTablesField now renders as a multi-select <select> with
  optgroups (Content, Users, Menus, Modules, Assets, Other) instead
  of individual checkboxes
- NextResetField displays the next reset as a formatted datetime in
  the site's configured timezone with a relative badge (in X hours)

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Jonathan Miller
2026-05-30 19:38:29 -05:00
parent 1713388b7d
commit e0eee892d0
3 changed files with 143 additions and 49 deletions
@@ -0,0 +1,93 @@
<?php
/**
* @package MokoWaaS
* @subpackage plg_system_mokowaas
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GNU General Public License version 3 or later; see LICENSE
*
* FILE INFORMATION
* DEFGROUP: Joomla.Plugin
* INGROUP: MokoWaaS
* VERSION: 02.26.00
* PATH: /src/Field/NextResetField.php
* BRIEF: Read-only field that displays the next scheduled reset in the site timezone
*/
namespace Moko\Plugin\System\MokoWaaS\Field;
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use Joomla\CMS\Form\FormField;
/**
* Displays the next scheduled demo reset as a formatted datetime
* in the Joomla site timezone with a relative "in X hours" suffix.
*
* @since 02.26.00
*/
class NextResetField extends FormField
{
protected $type = 'NextReset';
protected function getInput()
{
if (empty($this->value))
{
return '<div class="alert alert-secondary mb-0 py-2">No reset scheduled — save the plugin config to calculate.</div>';
}
$utcTimestamp = strtotime($this->value);
if ($utcTimestamp === false || $utcTimestamp <= 0)
{
return '<div class="alert alert-warning mb-0 py-2">Invalid timestamp stored.</div>';
}
// Convert to site timezone
$siteTimezone = Factory::getApplication()->get('offset', 'UTC');
try
{
$dt = new \DateTime('@' . $utcTimestamp);
$dt->setTimezone(new \DateTimeZone($siteTimezone));
$formatted = $dt->format('l, F j, Y \a\t g:i A T');
}
catch (\Throwable $e)
{
$formatted = gmdate('Y-m-d H:i:s', $utcTimestamp) . ' UTC';
}
// Calculate relative time
$diff = $utcTimestamp - time();
$relative = '';
if ($diff <= 0)
{
$relative = '<span class="badge bg-warning text-dark">overdue — save to recalculate</span>';
}
elseif ($diff < 3600)
{
$mins = (int) ceil($diff / 60);
$relative = '<span class="badge bg-info">in ' . $mins . ' minute' . ($mins !== 1 ? 's' : '') . '</span>';
}
elseif ($diff < 86400)
{
$hours = round($diff / 3600, 1);
$relative = '<span class="badge bg-info">in ' . $hours . ' hour' . ($hours != 1 ? 's' : '') . '</span>';
}
else
{
$days = round($diff / 86400, 1);
$relative = '<span class="badge bg-secondary">in ' . $days . ' day' . ($days != 1 ? 's' : '') . '</span>';
}
return '<div class="d-flex align-items-center gap-2">'
. '<span class="form-control-plaintext" style="font-weight:500">'
. '<span class="icon-calendar" aria-hidden="true"></span> '
. htmlspecialchars($formatted) . '</span> '
. $relative
. '<input type="hidden" name="' . $this->name . '" value="' . htmlspecialchars($this->value) . '" />'
. '</div>';
}
}
@@ -8,9 +8,9 @@
* FILE INFORMATION
* DEFGROUP: Joomla.Plugin
* INGROUP: MokoWaaS
* VERSION: 02.25.03
* VERSION: 02.26.00
* PATH: /src/Field/SnapshotTablesField.php
* BRIEF: Multi-select field that loads DB tables with sensible defaults pre-checked
* BRIEF: Multi-select list field that loads DB tables with sensible defaults
*/
namespace Moko\Plugin\System\MokoWaaS\Field;
@@ -18,16 +18,16 @@ namespace Moko\Plugin\System\MokoWaaS\Field;
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use Joomla\CMS\Form\Field\CheckboxesField;
use Joomla\CMS\Form\Field\ListField;
use Joomla\CMS\HTML\HTMLHelper;
/**
* Renders a checkbox list of all Joomla database tables, with content-related
* tables pre-selected by default. Tables are grouped by category (content,
* users, menus, modules, other).
* Renders a multi-select list box of all Joomla database tables, with
* content-related tables pre-selected by default.
*
* @since 02.25.00
* @since 02.26.00
*/
class SnapshotTablesField extends CheckboxesField
class SnapshotTablesField extends ListField
{
protected $type = 'SnapshotTables';
@@ -56,17 +56,17 @@ class SnapshotTablesField extends CheckboxesField
];
/**
* Group labels for table categorisation.
* Table suffixes grouped by category for optgroup display.
*
* @var array
* @since 02.25.00
*/
private const TABLE_GROUPS = [
'content' => ['content', 'categories', 'fields', 'tags', 'contentitem_tag_map', 'ucm_content', 'ucm_history'],
'users' => ['users', 'user_usergroup_map', 'user_profiles', 'usergroups', 'user_keys', 'user_mfa'],
'menus' => ['menu', 'menu_types'],
'modules' => ['modules', 'modules_menu'],
'assets' => ['assets'],
'Content' => ['content', 'categories', 'fields', 'fields_values', 'fields_groups', 'tags', 'contentitem_tag_map', 'ucm_content', 'ucm_history'],
'Users' => ['users', 'user_usergroup_map', 'user_profiles', 'usergroups', 'user_keys', 'user_mfa'],
'Menus' => ['menu', 'menu_types'],
'Modules' => ['modules', 'modules_menu'],
'Assets' => ['assets'],
];
protected function getOptions()
@@ -75,68 +75,65 @@ class SnapshotTablesField extends CheckboxesField
$prefix = $db->getPrefix();
$tables = $db->getTableList();
$options = [];
$grouped = [];
foreach ($tables as $table)
{
// Only show tables with the site's prefix
if (strpos($table, $prefix) !== 0)
{
continue;
}
// Convert real table name to #__ notation
$logical = '#__' . substr($table, strlen($prefix));
$suffix = substr($table, strlen($prefix));
$logical = '#__' . $suffix;
// Determine group for display ordering
$group = 'Other';
foreach (self::TABLE_GROUPS as $groupName => $patterns)
{
$suffix = substr($table, strlen($prefix));
foreach ($patterns as $pattern)
if (in_array($suffix, $patterns, true))
{
if ($suffix === $pattern)
{
$group = ucfirst($groupName);
break 2;
}
$group = $groupName;
break;
}
}
$obj = (object) [
'value' => $logical,
'text' => $logical,
'disable' => false,
'class' => '',
'onclick' => '',
];
$options[$group][] = $obj;
$grouped[$group][] = HTMLHelper::_('select.option', $logical, $logical);
}
// Flatten with group headers: content tables first, then alphabetical
// Build options with optgroups: priority groups first
$options = [];
$priority = ['Content', 'Users', 'Menus', 'Modules', 'Assets'];
$sorted = [];
foreach ($priority as $g)
{
if (isset($options[$g]))
if (!empty($grouped[$g]))
{
$sorted = array_merge($sorted, $options[$g]);
unset($options[$g]);
$options[] = HTMLHelper::_('select.optgroup', '— ' . $g . ' —');
foreach ($grouped[$g] as $opt)
{
$options[] = $opt;
}
$options[] = HTMLHelper::_('select.optgroup', '— ' . $g . ' —');
unset($grouped[$g]);
}
}
// Remaining tables (Other)
if (isset($options['Other']))
if (!empty($grouped['Other']))
{
sort($options['Other']);
$sorted = array_merge($sorted, $options['Other']);
$options[] = HTMLHelper::_('select.optgroup', '— Other');
foreach ($grouped['Other'] as $opt)
{
$options[] = $opt;
}
$options[] = HTMLHelper::_('select.optgroup', '— Other —');
}
return $sorted;
return $options;
}
protected function getInput()
@@ -148,10 +145,12 @@ class SnapshotTablesField extends CheckboxesField
}
elseif (is_string($this->value))
{
// Handle legacy textarea format (newline-separated)
$this->value = array_filter(array_map('trim', explode("\n", $this->value)));
}
// Force multiple attribute
$this->__set('multiple', true);
return parent::getInput();
}
}
@@ -314,13 +314,15 @@
description="PLG_SYSTEM_MOKOWAAS_DEMO_CRON_DESC"
default="" hint="min hour day month weekday (e.g. 0 */6 * * *)"
showon="demo_reset_schedule:custom" />
<field name="demo_next_reset" type="text"
<field name="demo_next_reset" type="NextReset"
label="PLG_SYSTEM_MOKOWAAS_DEMO_NEXT_RESET_LABEL"
description="PLG_SYSTEM_MOKOWAAS_DEMO_NEXT_RESET_DESC"
readonly="true" default="" />
/>
<field name="demo_snapshot_tables" type="SnapshotTables"
label="PLG_SYSTEM_MOKOWAAS_DEMO_TABLES_LABEL"
description="PLG_SYSTEM_MOKOWAAS_DEMO_TABLES_DESC"
multiple="true"
size="15"
/>
<field name="demo_snapshot_include_media" type="checkboxes"
label="PLG_SYSTEM_MOKOWAAS_DEMO_MEDIA_LABEL"