Zendesk — Add link to Azure DevOps work item in header v1.1.0

← Back to User Scripts

Adds a button to the ticket header that links to the Azure DevOps work item.

Script Content

// ==UserScript==
// @name         Zendesk — Add link to Azure DevOps work item in header
// @namespace    http://tampermonkey.net/
// @version      1.1.0
// @description  Adds a button to the ticket header that links to the Azure DevOps work item.
// @author       tjhleeds using Jules
// @match        https://*.zendesk.com/*
// @grant        none
// ==/UserScript==

(function() {
    'use strict';

    const LOG_PREFIX = '[Zendesk to Azure DevOps Userscript]';
    console.log(`${LOG_PREFIX} Script initialized`);

    const AZURE_DEVOPS_SVG = `

    
    
        
            
            
            
            
            
        
    

    `;

    // Track which ticket+URL combinations we've processed
    const processedTickets = new Map(); // ticketId -> devopsUrl

    const findTicketContainer = (ticketId) => {
        // Find the container that holds this specific ticket's content
        // This is usually a parent of the header with data-ticket-id
        const header = document.querySelector(`div[data-ticket-id="${ticketId}"]`);
        if (!header) return null;
        
        // Look for a container that likely holds both header and form fields
        // Try to find a parent with a test-id or class that indicates it's a ticket container
        let container = header;
        while (container && container !== document.body) {
            const testId = container.getAttribute('data-test-id');
            if (testId && testId.includes(`ticket-${ticketId}`)) {
                return container;
            }
            container = container.parentElement;
        }
        
        // If we can't find a specific container, return the header's parent
        return header.parentElement;
    };

    const getCurrentTicketId = () => {
        // Check the URL to see which ticket is currently active
        const urlMatch = window.location.href.match(/\/tickets\/(\d+)/);
        if (urlMatch) {
            return urlMatch[1];
        }
        return null;
    };

    const addLinkToTicket = (ticketId) => {
        console.log(`${LOG_PREFIX} addLinkToTicket() called for ticket ${ticketId}`);
        
        // Only process the currently active ticket
        const currentTicketId = getCurrentTicketId();
        if (currentTicketId && currentTicketId !== ticketId) {
            console.log(`${LOG_PREFIX} ⏭️ Skipping ticket ${ticketId} (not currently active, active is ${currentTicketId})`);
            return false;
        }
        
        // Find the specific ticket header
        const header = document.querySelector(`div[data-ticket-id="${ticketId}"]`);
        if (!header) {
            console.log(`${LOG_PREFIX} ❌ Header not found for ticket ${ticketId}`);
            return false;
        }
        console.log(`${LOG_PREFIX} ✅ Header found for ticket ${ticketId}`);

        // Check if we've already added a link to this specific header
        const existingLink = header.querySelector('.azure-devops-link');
        if (existingLink) {
            console.log(`${LOG_PREFIX} ⏭️ Link already exists in this header, skipping`);
            return true; // Already processed successfully
        }

        // Find the ticket container to search within
        const container = findTicketContainer(ticketId);
        console.log(`${LOG_PREFIX} Searching for DevOps Link field within container for ticket ${ticketId}`);

        // Search for DevOps Link field within the container (or globally if we can't find container)
        const searchRoot = container || document;
        const labels = Array.from(searchRoot.querySelectorAll('label'));
        const devopsLinkField = labels.find(label => label.textContent === 'DevOps Link');
        
        if (!devopsLinkField) {
            console.log(`${LOG_PREFIX} ❌ DevOps Link field not found for ticket ${ticketId}`);
            return false;
        }
        console.log(`${LOG_PREFIX} ✅ DevOps Link field found`);

        const devopsLinkInput = devopsLinkField.nextElementSibling;
        if (!devopsLinkInput || !devopsLinkInput.value) {
            console.log(`${LOG_PREFIX} ❌ DevOps Link input not found or empty`, devopsLinkInput);
            return false;
        }
        console.log(`${LOG_PREFIX} ✅ DevOps Link value: ${devopsLinkInput.value}`);

        // Check if we've already processed this exact ticket+URL combination
        const previousUrl = processedTickets.get(ticketId);
        if (previousUrl === devopsLinkInput.value) {
            console.log(`${LOG_PREFIX} ⏭️ Already processed ticket ${ticketId} with this URL`);
            return true;
        }

        const firstChildOfHeader = header.firstChild;
        if (!firstChildOfHeader) {
            console.log(`${LOG_PREFIX} ❌ Header has no first child`);
            return false;
        }

        const targetElement = firstChildOfHeader.firstChild;
        if (!targetElement) {
            console.log(`${LOG_PREFIX} ❌ First child has no first child`);
            return false;
        }

        const link = document.createElement('a');
        link.href = devopsLinkInput.value;
        link.target = '_blank';
        link.classList.add('azure-devops-link');
        link.innerHTML = AZURE_DEVOPS_SVG;

        firstChildOfHeader.insertBefore(link, targetElement);
        firstChildOfHeader.style.display = 'flex';
        firstChildOfHeader.style.alignItems = 'center';
        targetElement.style.marginLeft = '10px';
        
        console.log(`${LOG_PREFIX} 🎉 Link successfully added to header for ticket ${ticketId}!`);
        processedTickets.set(ticketId, devopsLinkInput.value);
        return true;
    };

    const checkCurrentTicket = () => {
        console.log(`${LOG_PREFIX} checkCurrentTicket() called`);
        
        const currentTicketId = getCurrentTicketId();
        if (!currentTicketId) {
            console.log(`${LOG_PREFIX} No ticket ID found in URL`);
            return;
        }
        
        console.log(`${LOG_PREFIX} Current ticket ID from URL: ${currentTicketId}`);
        addLinkToTicket(currentTicketId);
    };

    // Initial check when script loads
    console.log(`${LOG_PREFIX} Running initial check...`);
    setTimeout(() => {
        checkCurrentTicket();
    }, 1000);

    // Watch for DOM changes (new tickets loading, fields appearing)
    let mutationCount = 0;
    const observer = new MutationObserver((mutations) => {
        mutationCount++;
        console.log(`${LOG_PREFIX} Mutation #${mutationCount} detected`);
        
        // Check if any mutations added a ticket header or label
        let shouldCheck = false;
        for (const mutation of mutations) {
            if (mutation.type === 'childList') {
                for (const node of mutation.addedNodes) {
                    if (node.nodeType === 1) { // Element node
                        if (node.matches && (node.matches('div[data-ticket-id]') || node.matches('label'))) {
                            shouldCheck = true;
                            break;
                        }
                        if (node.querySelector && (node.querySelector('div[data-ticket-id]') || node.querySelector('label'))) {
                            shouldCheck = true;
                            break;
                        }
                    }
                }
            }
        }
        
        if (shouldCheck) {
            console.log(`${LOG_PREFIX} Relevant change detected, checking current ticket...`);
            checkCurrentTicket();
        }
    });

    observer.observe(document.body, {
        childList: true,
        subtree: true
    });
    
    console.log(`${LOG_PREFIX} MutationObserver started, watching document.body`);

    // Watch for URL changes (navigation between tickets)
    let lastUrl = window.location.href;
    const urlCheckInterval = setInterval(() => {
        const currentUrl = window.location.href;
        if (currentUrl !== lastUrl) {
            console.log(`${LOG_PREFIX} URL changed from ${lastUrl} to ${currentUrl}`);
            lastUrl = currentUrl;
            
            // Give the page a moment to render the new ticket
            setTimeout(() => {
                checkCurrentTicket();
            }, 500);
        }
    }, 500);
    
    console.log(`${LOG_PREFIX} URL monitoring started`);
})();