Fix frontend JS: CSRF resync, status-comment flow, markdown/XSS, kanban
- base.js lt.api: resync window.CSRF_TOKEN from response bodies before throwing on errors and attach err.data/err.status, so a desynced client auto-recovers without a reload - add lt.ticketStatus.submit: status changes that require a comment now prompt, post the comment, and retry update_ticket with it; wired into the ticket dropdown, dashboard quick-status, kanban drag-drop and the 1-4 keyboard shortcuts (bulk ops unchanged) — matches the new server requires_comment enforcement - base.js markdown.render: drop the unsafe marked/markdownit delegation; always use the built-in XSS-safe renderer - ticket.js: XHR upload sends the X-CSRF-Token header and resyncs the token; use lt.escHtml instead of a re-inlined escape chain; @-mention trigger requires a word boundary (no firing inside emails); idempotent, anchor-safe highlightMentions - base.js typeahead: discard out-of-order async results - markdown.js: balanced table tbody/thead; ticket-ref linkification runs after code extraction so #ids inside code aren't linked - dashboard.js kanban: don't swallow the click after a drag - keyboard-shortcuts.js: J/K skip hidden/skeleton rows; drop duplicate ? Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+18
-11
@@ -1000,18 +1000,20 @@ function performQuickStatusChange(ticketId) {
|
||||
if (!quickStatusEl) return;
|
||||
const newStatus = quickStatusEl.value;
|
||||
|
||||
lt.api.post('/api/update_ticket.php', { ticket_id: ticketId, status: newStatus })
|
||||
// Close this modal first so the comment modal (if requires_comment) stacks cleanly.
|
||||
closeQuickStatusModal();
|
||||
|
||||
lt.ticketStatus.submit(ticketId, newStatus)
|
||||
.then(data => {
|
||||
closeQuickStatusModal();
|
||||
if (data.success) {
|
||||
if (data && data.success) {
|
||||
lt.toast.success(`Status updated to ${newStatus}`, 3000);
|
||||
showTableSkeleton(5); setTimeout(() => window.location.reload(), 1000);
|
||||
} else {
|
||||
lt.toast.error('Error: ' + (data.error || 'Unknown error'), 4000);
|
||||
lt.toast.error('Error: ' + ((data && data.error) || 'Unknown error'), 4000);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
closeQuickStatusModal();
|
||||
if (error && error.cancelled) return;
|
||||
lt.toast.error('Error updating status', 4000);
|
||||
});
|
||||
}
|
||||
@@ -1168,8 +1170,9 @@ function populateKanbanCards() {
|
||||
card.dataset.ticketId = ticketId;
|
||||
card.dataset.status = status;
|
||||
card.addEventListener('click', (e) => {
|
||||
// Don't navigate if drag just ended (drag adds/removes is-dragging briefly)
|
||||
if (card.dataset.dragged) { delete card.dataset.dragged; return; }
|
||||
// Don't navigate if a drag just ended. The flag is cleared on a timer
|
||||
// (see handleKanbanSort), so a genuine later click is not swallowed.
|
||||
if (card.dataset.dragged) return;
|
||||
window.location.href = '/ticket/' + encodeURIComponent(ticketId);
|
||||
});
|
||||
card.onkeydown = (e) => { if (e.key === 'Enter' || e.key === ' ') card.click(); };
|
||||
@@ -1214,6 +1217,9 @@ function populateKanbanCards() {
|
||||
|
||||
movedCard.dataset.status = newStatus;
|
||||
movedCard.dataset.dragged = '1';
|
||||
// Clear the drag flag shortly after the drop so it suppresses only the
|
||||
// synthetic click fired on drop, not the user's next genuine click.
|
||||
setTimeout(function () { delete movedCard.dataset.dragged; }, 400);
|
||||
|
||||
// Optimistically update column counts
|
||||
const dec = document.querySelector(`.column-count[data-status="${oldStatus}"]`);
|
||||
@@ -1230,8 +1236,9 @@ function populateKanbanCards() {
|
||||
if (inc) inc.textContent = '(' + Math.max(0, (parseInt(inc.textContent.replace(/\D/g, ''), 10) || 1) - 1) + ')';
|
||||
};
|
||||
|
||||
// POST status update via the shared wrapper (adds CSRF + JSON, throws on non-2xx)
|
||||
lt.api.post('/api/update_ticket.php', { ticket_id: String(ticketId), status: newStatus })
|
||||
// Submit via the shared comment-aware helper. Dropping to Closed (or
|
||||
// reopening) prompts for a required comment and retries; cancel reverts.
|
||||
lt.ticketStatus.submit(String(ticketId), newStatus)
|
||||
.then(function (data) {
|
||||
if (data && data.success) {
|
||||
lt.toast.success('Ticket #' + ticketId + ' → ' + newStatus, 2500);
|
||||
@@ -1241,8 +1248,8 @@ function populateKanbanCards() {
|
||||
revert();
|
||||
}
|
||||
})
|
||||
.catch(function () {
|
||||
lt.toast.error('Status update failed — reverting');
|
||||
.catch(function (error) {
|
||||
if (!(error && error.cancelled)) lt.toast.error('Status update failed — reverting');
|
||||
revert();
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user