Jira assignee

Set Jira assignee

  1. // ==UserScript==
  2. // @name Jira assignee
  3. // @namespace http://tampermonkey.net/
  4. // @version 1.0.0
  5. // @description Set Jira assignee
  6. // @match https://hp-jira.external.hp.com/secure/RapidBoard.jspa*view=planning*
  7. // @icon https://www.google.com/s2/favicons?sz=64&domain=jira.com
  8. // @grant GM.setValue
  9. // @grant GM.getValue
  10. // @grant GM_registerMenuCommand
  11. // @license MIT
  12. // ==/UserScript==
  13.  
  14. (function() {
  15. 'use strict';
  16.  
  17. const injectCSS = css => {
  18. let el = document.createElement('style');
  19. el.type = 'text/css';
  20. el.innerText = css;
  21. document.head.appendChild(el);
  22. return el;
  23. };
  24.  
  25. 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;}
  26. .button-green {background-color: #2ea44f;}
  27. .button-pink {background-color: #EA4C89;}
  28. #assignee-list-config li {list-style-type: none}
  29. #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}
  30. #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}
  31. #assignee-list-config * {color: black;text-align: left;line-height: normal;font-size: 15px;min-height: 12px;}
  32. #assignee-list-config textarea { width: 300px;height: 400px;margin: 10px 0;}
  33. .last-assigned { background: lightblue; }
  34. `);
  35.  
  36. // this config is from backlog's quicker filters in header
  37. let assignes;
  38. let assignedTasks = {};
  39.  
  40. Promise.all([
  41. GM.getValue("assignee-config", '{}'),
  42. ]).then(function(values) {
  43. let assignesText = values[0];
  44. assignes = JSON.parse(assignesText);
  45.  
  46. GM_registerMenuCommand('Assignees config', function(){
  47. $("body").append(`<div id="assignee-list-config">
  48. <li>
  49. <span>Please input assignees config: <br>e.g <b>{"email": "display name"}</b></span>
  50. <textarea></textarea>
  51. </li>
  52. <li>
  53. <button id="assignee-list-config-ok">OK</button>
  54. <button id="assignee-list-config-cancel">Cancel</button>
  55. </li>
  56. </div>`);
  57.  
  58. $("#assignee-list-config textarea").val(JSON.stringify(assignes, null, 4));
  59.  
  60. $("#assignee-list-config textarea").change(function(){
  61. $("#assignee-list-config textarea").css("border-color", "");
  62. })
  63.  
  64. $("#assignee-list-config-ok").click(function(){
  65. try {
  66. let newAssignes = JSON.parse($("#assignee-list-config textarea").val());
  67. let newAssignesText = JSON.stringify(newAssignes);
  68.  
  69. if (newAssignesText !== assignesText) {
  70. assignes = newAssignes;
  71. assignesText = newAssignesText;
  72. GM.setValue("assignee-config", newAssignesText);
  73.  
  74. // remove old assignee list and add new one
  75. $(".assignee-list").remove();
  76. addAssignee();
  77. }
  78.  
  79. // remove config panel
  80. $("#assignee-list-config").remove();
  81. } catch (e) {
  82. $("#assignee-list-config textarea").css("border-color", "red");
  83. }
  84. })
  85.  
  86. $("#assignee-list-config-cancel").click(function(){
  87. $("#assignee-list-config").remove();
  88. })
  89. });
  90. })
  91.  
  92. function queryUserInfoFromCache(email) {
  93. let userInfo = localStorage.getItem("assignee_"+email);
  94. if (!userInfo) {
  95. fetch('https://hp-jira.external.hp.com/rest/api/2/user/search?username='+email, {
  96. method: 'GET',
  97. headers: {'Accept': 'application/json', 'Content-Type': 'application/json'},
  98. }).then(response => {
  99. return response.text();
  100. }).then(body => {
  101. if (body.length) {
  102. let data = JSON.parse(body)[0]
  103. console.log("-----------------", "email set in localStorage", email, data)
  104.  
  105. localStorage.setItem("assignee_"+email, JSON.stringify(data))
  106.  
  107. userInfo = data;
  108. }
  109. }).catch(err => console.error(err));
  110. }
  111.  
  112. return JSON.parse(userInfo)
  113. }
  114.  
  115. function assigneeEqual(email, text) {
  116. let userInfo = queryUserInfoFromCache(email);
  117. if (userInfo && userInfo.displayName === text) {
  118. return true
  119. }
  120.  
  121. if (assignes[email]) {
  122. let emailToName = email.replace("@hp.com", "").replaceAll(".", " ").replace(/[0-9]/g, '');
  123. text = text.toLowerCase();
  124.  
  125. return text.contains(assignes[email].toLowerCase()) || text.contains(emailToName)
  126. }
  127.  
  128. return false;
  129. }
  130.  
  131. function sleep(ms = 0) {
  132. return new Promise(resolve => setTimeout(resolve, ms));
  133. }
  134.  
  135. async function addAssignee() {
  136. while (true) {
  137. let planned = $(".ghx-sprint-planned");
  138. if (! planned.length) {
  139. console.log("-----------------class ghx-sprint-planned not found!");
  140. await sleep(300)
  141. continue
  142. }
  143. let tasks = planned.find(".js-issue-list").children();
  144. if (! tasks.length) {
  145. console.log("-----------------sprint tasks not found!");
  146. return
  147. }
  148.  
  149. if (_.isEmpty(assignes)) {
  150. await sleep(300)
  151. console.log("-----------------assignes not found!");
  152. return
  153. }
  154. // add story point hint
  155. $(".ghx-stat-total").each(function (index, value) {
  156. var estimate = $(this).find("aui-badge").text();
  157.  
  158. if (value && $(this).parents(".ghx-backlog-container").find(".estimate-count").attr("points") != estimate) {
  159. $(this).parents(".ghx-backlog-container").find(".estimate-count").remove();
  160. $(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>');
  161. }
  162. })
  163.  
  164. let lastAssignIssue = localStorage.getItem("last_assign_issue");
  165.  
  166. // add assignee
  167. tasks.each(function(index, element) {
  168. let issueId = $(this).attr("data-issue-key");
  169. if (issueId !== undefined) {
  170. if (lastAssignIssue === issueId) {
  171. $(this).addClass("last-assigned");
  172. }
  173.  
  174. if ($(this).find(".assignee-list").length) {
  175. return;
  176. }
  177.  
  178. // cut the shadow, and expose this tool to click
  179. $(this).find('.m-sortable-trigger').css("height", $(this).find(".ghx-issue-content").height());
  180.  
  181. let assignedTitle = "";
  182.  
  183. let assignedNode = $(this).find('.ghx-estimate');
  184. if (assignedNode.length === 1) {
  185. assignedTitle = assignedNode.children().first().attr("title").replace("Assignee: ", "");
  186. }
  187.  
  188. $(this).append('<div class="assignee-list"></div>');
  189.  
  190. for (const email in assignes) {
  191. let btnClass = "button-green";
  192.  
  193. // if has assigned from this tool, or assignee already existed
  194. if (
  195. assignedTasks[issueId] == email ||
  196. assignedTasks[issueId] == undefined && assigneeEqual(email, assignedTitle) && (assignedTasks[issueId] = email)
  197. ) {
  198. btnClass = "button-pink";
  199. }
  200.  
  201. $(this).children(".assignee-list").append('<button class="button '+btnClass+' set-assignee" email="'+email+'">'+assignes[email]+'</button>');
  202. }
  203. $(this).css("margin-bottom", "7px");
  204. }
  205. })
  206.  
  207. $(".assignee-list").unbind('click').click(function(e) {
  208. e.stopPropagation();
  209. });
  210.  
  211. $(".set-assignee").unbind('click').click(function(e) {
  212. if ($(this).hasClass("button-pink")) {
  213. return;
  214. }
  215.  
  216. let issueId = $(this).parent().parent().attr("data-issue-key");
  217. localStorage.setItem("last_assign_issue", issueId);
  218.  
  219. $(this).parents(".ghx-issues").find(".last-assigned").removeClass("last-assigned");
  220. $(this).parent().parent().addClass("last-assigned");
  221.  
  222. // remove color from others
  223. $(this).parent().find(".button-pink").removeClass("button-pink").addClass("button-green");
  224.  
  225. let changingBorderColor = setInterval(function flashText(target) {
  226. target.toggleClass("button-green").toggleClass("button-pink");
  227. }, 160, $(this));
  228.  
  229. let responseCode = 204;
  230. let email = $(this).attr("email");
  231.  
  232. fetch('https://hp-jira.external.hp.com/rest/api/2/issue/'+issueId, {
  233. method: 'PUT',
  234. body: `{"fields": {"assignee": {"name": "`+email+`"}}}`,
  235. headers: {'Accept': 'application/json', 'Content-Type': 'application/json'},
  236. }).then(response => {
  237. responseCode = response.status;
  238.  
  239. if (responseCode !== 204) {
  240. $(this).addClass("button-green").removeClass("button-pink");
  241. alert(`change assignee failed, Response status: ${response.status}`);
  242.  
  243. } else {
  244. $(this).removeClass("button-green").addClass("button-pink");
  245.  
  246. assignedTasks[issueId] = email;
  247. }
  248.  
  249. clearInterval(changingBorderColor);
  250.  
  251. return response.text();
  252. }).then(body => {
  253. if (responseCode !== 204) {
  254. console.log(`---------------------------------issue: ${issueId}, change assignee failed, status code ${responseCode}, response message ${body}`)
  255. }
  256. }).catch(err => console.error(err));
  257. });
  258.  
  259. await sleep(1000)
  260. }
  261. }
  262.  
  263. addAssignee();
  264.  
  265. let avatarColorDict = {
  266. "C": "#f691b2",
  267. "L": "#8eb021",
  268. "Y": "#654982",
  269. }
  270.  
  271. function getLetterCol(firstLetter) {
  272. if (firstLetter.length > 1) {
  273. firstLetter = firstLetter[0];
  274. }
  275.  
  276. firstLetter = firstLetter.toUpperCase();
  277. return avatarColorDict[firstLetter] || "#8eb021";
  278. }
  279.  
  280. // modify workload info
  281. $(document).on('DOMNodeInserted','#assigned-work-dialog',function() {
  282. // has not modified and display layer is show
  283. if (! $('#assigned-work-dialog').hasClass("fixed-by-script") && $("#aui-dialog-close").length) {
  284. // find out all assignee list boxes
  285. let sprintName = $("#assigned-work-dialog-title").text().replace("Workload by assignee - ", "");
  286. let assigneeList;
  287. $('span[data-fieldname="sprintName"]').each(function() {
  288. if ($(this).text() === sprintName) {
  289. assigneeList = $(this).parents(".ghx-backlog-container").find(".assignee-list");
  290. }
  291. })
  292.  
  293. if (assigneeList === undefined || ! assigneeList.length) {
  294. console.log("assigneeList not found, exit")
  295. return;
  296. }
  297.  
  298. // calculate workload
  299. let workloadList = {};
  300.  
  301. assigneeList.each(function(index, element) {
  302. let identifier = "Unassigned"
  303.  
  304. if ($(element).find(".button-pink").length) {
  305. identifier = $(element).find(".button-pink").attr("email");
  306. }
  307.  
  308. let point = parseFloat($(element).siblings(".ghx-issue-content").find(".ghx-estimate aui-badge").text());
  309.  
  310. if (!workloadList.hasOwnProperty(identifier)) {
  311. workloadList[identifier] = {"point": point, "issues": 1};
  312. } else {
  313. workloadList[identifier].point += point;
  314. workloadList[identifier].issues += 1;
  315. }
  316. })
  317.  
  318. $.each(workloadList, function(email, info) {
  319. let assigneeRowExist = false
  320.  
  321. $("#assigned-work-dialog").find("tbody tr").each(function(index, tr) {
  322. let firstTd = $(tr).children().first();
  323. let assignee = firstTd.text();
  324.  
  325. // assignee node has two format
  326. // <td><span class="ghx-no-avatar">Unassigned</span></td>
  327. // <td><img class="ghx-avatar-img" alt="" loading="lazy">assignee name</td>
  328. if (assignee !== "Unassigned") {
  329. assignee = firstTd.clone().children().remove().end().text();
  330. }
  331.  
  332. if (email === "Unassigned" || assigneeEqual(email, assignee)) {
  333. assigneeRowExist = true;
  334.  
  335. $('#assigned-work-dialog').addClass("fixed-by-script");
  336.  
  337. let issuesInTable = parseInt($(tr).children().eq(1).text());
  338. if (issuesInTable !== info.issues) {
  339. $(tr).children().eq(1).text(info.issues);
  340. $(tr).children().eq(2).text(info.point);
  341.  
  342. console.log(`change Workload for user: ${email}, issues: ${info.issues}, point: ${info.point}`);
  343. }
  344. return false;
  345. }
  346. })
  347.  
  348. if (!assigneeRowExist) {
  349. let userInfo = queryUserInfoFromCache(email);
  350.  
  351. if (!userInfo) {
  352. return;
  353. }
  354.  
  355. let firstLetter = userInfo.displayName[0];
  356. let color = getLetterCol(firstLetter);
  357. let image = `<span class="ghx-avatar-img ghx-auto-avatar" style="background-color: ${color}; ">${firstLetter}</span>`;
  358.  
  359. let avatar = userInfo.avatarUrls["48x48"];
  360. if (avatar !== "https://hp-jira.external.hp.com/secure/useravatar?avatarId=10122") {
  361. image = `<img src="${avatar}" class="ghx-avatar-img" alt="" loading="lazy">`;
  362. }
  363.  
  364.  
  365. $("#assigned-work-dialog").find("tbody").append(`<tr>
  366. <td>${image}${userInfo.displayName}</td>
  367. <td class="ghx-right">${info.issues}</td>
  368. <td class="ghx-right">${info.point}</td>
  369. </tr>`);
  370. }
  371. })
  372. }
  373. })
  374. })();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址