Zendesk Enhancements

Enhances Zendesk interface and adds additional functionality

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         Zendesk Enhancements
// @namespace    http://tampermonkey.net/
// @version      1.0
// @description  Enhances Zendesk interface and adds additional functionality
// @author       diogoodev
// @match        https://*.zendesk.com/*
// @grant        GM_addStyle
// @license MIT
// ==/UserScript==

(function() {
    'use strict';

    // Configurações do usuário
    const config = {
        responseMessage: '[Minha resposta ao cliente]\n',
        debounceTime: 300,
        notificationDuration: 3000
    };

    // Cache para elementos DOM
    const domCache = {
        tabToolbar: null,
        getTabToolbar() {
            if (!this.tabToolbar) {
                this.tabToolbar = document.querySelector('div[data-test-id="header-tablist"] > div.sc-19uji9v-0');
            }
            return this.tabToolbar;
        },
        resetCache() {
            this.tabToolbar = null;
        }
    };

    // Estado de ativação do script, persistido no localStorage
    let scriptEnabled = localStorage.getItem('scriptEnabled') === 'true' || true;

    // Traduções
    const translations = {
        'Text copied successfully!': 'Texto copiado com sucesso!',
        'Failed to copy text. Please try again.': 'Falha ao copiar texto. Tente novamente.',
        'Error accessing clipboard': 'Erro ao acessar a área de transferência',
        'No inactive tabs to close': 'Nenhuma aba inativa para fechar',
        'inactive tabs closed': 'abas inativas fechadas',
        'Script enabled': 'Script habilitado',
        'Script disabled': 'Script desabilitado',
        'Opened': 'Aberto',
        'URLs': 'URLs',
        'No URLs found in conversation': 'Nenhum URL encontrado na conversa',
        'Conversation container not found': 'Container da conversa não encontrado'
    };

    // Estilos CSS personalizados
    const customCSS = `
        .custom-button, .djm-task-link, .copy-conversation-button, .open-urls-button {
            padding: 5px 10px;
            background-color: limegreen;
            font-size: 16px;
            color: black;
            border: 1px solid transparent;
            border-radius: 4px;
            cursor: pointer;
            text-align: center;
            text-decoration: none;
            display: inline-block;
            margin: 5px;
            transition: background-color 0.15s ease, color 0.15s ease;
        }

        .custom-button:hover, .djm-task-link:hover, .copy-conversation-button:hover, .open-urls-button:hover {
            background-color: darkgreen;
            color: white;
        }

        .sc-1oduqug-0.jdCBDY {
            margin-top: 30px;
        }

        iframe#web-messenger-container {
            display: none;
        }

        .app_view.app-1019154.apps_ticket_sidebar iframe {
            height: 80vh!important;
        }

        .sc-1nvv38f-3.cjpyOe {
            flex: unset;
        }

        .jGrowl-notification {
            top: 30px;
        }

        .zendesk-custom-notification {
            position: fixed;
            top: 20px;
            right: 20px;
            background-color: #333;
            color: white;
            padding: 10px 15px;
            border-radius: 4px;
            z-index: 10000;
            box-shadow: 0 2px 10px rgba(0,0,0,0.2);
            animation: fadeIn 0.3s, fadeOut 0.3s 2.7s;
        }

        .toggle-script-button {
            position: fixed;
            bottom: 10px;
            left: 10px;
            z-index: 9999;
            opacity: 0.7;
        }

        .toggle-script-button:hover {
            opacity: 1;
        }

        @keyframes fadeIn {
            from { opacity: 0; }
            to { opacity: 1; }
        }

        @keyframes fadeOut {
            from { opacity: 1; }
            to { opacity: 0; }
        }
    `;

    GM_addStyle(customCSS);

    // Utilitários
    const utils = {
        // Debounce para evitar chamadas excessivas de função
        debounce(func, wait) {
            let timeout;
            return function(...args) {
                const context = this;
                clearTimeout(timeout);
                timeout = setTimeout(() => func.apply(context, args), wait);
            };
        },

        // Função para extrair URLs de um texto
        extractUrls(text) {
            const urlRegex = /https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)/g;
            return text.match(urlRegex) || [];
        },

        // Verifica se um elemento já foi processado
        isProcessed(element, key = 'processed') {
            return element.dataset[key] === 'true';
        },

        // Marca um elemento como processado
        markAsProcessed(element, key = 'processed') {
            element.dataset[key] = 'true';
        },

        // Obtém um elemento ou array de elementos de forma segura
        safeQuerySelector(selector, parent = document, all = false) {
            try {
                return all
                    ? Array.from(parent.querySelectorAll(selector) || [])
                    : parent.querySelector(selector);
            } catch (e) {
                console.error(`Error selecting "${selector}":`, e);
                return all ? [] : null;
            }
        }
    };

    // Função para notificar o usuário com mensagens traduzidas
    function notifyUser(message) {
        const translatedMessage = translations[message] || message;

        const existingNotification = document.querySelector('.zendesk-custom-notification');
        if (existingNotification) {
            existingNotification.remove();
        }

        const notification = document.createElement('div');
        notification.textContent = translatedMessage;
        notification.className = 'zendesk-custom-notification';
        document.body.appendChild(notification);

        setTimeout(() => {
            if (notification && notification.parentNode) {
                notification.remove();
            }
        }, config.notificationDuration);
    }

    // Função para copiar texto para a área de transferência
    function copyToClipboard(text) {
        try {
            navigator.clipboard.writeText(text)
                .then(() => notifyUser('Text copied successfully!'))
                .catch(err => {
                    console.error('Failed to copy text: ', err);
                    notifyUser('Failed to copy text. Please try again.');
                });
        } catch (e) {
            console.error('Error accessing clipboard: ', e);
            notifyUser('Error accessing clipboard');
        }
    }

    // Função para transformar URLs em links clicáveis
    function transformUrlsToLinks() {
        if (!scriptEnabled) return;

        const cells = utils.safeQuerySelector('.beWvMU', document, true);
        if (cells.length === 0) return;

        for (const cell of cells) {
            if (cell.querySelector('a.custom-button')) continue;

            const text = cell.textContent;
            const urls = utils.extractUrls(text);

            if (urls.length === 0) continue;

            let modifiedHtml = text;
            for (const url of urls) {
                modifiedHtml = modifiedHtml.replace(
                    new RegExp(url.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g'),
                    `<a href="${url}" target="_blank" class="custom-button">${url}</a>`
                );
            }

            cell.innerHTML = modifiedHtml;
        }
    }

    // Função para inicializar links nos campos de entrada
    function initializeInputLinks() {
        if (!scriptEnabled) return;

        const inputElements = utils.safeQuerySelector('.custom_field_14504424601628 input', document, true);
        if (inputElements.length === 0) return;

        for (const inputElement of inputElements) {
            if (utils.isProcessed(inputElement)) continue;
            utils.markAsProcessed(inputElement);

            const parentContainer = inputElement.parentNode.parentNode;
            const existingLink = parentContainer.querySelector('.djm-task-link');

            if (existingLink) {
                existingLink.remove();
            }

            const linkElement = document.createElement('a');
            linkElement.textContent = 'Task';
            linkElement.style.display = 'none';
            linkElement.classList.add('djm-task-link');
            parentContainer.insertBefore(linkElement, inputElement.nextSibling);

            checkForUrl(inputElement, linkElement);

            inputElement.addEventListener('input', utils.debounce(() => {
                checkForUrl(inputElement, linkElement);
            }, 200));
        }
    }

    // Função auxiliar para verificar URLs nos campos de entrada
    function checkForUrl(inputElement, linkElement) {
        if (!inputElement || !linkElement) return;

        const urls = utils.extractUrls(inputElement.value);

        if (urls.length > 0) {
            linkElement.href = urls[0];
            linkElement.setAttribute('target', '_blank');
            linkElement.style.display = 'inline-block';
        } else {
            linkElement.style.display = 'none';
        }
    }

    // Função para fechar abas inativas
    function closeInactiveTabs() {
        if (!scriptEnabled) return;

        const closeButtons = utils.safeQuerySelector(
            'div[role="tab"][data-selected="false"] button[data-test-id="close-button"]',
            document,
            true
        );

        if (closeButtons.length === 0) {
            notifyUser('No inactive tabs to close');
            return;
        }

        for (const btn of closeButtons) {
            btn.click();
        }

        notifyUser(`${closeButtons.length} inactive tabs closed`);
    }

    // Função para adicionar o botão "Fechar tudo"
    function addCloseAllButton() {
        if (!scriptEnabled) return;

        const toolbar = domCache.getTabToolbar();
        if (toolbar && !document.getElementById('close-all-button')) {
            const closeButton = document.createElement('button');
            closeButton.id = 'close-all-button';
            closeButton.textContent = 'Fechar tudo';
            closeButton.className = 'custom-button';
            closeButton.addEventListener('click', closeInactiveTabs);
            toolbar.appendChild(closeButton);
        }
    }

    // Função para adicionar botões "Copiar Conversa" e "Abrir Todos URLs"
    function addCopyButtons() {
        if (!scriptEnabled) return;

        const workspaces = utils.safeQuerySelector('.ember-view.workspace', document, true);
        if (workspaces.length === 0) return;

        for (const workspace of workspaces) {
            const headerElement =
                utils.safeQuerySelector('[data-test-id="conversation-header"]', workspace) ||
                utils.safeQuerySelector('.sc-1nvv38f-3.cjpyOe', workspace);

            if (!headerElement || headerElement.querySelector('.copy-conversation-button')) continue;

            // Botão "Copiar Conversa"
            const copyButton = document.createElement('button');
            copyButton.innerText = 'Copiar Conversa';
            copyButton.style.marginLeft = '10px';
            copyButton.classList.add('copy-conversation-button');
            copyButton.addEventListener('click', () => {
                const conversationContainer =
                    utils.safeQuerySelector('.sc-175iuw8-0.ecaNtR.conversation-polaris.polaris-react-component.rich_text', workspace) ||
                    utils.safeQuerySelector('[data-test-id="conversation-text"]', workspace);

                if (conversationContainer) {
                    const textToCopy = conversationContainer.innerText;
                    const modifiedText = textToCopy + config.responseMessage;
                    copyToClipboard(modifiedText);
                } else {
                    notifyUser('Conversation container not found');
                }
            });
            headerElement.appendChild(copyButton);

            // Botão "Abrir Todos URLs"
            const openUrlsButton = document.createElement('button');
            openUrlsButton.innerText = 'Abrir Todos URLs';
            openUrlsButton.style.marginLeft = '10px';
            openUrlsButton.classList.add('open-urls-button');
            openUrlsButton.addEventListener('click', () => {
                const conversationContainer =
                    utils.safeQuerySelector('.sc-175iuw8-0.ecaNtR.conversation-polaris.polaris-react-component.rich_text', workspace) ||
                    utils.safeQuerySelector('[data-test-id="conversation-text"]', workspace);

                if (conversationContainer) {
                    const text = conversationContainer.textContent;
                    const urls = utils.extractUrls(text);

                    if (urls.length > 0) {
                        // Limitar o número de abas abertas de uma vez para evitar bloqueio do navegador
                        const urlLimit = 10;
                        const openCount = Math.min(urls.length, urlLimit);

                        for (let i = 0; i < openCount; i++) {
                            window.open(urls[i], '_blank');
                        }

                        const message = openCount < urls.length
                            ? `Aberto ${openCount} de ${urls.length} URLs (limitado para evitar bloqueio do navegador)`
                            : `Aberto ${urls.length} URLs`;

                        notifyUser(message);
                    } else {
                        notifyUser('No URLs found in conversation');
                    }
                } else {
                    notifyUser('Conversation container not found');
                }
            });
            headerElement.appendChild(openUrlsButton);
        }
    }

    // Função para aplicar estilos às abas
    function applyTabStyles() {
        if (!scriptEnabled) return;

        const allTabs = utils.safeQuerySelector('div[role="tab"]', document, true);
        for (const tab of allTabs) {
            tab.style.backgroundColor = '';
            tab.style.borderLeft = '';
            tab.removeAttribute('style');
        }

        const selectedTabs = utils.safeQuerySelector('div[role="tab"][data-selected="true"], div[role="tab"][aria-selected="true"]', document, true);
        for (const tab of selectedTabs) {
            tab.style.backgroundColor = 'green';
            tab.style.borderLeft = '3px solid darkgreen';
        }
    }

    // Função para adicionar o botão de alternância do script
    function addToggleButton() {
        if (document.querySelector('.toggle-script-button')) return;

        const button = document.createElement('button');
        button.innerText = 'ZD Script';
        button.className = 'custom-button toggle-script-button';
        button.title = scriptEnabled ? 'Desabilitar script' : 'Habilitar script';
        button.style.backgroundColor = scriptEnabled ? 'limegreen' : '#ff6666';

        button.addEventListener('click', () => {
            scriptEnabled = !scriptEnabled;
            localStorage.setItem('scriptEnabled', scriptEnabled);

            button.style.backgroundColor = scriptEnabled ? 'limegreen' : '#ff6666';
            button.title = scriptEnabled ? 'Desabilitar script' : 'Habilitar script';

            const scriptElements = utils.safeQuerySelector('.custom-button:not(.toggle-script-button), .djm-task-link, .open-urls-button', document, true);
            for (const el of scriptElements) {
                el.style.display = scriptEnabled ? '' : 'none';
            }

            notifyUser(scriptEnabled ? 'Script enabled' : 'Script disabled');

            if (!scriptEnabled) {
                const allTabs = utils.safeQuerySelector('div[role="tab"]', document, true);
                for (const tab of allTabs) {
                    tab.removeAttribute('style');
                }
            } else {
                runAllFunctions();
            }
        });

        document.body.appendChild(button);
    }

    // Função para verificar a URL e executar funções correspondentes
    function checkURLAndRunFunctions() {
        if (!scriptEnabled) return;

        const currentPath = window.location.pathname;

        if (currentPath.startsWith('/agent/tickets/')) {
            initializeInputLinks();
        }

        if (currentPath.startsWith('/agent/filters/')) {
            transformUrlsToLinks();
        }

        applyTabStyles();
        addCloseAllButton();
        addCopyButtons();
    }

    // Cache para evitar refluos desnecessários
    const processedNodes = new WeakSet();

    // Função otimizada para executar todas as funções
    function runAllFunctions() {
        domCache.resetCache();
        checkURLAndRunFunctions();
    }

    // Função de inicialização
    function initialize() {
        runAllFunctions();
        addToggleButton();
        setupObservers();
    }

    // Configuração de observadores para mudanças na página de forma mais eficiente
    function setupObservers() {
        // Observer para mudanças de URL
        let lastUrl = location.href;
        const debouncedRunAll = utils.debounce(runAllFunctions, config.debounceTime);

        // Observador para mudanças críticas na página
        const contentObserver = new MutationObserver((mutations) => {
            // Verificar se houve alguma mudança significativa
            let shouldUpdate = false;

            // Verificar mudanças de URL
            if (location.href !== lastUrl) {
                lastUrl = location.href;
                shouldUpdate = true;
            }

            // Verificar outras mudanças significativas no DOM
            if (!shouldUpdate) {
                for (const mutation of mutations) {
                    // Ignorar mutações em elementos já processados
                    if (processedNodes.has(mutation.target)) continue;

                    if (mutation.type === 'childList' && mutation.addedNodes.length > 0) {
                        // Verificar se elementos importantes foram adicionados
                        for (const node of mutation.addedNodes) {
                            if (node.nodeType !== Node.ELEMENT_NODE) continue;

                            // Importante para novos tickets, abas, etc.
                            if (node.matches && (
                                node.matches('.ember-view.workspace') ||
                                node.matches('div[role="tab"]') ||
                                node.querySelector('.ember-view.workspace, div[role="tab"]')
                            )) {
                                shouldUpdate = true;
                                break;
                            }
                        }

                        if (shouldUpdate) break;
                    } else if (mutation.type === 'attributes' &&
                        (mutation.attributeName === 'data-selected' ||
                         mutation.attributeName === 'aria-selected')) {
                        // Para mudanças nas abas
                        applyTabStyles();
                        processedNodes.add(mutation.target);
                    }
                }
            }

            if (shouldUpdate) {
                debouncedRunAll();
            }
        });

        // Observar o documento inteiro
        contentObserver.observe(document.body, {
            childList: true,
            subtree: true,
            attributes: true,
            attributeFilter: ['data-selected', 'aria-selected']
        });

        // Delegação de eventos para cliques em abas
        document.addEventListener('click', (e) => {
            const tabElement = e.target.closest('div[role="tab"]');
            if (tabElement) {
                setTimeout(applyTabStyles, 50);
            }
        }, { capture: true, passive: true });
    }

    // Inicialização do script
    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', initialize);
    } else {
        initialize();
    }
})();