Skip to content

Instantly share code, notes, and snippets.

@jasoncodes
Created June 17, 2026 00:08
Show Gist options
  • Select an option

  • Save jasoncodes/0e92de17f7fdbf992c0c1967849c245d to your computer and use it in GitHub Desktop.

Select an option

Save jasoncodes/0e92de17f7fdbf992c0c1967849c245d to your computer and use it in GitHub Desktop.
Azure Boards userscript for Standups
// ==UserScript==
// @name Azure Boards userscript for Standups
// @namespace local.azure-devops-taskboard
// @version 0.1.0
// @match https://dev.azure.com/*
// @run-at document-start
// @grant none
// ==/UserScript==
// toggle to hide fixed headers on smaller screens
(() => {
'use strict';
const HOST = 'dev.azure.com';
const PATH_PART = '/_sprints/taskboard/';
const ROOT_ID = 'ado-taskboard-header-toggle';
const STYLE_ID = 'ado-taskboard-header-toggle-style';
const HIDDEN_CLASS = 'ado-taskboard-headers-hidden';
const TARGET_SELECTORS = ['.project-header', '.bolt-header', '.hide-on-mobile'];
if (location.hostname !== HOST || !location.pathname.includes(PATH_PART)) return;
let headersHidden = false;
localStorage.removeItem('adoTaskboardHeadersHidden');
const updateToggle = (value = headersHidden) => {
const button = document.getElementById(ROOT_ID);
if (!button) return;
button.setAttribute('aria-pressed', String(value));
button.setAttribute('aria-label', value ? 'Show taskboard headers' : 'Hide taskboard headers');
button.title = value ? 'Show taskboard headers' : 'Hide taskboard headers';
button.classList.toggle('active', value);
};
const setHidden = (value) => {
headersHidden = value;
document.documentElement.classList.toggle(HIDDEN_CLASS, value);
updateToggle(value);
};
const ensureStyle = () => {
if (document.getElementById(STYLE_ID) || !document.head) return;
const style = document.createElement('style');
style.id = STYLE_ID;
const hiddenSelector = TARGET_SELECTORS
.map((selector) => `html.${HIDDEN_CLASS} ${selector}`)
.join(',\n ');
style.textContent = `
${hiddenSelector} {
display: none !important;
}
#${ROOT_ID} {
align-items: center;
background: transparent;
border: 0;
color: inherit;
cursor: pointer;
display: flex;
flex-shrink: 0;
font: inherit;
height: 40px;
justify-content: center;
margin: 0;
padding: 0;
text-align: left;
width: 100%;
}
#${ROOT_ID}:hover,
#${ROOT_ID}:focus-visible,
#${ROOT_ID}.active {
background: rgba(0, 90, 158, 0.12);
}
#${ROOT_ID}:focus-visible {
outline: 1px solid currentColor;
outline-offset: -3px;
}
#${ROOT_ID}.active .navigation-icon {
color: rgb(0, 120, 212);
}
`;
document.head.append(style);
};
const buildToggle = () => {
const container = document.createElement('div');
container.id = `${ROOT_ID}-container`;
container.className = 'hub-group-container flex-column flex-noshrink relative hub-group-only';
const button = document.createElement('button');
button.id = ROOT_ID;
button.type = 'button';
button.className = 'hub-group navigation-element navigation-link focus-treatment flex-row flex-grow flex-center scroll-hidden relative bolt-link no-underline-link';
button.setAttribute('role', 'menuitem');
button.setAttribute('tabindex', '-1');
button.innerHTML = `
<span class="navigation-icon flex-row flex-center flex-noshrink justify-center">
<span class="fluent-icons-enabled" role="img" aria-hidden="true">
<span aria-hidden="true" class="navigation-icon flex-row flex-center justify-center flex-noshrink fabric-icon ms-Icon--FullScreen medium ado-header-toggle-icon"></span>
</span>
</span>
<span class="navigation-text expanded-only text-ellipsis flex-grow">Headers</span>
`;
button.addEventListener('click', () => setHidden(!headersHidden));
container.append(button);
return container;
};
const inject = () => {
if (document.getElementById(ROOT_ID)) {
updateToggle();
return true;
}
const nav = document.querySelector('.navigation-container .project-navigation');
const section = nav?.querySelector('.navigation-section');
if (!section || !document.head) return false;
ensureStyle();
section.append(buildToggle());
updateToggle(false);
return true;
};
document.getElementById(ROOT_ID)?.closest('.hub-group-container')?.remove();
document.getElementById(STYLE_ID)?.remove();
document.documentElement.classList.remove(HIDDEN_CLASS);
const readyTimer = setInterval(() => {
if (inject()) clearInterval(readyTimer);
}, 250);
new MutationObserver(inject).observe(document.documentElement, {
childList: true,
subtree: true,
});
})();
// assignee quick select buttons
(() => {
'use strict';
const ROOT_ID = 'ado-assignee-quickpick-prototype';
const STYLE_ID = `${ROOT_ID}-style`;
const TOGGLE_ID = `${ROOT_ID}-toggle`;
const HIDDEN_CLASS = `${ROOT_ID}-hidden`;
const EXCLUDED_ASSIGNEES = new Set(['@Me', 'Unassigned']);
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
const nextFrame = () => new Promise((resolve) => requestAnimationFrame(() => resolve()));
const displayName = (name) => name.split(/\s+/)[0];
const isAssigneeName = (name) => Boolean(name) && !EXCLUDED_ASSIGNEES.has(name);
let assignees = [];
const visible = (el) => {
if (!el) return false;
const rect = el.getBoundingClientRect();
return rect.width > 0 && rect.height > 0;
};
const assignedButton = () => document.querySelector('.vss-FilterBar button[aria-label^="Assigned to"]');
const filterButton = (ariaPrefix) => document.querySelector(`.vss-FilterBar button[aria-label^="${ariaPrefix}"]`);
const statesButton = () => filterButton('States filter') || filterButton('States');
const filterBar = () => document.querySelector('.vss-FilterBar');
const dropdown = () => Array.from(document.querySelectorAll('.work-item-filter-dropdown')).find(visible);
const listbox = () => {
const openDropdown = dropdown();
if (!openDropdown) return null;
return Array.from(openDropdown.querySelectorAll('ul[role="listbox"], [role="listbox"]')).find(visible)
|| (openDropdown.querySelector('li[role="option"]') ? openDropdown : null);
};
const currentFromButton = () => {
const button = assignedButton();
const aria = button?.getAttribute('aria-label') || '';
const match = aria.match(/^Assigned to filter:\s*(.+)$/);
if (match) return match[1].trim();
const text = (button?.textContent || '').trim().replace(/\s+/g, ' ');
return text && text !== 'Assigned to' ? text : null;
};
const rows = () => Array.from(dropdown()?.querySelectorAll('li[role="option"]') || []);
const rowName = (row) => (row.textContent || '').trim().replace(/\s+/g, ' ');
const checkbox = (row) => row.querySelector('[role="checkbox"]');
const checkedRows = () => rows().filter((row) => checkbox(row)?.getAttribute('aria-checked') === 'true');
const currentSelection = () => checkedRows().map(rowName).filter(isAssigneeName);
const listScroller = () => dropdown()?.querySelector('.bolt-dropdown-list-box-container');
const humanClick = (el) => {
const rect = el.getBoundingClientRect();
const x = rect.left + Math.min(24, Math.max(1, rect.width / 2));
const y = rect.top + rect.height / 2;
const options = {
bubbles: true,
cancelable: true,
composed: true,
view: window,
clientX: x,
clientY: y,
button: 0,
buttons: 1,
};
el.dispatchEvent(new PointerEvent('pointerdown', { ...options, pointerId: 1, pointerType: 'mouse', isPrimary: true }));
el.dispatchEvent(new MouseEvent('mousedown', options));
el.dispatchEvent(new PointerEvent('pointerup', { ...options, pointerId: 1, pointerType: 'mouse', isPrimary: true, buttons: 0 }));
el.dispatchEvent(new MouseEvent('mouseup', { ...options, buttons: 0 }));
el.dispatchEvent(new MouseEvent('click', { ...options, buttons: 0 }));
};
const clickRow = async (row) => {
humanClick(row);
await nextFrame();
};
const waitFor = async (fn, timeout = 4000) => {
const start = Date.now();
while (Date.now() - start < timeout) {
const value = fn();
if (value) return value;
await sleep(20);
}
return null;
};
const openAssigned = async () => {
const opened = await openFilterDropdown(assignedButton(), 'Assigned to filter button not found.');
return opened;
};
const openFilterDropdown = async (button, missingMessage = 'Filter button not found.') => {
if (listbox()) return true;
if (!button) throw new Error(missingMessage);
humanClick(button);
if (await waitFor(listbox, 1000)) return true;
button.focus();
button.dispatchEvent(new KeyboardEvent('keydown', {
key: 'Enter',
code: 'Enter',
keyCode: 13,
which: 13,
bubbles: true,
cancelable: true,
}));
button.dispatchEvent(new KeyboardEvent('keyup', {
key: 'Enter',
code: 'Enter',
keyCode: 13,
which: 13,
bubbles: true,
cancelable: true,
}));
return Boolean(await waitFor(() => dropdown() && rows().length > 0));
};
const closeDropdown = async (button = assignedButton()) => {
if (button?.getAttribute('aria-expanded') === 'true') {
humanClick(button);
await nextFrame();
if (!listbox()) return;
}
window.dispatchEvent(new KeyboardEvent('keydown', {
key: 'Escape',
code: 'Escape',
keyCode: 27,
which: 27,
bubbles: true,
}));
document.dispatchEvent(new KeyboardEvent('keydown', {
key: 'Escape',
code: 'Escape',
keyCode: 27,
which: 27,
bubbles: true,
}));
await nextFrame();
if (listbox()) {
document.body.click();
await nextFrame();
}
};
const closeAssigned = async () => closeDropdown(assignedButton());
const collectAssigneeNames = async () => {
const seen = new Set();
const names = [];
const captureVisibleNames = () => {
for (const name of rows().map(rowName)) {
if (!isAssigneeName(name) || seen.has(name)) continue;
seen.add(name);
names.push(name);
}
};
captureVisibleNames();
const scroller = listScroller();
if (!scroller) return names;
scroller.scrollTop = 0;
scroller.dispatchEvent(new Event('scroll', { bubbles: true }));
await nextFrame();
captureVisibleNames();
let previousTop = -1;
while (scroller.scrollTop !== previousTop) {
previousTop = scroller.scrollTop;
scroller.scrollTop += Math.max(31, scroller.clientHeight - 31);
scroller.dispatchEvent(new Event('scroll', { bubbles: true }));
await nextFrame();
captureVisibleNames();
}
return names;
};
const clearSelection = async (knownName = null) => {
if (knownName) {
const knownRow = await findRowByScrolling(knownName);
if (knownRow && checkbox(knownRow)?.getAttribute('aria-checked') === 'true') {
await clickRow(knownRow);
await waitFor(() => {
const row = findVisibleRow(knownName);
return !row || checkbox(row)?.getAttribute('aria-checked') === 'false';
}, 2000);
return;
}
}
const clearButton = Array.from(dropdown()?.querySelectorAll('button') || [])
.find((button) => (button.textContent || '').trim() === 'Clear' && !button.disabled && !button.classList.contains('disabled'));
if (clearButton) {
humanClick(clearButton);
await waitFor(() => checkedRows().length === 0, 1000);
return;
}
for (const row of checkedRows()) {
await clickRow(row);
}
};
const findVisibleRow = (name) => rows().find((row) => rowName(row) === name);
const findRowByScrolling = async (name) => {
const scroller = listScroller();
if (!scroller) return null;
scroller.scrollTop = 0;
await nextFrame();
let previousTop = -1;
while (scroller.scrollTop !== previousTop) {
const row = findVisibleRow(name);
if (row) return row;
previousTop = scroller.scrollTop;
scroller.scrollTop += Math.max(31, scroller.clientHeight - 31);
scroller.dispatchEvent(new Event('scroll', { bubbles: true }));
await nextFrame();
}
return findVisibleRow(name);
};
const selectSingle = async (name) => {
const currentBeforeOpen = currentFromButton();
setStatus(`Opening Assigned to...`);
if (!(await openAssigned())) throw new Error('Assigned to dropdown did not open.');
const before = currentSelection();
const currentName = before.length === 1 ? before[0] : currentBeforeOpen;
const sameSelection = currentName === name;
setStatus(sameSelection ? `Clearing ${name}...` : `Clearing existing selection...`);
await clearSelection(currentName);
if (!sameSelection) {
setStatus(`Selecting ${name}...`);
const target = await findRowByScrolling(name);
if (!target) throw new Error(`Could not find assignee row: ${name}`);
if (checkbox(target)?.getAttribute('aria-checked') !== 'true') {
await clickRow(target);
await waitFor(() => {
const row = findVisibleRow(name);
return row && checkbox(row)?.getAttribute('aria-checked') === 'true';
}, 2000);
}
}
const after = sameSelection ? [] : [name];
await closeAssigned();
setActive(after.length === 1 ? after[0] : null);
setStatus(after.length ? `Selected ${after[0]}` : 'No assignee filter');
};
const clearFilterBar = async () => {
const clearButton = document.querySelector('.vss-FilterBar button[aria-label="Clear filters"]');
if (!clearButton || clearButton.disabled || clearButton.classList.contains('disabled')) return;
humanClick(clearButton);
await waitFor(() => !currentFromButton(), 1000);
};
const placeQuickPicks = async () => {
const bar = await ensureFilterBar();
if (!bar) throw new Error('Filter bar not found.');
if (root.nextElementSibling !== bar) {
bar.before(root);
}
return bar;
};
const ensureFilterBar = async () => {
if (filterBar()) return filterBar();
const toggle = document.querySelector('#__bolt-filter')
|| document.querySelector('button[aria-label="Filter"]');
if (!toggle) return null;
humanClick(toggle);
const bar = await waitFor(filterBar, 2000);
if (!bar) return null;
await waitFor(() => assignedButton() && statesButton(), 2000);
return bar;
};
const setStatesExceptDone = async () => {
const button = await waitFor(statesButton, 2000);
setStatus('Opening States...');
if (!(await openFilterDropdown(button, 'States filter button not found.'))) {
throw new Error('States dropdown did not open.');
}
const stateRows = rows().filter((row) => rowName(row));
for (const row of stateRows) {
const name = rowName(row);
const isDone = name === 'Done';
const isChecked = checkbox(row)?.getAttribute('aria-checked') === 'true';
if ((isDone && isChecked) || (!isDone && !isChecked)) {
await clickRow(row);
}
}
await closeDropdown(button);
};
const prepareVisibleFilters = async () => {
setStatus('Resetting filters...');
await placeQuickPicks();
await closeDropdown();
await clearFilterBar();
await nextFrame();
await waitFor(statesButton, 2000);
setActive(null);
await syncAssignees();
await setStatesExceptDone();
await placeQuickPicks();
setStatus('Ready');
};
const setStatus = (text) => {
const status = document.querySelector(`#${ROOT_ID} .ado-assignee-status`);
if (status) status.textContent = text;
};
const setActive = (name) => {
document.querySelectorAll(`#${ROOT_ID} [data-assignee]`).forEach((button) => {
button.classList.toggle('active', button.dataset.assignee === name);
});
};
document.getElementById(ROOT_ID)?.remove();
document.getElementById(TOGGLE_ID)?.closest('.hub-group-container')?.remove();
document.getElementById(STYLE_ID)?.remove();
document.documentElement.classList.add(HIDDEN_CLASS);
const style = document.createElement('style');
style.id = STYLE_ID;
style.textContent = `
html.${HIDDEN_CLASS} #${ROOT_ID} {
display: none;
}
#${ROOT_ID} {
align-items: center;
display: flex;
flex-wrap: wrap;
gap: 4px;
margin: 6px 80px;
position: relative;
z-index: 20;
}
#${ROOT_ID} button {
background: var(--palette-neutral-4, #f3f2f1);
border: 1px solid var(--palette-neutral-20, #c8c6c4);
border-radius: 3px;
color: inherit;
cursor: pointer;
font: 12px var(--fontFamily, inherit);
line-height: 18px;
min-height: 24px;
padding: 2px 6px;
}
#${ROOT_ID} button:hover,
#${ROOT_ID} button.active {
background: rgba(0, 120, 212, 0.14);
border-color: rgb(0, 120, 212);
}
#${ROOT_ID} .ado-assignee-status {
color: var(--text-secondary-color, #605e5c);
display: none;
font-size: 12px;
margin-left: 6px;
}
#${TOGGLE_ID} {
align-items: center;
background: transparent;
border: 0;
color: inherit;
cursor: pointer;
display: flex;
flex-shrink: 0;
font: inherit;
height: 40px;
justify-content: center;
margin: 0;
padding: 0;
text-align: left;
width: 100%;
}
#${TOGGLE_ID}:hover,
#${TOGGLE_ID}:focus-visible,
#${TOGGLE_ID}.active {
background: rgba(0, 120, 212, 0.12);
}
#${TOGGLE_ID}.active .navigation-icon {
color: rgb(0, 120, 212);
}
`;
document.head.append(style);
const root = document.createElement('div');
root.id = ROOT_ID;
const status = document.createElement('span');
status.className = 'ado-assignee-status';
status.textContent = 'Prototype ready';
root.append(status);
const renderAssigneeButtons = () => {
root.querySelectorAll('[data-assignee]').forEach((button) => button.remove());
const fragment = document.createDocumentFragment();
for (const name of assignees) {
const button = document.createElement('button');
button.type = 'button';
button.dataset.assignee = name;
button.title = name;
button.textContent = displayName(name);
fragment.append(button);
}
root.insertBefore(fragment, status);
};
const syncAssignees = async () => {
setStatus('Loading assignees...');
if (!(await openAssigned())) throw new Error('Assigned to dropdown did not open.');
const names = await collectAssigneeNames();
await closeAssigned();
if (!names.length) throw new Error('No assignees found in Assigned to dropdown.');
assignees = names;
renderAssigneeButtons();
};
const buildToggle = () => {
const container = document.createElement('div');
container.className = 'hub-group-container flex-column flex-noshrink relative hub-group-only';
const button = document.createElement('button');
button.id = TOGGLE_ID;
button.type = 'button';
button.className = 'hub-group navigation-element navigation-link focus-treatment flex-row flex-grow flex-center scroll-hidden relative bolt-link no-underline-link';
button.setAttribute('role', 'menuitem');
button.setAttribute('tabindex', '-1');
button.setAttribute('aria-pressed', 'false');
button.setAttribute('aria-label', 'Show assignee quick picks');
button.title = 'Show assignee quick picks';
button.innerHTML = `
<span class="navigation-icon flex-row flex-center flex-noshrink justify-center">
<span class="fluent-icons-enabled" role="img" aria-hidden="true">
<span aria-hidden="true" class="navigation-icon flex-row flex-center justify-center flex-noshrink fabric-icon ms-Icon--People medium"></span>
</span>
</span>
<span class="navigation-text expanded-only text-ellipsis flex-grow">Assignees</span>
`;
button.addEventListener('click', async () => {
const hidden = document.documentElement.classList.toggle(HIDDEN_CLASS);
button.classList.toggle('active', !hidden);
button.setAttribute('aria-pressed', String(!hidden));
button.setAttribute('aria-label', hidden ? 'Show assignee quick picks' : 'Hide assignee quick picks');
button.title = hidden ? 'Show assignee quick picks' : 'Hide assignee quick picks';
if (!hidden) {
button.disabled = true;
try {
await prepareVisibleFilters();
} catch (error) {
console.error(error);
setStatus(error.message);
} finally {
button.disabled = false;
}
}
});
container.append(button);
return container;
};
const navSection = document.querySelector('.navigation-container .project-navigation .navigation-section');
navSection?.append(buildToggle());
root.addEventListener('click', async (event) => {
const button = event.target.closest('button');
if (!button) return;
button.disabled = true;
try {
if (button.dataset.assignee) {
await selectSingle(button.dataset.assignee);
}
} catch (error) {
console.error(error);
setStatus(error.message);
} finally {
button.disabled = false;
}
});
console.log('ADO assignee quick-pick prototype injected.');
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment