Zendesk — copy all tickets from filter v1.1.0
This userscript adds a copy button next to the Filter button on Zendesk filter pages. When clicked, it copies all visible tickets to the clipboard. Each ticket is formatted as a numbered Obsidian link: {N}. [[{ID} {Organisation} — {Title}]], with one ticket per line.
Script Content
// ==UserScript==
// @name Zendesk: Copy All Tickets from Filter
// @namespace https://www.timhilton.xyz/user-scripts
// @version 1.1.0
// @description Adds a button to copy all tickets from a filter view in the same format as Copy Title of Ticket.
// @author Tim Hilton using GitHub Copilot
// @match https://*.zendesk.com/agent/filters/*
// @grant none
// ==/UserScript==
(function() {
'use strict';
const LOG_PREFIX = '[Zendesk: Copy All Tickets from Filter]';
console.debug(`${LOG_PREFIX} Script initialised`);
const COPY_ICON_SVG = `
`;
const TICK_ICON_SVG = `
`;
const injectStyles = () => {
const style = document.createElement('style');
style.textContent = `
.copy-all-tickets-button svg path {
fill: currentColor;
}
`;
document.head.appendChild(style);
};
const extractTicketData = (row) => {
console.debug(`${LOG_PREFIX} Extracting ticket data from row`);
// Find the ID cell
const idCell = row.querySelector('[data-test-id="generic-table-cells-id"]');
const ticketId = idCell ? idCell.textContent.trim().replace('#', '') : null;
if (!ticketId) {
console.debug(`${LOG_PREFIX} ❌ Could not find ticket ID in row`);
return null;
}
// Find the Organisation cell - it's the cell with aria-label attribute that comes after the ID
const cells = row.querySelectorAll('td');
let organisation = 'Unknown Organisation';
let subject = 'Unknown Subject';
// Look for organisation - it's typically in a cell with aria-label attribute
for (const cell of cells) {
const ariaLabel = cell.getAttribute('aria-label');
if (ariaLabel && ariaLabel !== 'Normal' && !ariaLabel.startsWith('views-ticket')) {
// Check if this is the organisation cell (comes before subject)
const sortableButton = cell.querySelector('[data-test-id="generic-table-cells-sortable"]');
if (!sortableButton) {
organisation = ariaLabel;
break;
}
}
}
// Find the Subject cell
const subjectCell = row.querySelector('[data-test-id="ticket-table-cells-subject"]');
if (subjectCell) {
const subjectLink = subjectCell.querySelector('a');
subject = subjectLink ? subjectLink.textContent.trim() : subjectCell.textContent.trim();
}
if (organisation === 'Unknown Organisation') {
console.debug(`${LOG_PREFIX} ⚠️ Could not find organisation for ticket ${ticketId}`);
}
if (subject === 'Unknown Subject') {
console.debug(`${LOG_PREFIX} ⚠️ Could not find subject for ticket ${ticketId}`);
}
console.debug(`${LOG_PREFIX} Extracted: ${ticketId} ${organisation} — ${subject}`);
return { ticketId, organisation, subject };
};
const handleCopyAllTickets = (button) => {
console.debug(`${LOG_PREFIX} handleCopyAllTickets() called`);
const table = document.querySelector('[data-test-id="generic-table"]');
if (!table) {
console.log(`${LOG_PREFIX} ❌ Could not find table`);
return;
}
const rows = table.querySelectorAll('tbody [data-test-id="generic-table-row"]');
console.debug(`${LOG_PREFIX} Found ${rows.length} ticket rows`);
if (rows.length === 0) {
console.log(`${LOG_PREFIX} ⏭️ No tickets found in filter`);
return;
}
const tickets = [];
for (const row of rows) {
const ticketData = extractTicketData(row);
if (ticketData) {
const innerText = `${ticketData.ticketId} ${ticketData.organisation} — ${ticketData.subject}`;
const formattedTicket = `${tickets.length + 1}. [[${innerText}]]`;
tickets.push(formattedTicket);
}
}
if (tickets.length === 0) {
console.log(`${LOG_PREFIX} ❌ No valid tickets could be extracted`);
return;
}
const clipboardText = tickets.join('\n');
console.debug(`${LOG_PREFIX} Copying ${tickets.length} tickets to clipboard...`);
navigator.clipboard.writeText(clipboardText).then(() => {
console.log(`${LOG_PREFIX} 🎉 Successfully copied ${tickets.length} tickets to clipboard!`);
button.innerHTML = TICK_ICON_SVG;
setTimeout(() => {
if (document.body.contains(button)) {
button.innerHTML = COPY_ICON_SVG;
}
}, 1000);
}).catch((err) => {
console.log(`${LOG_PREFIX} ❌ Failed to copy to clipboard: ${err}`);
});
};
const setupCopyButton = () => {
console.debug(`${LOG_PREFIX} setupCopyButton() called`);
if (document.querySelector('.copy-all-tickets-button')) {
console.debug(`${LOG_PREFIX} Copy button already exists, skipping`);
return;
}
const filterButtonContainer = document.querySelector('[data-test-id="views_views-header-filter"]');
if (!filterButtonContainer) {
console.debug(`${LOG_PREFIX} Filter button container not found`);
return;
}
console.debug(`${LOG_PREFIX} Filter button container found`);
const button = document.createElement('button');
button.className = 'StyledButton-sc-qe3ace-0 jgboyG sc-wyziqx-1 bpzhtQ copy-all-tickets-button';
button.setAttribute('data-garden-id', 'buttons.button');
button.setAttribute('data-garden-version', '9.14.2');
button.setAttribute('type', 'button');
button.innerHTML = COPY_ICON_SVG;
button.style.marginLeft = '8px';
button.style.cursor = 'pointer';
button.addEventListener('click', () => handleCopyAllTickets(button));
filterButtonContainer.appendChild(button);
console.log(`${LOG_PREFIX} 🎉 Copy button added successfully!`);
};
injectStyles();
let mutationCount = 0;
const observer = new MutationObserver(() => {
mutationCount++;
console.debug(`${LOG_PREFIX} MutationObserver fired (#${mutationCount})`);
setupCopyButton();
});
console.debug(`${LOG_PREFIX} Starting MutationObserver on document.body`);
observer.observe(document.body, {
childList: true,
subtree: true,
});
})();