NOTICE: By continued use of this site you understand and agree to the binding Terms of Service and Privacy Policy.
// ==UserScript==
// @name Jira Move Issue to Sprint and Change Status (Persistent Buttons)
// @description Adds buttons to move the current issue into one or more sprints and change the status, persists after Jira async updates
// @namespace https://openuserjs.org/users/floodmeadows
// @license MIT
// @version 1.0
// @include https://jira.*.uk/browse/*
// @grant none
// ==/UserScript==
/* jshint esversion: 8 */
(function() {
'use strict';
// -----------------------------
// CONFIGURATION
// -----------------------------
const debug = false;
const currentUrl = new URL(document.URL);
const jiraBaseUrl = currentUrl.origin;
const sprintIds = {
inAnalysis: 20090,
readyForRefinement: 18522,
readyForDev: 18509
};
const transitionIds = {
inAnalysis: 1321,
readyForRefinement: 1311,
readyForDev: 1131,
toDo: 11
};
let issueKey = "";
// -----------------------------
// INITIALISE
// -----------------------------
function init() {
issueKey = document.getElementById('key-val')?.textContent.trim();
addButtons();
}
// -----------------------------
// BUTTON CREATION
// -----------------------------
function addButtons() {
console.log("addButtons called")
setTimeout(() => {
const container = document.getElementById('opsbar-opsbar-transitions');
if (!container) {
console.log("Button container not found. Can't add buttons.")
return
};
if (document.getElementById('custom-buttons-added')) {
console.log("Marker element not found. Returning without adding buttons.")
return;
}
console.log("Injecting buttons...");
const marker = document.createElement('span');
marker.id = 'custom-buttons-added';
container.appendChild(marker);
addLink(container, "Backlog", moveToBacklog);
addLink(container, "In Analysis", () => changeSprintAndState('inAnalysis'));
addLink(container, "Ready for Refinement", () => changeSprintAndState('readyForRefinement'));
addLink(container, "Ready for Dev", () => changeSprintAndState('readyForDev'));
}, 500); // delay to allow DOM to settle
}
function addLink(container, text, handler) {
const btn = document.createElement("a");
btn.href = "#";
btn.className = "aui-button toolbar-trigger issueaction-workflow-transition";
btn.style.marginLeft = "10px";
btn.textContent = text;
btn.addEventListener("click", handler);
container.appendChild(btn);
}
// -----------------------------
// BUSINESS LOGIC
// -----------------------------
async function changeSprintAndState(key) {
const url = `${jiraBaseUrl}/rest/agile/latest/sprint/${sprintIds[key]}/issue`;
const headers = new Headers({ "Content-Type": "application/json" });
const body = JSON.stringify({ "issues": [ issueKey ] });
try {
console.log(`Moving ${issueKey} to sprint ${key}`);
const response = await fetch(url, { method: 'POST', headers, body });
console.log("Sprint move response:", await response.text());
await applyTransition(transitionIds[key]);
reloadPage();
} catch (error) {
console.error('Sprint move error:', error);
}
}
async function moveToBacklog() {
const url = `${jiraBaseUrl}/rest/agile/latest/backlog/issue`;
const headers = new Headers({ "Content-Type": "application/json" });
const body = JSON.stringify({ "issues": [ issueKey ] });
try {
console.log(`Moving ${issueKey} to backlog`);
const response = await fetch(url, { method: 'POST', headers, body });
console.log("Backlog move response:", await response.text());
await applyTransition(transitionIds.toDo);
reloadPage();
} catch (error) {
console.error('Backlog move error:', error);
}
}
async function applyTransition(transitionId) {
const url = `${jiraBaseUrl}/rest/api/2/issue/${issueKey}/transitions`;
const headers = new Headers({ "Content-Type": "application/json" });
const body = JSON.stringify({ "transition": { "id": transitionId } });
try {
console.log(`Applying transition ${transitionId} to ${issueKey}`);
const response = await fetch(url, { method: 'POST', headers, body });
console.log("Transition response:", await response.status);
} catch (error) {
console.error('Transition error:', error);
}
}
function reloadPage() {
if (!debug) window.location.assign(currentUrl);
}
// -----------------------------
// OBSERVER TO HANDLE DOM CHANGES
// -----------------------------
const observer = new MutationObserver((mutationsList, observer) => {
console.log("DOM changed, checking buttons...");
addButtons();
});
observer.observe(document.body, { childList: true, subtree: true });
window.__observerActive = true;
console.log("MutationObserver started");
// Run on initial load
init();
})();