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