Add copy ticket button to Job Details and improve cross-browser copy functionality

Changes:
- Added copy ticket button (⧉) next to ticket numbers in Job Details modal
- Implemented robust cross-browser clipboard copy mechanism:
  1. Modern navigator.clipboard API (works in HTTPS contexts)
  2. Legacy document.execCommand('copy') fallback (works in older browsers)
  3. Prompt fallback as last resort
- Applied improved copy function to both Run Checks and Job Details pages
- Copy now works directly in all browsers (Firefox, Edge, Chrome) without popup

This eliminates the manual copy step in Edge that previously required a popup.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
Ivo Oskamp 2026-02-10 15:04:21 +01:00
parent c777728c91
commit 35ec337c54
2 changed files with 122 additions and 16 deletions

View File

@ -284,6 +284,60 @@
.replace(/'/g, "&#39;");
}
// Cross-browser copy to clipboard function
function copyToClipboard(text, button) {
// Method 1: Modern Clipboard API (works in most browsers with HTTPS)
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(text)
.then(function () {
showCopyFeedback(button);
})
.catch(function () {
// Fallback to method 2 if clipboard API fails
fallbackCopy(text, button);
});
} else {
// Method 2: Legacy execCommand method
fallbackCopy(text, button);
}
}
function fallbackCopy(text, button) {
var textarea = document.createElement('textarea');
textarea.value = text;
textarea.style.position = 'fixed';
textarea.style.opacity = '0';
textarea.style.top = '0';
textarea.style.left = '0';
document.body.appendChild(textarea);
textarea.focus();
textarea.select();
try {
var successful = document.execCommand('copy');
if (successful) {
showCopyFeedback(button);
} else {
// If execCommand fails, use prompt as last resort
window.prompt('Copy ticket number:', text);
}
} catch (err) {
// If all else fails, show prompt
window.prompt('Copy ticket number:', text);
}
document.body.removeChild(textarea);
}
function showCopyFeedback(button) {
if (!button) return;
var original = button.textContent;
button.textContent = '✓';
setTimeout(function () {
button.textContent = original;
}, 800);
}
(function () {
var currentRunId = null;
@ -319,12 +373,14 @@
html += '<div class="mb-2"><strong>Tickets</strong><div class="mt-1">';
tickets.forEach(function (t) {
var status = t.resolved_at ? 'Resolved' : 'Active';
var ticketCode = (t.ticket_code || '').toString();
html += '<div class="mb-2 border rounded p-2" data-alert-type="ticket" data-id="' + t.id + '">' +
'<div class="d-flex align-items-start justify-content-between gap-2">' +
'<div class="flex-grow-1 min-w-0">' +
'<div class="text-truncate">' +
'<span class="me-1" title="Ticket">🎫</span>' +
'<span class="fw-semibold">' + escapeHtml(t.ticket_code || '') + '</span>' +
'<span class="fw-semibold">' + escapeHtml(ticketCode) + '</span>' +
'<button type="button" class="btn btn-sm btn-outline-secondary ms-2 py-0 px-1" title="Copy ticket number" data-action="copy-ticket" data-code="' + escapeHtml(ticketCode) + '"></button>' +
'<span class="ms-2 badge ' + (t.resolved_at ? 'bg-secondary' : 'bg-warning text-dark') + '">' + status + '</span>' +
'</div>' +
'</div>' +
@ -371,7 +427,16 @@
ev.preventDefault();
var action = btn.getAttribute('data-action');
var id = btn.getAttribute('data-id');
if (!action || !id) return;
if (!action) return;
if (action === 'copy-ticket') {
var code = btn.getAttribute('data-code') || '';
if (!code) return;
copyToClipboard(code, btn);
return;
}
if (!id) return;
if (action === 'resolve-ticket') {
if (!confirm('Mark ticket as resolved?')) return;
apiJson('/api/tickets/' + encodeURIComponent(id) + '/resolve', {method: 'POST', body: '{}'})

View File

@ -447,6 +447,60 @@ function escapeHtml(s) {
.replace(/'/g, "&#39;");
}
// Cross-browser copy to clipboard function
function copyToClipboard(text, button) {
// Method 1: Modern Clipboard API (works in most browsers with HTTPS)
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(text)
.then(function () {
showCopyFeedback(button);
})
.catch(function () {
// Fallback to method 2 if clipboard API fails
fallbackCopy(text, button);
});
} else {
// Method 2: Legacy execCommand method
fallbackCopy(text, button);
}
}
function fallbackCopy(text, button) {
var textarea = document.createElement('textarea');
textarea.value = text;
textarea.style.position = 'fixed';
textarea.style.opacity = '0';
textarea.style.top = '0';
textarea.style.left = '0';
document.body.appendChild(textarea);
textarea.focus();
textarea.select();
try {
var successful = document.execCommand('copy');
if (successful) {
showCopyFeedback(button);
} else {
// If execCommand fails, use prompt as last resort
window.prompt('Copy ticket number:', text);
}
} catch (err) {
// If all else fails, show prompt
window.prompt('Copy ticket number:', text);
}
document.body.removeChild(textarea);
}
function showCopyFeedback(button) {
if (!button) return;
var original = button.textContent;
button.textContent = '✓';
setTimeout(function () {
button.textContent = original;
}, 800);
}
function getSelectedJobIds() {
var cbs = table.querySelectorAll('tbody .rc_row_cb');
var ids = [];
@ -840,20 +894,7 @@ table.addEventListener('change', function (e) {
if (action === 'copy-ticket') {
var code = btn.getAttribute('data-code') || '';
if (!code) return;
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(code)
.then(function () {
var original = btn.textContent;
btn.textContent = '✓';
setTimeout(function () { btn.textContent = original; }, 800);
})
.catch(function () {
// Fallback: select/copy via prompt
window.prompt('Copy ticket number:', code);
});
} else {
window.prompt('Copy ticket number:', code);
}
copyToClipboard(code, btn);
return;
}