Created
September 16, 2020 18:50
-
-
Save codycraven/ef9cde78c51b7b458854ce6f08a4df45 to your computer and use it in GitHub Desktop.
Bookmarklet to display an overlay showing all children of an epic with their status
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// This is a Jira tool for use as a bookmarklet that will recursively | |
// display child stories for an Epic. | |
// | |
// To use this bookmarklet, modify the `rankField` ID to match your Jira | |
// install. Then create a new bookmark in your browser, enter `javascript:` | |
// then paste this script after javascript: (line breaks will be removed). | |
// | |
// Now when you are viewing a Jira epic's page, click the bookmarklet you | |
// created and you should see an overlay showing all children (up to 50) | |
// and their sub-tasks (if any). | |
// | |
// Click the X in the top right corner or escape key to close the overlay. | |
(() => { | |
const rankField = 10019; | |
let containerEl; | |
const listeners = []; | |
function addListener(el, event, handler) { | |
listeners.push([el, event, handler]); | |
el.addEventListener(event, handler); | |
} | |
function closeContainer() { | |
listeners.forEach(([el, event, handler]) => el.removeEventListener(event, handler)); | |
document.getElementsByTagName("body")[0].removeChild(containerEl); | |
containerEl = undefined; | |
} | |
function applyStylesToEl(styles, el) { | |
for (let style in styles) { | |
el.style[style] = styles[style]; | |
} | |
} | |
function createMessageContainer() { | |
const d = document.createElement("div"); | |
applyStylesToEl( | |
{ | |
padding: "16px", | |
borderWidth: "1px", | |
borderStyle: "solid", | |
left: "50vw", | |
transform: "translate(-50%, 0)", | |
borderColor: "white", | |
background: "green", | |
color: "white", | |
position: "fixed", | |
top: "5px", | |
borderRadius: "16px", | |
zIndex: "5999999", | |
}, | |
d | |
); | |
return d; | |
} | |
function showMessage(el, timeout = 5000) { | |
document.body.appendChild(el); | |
window.setTimeout(() => { | |
document.body.removeChild(el); | |
}, timeout); | |
} | |
function findIssuesInEpic(domain, epicKey) { | |
return fetch( | |
`https://${domain}/rest/api/2/search?jql=${encodeURIComponent( | |
`"Epic Link"=${epicKey} ORDER BY cf[${rankField}] ASC` | |
)}` | |
).then((response) => { | |
return response.json(); | |
}); | |
} | |
function extractIssueKey(p) { | |
return /\/browse\/([^\\]+)/.exec(p)[1]; | |
} | |
function createStatus(issue) { | |
const el = document.createElement("span"); | |
el.textContent = issue.fields.status.name; | |
let fg; | |
let bg; | |
el.style.marginLeft = ".3rem"; | |
switch (issue.fields.status.statusCategory.colorName) { | |
case "yellow": | |
fg = "rgb(7, 71, 166)"; | |
bg = "rgb(222, 235, 255)"; | |
break; | |
case "green": | |
fg = "rgb(0, 102, 68)"; | |
bg = "rgb(227, 252, 239)"; | |
break; | |
default: | |
fg = "rgb(66, 82, 110)"; | |
bg = "rgb(223, 225, 230)"; | |
} | |
applyStylesToEl( | |
{ | |
color: fg, | |
backgroundColor: bg, | |
border: `1px solid ${fg}`, | |
borderRadius: "1rem", | |
fontSize: ".8rem", | |
paddingLeft: ".2rem", | |
paddingRight: ".2rem", | |
}, | |
el | |
); | |
return el; | |
} | |
function createKeyLink(issue) { | |
const el = document.createElement("a"); | |
el.setAttribute( | |
"href", | |
`https://${window.location.host}/browse/${issue.key}` | |
); | |
el.textContent = issue.key; | |
el.style.marginRight = ".3rem"; | |
return el; | |
} | |
function createSubtask(subtask) { | |
const liEl = document.createElement("li"); | |
const summaryEl = document.createElement("span"); | |
if (subtask.fields.status.statusCategory.key === "done") { | |
summaryEl.style.textDecoration = "line-through"; | |
} | |
summaryEl.appendChild(createKeyLink(subtask)); | |
summaryEl.appendChild(document.createTextNode(subtask.fields.summary)); | |
liEl.appendChild(summaryEl); | |
liEl.appendChild(createStatus(subtask)); | |
return liEl; | |
} | |
function createSubtasks(subtasks) { | |
const ulEl = document.createElement("ul"); | |
subtasks.forEach((subtask) => ulEl.appendChild(createSubtask(subtask))); | |
return ulEl; | |
} | |
function createTask(issue) { | |
const liEl = document.createElement("li"); | |
const summaryEl = document.createElement("span"); | |
if (issue.fields.resolution) { | |
summaryEl.style.textDecoration = "line-through"; | |
} | |
summaryEl.appendChild(createKeyLink(issue)); | |
summaryEl.appendChild(document.createTextNode(issue.fields.summary)); | |
liEl.appendChild(summaryEl); | |
liEl.appendChild(createStatus(issue)); | |
if (issue.fields.subtasks.length) { | |
liEl.appendChild(createSubtasks(issue.fields.subtasks)); | |
} | |
return liEl; | |
} | |
function createTaskList(issues) { | |
const ulEl = document.createElement("ul"); | |
issues.forEach((issue) => { | |
ulEl.appendChild(createTask(issue)); | |
}); | |
ulEl.style.marginTop = ".5rem"; | |
return ulEl; | |
} | |
function createEpicContainer(epicKey, issues) { | |
if (typeof containerEl === "undefined") { | |
containerEl = document.createElement("div"); | |
const closeEl = document.createElement("a"); | |
const headingEl = document.createElement("h2"); | |
const issuesEl = document.createElement("div"); | |
applyStylesToEl( | |
{ | |
position: "fixed", | |
width: "100vw", | |
height: "100vh", | |
zIndex: "5999999", | |
background: "white", | |
top: "0", | |
}, | |
containerEl | |
); | |
closeEl.textContent = "X"; | |
applyStylesToEl( | |
{ | |
position: "absolute", | |
right: "15px", | |
fontSize: "20px", | |
cursor: "pointer", | |
}, | |
closeEl | |
); | |
addListener(closeEl, "click", closeContainer); | |
addListener(document, "keydown", (e) => { | |
if (e.key === "Escape" || e.key === "Esc") { | |
e.preventDefault(); | |
closeContainer(); | |
} | |
}); | |
headingEl.textContent = `Children of Epic ${epicKey}`; | |
applyStylesToEl( | |
{ | |
position: "absolute", | |
top: "30px", | |
width: "100vw", | |
borderTop: "1px solid #333", | |
height: "calc(100vh - 30px)", | |
overflow: "auto", | |
}, | |
issuesEl | |
); | |
issuesEl.appendChild(createTaskList(issues)); | |
containerEl.appendChild(closeEl); | |
containerEl.appendChild(headingEl); | |
containerEl.appendChild(issuesEl); | |
document.getElementsByTagName("body")[0].appendChild(containerEl); | |
} | |
} | |
try { | |
const epicKey = extractIssueKey(window.location.pathname); | |
findIssuesInEpic(window.location.host, epicKey).then((details) => { | |
createEpicContainer(epicKey, details.issues); | |
}); | |
} catch (err) { | |
console.error(err); | |
const m = createMessageContainer(); | |
m.textContent = "Unable to fetch Jira Epic hierarchy"; | |
m.style.background = "red"; | |
showMessage(m); | |
} | |
})(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment