feat(helpdesk): file attachments on tickets and replies (#141)
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
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (pull_request) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (pull_request) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (pull_request) Has been cancelled
Platform: moko-platform CI / Gate 3: Self-Health Check (pull_request) Has been cancelled
Platform: moko-platform CI / Gate 4: Governance (pull_request) Has been cancelled
Platform: moko-platform CI / Gate 5: Template Integrity (pull_request) Has been cancelled
Platform: moko-platform CI / CI Summary (pull_request) 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
Universal: PR Check / Build RC Package (pull_request) Has been cancelled
Universal: PR Check / Report Issues (pull_request) Has been cancelled
Generic: Repo Health / Scripts governance (pull_request) Has been cancelled
Generic: Repo Health / Repository health (pull_request) Has been cancelled
Generic: Repo Health / Report Issues (pull_request) Has been cancelled
Generic: Repo Health / Site Health (push) Has been cancelled
Generic: Repo Health / Access control (push) Has been cancelled
Generic: Repo Health / Site Health (pull_request) Has been cancelled
Universal: PR Check / Branch Policy (pull_request) Has been cancelled
Generic: Repo Health / Access control (pull_request) Has been cancelled
Universal: Secret Scanning / Gitleaks Secret Scan (pull_request) Has been cancelled
Universal: Auto Version Bump / Version Bump (push) Has been cancelled
Universal: PR Check / Validate PR (pull_request) Has been cancelled
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Has been cancelled
Platform: moko-platform CI / Gate 1: Code Quality (pull_request) Has been cancelled
Platform: moko-platform CI / Gate 1: Code Quality (push) Has been cancelled
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
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (pull_request) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (pull_request) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (pull_request) Has been cancelled
Platform: moko-platform CI / Gate 3: Self-Health Check (pull_request) Has been cancelled
Platform: moko-platform CI / Gate 4: Governance (pull_request) Has been cancelled
Platform: moko-platform CI / Gate 5: Template Integrity (pull_request) Has been cancelled
Platform: moko-platform CI / CI Summary (pull_request) 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
Universal: PR Check / Build RC Package (pull_request) Has been cancelled
Universal: PR Check / Report Issues (pull_request) Has been cancelled
Generic: Repo Health / Scripts governance (pull_request) Has been cancelled
Generic: Repo Health / Repository health (pull_request) Has been cancelled
Generic: Repo Health / Report Issues (pull_request) Has been cancelled
Generic: Repo Health / Site Health (push) Has been cancelled
Generic: Repo Health / Access control (push) Has been cancelled
Generic: Repo Health / Site Health (pull_request) Has been cancelled
Universal: PR Check / Branch Policy (pull_request) Has been cancelled
Generic: Repo Health / Access control (pull_request) Has been cancelled
Universal: Secret Scanning / Gitleaks Secret Scan (pull_request) Has been cancelled
Universal: Auto Version Bump / Version Bump (push) Has been cancelled
Universal: PR Check / Validate PR (pull_request) Has been cancelled
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Has been cancelled
Platform: moko-platform CI / Gate 1: Code Quality (pull_request) Has been cancelled
Platform: moko-platform CI / Gate 1: Code Quality (push) Has been cancelled
- #__mokosuite_ticket_attachments table
- AttachmentService: upload, download, delete with type/size validation
- Allowed: jpg,png,gif,webp,pdf,doc,docx,xls,xlsx,csv,txt,zip (10MB max)
- Secure storage in /media/com_mokosuite/attachments/{ticket_id}/
- Upload field in reply form, auto-uploads after reply creation
- Download links on ticket and reply cards
- Controller tasks: uploadAttachment, downloadAttachment, deleteAttachment
This commit is contained in:
@@ -111,6 +111,21 @@ CREATE TABLE IF NOT EXISTS `#__mokosuite_ticket_canned` (
|
||||
PRIMARY KEY (`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `#__mokosuite_ticket_attachments` (
|
||||
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
`ticket_id` INT UNSIGNED NOT NULL,
|
||||
`reply_id` INT UNSIGNED DEFAULT NULL,
|
||||
`filename` VARCHAR(255) NOT NULL,
|
||||
`filepath` VARCHAR(512) NOT NULL,
|
||||
`filesize` INT UNSIGNED NOT NULL DEFAULT 0,
|
||||
`mimetype` VARCHAR(100) NOT NULL DEFAULT '',
|
||||
`uploaded_by` INT NOT NULL DEFAULT 0,
|
||||
`created` DATETIME NOT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_ticket` (`ticket_id`),
|
||||
KEY `idx_reply` (`reply_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `#__mokosuite_ticket_automation` (
|
||||
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
`title` VARCHAR(255) NOT NULL,
|
||||
|
||||
@@ -579,6 +579,48 @@ class DisplayController extends BaseController
|
||||
$this->jsonResponse(['success' => true, 'message' => 'Order saved.']);
|
||||
}
|
||||
|
||||
public function uploadAttachment()
|
||||
{
|
||||
Session::checkToken() or die(Text::_('JINVALID_TOKEN'));
|
||||
if (!$this->checkAcl('mokosuite.tickets')) { $this->jsonForbidden(); }
|
||||
$input = Factory::getApplication()->getInput();
|
||||
$ticketId = $input->getInt('ticket_id', 0);
|
||||
$replyId = $input->getInt('reply_id', 0) ?: null;
|
||||
if (!$ticketId) { $this->jsonResponse(['success' => false, 'message' => 'Missing ticket_id']); return; }
|
||||
$files = $input->files->get('attachments', [], 'raw');
|
||||
if (empty($files) || empty($files['name'])) { $this->jsonResponse(['success' => false, 'message' => 'No files uploaded']); return; }
|
||||
$saved = \Moko\Component\MokoSuite\Administrator\Service\AttachmentService::upload($ticketId, $replyId, $files);
|
||||
$this->jsonResponse(['success' => true, 'message' => count($saved) . ' file(s) uploaded', 'count' => count($saved)]);
|
||||
}
|
||||
|
||||
public function downloadAttachment()
|
||||
{
|
||||
if (!$this->checkAcl('mokosuite.tickets')) { $this->jsonForbidden(); }
|
||||
$id = Factory::getApplication()->getInput()->getInt('id', 0);
|
||||
$db = Factory::getDbo();
|
||||
$db->setQuery($db->getQuery(true)->select('*')->from('#__mokosuite_ticket_attachments')->where('id = ' . $id));
|
||||
$att = $db->loadObject();
|
||||
if (!$att) { throw new \RuntimeException('Attachment not found', 404); }
|
||||
$path = \Moko\Component\MokoSuite\Administrator\Service\AttachmentService::getAbsolutePath($att);
|
||||
if (!file_exists($path)) { throw new \RuntimeException('File not found', 404); }
|
||||
$app = Factory::getApplication();
|
||||
$app->setHeader('Content-Type', $att->mimetype ?: 'application/octet-stream');
|
||||
$app->setHeader('Content-Disposition', 'attachment; filename="' . $att->filename . '"');
|
||||
$app->setHeader('Content-Length', (string) filesize($path));
|
||||
$app->sendHeaders();
|
||||
readfile($path);
|
||||
$app->close();
|
||||
}
|
||||
|
||||
public function deleteAttachment()
|
||||
{
|
||||
Session::checkToken() or die(Text::_('JINVALID_TOKEN'));
|
||||
if (!$this->checkAcl('mokosuite.tickets')) { $this->jsonForbidden(); }
|
||||
$id = Factory::getApplication()->getInput()->getInt('id', 0);
|
||||
$ok = \Moko\Component\MokoSuite\Administrator\Service\AttachmentService::delete($id);
|
||||
$this->jsonResponse(['success' => $ok, 'message' => $ok ? 'Attachment deleted' : 'Not found']);
|
||||
}
|
||||
|
||||
public function saveAutomation()
|
||||
{
|
||||
Session::checkToken() or die(Text::_('JINVALID_TOKEN'));
|
||||
|
||||
@@ -0,0 +1,177 @@
|
||||
<?php
|
||||
/**
|
||||
* @package MokoSuite
|
||||
* @subpackage com_mokosuite
|
||||
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
|
||||
* @license GNU General Public License version 3 or later; see LICENSE
|
||||
*/
|
||||
|
||||
namespace Moko\Component\MokoSuite\Administrator\Service;
|
||||
|
||||
defined('_JEXEC') or die;
|
||||
|
||||
use Joomla\CMS\Factory;
|
||||
use Joomla\CMS\Filesystem\File;
|
||||
use Joomla\CMS\Filesystem\Folder;
|
||||
use Joomla\CMS\Log\Log;
|
||||
|
||||
class AttachmentService
|
||||
{
|
||||
private const STORAGE_DIR = JPATH_ROOT . '/media/com_mokosuite/attachments';
|
||||
|
||||
private const ALLOWED_EXTENSIONS = [
|
||||
'jpg', 'jpeg', 'png', 'gif', 'webp', 'svg',
|
||||
'pdf', 'doc', 'docx', 'xls', 'xlsx', 'csv', 'txt', 'rtf',
|
||||
'zip', 'gz', 'tar',
|
||||
];
|
||||
|
||||
private const MAX_FILE_SIZE = 10 * 1024 * 1024; // 10 MB
|
||||
|
||||
/**
|
||||
* Upload file(s) for a ticket or reply.
|
||||
*
|
||||
* @param int $ticketId Ticket ID
|
||||
* @param int|null $replyId Reply ID (null for ticket-level attachments)
|
||||
* @param array $files $_FILES array entry (single or multi)
|
||||
* @return array Saved attachment records
|
||||
*/
|
||||
public static function upload(int $ticketId, ?int $replyId, array $files): array
|
||||
{
|
||||
$saved = [];
|
||||
|
||||
// Normalize single file to array format
|
||||
if (!is_array($files['name'])) {
|
||||
$files = [
|
||||
'name' => [$files['name']],
|
||||
'type' => [$files['type']],
|
||||
'tmp_name' => [$files['tmp_name']],
|
||||
'error' => [$files['error']],
|
||||
'size' => [$files['size']],
|
||||
];
|
||||
}
|
||||
|
||||
$ticketDir = self::STORAGE_DIR . '/' . $ticketId;
|
||||
|
||||
if (!is_dir($ticketDir)) {
|
||||
Folder::create($ticketDir);
|
||||
}
|
||||
|
||||
$userId = (int) Factory::getUser()->id;
|
||||
$db = Factory::getDbo();
|
||||
|
||||
for ($i = 0, $count = count($files['name']); $i < $count; $i++)
|
||||
{
|
||||
if ($files['error'][$i] !== UPLOAD_ERR_OK) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$originalName = File::makeSafe($files['name'][$i]);
|
||||
$ext = strtolower(pathinfo($originalName, PATHINFO_EXTENSION));
|
||||
|
||||
// Validate extension
|
||||
if (!in_array($ext, self::ALLOWED_EXTENSIONS, true)) {
|
||||
Log::add("Attachment rejected: disallowed extension .{$ext}", Log::WARNING, 'mokosuite');
|
||||
continue;
|
||||
}
|
||||
|
||||
// Validate size
|
||||
if ($files['size'][$i] > self::MAX_FILE_SIZE) {
|
||||
Log::add("Attachment rejected: file too large ({$files['size'][$i]} bytes)", Log::WARNING, 'mokosuite');
|
||||
continue;
|
||||
}
|
||||
|
||||
// Generate unique filename to prevent overwrites
|
||||
$storedName = uniqid('att_', true) . '.' . $ext;
|
||||
$destPath = $ticketDir . '/' . $storedName;
|
||||
|
||||
if (!File::upload($files['tmp_name'][$i], $destPath)) {
|
||||
Log::add("Attachment upload failed: {$originalName}", Log::ERROR, 'mokosuite');
|
||||
continue;
|
||||
}
|
||||
|
||||
$record = (object) [
|
||||
'ticket_id' => $ticketId,
|
||||
'reply_id' => $replyId,
|
||||
'filename' => $originalName,
|
||||
'filepath' => $ticketId . '/' . $storedName,
|
||||
'filesize' => $files['size'][$i],
|
||||
'mimetype' => $files['type'][$i],
|
||||
'uploaded_by' => $userId,
|
||||
'created' => Factory::getDate()->toSql(),
|
||||
];
|
||||
|
||||
$db->insertObject('#__mokosuite_ticket_attachments', $record, 'id');
|
||||
$saved[] = $record;
|
||||
}
|
||||
|
||||
return $saved;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get attachments for a ticket.
|
||||
*/
|
||||
public static function getForTicket(int $ticketId): array
|
||||
{
|
||||
$db = Factory::getDbo();
|
||||
$db->setQuery(
|
||||
$db->getQuery(true)
|
||||
->select('a.*, u.name AS uploader_name')
|
||||
->from($db->quoteName('#__mokosuite_ticket_attachments', 'a'))
|
||||
->leftJoin($db->quoteName('#__users', 'u') . ' ON u.id = a.uploaded_by')
|
||||
->where($db->quoteName('a.ticket_id') . ' = ' . $ticketId)
|
||||
->order('a.created ASC')
|
||||
);
|
||||
return $db->loadObjectList() ?: [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the absolute filesystem path for an attachment.
|
||||
*/
|
||||
public static function getAbsolutePath(object $attachment): string
|
||||
{
|
||||
return self::STORAGE_DIR . '/' . $attachment->filepath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete an attachment (file + DB record).
|
||||
*/
|
||||
public static function delete(int $attachmentId): bool
|
||||
{
|
||||
$db = Factory::getDbo();
|
||||
$db->setQuery(
|
||||
$db->getQuery(true)
|
||||
->select('*')
|
||||
->from('#__mokosuite_ticket_attachments')
|
||||
->where('id = ' . $attachmentId)
|
||||
);
|
||||
$att = $db->loadObject();
|
||||
|
||||
if (!$att) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$path = self::STORAGE_DIR . '/' . $att->filepath;
|
||||
|
||||
if (file_exists($path)) {
|
||||
File::delete($path);
|
||||
}
|
||||
|
||||
$db->setQuery(
|
||||
$db->getQuery(true)
|
||||
->delete('#__mokosuite_ticket_attachments')
|
||||
->where('id = ' . $attachmentId)
|
||||
)->execute();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format file size for display.
|
||||
*/
|
||||
public static function formatSize(int $bytes): string
|
||||
{
|
||||
if ($bytes < 1024) return $bytes . ' B';
|
||||
if ($bytes < 1048576) return round($bytes / 1024, 1) . ' KB';
|
||||
return round($bytes / 1048576, 1) . ' MB';
|
||||
}
|
||||
}
|
||||
@@ -23,6 +23,7 @@ class HtmlView extends BaseHtmlView
|
||||
protected $priorities = [];
|
||||
protected $customFields = [];
|
||||
protected $fieldValues = [];
|
||||
protected $attachments = [];
|
||||
|
||||
public function display($tpl = null)
|
||||
{
|
||||
@@ -43,6 +44,9 @@ class HtmlView extends BaseHtmlView
|
||||
$this->fieldValues = $model->getFieldValues($id);
|
||||
}
|
||||
|
||||
// Load attachments
|
||||
$this->attachments = \Moko\Component\MokoSuite\Administrator\Service\AttachmentService::getForTicket($id);
|
||||
|
||||
if (!$this->ticket)
|
||||
{
|
||||
Factory::getApplication()->enqueueMessage('Ticket not found.', 'error');
|
||||
|
||||
@@ -8,6 +8,17 @@ use Joomla\CMS\Session\Session;
|
||||
$t = $this->ticket;
|
||||
$canned = $this->cannedResponses;
|
||||
$token = Session::getFormToken();
|
||||
$attachments = $this->attachments;
|
||||
$downloadUrl = Route::_('index.php?option=com_mokosuite&task=display.downloadAttachment');
|
||||
$uploadUrl = Route::_('index.php?option=com_mokosuite&task=display.uploadAttachment&format=json');
|
||||
$deleteAttUrl = Route::_('index.php?option=com_mokosuite&task=display.deleteAttachment&format=json');
|
||||
|
||||
// Group attachments by reply_id (null = ticket-level)
|
||||
$attByReply = [];
|
||||
foreach ($attachments as $att) {
|
||||
$key = $att->reply_id ?? 0;
|
||||
$attByReply[$key][] = $att;
|
||||
}
|
||||
|
||||
$statuses = $this->statuses ?? [];
|
||||
$priorities = $this->priorities ?? [];
|
||||
@@ -25,7 +36,21 @@ $priorities = $this->priorities ?? [];
|
||||
</div>
|
||||
<span class="badge bg-dark">Original</span>
|
||||
</div>
|
||||
<div class="card-body"><?php echo nl2br($this->escape($t->body)); ?></div>
|
||||
<div class="card-body">
|
||||
<?php echo nl2br($this->escape($t->body)); ?>
|
||||
<?php if (!empty($attByReply[0])): ?>
|
||||
<hr>
|
||||
<div class="small">
|
||||
<strong>Attachments:</strong>
|
||||
<?php foreach ($attByReply[0] as $att): ?>
|
||||
<a href="<?php echo $downloadUrl . '&id=' . $att->id; ?>" class="d-inline-block me-3">
|
||||
<span class="icon-download"></span> <?php echo $this->escape($att->filename); ?>
|
||||
<span class="text-muted">(<?php echo \Moko\Component\MokoSuite\Administrator\Service\AttachmentService::formatSize($att->filesize); ?>)</span>
|
||||
</a>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Replies -->
|
||||
@@ -40,7 +65,21 @@ $priorities = $this->priorities ?? [];
|
||||
<span class="badge bg-warning text-dark">Internal Note</span>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<div class="card-body"><?php echo nl2br($this->escape($reply->body)); ?></div>
|
||||
<div class="card-body">
|
||||
<?php echo nl2br($this->escape($reply->body)); ?>
|
||||
<?php if (!empty($attByReply[$reply->id])): ?>
|
||||
<hr>
|
||||
<div class="small">
|
||||
<strong>Attachments:</strong>
|
||||
<?php foreach ($attByReply[$reply->id] as $att): ?>
|
||||
<a href="<?php echo $downloadUrl . '&id=' . $att->id; ?>" class="d-inline-block me-3">
|
||||
<span class="icon-download"></span> <?php echo $this->escape($att->filename); ?>
|
||||
<span class="text-muted">(<?php echo \Moko\Component\MokoSuite\Administrator\Service\AttachmentService::formatSize($att->filesize); ?>)</span>
|
||||
</a>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
|
||||
@@ -59,6 +98,10 @@ $priorities = $this->priorities ?? [];
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<textarea id="reply-body" class="form-control mb-2" rows="5" placeholder="Type your reply..."></textarea>
|
||||
<div class="mb-2">
|
||||
<input type="file" id="reply-attachments" class="form-control form-control-sm" multiple
|
||||
accept=".jpg,.jpeg,.png,.gif,.webp,.pdf,.doc,.docx,.xls,.xlsx,.csv,.txt,.zip">
|
||||
</div>
|
||||
<div class="d-flex gap-2">
|
||||
<button type="button" class="btn btn-primary" id="btn-reply"
|
||||
data-url="<?php echo Route::_('index.php?option=com_mokosuite&task=display.addTicketReply&format=json'); ?>"
|
||||
@@ -190,22 +233,39 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
});
|
||||
}
|
||||
|
||||
// Reply buttons
|
||||
// Reply buttons (with attachment upload)
|
||||
document.querySelectorAll('#btn-reply, #btn-internal').forEach(function(btn) {
|
||||
btn.addEventListener('click', function() {
|
||||
var body = document.getElementById('reply-body').value.trim();
|
||||
if (!body) return;
|
||||
var fileInput = document.getElementById('reply-attachments');
|
||||
if (!body && (!fileInput || !fileInput.files.length)) return;
|
||||
var el = this;
|
||||
el.disabled = true;
|
||||
var fd = new FormData();
|
||||
fd.append('ticket_id', el.dataset.ticket);
|
||||
fd.append('body', body);
|
||||
fd.append('body', body || '(attachment)');
|
||||
fd.append('is_internal', el.dataset.internal || '0');
|
||||
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) location.reload(); else Joomla.renderMessages({error:[d.message]}); })
|
||||
.finally(function(){ el.disabled = false; });
|
||||
.then(function(d){
|
||||
if (!d.success) { Joomla.renderMessages({error:[d.message]}); el.disabled = false; return; }
|
||||
// Upload attachments if any
|
||||
if (fileInput && fileInput.files.length > 0) {
|
||||
var afd = new FormData();
|
||||
afd.append('ticket_id', el.dataset.ticket);
|
||||
if (d.reply_id) afd.append('reply_id', d.reply_id);
|
||||
for (var i = 0; i < fileInput.files.length; i++) {
|
||||
afd.append('attachments[' + i + ']', fileInput.files[i]);
|
||||
}
|
||||
afd.append(el.dataset.token, '1');
|
||||
fetch('<?php echo $uploadUrl; ?>', {method:'POST', body:afd, headers:{'X-Requested-With':'XMLHttpRequest'}})
|
||||
.then(function(){ location.reload(); });
|
||||
} else {
|
||||
location.reload();
|
||||
}
|
||||
})
|
||||
.catch(function(){ el.disabled = false; });
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user