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