Jira - Verberg 'Labels: None'

Past de weergave van Jira cards aan met o.a. status-icoontjes en een betere uitlijning. Update test

Versión del día 01/10/2025. Echa un vistazo a la versión más reciente.

Tendrás que instalar una extensión para tu navegador como Tampermonkey, Greasemonkey o Violentmonkey si quieres utilizar este script.

Necesitarás instalar una extensión como Tampermonkey o Violentmonkey para instalar este script.

Necesitarás instalar una extensión como Tampermonkey o Violentmonkey para instalar este script.

Necesitarás instalar una extensión como Tampermonkey o Userscripts para instalar este script.

Necesitará instalar una extensión como Tampermonkey para instalar este script.

Necesitarás instalar una extensión para administrar scripts de usuario si quieres instalar este script.

(Ya tengo un administrador de scripts de usuario, déjame instalarlo)

Necesitará instalar una extensión como Stylus para instalar este estilo.

Necesitará instalar una extensión como Stylus para instalar este estilo.

Necesitará instalar una extensión como Stylus para instalar este estilo.

Necesitará instalar una extensión del gestor de estilos de usuario para instalar este estilo.

Necesitará instalar una extensión del gestor de estilos de usuario para instalar este estilo.

Necesitará instalar una extensión del gestor de estilos de usuario para instalar este estilo.

(Ya tengo un administrador de estilos de usuario, déjame instalarlo)

// ==UserScript==
// @name         Jira - Verberg 'Labels: None'
// @namespace    http://tampermonkey.net/
// @version      6.7
// @description  Past de weergave van Jira cards aan met o.a. status-icoontjes en een betere uitlijning. Update test
// @author       Roy
// @match        https://jira.onderwijstransparant.nl/*
// @grant        none
// ==/UserScript==

(function() {
    'use strict';

    /**
     * Injecteert een <style> tag in de <head> van de pagina met alle custom CSS.
     * Dit wordt één keer uitgevoerd en voorkomt dat stijlen inline of herhaaldelijk worden toegevoegd.
     */
    function injectCustomStyles() {
        const styleId = 'jira-custom-card-styles';
        // Voorkom het dubbel injecteren van de stijlen.
        if (document.getElementById(styleId)) return;

        const style = document.createElement('style');
        style.id = styleId;
        style.innerHTML = `
            /* Geeft tickets van het type 'Bug' een lichtrode achtergrond. */
            .ghx-issue-compact.ghx-bug-card,
            .ghx-issue-compact.ghx-bug-card .ghx-end {
                background-color: #ffe7e7 !important;
            }

            /* Stijl voor het categorie-label dat uit de titel wordt gehaald. */
            .aui-label.ghx-title-label {
                background-color: #ffffff;
                color: #333;
                border: 1px solid #ccc;
            }

            /* --- DIVIDER STIJLEN --- */
            /* Stijlen voor een ticket met het label 'divider', om het als scheidingslijn te tonen. */
            .ghx-issue-compact.ghx-divider {
                height: auto !important;
                margin: 5px 0 !important;
                background: transparent !important;
                border: none !important;
                box-shadow: none !important;
                display: flex !important;
                align-items: center;
                justify-content: center;
                padding: 10px 0;
                position: relative;
                overflow: visible;
                cursor: move !important;
            }
            /* De horizontale lijn van de divider. */
            .ghx-issue-compact.ghx-divider::before {
                content: '';
                position: absolute;
                left: 0;
                right: 0;
                top: 50%;
                height: 1px;
                background-color: #ccc;
                z-index: 0;
            }
            /* Verberg alle standaard ticket-elementen behalve de titel. */
            .ghx-issue-compact.ghx-divider .ghx-row > *:not(.ghx-summary) { display: none !important; }
            /* Stijl de titel van de divider als een gecentreerd label op de lijn. */
            .ghx-issue-compact.ghx-divider .ghx-summary {
                display: block !important; text-align: center; font-size: 14px;
                font-weight: bold; color: #707070; background-color: #f5f5f5;
                padding: 2px 15px; border-radius: 10px; position: relative;
                z-index: 1; flex-grow: 0 !important; opacity: 1 !important;
            }
            /* Verberg overige elementen van de divider. */
            .ghx-issue-compact.ghx-divider .ghx-grabber,
            .ghx-issue-compact.ghx-divider .ghx-end,
            .ghx-issue-compact.ghx-divider .ghx-plan-extra-fields { display: none !important; }
            .ghx-issue-compact.ghx-divider .ghx-row { display: block !important; text-align: center; }

            /* --- LABEL STIJLEN --- */
            /* Stijl voor 'tijdskritisch' label, met een icoon en opvallende kleur. */
            .aui-label.ghx-time-critical-label {
                background-color: #ff5722; /* Levendig oranje-rood */
                color: white;
                border-color: #e64a19;
                display: flex;
                align-items: center;
                gap: 5px; /* Ruimte tussen icoon en tekst */
                font-weight: bold;
                border-radius: 3px;
            }


            /* --- STATUS PROGRESSIE ICOON STIJLEN --- */
            /* De container voor het status-icoon (de 3 balkjes). */
            .ghx-status-indicator {
                display: flex;
                flex-direction: column-reverse; /* Zorgt ervoor dat de balkjes van onder naar boven gevuld worden. */
                gap: 1px;
                width: 8px;
                height: 11px; /* 3 balkjes * 3px hoogte + 2 * 1px tussenruimte */
                justify-content: center;
                flex-shrink: 0;
                margin: 0 2px;
                vertical-align: middle;
            }
            /* Stijl voor een enkel balkje binnen het status-icoon. */
            .ghx-status-indicator-bar {
                height: 3px; /* Vaste hoogte voor consistentie. */
                width: 100%;
                background-color: #fff; /* Wit voor 'lege' balkjes. */
                border: 1px solid #ccc; /* Grijze rand voor 'lege' balkjes. */
                box-sizing: border-box; /* Zorgt dat de rand binnen de hoogte valt. */
            }

            /* Specifieke kleuren voor de balkjes op basis van de status van het ticket. */
            .ghx-issue-compact.status-inprogress .ghx-status-indicator-bar:nth-child(-n+1) {
                background-color: #59afe1; border-color: #59afe1;
            }
            .ghx-issue-compact.status-test .ghx-status-indicator-bar:nth-child(-n+2) {
                background-color: #f6c342; border-color: #f6c342;
            }
            .ghx-issue-compact.status-done .ghx-status-indicator-bar,
            .ghx-issue-compact.status-closed .ghx-status-indicator-bar {
                background-color: #8eb021; border-color: #8eb021;
            }
        `;
        document.head.appendChild(style);
    }

    /**
     * Maakt een gestyled icoon voor 'small', 'medium', 'large' labels.
     * @param {string} sizeText - De tekst voor het icoon (S, M, of L).
     * @returns {HTMLElement} Het HTML-element voor het icoon.
     */
    function createSizeIcon(sizeText) {
        const icon = document.createElement('div');
        icon.textContent = sizeText;
        Object.assign(icon.style, {
            width: '24px', height: '24px', borderRadius: '50%', backgroundColor: '#f5f5f5',
            border: '1px solid #ccc', color: '#333', display: 'flex', alignItems: 'center',
            justifyContent: 'center', fontWeight: 'bold', fontSize: '12px',
            flexShrink: '0', fontFamily: 'Arial, sans-serif'
        });
        return icon;
    }

    /**
     * Configuratie-object voor custom acties op basis van labelnamen.
     * Keys moeten in lowercase zijn voor case-insensitive matching.
     */
    const labelActions = {
        'divider': {
            type: 'custom',
            handler: function(card) {
                if (card.classList.contains('ghx-divider')) return false;
                card.classList.add('ghx-divider');

                const summary = card.querySelector('.ghx-summary');
                if (summary) {
                    summary.style.opacity = '1';
                    const inner = summary.querySelector('.ghx-inner');
                    if (inner && inner.dataset.originalTitle) {
                        inner.innerHTML = inner.dataset.originalTitle;
                    }
                }
                card.querySelectorAll('.ghx-plan-extra-fields, .ghx-end.ghx-row').forEach(el => el.remove());
                return false;
            }
        },
        'p048': { type: 'style', styles: { backgroundColor: '#f79232', color: '#fff', borderColor: '#f79232' } },
        'urgent': { type: 'style', styles: { backgroundColor: '#d04437', color: 'white', borderColor: '#d04437' } },
        'on-hold': {
            type: 'custom',
            handler: function(card, labelText, rightContainer) {
                if (card.querySelector('.on-hold-label')) return false;
                const onHoldLabel = document.createElement('span');
                onHoldLabel.textContent = `🛑 On Hold`;
                onHoldLabel.className = 'aui-label on-hold-label';
                Object.assign(onHoldLabel.style, { marginLeft: '5px', flexShrink: '0' });
                const keyElement = card.querySelector('.ghx-key');
                const summaryElement = card.querySelector('.ghx-summary');
                if (keyElement) keyElement.insertAdjacentElement('afterend', onHoldLabel);
                if (summaryElement) summaryElement.style.opacity = '0.6';
                return false;
            }
        },
        'small': { type: 'custom', handler: function(card, labelText, rightContainer) { if (!card.querySelector('.size-icon-s')) { const icon = createSizeIcon('S'); icon.classList.add('size-icon-s'); rightContainer.appendChild(icon); } return false; } },
        'medium': { type: 'custom', handler: function(card, labelText, rightContainer) { if (!card.querySelector('.size-icon-m')) { const icon = createSizeIcon('M'); icon.classList.add('size-icon-m'); rightContainer.appendChild(icon); } return false; } },
        'large': { type: 'custom', handler: function(card, labelText, rightContainer) { if (!card.querySelector('.size-icon-l')) { const icon = createSizeIcon('L'); icon.classList.add('size-icon-l'); rightContainer.appendChild(icon); } return false; } },
        'onderzoek': {
            type: 'custom',
            handler: function(card, labelText, rightContainer) {
                if (card.querySelector('.research-label')) return false;
                const researchLabel = document.createElement('span');
                researchLabel.className = 'aui-label research-label';
                researchLabel.innerHTML = '🔍 Onderzoek';
                Object.assign(researchLabel.style, { borderColor: '#3572b0', display: 'flex', alignItems: 'center', gap: '4px' });
                rightContainer.appendChild(researchLabel);
                return false;
            }
        },
        'tijdskritisch': {
            type: 'custom',
            handler: function(card, labelText, rightContainer) {
                if (card.querySelector('.ghx-time-critical-label')) return false; // Voorkom duplicaten
                const timeCriticalLabel = document.createElement('span');
                timeCriticalLabel.className = 'aui-label ghx-time-critical-label';
                timeCriticalLabel.innerHTML = '⏰ Tijdskritisch'; // Icoon + tekst
                rightContainer.appendChild(timeCriticalLabel);
                return false; // Voorkom dat de standaard label-logica wordt uitgevoerd
            }
        },
        'release': {
            type: 'custom',
            handler: function(card, labelText, rightContainer) {
                if (card.querySelector('.release-label')) return false;
                const releaseLabel = document.createElement('span');
                releaseLabel.className = 'aui-label release-label';
                releaseLabel.innerHTML = '🔨 Uitvoeren release';
                Object.assign(releaseLabel.style, { display: 'flex', alignItems: 'center', gap: '4px' });
                rightContainer.appendChild(releaseLabel);
                return false;
            }
        }
    };

    /**
     * De hoofdfunctie die een enkele issue-kaart verwerkt en de DOM aanpast.
     * @param {HTMLElement} card - Het DOM-element van de issue-kaart.
     */
    function processSingleCard(card) {
        // --- Stap 1: Check voor speciale kaart-types (zoals 'divider') ---
        const labelTooltip = card.querySelector('span[data-tooltip^="Labels:"]');
        if (labelTooltip && labelTooltip.dataset.tooltip.toLowerCase().includes('divider')) {
            const action = labelActions['divider'];
            if (action && action.handler) {
                action.handler(card);
            }
            card.classList.add('ghx-layout-processed');
            return;
        }

        // --- Stap 2: Standaard kaart verwerking. ---
        card.querySelector('.ghx-flags')?.remove();

        const typeSpan = card.querySelector('.ghx-type');
        if (typeSpan && typeSpan.title.toLowerCase() === 'bug') {
            card.classList.add('ghx-bug-card');
        } else {
            card.classList.remove('ghx-bug-card');
        }

        // Reset eventuele eerdere aanpassingen om duplicaten te voorkomen.
        const summaryElementReset = card.querySelector('.ghx-summary');
        if (summaryElementReset) {
            summaryElementReset.style.opacity = '1';
            const innerSpan = summaryElementReset.querySelector('.ghx-inner');
            if (innerSpan && innerSpan.dataset.originalTitle) {
                innerSpan.innerHTML = innerSpan.dataset.originalTitle;
            }
        }

        const issueContent = card.querySelector('.ghx-issue-content');
        if (!issueContent) return;

        const mainRow = issueContent.querySelector('.ghx-row');
        if (!mainRow) return;

        mainRow.querySelectorAll('.on-hold-label, .ghx-status-indicator').forEach(el => el.remove());
        Object.assign(mainRow.style, { display: 'flex', alignItems: 'center', gap: '5px' });

        // Creëer containers om de rechter-elementen in de juiste volgorde te groeperen.
        const masterRightContainer = document.createElement('span');
        masterRightContainer.className = 'ghx-end';
        Object.assign(masterRightContainer.style, { display: 'flex', alignItems: 'center', gap: '8px', marginLeft: 'auto', flexShrink: '0' });

        const labelsSubContainer = document.createElement('span');
        Object.assign(labelsSubContainer.style, { display: 'flex', alignItems: 'center', gap: '5px' });

        const sizeIconsSubContainer = document.createElement('span');
        Object.assign(sizeIconsSubContainer.style, { display: 'flex', alignItems: 'center', gap: '5px' });

        const titleCategorySubContainer = document.createElement('span');
        Object.assign(titleCategorySubContainer.style, { display: 'flex', alignItems: 'center', gap: '5px' });

        const otherFieldsSubContainer = document.createElement('span');
        Object.assign(otherFieldsSubContainer.style, { display: 'flex', alignItems: 'center', gap: '5px' });




        // Verzamel alle elementen uit de extra-velden rijen.
        const elementsToProcess = [];
        const containersToRemove = [];
        card.querySelectorAll('.ghx-plan-extra-fields, .ghx-issue-content > .ghx-end.ghx-row').forEach(container => {
            elementsToProcess.push(...container.children);
            containersToRemove.push(container);
        });

        // Verwerk de status van het ticket en voeg het status-icoon toe.
        const statusField = elementsToProcess.find(field => field.matches('span[data-tooltip^="Status:"]'));
        if (statusField) {
            const statusText = (statusField.dataset.tooltip || '').replace('Status:', '').trim().toLowerCase();
            const statusClasses = ['status-open', 'status-reopened', 'status-inprogress', 'status-test', 'status-closed', 'status-done'];
            card.classList.remove(...statusClasses);

            const statusMap = {
                'open': { class: 'status-open', name: 'Open' },
                'reopened': { class: 'status-reopened', name: 'Reopened' },
                'in progress': { class: 'status-inprogress', name: 'In Progress' },
                'test': { class: 'status-test', name: 'Test' },
                'closed': { class: 'status-closed', name: 'Closed' },
                'done': { class: 'status-done', name: 'Done' }
            };

            if (statusMap[statusText]) {
                card.classList.add(statusMap[statusText].class);
                const progressIndicator = document.createElement('div');
                progressIndicator.className = 'ghx-status-indicator';
                progressIndicator.title = `Status: ${statusMap[statusText].name}`;
                for (let i = 0; i < 3; i++) {
                    const bar = document.createElement('div');
                    bar.className = 'ghx-status-indicator-bar';
                    progressIndicator.appendChild(bar);
                }
                if(typeSpan) typeSpan.insertAdjacentElement('afterend', progressIndicator);
            }
        }

        // Verwerk de overige velden en sorteer ze in de juiste sub-containers.
        elementsToProcess.forEach(field => {
            if (field.matches('span[data-tooltip^="Status:"]') || field.classList.contains('ghx-extra-field-seperator')) {
                return;
            }

            if (field.matches('span[data-tooltip^="Labels:"]')) {
                const labelContentEl = field.querySelector('.ghx-extra-field-content');
                if (labelContentEl && labelContentEl.textContent.trim() !== 'None') {
                    const labels = labelContentEl.textContent.trim().split(/,\s*/).filter(Boolean);
                    labels.forEach(label => {
                        const labelLower = label.toLowerCase();
                        const action = labelActions[labelLower];
                        let addToRightAsDefault = true;

                        if (action?.type === 'custom') {
                            let targetContainer = labelsSubContainer;
                            if (['small', 'medium', 'large'].includes(labelLower)) {
                                targetContainer = sizeIconsSubContainer;
                            }
                            if (action.handler(card, label, targetContainer) === false) {
                                addToRightAsDefault = false;
                            }
                        }

                        if (addToRightAsDefault) {
                            const lozenge = document.createElement('span');
                            lozenge.className = 'aui-label';
                            lozenge.textContent = label;
                            if (action?.type === 'style') Object.assign(lozenge.style, action.styles);
                            labelsSubContainer.appendChild(lozenge);
                        }
                    });
                }
            } else {
                otherFieldsSubContainer.appendChild(field.cloneNode(true));
            }
        });

        // Verwijder de oude containers en voeg de nieuwe, geordende containers toe.
        containersToRemove.forEach(container => container.remove());
        mainRow.querySelector('.ghx-end')?.remove();

        // Volgorde: 1. Labels -> 2. S/M/L Iconen -> 3. Titel Categorie -> 4. Andere velden
        if (labelsSubContainer.hasChildNodes()) masterRightContainer.appendChild(labelsSubContainer);
        if (sizeIconsSubContainer.hasChildNodes()) masterRightContainer.appendChild(sizeIconsSubContainer);
        if (titleCategorySubContainer.hasChildNodes()) masterRightContainer.appendChild(titleCategorySubContainer);
        if (otherFieldsSubContainer.hasChildNodes()) masterRightContainer.appendChild(otherFieldsSubContainer);

        if (masterRightContainer.hasChildNodes()) {
            mainRow.appendChild(masterRightContainer);
        }

        card.classList.add('ghx-layout-processed');
    }

    /**
     * Zoekt naar onverwerkte issue-kaarten en voert `processSingleCard` voor elke kaart uit.
     */
    function runProcessor() {
        const cards = document.querySelectorAll('.ghx-issue-compact:not(.ghx-layout-processed)');
        if (cards.length > 0) {
            cards.forEach(processSingleCard);
        }
    }

    // --- Script Initialisatie ---
    injectCustomStyles();

    const observer = new MutationObserver((mutations) => {
        clearTimeout(observer.timeout);
        observer.timeout = setTimeout(runProcessor, 100);
    });

    observer.observe(document.body, { childList: true, subtree: true });

    setTimeout(runProcessor, 500);

})();