Skip to content

Instantly share code, notes, and snippets.

@autarch
Last active May 14, 2026 20:40
Show Gist options
  • Select an option

  • Save autarch/7813add135721511844ca76adb1b23c8 to your computer and use it in GitHub Desktop.

Select an option

Save autarch/7813add135721511844ca76adb1b23c8 to your computer and use it in GitHub Desktop.
// ==UserScript==
// @name Spruce Bulk Patch Actions
// @namespace http://tampermonkey.net/
// @version 1.1
// @description Multi-select patches and bulk unschedule/hide
// @include https://spruce.corp.mongodb.com/user/*/patches*
// @run-at document-idle
// @grant none
// ==/UserScript==
(function () {
'use strict';
const GQL_ENDPOINT = 'https://evergreen.mongodb.com/graphql/query';
const TOOLBAR_ID = 'bulk-patch-toolbar';
const selected = new Set();
let isWorking = false;
async function gql(query, variables) {
const res = await fetch(GQL_ENDPOINT, {
method: 'POST',
credentials: 'include',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ query, variables }),
});
const data = await res.json();
if (data.errors && data.errors.length > 0) {
throw new Error(data.errors.map(e => e.message).join('; '));
}
return data.data;
}
function unscheduleVersionTasks(versionId) {
return gql(
`mutation UnscheduleVersionTasks($versionId: String!, $abort: Boolean!) {
unscheduleVersionTasks(versionId: $versionId, abort: $abort)
}`,
{ versionId, abort: true }
);
}
function setPatchVisibility(patchIds, hidden) {
return gql(
`mutation SetPatchVisibility($patchIds: [String!]!, $hidden: Boolean!) {
setPatchVisibility(patchIds: $patchIds, hidden: $hidden) {
id
hidden
}
}`,
{ patchIds, hidden }
);
}
function updateToolbar() {
const toolbar = document.getElementById(TOOLBAR_ID);
if (!toolbar) return;
const count = selected.size;
const countEl = toolbar.querySelector('.bulk-count');
const unschedBtn = toolbar.querySelector('.bulk-unschedule');
const hideBtn = toolbar.querySelector('.bulk-hide');
countEl.textContent = count === 0 ? '' : `${count} selected`;
const shouldDisable = isWorking || count === 0;
unschedBtn.disabled = shouldDisable;
hideBtn.disabled = shouldDisable;
}
function setStatus(toolbar, msg, isError) {
const statusEl = toolbar.querySelector('.bulk-status');
statusEl.textContent = msg;
statusEl.style.color = isError ? '#F77B7B' : '#00A35C';
if (msg) {
setTimeout(() => {
statusEl.textContent = '';
statusEl.style.color = '';
}, 4000);
}
}
async function handleUnschedule() {
const toolbar = document.getElementById(TOOLBAR_ID);
const ids = [...selected];
isWorking = true;
updateToolbar();
toolbar.querySelector('.bulk-status').textContent = 'Working…';
toolbar.querySelector('.bulk-status').style.color = '#fff';
const results = await Promise.allSettled(ids.map(id => unscheduleVersionTasks(id)));
const failed = results.filter(r => r.status === 'rejected').length;
results.forEach((r, i) => {
if (r.status === 'rejected') console.error(`Failed to unschedule ${ids[i]}:`, r.reason);
});
isWorking = false;
updateToolbar();
if (failed > 0) {
setStatus(toolbar, `Done — ${failed} of ${ids.length} failed. Check console.`, true);
} else {
setStatus(toolbar, `Unscheduled tasks for ${ids.length} patch(es).`, false);
}
}
async function handleHide() {
const toolbar = document.getElementById(TOOLBAR_ID);
const ids = [...selected];
isWorking = true;
updateToolbar();
toolbar.querySelector('.bulk-status').textContent = 'Working…';
toolbar.querySelector('.bulk-status').style.color = '#fff';
try {
const result = await setPatchVisibility(ids, true);
const hiddenIds = new Set((result?.setPatchVisibility ?? []).map(p => p.id));
hiddenIds.forEach(id => {
const marker = document.querySelector(`[data-patch-id="${id}"]`);
if (marker) marker.closest('[data-cy="patch-card"]')?.remove();
selected.delete(id);
});
setStatus(toolbar, `Hid ${hiddenIds.size} patch(es).`, false);
} catch (e) {
console.error('Failed to hide patches:', e);
setStatus(toolbar, `Error: ${e.message}`, true);
}
isWorking = false;
updateToolbar();
}
function pruneStaleSelection() {
selected.forEach(id => {
if (!document.querySelector(`[data-patch-id="${id}"]`)) {
selected.delete(id);
}
});
}
function injectToolbar() {
if (document.getElementById(TOOLBAR_ID)) return;
const firstCard = document.querySelector('[data-cy="patch-card"]');
if (!firstCard) return;
const listContainer = firstCard.parentElement;
if (!listContainer) return;
const styleEl = document.createElement('style');
styleEl.textContent = `
#${TOOLBAR_ID} button:disabled { opacity: 0.45; cursor: not-allowed; }
#${TOOLBAR_ID} button:not(:disabled):hover { opacity: 0.85; }
`;
document.head.appendChild(styleEl);
const toolbar = document.createElement('div');
toolbar.id = TOOLBAR_ID;
toolbar.style.cssText = [
'position: sticky',
'top: 0',
'z-index: 1000',
'padding: 10px 16px',
'background: #1a1e26',
'display: flex',
'align-items: center',
'gap: 12px',
'border-bottom: 1px solid #2e3d52',
'margin-bottom: 4px',
].join(';');
const btnStyle = [
'background: #00A35C',
'color: #fff',
'border: none',
'border-radius: 4px',
'padding: 6px 14px',
'font-size: 13px',
'font-weight: 600',
'cursor: pointer',
'transition: opacity 0.15s',
].join(';');
toolbar.innerHTML = `
<button class="bulk-unschedule" style="${btnStyle}" disabled>Unschedule All Tasks</button>
<button class="bulk-hide" style="${btnStyle}" disabled>Hide Patches</button>
<span class="bulk-count" style="color: #fff; font-size: 13px; min-width: 80px;"></span>
<span class="bulk-status" style="font-size: 13px; margin-left: 4px;"></span>
`;
toolbar.querySelector('.bulk-unschedule').addEventListener('click', handleUnschedule);
toolbar.querySelector('.bulk-hide').addEventListener('click', handleHide);
listContainer.insertBefore(toolbar, listContainer.firstChild);
}
function injectCheckboxes() {
pruneStaleSelection();
injectToolbar();
updateToolbar();
document.querySelectorAll('[data-cy="patch-card"]:not([data-bulk-patched])').forEach(card => {
const link = card.querySelector('[data-cy="patch-card-patch-link"]');
if (!link) return;
const href = link.getAttribute('href') || '';
const match = href.match(/\/(?:version|patch)\/([^/]+)\//);
if (!match) return;
const patchId = match[1];
const dropdownBtn = card.querySelector('[data-cy="patch-card-dropdown"]');
if (!dropdownBtn) return;
card.setAttribute('data-bulk-patched', 'true');
const marker = document.createElement('span');
marker.setAttribute('data-patch-id', patchId);
marker.style.display = 'none';
card.appendChild(marker);
const cb = document.createElement('input');
cb.type = 'checkbox';
cb.className = 'bulk-checkbox';
cb.dataset.patchId = patchId;
cb.style.cssText = [
'margin-left: 8px',
'margin-right: 4px',
'cursor: pointer',
'width: 16px',
'height: 16px',
'flex-shrink: 0',
'accent-color: #00A35C',
].join(';');
cb.addEventListener('change', () => {
if (cb.checked) {
selected.add(patchId);
} else {
selected.delete(patchId);
}
updateToolbar();
});
// Place before the dropdown container so the card's own flex layout centers it
dropdownBtn.parentElement.insertAdjacentElement('beforebegin', cb);
});
}
// Debounced MutationObserver to handle React re-renders
let debounceTimer;
const observer = new MutationObserver(() => {
clearTimeout(debounceTimer);
debounceTimer = setTimeout(injectCheckboxes, 150);
});
observer.observe(document.body, { childList: true, subtree: true });
// Initial injection after React mounts
setTimeout(injectCheckboxes, 500);
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment