feat: JS placeholder resolution in FolderPicker for portable profiles
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
Generic: Repo Health / Report Issues (push) Has been cancelled
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
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
Generic: Repo Health / Report Issues (push) Has been cancelled
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
FolderPickerField now resolves [site_name], [host], [date], etc. in JavaScript before browsing directories, and reverse-replaces actual values back to placeholders when user selects a path. This makes profiles portable across sites. Default placeholder hint uses [host] for directory paths. Authored-by: Moko Consulting Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -12,6 +12,7 @@ namespace Joomla\Component\MokoJoomBackup\Administrator\Field;
|
||||
|
||||
defined('_JEXEC') or die;
|
||||
|
||||
use Joomla\CMS\Factory;
|
||||
use Joomla\CMS\Form\FormField;
|
||||
use Joomla\CMS\Language\Text;
|
||||
|
||||
@@ -35,13 +36,43 @@ class FolderPickerField extends FormField
|
||||
$absPath = $rawValue;
|
||||
}
|
||||
|
||||
// If path contains placeholders, show info status instead of checking disk
|
||||
// Build placeholder map for JS resolution
|
||||
$hostname = preg_replace('/[^a-zA-Z0-9._-]/', '', $_SERVER['HTTP_HOST'] ?? $_SERVER['SERVER_NAME'] ?? php_uname('n'));
|
||||
$siteName = '';
|
||||
|
||||
try {
|
||||
$siteName = Factory::getApplication()->get('sitename', '');
|
||||
} catch (\Throwable $e) {
|
||||
// fallback
|
||||
}
|
||||
|
||||
$sanitizedSiteName = preg_replace('/[^a-zA-Z0-9._-]/', '', str_replace(' ', '-', trim($siteName)));
|
||||
|
||||
$placeholders = [
|
||||
'[host]' => $hostname,
|
||||
'[site_name]' => $sanitizedSiteName ?: 'joomla',
|
||||
'[profile_id]' => '1',
|
||||
'[profile_name]' => 'default',
|
||||
'[type]' => 'full',
|
||||
'[year]' => date('Y'),
|
||||
'[month]' => date('m'),
|
||||
'[day]' => date('d'),
|
||||
'[date]' => date('Ymd'),
|
||||
];
|
||||
|
||||
$placeholdersJson = json_encode($placeholders);
|
||||
|
||||
// Resolve placeholders for the status display
|
||||
$resolvedPath = str_replace(array_keys($placeholders), array_values($placeholders), $absPath);
|
||||
$hasPlaceholders = preg_match('/\[.+\]/', $absPath);
|
||||
|
||||
if ($hasPlaceholders) {
|
||||
$statusClass = 'text-info';
|
||||
$statusIcon = 'icon-info-circle';
|
||||
$exists = is_dir($resolvedPath);
|
||||
$statusClass = $exists ? 'text-success' : 'text-info';
|
||||
$statusIcon = $exists ? 'icon-publish' : 'icon-info-circle';
|
||||
$statusText = Text::_('COM_MOKOJOOMBACKUP_FOLDER_PLACEHOLDER');
|
||||
$resolvedSafe = htmlspecialchars($resolvedPath, ENT_QUOTES, 'UTF-8');
|
||||
$statusDetail = "{$statusText}: <code>{$resolvedSafe}</code>";
|
||||
} else {
|
||||
$exists = is_dir($absPath);
|
||||
$statusClass = $exists ? 'text-success' : 'text-danger';
|
||||
@@ -49,24 +80,24 @@ class FolderPickerField extends FormField
|
||||
$statusText = $exists
|
||||
? Text::_('COM_MOKOJOOMBACKUP_FOLDER_EXISTS')
|
||||
: Text::_('COM_MOKOJOOMBACKUP_FOLDER_NOT_FOUND');
|
||||
$absPathSafe = htmlspecialchars($absPath, ENT_QUOTES, 'UTF-8');
|
||||
$statusDetail = "{$statusText}: <code>{$absPathSafe}</code>";
|
||||
}
|
||||
|
||||
$absPathSafe = htmlspecialchars($absPath, ENT_QUOTES, 'UTF-8');
|
||||
|
||||
return <<<HTML
|
||||
<div class="input-group">
|
||||
<input type="text" name="{$name}" id="{$id}" value="{$value}"
|
||||
class="form-control" maxlength="512"
|
||||
placeholder="/home/user/backups or administrator/components/com_mokojoombackup/backups" />
|
||||
placeholder="/home/user/backups/[host] or administrator/components/com_mokojoombackup/backups" />
|
||||
<button type="button" class="btn btn-outline-secondary" id="{$id}_btn">
|
||||
<span class="icon-folder-open" aria-hidden="true"></span>
|
||||
Browse
|
||||
</button>
|
||||
</div>
|
||||
<div class="mt-1">
|
||||
<div class="mt-1" id="{$id}_status">
|
||||
<small class="{$statusClass}">
|
||||
<span class="{$statusIcon}" aria-hidden="true"></span>
|
||||
{$statusText}: <code>{$absPathSafe}</code>
|
||||
{$statusDetail}
|
||||
</small>
|
||||
</div>
|
||||
<div id="{$id}_browser" class="card mt-2" style="display:none; max-height:300px; overflow-y:auto;">
|
||||
@@ -81,6 +112,25 @@ class FolderPickerField extends FormField
|
||||
var browser = document.getElementById(fieldId + '_browser');
|
||||
var tree = document.getElementById(fieldId + '_tree');
|
||||
var input = document.getElementById(fieldId);
|
||||
var placeholders = {$placeholdersJson};
|
||||
|
||||
// Resolve placeholders in a path (forward: [site_name] -> actual value)
|
||||
function resolve(path) {
|
||||
for (var key in placeholders) {
|
||||
path = path.split(key).join(placeholders[key]);
|
||||
}
|
||||
return path;
|
||||
}
|
||||
|
||||
// Reverse-replace actual values back to placeholders for portable storage
|
||||
function unresolve(path) {
|
||||
for (var key in placeholders) {
|
||||
if (placeholders[key] && placeholders[key].length > 1) {
|
||||
path = path.split(placeholders[key]).join(key);
|
||||
}
|
||||
}
|
||||
return path;
|
||||
}
|
||||
|
||||
btn.addEventListener('click', function() {
|
||||
if (browser.style.display !== 'none') {
|
||||
@@ -88,7 +138,8 @@ class FolderPickerField extends FormField
|
||||
return;
|
||||
}
|
||||
browser.style.display = 'block';
|
||||
loadDir(input.value || '/');
|
||||
// Resolve placeholders before browsing so the server sees real paths
|
||||
loadDir(resolve(input.value || '/'));
|
||||
});
|
||||
|
||||
function loadDir(path) {
|
||||
@@ -152,12 +203,13 @@ class FolderPickerField extends FormField
|
||||
item.appendChild(document.createTextNode(' ' + dir.name));
|
||||
item.addEventListener('click', function(e) {
|
||||
e.preventDefault();
|
||||
input.value = dir.path;
|
||||
// Store with placeholders reversed back in
|
||||
input.value = unresolve(dir.path);
|
||||
loadDir(dir.path);
|
||||
});
|
||||
item.addEventListener('dblclick', function(e) {
|
||||
e.preventDefault();
|
||||
input.value = dir.path;
|
||||
input.value = unresolve(dir.path);
|
||||
browser.style.display = 'none';
|
||||
});
|
||||
list.appendChild(item);
|
||||
@@ -171,6 +223,13 @@ class FolderPickerField extends FormField
|
||||
small.className = 'text-muted';
|
||||
small.textContent = 'Current: ' + (data.current || path);
|
||||
info.appendChild(small);
|
||||
|
||||
// Show what will be stored (with placeholders)
|
||||
var stored = document.createElement('small');
|
||||
stored.className = 'text-info d-block';
|
||||
stored.textContent = 'Stored as: ' + unresolve(data.current || path);
|
||||
info.appendChild(stored);
|
||||
|
||||
tree.appendChild(info);
|
||||
}
|
||||
})();
|
||||
|
||||
Reference in New Issue
Block a user