Skip to content

Instantly share code, notes, and snippets.

@u1-liquid
Last active November 21, 2025 14:13
Show Gist options
  • Select an option

  • Save u1-liquid/d0f2cb4e10fc8d9fc60264846a4caa60 to your computer and use it in GitHub Desktop.

Select an option

Save u1-liquid/d0f2cb4e10fc8d9fc60264846a4caa60 to your computer and use it in GitHub Desktop.
// ==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