|
1 | 1 | // ==UserScript== |
2 | 2 |
|
3 | 3 | // @name More Awesome Azure DevOps (userscript) |
4 | | -// @version 3.7.6 |
| 4 | +// @version 3.9.0 |
5 | 5 | // @author Alejandro Barreto (NI) |
6 | 6 | // @description Makes general improvements to the Azure DevOps experience, particularly around pull requests. Also contains workflow improvements for NI engineers. |
7 | 7 | // @license MIT |
8 | 8 |
|
9 | 9 | // @namespace https://github.com/alejandro5042 |
10 | 10 | // @homepageURL https://alejandro5042.github.io/azdo-userscripts/ |
11 | 11 | // @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 |
13 | 13 | // @contributionURL https://github.com/alejandro5042/azdo-userscripts |
14 | 14 |
|
15 | 15 | // @include https://dev.azure.com/* |
|
76 | 76 | 'agent-arbitration-status-off': 'Off', |
77 | 77 | }); |
78 | 78 |
|
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> |
82 | 81 | <ul> |
83 | | - <li>The left-side jobs pane is now resizable.</li> |
| 82 | + <li>Switch from Rebrandly to GitHub for update URL (#247)</li> |
84 | 83 | </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> |
87 | 84 | <p>Comments, bugs, suggestions? File an issue on <a href="https://github.com/alejandro5042/azdo-userscripts" target="_blank">GitHub</a> 🧡</p> |
88 | 85 | `); |
89 | 86 | } |
|
159 | 156 | onPageUpdatedThrottled.flush(); |
160 | 157 |
|
161 | 158 | // 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 }); |
163 | 162 | } |
164 | 163 |
|
165 | 164 | function watchForStatusCardAndMoveToRightSideBar(session) { |
|
976 | 975 | }`); |
977 | 976 |
|
978 | 977 | function watchForWorkItemForms() { |
979 | | - eus.globalSession.onEveryNew(document, '.menu-item.follow-item-menu-item-gray', followButton => { |
| 978 | + eus.globalSession.onEveryNew(document, '#__bolt-follow', followButton => { |
980 | 979 | 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')); |
983 | 982 | }); |
984 | 983 | }); |
985 | 984 | // 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 => { |
987 | 986 | await annotateWorkItemWithFollowerList(commentEditor); |
988 | 987 | }); |
989 | 988 | } |
990 | 989 |
|
991 | 990 | 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()); |
993 | 993 |
|
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`, { |
996 | 996 | method: 'POST', |
997 | 997 | headers: { |
998 | 998 | 'Content-Type': 'application/json', |
|
1011 | 1011 | queryFlags: 'alwaysReturnBasicInformation', |
1012 | 1012 | }), |
1013 | 1013 | }); |
1014 | | - |
1015 | 1014 | const followers = [...(await queryResponse.json()).value].sort((a, b) => a.subscriber.displayName.localeCompare(b.subscriber.displayName)); |
1016 | 1015 | 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 | + } |
1020 | 1037 |
|
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; |
1023 | 1043 | } |
1024 | 1044 |
|
1025 | 1045 | function watchForRepoBrowsingPages(session) { |
|
1299 | 1319 |
|
1300 | 1320 | const trophiesAwarded = []; |
1301 | 1321 |
|
| 1322 | + /* eslint-disable brace-style */ |
| 1323 | + |
1302 | 1324 | // 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). |
1303 | 1325 | if (prId >= 1000 && prId.toString().match('^[1-9]0+$')) { |
1304 | 1326 | const milestoneTrophyMessage = $('<div>').addClass('bolt-table-cell-content').text(`🏆 ${prAuthor} got pull request #${prId}`); |
1305 | 1327 | trophiesAwarded.push(milestoneTrophyMessage); |
1306 | 1328 | } |
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('')) { |
1311 | 1337 | const fishTrophyMessage = $('<div>').addClass('bolt-table-cell-content').text(`🐠 ${prAuthor} got a fish trophy`); |
1312 | 1338 | trophiesAwarded.push(fishTrophyMessage); |
1313 | 1339 | } |
1314 | 1340 |
|
| 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 | + } |
1315 | 1371 | // 1337 leetspeak. |
1316 | | - if (prId === 1337) { |
| 1372 | + else if (prId === 1337) { |
1317 | 1373 | const leetMessage = $('<div>').addClass('bolt-table-cell-content').text(`👨💻 ${prAuthor} speaks leet`); |
1318 | 1374 | 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); |
1322 | 1375 | } |
| 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 */ |
1323 | 1383 |
|
1324 | 1384 | if (trophiesAwarded.length > 0) { |
1325 | 1385 | const header = $('<div/>').addClass('bolt-header-title body-xl m').text('Trophies'); |
|
1680 | 1740 | <div class="bolt-pill-observe"></div> |
1681 | 1741 | </div> |
1682 | 1742 | </div>`)[0]; |
1683 | | - pullRequestRow.querySelector('.body-l').insertAdjacentElement('afterend', labelContainer); |
| 1743 | + pullRequestRow.querySelector('.bolt-table-two-line-cell-item').insertAdjacentElement('beforeend', labelContainer); |
1684 | 1744 | labels = pullRequestRow.querySelector('.bolt-pill-group-inner'); |
1685 | 1745 | } |
1686 | 1746 |
|
|
0 commit comments