Guard bulk-action buttons against double-submit (#36)
The 4 bulk-action confirm buttons (close/assign/priority/status) called their performBulk*() handler directly on click with no in-flight guard. Double-clicking a confirm button fired two concurrent POST /api/ bulk_operation.php requests for the same ticket IDs, duplicating the close-reason comment, audit-log entry, and Matrix notification on every affected ticket. Each performBulk*() function now takes the clicked button, no-ops if it's already disabled, disables it before firing the request, and re-enables it in .finally() regardless of outcome. Verified by extracting performBulkAssign() from the real source and driving it with a mock lt.api.post() that never resolves until told to: a simulated rapid double-click fired exactly one request (the second call was a no-op while the button was disabled), and a subsequent click after the request resolved correctly fired a new request. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0117oBw2jN4kALYeS8HPq4zV
This commit is contained in:
+28
-12
@@ -140,25 +140,25 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
break;
|
||||
// Bulk operation perform actions
|
||||
case 'perform-bulk-assign':
|
||||
performBulkAssign();
|
||||
performBulkAssign(target);
|
||||
break;
|
||||
case 'close-bulk-assign-modal':
|
||||
closeBulkAssignModal();
|
||||
break;
|
||||
case 'perform-bulk-priority':
|
||||
performBulkPriority();
|
||||
performBulkPriority(target);
|
||||
break;
|
||||
case 'close-bulk-priority-modal':
|
||||
closeBulkPriorityModal();
|
||||
break;
|
||||
case 'perform-bulk-status':
|
||||
performBulkStatusChange();
|
||||
performBulkStatusChange(target);
|
||||
break;
|
||||
case 'close-bulk-status-modal':
|
||||
closeBulkStatusModal();
|
||||
break;
|
||||
case 'perform-bulk-close':
|
||||
performBulkCloseAction();
|
||||
performBulkCloseAction(undefined, target);
|
||||
break;
|
||||
case 'close-bulk-close-modal':
|
||||
closeBulkCloseModal();
|
||||
@@ -491,7 +491,10 @@ function closeBulkCloseModal() {
|
||||
if (modal) setTimeout(() => modal.remove(), 300);
|
||||
}
|
||||
|
||||
function performBulkCloseAction(ticketIds) {
|
||||
function performBulkCloseAction(ticketIds, btn) {
|
||||
if (btn && btn.disabled) return; // already in flight — guards against a double-click firing two requests
|
||||
if (btn) btn.disabled = true;
|
||||
|
||||
ticketIds = ticketIds || getSelectedTicketIds();
|
||||
const commentEl = document.getElementById('bulkCloseComment');
|
||||
const comment = commentEl ? commentEl.value.trim() : '';
|
||||
@@ -524,7 +527,8 @@ function performBulkCloseAction(ticketIds) {
|
||||
}
|
||||
closeBulkCloseModal();
|
||||
lt.toast.error('Bulk close failed: ' + error.message, 5000);
|
||||
});
|
||||
})
|
||||
.finally(() => { if (btn) btn.disabled = false; });
|
||||
}
|
||||
|
||||
var _bulkAssignUserId = null;
|
||||
@@ -596,7 +600,8 @@ function closeBulkAssignModal() {
|
||||
if (modal) setTimeout(() => modal.remove(), 300);
|
||||
}
|
||||
|
||||
function performBulkAssign() {
|
||||
function performBulkAssign(btn) {
|
||||
if (btn && btn.disabled) return; // already in flight — guards against a double-click firing two requests
|
||||
const userId = _bulkAssignUserId;
|
||||
const ticketIds = getSelectedTicketIds();
|
||||
|
||||
@@ -605,6 +610,8 @@ function performBulkAssign() {
|
||||
return;
|
||||
}
|
||||
|
||||
if (btn) btn.disabled = true;
|
||||
|
||||
lt.api.post('/api/bulk_operation.php', {
|
||||
operation_type: 'bulk_assign',
|
||||
ticket_ids: ticketIds,
|
||||
@@ -625,7 +632,8 @@ function performBulkAssign() {
|
||||
})
|
||||
.catch(error => {
|
||||
lt.toast.error('Bulk assign failed: ' + error.message, 5000);
|
||||
});
|
||||
})
|
||||
.finally(() => { if (btn) btn.disabled = false; });
|
||||
}
|
||||
|
||||
function showBulkPriorityModal() {
|
||||
@@ -672,7 +680,8 @@ function closeBulkPriorityModal() {
|
||||
if (modal) setTimeout(() => modal.remove(), 300);
|
||||
}
|
||||
|
||||
function performBulkPriority() {
|
||||
function performBulkPriority(btn) {
|
||||
if (btn && btn.disabled) return; // already in flight — guards against a double-click firing two requests
|
||||
const priorityEl = document.getElementById('bulkPriority');
|
||||
if (!priorityEl) return;
|
||||
const priority = priorityEl.value;
|
||||
@@ -683,6 +692,8 @@ function performBulkPriority() {
|
||||
return;
|
||||
}
|
||||
|
||||
if (btn) btn.disabled = true;
|
||||
|
||||
lt.api.post('/api/bulk_operation.php', {
|
||||
operation_type: 'bulk_priority',
|
||||
ticket_ids: ticketIds,
|
||||
@@ -703,7 +714,8 @@ function performBulkPriority() {
|
||||
})
|
||||
.catch(error => {
|
||||
lt.toast.error('Bulk priority update failed: ' + error.message, 5000);
|
||||
});
|
||||
})
|
||||
.finally(() => { if (btn) btn.disabled = false; });
|
||||
}
|
||||
|
||||
// Make table rows clickable
|
||||
@@ -786,7 +798,8 @@ function closeBulkStatusModal() {
|
||||
if (modal) setTimeout(() => modal.remove(), 300);
|
||||
}
|
||||
|
||||
function performBulkStatusChange() {
|
||||
function performBulkStatusChange(btn) {
|
||||
if (btn && btn.disabled) return; // already in flight — guards against a double-click firing two requests
|
||||
const bulkStatusEl = document.getElementById('bulkStatus');
|
||||
if (!bulkStatusEl) return;
|
||||
const status = bulkStatusEl.value;
|
||||
@@ -800,6 +813,8 @@ function performBulkStatusChange() {
|
||||
const commentEl = document.getElementById('bulkStatusComment');
|
||||
const comment = commentEl ? commentEl.value.trim() : '';
|
||||
|
||||
if (btn) btn.disabled = true;
|
||||
|
||||
lt.api.post('/api/bulk_operation.php', {
|
||||
operation_type: 'bulk_status',
|
||||
ticket_ids: ticketIds,
|
||||
@@ -829,7 +844,8 @@ function performBulkStatusChange() {
|
||||
}
|
||||
closeBulkStatusModal();
|
||||
lt.toast.error('Bulk status change failed: ' + error.message, 5000);
|
||||
});
|
||||
})
|
||||
.finally(() => { if (btn) btn.disabled = false; });
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user