refactor: unify PIN UI into SupportPinHelper renderBadge/renderScript
Universal: Auto Version Bump / Version Bump (push) Successful in 9s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 29s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 47s
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (push) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (push) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (push) Has been cancelled
Platform: moko-platform CI / Gate 3: Self-Health Check (push) Has been cancelled
Platform: moko-platform CI / Gate 4: Governance (push) Has been cancelled
Platform: moko-platform CI / Gate 5: Template Integrity (push) Has been cancelled
Platform: moko-platform CI / CI Summary (push) Has been cancelled
Universal: Auto Version Bump / Version Bump (push) Successful in 9s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 29s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 47s
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (push) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (push) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (push) Has been cancelled
Platform: moko-platform CI / Gate 3: Self-Health Check (push) Has been cancelled
Platform: moko-platform CI / Gate 4: Governance (push) Has been cancelled
Platform: moko-platform CI / Gate 5: Template Integrity (push) Has been cancelled
Platform: moko-platform CI / CI Summary (push) Has been cancelled
Replaces 3 separate inline PIN implementations (dashboard, cpanel module, cache module) with shared helper methods. Adds click-to-copy on all PIN badges.
This commit is contained in:
@@ -31,6 +31,7 @@
|
||||
- **HQ config sync** — client stores HQ-configured `support_pin_hours` from heartbeat response, PIN TTL now configurable from HQ
|
||||
|
||||
### Changed
|
||||
- **Support PIN UI unified** — `SupportPinHelper::renderBadge()` and `renderScript()` replace 3 separate inline implementations (dashboard, cpanel module, cache module) with click-to-copy on all PIN badges
|
||||
- Admin sidebar menu module now loads component-local language files (fixes untranslated keys for MokoSuiteCross and other components)
|
||||
- Support PIN TTL is now configurable via HQ global options instead of hardcoded 72 hours
|
||||
- Removed MokoSuiteHQ from extension catalog (internal app, not for client sites)
|
||||
|
||||
@@ -115,6 +115,145 @@ class SupportPinHelper
|
||||
*
|
||||
* @return array{success: bool, pin?: string, message: string}
|
||||
*/
|
||||
/**
|
||||
* Render PIN badge HTML (active PIN with copy, or request button).
|
||||
*
|
||||
* @param array $state Result from getState().
|
||||
* @param string $token CSRF form token name.
|
||||
* @param string $context 'dashboard'|'cpanel'|'cache' — controls layout variant.
|
||||
*
|
||||
* @return string HTML fragment (no wrapping div).
|
||||
*/
|
||||
public static function renderBadge(array $state, string $token, string $context = 'dashboard'): string
|
||||
{
|
||||
if (!$state['available'])
|
||||
{
|
||||
return '';
|
||||
}
|
||||
|
||||
$requestUrl = \Joomla\CMS\Router\Route::_('index.php?option=com_mokosuiteclient&task=display.requestPin&format=json');
|
||||
$pin = $state['pin'];
|
||||
|
||||
$html = '';
|
||||
|
||||
if (!empty($pin))
|
||||
{
|
||||
$escaped = htmlspecialchars($pin, ENT_QUOTES, 'UTF-8');
|
||||
|
||||
if ($context === 'cache')
|
||||
{
|
||||
$html .= '<a href="#" class="btn btn-sm btn-outline-secondary rounded-0 border-end-0 d-flex align-items-center gap-1 px-3 py-2 mokosuiteclient-pin-copy" data-pin="' . $escaped . '" title="Support PIN — click to copy" style="font-size:0.8rem;">';
|
||||
$html .= '<span class="icon-key" aria-hidden="true"></span>';
|
||||
$html .= '<span class="mokosuiteclient-pin-text">' . $escaped . '</span>';
|
||||
$html .= '</a>';
|
||||
}
|
||||
else
|
||||
{
|
||||
$html .= '<span class="badge bg-dark mokosuiteclient-pin-copy" style="font-family:monospace;letter-spacing:0.08em;cursor:pointer;" title="Click to copy" data-pin="' . $escaped . '">';
|
||||
$html .= '<span class="icon-key small me-1" aria-hidden="true"></span>' . $escaped . '</span>';
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if ($context === 'cache')
|
||||
{
|
||||
$html .= '<a href="#" class="btn btn-sm btn-outline-secondary rounded-0 border-end-0 d-flex align-items-center gap-1 px-3 py-2 mokosuiteclient-pin-request" data-url="' . $requestUrl . '" data-token="' . $token . '" title="Request support PIN" style="font-size:0.8rem;">';
|
||||
$html .= '<span class="icon-key" aria-hidden="true"></span>';
|
||||
$html .= '<span class="mokosuiteclient-pin-text">PIN</span>';
|
||||
$html .= '</a>';
|
||||
}
|
||||
else
|
||||
{
|
||||
$html .= '<button type="button" class="btn btn-sm btn-outline-dark py-0 px-2 mokosuiteclient-pin-request" data-url="' . $requestUrl . '" data-token="' . $token . '" style="font-size:0.75rem;" title="Request a support PIN">';
|
||||
$html .= '<span class="icon-key" aria-hidden="true"></span> Request PIN</button>';
|
||||
}
|
||||
}
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render shared JS for PIN copy and request functionality.
|
||||
*
|
||||
* @return string <script> block.
|
||||
*/
|
||||
public static function renderScript(): string
|
||||
{
|
||||
return <<<'JS'
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
document.querySelectorAll('.mokosuiteclient-pin-copy').forEach(function(el) {
|
||||
el.addEventListener('click', function(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
var pin = this.dataset.pin;
|
||||
if (pin && navigator.clipboard) {
|
||||
var textEl = this.querySelector('.mokosuiteclient-pin-text');
|
||||
navigator.clipboard.writeText(pin).then(function() {
|
||||
if (textEl) {
|
||||
var orig = textEl.textContent;
|
||||
textEl.textContent = 'Copied!';
|
||||
setTimeout(function() { textEl.textContent = orig; }, 1500);
|
||||
} else {
|
||||
Joomla.renderMessages({message: ['PIN copied: ' + pin]});
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
document.querySelectorAll('.mokosuiteclient-pin-request').forEach(function(el) {
|
||||
el.addEventListener('click', function(e) {
|
||||
e.preventDefault();
|
||||
if (this.dataset.busy) return;
|
||||
this.dataset.busy = '1';
|
||||
var btn = this;
|
||||
var textEl = btn.querySelector('.mokosuiteclient-pin-text');
|
||||
var origHtml = btn.innerHTML;
|
||||
if (textEl) { textEl.textContent = '...'; } else { btn.textContent = '...'; }
|
||||
var fd = new FormData();
|
||||
fd.append(btn.dataset.token, '1');
|
||||
fetch(btn.dataset.url, {method:'POST', body:fd, headers:{'X-Requested-With':'XMLHttpRequest'}})
|
||||
.then(function(r) { return r.json(); })
|
||||
.then(function(data) {
|
||||
if (data.success && data.pin) {
|
||||
btn.classList.remove('mokosuiteclient-pin-request');
|
||||
btn.classList.add('mokosuiteclient-pin-copy');
|
||||
btn.dataset.pin = data.pin;
|
||||
btn.title = 'Click to copy';
|
||||
if (textEl) {
|
||||
textEl.textContent = data.pin;
|
||||
} else {
|
||||
btn.className = 'badge bg-dark mokosuiteclient-pin-copy';
|
||||
btn.style = 'font-family:monospace;letter-spacing:0.08em;cursor:pointer;';
|
||||
btn.innerHTML = '<span class="icon-key small me-1" aria-hidden="true"></span>' + data.pin;
|
||||
}
|
||||
btn.addEventListener('click', function(ev) {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
navigator.clipboard.writeText(data.pin).then(function() {
|
||||
var t = btn.querySelector('.mokosuiteclient-pin-text');
|
||||
if (t) { var o = t.textContent; t.textContent = 'Copied!'; setTimeout(function() { t.textContent = o; }, 1500); }
|
||||
else { Joomla.renderMessages({message: ['PIN copied: ' + data.pin]}); }
|
||||
});
|
||||
});
|
||||
} else {
|
||||
Joomla.renderMessages({error: [data.message || 'Failed to generate PIN']});
|
||||
btn.innerHTML = origHtml;
|
||||
}
|
||||
delete btn.dataset.busy;
|
||||
})
|
||||
.catch(function() {
|
||||
Joomla.renderMessages({error: ['Network error']});
|
||||
btn.innerHTML = origHtml;
|
||||
delete btn.dataset.busy;
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
JS;
|
||||
}
|
||||
|
||||
public static function requestNew(DatabaseInterface $db): array
|
||||
{
|
||||
$state = self::getState($db);
|
||||
|
||||
@@ -56,21 +56,17 @@ $actionLogsEnabled = Joomla\CMS\Component\ComponentHelper::isEnabled('com_action
|
||||
<span class="icon-shield-alt" aria-hidden="true" style="font-size:1.1rem;color:#1a2744"></span>
|
||||
<span class="fw-bold"><?php echo $this->escape($siteInfo->sitename); ?></span>
|
||||
<span class="badge bg-primary">MokoSuite <?php echo $this->escape($siteInfo->mokosuiteclient_version); ?></span>
|
||||
<?php echo \Moko\Component\MokoSuiteClient\Administrator\Helper\SupportPinHelper::renderBadge(
|
||||
['available' => !empty($this->supportPinAvailable), 'pin' => $this->supportPin ?? ''],
|
||||
$token, 'dashboard'
|
||||
); ?>
|
||||
<?php if (!empty($this->supportPin)): ?>
|
||||
<span class="badge bg-dark" style="font-family:monospace;letter-spacing:0.08em;cursor:help;" title="Support PIN — valid for 72 hours"><span class="icon-key small me-1" aria-hidden="true"></span><?php echo $this->escape($this->supportPin); ?></span>
|
||||
<button type="button" class="btn btn-sm btn-outline-primary py-0 px-1" id="mokosuiteclient-btn-heartbeat-pin"
|
||||
data-url="<?php echo Route::_('index.php?option=com_mokosuiteclient&task=display.sendHeartbeat&format=json'); ?>"
|
||||
data-token="<?php echo $token; ?>"
|
||||
title="Send heartbeat with PIN to MokoSuiteHQ">
|
||||
<span class="icon-upload" aria-hidden="true"></span>
|
||||
</button>
|
||||
<?php elseif (!empty($this->supportPinAvailable)): ?>
|
||||
<button type="button" class="btn btn-sm btn-outline-dark py-0 px-2" id="mokosuiteclient-request-pin"
|
||||
data-url="<?php echo Route::_('index.php?option=com_mokosuiteclient&task=display.requestPin&format=json'); ?>"
|
||||
data-token="<?php echo $token; ?>"
|
||||
style="font-size:0.75rem;" title="Request a support PIN (valid 72 hours)">
|
||||
<span class="icon-key" aria-hidden="true"></span> Request PIN
|
||||
</button>
|
||||
<?php endif; ?>
|
||||
<span class="badge bg-secondary">Joomla <?php echo $this->escape($siteInfo->joomla_version); ?></span>
|
||||
<span class="badge bg-secondary">PHP <?php echo $this->escape($siteInfo->php_version); ?></span>
|
||||
@@ -459,3 +455,5 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<?php echo \Moko\Component\MokoSuiteClient\Administrator\Helper\SupportPinHelper::renderScript(); ?>
|
||||
|
||||
@@ -144,43 +144,6 @@ document.addEventListener('DOMContentLoaded', function () {
|
||||
});
|
||||
}
|
||||
|
||||
// Request PIN button
|
||||
var pinBtn = document.getElementById('mokosuiteclient-request-pin');
|
||||
if (pinBtn) {
|
||||
pinBtn.addEventListener('click', function () {
|
||||
var btn = this;
|
||||
btn.disabled = true;
|
||||
btn.textContent = '...';
|
||||
var fd = new FormData();
|
||||
fd.append(btn.dataset.token, '1');
|
||||
fetch(btn.dataset.url, {method: 'POST', body: fd, headers: {'X-Requested-With': 'XMLHttpRequest'}})
|
||||
.then(function (r) { return r.json(); })
|
||||
.then(function (d) {
|
||||
if (d.success && d.pin) {
|
||||
var badge = document.createElement('span');
|
||||
badge.className = 'badge bg-dark';
|
||||
badge.style.cssText = 'font-family:monospace;letter-spacing:0.08em;cursor:help;';
|
||||
badge.title = 'Support PIN — valid for 72 hours';
|
||||
badge.textContent = d.pin;
|
||||
var icon = document.createElement('span');
|
||||
icon.className = 'icon-key small me-1';
|
||||
icon.setAttribute('aria-hidden', 'true');
|
||||
badge.prepend(icon);
|
||||
btn.replaceWith(badge);
|
||||
} else {
|
||||
Joomla.renderMessages({error: [d.message || 'Failed to generate PIN']});
|
||||
btn.disabled = false;
|
||||
btn.textContent = 'Request PIN';
|
||||
}
|
||||
})
|
||||
.catch(function () {
|
||||
Joomla.renderMessages({error: ['Network error']});
|
||||
btn.disabled = false;
|
||||
btn.textContent = 'Request PIN';
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Regular Labs import
|
||||
var rlBtn = document.getElementById('btn-import-regularlabs');
|
||||
if (rlBtn) {
|
||||
|
||||
@@ -12,7 +12,6 @@ use Joomla\CMS\Session\Session;
|
||||
$token = Session::getFormToken();
|
||||
$cacheUrl = 'index.php?option=com_mokosuiteclient&task=display.clearCache&format=json';
|
||||
$tempUrl = 'index.php?option=com_mokosuiteclient&task=display.clearTemp&format=json';
|
||||
$pinUrl = 'index.php?option=com_mokosuiteclient&task=display.requestPin&format=json';
|
||||
$pinAvailable = $supportPinAvailable ?? false;
|
||||
$pin = $supportPin ?? '';
|
||||
$frontendUrl = $frontendUrl ?? '';
|
||||
@@ -25,12 +24,16 @@ $frontendUrl = $frontendUrl ?? '';
|
||||
<span class="icon-external-link-alt" aria-hidden="true"></span> Site
|
||||
</a>
|
||||
<?php endif; ?>
|
||||
<?php if ($pinAvailable): ?>
|
||||
<a href="#" class="btn btn-sm btn-outline-secondary <?php echo $frontendUrl ? 'rounded-0 border-end-0' : 'rounded-0 rounded-start border-end-0'; ?> d-flex align-items-center gap-1 px-3 py-2" id="mokosuiteclient-pin" title="<?php echo $pin ? 'Support PIN — click to copy' : 'Request support PIN'; ?>" style="font-size:0.8rem;">
|
||||
<span class="icon-key" aria-hidden="true" id="mokosuiteclient-pin-icon"></span>
|
||||
<span id="mokosuiteclient-pin-text"><?php echo $pin ? htmlspecialchars($pin) : 'PIN'; ?></span>
|
||||
</a>
|
||||
<?php endif; ?>
|
||||
<?php if ($pinAvailable):
|
||||
$pinHtml = \Moko\Component\MokoSuiteClient\Administrator\Helper\SupportPinHelper::renderBadge(
|
||||
['available' => true, 'pin' => $pin],
|
||||
$token, 'cache'
|
||||
);
|
||||
if (!$frontendUrl) {
|
||||
$pinHtml = str_replace('rounded-0 border-end-0', 'rounded-0 rounded-start border-end-0', $pinHtml);
|
||||
}
|
||||
echo $pinHtml;
|
||||
endif; ?>
|
||||
<a href="#" class="btn btn-sm btn-outline-primary <?php echo ($pinAvailable || $frontendUrl) ? 'rounded-0 border-end-0' : 'rounded-0 rounded-start border-end-0'; ?> d-flex align-items-center gap-1 px-3 py-2" id="mokosuiteclient-clear-cache" title="Clear all Joomla cache" style="font-size:0.8rem;">
|
||||
<span class="icon-bolt" aria-hidden="true" id="mokosuiteclient-cache-icon"></span> Cache
|
||||
</a>
|
||||
@@ -92,55 +95,6 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
setupCleaner('mokosuiteclient-clear-cache', 'mokosuiteclient-cache-icon', '<?php echo $cacheUrl; ?>', '<?php echo $token; ?>');
|
||||
setupCleaner('mokosuiteclient-clear-temp', 'mokosuiteclient-temp-icon', '<?php echo $tempUrl; ?>', '<?php echo $token; ?>');
|
||||
|
||||
// Support PIN button
|
||||
var pinBtn = document.getElementById('mokosuiteclient-pin');
|
||||
var pinIcon = document.getElementById('mokosuiteclient-pin-icon');
|
||||
var pinText = document.getElementById('mokosuiteclient-pin-text');
|
||||
if (pinBtn && pinText) {
|
||||
pinBtn.addEventListener('click', function(e) {
|
||||
e.preventDefault();
|
||||
var current = pinText.textContent.trim();
|
||||
|
||||
if (current.indexOf('MOKO-') === 0) {
|
||||
navigator.clipboard.writeText(current).then(function() {
|
||||
var orig = pinText.textContent;
|
||||
pinText.textContent = 'Copied!';
|
||||
setTimeout(function() { pinText.textContent = orig; }, 1500);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (pinBtn.dataset.busy) return;
|
||||
pinBtn.dataset.busy = '1';
|
||||
if (pinIcon) pinIcon.className = 'icon-spinner icon-spin';
|
||||
pinText.textContent = '...';
|
||||
|
||||
var fd = new FormData();
|
||||
fd.append('<?php echo $token; ?>', '1');
|
||||
|
||||
fetch('<?php echo $pinUrl; ?>', {
|
||||
method: 'POST',
|
||||
headers: {'X-Requested-With': 'XMLHttpRequest'},
|
||||
body: fd
|
||||
})
|
||||
.then(function(r) { return r.json(); })
|
||||
.then(function(data) {
|
||||
if (data.success && data.pin) {
|
||||
pinText.textContent = data.pin;
|
||||
pinBtn.title = 'Support PIN — click to copy';
|
||||
if (pinIcon) pinIcon.className = 'icon-key';
|
||||
} else {
|
||||
pinText.textContent = 'PIN';
|
||||
if (pinIcon) pinIcon.className = 'icon-key';
|
||||
}
|
||||
delete pinBtn.dataset.busy;
|
||||
})
|
||||
.catch(function() {
|
||||
pinText.textContent = 'PIN';
|
||||
if (pinIcon) pinIcon.className = 'icon-key';
|
||||
delete pinBtn.dataset.busy;
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
<?php echo \Moko\Component\MokoSuiteClient\Administrator\Helper\SupportPinHelper::renderScript(); ?>
|
||||
|
||||
@@ -70,16 +70,10 @@ $diskColor = ($diskPct !== null && $diskPct > 90) ? 'bg-danger' : (($diskPct !==
|
||||
<?php endif; ?>
|
||||
<span class="fw-bold"><?php echo htmlspecialchars($siteInfo->sitename ?? ''); ?></span>
|
||||
<span class="badge bg-primary">MokoSuite <?php echo htmlspecialchars($siteInfo->mokosuiteclient_version ?? ''); ?></span>
|
||||
<?php if (!empty($supportPin)): ?>
|
||||
<span class="badge bg-dark" style="font-family:monospace;letter-spacing:0.08em;cursor:help;" title="Support PIN — valid for 72 hours"><span class="icon-key small me-1" aria-hidden="true"></span><?php echo htmlspecialchars($supportPin); ?></span>
|
||||
<?php elseif (!empty($supportPinAvailable)): ?>
|
||||
<button type="button" class="btn btn-sm btn-outline-dark py-0 px-2" id="mokosuiteclient-request-pin"
|
||||
data-url="<?php echo Route::_('index.php?option=com_mokosuiteclient&task=display.requestPin&format=json'); ?>"
|
||||
data-token="<?php echo Session::getFormToken(); ?>"
|
||||
style="font-size:0.75rem;" title="Request a support PIN (valid 72 hours)">
|
||||
<span class="icon-key" aria-hidden="true"></span> Request PIN
|
||||
</button>
|
||||
<?php endif; ?>
|
||||
<?php echo \Moko\Component\MokoSuiteClient\Administrator\Helper\SupportPinHelper::renderBadge(
|
||||
['available' => !empty($supportPinAvailable), 'pin' => $supportPin ?? ''],
|
||||
$token, 'cpanel'
|
||||
); ?>
|
||||
<span class="badge bg-secondary">Joomla <?php echo htmlspecialchars($siteInfo->joomla_version ?? ''); ?></span>
|
||||
<span class="badge bg-secondary">PHP <?php echo htmlspecialchars($siteInfo->php_version ?? ''); ?></span>
|
||||
<span class="badge bg-secondary"><?php echo htmlspecialchars($siteInfo->db_type ?? ''); ?></span>
|
||||
@@ -95,37 +89,4 @@ $diskColor = ($diskPct !== null && $diskPct > 90) ? 'bg-danger' : (($diskPct !==
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
var btn = document.getElementById('mokosuiteclient-request-pin');
|
||||
if (!btn) return;
|
||||
btn.addEventListener('click', function() {
|
||||
var el = this;
|
||||
el.disabled = true;
|
||||
el.textContent = '...';
|
||||
var fd = new FormData();
|
||||
fd.append(el.dataset.token, '1');
|
||||
fetch(el.dataset.url, {method:'POST', body:fd, headers:{'X-Requested-With':'XMLHttpRequest'}})
|
||||
.then(function(r){return r.json()})
|
||||
.then(function(d){
|
||||
if (d.success && d.pin) {
|
||||
var badge = document.createElement('span');
|
||||
badge.className = 'badge bg-dark';
|
||||
badge.style = 'font-family:monospace;letter-spacing:0.08em;cursor:help;';
|
||||
badge.title = 'Support PIN — valid for 72 hours';
|
||||
badge.innerHTML = '<span class="icon-key small me-1" aria-hidden="true"></span>' + d.pin;
|
||||
el.replaceWith(badge);
|
||||
} else {
|
||||
Joomla.renderMessages({error:[d.message||'Failed to generate PIN']});
|
||||
el.disabled = false;
|
||||
el.innerHTML = '<span class="icon-key" aria-hidden="true"></span> Request PIN';
|
||||
}
|
||||
})
|
||||
.catch(function(){
|
||||
Joomla.renderMessages({error:['Network error']});
|
||||
el.disabled = false;
|
||||
el.innerHTML = '<span class="icon-key" aria-hidden="true"></span> Request PIN';
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
<?php echo \Moko\Component\MokoSuiteClient\Administrator\Helper\SupportPinHelper::renderScript(); ?>
|
||||
|
||||
Reference in New Issue
Block a user