Change card colors based on Jira task priority and add an emoji if a task has been in a status for more than 3 working days, excluding specific columns
// ==UserScript==
// @name Jira Task Priority Colorizer
// @namespace http://tampermonkey.net/
// @version 0.2.3
// @description Change card colors based on Jira task priority and add an emoji if a task has been in a status for more than 3 working days, excluding specific columns
// @author erolatex
// @include https://*/secure/RapidBoard.jspa*
// @grant none
// @license MIT
// ==/UserScript==
(function() {
'use strict';
// Columns to exclude from emoji update
const excludedColumns = ['ready for development', 'deployed'];
// CSS styles to change card colors and position the emoji
const styleContent = `
.ghx-issue[data-priority*="P0"] {
background-color: #FFADB0 !important;
}
.ghx-issue[data-priority*="P1"] {
background-color: #FF8488 !important;
}
.ghx-issue[data-priority*="P2"] {
background-color: #FFD3C6 !important;
}
.ghx-issue[data-priority*="P3"],
.ghx-issue[data-priority*="P4"] {
background-color: #FFF !important;
}
.stale-emoji {
position: absolute;
bottom: 5px;
right: 5px;
font-size: 14px;
display: flex;
align-items: center;
background-color: #d2b48c;
border-radius: 3px;
padding: 2px 4px;
font-weight: bold;
}
.stale-emoji span {
margin-left: 5px;
font-size: 12px;
color: #000;
}
.ghx-issue {
position: relative;
}
.column-badge.bad-count {
margin-left: 5px;
background-color: #d2b48c;
border-radius: 3px;
padding: 0 4px;
font-size: 11px;
color: #333;
font-weight: normal;
text-align: center;
display: inline-block;
vertical-align: middle;
line-height: 20px;
}
.ghx-limits {
display: flex;
align-items: center;
gap: 5px;
}
`;
// Inject CSS styles into the page
const styleElement = document.createElement('style');
styleElement.type = 'text/css';
styleElement.appendChild(document.createTextNode(styleContent));
document.head.appendChild(styleElement);
/**
* Calculates the number of working days between two dates.
* Excludes Saturdays and Sundays.
* @param {Date} startDate - The start date.
* @param {Date} endDate - The end date.
* @returns {number} - The number of working days.
*/
function calculateWorkingDays(startDate, endDate) {
let count = 0;
let currentDate = new Date(startDate);
// Set time to midnight to avoid timezone issues
currentDate.setHours(0, 0, 0, 0);
endDate = new Date(endDate);
endDate.setHours(0, 0, 0, 0);
while (currentDate <= endDate) {
const dayOfWeek = currentDate.getDay();
if (dayOfWeek !== 0 && dayOfWeek !== 6) { // Exclude Sunday (0) and Saturday (6)
count++;
}
currentDate.setDate(currentDate.getDate() + 1);
}
return count;
}
/**
* Calculates the number of working days based on the total days in the column.
* Assumes days are counted backward from the current date.
* @param {number} daysInColumn - Total number of days in the column.
* @returns {number} - The number of working days.
*/
function calculateWorkingDaysFromDaysInColumn(daysInColumn) {
let workingDays = 0;
let currentDate = new Date();
// Set time to midnight to avoid timezone issues
currentDate.setHours(0, 0, 0, 0);
for (let i = 0; i < daysInColumn; i++) {
const dayOfWeek = currentDate.getDay();
if (dayOfWeek !== 0 && dayOfWeek !== 6) { // Exclude Sunday (0) and Saturday (6)
workingDays++;
}
// Move to the previous day
currentDate.setDate(currentDate.getDate() - 1);
}
return workingDays;
}
/**
* Updates the priorities of the cards and adds/removes the emoji based on working days.
*/
function updateCardPriorities() {
// Disconnect the observer to prevent it from reacting to our changes
observer.disconnect();
let cards = document.querySelectorAll('.ghx-issue');
let poopCountPerColumn = {};
cards.forEach(card => {
// Update priority attribute
let priorityElement = card.querySelector('.ghx-priority');
if (priorityElement) {
let priority = priorityElement.getAttribute('title') || priorityElement.getAttribute('aria-label') || priorityElement.innerText || priorityElement.textContent;
if (priority) {
card.setAttribute('data-priority', priority);
}
}
// Initialize working days count
let workingDays = 0;
// Attempt to get the start date from a data attribute (e.g., data-start-date)
let startDateAttr = card.getAttribute('data-start-date'); // Example: '2024-04-25'
if (startDateAttr) {
let startDate = new Date(startDateAttr);
let today = new Date();
workingDays = calculateWorkingDays(startDate, today);
} else {
// If start date is not available, use daysInColumn
let daysElement = card.querySelector('.ghx-days');
if (daysElement) {
let title = daysElement.getAttribute('title');
if (title) {
let daysMatch = title.match(/(\d+)\s+days?/);
if (daysMatch && daysMatch[1]) {
let daysInColumn = parseInt(daysMatch[1], 10);
workingDays = calculateWorkingDaysFromDaysInColumn(daysInColumn);
}
}
}
}
// Check and update the emoji 💩
let columnElement = card.closest('.ghx-column');
if (workingDays > 3 && columnElement) {
let columnTitle = columnElement.textContent.trim().toLowerCase();
if (!excludedColumns.some(col => columnTitle.includes(col))) {
let existingEmoji = card.querySelector('.stale-emoji');
if (!existingEmoji) {
let emojiContainer = document.createElement('div');
emojiContainer.className = 'stale-emoji';
let emojiElement = document.createElement('span');
emojiElement.textContent = '💩';
let daysText = document.createElement('span');
daysText.textContent = `${workingDays} d`;
emojiContainer.appendChild(emojiElement);
emojiContainer.appendChild(daysText);
card.appendChild(emojiContainer);
} else {
let daysText = existingEmoji.querySelector('span:last-child');
daysText.textContent = `${workingDays} d`;
}
// Count poop emoji per column
let columnId = columnElement.getAttribute('data-column-id') || columnElement.getAttribute('data-id');
if (columnId) {
if (!poopCountPerColumn[columnId]) {
poopCountPerColumn[columnId] = 0;
}
poopCountPerColumn[columnId]++;
}
}
} else {
let existingEmoji = card.querySelector('.stale-emoji');
if (existingEmoji) {
existingEmoji.remove();
}
}
});
// Update poop count badges for each column
Object.keys(poopCountPerColumn).forEach(columnId => {
let columnHeader = document.querySelector(`.ghx-column[data-id="${columnId}"]`);
if (columnHeader) {
let limitsContainer = columnHeader.querySelector('.ghx-column-header .ghx-limits');
let existingBadge = columnHeader.querySelector('.column-badge.bad-count');
if (!existingBadge) {
// Change from 'div' to 'span' and adjust classes
existingBadge = document.createElement('span');
existingBadge.className = 'ghx-constraint aui-lozenge aui-lozenge-subtle column-badge bad-count';
}
existingBadge.textContent = `💩 ${poopCountPerColumn[columnId]}`;
existingBadge.style.display = 'inline-block';
if (limitsContainer) {
let maxBadge = limitsContainer.querySelector('.ghx-constraint.ghx-busted-max');
if (maxBadge) {
limitsContainer.insertBefore(existingBadge, maxBadge);
} else {
limitsContainer.appendChild(existingBadge);
}
} else {
// If limitsContainer doesn't exist, create it
limitsContainer = document.createElement('div');
limitsContainer.className = 'ghx-limits';
limitsContainer.appendChild(existingBadge);
let headerContent = columnHeader.querySelector('.ghx-column-header-content');
if (headerContent) {
headerContent.appendChild(limitsContainer);
}
}
}
});
// Reconnect the observer after making changes
observer.observe(document.body, { childList: true, subtree: true });
}
// MutationObserver to watch for changes in the DOM and update priorities accordingly
const observer = new MutationObserver(() => {
updateCardPriorities();
});
// Start observing the body
observer.observe(document.body, { childList: true, subtree: true });
// Update priorities when the page loads
window.addEventListener('load', function() {
updateCardPriorities();
});
// Periodically update priorities every 5 seconds
setInterval(updateCardPriorities, 5000);
})();