NOTICE: By continued use of this site you understand and agree to the binding Terms of Service and Privacy Policy.
// ==UserScript== // @name Jira create iOS or Services ticket from Android // @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.4 // @include https://jira.*.uk/browse/* // @updateURL https://openuserjs.org/meta/floodmeadows/Jira_create_iOS_or_Services_ticket_from_Android.meta.js // @downloadURL https://openuserjs.org/install/floodmeadows/Jira_create_iOS_or_Services_ticket_from_Android.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 customFieldForStoryPoints = "customfield_10004" //---------------------------------------------// const debug = true; (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 currentIssueEpicBaseDomElement = document.getElementById(`${customFieldForEpicLink}-val`); var newIssueEpicLink = null; if (currentIssueEpicBaseDomElement === undefined || currentIssueEpicBaseDomElement == null) { console.log('Error: Current issue is not assigned to an epic.'); } else { newIssueEpicLink = currentIssueEpicBaseDomElement.children[0].attributes.href.nodeValue.substring(8); } const currentIssueStoryPointsBaseDomElement = document.getElementById(`${customFieldForStoryPoints}-val`); var newIssueStoryPoints = null; if (currentIssueStoryPointsBaseDomElement === undefined || currentIssueStoryPointsBaseDomElement == null) { console.log('Notice: Current issue has no story points.'); } else { newIssueStoryPoints = Number(currentIssueStoryPointsBaseDomElement.childNodes[0].nodeValue.trim()); } // Copying the sprint value // ------------------------ // It's not possible to set the sprint value directly in issue details, as with other issue properties (e.g. Summary or Story points). // Have to find sprint ID, then POST to /rest/agile/1.0/sprint/{sprintId}/issue to move the issue into that sprint // See https://developer.atlassian.com/cloud/jira/software/rest/api-group-sprint/#api-rest-agile-1-0-sprint-sprintid-issue-post // Issue ID can be found using document.getElementsByClassName('js-find-on-board-sprint')[0].href (e.g. 'https://jira.tools.tax.service.gov.uk/secure/FindOnBoard.jspa?sprintId=19012') // Then need to parse the sprintId from the query string param // Also need to check if document.getElementsByClassName('js-find-on-board-sprint')[0].parentNode.parentNode.children[0].childNodes[0].nodeValue == "Active Sprint:" || "Future Sprint:" // If the nodeValue == "Completed Sprint:" || "Completed Sprints:", or if document.getElementsByClassName('js-find-on-board-sprint') is null or empty, then the current issue is not in a current or future sprint and the new issue should not be moved to a sprint. const currentIssueState = document.getElementById('opsbar-transitions_more').children[0].innerHTML; const newIssueTransitionName = currentIssueState; const currentIssueAssigneeBaseDomElement = document.getElementById('assignee-val').children[0].attributes.rel; var newIssueAssignee = null; if( currentIssueAssigneeBaseDomElement === undefined || currentIssueAssigneeBaseDomElement == null ) { console.log('Notice: Current issue is not assigned to anyone.'); } else { newIssueAssignee = currentIssueAssigneeBaseDomElement.value; } //---------------------------// var headers = new Headers(); headers.append("Content-Type", "application/json"); const jsonToCreateNewIssue = JSON.stringify({ "fields": { "project": { "key": jiraProjectKey }, "summary": newIssueSummary, "description": newIssueDescription, "issuetype": { "id": issueTypeId }, "components": [{ "name": component }], "customfield_10008": newIssueEpicLink, //todo: work out how to use customFieldForEpicLink for the property name "customfield_10004": newIssueStoryPoints, //todo: work out how to use customFieldForStoryPoints for the property name "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; } }