您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
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); })();