Skip to content

Instantly share code, notes, and snippets.

@codycraven
Created September 16, 2020 18:50
Show Gist options
  • Save codycraven/ef9cde78c51b7b458854ce6f08a4df45 to your computer and use it in GitHub Desktop.
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 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