Skip to content

Commit 95dc5a8

Browse files
Merge branch 'master' into patch-1
2 parents 19f378d + 3e6dd6a commit 95dc5a8

2 files changed

Lines changed: 92 additions & 32 deletions

File tree

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ A collection of userscripts to improve the Azure DevOps UI.
66

77
## Getting Started
88

9-
1. [Install the Violentmonkey extension](https://violentmonkey.github.io/)
9+
1. Install a userscript manager such as [Violentmonkey](https://violentmonkey.github.io/) or [Tampermonkey](https://www.tampermonkey.net/)
1010
2. Refresh this page if you just installed this extension (or the download link won't work)
1111
3. [Install this userscript](https://github.com/alejandro5042/azdo-userscripts/raw/master/src/azdo-pr-dashboard.user.js)
1212

src/azdo-pr-dashboard.user.js

Lines changed: 91 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,15 @@
11
// ==UserScript==
22

33
// @name More Awesome Azure DevOps (userscript)
4-
// @version 3.7.6
4+
// @version 3.9.0
55
// @author Alejandro Barreto (NI)
66
// @description Makes general improvements to the Azure DevOps experience, particularly around pull requests. Also contains workflow improvements for NI engineers.
77
// @license MIT
88

99
// @namespace https://github.com/alejandro5042
1010
// @homepageURL https://alejandro5042.github.io/azdo-userscripts/
1111
// @supportURL https://alejandro5042.github.io/azdo-userscripts/SUPPORT.html
12-
// @updateURL https://rebrand.ly/update-azdo-pr-dashboard-user-js
12+
// @updateURL https://github.com/alejandro5042/azdo-userscripts/releases/latest/download/azdo-pr-dashboard.user.js
1313
// @contributionURL https://github.com/alejandro5042/azdo-userscripts
1414

1515
// @include https://dev.azure.com/*
@@ -76,14 +76,11 @@
7676
'agent-arbitration-status-off': 'Off',
7777
});
7878

79-
eus.showTipOnce('release-2024-06-06', 'New in the AzDO userscript', `
80-
<p>Highlights from the 2024-06-06 update!</p>
81-
<p>Changes to the build logs view:</p>
79+
eus.showTipOnce('release-2026-04-17', 'New in the AzDO userscript', `
80+
<p>Highlights from the 2026-04-17 update!</p>
8281
<ul>
83-
<li>The left-side jobs pane is now resizable.</li>
82+
<li>Switch from Rebrandly to GitHub for update URL (#247)</li>
8483
</ul>
85-
<p>See also <a href="https://github.com/alejandro5042/azdo-userscripts/commits/master/?since=2021-11-15&until=2024-04-16" target="_blank">other changes since our last update notification</a>.</p>
86-
<hr>
8784
<p>Comments, bugs, suggestions? File an issue on <a href="https://github.com/alejandro5042/azdo-userscripts" target="_blank">GitHub</a> 🧡</p>
8885
`);
8986
}
@@ -159,7 +156,9 @@
159156
onPageUpdatedThrottled.flush();
160157

161158
// Call our event handler if we notice new elements being inserted into the DOM. This happens as the page is loading or updating dynamically based on user activity.
162-
$('body > div.full-size')[0].addEventListener('DOMNodeInserted', onPageUpdatedThrottled);
159+
const targetNode = $('body > div.full-size')[0];
160+
const observer = new MutationObserver(onPageUpdatedThrottled);
161+
observer.observe(targetNode, { childList: true, subtree: true });
163162
}
164163

165164
function watchForStatusCardAndMoveToRightSideBar(session) {
@@ -976,23 +975,24 @@
976975
}`);
977976

978977
function watchForWorkItemForms() {
979-
eus.globalSession.onEveryNew(document, '.menu-item.follow-item-menu-item-gray', followButton => {
978+
eus.globalSession.onEveryNew(document, '#__bolt-follow', followButton => {
980979
followButton.addEventListener('click', async _ => {
981-
await eus.sleep(100); // We need to allow the other handlers to send the request to follow/unfollow. After the request is sent, we can annotate our follows list correctly.
982-
await annotateWorkItemWithFollowerList(document.querySelector('.discussion-messages-right'));
980+
await eus.sleep(1000); // We need to allow the other handlers to send the request to follow/unfollow. After the request is sent, we can annotate our follows list correctly.
981+
await annotateWorkItemWithFollowerList(document.querySelector('.comment-editor.enter-new-comment'));
983982
});
984983
});
985984
// Annotate work items (under the comment box) with who is following it.
986-
eus.globalSession.onEveryNew(document, '.discussion-messages-right', async commentEditor => {
985+
eus.globalSession.onEveryNew(document, '.comment-editor.enter-new-comment', async commentEditor => {
987986
await annotateWorkItemWithFollowerList(commentEditor);
988987
});
989988
}
990989

991990
async function annotateWorkItemWithFollowerList(commentEditor) {
992-
document.querySelectorAll('.work-item-followers-list').forEach(e => e.remove());
991+
const commentEditorContainer = commentEditor.closest('.new-comment-div');
992+
commentEditorContainer.querySelectorAll('.work-item-followers-list').forEach(e => e.remove());
993993

994-
const workItemId = commentEditor.closest('.witform-layout').querySelector('.work-item-form-id > span').innerText;
995-
const queryResponse = await fetch(`${azdoApiBaseUrl}/_apis/notification/subscriptionquery?api-version=6.0`, {
994+
const workItemId = getCurrentWorkItemId(commentEditor);
995+
const queryResponse = await fetch(`${azdoApiBaseUrl}_apis/notification/subscriptionquery?api-version=6.0`, {
996996
method: 'POST',
997997
headers: {
998998
'Content-Type': 'application/json',
@@ -1011,15 +1011,35 @@
10111011
queryFlags: 'alwaysReturnBasicInformation',
10121012
}),
10131013
});
1014-
10151014
const followers = [...(await queryResponse.json()).value].sort((a, b) => a.subscriber.displayName.localeCompare(b.subscriber.displayName));
10161015
const followerList = followers
1017-
.map(s => `<a href="mailto:${s.subscriber.uniqueName}">${s.subscriber.displayName}</a>`)
1018-
.join(', ')
1019-
|| 'Nobody';
1016+
.map(s => `<a class="bolt-link no-underline-link" target="_blank" href="https://teams.microsoft.com/l/chat/0/0?users=${s.subscriber.uniqueName}">${s.subscriber.displayName}</a>`)
1017+
.join(', ');
1018+
if (followerList) {
1019+
const annotation = `<div class="work-item-followers-list" style="margin: 1em 0em; opacity: 0.7"><span class="menu-item-icon bowtie-icon bowtie-watch-eye-fill" aria-hidden="true"></span> ${followerList}</div>`;
1020+
commentEditor.insertAdjacentHTML('afterend', annotation);
1021+
}
1022+
}
1023+
1024+
function getCurrentWorkItemId(commentEditor) {
1025+
// Try getting the link from the work item header, in case this is opened in preview view
1026+
const workItemPage = commentEditor.closest('.work-item-form-page');
1027+
const header = workItemPage.querySelector('.work-item-form-header');
1028+
const links = header.querySelectorAll('a');
1029+
// Loop through the links and check if their target matches a link for a work item
1030+
const workItemLink = Array.from(links).find(link => link.href.includes('_workitems'));
1031+
1032+
// Default to the window URL if the find operation fails
1033+
let currentUrl = window.location.href;
1034+
if (workItemLink) {
1035+
currentUrl = workItemLink.href;
1036+
}
10201037

1021-
const annotation = `<div class="work-item-followers-list" style="margin: 1em 0em; opacity: 0.8"><span class="menu-item-icon bowtie-icon bowtie-watch-eye-fill" aria-hidden="true"></span> ${followerList}</div>`;
1022-
commentEditor.insertAdjacentHTML('BeforeEnd', annotation);
1038+
const [baseUrl] = currentUrl.split('?');
1039+
const urlSegments = baseUrl.split('/');
1040+
const workItemId = urlSegments[urlSegments.length - 1];
1041+
1042+
return workItemId;
10231043
}
10241044

10251045
function watchForRepoBrowsingPages(session) {
@@ -1299,27 +1319,67 @@
12991319

13001320
const trophiesAwarded = [];
13011321

1322+
/* eslint-disable brace-style */
1323+
13021324
// Milestone trophy: Awarded if pull request ID is greater than 1000 and is a non-zero digit followed by only zeroes (e.g. 1000, 5000, 10000).
13031325
if (prId >= 1000 && prId.toString().match('^[1-9]0+$')) {
13041326
const milestoneTrophyMessage = $('<div>').addClass('bolt-table-cell-content').text(`🏆 ${prAuthor} got pull request #${prId}`);
13051327
trophiesAwarded.push(milestoneTrophyMessage);
13061328
}
1307-
1308-
// Fish trophy: Give a man a fish, he'll waste hours trying to figure out why. (Awarded if the ID is a palindrome.)
1309-
// Requires an id with at least three numbers.
1310-
if (prId > 100 && prId.toString() === prId.toString().split('').reverse().join('')) {
1329+
// Repeating digits trophy: Awarded if the ID is greater than 100 and consists of only one repeated digit (e.g. 1111, 2222).
1330+
else if (prId > 100 && /^(\d)\1+$/.test(prId.toString())) {
1331+
const repeatingDigitMessage = $('<div>').addClass('bolt-table-cell-content').text(`🔢 ${prAuthor} got a fancy pull request #${prId}`);
1332+
trophiesAwarded.push(repeatingDigitMessage);
1333+
}
1334+
// Fish trophy: Give a man a fish, he'll waste hours trying to figure out why.
1335+
// Awarded if the ID is greater than 100 and is a palindrome (e.g. 12321).
1336+
else if (prId > 100 && prId.toString() === prId.toString().split('').reverse().join('')) {
13111337
const fishTrophyMessage = $('<div>').addClass('bolt-table-cell-content').text(`🐠 ${prAuthor} got a fish trophy`);
13121338
trophiesAwarded.push(fishTrophyMessage);
13131339
}
13141340

1341+
// First PR.
1342+
if (prId === 1) {
1343+
const firstPrTrophyMessage = $('<div>').addClass('bolt-table-cell-content').text(`🥇 ${prAuthor} is first`);
1344+
trophiesAwarded.push(firstPrTrophyMessage);
1345+
}
1346+
// 42: The answer to life, the universe, and everything.
1347+
else if (prId === 42) {
1348+
const meaningOfLifeMessage = $('<div>').addClass('bolt-table-cell-content').text(`🌌 ${prAuthor} found the answer to life, the universe, and everything`);
1349+
trophiesAwarded.push(meaningOfLifeMessage);
1350+
}
1351+
// 404: Not found.
1352+
else if (prId === 404) {
1353+
const notFoundMessage = $('<div>').addClass('bolt-table-cell-content').text(`🔍 ${prAuthor}'s pull request was not found (just kidding, here it is)`);
1354+
trophiesAwarded.push(notFoundMessage);
1355+
}
1356+
// 418: I'm a teapot.
1357+
else if (prId === 418) {
1358+
const teapotMessage = $('<div>').addClass('bolt-table-cell-content').text(`🫖 ${prAuthor} is a teapot`);
1359+
trophiesAwarded.push(teapotMessage);
1360+
}
1361+
// 666: The number of the beast.
1362+
else if (prId === 666) {
1363+
const beastMessage = $('<div>').addClass('bolt-table-cell-content').text(`😈 ${prAuthor} is a beast`);
1364+
trophiesAwarded.push(beastMessage);
1365+
}
1366+
// 777: Lucky sevens.
1367+
else if (prId === 777) {
1368+
const luckyMessage = $('<div>').addClass('bolt-table-cell-content').text(`🎰 ${prAuthor} hit the jackpot`);
1369+
trophiesAwarded.push(luckyMessage);
1370+
}
13151371
// 1337 leetspeak.
1316-
if (prId === 1337) {
1372+
else if (prId === 1337) {
13171373
const leetMessage = $('<div>').addClass('bolt-table-cell-content').text(`👨‍💻 ${prAuthor} speaks leet`);
13181374
trophiesAwarded.push(leetMessage);
1319-
} else if (prId === 1) { // First PR.
1320-
const firstPrTrophyMessage = $('<div>').addClass('bolt-table-cell-content').text(`🥇 ${prAuthor} is first`);
1321-
trophiesAwarded.push(firstPrTrophyMessage);
13221375
}
1376+
// 31337 elite leetspeak.
1377+
else if (prId === 31337) {
1378+
const eliteLeetMessage = $('<div>').addClass('bolt-table-cell-content').text(`🧠 ${prAuthor} speaks elite!`);
1379+
trophiesAwarded.push(eliteLeetMessage);
1380+
}
1381+
1382+
/* eslint-enable brace-style */
13231383

13241384
if (trophiesAwarded.length > 0) {
13251385
const header = $('<div/>').addClass('bolt-header-title body-xl m').text('Trophies');
@@ -1680,7 +1740,7 @@
16801740
<div class="bolt-pill-observe"></div>
16811741
</div>
16821742
</div>`)[0];
1683-
pullRequestRow.querySelector('.body-l').insertAdjacentElement('afterend', labelContainer);
1743+
pullRequestRow.querySelector('.bolt-table-two-line-cell-item').insertAdjacentElement('beforeend', labelContainer);
16841744
labels = pullRequestRow.querySelector('.bolt-pill-group-inner');
16851745
}
16861746

0 commit comments

Comments
 (0)