Instantly share code, notes, and snippets.
Last active
November 21, 2025 14:13
-
Star
0
(0)
You must be signed in to star a gist -
Fork
0
(0)
You must be signed in to fork a gist
-
-
Save u1-liquid/d0f2cb4e10fc8d9fc60264846a4caa60 to your computer and use it in GitHub Desktop.
This file contains hidden or 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 SonarCloud Review Helper - Safe/Fixed button | |
| // @namespace https://github.com/u1-liquid | |
| // @version 0.2 | |
| // @description Add Safe/Fixed buttons next to Review on SonarCloud | |
| // @match https://sonarcloud.io/* | |
| // @grant none | |
| // @author u1-liquid | |
| // @source https://gist.github.com/u1-liquid/d0f2cb4e10fc8d9fc60264846a4caa60 | |
| // @run-at document-idle | |
| // @updateURL https://gist.github.com/u1-liquid/d0f2cb4e10fc8d9fc60264846a4caa60/raw/sonarcloud-review-button.user.js | |
| // @downloadURL https://gist.github.com/u1-liquid/d0f2cb4e10fc8d9fc60264846a4caa60/raw/sonarcloud-review-button.user.js | |
| // @supportURL https://gist.github.com/u1-liquid/d0f2cb4e10fc8d9fc60264846a4caa60#new_comment_field | |
| // ==/UserScript== | |
| (function () { | |
| 'use strict'; | |
| function findReviewButton() { | |
| const spans = document.querySelectorAll('button span span'); | |
| for (const span of spans) { | |
| if (span.textContent.trim() === 'Review') { | |
| const button = span.closest('button'); | |
| if (button) return button; | |
| } | |
| } | |
| return null; | |
| } | |
| function waitForElement(selector, timeout = 5000, filterFn) { | |
| const start = performance.now(); | |
| return new Promise((resolve) => { | |
| function check() { | |
| const candidates = document.querySelectorAll(selector); | |
| let found = null; | |
| if (filterFn) { | |
| for (const el of candidates) { | |
| if (filterFn(el)) { | |
| found = el; | |
| break; | |
| } | |
| } | |
| } else if (candidates.length > 0) { | |
| found = candidates[0]; | |
| } | |
| if (found) { | |
| resolve(found); | |
| return; | |
| } | |
| if (performance.now() - start >= timeout) { | |
| resolve(null); | |
| return; | |
| } | |
| requestAnimationFrame(check); | |
| } | |
| check(); | |
| }); | |
| } | |
| function hideReactModalPortal() { | |
| const portal = document.querySelector('#sonarcloud > div.ReactModalPortal'); | |
| if (portal) { | |
| portal.style.display = 'none'; | |
| } | |
| } | |
| async function setIssueStatus(statusLabel) { | |
| const reviewButton = findReviewButton(); | |
| if (!reviewButton) { | |
| console.warn('[SonarCloud Review Helper] Review button not found.'); | |
| return; | |
| } | |
| // hide modal portal if already present | |
| hideReactModalPortal(); | |
| // 1. open Review dialog | |
| reviewButton.click(); | |
| // hide again in case portal appeared after click | |
| hideReactModalPortal(); | |
| // 2. select status (Safe/Fixed) | |
| const statusButton = await waitForElement( | |
| 'button[aria-label]', | |
| 5000, | |
| (el) => el.getAttribute('aria-label') === statusLabel | |
| ); | |
| if (!statusButton) { | |
| console.warn( | |
| `[SonarCloud Review Helper] Status button with aria-label="${statusLabel}" not found.` | |
| ); | |
| return; | |
| } | |
| statusButton.click(); | |
| // ensure modal portal stays hidden while we proceed | |
| hideReactModalPortal(); | |
| // 3. click "Change status" | |
| const changeStatusSpan = await waitForElement( | |
| 'button span span', | |
| 5000, | |
| (el) => el.textContent.trim() === 'Change status' | |
| ); | |
| if (!changeStatusSpan) { | |
| console.warn('[SonarCloud Review Helper] "Change status" button span not found.'); | |
| return; | |
| } | |
| const changeStatusButton = changeStatusSpan.closest('button'); | |
| if (!changeStatusButton) { | |
| console.warn('[SonarCloud Review Helper] "Change status" button element not found.'); | |
| return; | |
| } | |
| changeStatusButton.click(); | |
| // final hide to cover any re-opened portal | |
| hideReactModalPortal(); | |
| } | |
| function injectButtons() { | |
| const reviewButton = findReviewButton(); | |
| if (!reviewButton) return; | |
| const originalParent = reviewButton.parentElement; | |
| if (!originalParent) return; | |
| // If already wrapped, reuse the wrapper | |
| let wrapper = reviewButton.closest('div[data-sc-review-wrapper="1"]'); | |
| if (!wrapper) { | |
| // Create wrapper div with requested class | |
| wrapper = document.createElement('div'); | |
| wrapper.setAttribute('data-sc-review-wrapper', '1'); | |
| wrapper.className = 'sw-flex sw-items-center sw-gap-2'; | |
| // Insert wrapper before the Review button and move Review inside it | |
| originalParent.insertBefore(wrapper, reviewButton); | |
| wrapper.appendChild(reviewButton); | |
| } | |
| // Avoid reinjecting Safe/Fixed if they already exist | |
| if (wrapper.querySelector('button[data-sc-status-button="Safe"]')) { | |
| return; | |
| } | |
| function createStatusButton(status) { | |
| const btn = reviewButton.cloneNode(true); | |
| btn.setAttribute('data-sc-status-button', status); | |
| const innerSpan = btn.querySelector('span span'); | |
| if (innerSpan) { | |
| innerSpan.textContent = status; | |
| } else { | |
| btn.textContent = status; | |
| } | |
| btn.addEventListener('click', (e) => { | |
| e.preventDefault(); | |
| e.stopPropagation(); | |
| // Set cursor | |
| wrapper.parentElement.style.cursor = 'wait'; | |
| wrapper.style.cursor = 'wait'; | |
| const buttons = wrapper.querySelectorAll('button'); | |
| buttons.forEach((b) => { | |
| b.style.cursor = 'wait'; | |
| }); | |
| // Run the status change flow | |
| setIssueStatus(status); | |
| }); | |
| return btn; | |
| } | |
| const safeBtn = createStatusButton('Safe'); | |
| const fixedBtn = createStatusButton('Fixed'); | |
| wrapper.appendChild(safeBtn); | |
| wrapper.appendChild(fixedBtn); | |
| console.log('[SonarCloud Review Helper] Injected Safe/Fixed buttons.'); | |
| } | |
| function initObserver() { | |
| const observer = new MutationObserver(() => { | |
| injectButtons(); | |
| }); | |
| if (document.body) { | |
| observer.observe(document.body, { childList: true, subtree: true }); | |
| } else { | |
| window.addEventListener('DOMContentLoaded', () => { | |
| observer.observe(document.body, { childList: true, subtree: true }); | |
| injectButtons(); | |
| }); | |
| } | |
| } | |
| injectButtons(); | |
| initObserver(); | |
| })(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment