Skip to content

Instantly share code, notes, and snippets.

@chrisbreiding
Last active July 10, 2023 13:42
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save chrisbreiding/f41e16aa8c6876b13e7627de513c303d to your computer and use it in GitHub Desktop.
Save chrisbreiding/f41e16aa8c6876b13e7627de513c303d to your computer and use it in GitHub Desktop.
// ==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