Jira - Verberg 'Labels: None'

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

Pada tanggal 01 Oktober 2025. Lihat %(latest_version_link).

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==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);

})();