Skip to content

Instantly share code, notes, and snippets.

@angry-dan
Last active December 15, 2015 17:50
Show Gist options
  • Save angry-dan/5299401 to your computer and use it in GitHub Desktop.
Save angry-dan/5299401 to your computer and use it in GitHub Desktop.
Sum of hours in a Redmine task list
// ==UserScript==
// @name Redmine Hour Counter
// @namespace http://projects.deeson.info
// @description Adds up selected hours.
// @include https://projects.deeson.info/*
// @grant none
// ==/UserScript==
/**
* Adds the count icon at the top of the page.
* When clicked it counts the total number of hours.
*/
loadJQuery(function ($) {
function update() {
var selector = 'tr:not(.parent) .estimated_hours';
if ($('tr.context-menu-selection').length) {
selector = 'tr.context-menu-selection:not(.parent) .estimated_hours';
}
var incomplete;
var hours = $(selector)
.map(function() {
return parseFloat($(this).text())
})
.toArray()
.reduce(function(initial, current) {
if (isNaN(current)) {
incomplete = true;
return initial;
}
return initial + current;
}, 0);
var message = hours + (hours == 1 ? ' hour' : ' hours');
if (incomplete) {
message += " <small>(incomplete)</small>";
}
if (!hours) {
message = '<small>No estimate</small>';
}
$button.html(message);
}
var enableUx = $(document).has('.subject').length;
// enableUx = false;
if ($(document).has('.estimated_hours').length) {
var $button = $('<span class="icon icon-time" style="float: right;"></span>');
$('#query_form p.buttons').append($button);
$(update);
$(document).click(update);
if (enableUx) {
// Array of header cells to hide.
var headersToHide = [];
$('.subject')
.each(function(){
var subject = $(this),
inner = $('a',subject);
// Estimates
var estimate = subject.siblings('.estimated_hours');
estimate.hide();
if (!headersToHide[0]) {
headersToHide.push(estimate.index());
}
if (estimate.html()) {
inner.prepend('<span class="inline_estimated_hours" style="font-size: smaller;">[' + estimate.html() + ']</span> ');
}
// Assignees
// Due dates
// Tracker, Status, all go as icons. Priority, Severity might be combined into one somehow?
// Category, Updated, Target version goes
});
var headers = $('.issues th');
$.each(headersToHide, function(k,i) {
headers.eq(i).hide();
});
}
}
trackerIcons();
/**
* Uses jQuery to convert the tracker names to icons.
*/
function trackerIcons() {
$('table.issues td.tracker')
.each(function(){
var originalText = $(this).text();
var css = originalText.toLowerCase().replace(/[^a-z0-9]+/g,'-');
var replacement = $('<span />').addClass(css).addClass('tracker-icon').text(originalText).attr('title', originalText);
$(this).empty().append(replacement);
});
addGlobalStyle(getTrackerIconCss());
}
/**
* Returns CSS for presenting the tracker icons.
*/
function getTrackerIconCss() {
var css = '.tracker-icon{background-position: 0 0;display: inline-block;height: 16px;text-indent: -999em;width: 16px;background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAIAAAAAgCAMAAADt/IAXAAADAFBMVEUOXI3u7u4OXI0OXI3u7u4OXI4QY5f////9/f3///8QY5cPX5H///8QY5f19fXy8vIPXY////8QY5f///////8QY5cQY5cQY5f///8QY5f///8PYJP///8QY5f///8QY5cQY5f///8PXo/5+fkPYJP///8QY5f////+/v4PXY0QY5cPYpYQY5fz8/MPXpD29vb////8/Pz///8QY5f19fXu7u4PXY309PQOXI3///////8QY5f///////8QY5f39/f///8QY5f///8QY5cQY5f////x8fH////x8fEPXY8QY5f///////8QY5f///////8PX5L////z8/P///8PXpDw8PAQY5fu7u709PQPYpX///8PXY7w8PDw8PDv7+8PXI3///////8QY5f///////////////8QY5f///8QY5f///8PYZTz8/MQY5fz8/P6+vr8/Pz///8QY5cQY5cPYZUQY5f///////8QY5f///////8QY5f///////8QY5f///8QY5cQY5f///////8QY5f///////8PYJLv7+/////5+fkPYJMQY5f////29vYPX5EQY5cQY5f///8QY5f////////////u7u4QY5f///8PYZQQY5cQY5f////09PT7+/sQY5cQY5f///8QY5cQY5f///////8PXY4QY5f///////8QY5f///8QY5cQY5cQY5f9/f0PX5H///8QY5f///////8QY5f///////8PYpYPXpAQY5cQY5cQY5f////w8PAQY5f4+Pjx8fEQY5f///////////8QY5cQY5cQY5cQY5f///8PXY719fUQY5f///8QY5cQY5f///8QY5fy8vL///8PXpAPXo8QY5f///8PX5EQY5f///8QY5cQY5fx8fEQY5cPXY8PXY7///8PYZT6+voQY5cQY5f///////8QY5f///8QY5cAAAAOXpAOXpAOXpHv7+8OXpAPXY7u7u77+/v9/f3x8fH6+vr8/Pz39/cPXY/+/v4PYZX///8QY5dLTLOzAAAA7nRSTlP+/vz//v5qZ6eEbch2dsvg4JmYnTGZRZ14RIe8TngvTop82rW1Xnw9o/IZo17U1MVyqDZyzfnxzvkOV1eAVlbCi4BJL41L5Czh4SxgenoE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);}';
css += '.tracker-icon.bug {background-position: 0px 0;}';
css += '.tracker-icon.feature {background-position: -16px 0;}';
css += '.tracker-icon.epic-feature {background-position: -32px 0;}';
css += '.tracker-icon.support {background-position: -48px 0;}';
css += '.tracker-icon.content {background-position: -64px 0;}';
css += '.tracker-icon.checklist {background-position: -80px 0;}';
css += '.tracker-icon.checklist-summary {background-position: -96px 0;}';
css += '.tracker-icon.code-quality {background-position: -112px 0;}';
css += '.tracker-icon.change-request {background-position: -128px 0;}';
css += '.context-menu-selection .tracker-icon.bug {background-position: 0px 16px;}';
css += '.context-menu-selection .tracker-icon.feature {background-position: -16px 16px;}';
css += '.context-menu-selection .tracker-icon.epic-feature {background-position: -32px 16px;}';
css += '.context-menu-selection .tracker-icon.support {background-position: -48px 16px;}';
css += '.context-menu-selection .tracker-icon.content {background-position: -64px 16px;}';
css += '.context-menu-selection .tracker-icon.checklist {background-position: -80px 16px;}';
css += '.context-menu-selection .tracker-icon.checklist-summary {background-position: -96px 16px;}';
css += '.context-menu-selection .tracker-icon.code-quality {background-position: -112px 16px;}';
css += '.context-menu-selection .tracker-icon.change-request {background-position: -128px 16px;}';
return css;
}
/**
* Inject CSS into the document head.
*/
function addGlobalStyle(css) {
var head, style;
head = document.getElementsByTagName('head')[0];
if (!head) { return; }
style = document.createElement('style');
style.type = 'text/css';
style.innerHTML = css;
head.appendChild(style);
}
});
/**
* Adapted from http://erikvold.com/blog/index.cfm/2010/6/14/using-jquery-with-a-user-script.
* Injects a JQuery script element into the page, then injects our script into the dom
* for execution at local scope (i.e. with access to JQuery)
*
* Remember that the contents of callback are evaluated at the window scope, so
* it won't have access to anything else defined in this file.
*/
function loadJQuery(callback) {
var script = document.createElement("script");
script.setAttribute("src", "//ajax.googleapis.com/ajax/libs/jquery/1/jquery.min.js");
script.addEventListener('load', function() {
var script = document.createElement("script");
script.textContent = "(" + callback.toString() + ")(jQuery.noConflict(true));";
document.body.appendChild(script);
}, false);
document.body.appendChild(script);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment