Last active
May 28, 2025 21:47
-
-
Save joemiller/1a89e7a25585d9b631d37882d2c3f8b9 to your computer and use it in GitHub Desktop.
Tampermonkey script to add "select all / deselect all' buttons to Buildkite's `input` modal when used with multiple selections
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 Buildkite Select All Button | |
// @namespace http://tampermonkey.net/ | |
// @version 1.1 | |
// @description Adds a "Select All" button to Buildkite pipeline input modals (skips "!! APPLY ALL STACKS !!") | |
// @author You | |
// @match https://buildkite.com/* | |
// @grant none | |
// ==/UserScript== | |
(function() { | |
'use strict'; | |
// Function to add select all button to a modal | |
function addSelectAllButton(modal) { | |
// Check if we already added a button to this modal | |
if (modal.querySelector('.select-all-button-container')) { | |
return; | |
} | |
// Find all checkbox inputs in the modal | |
const checkboxes = modal.querySelectorAll('input[type="checkbox"]'); | |
// Only proceed if we found checkboxes | |
if (checkboxes.length === 0) { | |
return; | |
} | |
// Find a good place to insert the button - look for the fieldset or form group | |
let insertTarget = null; | |
// Try to find the fieldset containing the checkboxes | |
const fieldset = modal.querySelector('fieldset'); | |
if (fieldset) { | |
insertTarget = fieldset; | |
} else { | |
// Fallback: find the parent of the first checkbox | |
const firstCheckbox = checkboxes[0]; | |
let parent = firstCheckbox.parentElement; | |
while (parent && parent !== modal) { | |
if (parent.querySelector('input[type="checkbox"]') && | |
parent.querySelectorAll('input[type="checkbox"]').length === checkboxes.length) { | |
insertTarget = parent; | |
break; | |
} | |
parent = parent.parentElement; | |
} | |
} | |
if (!insertTarget) { | |
console.warn('Could not find suitable place to insert Select All button'); | |
return; | |
} | |
// Create button container | |
const buttonContainer = document.createElement('div'); | |
buttonContainer.className = 'select-all-button-container'; | |
buttonContainer.style.cssText = 'margin: 10px 0; display: flex; gap: 10px;'; | |
// Create Select All button | |
const selectAllBtn = document.createElement('button'); | |
selectAllBtn.textContent = 'Select All'; | |
selectAllBtn.type = 'button'; | |
selectAllBtn.className = 'btn btn-default'; | |
selectAllBtn.style.cssText = 'padding: 5px 10px; font-size: 13px;'; | |
// Create Deselect All button | |
const deselectAllBtn = document.createElement('button'); | |
deselectAllBtn.textContent = 'Deselect All'; | |
deselectAllBtn.type = 'button'; | |
deselectAllBtn.className = 'btn btn-default'; | |
deselectAllBtn.style.cssText = 'padding: 5px 10px; font-size: 13px;'; | |
// Function to check if checkbox should be skipped | |
function shouldSkipCheckbox(checkbox) { | |
// Check parent label | |
const parentLabel = checkbox.closest('label'); | |
if (parentLabel && parentLabel.textContent.includes('!! APPLY ALL STACKS !!')) { | |
return true; | |
} | |
// Check for label with for attribute | |
if (checkbox.id) { | |
const associatedLabel = document.querySelector(`label[for="${checkbox.id}"]`); | |
if (associatedLabel && associatedLabel.textContent.includes('!! APPLY ALL STACKS !!')) { | |
return true; | |
} | |
} | |
// Check siblings and parent for text content | |
const parent = checkbox.parentElement; | |
if (parent && parent.textContent.includes('!! APPLY ALL STACKS !!')) { | |
return true; | |
} | |
return false; | |
} | |
// Add click handlers | |
selectAllBtn.addEventListener('click', function(e) { | |
e.preventDefault(); | |
checkboxes.forEach(checkbox => { | |
if (!checkbox.checked && !shouldSkipCheckbox(checkbox)) { | |
checkbox.click(); | |
} | |
}); | |
}); | |
deselectAllBtn.addEventListener('click', function(e) { | |
e.preventDefault(); | |
checkboxes.forEach(checkbox => { | |
if (checkbox.checked && !shouldSkipCheckbox(checkbox)) { | |
checkbox.click(); | |
} | |
}); | |
}); | |
// Add buttons to container | |
buttonContainer.appendChild(selectAllBtn); | |
buttonContainer.appendChild(deselectAllBtn); | |
// Insert before the fieldset/checkbox container | |
insertTarget.parentNode.insertBefore(buttonContainer, insertTarget); | |
} | |
// Function to check for modals | |
function checkForModals() { | |
// Look for unblock dialogs or modals containing checkboxes | |
const modals = document.querySelectorAll('[id*="unblock_dialog"], [id*="modal_dialog"], .modal-content, [role="dialog"]'); | |
modals.forEach(modal => { | |
// Check if this modal has checkboxes | |
if (modal.querySelector('input[type="checkbox"]')) { | |
addSelectAllButton(modal); | |
} | |
}); | |
// Also check for any standalone forms with multiple checkboxes | |
const forms = document.querySelectorAll('form'); | |
forms.forEach(form => { | |
const checkboxes = form.querySelectorAll('input[type="checkbox"]'); | |
if (checkboxes.length > 1) { | |
addSelectAllButton(form); | |
} | |
}); | |
} | |
// Set up MutationObserver to watch for dynamic content | |
const observer = new MutationObserver(function(mutations) { | |
// Debounce to avoid excessive checks | |
clearTimeout(observer.timeout); | |
observer.timeout = setTimeout(checkForModals, 100); | |
}); | |
// Start observing | |
observer.observe(document.body, { | |
childList: true, | |
subtree: true | |
}); | |
// Initial check | |
checkForModals(); | |
// Also check when URL changes (for single-page apps) | |
let lastUrl = location.href; | |
new MutationObserver(() => { | |
const url = location.href; | |
if (url !== lastUrl) { | |
lastUrl = url; | |
setTimeout(checkForModals, 500); | |
} | |
}).observe(document, {subtree: true, childList: true}); | |
})(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment