Zendesk — copy title of ticket v3.0.0
← Back to User Scripts
Script Content
// ==UserScript==
// @name Zendesk: Copy Title of Ticket
// @namespace https://www.timhilton.xyz/user-scripts
// @version 3.0.0
// @description Displays a copy link in the ticket title.
// @author Tim Hilton using Jules and GitHub Copilot
// @match https://*.zendesk.com/*
// @grant none
// ==/UserScript==
(function() {
'use strict';
const LOG_PREFIX = '[Zendesk: Copy Title]';
console.debug(`${LOG_PREFIX} Script initialized`);
const URL_PATTERN = /\/agent\/tickets\/(\d+)/;
let activeObserver = null;
const escapeForObsidian = (text) => text.replace(/[\[\]:\/\\^|#?*"<>]/g, '-');
const injectStyles = () => {
const style = document.createElement('style');
style.textContent = `
section[data-theme='light'] .copy-title-icon svg path {
fill: rgb(92, 105, 112);
}
section[data-theme='dark'] .copy-title-icon svg path {
fill: rgb(145, 156, 165);
}
.copy-title-icon {
border-radius: 4px;
transition: border-color 0.25s ease-in-out, box-shadow 0.1s ease-in-out, background-color 0.25s ease-in-out, color 0.25s ease-in-out, outline-color 0.1s ease-in-out, z-index 0.25s ease-in-out;
}
.copy-title-icon:hover {
background-color: rgba(38, 148, 214, 0.08);
color: rgb(176, 184, 190);
}
`;
document.head.appendChild(style);
};
const COPY_ICON_SVG = `
`;
const TICK_ICON_SVG = `
`;
const handleCopyClick = (icon, titleInput) => {
console.debug(`${LOG_PREFIX} handleCopyClick() called`);
const title = titleInput.value;
const organisationElement = document.querySelector('[data-tracking-id="tabs-nav-item-organizations"]');
const organisation = organisationElement ? organisationElement.textContent.trim() : 'Unknown Organisation';
const ticketIdMatch = window.location.href.match(/tickets\/(\d+)/);
const ticketId = ticketIdMatch ? ticketIdMatch[1] : 'Unknown Ticket ID';
const formattedString = `${ticketId} ${escapeForObsidian(organisation)} — ${escapeForObsidian(title)}`;
const url = window.location.href;
const htmlString = `${formattedString}`;
const markdownString = `[${formattedString}](${url})`;
console.debug(`${LOG_PREFIX} Copying formatted string to clipboard (plain text, HTML and markdown)...`);
const writeClipboard = async () => {
const plainBlob = new Blob([formattedString], { type: 'text/plain' });
const htmlBlob = new Blob([htmlString], { type: 'text/html' });
const markdownBlob = new Blob([markdownString], { type: 'web text/markdown' });
if (typeof ClipboardItem !== 'undefined' && navigator.clipboard.write) {
try {
await navigator.clipboard.write([new ClipboardItem({
'text/plain': plainBlob,
'text/html': htmlBlob,
'web text/markdown': markdownBlob,
})]);
} catch (_) {
await navigator.clipboard.write([new ClipboardItem({
'text/plain': plainBlob,
'text/html': htmlBlob,
})]);
}
} else {
await navigator.clipboard.writeText(formattedString);
}
};
writeClipboard().then(() => {
console.log(`${LOG_PREFIX} 🎉 Successfully copied title to clipboard!`);
icon.innerHTML = TICK_ICON_SVG;
setTimeout(() => {
if (document.body.contains(icon)) {
icon.innerHTML = COPY_ICON_SVG;
}
}, 1000);
}).catch((err) => {
console.log(`${LOG_PREFIX} ❌ Failed to copy to clipboard: ${err}`);
});
};
const cleanup = () => {
console.debug(`${LOG_PREFIX} Cleaning up observers`);
if (activeObserver) {
activeObserver.disconnect();
activeObserver = null;
}
};
const observeForTitleInput = (callback) => {
cleanup();
const container = document.querySelector('[data-test-id="workspace-content"]') || document.body;
const existing = document.querySelector('input[data-test-id="omni-header-subject"]');
if (existing) {
console.debug(`${LOG_PREFIX} Title input already present`);
callback(existing);
return;
}
console.debug(`${LOG_PREFIX} Starting observer for title input`);
activeObserver = new MutationObserver(() => {
const element = document.querySelector('input[data-test-id="omni-header-subject"]');
if (element) {
console.debug(`${LOG_PREFIX} Title input found`);
activeObserver.disconnect();
activeObserver = null;
callback(element);
}
});
activeObserver.observe(container, {
childList: true,
subtree: true,
});
};
const injectCopyIcon = (titleInput) => {
if (document.querySelector('.copy-title-icon')) {
console.debug(`${LOG_PREFIX} Copy icon already exists, skipping`);
return;
}
const thirdAncestor = titleInput.parentElement?.parentElement?.parentElement;
if (!thirdAncestor) {
console.debug(`${LOG_PREFIX} Third ancestor not found`);
return;
}
const icon = document.createElement('div');
icon.innerHTML = COPY_ICON_SVG;
icon.classList.add('copy-title-icon');
icon.style.width = '32px';
icon.style.height = '32px';
icon.style.display = 'flex';
icon.style.alignItems = 'center';
icon.style.justifyContent = 'center';
icon.style.cursor = 'pointer';
icon.style.paddingLeft = '10px';
icon.style.paddingRight = '10px';
icon.addEventListener('click', () => handleCopyClick(icon, titleInput));
thirdAncestor.after(icon);
console.log(`${LOG_PREFIX} 🎉 Copy icon added successfully!`);
};
const handleNavigation = (url) => {
console.debug(`${LOG_PREFIX} Handling navigation to ${url}`);
cleanup();
if (!URL_PATTERN.test(url)) {
console.debug(`${LOG_PREFIX} URL does not match pattern, skipping`);
return;
}
console.debug(`${LOG_PREFIX} ✅ URL matches ticket pattern`);
setTimeout(() => {
observeForTitleInput(injectCopyIcon);
}, 100);
};
injectStyles();
if ('navigation' in window) {
navigation.addEventListener('navigate', (event) => {
console.debug(`${LOG_PREFIX} Navigation API: navigate event`);
handleNavigation(event.destination.url);
});
console.debug(`${LOG_PREFIX} Navigation API listener registered`);
} else {
let lastUrl = location.href;
const checkUrlInterval = setInterval(() => {
if (location.href !== lastUrl) {
console.debug(`${LOG_PREFIX} URL change detected`);
lastUrl = location.href;
handleNavigation(location.href);
}
}, 500);
console.debug(`${LOG_PREFIX} URL polling fallback started`);
}
console.debug(`${LOG_PREFIX} Checking initial URL`);
handleNavigation(location.href);
})();