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);
})();