您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Custom quick actions (commit log, work log), check of issues (status, assignee) and show week work log in JIRA 6.x
// ==UserScript== // @name Jira quick actions // @namespace sremy // @version 1.12 // @description Custom quick actions (commit log, work log), check of issues (status, assignee) and show week work log in JIRA 6.x // @author Sébastien REMY // @match https://*jira*/browse/* // @match http://localhost:8080/browse/* // @require https://cdn.jsdelivr.net/npm/clipboard@2/dist/clipboard.min.js // @require https://cdn.jsdelivr.net/npm/[email protected]/notify.min.js // @grant none // ==/UserScript== // ** Constants to customize ** // Name of custom field for "Epic link" const CUSTOM_FIELD_EPIC_LINK = 'customfield_10006'; //const CUSTOM_FIELD_EPIC_LINK = jQuery('div[data-fieldtype="gh-epic-link"]').attr('id') // Customize these status for the function checkIssueStatus(,) ! const INITIAL_STATUS_LIST = ['TO DO', 'OPEN']; const NEXT_STATUS_SELECTOR_LIST = ['#action_id_21', '#action_id_41']; var $ = jQuery; // or https://cdn.jsdelivr.net/npm/[email protected]/dist/jquery.min.js let worklogToClipboard; let clipboard = new ClipboardJS('#clipboardBtn', { text: function(trigger) { return $('#key-val').text() + ' ' + $('#summary-val').text(); } }); clipboard.on('success', function(e) { $.notify("Copied to clipboard. " + e.text, "info"); }); clipboard.on('error', function(e) { $.notify("Failed to copy", "error"); }); function sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } function rafAsync() { return new Promise(resolve => { // Callback function called before the browser performs the next repaint requestAnimationFrame(resolve); // faster than set time out }); } // Waits until the element pointed by selector exists on the page function whenElementExists(selector) { if (document.querySelector(selector) === null) { return rafAsync().then(() => whenElementExists(selector)); } else { return Promise.resolve(true); } } /* async function addWorkLog(duration) { jQuery('#log-work').click(); whenElementExists('#log-work-time-logged') // work log input text .then(() => { jQuery('#log-work-time-logged')[0].value = duration; jQuery('#log-work-submit').click(); }); // BUG: query has no time to be sent, overrided by following // assignToMe(); checkIssueStatus(); }*/ function addWorkLogDateRest(worklogDate, duration, callback = {}) { // POST /rest/api/2/issue/{issueIdOrKey}/worklog worklogDate = worklogDate.replace(/Z/, '+0000'); //console.log(worklogDate); let bodyContent = { "timeSpent": duration, "started": worklogDate // "2020-02-29T00:46:47.624+0000" }; const options = { method: 'POST', body: JSON.stringify(bodyContent), headers: { 'Content-Type': 'application/json' } }; const issueId = JIRA.Issue.getIssueId(); fetch(`/rest/api/2/issue/${issueId}/worklog`, options) .then(callback) .then(refreshPage); //.then(res => res.json()) //.then(res => console.log(res)); if(getAssignee() === undefined) { assignToMe(); } checkIssueStatus(INITIAL_STATUS_LIST, selectFirstExistingStatus(NEXT_STATUS_SELECTOR_LIST)); } // Add worklog starting now function addWorkLogNowRest(duration) { addWorkLogDateRest((new Date().toISOString()).replace(/Z/, '+0000'), duration); } // Return the selector status present in the page among the list function selectFirstExistingStatus(statusSelectorList) { for(let status of statusSelectorList) { if($(status).length > 0) { return status; } } return null; } // Return true if the string 'searchedString' is contained (ignoring case) in the array 'stringslist' function isInList(searchedString, stringslist) { if (!searchedString) return false; for (let item of stringslist) { if (item && item.toUpperCase() === searchedString.toUpperCase()) { return true; } } return false; } // If issue status is still the initial one, I move to the next one function checkIssueStatus(initialStatusList, nextStatusSelector) { if(nextStatusSelector === null) return; let selectIssueStatus = jQuery('#status-val'); if(selectIssueStatus.length) { let status = selectIssueStatus[0].innerText; if(isInList(status, initialStatusList)) { let selectNextStatus = $(nextStatusSelector); if(selectNextStatus.length === 1) { $.notify("Status was still " + status + " => changing to " + selectNextStatus[0].innerText, "warn"); selectNextStatus.click(); } } } } // If issue is not assigned to me, I assign to me function assignToMe() { const SELECTOR_ASSIGN_TO_ME = '#assign-to-me'; let selectAssignToMe = jQuery(SELECTOR_ASSIGN_TO_ME); if(selectAssignToMe.length) { $.notify("Issue assigned to you", "info"); selectAssignToMe[0].click(); } } // Get the User assigned to the current issue or return undefined if not assigned function getAssignee() { let spanAssignee = jQuery("#assignee-val"); if(spanAssignee.length) { return spanAssignee.children('span[rel]').attr('rel'); } return undefined; } function reqListener () { console.log(this.responseText); } function jsonReqListener () { console.log(this.response); } function getCookie(cname) { var name = cname + "="; var decodedCookie = decodeURIComponent(document.cookie); var ca = decodedCookie.split(';'); for(var i = 0; i <ca.length; i++) { var c = ca[i]; while (c.charAt(0) == ' ') { c = c.substring(1); } if (c.indexOf(name) == 0) { return c.substring(name.length, c.length); } } return ""; } // linkType = 'clones', 'blocks', etc // issueKeyLinked = JIRA-007 function createLinkRest(linkType, issueKeyLinked) { let issueId = $('#key-val').attr('rel'); // example: 10003 let atlToken = getCookie('atlassian.xsrf.token'); let params = `inline=true&decorator=dialog&id=${issueId}&jiraAppId=&linkDesc=${linkType}&issueKeys=${issueKeyLinked}&comment=&atl_token=${atlToken}`; //console.log(params); let req = new XMLHttpRequest(); //req.addEventListener("load", reqListener); req.open("POST", "/secure/LinkJiraIssue.jspa"); req.addEventListener("load", refreshPage); req.setRequestHeader('Content-type', 'application/x-www-form-urlencoded; charset=UTF-8'); req.send(params); refreshPage(); } // issueKeyLinked = JIRA-007 // Set the constant customField to reflect the Epic custom field (or what you want) function setEpicRest(epicIssueKey) { setCustomFieldRest(CUSTOM_FIELD_EPIC_LINK, epicIssueKey); } function setCustomFieldRest(customField, epicIssueKey) { let issueId = $('#key-val').attr('rel'); // example: 10003 let atlToken = getCookie('atlassian.xsrf.token'); let params = `${customField}=key%3A${epicIssueKey}&issueId=${issueId}&atl_token=${atlToken}&singleFieldEdit=true&fieldsToForcePresent=${customField}`; //console.log(params); let req = new XMLHttpRequest(); req.open("POST", "/secure/AjaxIssueAction.jspa"); req.addEventListener("load", refreshPage); req.setRequestHeader('Content-type', 'application/x-www-form-urlencoded; charset=UTF-8'); req.send(params); } /* issue_list = [ { "expand": "operations,editmeta,changelog,transitions,renderedFields", "id": "10200", "self": "http://localhost:8080/rest/api/2/issue/10200", "key": "SEB-7", "fields": { "worklog": { "startAt": 0, "maxResults": 20, "total": 5, "worklogs": [ { "self": "http://localhost:8080/rest/api/2/issue/10200/worklog/10300", "author": { "self": "http://localhost:8080/rest/api/2/user?username=admin", "name": "admin", "key": "admin", */ function filterWorklog(issue_list, author) { let worklogByDay = {}; let issuesWorklogged = {}; const {start_date, end_date} = getWeekLimit(); issue_list.forEach(issue => { let worklogList = issue.fields.worklog.worklogs worklogList.forEach(wl => { let wlday = new Date(wl.started) if(wl.author.key === author && wlday >= start_date && wlday <= end_date) { // keep this worklog & issue let dayOfWl = formatSlashDate(wlday); let wlDay = worklogByDay[dayOfWl] if(wlDay === undefined) { worklogByDay[dayOfWl] = {}; worklogByDay[dayOfWl].list = []; worklogByDay[dayOfWl].timeSpent = 0; wlDay = worklogByDay[dayOfWl]; } wl['issue'] = issue; wl['issuekey'] = issue.key; wl['issueid'] = issue.id; wlDay.list.push(wl); wlDay.timeSpent += wl.timeSpentSeconds; issuesWorklogged[issue.key] = issue.fields.summary; } }); }); //console.log(worklogByDay); // Export an issues list KEY: Summary for clipboard let summary = ''; for(let jirakey in issuesWorklogged) { summary += jirakey + ': ' + issuesWorklogged[jirakey] + '\n'; } // Choose one of the following: alert dialog or copy to clipboard //jQuery('#btn-copy-worklog').on('click', () => showWorkLogSummary(summary)); if(worklogToClipboard !== undefined) { worklogToClipboard.destroy(); } let options = { text: function(trigger) { $.notify("Copied:\n" + summary, "info"); return summary; } }; worklogToClipboard = new ClipboardJS('#btn-copy-worklog', options); return worklogByDay; } function showWorkLogSummary(summary) { alert(summary); } // Returns the date of Monday and Friday of the current week function getWeekLimit(){ let monday = new Date(); monday.setHours(0); monday.setMinutes(0); monday.setSeconds(0); monday.setMilliseconds(0); let friday = new Date(monday); let monday_day = monday.getDate() - monday.getDay() + 1; monday.setDate(monday_day); let saturday_day = monday_day + 5; friday.setDate(saturday_day); friday.setMilliseconds(-1); return {start_date: monday, end_date: friday}; } function jumpNDays(date, daysToAdd) { date.setDate(date.getDate() + daysToAdd); return date; } function formatSlashDate(date) {// ((date.getMonth() > 8) ? (date.getMonth() + 1) : ('0' + (date.getMonth() + 1))) + '/' + ((date.getDate() > 9) ? date.getDate() : ('0' + date.getDate())) + '/' + date.getFullYear() let mois = date.getMonth() + 1; return ((date.getDate() > 9) ? date.getDate() : ('0' + date.getDate())) + '/' + ((mois > 9) ? mois : ('0' + mois)) + '/' + date.getFullYear() } // Formats date to YYYY-MM-DD function formatDate(date) { let mois = date.getMonth() + 1; return date.getFullYear() + '-' + (mois < 10 ? '0' : '') + mois + '-' + (date.getDate() < 10 ? '0' + date.getDate() : date.getDate()); } function getWeekWorkLog() { // GET {{protocol}}://{{host}}/{{basePath}}rest/api/2/search?jql=(worklogDate >= "2020-02-24" AND worklogDate <= "2020-02-28") AND assignee = currentUser() &fields=worklog,summary const {start_date, end_date} = getWeekLimit(); let queryparams = `jql=(worklogDate >= '${formatDate(start_date)}' AND worklogDate <= '${formatDate(end_date)}') AND worklogAuthor = currentUser()&fields=worklog,summary`; let req = new XMLHttpRequest(); req.addEventListener("load", workLogProcessor); req.open("GET", "/rest/api/2/search?" + queryparams, true); req.responseType = 'json'; req.setRequestHeader('Content-type', 'application/json; charset=UTF-8'); req.send(); } async function fetchLargeWorklog(issue_list) { let cumulateWorklog = function(res, issue) { //console.log(res); issue.fields.worklog = res; }; let queries = []; issue_list.forEach(issue => { let worklog = issue.fields.worklog; if(worklog.total > worklog.maxResults) { const options = { method: 'GET', headers: { 'Content-Type': 'application/json' } }; queries.push( fetch(`/rest/api/2/issue/${issue.key}/worklog`, options) .then(res => res.json()) .then(res => cumulateWorklog(res, issue)) ); } }); await Promise.all(queries); } async function workLogProcessor() { let issue_list = this.response['issues']; if(issue_list === undefined) { console.log('No response. Not logged in?'); $('#worklog-day-1').append('<div style="font-weight: bold">No response</div>'); return; } //console.log(issue_list); let worklogByDay = await fetchLargeWorklog(issue_list) .then(() => {return filterWorklog(issue_list, JIRA.Users.LoggedInUser.userName())}); //let worklogByDay = filterWorklog(issue_list, JIRA.Users.LoggedInUser.userName()); let weekLimit = getWeekLimit(); let iday = weekLimit.start_date; $('#week-start').get(0).innerText = formatSlashDate(weekLimit.start_date); $('#week-end').get(0).innerText = formatSlashDate(weekLimit.end_date); for(let i = 1; i <= 5; i++) { let dayCol = $('#worklog-day-' + i); const english_days = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"]; let dayOfWl = formatSlashDate(iday); let spanTimeSpentInDay = ''; if(worklogByDay[dayOfWl] !== undefined && worklogByDay[dayOfWl].timeSpent !== undefined) { let durationInHours = (worklogByDay[dayOfWl].timeSpent / 3600); if(durationInHours === 8) { spanTimeSpentInDay = `<span class="roundtext greentext">${durationInHours}h</span>`; } else { spanTimeSpentInDay = `<span class="roundtext redtext">${durationInHours}h</span>`; } } dayCol.append(`<div style="font-weight: bold">${english_days[iday.getDay()]}</div> <div style="border-bottom: 1px solid #ccc; margin-bottom: 15px;">${dayOfWl}${spanTimeSpentInDay}</div>`); //console.log(worklogByDay[dayOfWl]); if(worklogByDay[dayOfWl] !== undefined) { for(let wl of worklogByDay[dayOfWl].list) { let link = window.location.toString().replace(/\/[^\/]*$/, '/' + wl.issuekey); dayCol.append(`<div style="margin: 5px;"><a href="${link}" title="${wl.issuekey} => ${wl.issue.fields.summary}" >${wl.issuekey}</a>:<span class="roundtext">${wl.timeSpent}</span></div>`); } } jumpNDays(iday, 1); } let wlDialog = jQuery('#show-worklog-dialog').get(0); wlDialog.style.marginTop = -wlDialog.offsetHeight/2 +"px"; } function showWeekWorkLogDIalog() { let htmlDialog = ` <div id="show-worklog-dialog" class="jira-dialog box-shadow jira-dialog-open popup-width-custom jira-dialog-content-ready" style="width: 810px; margin-left: -406px; margin-top: -150px;"> <div class="jira-dialog-heading"> <div class="aui-toolbar2 qf-form-operations"> <div class="aui-toolbar2-inner"> <div class="aui-toolbar2-secondary"> <button id="btn-copy-worklog" class="aui-button">Copy summary</button> </div> </div> </div> <h2 title="Work Log">Work log</h2> </div> <div class="jira-dialog-content"> <div class="qf-container"> <div class="qf-unconfigurable-form"> <form name="jiraform" action="#" class="aui"> <div class="form-body" style="max-height: 600px;"> <div class="qf-field"> <div style="margin: 10px;"> <label>Week</label> <label> from </label> <label style="color: black;" id="week-start">-</label> <label> to </label> <label style="color: black;" id="week-end">-</label> </div> </div> <div class="container"> <div class="row"> <div id="worklog-day-1" class="col item"> </div> <div id="worklog-day-2" class="col item"> </div> <div id="worklog-day-3" class="col item"> </div> <div id="worklog-day-4" class="col item"> </div> <div id="worklog-day-5" class="col item"> </div> </div> </div> <div class="buttons-container form-footer"> <div class="buttons"> <span class="icon throbber"></span> <a href="#" title="Press Esc to cancel" class="cancel" onclick='let element = document.getElementById("show-worklog-dialog"); element.parentNode.removeChild(element); event.preventDefault();'>Close</a> </div> </div> </div> </form> </div> </div> </div> </div> `; $('#jira').append(htmlDialog); getWeekWorkLog(); } function refreshPage() { JIRA.trigger(JIRA.Events.REFRESH_ISSUE_PAGE, [JIRA.Issue.getIssueId()]); } function init() { 'use strict'; if(JIRA.Users.LoggedInUser.isAnonymous()) { return; } if(!$('#clipboardBtn').length) { $('.toolbar-split-left').append('<button id="clipboardBtn" class="aui-button aui-style"> <svg viewBox="0 0 896 1024" width="15" xmlns="http://www.w3.org/2000/svg"> <path d="M128 768h256v64H128v-64z m320-384H128v64h320v-64z m128 192V448L384 640l192 192V704h320V576H576z m-288-64H128v64h160v-64zM128 704h160v-64H128v64z m576 64h64v128c-1 18-7 33-19 45s-27 18-45 19H64c-35 0-64-29-64-64V192c0-35 29-64 64-64h192C256 57 313 0 384 0s128 57 128 128h192c35 0 64 29 64 64v320h-64V320H64v576h640V768zM128 256h512c0-35-29-64-64-64h-64c-35 0-64-29-64-64s-29-64-64-64-64 29-64 64-29 64-64 64h-64c-35 0-64 29-64 64z" /> </svg> </button>'); $('.toolbar-split-left').append('<ul id="worklog-issue_container" class="toolbar-group pluggable-ops">\ <li class="toolbar-item">\ <a id="worklog2h-issue" title="Add 2h in work log" class="toolbar-trigger"><span class="trigger-label">2h</span></a>\ </li>\ <li class="toolbar-item">\ <a id="worklog4h-issue" title="Add 4h in work log" class="toolbar-trigger"><span class="trigger-label">4h</span></a>\ </li>\ <li class="toolbar-item">\ <a id="worklog1d-issue" title="Add 1d in work log" class="toolbar-trigger"><span class="trigger-label">1d</span></a>\ </li>\ </ul>\ '); jQuery('#worklog2h-issue').on('click', () => addWorkLogNowRest('2h')); jQuery('#worklog4h-issue').on('click', () => addWorkLogNowRest('4h')); jQuery('#worklog1d-issue').on('click', () => addWorkLogNowRest('1d')); $('.toolbar-split-left').append('<ul id="epic-issue_container" class="toolbar-group pluggable-ops">\ <li class="toolbar-item">\ <a id="technic-epic-issue" title="Set Epic" class="toolbar-trigger"><span class="trigger-label">Set Epic</span></a>\ </li>\ </ul>\ '); jQuery('#technic-epic-issue').on('click', () => setEpicRest('SEB-6')); $('.toolbar-split-left').append('<ul id="custom-link-issue_container" class="toolbar-group pluggable-ops">\ <li class="toolbar-item">\ <a id="custom-link-issue" title="Add Link" class="toolbar-trigger"><span class="trigger-label">Add Link</span></a>\ </li>\ </ul>\ ') jQuery('#custom-link-issue').on('click', () => createLinkRest('blocks', 'SEB-3')); $('.toolbar-split-left').append('<ul id="custom-get-weekworklog_container" class="toolbar-group pluggable-ops">\ <li class="toolbar-item">\ <a id="custom-get-weekworklog" title="Get week work log" class="toolbar-trigger"><span class="trigger-label">Show week work log</span></a>\ </li>\ </ul>\ ') //jQuery('#custom-get-weekworklog').on('click', () => getWeekWorkLog()); jQuery('#custom-get-weekworklog').on('click', () => showWeekWorkLogDIalog()); } // CSS let style = document.createElement('style'); style.type = 'text/css'; style.innerHTML = `.container {\n width: 750px;\n padding-right: 15px;\n padding-left: 15px;\n margin-left: auto; margin-right: auto; margin-top: 10px; margin-bottom: 10px; }\n .row { display: flex; flex-wrap: wrap; } .col { flex-basis: 0; -webkit-box-flex: 1; flex-grow: 1; max-width: 100%; } .roundtext { background-color: #c5d4e7; /* lighter than lightsteelblue */ font-weight: bold; border-radius: 4px; width: auto; /* Making auto-sizable width */ height: auto; /* Making auto-sizable height */ padding: 2px 2px 2px 2px; /* Making space around letters top right bottom left */ margin: 5px; font-size: 13px; } .greentext { background-color: #c2f2c2; } .redtext { background-color: #f9aeae; /* or lightcoral */ } `; document.getElementsByTagName('head')[0].appendChild(style); } function exportUsefulCommands() { let WL = {}; window.Worklog = WL; // Add a custom worklog from browser console, example: // Worklog.add('2020-03-28', '4h') WL.add = function (rawdate = new Date().toISOString(), duration = '1d') { const issueId = JIRA.Issue.getIssueId(); //console.log(issueId + ' ' + date); let date; if(typeof(rawdate) === 'string') { if(rawdate.length === 8 && rawdate.startsWith('20')) { // 20200328 let year = rawdate.slice(0, 4); let month = rawdate.slice(4, 6); let day = rawdate.slice(6, 8); date = `${year}-${month}-${day}T12:00:00.000+0000`; } else { // if (rawdate.length === 10 && rawdate.includes('-')) date = new Date(Date.parse(rawdate)).toISOString(); } } else { date = rawdate; } // date format: "2020-03-28T13:18:17.222+0000" addWorkLogDateRest(date, duration); } function isWE(date){ let day = date.getDay(); if(day === 0 || day === 6) { return true; } return false; } // To add 10 days of work logs begining at date in parameter, type in browser console the following instruction: // Worklog.addDays("20200222", 10) // WE days are not logged WL.addDays = function (rawdate = new Date().toISOString(), nbDays) { let date; let daysWalked = 0; if(typeof(rawdate) === 'string') { if(rawdate.length === 8 && rawdate.startsWith('20')) { // 20200328 let year = rawdate.slice(0, 4); let month = rawdate.slice(4, 6); let day = rawdate.slice(6, 8); date = new Date(); date.setYear(year); date.setMonth(month-1); date.setDate(day); } else { // if (rawdate.length === 10 && rawdate.includes('-')) date = new Date(Date.parse(rawdate)); } } else { date = rawdate; } while(daysWalked < nbDays) { if(!isWE(date)) { daysWalked++; console.log(date); addWorkLogDateRest(date.toISOString(), '1d'); } jumpNDays(date, 1); } //return date; } } exportUsefulCommands(); $(document).ajaxComplete(init);