Skip to content

Instantly share code, notes, and snippets.

@joekrill
Last active January 5, 2023 14:27
Show Gist options
  • Select an option

  • Save joekrill/01ad09ff385b533aa64d092d7c6f5314 to your computer and use it in GitHub Desktop.

Select an option

Save joekrill/01ad09ff385b533aa64d092d7c6f5314 to your computer and use it in GitHub Desktop.

Revisions

  1. joekrill revised this gist Jan 5, 2023. 1 changed file with 13 additions and 3 deletions.
    16 changes: 13 additions & 3 deletions pivotal-branch-name-generator.js
    Original file line number Diff line number Diff line change
    @@ -1,18 +1,28 @@
    // ==UserScript==
    // @name Generate branch names from a pivotal story
    // @name Generate branch name for pivotal story
    // @namespace joekrill.com
    // @match https://www.pivotaltracker.com/*
    // @grant GM_setClipboard
    // @grant GM_addStyle
    // @version 1.0
    // @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" }')
    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
  2. joekrill created this gist Jan 4, 2023.
    118 changes: 118 additions & 0 deletions pivotal-branch-name-generator.js
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,118 @@
    // ==UserScript==
    // @name Generate branch names from a pivotal story
    // @namespace joekrill.com
    // @match https://www.pivotaltracker.com/*
    // @grant GM_setClipboard
    // @grant GM_addStyle
    // @version 1.0
    // @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" }')

    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
    });