Skip to content

Instantly share code, notes, and snippets.

@2xAA
Last active June 22, 2023 08:20
Show Gist options
  • Save 2xAA/e7e91dfe2fb48fb5bdcb5403c3ddce43 to your computer and use it in GitHub Desktop.
Save 2xAA/e7e91dfe2fb48fb5bdcb5403c3ddce43 to your computer and use it in GitHub Desktop.
A userscript to add a copy branch name button and other useful defaults to Azure DevOps.
// ==UserScript==
// @name Better Azure DevOps
// @description Fixes AzureDevOps' bullshit
// @match *://dev.azure.com/*
// ==/UserScript==
const constants = {
copyBranchNameButton: "BAD-COPY_BRANCH_NAME_BUTTON",
};
const callback = async (eventName, data) => {
const { pathname } = window.location;
// Branch page
if (pathname.match(/^\/.*\/_git\/[^/]*$/)) {
if (eventName === "pushState") {
// horrible hack, but there you go 🤷🏻
await nothing();
}
const dropdown = await waitForSelector(".version-dropdown");
if (document.getElementById(constants.copyBranchNameButton)) {
return;
}
const branchName = dropdown.children[0].textContent;
const button = createCopyButton(branchName, constants.copyBranchNameButton);
insertAfter(inlineMenuItem(button), dropdown);
}
// Pull Request
if (pathname.match(/^\/.*\/_git\/.*\/pullrequest\/([0-9]*)$/)) {
const branchNameEl = await waitForSelector(
".pr-secondary-title-row .pr-header-branches > a:first-child"
);
if (document.getElementById(constants.copyBranchNameButton)) {
return;
}
const branchName = branchNameEl.textContent;
const button = createCopyButton(branchName, constants.copyBranchNameButton);
insertAfter(inlineMenuItem(button), branchNameEl);
// Set active comments as default activity filter
const activityFilterDropdownButton = await waitForSelector(
".repos-activity-filter-dropdown button"
);
activityFilterDropdownButton.click();
const activeCommentsDropdownItem = await waitForSelector(
"#__bolt-active_comments"
);
activeCommentsDropdownItem.click();
}
};
(() => {
callback();
const pushState = history.pushState;
history.pushState = function () {
pushState.apply(history, arguments);
callback("pushState", arguments);
};
})();
// https://stackoverflow.com/a/61511955
function waitForSelector(selector) {
return new Promise((resolve) => {
if (document.querySelector(selector)) {
return resolve(document.querySelector(selector));
}
const observer = new MutationObserver(() => {
if (document.querySelector(selector)) {
resolve(document.querySelector(selector));
observer.disconnect();
}
});
observer.observe(document.body, {
childList: true,
subtree: true,
});
});
}
function insertAfter(newNode, referenceNode) {
referenceNode.parentNode.insertBefore(newNode, referenceNode.nextSibling);
}
function applyButtonStyles(node) {
const styles =
"icon-only bolt-button bolt-icon-button enabled icon-only bolt-focus-treatment".split(
" "
);
styles.forEach((style) => node.classList.add(style));
return node;
}
function inlineMenuItem(node) {
const div = document.createElement("div");
div.className =
"bolt-header-command-item-button bolt-expandable-button inline-flex-row";
div.appendChild(node);
return div;
}
function createCopyButton(textToCopy, id) {
const button = document.createElement("button");
const span = document.createElement("span");
span.classList.add("left-icon", "flex-noshrink", "fabric-icon", "ms-Icon--Copy");
button.appendChild(span);
button.id = id;
button.title = `Copy "${textToCopy}" to clipboard`;
button.addEventListener("click", async () => {
await navigator.clipboard.writeText(textToCopy);
button.textContent = "🫡";
setTimeout(() => {
button.textContent = "";
button.appendChild(span);
}, 1300);
});
applyButtonStyles(button);
return button;
}
function sleep(milliseconds) {
return new Promise((resolve) => setTimeout(resolve, milliseconds));
}
function nothing() {
return new Promise((resolve) => resolve());
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment