Skip to content

Instantly share code, notes, and snippets.

@joekrill
Last active January 5, 2023 14:27
Show Gist options
  • Save joekrill/01ad09ff385b533aa64d092d7c6f5314 to your computer and use it in GitHub Desktop.
Save joekrill/01ad09ff385b533aa64d092d7c6f5314 to your computer and use it in GitHub Desktop.
// ==UserScript==
// @name Generate branch name for pivotal story
// @namespace joekrill.com
// @match https://www.pivotaltracker.com/*
// @grant GM_setClipboard
// @grant GM_addStyle
// @version 1.1
// @author Joe Krill
// @description Adds a button to Pivotal issues that automatically generates a git branch name and copies it to the clibboard
// @require https://cdn.jsdelivr.net/npm/@violentmonkey/dom@2
// ==/UserScript==
// This style causes the text "Branch name copied" to show up in the same way that "Story ID copied" shows up when
// you click the story ID.
GM_addStyle(`
.story section.edit .controls.with_branch_name .bubble:before {
content: "Branch name copied";
}
section.edit .controls .bubble {
width: 202px !important;
}
section.model_details .actions {
width: 315px !important;
}
`)
class CopyBranchNameButtonElement extends HTMLButtonElement {
// A git branch icon
// Original source: https://thenounproject.com/icon/git-branch-4411741/
static ICON_SRC = `
<?xml version="1.0" encoding="UTF-8"?>
<svg version="1.1" viewBox="0 0 1200 1200" xmlns="http://www.w3.org/2000/svg">
<path d="m975 350c0.0625-31.582-11.836-62.016-33.297-85.188-21.461-23.168-50.898-37.355-82.391-39.707-31.496-2.3555-62.715 7.3008-87.379 27.027-24.668 19.723-40.953 48.055-45.582 79.293-4.6328 31.242 2.7344 63.078 20.617 89.109s44.957 44.328 75.781 51.215c-2.9805 29.809-17.477 57.273-40.402 76.559-22.926 19.281-52.469 28.859-82.348 26.691h-200c-23.004-1.1211-45.992 2.5469-67.508 10.766-21.516 8.2188-41.094 20.812-57.492 36.984v-150c40.832-8.3359 74.824-36.469 90.645-75.023 15.82-38.555 11.383-82.457-11.828-117.07-23.211-34.613-62.141-55.379-103.82-55.379s-80.605 20.766-103.82 55.379c-23.211 34.613-27.648 78.516-11.828 117.07 15.82 38.555 49.812 66.688 90.645 75.023v254.75c-40.621 8.6797-74.277 36.973-89.809 75.5-15.531 38.523-10.91 82.25 12.328 116.68 23.242 34.43 62.066 55.062 103.61 55.062s80.363-20.633 103.61-55.062c23.238-34.426 27.859-78.152 12.328-116.68-15.531-38.527-49.188-66.82-89.809-75.5 3.1641-29.672 17.738-56.953 40.641-76.078 22.906-19.125 52.348-28.602 82.109-26.422h200c43.035 2.1523 85.219-12.539 117.61-40.961 32.387-28.418 52.434-68.336 55.891-111.29 28.551-5.4648 54.309-20.711 72.832-43.117 18.527-22.402 28.664-50.562 28.668-79.633zm-700 0c0-19.891 7.9023-38.969 21.969-53.031 14.062-14.066 33.141-21.969 53.031-21.969s38.969 7.9023 53.031 21.969c14.066 14.062 21.969 33.141 21.969 53.031s-7.9023 38.969-21.969 53.031c-14.062 14.066-33.141 21.969-53.031 21.969s-38.969-7.9023-53.031-21.969c-14.066-14.062-21.969-33.141-21.969-53.031zm150 500c0 19.891-7.9023 38.969-21.969 53.031-14.062 14.066-33.141 21.969-53.031 21.969s-38.969-7.9023-53.031-21.969c-14.066-14.062-21.969-33.141-21.969-53.031s7.9023-38.969 21.969-53.031c14.062-14.066 33.141-21.969 53.031-21.969s38.969 7.9023 53.031 21.969c14.066 14.062 21.969 33.141 21.969 53.031zm425-425c-19.891 0-38.969-7.9023-53.031-21.969-14.066-14.062-21.969-33.141-21.969-53.031s7.9023-38.969 21.969-53.031c14.062-14.066 33.141-21.969 53.031-21.969s38.969 7.9023 53.031 21.969c14.066 14.062 21.969 33.141 21.969 53.031s-7.9023 38.969-21.969 53.031c-14.062 14.066-33.141 21.969-53.031 21.969z" fill="#191919"/>
</svg>
`
constructor() {
super();
this.innerHTML = CopyBranchNameButtonElement.ICON_SRC
this.title = "Copy this story's branch name to your clipboard"
this.addEventListener('click', e => {
e.preventDefault();
GM_setClipboard(this.getAttribute("data-branch-name"));
this.flashBranchCopiedNotice()
});
}
flashBranchCopiedNotice() {
const actions = getParent(this, ".controls");
if (actions) {
actions.classList.add("copied", "with_branch_name")
setTimeout(() => {
actions.classList.remove("copied", "with_branch_name")
}, 1000)
}
}
}
customElements.define("copy-branch-name-button", CopyBranchNameButtonElement, { extends: "button" });
const slugify = (text) => {
return text
.toString() // Cast to string (optional)
.normalize('NFKD') // The normalize() using NFKD method returns the Unicode Normalization Form of a given string.
.toLowerCase() // Convert the string to lowercase letters
.trim() // Remove whitespace from both sides of a string (optional)
.replace(/\s+/g, '-') // Replace spaces with -
.replace(/[^\w\-]+/g, '') // Remove all non-word chars
.replace(/\_/g,'-') // Replace _ with -
.replace(/\-\-+/g, '-') // Replace multiple - with single -
.replace(/\-$/g, ''); // Remove trailing -
}
var pivotalStoryIdNode = function(node) {
return node.nodeType == Node.ELEMENT_NODE &&
node.nodeName == 'INPUT' &&
node.getAttribute('aria-label') == 'story id';
}
// Used to indicate the given node has been visitied and the button for the related story has been daded.
const DATA_ATTRIBUTE_NODE_VISITED = "data-has-branch-name-added";
// Get the closest matching element
const getParent = function (elem, selector) {
for ( ; elem && elem !== document; elem = elem.parentNode ) {
if ( elem.matches( selector ) ) return elem;
}
return null;
}
const handleAddedNode = function (node) {
if (pivotalStoryIdNode(node) && !node.getAttribute(DATA_ATTRIBUTE_NODE_VISITED)) {
const storyId = node.value.replace(/[^0-9]/g, '');
node.setAttribute(DATA_ATTRIBUTE_NODE_VISITED, "true")
const form = getParent(node, "form");
const wrapper = getParent(node, ".button_with_field");
if (form && wrapper) {
const nameField = form.querySelector("textarea[name='story[name]']")
if (nameField) {
const branchName = `${storyId}-${slugify(nameField.value)}`
const button = document.createElement('button', { is: "copy-branch-name-button" });
button.setAttribute("data-branch-name", branchName);
wrapper.insertBefore(button, wrapper.firstChild);
}
}
}
if (node.hasChildNodes()) {
node.childNodes.forEach(handleAddedNode);
}
};
const mutationCallback = function (mutationsList, observer) {
mutationsList.forEach((mutationRecord) => {
if (mutationRecord.addedNodes) {
mutationRecord.addedNodes.forEach(handleAddedNode);
}
});
}
new MutationObserver(mutationCallback).observe(document.body, {
childList: true,
subtree: true
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment