feat(modules): enhance module upload UI with drag-and-drop support and AJAX handling

This commit is contained in:
Divarion-D
2026-04-11 22:46:06 +03:00
parent d22b47bb65
commit 84cab831bf
2 changed files with 128 additions and 36 deletions
@@ -13,6 +13,9 @@ class ModulesController extends BaseAdminController {
$flash = null;
if (strtoupper($_SERVER['REQUEST_METHOD'] ?? 'GET') === 'POST') {
$isAjax = !empty($_SERVER['HTTP_X_REQUESTED_WITH'])
&& strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) === 'xmlhttprequest';
try {
$action = (string) $this->input('module_action', '');
$name = (string) $this->input('module_name', '');
@@ -64,6 +67,11 @@ class ModulesController extends BaseAdminController {
} catch (Throwable $e) {
$flash = ['type' => 'danger', 'message' => $e->getMessage()];
}
if ($isAjax) {
header('Content-Type: application/json');
exit(json_encode($flash ?: ['type' => 'info', 'message' => 'No action taken']));
}
}
$modules = $manager->listModules();
+120 -36
View File
@@ -45,20 +45,24 @@
<div class="col-xl-12">
<div class="card">
<div class="card-body">
<h5 class="card-title">Upload Module ZIP</h5>
<p class="text-muted">Upload a module ZIP package and install it immediately.</p>
<h5 class="card-title"><i class="mdi mdi-package-variant-closed mr-1"></i> Upload Module ZIP</h5>
<form action="#" method="POST" enctype="multipart/form-data" class="mb-4">
<input type="hidden" name="module_action" value="upload_install" />
<div class="form-row align-items-center">
<div class="col-md-8 mb-2">
<input type="file" class="form-control" name="module_zip" accept=".zip" required>
</div>
<div class="col-md-4 mb-2 text-right">
<button type="submit" class="btn btn-primary">Upload & Install</button>
<div class="p-3 border border-dashed rounded text-center" id="module-drop-zone" style="border-style: dashed !important; border-width: 2px !important; cursor: pointer; transition: background-color 0.2s;">
<i class="mdi mdi-cloud-upload-outline d-block mb-2" style="font-size: 2.5rem; color: #6c757d;"></i>
<p class="text-muted mb-2">Drag & drop a <code>.zip</code> module here or click to browse</p>
<div class="custom-file mx-auto" style="max-width: 400px;">
<input type="file" class="custom-file-input" name="module_zip" id="module_zip_input" accept=".zip" required>
<label class="custom-file-label" for="module_zip_input" data-browse="Browse">Choose file...</label>
</div>
<small class="text-muted d-block mt-2">Only <code>.zip</code> packages are accepted</small>
</div>
<div class="text-right mt-3">
<button type="submit" class="btn btn-primary" id="module_upload_btn" disabled>
<i class="mdi mdi-upload mr-1"></i> Upload &amp; Install
</button>
</div>
</form>
<div class="table-responsive">
<table class="table table-striped table-borderless mb-0">
<thead>
@@ -84,44 +88,36 @@
<td><?= htmlspecialchars($module['version'] ?: '-') ?></td>
<td><?= htmlspecialchars($module['requires_core'] ?: '-') ?></td>
<td>
<?php if (!empty($module['enabled'])): ?>
<span class="badge badge-success">Enabled</span>
<?php else: ?>
<span class="badge badge-secondary">Disabled</span>
<?php endif; ?>
<span class="badge module-status-badge <?= !empty($module['enabled']) ? 'badge-success' : 'badge-secondary' ?>"
data-module="<?= htmlspecialchars($module['name']) ?>">
<?= !empty($module['enabled']) ? 'Enabled' : 'Disabled' ?>
</span>
</td>
<td class="text-right">
<div class="btn-group" role="group">
<form action="#" method="POST" class="mr-1">
<input type="hidden" name="module_name" value="<?= htmlspecialchars($module['name']) ?>">
<input type="hidden" name="module_action" value="install">
<button type="submit" class="btn btn-sm btn-primary">Install</button>
<button type="submit" class="btn btn-sm btn-primary"></i>Install</button>
</form>
<form action="#" method="POST" class="mr-1">
<input type="hidden" name="module_name" value="<?= htmlspecialchars($module['name']) ?>">
<input type="hidden" name="module_action" value="update">
<button type="submit" class="btn btn-sm btn-info">Update</button>
<button type="submit" class="btn btn-sm btn-info"></i>Update</button>
</form>
<?php if (!empty($module['enabled'])): ?>
<form action="#" method="POST" class="mr-1">
<input type="hidden" name="module_name" value="<?= htmlspecialchars($module['name']) ?>">
<input type="hidden" name="module_action" value="disable">
<button type="submit" class="btn btn-sm btn-warning">Disable</button>
</form>
<?php else: ?>
<form action="#" method="POST" class="mr-1">
<input type="hidden" name="module_name" value="<?= htmlspecialchars($module['name']) ?>">
<input type="hidden" name="module_action" value="enable">
<button type="submit" class="btn btn-sm btn-success">Enable</button>
</form>
<?php endif; ?>
<button type="button"
class="btn btn-sm mr-1 module-toggle-btn <?= !empty($module['enabled']) ? 'btn-warning' : 'btn-success' ?>"
data-module="<?= htmlspecialchars($module['name']) ?>"
data-enabled="<?= !empty($module['enabled']) ? '1' : '0' ?>">
<?= !empty($module['enabled']) ? 'Disable' : 'Enable' ?>
</button>
<form action="#" method="POST">
<form action="#" method="POST" onsubmit="return confirm('Uninstall module <?= htmlspecialchars($module['name']) ?>?');">
<input type="hidden" name="module_name" value="<?= htmlspecialchars($module['name']) ?>">
<input type="hidden" name="module_action" value="uninstall">
<button type="submit" class="btn btn-sm btn-danger">Uninstall</button>
<button type="submit" class="btn btn-sm btn-danger"></i>Uninstall</button>
</form>
</div>
</td>
@@ -138,9 +134,97 @@
</div>
</div>
<script>
(function() {
var input = document.getElementById('module_zip_input');
var label = input.nextElementSibling;
var btn = document.getElementById('module_upload_btn');
var zone = document.getElementById('module-drop-zone');
input.addEventListener('change', function() {
var name = this.files[0] ? this.files[0].name : 'Choose file...';
label.textContent = name;
btn.disabled = !this.files.length;
});
zone.addEventListener('click', function(e) {
if (e.target === zone || e.target.closest('.mdi, p, small')) input.click();
});
['dragover', 'dragenter'].forEach(function(ev) {
zone.addEventListener(ev, function(e) {
e.preventDefault();
zone.style.backgroundColor = 'rgba(0,123,255,0.06)';
});
});
['dragleave', 'drop'].forEach(function(ev) {
zone.addEventListener(ev, function(e) {
e.preventDefault();
zone.style.backgroundColor = '';
});
});
zone.addEventListener('drop', function(e) {
if (e.dataTransfer.files.length) {
input.files = e.dataTransfer.files;
input.dispatchEvent(new Event('change'));
}
});
})();
document.addEventListener('click', function(e) {
var btn = e.target.closest('.module-toggle-btn');
if (!btn) return;
e.preventDefault();
var moduleName = btn.getAttribute('data-module');
var isEnabled = btn.getAttribute('data-enabled') === '1';
var newAction = isEnabled ? 'disable' : 'enable';
btn.disabled = true;
btn.innerHTML = '<i class="mdi mdi-loading mdi-spin mr-1"></i>...';
var formData = new FormData();
formData.append('module_name', moduleName);
formData.append('module_action', newAction);
var xhr = new XMLHttpRequest();
xhr.open('POST', window.location.href.split('#')[0]);
xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest');
xhr.onload = function() {
var resp;
try {
resp = JSON.parse(xhr.responseText);
} catch (ex) {
resp = null;
}
if (resp && resp.type === 'danger') {
btn.disabled = false;
btn.innerHTML = (isEnabled ? 'Disable' : 'Enable');
alert(resp.message || 'Operation failed.');
return;
}
var nowEnabled = !isEnabled;
btn.setAttribute('data-enabled', nowEnabled ? '1' : '0');
btn.className = 'btn btn-sm mr-1 module-toggle-btn ' + (nowEnabled ? 'btn-warning' : 'btn-success');
btn.innerHTML = (nowEnabled ? 'Disable' : 'Enable');
btn.disabled = false;
var badge = document.querySelector('.module-status-badge[data-module="' + moduleName + '"]');
if (badge) {
badge.className = 'badge module-status-badge ' + (nowEnabled ? 'badge-success' : 'badge-secondary');
badge.textContent = nowEnabled ? 'Enabled' : 'Disabled';
}
};
xhr.onerror = function() {
btn.disabled = false;
btn.innerHTML = (isEnabled ? 'Disable' : 'Enable');
alert('Failed to toggle module. Please try again.');
};
xhr.send(formData);
});
</script>
<?php if (!isset($__viewMode)): ?>
<?php
require_once __DIR__ . '/../layouts/footer.php';
renderUnifiedLayoutFooter('admin');
?>
<?php endif; ?>
<?php
require_once __DIR__ . '/../layouts/footer.php';
renderUnifiedLayoutFooter('admin');
?>
<?php endif; ?>