Skip to content

Instantly share code, notes, and snippets.

@curtisj44
Last active January 15, 2019 20:46
Show Gist options
  • Save curtisj44/9491644 to your computer and use it in GitHub Desktop.
Save curtisj44/9491644 to your computer and use it in GitHub Desktop.
User Script: Trello
// ==UserScript==
// @description Make Trello more awesome
// @downloadURL https://gist.github.com/curtisj44/9491644
// @grant none
// @include https://trello.com/*
// @name Trello
// @namespace trello
// @require https://ajax.googleapis.com/ajax/libs/jquery/3.2.1/jquery.min.js
// @version 2.3.0
// ==/UserScript==
(function () {
'use strict';
var
$lists,
$listHeaders,
addBoardTotal = function () {
var total = 0;
$.each($listHeaders, function (index, value) {
var $text = $(value).clone().children().remove().end().text();
if ($text !== '') {
total += parseInt($text.replace(' cards', '').replace(' card', ''), 10);
}
});
$('.board-header')
.find('b').remove().end()
.append('<b class="board-header-btn" style="padding: 0 10px; background: rgba(0, 0, 0, .12);">' + total + ' cards</b>');
},
addCardTotal = function () {
$listHeaders.removeClass('hide').css('display', 'block');
},
addCustomStyles = function () {
// Hide options I never use
const attachments = `.js-google-drive-attachment,
.js-dropbox-attachment,
.js-box-attachment,
.js-one-drive-attachment,
.js-add-attachment-url + hr,
.js-add-attachment-url + hr + .quiet`;
const shareButton = 'a.button-link[title="Share"]';
const sidebarHorizontalRules = '.window-module hr';
const watchButton = 'a.button-link[title^="Watch"]';
$('body').append(
`<style>
${ attachments },
${ shareButton },
${ sidebarHorizontalRules },
${ watchButton } {
display: none !important;
}
</style>`
);
},
// Calculates total sprint capacity based on one of the follow options based on
// card title and/or a custom field
//
// Options
// A: "Title of card [n]", where `n` is any number
// B: "Title of card [XS]", where `XS` is one of the `sizes` values defined below
// C: "Estimate" custom field using the `sizes` values defined below
// D: "Estimate (in days)" custom field using any number
addEstimateTotal = function ($list, $cards) {
var
capacity = 0,
estimateDetails,
missingEstimates = 0,
re = /\[(.*?)\]/g,
sizes = {
'XXS': '.25',
'XS': '.5',
'S': '1',
'M': '3',
'L': '5',
'XL': '7',
'XXL': '10'
},
total = 0,
unit = 'hours',
flagCard = function (listName, $card) {
var
listsToInclude = listName.indexOf('Backlog') > -1 ||
listName.indexOf('Benched') > -1 ||
listName.indexOf('BLOCKED') > -1 ||
listName.indexOf('Done') > -1 ||
listName.indexOf('On Deck') > -1 ||
listName.indexOf('On Hold') > -1 ||
listName.indexOf('Pull Requested') > -1 ||
listName.indexOf('Release') > -1 ||
listName.indexOf('Sprint') > -1 ||
listName.indexOf('To Design') > -1 ||
listName.indexOf('To Develop') > -1 ||
listName.indexOf('Working On') > -1;
if (listsToInclude) {
missingEstimates++;
$card.css('background', 'rgba(230, 200, 200, 1)');
}
},
unFlagCard = function ($card) {
$card.removeAttr('style');
},
isSizeEstimate = function (estimate) {
return (sizes[estimate] > 0);
};
$cards.each(function (index, value) {
var
$card = $(value),
bracketIndex = 0,
estimate = 0,
listName = getListName($list),
cardTitle = getCardTitle($card);
unFlagCard($card);
if (cardTitle.indexOf('[') > -1) {
if (cardTitle.indexOf('[') !== cardTitle.lastIndexOf('[')) {
bracketIndex = 1;
}
estimate = cardTitle.match(re)[bracketIndex].replace('[', '').replace(']', '');
if ($.isNumeric(estimate)) {
if (cardTitle.indexOf('Sprint Planning') > -1) {
// Capture the capacity size from the "Sprint Planning" card
// Requires this card title format to determine the capacity: `Sprint Planning [x]`, where `x` is the number of days
capacity = estimate;
$card.css('background', 'rgba(200, 200, 200, 1)');
} else {
// capture the hourly estimates from all of the other cards
total = ((total * 100000) + (estimate * 100000)) / 100000;
}
// "[?]" cards
} else if (estimate.indexOf('?') > -1) {
flagCard(listName, $card);
// "[RELEASE]" cards
} else if (estimate.indexOf('RELEASE') > -1) {
$card.css('background', 'rgba(200, 208, 200, 1)');
// t-shirt size estimates
} else if (isSizeEstimate(estimate)) {
unit = 'days';
total = ((total * 100000) + (sizes[estimate] * 100000)) / 100000;
// missing estimates
} else {
flagCard(listName, $card);
}
} else {
// Check for "Estimate" custom field
var
$badgeText = $card.find('.badge-text'),
hasEstimateField = false;
$badgeText.each(function (index, value) {
var
badgeText = $(value).text(),
isEstimateField = badgeText.indexOf('Estimate:') > -1,
isEstimateInDaysField = badgeText.indexOf('Estimate (in days):') > -1;
if (isEstimateField) {
hasEstimateField = true;
estimate = badgeText.replace('Estimate: ', '');
unit = 'days';
total = ((total * 100000) + (sizes[estimate] * 100000)) / 100000;
} else if (isEstimateInDaysField) {
hasEstimateField = true;
estimate = badgeText.replace('Estimate (in days): ', '');
unit = 'days';
total = total + parseInt(estimate, 10);
}
});
// missing estimate
if (!hasEstimateField) {
flagCard(listName, $card);
}
}
});
$list.find('.list-header-num-cards span').remove();
// add missing estimate count
if (missingEstimates > 0) {
missingEstimates = '<span style="float: right; margin: 0 -26px 0 36px; padding: 0 .6em; border-radius: 3px; background: rgba(230, 200, 200, 1); color: #4d4d4d; font-weight: bold; font-size: 10px;">' +
missingEstimates +
'</span>';
$list.find('.list-header-num-cards').append(missingEstimates);
}
// add estimate count
if (total > 0) {
estimateDetails = total;
if (capacity) {
estimateDetails += ' / ' + capacity;
}
estimateDetails = '<span style="float: right; margin-right: -26px;">' +
estimateDetails + ' ' + unit +
'</span>';
$list.find('.list-header-num-cards').append(estimateDetails);
}
},
getCardTitle = function ($card) {
return $card.find('.list-card-title').text();
},
getListName = function ($list) {
return $list.find('.list-header-name').text();
},
highlightLists = function ($list, $cards) {
var listName = getListName($list);
// release lists
if (listName.indexOf('2015-') > -1 || listName.indexOf('2016-') > -1) {
$list.css('background', 'rgba(200, 230, 200, 1)');
}
// "Working On" list
if (listName.indexOf('Working On') > -1) {
$list.css('background', 'rgba(240, 230, 200, 1)');
}
},
init = function () {
if ($('.list').length > 0) {
watchLists();
} else {
window.setTimeout(init, 600);
}
addCustomStyles();
},
modifyCards = function ($lists) {
$lists
.css('flex', '0 0 380px')
.find('.list-card').css('max-width', 'none');
},
modifyLists = function () {
// console.log('modifyLists');
$lists = $('.list');
$listHeaders = $('.list-header-num-cards');
addCardTotal();
$lists.each(function (index, value) {
var
$list = $(value),
$cards = $list.find('.list-card');
addEstimateTotal($list, $cards);
highlightLists($list, $cards);
$list.find('.list-header-name').each(function (index1, value1) {
if ($(value1).text() === 'To Do') {
modifyCards($lists);
}
});
});
},
watchLists = function () {
// console.log('watchLists');
// TODO: this doesn't work anymore :(
// $(document).ajaxComplete(function (event, request, settings) {
// // console.log(event);
// // console.log(request);
// // console.log(settings);
// modifyLists();
// addBoardTotal();
// });
var observer = new MutationObserver(function (mutationsList) {
for (var mutation of mutationsList) {
if (mutation.type == 'childList') {
console.log('A child node has been added or removed.');
modifyLists();
addBoardTotal();
} else if (mutation.type == 'attributes') {
console.log('The ' + mutation.attributeName + ' attribute was modified.');
}
}
});
observer.observe(
document.getElementById('board'),
{
// attributes: true,
childList: true,
subtree: true
}
);
};
$(init);
}());
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment