Last active
July 10, 2023 13:42
-
-
Save chrisbreiding/f41e16aa8c6876b13e7627de513c303d 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 Github Project Status Colors | |
// @description Enhances GitHub Projects UI | |
// @match https://github.com/orgs/cypress-io/projects/10/views/* | |
// @version 12 | |
// @grant none | |
// @downloadURL https://gist.github.com/chrisbreiding/f41e16aa8c6876b13e7627de513c303d/raw/7de21eb9f36afdb0625065358ac002f77c2a573d/github-project-status-colors.user.js | |
// @updateURL https://gist.github.com/chrisbreiding/f41e16aa8c6876b13e7627de513c303d/raw/7de21eb9f36afdb0625065358ac002f77c2a573d/github-project-status-colors.user.js | |
// ==/UserScript== | |
(function () { | |
const isDarkMode = document.body.parentNode.dataset.colorMode === 'dark'; | |
const getValue = (configValue) => { | |
const { property, value } = configValue; | |
return { | |
property, | |
value: value | |
? isDarkMode ? value.dark : value.light | |
: value | |
} | |
} | |
// 'light' values are applied to light mode, 'dark' values to dark mode | |
const config = { | |
statusColors: { | |
// Backlog items should have default styling. This ensures it's reset if changed from another status to 'Backlog'. | |
'Backlog': { | |
background: [{ property: 'backgroundColor', value: null }, { property: 'backgroundImage', value: null }], | |
}, | |
'Done': { | |
background: [{ property: 'backgroundColor', value: { light: '#e5ffe5', dark: '#2d532d' } }], | |
}, | |
'In Progress': { | |
background: [{ property: 'backgroundColor', value: { light: '#fdfdbb', dark: '#606003' } }], | |
}, | |
'In Review': { | |
background: [{ property: 'backgroundColor', value: { light: '#ffe2c9', dark: '#6a3c14' } }], | |
}, | |
'New': { | |
background: [{ property: 'backgroundColor', value: { light: '#daf1ff', dark: '#0a3955' } }], | |
}, | |
'Prioritized': { | |
background: [{ property: 'backgroundColor', value: { light: '#eeeeee', dark: '#404040' } }], | |
}, | |
'Blocked': { | |
background: [{ property: 'backgroundImage', value: { light: 'repeating-linear-gradient(135deg, #f7ceab, #f7ceab 20px, #ffe2c9 20px, #ffe2c9 40px)', dark: 'repeating-linear-gradient(135deg, #a25616, #a25616 20px, #6a3c14 20px, #6a3c14 40px)' } }] | |
}, | |
}, | |
labelColors: { | |
'Epic': { | |
background: [ | |
{ property: 'backgroundImage', value: { light: 'linear-gradient(rgba(0, 0, 0, 0.1) 2px, transparent 2px), linear-gradient(90deg, rgba(0, 0, 0, 0.1) 2px, transparent 2px), linear-gradient(rgba(0, 0, 0, 0.1) 1px, transparent 1px), linear-gradient(90deg, rgba(0, 0, 0, 0.1) 1px, transparent 1px)', dark: 'linear-gradient(rgba(255, 255, 255, 0.1) 2px, transparent 2px), linear-gradient(90deg, rgba(255, 255, 255, 0.1) 2px, transparent 2px), linear-gradient(rgba(255, 255, 255, 0.1) 1px, transparent 1px), linear-gradient(90deg, rgba(255, 255, 255, 0.1) 1px, transparent 1px)' } }, | |
{ property: 'backgroundSize', value: { light: '50px 50px, 50px 50px, 10px 10px, 10px 10px', dark: '50px 50px, 50px 50px, 10px 10px, 10px 10px' } }, | |
{ property: 'backgroundPosition', value: { light: '-10px 10px, 10px -10px, -8px -8px, -8px -8px', dark: '-10px 10px, 10px -10px, -8px -8px, -8px -8px' } }, | |
], | |
text: [ | |
{ property: 'fontWeight', value: { light: '600', dark: '600' } }, | |
], | |
}, | |
}, | |
assigneeColors: { | |
'AtofStryker': '#db4d5e', | |
'cacieprins': '#4d83db', | |
'chrisbreiding': '#828282', | |
'dkasper-was-taken': '#b84ddb', | |
'emilyrohrbough': '#dbd24d', | |
'mjhenkes': '#db944d', | |
'mschile': '#77b53c', | |
'nagash77': '#a14ddb', | |
'ryanthemanuel': '#111111', | |
}, | |
pollingInterval: 1000, // ms | |
}; | |
const warnOfDomChange = (description) => { | |
console.warn('⚠️ GitHub Project Tampermonkey Script: Could not find', description, '\n⚠️ GitHub may have changed its DOM structure'); | |
}; | |
const collectStyles = (configValues) => { | |
if (!configValues) return; | |
// these default styles ensure that any of these get reset if it no | |
// longer applies | |
const styles = { | |
background: { | |
backgroundColor: [], | |
backgroundImage: [], | |
backgroundSize: [], | |
backgroundPosition: [], | |
}, | |
text: { | |
fontWeight: [], | |
}, | |
}; | |
(configValues.background || []).forEach((configValue) => { | |
const { property, value } = getValue(configValue); | |
styles.background[property].push(value); | |
}); | |
(configValues.text || []).forEach((configValue) => { | |
const { property, value } = getValue(configValue); | |
styles.text[property].push(value); | |
}); | |
return styles; | |
}; | |
const applyStyles = (cardEl, configColors, selector) => { | |
const els = cardEl.querySelectorAll(selector); | |
if (!els.length) return; | |
Array.from(els).forEach((el) => { | |
const text = el.textContent; | |
Object.entries(configColors).forEach(([type, configStyles]) => { | |
if (text !== type) return; | |
const styles = collectStyles(configStyles); | |
// clear the properties with null value in case they previously | |
// matched, but no longer do | |
Object.entries(styles.background).forEach(([property, styleValues]) => { | |
const value = styleValues.length ? styleValues.join(',') : null; | |
cardEl.children[0].style[property] = value; | |
}); | |
const textEl = cardEl.querySelector('[data-testid=card-side-panel-trigger]'); | |
if (!textEl) return; | |
Object.entries(styles.text).forEach(([property, styleValues]) => { | |
const value = styleValues.length ? styleValues.join(',') : null; | |
textEl.style[property] = value; | |
}); | |
}); | |
}); | |
}; | |
const applyColors = () => { | |
requestAnimationFrame(() => { | |
const cardsSelector = '[data-testid="board-view-column-card"]'; | |
const cardEls = document.querySelectorAll(cardsSelector); | |
if (!cardEls || !cardEls.length) return warnOfDomChange(`any cards with selector: ${cardsSelector}`); | |
Array.from(cardEls).forEach((cardEl) => { | |
applyStyles(cardEl, config.statusColors, '[data-testid="single-select-token"]'); | |
applyStyles(cardEl, config.labelColors, '[data-testid="issue-label"]'); | |
}); | |
Object.keys(config.assigneeColors).forEach((assignee) => { | |
const assigneeImgWrappers = document.querySelectorAll(`[data-testid="AvatarItemFilterButton"][aria-label="${assignee}"]`); | |
const color = config.assigneeColors[assignee]; | |
Array.from(assigneeImgWrappers).forEach((assigneeImgWrapper) => { | |
assigneeImgWrapper.style.outline = `solid 4px ${color}`; | |
}) | |
}); | |
}); | |
}; | |
window.addEventListener('DOMContentLoaded', applyColors); | |
setInterval(applyColors, config.pollingInterval); | |
applyColors(); | |
/* Toggle past iterations */ | |
const togglePastIterations = (shouldHide = false) => { | |
let shouldChange = true; | |
Array.from(document.querySelectorAll('[data-testid=board-view-column-title-text]')).forEach((node) => { | |
const currentLabel = Array.from(node.parentElement.children).find((node) => node.textContent === 'Current'); | |
if (currentLabel) { | |
shouldChange = false; | |
} | |
if (node.textContent === 'No Iteration' && !document.querySelector('.toggle-past-iterations')) { | |
const button = document.createElement('button'); | |
button.className= 'toggle-past-iterations'; | |
button.textContent = 'Toggle Past Iterations'; | |
button.style.border = 'none'; | |
button.style.borderRadius = '3px'; | |
button.style.backgroundColor = '#e3e3e3'; | |
button.style.padding = '4px 8px'; | |
node.parentElement.parentElement.appendChild(button); | |
return; | |
} | |
if (node.textContent === 'No Iteration' || !shouldChange) return; | |
node.closest('[data-testid=board-view-column]').style.display = shouldHide ? 'none' : null; | |
}); | |
}; | |
const localStorageKey = '__gpeHidePastIterations'; | |
const getLocalStorageValue = () => JSON.parse(localStorage[localStorageKey] || null); | |
document.addEventListener('click', (e) => { | |
if (e.target.className !== 'toggle-past-iterations') return; | |
const change = !getLocalStorageValue(); | |
localStorage[localStorageKey] = JSON.stringify(change); | |
togglePastIterations(change); | |
}); | |
window.addEventListener('DOMContentLoaded', () => togglePastIterations(getLocalStorageValue())); | |
togglePastIterations(getLocalStorageValue()); | |
}()); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment