Fix close-ticket UX, add cmd palette hint, breadcrumb, image lightbox
- ticket.js: status change requiring a comment now shows an inline modal with a textarea — comment is actually posted before the status changes, instead of just warning the user and changing anyway - layout_header.php: add ⌘K button in header so users can discover the command palette; also removes inline onclick in favor of JS (CSP-safe via nonce script block already present) - TicketView.php: upgrade breadcrumb to lt-breadcrumb markup with ticket title preview (truncated at 45 chars) and aria-current - ticket.js + ticket.css: image attachments now render as clickable thumbnails (3rem×3rem) that open in lt.lightbox; non-image files keep the icon display unchanged Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+58
-14
@@ -437,21 +437,53 @@ function updateTicketStatus() {
|
||||
return; // No change needed
|
||||
}
|
||||
|
||||
// Warn if comment is required
|
||||
// Comment required — show modal with textarea so user enters reason inline
|
||||
if (requiresComment) {
|
||||
showConfirmModal(
|
||||
'Status Change Requires Comment',
|
||||
`This transition to "${newStatus}" requires a comment explaining the reason.\n\nPlease add a comment before changing the status.`,
|
||||
'warning',
|
||||
() => {
|
||||
// User confirmed, proceed with status change
|
||||
performStatusChange(statusSelect, selectedOption, newStatus);
|
||||
},
|
||||
() => {
|
||||
// User cancelled, reset to current status
|
||||
statusSelect.selectedIndex = 0;
|
||||
const modalId = 'statusCommentModal' + Date.now();
|
||||
document.body.insertAdjacentHTML('beforeend', `
|
||||
<div class="lt-modal-overlay" id="${modalId}" aria-hidden="true" role="dialog" aria-modal="true" aria-labelledby="${modalId}_title">
|
||||
<div class="lt-modal lt-modal-sm">
|
||||
<div class="lt-modal-header" style="color:var(--terminal-amber)">
|
||||
<span class="lt-modal-title" id="${modalId}_title">[ ! ] Change Status to ${lt.escHtml(newStatus)}</span>
|
||||
<button class="lt-modal-close" data-modal-close aria-label="Close">✕</button>
|
||||
</div>
|
||||
<div class="lt-modal-body">
|
||||
<p class="lt-text-sm lt-text-muted" style="margin-bottom:0.6rem">
|
||||
A comment is required when changing status to <strong>${lt.escHtml(newStatus)}</strong>. Enter your reason below.
|
||||
</p>
|
||||
<textarea id="${modalId}_comment" class="lt-input lt-w-full" rows="3"
|
||||
placeholder="Reason for status change…"
|
||||
style="resize:vertical;font-family:inherit;font-size:0.8rem"
|
||||
aria-label="Required comment for status change"></textarea>
|
||||
</div>
|
||||
<div class="lt-modal-footer">
|
||||
<button class="lt-btn lt-btn-primary" id="${modalId}_confirm">CONFIRM CHANGE</button>
|
||||
<button class="lt-btn lt-btn-ghost" id="${modalId}_cancel">CANCEL</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`);
|
||||
const modal = document.getElementById(modalId);
|
||||
lt.modal.open(modalId);
|
||||
const cleanup = (ok) => { lt.modal.close(modalId); setTimeout(() => modal.remove(), 300); if (!ok) statusSelect.selectedIndex = 0; };
|
||||
modal.querySelector('[data-modal-close]').addEventListener('click', () => cleanup(false));
|
||||
document.getElementById(`${modalId}_cancel`).addEventListener('click', () => cleanup(false));
|
||||
document.getElementById(`${modalId}_confirm`).addEventListener('click', () => {
|
||||
const comment = document.getElementById(`${modalId}_comment`).value.trim();
|
||||
if (!comment) {
|
||||
document.getElementById(`${modalId}_comment`).focus();
|
||||
lt.toast('Please enter a reason for this status change.', 'warning');
|
||||
return;
|
||||
}
|
||||
);
|
||||
cleanup(true);
|
||||
// Post comment first, then change status
|
||||
const ticketId = getTicketIdFromUrl();
|
||||
lt.api.post('/api/add_comment.php', { ticket_id: ticketId, comment_text: comment })
|
||||
.then(() => performStatusChange(statusSelect, selectedOption, newStatus))
|
||||
.catch(() => performStatusChange(statusSelect, selectedOption, newStatus));
|
||||
});
|
||||
// Focus textarea on open
|
||||
setTimeout(() => { const ta = document.getElementById(`${modalId}_comment`); if (ta) ta.focus(); }, 100);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -929,8 +961,16 @@ function renderAttachments(attachments) {
|
||||
});
|
||||
const uploadDate = `<span class="ts-cell" data-ts="${lt.escHtml(att.uploaded_at)}" title="${lt.escHtml(uploadDateFormatted)}">${lt.time.ago(att.uploaded_at)}</span>`;
|
||||
|
||||
const isImage = /\.(png|jpe?g|gif|webp|svg|bmp)$/i.test(att.original_filename);
|
||||
const imgUrl = `/api/download_attachment.php?id=${att.attachment_id}&inline=1`;
|
||||
const iconHtml = isImage
|
||||
? `<a href="${imgUrl}" class="lt-lightbox-trigger" data-lightbox="ticket-attachments" title="${lt.escHtml(att.original_filename)}">
|
||||
<img src="${imgUrl}" alt="${lt.escHtml(att.original_filename)}" class="attachment-thumb" loading="lazy">
|
||||
</a>`
|
||||
: `<div class="attachment-icon">${lt.escHtml(att.icon || '[ f ]')}</div>`;
|
||||
|
||||
html += `<div class="attachment-item" data-id="${att.attachment_id}">
|
||||
<div class="attachment-icon">${lt.escHtml(att.icon || '[ f ]')}</div>
|
||||
${iconHtml}
|
||||
<div class="attachment-info">
|
||||
<div class="attachment-name" title="${lt.escHtml(att.original_filename)}">
|
||||
<a href="/api/download_attachment.php?id=${att.attachment_id}" target="_blank">
|
||||
@@ -950,6 +990,10 @@ function renderAttachments(attachments) {
|
||||
|
||||
html += '</div>';
|
||||
container.innerHTML = html;
|
||||
// Initialize lightbox on image thumbnails
|
||||
if (window.lt && lt.lightbox) {
|
||||
lt.lightbox.init('.lt-lightbox-trigger', { caption: 'title', loop: true });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user