Jira assignee

Set Jira assignee

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name         Jira assignee
// @namespace    http://tampermonkey.net/
// @version      1.0.0
// @description  Set Jira assignee
// @match        https://hp-jira.external.hp.com/secure/RapidBoard.jspa*view=planning*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=jira.com
// @grant        GM.setValue
// @grant        GM.getValue
// @grant        GM_registerMenuCommand
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';

    const injectCSS = css => {
        let el = document.createElement('style');
        el.type = 'text/css';
        el.innerText = css;
        document.head.appendChild(el);
        return el;
    };

    injectCSS(`.button {margin: 0 5px 5px 5px;appearance: none;border: 2px solid rgba(27, 31, 35, .15);border-radius: 6px;box-shadow: rgba(27, 31, 35, .1) 0 1px 0;box-sizing: border-box;color: #fff;cursor: pointer;display: inline-block;font-family: -apple-system,system-ui,"Segoe UI",Helvetica,Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji";font-size: 13px;font-weight: 600;line-height: 10px;padding: 5px 6px;position: relative;text-align: center;text-decoration: none;user-select: none;-webkit-user-select: none;touch-action: manipulation;vertical-align: middle;white-space: nowrap;}
        .button-green {background-color: #2ea44f;}
        .button-pink {background-color: #EA4C89;}
        #assignee-list-config li {list-style-type: none}
        #assignee-list-config {position: fixed;z-index: 10000000;top: 0px;right: 60px;width: 360px;height: 482px; padding: 30px; background: -webkit-gradient(linear, 0 0, 0 100%, from(#fcfcfc), to(#f2f2f7)) !important}
        #assignee-list-config button {margin: 0 26px;text-align: center;white-space: nowrap;background-color: #f9f9f9 !important;border: 1px solid #ccc !important;box-shadow: inset 0 10px 5px white !important;border-radius: 3px !important;padding: 3px 3px !important; width: 100px}
        #assignee-list-config * {color: black;text-align: left;line-height: normal;font-size: 15px;min-height: 12px;}
        #assignee-list-config textarea { width: 300px;height: 400px;margin: 10px 0;}
        .last-assigned { background: lightblue; }
    `);

    // this config is from backlog's quicker filters in header
    let assignes;
    let assignedTasks = {};

    Promise.all([
        GM.getValue("assignee-config", '{}'),
    ]).then(function(values) {
        let assignesText = values[0];
        assignes = JSON.parse(assignesText);

        GM_registerMenuCommand('Assignees config', function(){
            $("body").append(`<div id="assignee-list-config">
                <li>
                    <span>Please input assignees config: <br>e.g <b>{"email": "display name"}</b></span>
                    <textarea></textarea>
                </li>
                <li>
                    <button id="assignee-list-config-ok">OK</button>
                    <button id="assignee-list-config-cancel">Cancel</button>
                </li>
            </div>`);

            $("#assignee-list-config textarea").val(JSON.stringify(assignes, null, 4));

            $("#assignee-list-config textarea").change(function(){
                $("#assignee-list-config textarea").css("border-color", "");
            })

            $("#assignee-list-config-ok").click(function(){
                try {
                    let newAssignes = JSON.parse($("#assignee-list-config textarea").val());
                    let newAssignesText = JSON.stringify(newAssignes);

                    if (newAssignesText !== assignesText) {
                        assignes = newAssignes;
                        assignesText = newAssignesText;
                        GM.setValue("assignee-config", newAssignesText);

                        // remove old assignee list and add new one
                        $(".assignee-list").remove();
                        addAssignee();
                    }

                    // remove config panel
                    $("#assignee-list-config").remove();
                } catch (e) {
                    $("#assignee-list-config textarea").css("border-color", "red");
                }
            })

            $("#assignee-list-config-cancel").click(function(){
                $("#assignee-list-config").remove();
            })
        });
    })

    function queryUserInfoFromCache(email) {
        let userInfo = localStorage.getItem("assignee_"+email);
        if (!userInfo) {
            fetch('https://hp-jira.external.hp.com/rest/api/2/user/search?username='+email, {
                method: 'GET',
                headers: {'Accept': 'application/json', 'Content-Type': 'application/json'},
            }).then(response => {
                return response.text();
            }).then(body => {
                if (body.length) {
                    let data = JSON.parse(body)[0]
                    console.log("-----------------", "email set in localStorage", email, data)

                    localStorage.setItem("assignee_"+email, JSON.stringify(data))

                    userInfo = data;
                }
            }).catch(err => console.error(err));
        }

        return JSON.parse(userInfo)
    }

    function assigneeEqual(email, text) {
        let userInfo = queryUserInfoFromCache(email);
        if (userInfo && userInfo.displayName === text) {
            return true
        }

        if (assignes[email]) {
            let emailToName = email.replace("@hp.com", "").replaceAll(".", " ").replace(/[0-9]/g, '');
            text = text.toLowerCase();

            return text.contains(assignes[email].toLowerCase()) || text.contains(emailToName)
        }

        return false;
    }

    function sleep(ms = 0) {
        return new Promise(resolve => setTimeout(resolve, ms));
    }

    async function addAssignee() {
        while (true) {
            let planned = $(".ghx-sprint-planned");
            if (! planned.length) {
                console.log("-----------------class ghx-sprint-planned not found!");
                await sleep(300)
                continue
            }
            let tasks = planned.find(".js-issue-list").children();
            if (! tasks.length) {
                console.log("-----------------sprint tasks not found!");
                return
            }

            if (_.isEmpty(assignes)) {
                await sleep(300)
                console.log("-----------------assignes not found!");
                return
            }
            // add story point hint
            $(".ghx-stat-total").each(function (index, value) {
                var estimate = $(this).find("aui-badge").text();

                if (value && $(this).parents(".ghx-backlog-container").find(".estimate-count").attr("points") != estimate) {
                    $(this).parents(".ghx-backlog-container").find(".estimate-count").remove();
                    $(this).parents(".ghx-backlog-container").find(".ghx-issue-count").after('<div class="estimate-count" style="font-weight: bold; display: inline; margin-left: 10px;" points="'+estimate+'">Total points: ' + estimate +'</div>');
                }
            })

            let lastAssignIssue = localStorage.getItem("last_assign_issue");

            // add assignee
            tasks.each(function(index, element) {
                let issueId = $(this).attr("data-issue-key");
                if (issueId !== undefined) {
                    if (lastAssignIssue === issueId) {
                        $(this).addClass("last-assigned");
                    }

                    if ($(this).find(".assignee-list").length) {
                        return;
                    }

                    // cut the shadow, and expose this tool to click
                    $(this).find('.m-sortable-trigger').css("height", $(this).find(".ghx-issue-content").height());

                    let assignedTitle = "";

                    let assignedNode = $(this).find('.ghx-estimate');
                    if (assignedNode.length === 1) {
                        assignedTitle = assignedNode.children().first().attr("title").replace("Assignee: ", "");
                    }

                    $(this).append('<div class="assignee-list"></div>');

                    for (const email in assignes) {
                        let btnClass = "button-green";

                        // if has assigned from this tool, or assignee already existed
                        if (
                            assignedTasks[issueId] == email ||
                            assignedTasks[issueId] == undefined && assigneeEqual(email, assignedTitle) && (assignedTasks[issueId] = email)
                        ) {
                            btnClass = "button-pink";
                        }

                        $(this).children(".assignee-list").append('<button class="button '+btnClass+' set-assignee" email="'+email+'">'+assignes[email]+'</button>');
                    }
                    $(this).css("margin-bottom", "7px");
                }
            })

            $(".assignee-list").unbind('click').click(function(e) {
                e.stopPropagation();
            });

            $(".set-assignee").unbind('click').click(function(e) {
                if ($(this).hasClass("button-pink")) {
                    return;
                }

                let issueId = $(this).parent().parent().attr("data-issue-key");
                localStorage.setItem("last_assign_issue", issueId);

                $(this).parents(".ghx-issues").find(".last-assigned").removeClass("last-assigned");
                $(this).parent().parent().addClass("last-assigned");

                // remove color from others
                $(this).parent().find(".button-pink").removeClass("button-pink").addClass("button-green");

                let changingBorderColor = setInterval(function flashText(target) {
                        target.toggleClass("button-green").toggleClass("button-pink");
                }, 160, $(this));

                let responseCode = 204;
                let email = $(this).attr("email");

                fetch('https://hp-jira.external.hp.com/rest/api/2/issue/'+issueId, {
                    method: 'PUT',
                    body: `{"fields": {"assignee": {"name": "`+email+`"}}}`,
                    headers: {'Accept': 'application/json', 'Content-Type': 'application/json'},
                }).then(response => {
                    responseCode = response.status;

                    if (responseCode !== 204) {
                        $(this).addClass("button-green").removeClass("button-pink");
                        alert(`change assignee failed, Response status: ${response.status}`);

                    } else {
                        $(this).removeClass("button-green").addClass("button-pink");

                        assignedTasks[issueId] = email;
                    }

                    clearInterval(changingBorderColor);

                    return response.text();
                }).then(body => {
                    if (responseCode !== 204) {
                        console.log(`---------------------------------issue: ${issueId}, change assignee failed, status code ${responseCode}, response message ${body}`)
                    }
                }).catch(err => console.error(err));
            });

            await sleep(1000)
        }
    }

    addAssignee();

    let avatarColorDict = {
        "C": "#f691b2",
        "L": "#8eb021",
        "Y": "#654982",
    }

    function getLetterCol(firstLetter) {
        if (firstLetter.length > 1) {
            firstLetter = firstLetter[0];
        }

        firstLetter = firstLetter.toUpperCase();
        return avatarColorDict[firstLetter] || "#8eb021";
    }

    // modify workload info
    $(document).on('DOMNodeInserted','#assigned-work-dialog',function() {
        // has not modified and display layer is show
        if (! $('#assigned-work-dialog').hasClass("fixed-by-script") && $("#aui-dialog-close").length) {
            // find out all assignee list boxes
            let sprintName = $("#assigned-work-dialog-title").text().replace("Workload by assignee - ", "");
            let assigneeList;
            $('span[data-fieldname="sprintName"]').each(function() {
                if ($(this).text() === sprintName) {
                    assigneeList = $(this).parents(".ghx-backlog-container").find(".assignee-list");
                }
            })

            if (assigneeList === undefined || ! assigneeList.length) {
                console.log("assigneeList not found, exit")
                return;
            }

            // calculate workload
            let workloadList = {};

            assigneeList.each(function(index, element) {
                let identifier = "Unassigned"

                if ($(element).find(".button-pink").length) {
                    identifier = $(element).find(".button-pink").attr("email");
                }

                let point = parseFloat($(element).siblings(".ghx-issue-content").find(".ghx-estimate aui-badge").text());

                if (!workloadList.hasOwnProperty(identifier)) {
                    workloadList[identifier] = {"point": point, "issues": 1};
                } else {
                    workloadList[identifier].point += point;
                    workloadList[identifier].issues += 1;
                }
            })

            $.each(workloadList, function(email, info) {
                let assigneeRowExist = false

                $("#assigned-work-dialog").find("tbody tr").each(function(index, tr) {
                    let firstTd = $(tr).children().first();
                    let assignee = firstTd.text();

                    // assignee node has two format
                    // <td><span class="ghx-no-avatar">Unassigned</span></td>
                    // <td><img class="ghx-avatar-img" alt="" loading="lazy">assignee name</td>
                    if (assignee !== "Unassigned") {
                        assignee = firstTd.clone().children().remove().end().text();
                    }

                    if (email === "Unassigned" || assigneeEqual(email, assignee)) {
                        assigneeRowExist = true;

                        $('#assigned-work-dialog').addClass("fixed-by-script");

                        let issuesInTable = parseInt($(tr).children().eq(1).text());
                        if (issuesInTable !== info.issues) {
                            $(tr).children().eq(1).text(info.issues);
                            $(tr).children().eq(2).text(info.point);

                            console.log(`change Workload for user: ${email}, issues: ${info.issues}, point: ${info.point}`);
                        }
                        return false;
                    }
                })

                if (!assigneeRowExist) {
                    let userInfo = queryUserInfoFromCache(email);

                    if (!userInfo) {
                        return;
                    }

                    let firstLetter = userInfo.displayName[0];
                    let color = getLetterCol(firstLetter);
                    let image = `<span class="ghx-avatar-img ghx-auto-avatar" style="background-color: ${color}; ">${firstLetter}</span>`;

                    let avatar = userInfo.avatarUrls["48x48"];
                    if (avatar !== "https://hp-jira.external.hp.com/secure/useravatar?avatarId=10122") {
                        image = `<img src="${avatar}" class="ghx-avatar-img" alt="" loading="lazy">`;
                    }


                    $("#assigned-work-dialog").find("tbody").append(`<tr>
                        <td>${image}${userInfo.displayName}</td>
                        <td class="ghx-right">${info.issues}</td>
                        <td class="ghx-right">${info.point}</td>
                    </tr>`);
                }
            })
        }
    })
})();