From 6c491c1baa68e00c9fcd8679185505573e1ff44e Mon Sep 17 00:00:00 2001 From: Jared Vititoe Date: Sun, 5 Apr 2026 12:15:40 -0400 Subject: [PATCH] Fix close-ticket UX, add cmd palette hint, breadcrumb, image lightbox MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- assets/css/ticket.css | 13 ++++++++ assets/js/ticket.js | 72 +++++++++++++++++++++++++++++++++-------- views/TicketView.php | 14 +++++--- views/layout_header.php | 6 ++++ 4 files changed, 86 insertions(+), 19 deletions(-) diff --git a/assets/css/ticket.css b/assets/css/ticket.css index 3ddaf61..82b67d0 100644 --- a/assets/css/ticket.css +++ b/assets/css/ticket.css @@ -195,6 +195,19 @@ body.edit-mode .editable-metadata { gap: 0.4rem; } +/* Image thumbnail in attachment list */ +.attachment-thumb { + display: block; + width: 3rem; + height: 3rem; + object-fit: cover; + border-radius: 3px; + border: 1px solid var(--border-color); + cursor: zoom-in; + flex-shrink: 0; +} +.lt-lightbox-trigger { display: block; line-height: 0; } + /* ── Dependencies list ───────────────────────────────────────── */ .dependencies-list { display: flex; diff --git a/assets/js/ticket.js b/assets/js/ticket.js index 705b48a..d2ca407 100644 --- a/assets/js/ticket.js +++ b/assets/js/ticket.js @@ -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', ` + + `); + 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 = `${lt.time.ago(att.uploaded_at)}`; + 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 + ? ` + ${lt.escHtml(att.original_filename)} + ` + : `
${lt.escHtml(att.icon || '[ f ]')}
`; + html += `