您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Changes jira display for custom label styling. Specific for my usecase. Don't bother downloading if you are not a coworker.
// ==UserScript== // @name Jira - Custom script specific for work usecase // @namespace http://tampermonkey.net/ // @version 6.8 // @description Changes jira display for custom label styling. Specific for my usecase. Don't bother downloading if you are not a coworker. // @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); })();