Greasy Fork

来自缓存

Greasy Fork is available in English.

Jira assignee

Set Jira assignee

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

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

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

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

您需要先安装一款用户脚本管理器扩展,例如 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>`);
                }
            })
        }
    })
})();