NOTICE: By continued use of this site you understand and agree to the binding Terms of Service and Privacy Policy.
// ==UserScript== // @name Jira create related tickets // @namespace https://openuserjs.org/users/floodmeadows // @description Adds buttons to create a linked iOS or Microservices ticket based on an existing Android ticket. The name of the new story will be based on the name of the current story; it will have a "relates to" link to the current story; the Component will be set to "iOS" or "Microservices"; it will be linked to the same epic and have the same state and assignee as the current story. // @copyright 2021, floodmeadows (https://openuserjs.org/users/floodmeadows) // @license MIT // @version 0.1 // @include https://jira.*.uk/browse/* // @updateURL https://openuserjs.org/meta/floodmeadows/Jira_create_related_tickets.meta.js // @downloadURL https://openuserjs.org/install/floodmeadows/Jira_create_related_tickets.user.js // @grant none // ==/UserScript== /* jshint esversion: 6 */ //--- Customise these to your Jira project ----// const customFieldForEpicLink = "customfield_10008"; // see https://stackoverflow.com/questions/24385644/jira-api-get-epic-for-issue //---------------------------------------------// const debug = false; (function () { 'use strict'; addButton("Create iOS ticket","iOS","Android","iOS"); // button text, component, text to search for at end of current story name, text to replace it with in new story addButton("Create Services ticket","Microservices","Android","Services"); })(); function addButton(buttonText,component,searchStringInCurrentStoryName,replaceStringInNewStoryName) { const newElement = document.createElement("a"); newElement.setAttribute("href", "#"); newElement.setAttribute("class", "aui-button toolbar-trigger issueaction-workflow-transition"); newElement.addEventListener("click", function () { createIssue(component,searchStringInCurrentStoryName,replaceStringInNewStoryName); }); const text = document.createTextNode(buttonText); newElement.appendChild(text); const target = document.getElementById('opsbar-opsbar-admin'); target.appendChild(newElement); } function createIssue(component,searchStringInCurrentStoryName,replaceStringInNewStoryName) { //--- Get standard info ---// const currentUrl = new URL(document.URL); const jiraBaseUrl = currentUrl.protocol + '//' + currentUrl.host; const createIssueUrl = jiraBaseUrl + '/rest/api/latest/issue'; const addLinkUrl = jiraBaseUrl + '/rest/api/latest/issueLink'; // Parse the Jira project key out of the current URL. e.g. if the current issue key is "ABC-1234" then the project key will be ABC. const pathArr = location.pathname.split("/"); const jiraProjectKey = pathArr[pathArr.length-1].split("-")[0]; const currentIssueKey = document.getElementById("key-val").childNodes[0].nodeValue; const currentIssueSummary = document.getElementById("summary-val").childNodes[0].nodeValue; const newIssueSummary = newIssueSummaryFromCurrentIssueName(currentIssueSummary,searchStringInCurrentStoryName,replaceStringInNewStoryName); const newIssueDescription = `See ${currentIssueKey} for details.`; const currentIssueTypeName = document.getElementById('type-val').childNodes[2].nodeValue.trim(); const issueTypeId = issueTypeIdFromName(currentIssueTypeName); const currentIssueEpicLink = document.getElementById(`${customFieldForEpicLink}-val`).children[0].attributes.href.nodeValue.substring(8); const currentIssueState = document.getElementById('opsbar-transitions_more').children[0].innerHTML; const newIssueTransitionName = currentIssueState; const currentIssueAssignee = document.getElementById('assignee-val').children[0].attributes.rel.value; var newIssueAssignee; if( currentIssueAssignee === undefined ) { newIssueAssignee = ""; } else { newIssueAssignee = currentIssueAssignee; } //---------------------------// var headers = new Headers(); headers.append("Content-Type", "application/json"); var jsonToCreateNewIssue = ""; jsonToCreateNewIssue = JSON.stringify({ "fields": { "project": { "key": jiraProjectKey }, "summary": newIssueSummary, "description": newIssueDescription, "issuetype": { "id": issueTypeId }, "components": [{ "name": component }], // "labels": [ // "" // ], "customfield_10008": currentIssueEpicLink, "assignee": { "name": newIssueAssignee } } }); if (debug) console.log(jsonToCreateNewIssue); var requestOptions = { method: 'POST', headers: headers, body: jsonToCreateNewIssue }; fetch(createIssueUrl, requestOptions) .then(response => { const jsonPromise = response.json() .then(data => { const newIssue = data; const newIssueKey = newIssue.key; console.log(newIssueKey); var jsonToAddLink = ""; jsonToAddLink = JSON.stringify({ "type": { "name": "Relates", "inward": "relates to", "outward": "relates to" }, "inwardIssue": { "key": currentIssueKey }, "outwardIssue": { "key": newIssueKey } }); requestOptions = { method: 'POST', headers: headers, body: jsonToAddLink }; fetch(addLinkUrl, requestOptions) .then(response => { console.log(response.text()); const updateIssueUrl = `${jiraBaseUrl}/rest/api/latest/issue/${newIssueKey}/transitions`; var jsonToUpdateStatus = ""; jsonToUpdateStatus = JSON.stringify({ "transition": { "id": issueTransitionIdFromName(newIssueTransitionName), "name": newIssueTransitionName } }); requestOptions = { method: 'POST', headers: headers, body: jsonToUpdateStatus }; fetch(updateIssueUrl, requestOptions) .then(response => { console.log(response.text()); if(!debug) window.location.assign(currentUrl); }) .catch(error => console.log('error', error)); }) .catch(error => console.log('error', error)); }); }) .catch(error => console.log('error', error)); } function newIssueSummaryFromCurrentIssueName(currentIssueSummary,searchString,replaceString) { var newIssueName = ""; if( currentIssueSummary.endsWith(searchString) || currentIssueSummary.endsWith(searchString+')') || currentIssueSummary.endsWith(searchString+']') ) { var pos = currentIssueSummary.lastIndexOf(searchString) var currentIssueNameBeforePlatformName = currentIssueSummary.substring(0,pos) var currentIssueNameAfterPlatformName = currentIssueSummary.substring(pos+searchString.length,currentIssueSummary.length) newIssueName = currentIssueNameBeforePlatformName + replaceString + currentIssueNameAfterPlatformName; } else { newIssueName = `${currentIssueSummary} (${replaceString})`; } return newIssueName; } function issueTypeIdFromName(name) { //Todo: make a call to /rest/api/2/issue/createmeta/{projectIdOrKey}/issuetypes and look up the issueTypeId from the response, rather than having the list hard coded here const issueTypeIds = new Map([ ["Bug", "1"], ["Technical Story", "10"], ["Documentation", "10000"], ["UX/UR", "11000"], ["Tech-debt", "11301"], ["Spike", "12"], ["Technical Debt", "13"], ["UI Story", "14"], ["New Feature", "2"], ["Task", "3"], ["Improvement", "4"], ["Sub-task", "5"], ["Epic", "6"], ["Story", "7"], ["Technical task", "8"] ]); // The values are specified as text, not integers, because that's how Jira returns them. // To get these values for your instance of Jira, you can use https://openuserjs.org/scripts/floodmeadows/Jira_get_issue_types if( issueTypeIds.has(name) ) { return issueTypeIds.get(name); } else { console.log("Error: No issue type ID in lookup table for current issue type (" + name + ")."); return false; } } function issueTransitionIdFromName(name) { const issueTransitionIds = new Map([ ["Backlog", "11"], ["Done", "41"], ["In Dev", "61"], ["Ready for Dev", "71"], ["In PR", "81"], ["In Test", "91"], ["Ready for Release", "101"], ["To Do", "111"], ["In Analysis", "121"], ["Ready for Refinement", "141"], ["Won't Fix", "161"], ["In Progress", "171"], ["Ready for Test", "191"], ["In Review", "261"], ["Reporting", "321"], ["To be Prioritised", "331"] ]); // To get these values for your instance of Jira, you can use https://openuserjs.org/scripts/floodmeadows/Jira_get_issue_transitions if( issueTransitionIds.has(name) ) { return issueTransitionIds.get(name); } else { console.log("Error: No transition ID for '" + name + "'"); return false; } }