Last active
January 5, 2023 14:27
-
-
Save joekrill/01ad09ff385b533aa64d092d7c6f5314 to your computer and use it in GitHub Desktop.
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
// ==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