Jira Task Priority Colorizer

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

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

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