The BugMonkey plugin allows you to customize your FogBugz site to suite your needs. Read more about the plugin here.
Over the years, we’ve accumulated quite a few useful customizations that we’ve worked on, helped customers write, or been given by enterprising users. Here’s an archive of our favorites, and a major thanks to anyone and everyone who has contributed!

Contents

Save Room with Collapsible Filters

Originally posted by nonplus.

Once you have lots of (shared) filters, the filter menu can become cumbersome to use, especially when you try to access your own filters. With this customization, you can organize filters into collapsible groups based on common prefixes.

Anything in the filter name before the first colon is considered the filter’s group name (e.g. if filter name is “Testing: Most Recent Build” then “Testing” is the group name). To group two or more filters together, use the same prefix for them, e.g. “Testing: Production”, “Testing: Development”.

If a group contains two or more filters, the Filters menu will show “Group Name: n filters” instead of the actual filters. Clicking on this item will show or hide the group’s original list of filters indented below it.

filters

Here’s the script for FogBugz for Your Server:

name:          Grouped Filters
description:   Changes Filters menu to group filters by group name
author:        Stepan Riha
version:       1.0.0.4

js:

$(document).ready(function() {

    // Change visibility of links in group
    function toggleGroup() {
        var $this = $(this);
        var group = $this.data('group');

        if(!group.prepared) {
            prepareLinks(group);
        }

        processLinks(group, group.isOpen
            ? function($link) { $link.hide(); }
            : function($link) { $link.show(); });

        group.isOpen = !group.isOpen;

        return false;
    };

    // Replace filter group name with its list of filters
    function prepareLinks(group) {
        var groupname = jQuery.trim(group.text+":");
        processLinks(group, function($link) {
            var html = $link.html();
            html = html.replace(groupname, '');
            $link.html(html).addClass('filter_group-link');
        });
        group.prepared = true;
    };

    // Apply callback to each link in group
    function processLinks(group, callback) {
        for(var j = 0; j < group.links.length; j++) {
            callback(group.links[j]);
        }
    };

    // Collect filter links and group by prefix
    var groups = [];
    var $prev = null;
    var group = null;
    $('#filterPopup a').each(function() {
        var $this = $(this);
        // Use everything up to first : as group name
        var text = $this.text();
        text = text.replace(/:.*/, '');
        // Create new group, if necessary
        if(!group || group.text != text) {
            group = { text: text, links: [] };
            groups.push(group);
        }
        group.links.push($this);
    });

    // Process groups of 2 or more links
    for(var i = 0; i < groups.length; i++) {
        var group = groups[i];
        var links = group.links;
        if(links.length > 1) {
            // Hide links
            processLinks(group, function($link) { $link.hide(); });

            // Create group link
            group.link = $("<a href='#' class='filter_group'><span>" + group.text + ": " + links.length + " filters</span></a>")
                    .bind("click", toggleGroup)
                    .data('group', group)
                    .insertBefore(links[0]);
        }
    }
});

css:

a.filter_group {
    padding-left: 17px !important;
    font-style: italic;
}
a.filter_group span {
    padding-left: 5px;
    border-left: solid 3px #B1C9DD;
}
a.filter_group:hover span {
    border-left-color: #E0E9F1;
}
a.filter_group-link {
    padding-left: 23px !important;
}

Here’s a new version of the script for FogBugz On Demand:

name:          New Grouped Filters
description:   Changes Filters menu to group filters by group name (for new header)
author:        Stepan Riha and Adam Wishneusky
version:       1.1.0.0

js:

$(document).ready(function() {
    // Change visibility of links in group
    function toggleGroup() {
        var $this = $(this);
        var group = $this.data('group');
        if(!group.prepared) {
            prepareLinks(group);
        }
        processLinks(group, group.isOpen ? function($link) { $link.hide(); }
            : function($link) { $link.show(); });
        group.isOpen = !group.isOpen;
        return false;
    }
    // Replace filter group name with its list of filters
    function prepareLinks(group) {
        var groupname = jQuery.trim(group.text+":");
        processLinks(group, function($link) {
            var html = $link.html();
            html = html.replace(groupname, '');
            $link.html(html).addClass('filter_group-link');
        });
        group.prepared = true;
    }
    // Apply callback to each link in group
    function processLinks(group, callback) {
        for(var j = 0; j < group.links.length; j++) {
            callback(group.links[j]);
        }
    }
    // Collect filter links and group by prefix
    var groups = [];
    var $prev = null;
    var group = null;
    // selector for oldbugz + old header was $('#filterPopup a')
    // selector for ocelot + old header was $('.list-choices-popup div.list-choices-header').parent().find('a')
    // in the old header code, each filter was just an <a> tag.
    // the new header has an <li> for each item with an <a> inside
    // to keep the selectors not-insane, let's do shared and personal filters separately
    $(li.button.cases-button > span > ul.dropdown-menu > li:contains("Shared Filters") > ul > li').each(function(){ addToGroups(this); });
    $(li.button.cases-button > span > ul.dropdown-menu > li:contains("My Filters") > ul > li').each(function() { addToGroups(this); });
    function addToGroups($el) {
        // $el looks like:
        // <li>
        //   <a href="default.asp?pgx=LF&ixFilter=3149" id="" class="" title="">
        //     <img src="images/outline.gif" class="header-filter-icon">
        //     FogBugz PC
        //   </a>
        // </li>
        var $this = $($el);
        // Use everything up to first : as group name
        var text = $this.find('a').text().trim();
        text = text.replace(/:.*/, '');
        // Create new group, if necessary
        if(!group || group.text != text) {
            group = { text: text, links: [] };
            groups.push(group);
        }
        group.links.push($this);
    }
    // Process groups of 2 or more links
    for(var i = 0; i < groups.length; i++) {
        group = groups[i];
        var links = group.links;
        if(links.length > 1) {
            // Hide links
            processLinks(group, function($link) { $link.hide(); });
            // Create group link
            group.link = $("<li class='filter_group'><a href='#'><span>" + group.text + ": " + links.length + " filters</span></a></li>")
                    .bind("click", toggleGroup)
                    .data('group', group)
                    .insertBefore(links[0]);
        }
    }
});

css:

// in the old header version, these were all 'a's not 'li's
li.filter_group {
    padding-left: 14px !important;
    font-style: italic;
}
li.filter_group span {
    padding-left: 5px;
    border-left: solid 3px #B1C9DD;
}
// color for old header: E0E9F1;
li.filter_group:hover span {
    border-left-color: #B1C9DD;
}
li.filter_group-link {
    padding-left: 23px !important;
}

Search Syntax Helper

Originally posted by John Fuex. Update for FogBugz On Demand by Rohland de Charmoy.

Here’s a BugMonkey script that adds an icon next to the search box with a dropdown of search axis values. Mousing over an item in the dropdown will show more info on how to use that axis, and clicking on it will insert it in the search box.

It isn’t all that pretty yet, but it’s functional. Feel free to update the script here if you want to add some cosmetics to it or improve the mouseover text on the search items.

Snap1

This version is for FogBugz for Your Server:

name:          Search Box Helper
description:   Adds a syntax helper widget to the search box.
author:        John Fuex
version:       1.0.0.0

js:

   var searchAxes = getSearchAxes();
   var srchInput = $('#idDropList_searchFor_oText');

   var imgSearchHelperButton = $('<span></span>');
   imgSearchHelperButton.attr('id','searchHelperButton')
                        .text('?')
                        .css('position','absolute')
                        .css('left',srchInput.position().left - 25)
                        .css('top', srchInput.position().top);  
   srchInput.after(imgSearchHelperButton);
   var divSearchHelper = $('<div></div>')
   divSearchHelper.attr('id','divSearchHelper')                  
                  .css('position','absolute')
                  .css('width',srchInput.css('width'))
                  .css('top', srchInput.position().top + srchInput.outerHeight()) 
                  .css('left',imgSearchHelperButton.position().left)                  
                  .css('z-index','500')
                  .css('display','none');
  for (var axisID=0; axisID<searchAxes.length; axisID+=2) {
      var divHelpItem = $('<div></div>')      
      var itemText = searchAxes[axisID];
      var itemDescription = searchAxes[axisID+1]
      if(itemText.substr(0,1)=='#') {
         itemText = itemText.substr(1);
         divHelpItem .addClass('searchHelperItemHeader')
      }
      else {
         divHelpItem .addClass('searchHelperItem');
      }
      var helpItem = $("<a/>").text(itemText).attr('title',itemDescription);      
      divHelpItem.append(helpItem);
      divSearchHelper.append(divHelpItem );
  }
  srchInput.after(divSearchHelper);
   // Attach event handlers
   $('#searchHelperButton').click(function () { $('#divSearchHelper').toggle();});
   $(".searchHelperItem").click(function() { 
                                        var srchInput = $('#idDropList_searchFor_oText');
                                        var newSearchText = srchInput.val() + (srchInput.val() != '' ? ' ' : '') + $(this).text() + ':';                                        
                                        srchInput.val(newSearchText);
                                        $('#divSearchHelper').toggle(false);
                                        srchInput.focus();
                                  });
function getSearchAxes() {
return ["#Cases","Axes for Searching Cases",
           "AlsoEditedBy","cases edited by the specified user, to be used in combination with EditedBy",
           "Area","cases in the specified area",
           "AssignedTo","cases assigned to the specified user",
           "Attachment","cases with an attachment with the specified name",
           "Category","cases with the specified category",
           "Closed","(date) cases closed on the date specified",
           "ClosedBy","cases last closed by the specified user",
           "CommunityUser","cases that were submitted by the specified community user",
           "Computer","cases containing specific text in the second custom field. Note that this field may have been renamed in your installation",
           "Correspondent","cases with the specified email correspondent",
           "CreatedBy","cases created by the specified user",
           "Department","cases belonging to the specified department",
           "Due","(date) cases due on the date specified",
           "Edited","(date) cases modified on the date specified",
           "EditedBy","cases with a bug event generated by the specified user",
           "ElapsedTime","cases with the specified (range of) elapsed time",
           "EstimateCurrent","cases with the specified (range of) current estimate",
           "EstimateOriginal","cases with the specified (range of) original estimate",
           "From","cases with emails from the specified email address",
           "LastEdited","(date) cases that were modified on the date specified and have not been modified since then",
           "LastEditedBy","cases last edited by the specified user",
           "LastViewed","(date) cases that you last viewed on the date specified",
           "Milestone","cases assigned to the specified milestone",
           "Occurrences","Number of occurrences for a BugzScout case",
           "Opened","(date) cases opened on the date specified",
           "OpenedBy","cases last opened or reopened by the specified user",
           "OrderBy","This takes another axis as its argument and sorts the search results by that axis",
           "Outline","returns cases in the same subcase hierarchy as the specified case",
           "Parent","returns all subcases of the specified case",
           "Root","all cases in the hierarchy underneath the specified case",
           "Priority","cases with the specified priority",
           "Project","cases in the specified project",
           "ProjectGroup", "Cases in the specified project group (Requires the Project Groups Plugin",
           "RelatedTo","cases that are linked to the specified case",
           "Release","same as milestone",
           "ReleaseNotes","search cases with text in release notes, use * to see all cases with release notes",
           "RemainingTime","cases with the specified (range of) original estimate",
           "Resolved","(date) cases resolved on the date specified",
           "ResolvedBy","cases last resolved by the specified user",
           "Show","cases with the specified attribute (Read, Unread, Subscribed or Spam)",
           "StarredBy","starredby:me shows cases you have starred",
           "Status","cases with the specified status",
           "Tag","cases with the specified tag",
           "Title","cases containing the specified words in the title",
           "To","cases with email to the specified email address",
           "Version","cases containing specific text in the first custom field. Note that this field may have been renamed in your installation",
           "ViewedBy","viewedby:me shows cases you have previously viewed",

       "#Wiki Pages","Axes for Wiki Pages",
        "Edited","(date) wiki pages that were modified on the date specified",
        "EditedBy","wiki pages edited by the specified user",
        "LastEdited","(date) wiki pages that were modified on the date specified and have not been modified since then",
        "LastEditedBy","wiki pages last edited by the specified user",
        "LastViewed","(date) wiki pages that you last viewed on the date specified",
        "Show","wiki pages with the specified attribute (Read, Unread or Subscribed)",
        "StarredBy","starredby:me shows wiki pages you have starred",
        "Title","Finds wiki pages containing the specified words in the title",
        "ViewedBy","viewedby:me shows wiki pages you have previously viewed",
        "Wiki","wiki pages in the specified wiki",

       "#Discussion Topics","Axes for Discussion Topics",
           "CreatedBy","topics created by the specified user",
           "DiscussionGroup","topics in the specified discussion group",
           "Edited","(date) topics that were modified on the date specified",
           "EditedBy","topics edited by the specified user",
           "LastEdited","(date) topics modified on the date specified and which have not been modified since then",
           "LastEditedBy","topics last edited by the specified user",
           "LastViewed","(date) topics that you last viewed on the date specified",
           "Opened","(date) topics opened on the date specified",
           "Show","topics with the specified attribute (Read or Unread)",
           "StarredBy","starredby:me shows topics you have starred",
           "Title","topics containing the specified words in the title",
           "Type","type:case for cases, type:wiki for wiki pages, type:discuss for discussion topics",
           "ViewedBy","viewedby:me shows topics you have previously viewed"
       ];
}

css:

#divSearchHelper {
   background-color: white;
   border: 1px solid #000000;
   min-height:30px;
   max-height:200px;
   overflow-y: scroll;
}
#searchHelperButton {
    font-family: Sand, fantasy 
    background-color:#E0E9F1;
    border: 1px;
    cursor:hand;cursor:pointer;
}
.searchHelperItemHeader {
    cursor:hand;cursor:default;
    margin-left: 1em;
    font-weight: bold;
}
.searchHelperItem {
    cursor:hand;cursor:pointer;
    margin-left: 2em;
}

This version is for FogBugz On Demand:

$(function(){
	var initialiseSearchAxes = function(){
		var searchAxes = getSearchAxes();
		var srchInput = $('#searchDropListContainer').closest('li');
		var imgSearchHelperButton = $('<li class="dropdown js-header-dropdown"></li>');
		imgSearchHelperButton.attr('id','searchHelperButton').html("<a class='section-link' href='javascript:void(0);'>?</a>");
		srchInput.after(imgSearchHelperButton);

		var searchHelper = $('<ul class="dropdown-menu js-header-dropdown-menu"></ul>');
		searchHelper.attr('id','searchHelper');
		searchHelper.append('<li><h3>Search Axis</h3><ul></ul></li>');
		var listParent = searchHelper.find('ul');

		for (var axisID=0; axisID<searchAxes.length; axisID+=2) {  
		  var helpItem = $('<li/>'); 
		  var itemText = searchAxes[axisID];
		  var itemDescription = searchAxes[axisID+1]
		  if(itemText.substr(0,1)=='#') {
			 itemText = itemText.substr(1);
			 helpItem.append($('<h3/>').text(itemText));
		  } else {
			helpItem.append($('<a class="js-header-dropdown-link" href="javascript:void(0);"></a>').text(itemText).attr('title',itemDescription));      
		  }
		  listParent.append(helpItem);
		}
		imgSearchHelperButton.append(searchHelper);

		searchHelper.find('a').click(function(){
			var searchInput = srchInput.find('input');
			var currentValue = searchInput.val();
			var textToAttach = $(this).text();
			var newValue = currentValue == '' ? textToAttach : (currentValue + " " + textToAttach);
			searchInput.val(newValue + ":");
			searchInput.focus();
		});

		function getSearchAxes() {
		return ["#Cases","Axes for Searching Cases",
			   "AlsoEditedBy","cases edited by the specified user, to be used in combination with EditedBy",
			   "Area","cases in the specified area",
			   "AssignedTo","cases assigned to the specified user",
			   "Attachment","cases with an attachment with the specified name",
			   "Category","cases with the specified category",
			   "Closed","(date) cases closed on the date specified",
			   "ClosedBy","cases last closed by the specified user",
			   "CommunityUser","cases that were submitted by the specified community user",
			   "Computer","cases containing specific text in the second custom field. Note that this field may have been renamed in your installation",
			   "Correspondent","cases with the specified email correspondent",
			   "CreatedBy","cases created by the specified user",
			   "Department","cases belonging to the specified department",
			   "Due","(date) cases due on the date specified",
			   "Edited","(date) cases modified on the date specified",
			   "EditedBy","cases with a bug event generated by the specified user",
			   "ElapsedTime","cases with the specified (range of) elapsed time",
			   "EstimateCurrent","cases with the specified (range of) current estimate",
			   "EstimateOriginal","cases with the specified (range of) original estimate",
			   "From","cases with emails from the specified email address",
			   "LastEdited","(date) cases that were modified on the date specified and have not been modified since then",
			   "LastEditedBy","cases last edited by the specified user",
			   "LastViewed","(date) cases that you last viewed on the date specified",
			   "Milestone","cases assigned to the specified milestone",
			   "Occurrences","Number of occurrences for a BugzScout case",
			   "Opened","(date) cases opened on the date specified",
			   "OpenedBy","cases last opened or reopened by the specified user",
			   "OrderBy","This takes another axis as its argument and sorts the search results by that axis",
			   "Outline","returns cases in the same subcase hierarchy as the specified case",
			   "Parent","returns all subcases of the specified case",
			   "Root","all cases in the hierarchy underneath the specified case",
			   "Priority","cases with the specified priority",
			   "Project","cases in the specified project",
			   "ProjectGroup", "Cases in the specified project group (Requires the Project Groups Plugin",
			   "RelatedTo","cases that are linked to the specified case",
			   "Release","same as milestone",
			   "ReleaseNotes","search cases with text in release notes, use * to see all cases with release notes",
			   "RemainingTime","cases with the specified (range of) original estimate",
			   "Resolved","(date) cases resolved on the date specified",
			   "ResolvedBy","cases last resolved by the specified user",
			   "Show","cases with the specified attribute (Read, Unread, Subscribed or Spam)",
			   "StarredBy","starredby:me shows cases you have starred",
			   "Status","cases with the specified status",
			   "Tag","cases with the specified tag",
			   "Title","cases containing the specified words in the title",
			   "To","cases with email to the specified email address",
			   "Version","cases containing specific text in the first custom field. Note that this field may have been renamed in your installation",
			   "ViewedBy","viewedby:me shows cases you have previously viewed",
		   "#Wiki Pages","Axes for Wiki Pages",
			"Edited","(date) wiki pages that were modified on the date specified",
			"EditedBy","wiki pages edited by the specified user",
			"LastEdited","(date) wiki pages that were modified on the date specified and have not been modified since then",
			"LastEditedBy","wiki pages last edited by the specified user",
			"LastViewed","(date) wiki pages that you last viewed on the date specified",
			"Show","wiki pages with the specified attribute (Read, Unread or Subscribed)",
			"StarredBy","starredby:me shows wiki pages you have starred",
			"Title","Finds wiki pages containing the specified words in the title",
			"ViewedBy","viewedby:me shows wiki pages you have previously viewed",
			"Wiki","wiki pages in the specified wiki",
		   "#Discussion Topics","Axes for Discussion Topics",
			   "CreatedBy","topics created by the specified user",
			   "DiscussionGroup","topics in the specified discussion group",
			   "Edited","(date) topics that were modified on the date specified",
			   "EditedBy","topics edited by the specified user",
			   "LastEdited","(date) topics modified on the date specified and which have not been modified since then",
			   "LastEditedBy","topics last edited by the specified user",
			   "LastViewed","(date) topics that you last viewed on the date specified",
			   "Opened","(date) topics opened on the date specified",
			   "Show","topics with the specified attribute (Read or Unread)",
			   "StarredBy","starredby:me shows topics you have starred",
			   "Title","topics containing the specified words in the title",
			   "Type","type:case for cases, type:wiki for wiki pages, type:discuss for discussion topics",
			   "ViewedBy","viewedby:me shows topics you have previously viewed"
		   ];
		}
	};

	var searchAxesInitialised = false;
	if (fb.pubsub){
        fb.pubsub.subscribe('/nav/end', function(e){
			if (!searchAxesInitialised){
				initialiseSearchAxes();
				searchAxesInitialised = true;
			}
        });
    }
});

Quick-add Subcases and Parent Cases

Originally posted by Rene Cavet.

Here’s a script that adds links to quickly create new subcases or parent cases from the case view.

name:          +Sub/Parent Case links
description:   Add new subcase/parent case quick links to the case view
author:        Adam Wishneusky, Chad McElligott, Michel de Ruiter
version:       1.3.0.0

js:

if (!window.goBug)
  return;
function getSel() {
  if        (window.getSelection)
    return   window.getSelection().toString();
  else if (document.getSelection)
    return document.getSelection().toString();
  else if (document.selection)
    return document.selection.createRange().text;
  return '';
};
function addButtons() {
  $('.icon-left.subcase,.icon-left.addparent').remove(); // Existing buttons
  if ($("ul.buttons").length == 0)
    return;
  var sLinkStart = 'default.asp?command=new&pg=pgEditBug'                +
    '&ixCategory='         +                    goBug.ixCategory         +
    '&ixProject='          +                    goBug.ixProject          +
    '&ixArea='             +                    goBug.ixArea             +
    '&ixFixFor='           +                    goBug.ixFixFor           +
    '&ixPersonAssignedTo=' +                    goBug.ixPersonAssignedTo +
    '&sCustomerEmail='     + encodeURIComponent(goBug.sCustomerEmail)    +
    '&ixPriority='         +                    goBug.ixPriority         +
    '&sTags='              + encodeURIComponent(goBug.ListTagsAsArray()) +
    '&sEvent='; // To be updated dynamically.
  var sNewButtons = '<li><a class="actionButton2 icon-left subcase" href="'   +
    sLinkStart +
    '&ixBugParent='        +                    goBug.ixBug              +
    '&b=c">Subcase</a><li>';
  if (goBug.ixBugParent == 0)
    sNewButtons +=  '<li><a class="actionButton2 icon-left addparent" href="' +
    sLinkStart +
    '&ixBugChildren='      +                    goBug.ixBug              +
    '&b=c">Parent</a></li>';
  $("ul.buttons").prepend($(sNewButtons));
}
addButtons();
$(window).on('BugViewChange', addButtons);
$(document).on('mouseup', '#bugviewContainer', function() { // Update sEvent:
  $('a.subcase,a.addparent').each(function() {
    this.setAttribute('href', this.getAttribute('href')
      .replace(/&sEvent=[^&]*/, '&sEvent=' + encodeURIComponent(getSel())));
  });
});

css:

/* Green plus sign: */
.icon-left.subcase::before,
.icon-left.addparent::before {
  background-position: 0px -163px;
  height: 16px;
}
#mainArea ul {
  font-size: 12px !important;
}
#bugviewContainer .buttonbar ul.toolbar.buttons {
  white-space: nowrap;
}

Replace Keywords with Links in BugEvents

Originally posted by Roman.

Here’s some sample javascript which can be used to replace text in bug events. I’m currently using this for creating links and replacing long urls with shorter ones within bug events.

In this sample there are two objects which I use as a poor man’s replacement for hashes:

  • aRegExps contains regular expressions to match in the text of the bug event
  • aReplacements contains the replacement texts for the corresponding regexps

What these replacements do:

  • svnlink – This replaces a string in the form of SVN#revision with a link to our svn viewVC server
  • shortenlink – Replaces a very long url (we have a lot of those here) with a shorter one (note that this doesn’t modify the href in the anchor tag, just the text that is displayed)
  • replaceurl – Replaces one url with another

To add more replacements just add a matching pair of aRegExps and aReplacements objects.

A couple of notes about this sample:

  • The regular expressions in the sample are obviously examples which I replaced before posting here and need to be customized.
  • Remember to escape special characters in the regexps
  • It has been mostly tested in Chrome and Firefox. No guarantees on other browsers.

Here’s the sample:

Note that you need to edit the below js code to adit/add your keywords and corresponding replacements.

name:          Replace links
description:   Replaces keywords with links in BugEvents
author:        Roman Hernandez
version:       1.0

js:

var aRegExps = new Object();
var aReplacements = new Object();

aRegExps["svnlink"] = RegExp(/SVN#(d+)/g);
aReplacements["svnlink"] = "<a href="http://your.svn.url/viewvc?view=rev&revision=$1">SVN#$1</a>";

aRegExps["shortenlink"] = RegExp(/>http://some.common.url/path?link=something/g);
aReplacements["shortenlink"] = ">SVN#";

aRegExps["replaceurl"] = RegExp(/http://url.to.replace/g);
aReplacements["replaceurl"] = "http://replacement.url.here";

// Find all the bugevent body elements
$("div.body").each(
  function (){
     var content = $(this).html();
     var replace = false;
     for (var key in aRegExps){
       if (aRegExps[key].test(content)){
        replace = true;
        content = content.replace(aRegExps[key], aReplacements[key]);
       }    
     }
     if (replace)
       $(this).html(content);
  }
);

Custom Nav-bar Dropdowns

Originally posted by Jon Erickson.

Use this customization to create a custom menu that looks and performs exactly like the native menus in FogBugz (Filters, Schedules, Wiki, etc).

CxOhm

name:          Custom Menu
description:   Custom Menu
author:        Jon Erickson
version:       1.0.0.0

js:

/*
====================================================================================================
CUSTOMIZE THESE VARIABLES

    urlToFogBugz
        URL to your fogbugz installation, make sure that there is an ending slash '/'

    customMenuTitle
        Title of the custom menu you wish to be displayed

    customMenuId
        unique id for the menu, needs to be unique from all other non-custom fogbugz menus as well
====================================================================================================
*/

var urlToFogBugz    = 'http://url.to.fogbugz/',
    customMenuTitle = 'Custom Menu Title',
    customMenuId    = 'myCustomMenu';

(function($) {
    if ($('#Menu_LogInOut > span').text() === 'Log Off') { // If user is currently logged on.
        function CreateMenuLink(text, url) {
            return $('<a>').attr({ 'onclick': 'return doPopupClick();', 'href': url }).text(text);
        }

        function CreateMenuBugLink(text, urlParams) {
            return CreateMenuLink(text, urlToFogBugz + 'default.asp?command=new&pg=pgEditBug' + urlParams);
        }

        // Create your custom links that you want to appear in the menu
        // fogbugz will automatically prefill the drop downs with the query string parameters
        var prefilledBugLink1 = CreateMenuBugLink('Add Prefilled Bug 1', '&ixProject=10&ixArea=46&ixCategory=4'),
            prefilledBugLink2 = CreateMenuBugLink('Add Prefilled Bug 2', '&ixProject=10&ixArea=46&ixCategory=4'),
            externalLink1 = CreateMenuLink('Google', 'http://www.google.com/'),
            externalLink2 = CreateMenuLink('Bing', 'http://www.bing.com/');

        // Put links in the order that you want them to appear in the menu
        // Put in horizontal rules in desired positions
        var helpDiv = $('<div>')
            .append(prefilledBugLink1)
            .append(prefilledBugLink2)
            .append('<hr />')           
            .append(externalLink1)
            .append('<hr />')           
            .append(externalLink2);

        // shouldn't need to customize anything below this comment...
        var customMenu = $('<span>')
            .attr('id', customMenuId)
            .css({ 
                'display': 'none', 
                'position': 'absolute', 
                'left': '0px', 
                'top': '0px' 
            })
            .addClass('popupMenu')
            .addClass(customMenuId);

        customMenu
            .append('<table>')
            .children('table')
            .append('<tbody></tbody>')
            .children('tbody')
            .append('<tr></tr>')
            .children('tr')
            .append('<td></td>')
            .children('td')
            .append(helpDiv);

        var helpMenu = $('<a>')
            .attr({ 
                'id': 'Menu_' + customMenuId, 
                'title': customMenuTitle, 
                'href': urlToFogBugz
            })
            .addClass('navlink')
            .addClass('menu')
            .text(customMenuTitle);

        $('#mainnav')
            .append(helpMenu)
            .append(customMenu);

        $(function() {
            theMgr.add(customMenuId);
            $('#Menu_' + customMenuId).click(function(e) {
                e.preventDefault();
                return theMgr.showPopup(customMenuId, this, 0, this.offsetHeight + 2, null, true) || KeyManager.browseMenus('mainnav') || KeyManager.oMenuBrowser.setElCurrent(this) || KeyManager.browsePopup(customMenuId);
            });
        });
    }
})(jQuery);

Chuck Norris Eats Bugs for Breakfast

Originally posted by Gal Segal.

Great new script for you Chuck Norris lovers – the plugin that will freak you out! Do you want your developers to log into FogBugz more often? Well, this will give them a real reason. Introducing the “Chuck Norris Jokes Rotator”!

name:          Chuck Norris Jokes Rotator
description:   Adds some Chuck Norris jokes on top
author:        Gal Segal
version:       1.0.0.0

js:

(function($){
  $.fn.chuckIt = function(options) {

    var plugin = function(container) {
            this.container = container;
            this.container.css({
                color: '#000',
                position:'absolute',
                top:'0',
                left:'10px',
                display:'inline'
            });
            this.options = $.extend({}, this.defaults, options);
        };

        plugin.prototype = {
            updateInterval:undefined,
            container:undefined,
            defaults: {
                interval:10000
            },
            start: function() {
                var self = this;
                $.ajax({
                  url: "http://code.icndb.com/jquery.icndb.min.js",
                  dataType: "script",
                  success: function(d) {
                        self.setInterval();
                        self.updateJoke();
                    }
                }); 
            },
            setInterval: function () {
                this.updateInterval = setInterval($.proxy(this.updateJoke, this), this.options.interval);
            },
            clearInterval: function() {
                clearInterval(this.updateInterval);
            },
            updateJoke: function() {
                var self = this;
                this.clearInterval();
                $.icndb.getRandomJoke({
                    success:function(joke){
                        console.log(self);
                        self.container.fadeOut(500, function(){
                            self.container.html(joke.joke)
                                   .fadeIn(500);
                        });     
                        self.setInterval();
                    }
                });
            }
        };

    $(this).each(function(i, v) {
        var pluginInstance = new plugin($(v));
        pluginInstance.start();
    });
  };
})(jQuery);
$(document).ready(function(){
    $('<div id="joke"></div>').appendTo('#banner').chuckIt();
});

Hide Empty Case Edits

Originally posted by Quentin Schroeder.

In using FogBugz, I noticed that most of the users didn’t like seeing the full history of everywhere a bug had been in its storied life. For particularly old bugs, there might be a full page of “Assigned to Peter”, “Assigned to Paul”, “Assigned to Mary”, “Milestone changed to 1.2.3″ et cetera. Since people managing the bugs usually needed to see these ‘empty edits’ and the people fixing bugs only rarely did, I use BugMonkey to hide the ‘empty edits’. I use a session cookie to remember the setting.

Also, I very rarely need to see the full header of an email. We use email as a method for our support staff to submit information about cases. As such, their email should look more like an edit and less like something else. I’ve reformatted the email so that the header is togglable (default to off) and the emails look more like edits. If the email is an Out of Office Autoreply, hide it.

This will also provide color coding for incoming vs. outgoing emails, so the email chain can be easily scanned for information.

name:          Hide empty edits and tidy up
description:   Hides edits with no text, optionally hides email headers, colors incoming and outgoing emails
author:        alficles, FogBugz 8 compatibility edits by Quentin Schroeder
version:       1.0.0.0

js:

// Cookie functions (mostly) stolen from some blog on the net.
function createCookie(name, value, days)
{
    if (days) {
        var date = new Date();
        date.setTime(date.getTime()+(days*24*60*60*1000));
        var expires = "; expires="+date.toGMTString();
    }
    else var expires = "";
    document.cookie = name+"="+value+expires+"; path=/";
}
function readCookie(name)
{
    var ca = document.cookie.split(';');
    var nameEQ = name + "=";
    for(var i=0; i < ca.length; i++) {
        var c = ca[i];
        while (c.charAt(0)==' ') c = c.substring(1, c.length); //delete spaces
        if (c.indexOf(nameEQ) == 0) return c.substring(nameEQ.length, c.length);
    }
    return null;
}
function eraseCookie(name)
{
    createCookie(name, "", -1);
}
$("div.emailHeader").parent().prepend("<div class="emailHeaderToggle">Toggle Email Header</div>");
$("div.emailHeader").each(function(i) { 
    var whoEle = $(this).find("div.emailHeaderValue:first");
    var actEle = whoEle.parents("div.bugevent").find("span.action");
    var who = whoEle.text();
    var act = actEle.text();
    var nameReg = /"([^"]*)"/;
    if (!(/^Replied/.exec(act))) {
        if (nameReg.test(whoEle.text())) {
            actEle.append(" by "+nameReg.exec(who)[1]);
        }
        else {
            actEle.append(" by "+whoEle.html());
        }
    }
});
$("div.emailHeaderToggle").click(function() {
    var emailHeader = $(this).parent().find("div.emailHeader");
    var old_val = emailHeader.css("display");
    if (old_val == "block") { emailHeader.css("display","none"); }
    else { emailHeader.css("display","block"); }
});
var emptyCount = 0;
$(".bugevent").each(function() {
    var bodyEle = $(this).find(".body");
    var bodyTxt = bodyEle.text();
    if (/^[ tn]*$/.exec(bodyTxt)) {
        $(this).addClass("emptyBody");
        emptyCount += 1;
    }
});
$(".email .emailHeader .emailHeaderValue").each(function() {
    if (/^Out of Office AutoReply/i.exec($(this).text())) {
        $(this).parents(".bugevent").addClass("emptyBody");
        emptyCount += 1;
    }
});
if (emptyCount > 0) {
    $("#BugEvents").prepend("<div id="EmptyBodyToggle">Show/Hide Empty Edits ("+emptyCount+")</div>");
}
$("#EmptyBodyToggle").click(function() {
    var old_val = $(".emptyBody").css("display");
    if (old_val == "block")
    {
        $(".emptyBody").css("display","none");
        createCookie("showEmpty","false");
    } else {
        $(".emptyBody").css("display","block");
        createCookie("showEmpty","true");
    }
});
if (readCookie("showEmpty") == "true") {
    $(".emptyBody").css("display","block");
}
$(".email").each(function() {
    var actions = this.getElementsByClassName('action');
    if (actions.length > 0)
        if (/Replied/.exec(actions[0].innerText)) {
            $(this).children('.body').addClass("outgoing");
        }
        else { // outgoing message
            $(this).children('.body').addClass("incoming");
        }
});

css:

div.emailBody {
    padding: 0px;
    background-color: inherit;
}
div.email {
    border: none;
}
div.incoming {
    background-color: #E0F5E0 !important; // green
}
div.outgoing {
    background-color: #EBF5FF !important; // blue
}
div.emailHeader {
    display: none;
    border: 1px solid #ADABA8;
    margin-bottom: 10px;
    margin-top: 5px;
}
div.editable div.emailHeader {
    display: block;
}
div.emailHeaderToggle {
    font-size: 70%;
    font-style: italic;
    color: #888;
    padding: 2px;
    cursor: pointer;
}
div.emailHeaderToggle:hover {
    text-decoration: underline;
    color: #000;
}
#EmptyBodyToggle {
    font-style: italic;
    color: #888;
    padding-bottom: 7px;
    cursor: pointer;
}
#EmptyBodyToggle:hover {
    text-decoration: underline;
    color: #000;
}
.emptyBody {
    display: none;
    padding-left: 5px;
    margin-left: 5px;
    border-left: 3px solid #d6d6d6;
}

Custom Categories Per-Project

Originally posted by Dane Bertram.

Here’s a script that adds support for per-project categories. The details are in the linked question, but basically, you just prefix any per-project categories you want to have with their associated project’s name. For example, to have a “Hot Lead” category that only applies to the “Sales” project, you’d create a category called “Sales – Hot Lead”. Any categories that don’t have a “project prefix” are considered global categories and will be visible for all projects.

name:          Filter categories by their project prefix
description:   Allows you to create per-project categories by prefixing the category name with the project. Eg. "Sales - Hot Lead" will only apply to the "Sales" project. Categories without a project prefix (basically, those that don't contain " - " will be displayed for all projects.
author:        Dane Bertram & Daniel LeCheminant
version:       1.0.0.0

js:

var toggleProjectCategories = function(sProject){
    if(!$('#ixCategory').length) return;

    var existingIxCat = parseInt($('#ixCategory :selected').val());
    var cats = $('#ixCategory').empty();

    $(DB.Category).each(function(ix, cat){
        if(cat.fDeleted) return; // skip deleted categories
        var sCategoryPrefix = /^(.+) - (.+)/.exec(cat.sCategory); // capture the project prefix and non-prefixed category name
        if(!sCategoryPrefix || sCategoryPrefix[1] === sProject){
            // either a global category (no project prefix), or a per-project
            // category that matches the currently selected project
            var newOpt = $('<option>')
                .val(cat.ixCategory)
                .text(sCategoryPrefix ? sCategoryPrefix[2] : cat.sCategory)
                .appendTo(cats);

            // if we're transitioning into edit mode, keep the previously-selected category selected
            if(cat.ixCategory === existingIxCat) newOpt.attr('selected', 'selected');
        }
    })

    DropListControl.refresh(cats[0]);
}

var init = function(){
    $('#ixProject').change(function() {
        toggleProjectCategories($(this).find(":selected").text());
    });
    toggleProjectCategories($('#ixProject :selected').text());
}

$(window).bind("BugViewChange", init);
init();

“Visual” Status and Priority Indicators

Originally posted by tghw.

The following BugMonkey scripts provide “visual” versions of the Status and Priority columns in the list view.

visual_status_priority
Visual Status

name:          Visual Status
description:   Replaces status words with colored symbols.
author:        Tyler Hicks-Wright & Dane Bertram
version:       1.1.0.0

js:

function getStatusCol() {
  var hs = $('th.c-h a:contains("' + FB_STATUS + '")');
  if(hs.length == 0) return null;
  return hs.eq(0).parent().parent().parent().attr('class').match(/col_d+/)[0];
}
var col;
if((col = getStatusCol())) {
  $('.' + col).each(function(i, e) {
    var span = $(e).find('span');
    if(span.length > 0) {
      var status = span.text();
      span.attr({title: status}).css({fontWeight: 'bold', textAlign: 'center'});
      if(status.match(new RegExp('(Active|' + FB_ACTIVE + ').*'))) span.text('A').css({color: 'green'});
      else if(status.match(new RegExp('(Resolved|' + FB_RESOLVE + ').*'))) span.text('R').css({color: 'goldenrod'});
      else if(status.match(new RegExp('(Closed|' + FB_CLOSED + ').*'))) span.text('C').css({color: 'darkred'});
      else if(status.match(/Verified.*/)) span.text('V').css({color: 'blue'});
      else if(status.match(/Approved.*/)) span.html('').css({color: 'green'});
      else if(status.match(/Rejected.*/)) span.html('').css({color: 'red'});
    }
    else {
      var a = $(e).find('a:first');
      a.text('?').css({textAlign: 'center'});
    }
  });
}

Visual Priority

name:          Visual Priority
description:   Shows priority in a smaller, more colorful way.
author:        Tyler Hicks-Wright & Dane Bertram
version:       1.1.0.1

js:

function getPriorityCol() {
  var hs = $('th.c-h a:contains("' + FB_PRIORITY + '")');
  if(hs.length == 0) return null;
  return hs.eq(0).parent().parent().parent().attr('class').match(/col_d+/)[0];
}
var col;
if((col = getPriorityCol())) {
  $('.' + col).each(function(i, e) {
    var span = $(e).find('span');
    if(span.length > 0) {
      var priority = span.text();
      var td = span.parent().parent().parent();
      td.css({textAlign: 'center'});
      span.attr({title: priority}).css({fontWeight: 'bold', textAlign: 'center'});
      if(priority.match(/1.*/)) { span.text('1'); td.css({backgroundColor: '#f00'}); }
      else if(priority.match(/2.*/)) { span.text('2'); td.css({backgroundColor: '#f60'}); }
      else if(priority.match(/3.*/)) { span.text('3'); td.css({backgroundColor: '#fa0'}); }
      else if(priority.match(/4.*/)) { span.html('4'); td.css({backgroundColor: '#ff0'}); }
      else if(priority.match(/5.*/)) { span.html('5'); td.css({backgroundColor: '#af0'}); }
      else if(priority.match(/6.*/)) { span.html('6'); td.css({backgroundColor: '#0f0'}); }
      else if(priority.match(/7.*/)) { span.html('7'); td.css({backgroundColor: '#00f'}); }
    }
    else {
      var a = $(e).find('a:first');
      a.html('<img src="images/exclamation.png" />').css({textAlign: 'center'});
    }
  });
}

Prevent Blank Case Titles

Originally posted by adambox.

This script simply disables the “OK” button when editing a case if the value of the Title field or event body is left blank. For non-logged-in users, it also requires an email address.

Note that this is only for new cases. If you want to require fields during edits, you need to hook into the BugViewChange event.

name:          Require title, event and email
description:   Cannot submit a case if the title or event is empty, and email is required for non-logged-in users
author:        Quentin Schroeder and Adam Wishneusky
version:       2.0.0.0

js:

$(document).ready(function(){
  // don't do anything if we're not on the case edit page
  if (!$('#bugviewContainer').length) return;
  // $(this).attr("title", "Facilita Support");
  var okButton = $('#Button_OKEdit')[0];
  if (!okButton) return;
  okButton.disabled = true;
  okButton.title = "Case title cannot be blank";

  var verifyFields = function(event)
  {
    var okButton = $('#Button_OKEdit')[0];
    if (($('#idBugTitleEdit')[0].value.length > 0) &&
        ($('#sEventEdit')[0].value.length > 0) &&
        (IsLoggedIn() || ($('#idDropList_sCustomerEmail_oText')[0].value.length > 0) ))
    {   
        okButton.disabled = false;
        okButton.title = "";
    } 
    else 
    {
        okButton.disabled = true;
        okButton.title = "Case title cannot be blank";
    }
    if (this.originalOnKeyUp)
      this.originalOnKeyUp(event);
  }

  // remove this if you don't want to require titles
  var titleText = $('#idBugTitleEdit')[0];
  if (titleText.onkeyup) 
    titleText.originalOnKeyUp = titleText.onkeyup;
  titleText.onkeyup = verifyFields;

  // remove this if you don't want to require event text
  var eventText = $('#sEventEdit')[0];
  if (eventText.onkeyup) 
    eventText.originalOnKeyUp = eventText.onkeyup;
  eventText.onkeyup = verifyFields;

  // remove this if you don't want to require anonymous visitors' email addresses
  if (!IsLoggedIn())
  {
    var emailText = $('#idDropList_sCustomerEmail_oText')[0];
    if (emailText.onkeyup) 
      emailText.originalOnKeyUp = emailText.onkeyup;
    emailText.onkeyup = verifyFields;
  }
});

Toggle Hierarchies

Originally posted by Dane Bertram.

This script adds a “Toggle Hierarchies” button to the upper-right corner of the list cases page. Clicking it will collapse/expand all the case hierarchies on the page.

if( window.location.href.indexOf("pg=pgList") > 0 || window.location.href.indexOf("pgx=LF") > 0)
{
    $('#listNav')
    .prepend('<a href="#">Toggle Hierarchies</a>&nbsp;|&nbsp;')
    .click(function() {
        var rgTRs = $("#bugGrid tr");
        for (ix = 0; ix < rgTRs.length; ix++)
        {
            var oRow = rgTRs[ix];

           if ($("a.arrow", oRow).length > 0)
           {
               GridControl.toggleNode(oRow.uid);
           }
        }
        return false;
    });
}

Hide “Send & Close” and “Resolve & Close”

Originally posted by Glenn Arndt.

Add the following CSS to hide the “Send & Close” and “Resolve & Close” buttons when editing a case.

.dlgButtonWide#Button_SendAndCloseEmail { font-weight: normal; display: none; }
.dlgButtonWide#Button_ResolveAndClose { font-weight: normal; display: none; }

Notification for Updates to Case You’re Working On

Originally posted by tghw.

When you view the edit page for a case, if another user edits it and then you submit your changes, FogBugz warns you that the case has been changed and does’t commit your edit. This is good to prevent some mistakes, but it would be much better if I was notified real-time when someone had made a change, while I’m viewing the case. Do this with the following script!

name:          Case Updated Notification
description:   Shows a notification when the case you are looking has been updated.
author:        Tyler Hicks-Wright et al.
version:       1.0.1.0

js:

/*
If you update this, please also update http://help.fogcreek.com/8534/bugmonkey-script-archive#Notification_for_Updates_to_Case_You8217re_Working_On

Change Log:
   v1.0.1.0 - Dane: Don't DOS FogBugz (i.e., wait for existing request to finish/fail before starting another)
   v1.0.0.9 - Dane: If you turn auto-update off, actually stop polling instead of just ignoring responses
   v1.0.0.8 - Dane: Better ajax error handling (works around FogBugz' customized jQuery lib)
   v1.0.0.7 - Michel: Play nice with sibling links.
   v1.0.0.6 - Quentin & Dane: Handle ajax errors gracefully to avoid loading the full error page
   v1.0.0.5 - Michel: Version 8.7 broke Reload link, added css.
   v1.0.0.4 - David:  Parse latest event out of form value, don't have to check the API at time 0.
   v1.0.0.3 - David:  Add control over the auto-update functionality.
   v1.0.0.2 - Rock: The Info object now waits after showing, so we need to check the latest bug event earlier
   v1.0.0.2 - Rock: Updated to work with jQuery 1.6 (strict JSON parsing)
   v1.0.0.1 - Dane: Don't try to poll on the new bug creation page.
   v1.0.0.0 - Tyler: First implementation
*/

// based off of http://code.google.com/p/microajax/
function microAjax(url, successCallback, errorCallback) {
    this.bindFunction = function (caller, object) {
        return function() {
            return caller.apply(object, [object]);
        };
    };

    this.stateChange = function (object) {
        if (this.request.readyState === 4) {
            var status = this.request.status;
            if (status >= 200 && status < 300 || status === 304) {
                this.successCallback(this.request.responseText);
            } else {
                this.errorCallback(this.request.responseText);
            }
        }
    };

    this.getRequest = function () {
        if (window.ActiveXObject)
            return new ActiveXObject('Microsoft.XMLHTTP');
        else if (window.XMLHttpRequest)
            return new XMLHttpRequest();
        return false;
    };

    var noop = function () { };

    this.postBody = (arguments[3] || "");
    this.successCallback = (successCallback || noop);
    this.errorCallback = (errorCallback || noop);
    this.url = url;
    this.request = this.getRequest();

    if (this.request) {
        var req = this.request;
        req.onreadystatechange = this.bindFunction(this.stateChange, this);

        if (this.postBody !== "") {
            req.open("POST", url, true);
            req.setRequestHeader('X-Requested-With', 'XMLHttpRequest');
            req.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');
            req.setRequestHeader('Connection', 'close');
        } else {
            req.open("GET", url, true);
        }
        req.send(this.postBody);
        return req;
    }
    return null;
}

var pollingFrequency = 10000;
var updating = true;
var updateInterval = null;
var ixBugEventLatest = null;
var ourUpdate = false;
var pendingRequest = null;

function checkForUpdates(ixBug) {
    if (!updating || pendingRequest !== null) return;

    var url = 'bugData.asp?'
        + 'sRequest=bugs'
        + '&sBugs=' + ixBug
        + '&sOutputType=json'
        + '&sActionToken=' + g_ActionTokens.getToken('loadBug')
        + '&_=' + $.now(); // cache buster

    pendingRequest = microAjax(url, function (data) {
        if (data === "") return;
        data = eval('(' + data + ')');
        var bug = data[ixBug];
        if(bug.ixBugEventLatest != ixBugEventLatest) {
            var link = $('div.ixBug div a').attr('href');
            if($('.ghostFontBig:visible').length == 0) {
                Info.show('This case has updates.');
                $('#loadingBar').html('This case has updates. <a href="'+link+'">Reload</a>');
                ourUpdate = true;
            }
        }

        if ($('#bugevent_'+bug.ixBugEventLatest).length > 0) {
            ixBugEventLatest = bug.ixBugEventLatest;
            if (ourUpdate) {
                Info.hide();
                ourUpdate = false;
            }
        }
        pendingRequest = null;
    }, function () {
        Info.show('Lost connection to FogBugz... (will retry automatically)');
        ourUpdate = true;
        pendingRequest = null;
    });
}

if ($('div.ixBug').length == 1) {
    var poller = function (ixBug) {
        return function () {
            checkForUpdates(ixBug);
        };
    };
    var ixBug = $('div.ixBug div a').text();
    ixBugEventLatest = $("input[name='ixBugEventLatest']").val();
    if (ixBug.length > 0) {
        updateInterval = setInterval(poller(ixBug), pollingFrequency);
        $('div.subtitle').append('<span class="autoUpdate"> | Auto Update: <a href="#" class="update" title="Enable/disable auto updating for this page">On</a></span>');
        $('div.subtitle a.update').click(function(event) {
            event.preventDefault();
            updating = !updating;
            $(this).text(updating ? 'On' : 'Off');
            if (!updating) {
                clearInterval(updateInterval);
                if (ourUpdate) {
                    Info.hide();
                }
            } else {
                updateInterval = setInterval(poller(ixBug), pollingFrequency);
            }
        });
    }
}

css:

#bugviewContainer .top .subtitle .autoUpdate { color: #68615E; }

Floating Action Bar

Originally posted by Dane Bertram.

Here’s a BugMonkey customization that makes the case action bar stick to the top of the screen when you scroll down the page (in view mode), and does the same for the editor (in edit mode):

name:          Floating Bug Controls++
description:   Makes the bug controls stick to the top of the page when scrolling (including the editor)
author:        Kevin Gessner & Dane Bertram
version:       1.2.0.0

js:

$(function() {
  if (!$('#bugviewContainer').length) return;

  $('#bugviewActionButtonsTop').addClass('bugviewWidth');

  var inEditMode = function() { return $('#bugviewContainerEdit .editor').length > 0; };
  var getDelta = function(sSelector) { return $(window).scrollTop() - $(sSelector).offset().top; };
  var floatBugControls = function() {

    var floatActionBar = !inEditMode() && getDelta('#bugviewContainer') > 0;
    $('#bugviewActionButtonsTop').toggleClass('floating', floatActionBar);
    $('#bugviewContainerTop').css('margin-top', floatActionBar ? $('#bugviewActionButtonsTop').outerHeight() : 0);

    var floatEditor = inEditMode() && getDelta('#bugviewContainerSide') > 0;
    var firstBugEvent = $('#BugEvents').find('.pseudobugevent, .bugevent').first();
    $('#bugviewContainerEdit').toggleClass('floating', floatEditor);

    if (floatEditor) {
      $('#bugviewContainerEdit').width($('#BugEvents .bugevent').outerWidth());
      firstBugEvent.css('margin-top', $('#bugviewContainerEdit').outerHeight());
    } else {
      firstBugEvent.css('margin-top', 0);
    }
  };

  $(window).scroll(floatBugControls);
  $(window).bind('BugViewChange', floatBugControls);
  floatBugControls();
});

css:

#bugviewActionButtonsTop, #bugviewContainerEdit {
  z-index: 10;
}
#bugviewActionButtonsTop.floating, #bugviewContainerEdit.floating {
  position: fixed;
  top: 0;
}
#bugviewActionButtonsTop.floating {
  border-bottom: solid 1px #C7C7C7;
  box-shadow: #c7c7c7 0 1px 5px;
}
/* play nicely with the Floating Top Nav customization: http://fogbugz.stackexchange.com/questions/9921/9923 */
.floatingTopNav #bugviewActionButtonsTop.floating, .floatingTopNav #bugviewContainerEdit.floating {
  top: 63px;
}

Easy Viewing of Email HTML Source

Originally posted by andrewmolyneux.

This customization adds a “[Show HTML Source]” link to the top of the email body in any email bug events. Clicking on that link causes the original source email to be fetched and parsed. If it is a multipart/alternative message with a text/html part, the email body in the bug event is replaced with the HTML source code.

Caveats

  • This code supports decoding base64-encoded messages, but other transfer encodings (e.g. quoted-printable) are displayed as-is. My requirement was to get the message to the point where it was feasible for a human being to pick out the content.
  • I didn’t read any specs before writing the email parsing code, so there are probably lots of corner cases that it fails to handle.
  • I haven’t attempted to optimize the email parsing code at all, so it’s horribly inefficient in terms of space and time. Clicking “Show HTML Source” for large email messages may bring your browser to its knees.
  • No attempt is made to convert between character encodings. If the HTML source is encoded as anything other than UTF-8, it’ll be a bit of a mess.

Credits

The code to decode base64 came from http://www.webtoolkit.info/javascript-base64.html.

name:          HTML email source display
description:   Allows viewing HTML source of multipart/alternative email messages with an HTML part
author:        Andrew Molyneux, Adam Wishneusky
version:       1.0.0.0

js:

$(function() {
  // Base64 from http://www.webtoolkit.info/javascript-base64.html
  // Tweaked formatting and removed support for encoding
  var Base64 = {
    _keyStr : "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=",
    decode : function (input) {
      var output = "";
      var chr1, chr2, chr3;
      var enc1, enc2, enc3, enc4;
      var i = 0;
      input = input.replace(/[^A-Za-z0-9+/=]/g, "");
      while (i < input.length) {
        enc1 = this._keyStr.indexOf(input.charAt(i++));
        enc2 = this._keyStr.indexOf(input.charAt(i++));
        enc3 = this._keyStr.indexOf(input.charAt(i++));
        enc4 = this._keyStr.indexOf(input.charAt(i++));
        chr1 = (enc1 << 2) | (enc2 >> 4);
        chr2 = ((enc2 & 15) << 4) | (enc3 >> 2);
        chr3 = ((enc3 & 3) << 6) | enc4;
        output = output + String.fromCharCode(chr1);
        if (enc3 != 64) {
          output = output + String.fromCharCode(chr2);
        }
        if (enc4 != 64) {
          output = output + String.fromCharCode(chr3);
        }
      }
      output = Base64._utf8_decode(output);
      return output;
    },
    _utf8_decode : function (utftext) {
      var string = "";
      var i = 0;
      var c = c1 = c2 = 0;
      while ( i < utftext.length ) {
        c = utftext.charCodeAt(i);
        if (c < 128) {
          string += String.fromCharCode(c);
          i++;
        } else if((c > 191) && (c < 224)) {
          c2 = utftext.charCodeAt(i+1);
          string += String.fromCharCode(((c & 31) << 6) | (c2 & 63));
          i += 2;
        } else {
          c2 = utftext.charCodeAt(i+1);
          c3 = utftext.charCodeAt(i+2);
          string += String.fromCharCode(((c & 15) << 12) | ((c2 & 63) << 6) | (c3 & 63));
          i += 3;
        }
      }
      return string;
    }
  };
  function htmlEncode(value) {
    return $('<div/>').text(value).html();
  }
  function parseHeaders(headers) {
    var result = {};
    var lines = headers.split("rn");
    var lastfieldName = '';
    $.each(lines, function(i, line) {
      if (/^s+/.test(line)) {
        result[lastFieldName] += line;
      } else {
        var parts = line.split(':');
        if (parts.length >= 2) {
          var fieldName = $.trim(parts[0]);
          var fieldValue = $.trim(parts.slice(1).join(':'));
          result[fieldName] = fieldValue;
          lastFieldName = fieldName;
        }
      }
    });
    return result;
  }
  function getHeadersAndBody(msg) {
    var parts = msg.split("rnrn");
    if (parts.length < 2) {
      return false;
    }
    var headersText = parts[0];
    var bodyText = parts.slice(1).join("rnrn");
    return {headers: parseHeaders(headersText), body: bodyText};
  }
  // Given the value of Content-Type, check if it's multipart/alternative.
  // If it is, return the boundary string. Otherwise, return false.
  function getBoundary(contentType) {
    if (!(/^multipart/alternative/.test(contentType))) {
      return false;
    }
    var parts = contentType.split(';');
    var reBoundary = /^s*boundary="?([^"]+)"?s*$/;
    var result = false;
    $.each(parts, function(i, part) {
      var matches = part.match(reBoundary);
      if (matches !== null) {
        result = matches[1];
        return false;
      }
    });
    return result;
  }
  // Given the body of a multipart/alternative email message, find
  // the part with the given contentType.
  function findPart(body, boundary, contentType) {
    var reContentType = new RegExp('^' + contentType.replace('/', '/'));
    var parts = body.split('--' + boundary);
    var result = false;
    $.each(parts, function(i, part) {
      var message = getHeadersAndBody(part);
      if (message !== false) {
        if (reContentType.test(message.headers['Content-Type'])) {
          result = message;
          return false;
        }
      }
    });
    return result;
  }
  var reBugEventId = /^bugevent_([0-9]+)$/;
  $('.bugevent.email').each(function(i,bugEvent) {
    var bugEventId = bugEvent.id.match(reBugEventId)[1];
    $(bugEvent).find('.emailBody').each(function(i2,emailBody) {
      var linkDiv = $('<div>');
      var link = $('<a>', {href: '', text: '[Show HTML Source]'});
      link.click(function(event) {
        $.ajax({
          type:     'get',
          url:      'default.asp?pg=pgDownload&pgType=pgSource&ixBugEvent=' + bugEventId,
          dataType: 'text',
          success:  function(data) {
            var message = getHeadersAndBody(data);
            var contentType = message.headers['Content-Type'];
            var boundary = getBoundary(contentType);
            if (boundary === false) {
              link.replaceWith('<p>No HTML found.</p>');
              return;
            }
            var htmlPart = findPart(message.body, boundary, 'text/html');
            if (htmlPart === false) {
              link.replaceWith('<p>No HTML found.</p>');
              return;
            }
            var htmlBody = htmlPart.body;
            if (htmlPart.headers['Content-Transfer-Encoding'] == 'base64') {
              htmlBody = Base64.decode($.trim(htmlBody));
            }
            $(emailBody).empty();
            $(emailBody).append("<p>" + htmlEncode(htmlBody).replace(/rn/g,'<br>rn') + "</p>");
          }
        });
        return false;
      });
      linkDiv.append(link);
      $(emailBody).prepend(linkDiv);
    });
  });
});

Auto-create Non-HTTP Links in Cases

Originally posted by Rob Sobers.

Here’s a script that auto-links protocols other than http and https

name:          Auto-link more protocols in cases
description:   Makes links out of ftp:// file:// mailto:// etc in cases
author:        Rob Sobers
version:       1.0.0.0

js:

jQuery.fn.addlink = function ()
{  
    var regex = /((ftp|telnet|gopher|file|news|mailto)://w*(.[a-zA-Z0-9/$-_@!*""'(),=;#?:+%~]*)*[a-zA-Z0-9/])/igm
    return this.each(function ()
    {
      if (this.className.indexOf("editable") < 0 && this.innerHTML.indexOf("emailActions") < 0) {
        this.innerHTML = this.innerHTML.replace(regex, "<a href="$1">$1</a>");
      }
    });
};
$(".bugevent .body").addlink();
$(".bugevent .emailBody").addlink();

Hide Case Events You’ve Already Seen

Originally posted by Daniel LeCheminant.

The following BugMonkey script shows one way to have more control over which/how case events are displayed:

The following script shows one way to have more control over which/how case events are displayed:

// Provide a menu on case view that allows you to do one of the following:
//   View all events
//   Show all events as one-line summaries
//   Show seen events as one-line summaries
//   Hide seen events
$(document).ready(function() {
    // Don't do anything unless we're looking at a single bug
    if (!window.goBug || 
        !$("#bugviewContainer").length || 
        $("#miniBugList").length) 
        return;

    // The text displayed in the display menu
    var modes = {
        showAll: {
            text: "Show All",
            label: "Showing all events",
            show: ".bugevent"
        },
        summarizeAll: {
            text: "Summarize All",
            label: "Summarizing all events",
            show: ".small-summary"
        },
        summarizeSeen: {
            text: "Summarize Seen",
            label: "Summarizing #seen events",
            show: ".bugevent:not(.seen),.small-summary.seen"
        },
        hideSeen: {
            text: "Hide Seen",
            label: "Hiding #seen events",
            show: ".bugevent:not(.seen)"
        }
    };

    // The name of the cookie used to save settings
    var sCookie = "sSeenEventMode";

    // Read the user's current settings.  Default is to show all
    var sSeenMode = getCookie(sCookie) || "showAll";

    // Determine the last event the user saw
    var ixLastView = goBug.ixBugEventLastView;

    // A table describing the translation from a standard bug event 
    // to its summary
    var summTrans = [
        { sel: ".action", color: "#000", css: { "font-weight": "bold"} },
        { sel: ".date a", color: "#68615e" },
        { fnHtml: function(ev) {
            return (ev.find(".emailBody").html() || ev.find(".body").html() || "").
               replace(/<[^>]*>/g, " ");
        }, color: "#888"
        },
        { sel: ".changes", color: "#aaa" }
    ];

    var jAllEvents = $("#BugEvents .bugevent");

    // Process bug events we've already seen
    var numSeen =
    jAllEvents
    // Filter out events we haven't seen
    .filter(function() { 
       return /d+/.exec($(this).attr("id")) <= ixLastView; 
    })
    .addClass("seen")
    .length;

    // Process all visible bug events
    jAllEvents
    .each(function() {
        var ev = $(this);

        // Create the event summary 
        var divSummary = $("<div>")
        .addClass("small-summary")
        .css({
            "white-space": "nowrap",
            overflow: "hidden",
            "text-overflow": "ellipsis",
            "font-size": "10px",
            "margin-bottom": "4px",
            "cursor": "pointer"
        })
        .insertBefore(this)
        .click(function() {
            // Clicking anywhere on the summary hides it 
            // and displays the full bug event
            $(this).hide().next(".bugevent").show();
        });

        if (ev.hasClass("seen")) divSummary.addClass("seen");

        // Convert the bug event into a summary
        $.each(summTrans, function(ix, tbl) {
            var entry = $("<span>")
            .css(tbl.css)
            .css({
                color: tbl.color,
                "margin-right": "4px"
            })
            .appendTo(divSummary);

            if (tbl.fnHtml)
                entry.html(tbl.fnHtml(ev));
            else
                entry.text(ev.find(tbl.sel).text())
        });
    });

    var divMenu = $("<div>")
    .css({
        "margin-bottom": "1em",
        border: "1px dotted #888",
        padding: "4px",
        "background-color": "#f4f4f4"
    })
    .insertBefore("#BugEvents");

    var spanLabel = $("<span>")
    .css({
        "font-weight": "bold",
        "float": "right"
    })
    .appendTo(divMenu);

    // Set the display mode
    var setMode = function(ev, sMode) {
        // If this is being used as an event handler, 
        // get the mode from the link that was clicked
        sMode = sMode || $(this).attr("mode");

        // Don't underline the link that represents the current display mode
        divMenu.find("a")
        .css("text-decoration", function() {
            return $(this).attr("mode") == sMode ? "none" : "underline";
        });

        // Only show the events/summaries allowed by the user's selection
        $("#BugEvents").find(".bugevent,.small-summary")
        .hide()
        .filter(modes[sMode].show)
        .show();

        // Update the label
        spanLabel.html(modes[sMode].label.replace(/#seen/g, numSeen));

        // Remember the user's selection
        setCookie(sCookie, sMode);
    };

    // Add the display modes to the menu
    for (var mode in modes) {
        $("<a>")
        .text(modes[mode].text)
        .css("margin", ".5em")
        .attr({ href: "javascript:void(0)", mode: mode })
        .appendTo(divMenu)
        .click(setMode);
    }

    // Apply the user's saved mode
    setMode(null, sSeenMode);
});

If you’d like to use the code as is, you can use the Closure compiled version:

$(document).ready(function(){if(!(!window.goBug||!$("#bugviewContainer").length||$("#miniBugList").length)){var e={showAll:{text:"Show All",label:"Showing all events",show:".bugevent"},summarizeAll:{text:"Summarize All",label:"Summarizing all events",show:".small-summary"},summarizeSeen:{text:"Summarize Seen",label:"Summarizing #seen events",show:".bugevent:not(.seen),.small-summary.seen"},hideSeen:{text:"Hide Seen",label:"Hiding #seen events",show:".bugevent:not(.seen)"}},i=getCookie("sSeenEventMode")||
"showAll",j=goBug.ixBugEventLastView,k=[{sel:".action",color:"#000",css:{"font-weight":"bold"}},{sel:".date a",color:"#68615e"},{fnHtml:function(b){return(b.find(".emailBody").html()||b.find(".body").html()||"").replace(/<[^>]*>/g," ")},color:"#888"},{sel:".changes",color:"#aaa"}],c=$("#BugEvents .bugevent"),l=c.filter(function(){return/d+/.exec($(this).attr("id"))<=j}).addClass("seen").length;c.each(function(){var b=$(this),a=$("<div>").addClass("small-summary").css({"white-space":"nowrap",overflow:"hidden",
"text-overflow":"ellipsis","font-size":"10px","margin-bottom":"4px",cursor:"pointer"}).insertBefore(this).click(function(){$(this).hide().next(".bugevent").show()});b.hasClass("seen")&&a.addClass("seen");$.each(k,function(n,d){var g=$("<span>").css(d.css).css({color:d.color,"margin-right":"4px"}).appendTo(a);d.fnHtml?g.html(d.fnHtml(b)):g.text(b.find(d.sel).text())})});var f=$("<div>").css({"margin-bottom":"1em",border:"1px dotted #888",padding:"4px","background-color":"#f4f4f4"}).insertBefore("#BugEvents"),
m=$("<span>").css({"font-weight":"bold","float":"right"}).appendTo(f);c=function(b,a){a=a||$(this).attr("mode");f.find("a").css("text-decoration",function(){return $(this).attr("mode")==a?"none":"underline"});$("#BugEvents").find(".bugevent,.small-summary").hide().filter(e[a].show).show();m.html(e[a].label.replace(/#seen/g,l));setCookie("sSeenEventMode",a)};for(var h in e)$("<a>").text(e[h].text).css("margin",".5em").attr({href:"javascript:void(0)",mode:h}).appendTo(f).click(c);c(null,i)}});

Highlight (Over)due Cases in the List View

Originally posted by Daniel LeCheminant.

Here’s a BugMonkey script that one of our engineers, Daniel came up with:

When you install it

  • You will need to activate it in the More menu.
    • If you’d like to have it enabled by default, you can simply change the value of fDefaultEnabled from false to true.
  • You will need to add the 3 CSS classes at the top of the script to your site CSS in BugMonkey.
    • If you don’t want it to highlight cases due tomorrow or today, you can leave out those classes.

What it does

  • It adds an item to the More menu on the case list page.
  • It patches a built-in FogBugz function so that selecting and de-selecting items in the list works properly (instead of losing the highlight when you de-select).
  • It’s pretty verbose, so if you want, you can strip the comments or shrink it down significantly with closure compiler (use the “simple” optimizations to go from 3k to 1k).

Daniel tested it lightly in new versions of Firefox, Chrome, and IE. Let us know if it has any issues.

name:          Highlight Overdue Cases
description:   Highlights overdue cases in the case list
author:        Daniel LeCheminant
version:       1.0.0.1

js:

    // Settings
    // Highlight the whole row, or just the Due column
    var fWholeRow = true;
    // Text that appears in the "More" menu
    var sMenuText = "Toggle Due Date Highlighting";
    // Whether the highlighting should be on or off by default
    var fDefaultEnabled = false;
    // If we're not looking at a bug list, we don't do anything
    if (!$("#bugListContainer").length) return;
    var dueClasses = {
        late: "due-late",
        today: "due-today",
        tomorrow: "due-tomorrow"
    };
    var reToday = new RegExp(FB_TODAY, "i");
    var reTomorrow = new RegExp(FB_TOMORROW, "i");
    // Read out the cookie for the initial state, otherwise use the default
    var sCookie = "fHighlightDue";
    var sEnabled = getCookie(sCookie);
    // Convert the string to a boolean
    var fEnabled = sEnabled ? sEnabled === "true" : fDefaultEnabled;
    var parseDate = (GetLocaleDate() == "dd/mm/yyyy") ? 
    function(s) {
       return Date.parse(s.substr(3,2) + "/" + s.substr(0,2) + "/" + s.substr(6));
    } :
    function(s) {
       return Date.parse(s);
    };
    var highlightDue = function(fHighlight) {
        // Remove any existing highlighting
        for (var dueClass in dueClasses) {
            $("#bugListContainer td,tr")
            .find("." + dueClasses[dueClass])
            .removeClass(dueClasses[dueClass]);
        };
        if (fHighlight) {
            // Let's find the where the Due column is
            var dueClass = /col_[d]+/.exec($("th:has(a[title=" + FB_DUE + "]):first").attr("class"));
            // If the column isn't displayed, we can give up
            if (!dueClass) return;
            // Only check cells that are in the due column
            var dueSel = "#bugListContainer td." + dueClass + ":contains(/)";
            // Get the current time
            var dtNow = new Date();
            // Apply a CSS class that reflects the overdue state of the bug
            $(dueSel)
            .each(function() {
                var sDue = $(this).text();
                var dueClass =
                    reToday.test(sDue) ? "today" :
                    reTomorrow.test(sDue) ? "tomorrow" :
                    parseDate(sDue) < dtNow ? "late" :
                    null;
                if (dueClass)
                    (fWholeRow ? $(this).parent().find("td") : $(this)).addClass(dueClasses[dueClass]);
            });
        }
    }
    // Add a link to the "More" menu
    $('<a>')
    .attr("href", "javascript:void(0)")
    .text(sMenuText)
    .appendTo("#idFilterOptInnerToolbarActions")
    .wrap("<nobr>")
    .click(function() {
        // Toggle the highlight state, and remember it in a cookie
        fEnabled = !fEnabled;
        setCookie(sCookie, fEnabled);
        // Apply the highlighting
        highlightDue(fEnabled);
        // Get rid of the "More" menu
        theMgr.hideAllPopups();
    })
    .prepend('<img src="images/icon-clock.gif" width="16" height="16">')
    // Patch the paintRow function so it doesn't screw up highlighting when the rows are
    // checked/unchecked
    if (window.paintRow) {
        paintRow = function(row, color) {
            $("td,th", row).css("background-color", (color == "#FBFBFB" || color == "#EBF0F4") ? "" : color);
        }
    }
    highlightDue(fEnabled);

css:

    tr.due-late td,td.due-late { background-color: #fbb; }
    tr.due-today td,td.due-today { background-color: #fdd; }
    tr.due-tomorrow td,td.due-tomorrow { background-color: #ffc; }

View Text-based Attachments Inline with Bug Events

Originally posted by Dane Bertram.

Here’s a script that allows you to view case attachments in the browser without downloading them.

name:        Inline text-based attachments
description: Inlines the specified attachments types directly into the bug view
author:      Dane Bertram
version:     1.0.0.0
minApi:      1.0

js: 

var inlineExtensions = ['txt', 'js'];

var regex = new RegExp('.(' + inlineExtensions.join('|') + ')$');
$('div.attachments a[href^=default.asp?pg=pgDownload]')
.filter(function(){ return regex.test($(this).attr('href')); }) // only the extensions we want inlined
.each(function(){
    var $anchor = $(this);
    $.get($anchor.attr('href'), function(data) {
        $('<pre>')
        .css({ 'max-height' : '200px', 'border' : '1px solid #C7C7C7' })
        .text(data)
        .appendTo($anchor.parent('p'));
    });
});

Collapse/Expand Email Blocks

Originally posted by Dave Cross.

I have a handful of cases that have accumulated a large number of lengthy emails over time. I find the ability to collapse them all facilitates reviewing the updates without the pollution of long email conversations. Any individual email block can be expanded by double-clicking the collapsed header.

By default emails display as usual, but any page that contains one or more emails will include a link at the top of the Bug Event panel to collapse them down. Once a user has collapsed emails for a case the emails will appear collapsed on the next page visit. This collapsed-state recall is on a per-case basis, and only works for the current user on the current browser (due to it’s implementation using cookies).

No collapse/expand option will be displayed if the case does not contain email events.

name:          Collapse Email Blocks
description:   Collapse/expand an email block by double-clicking it
author:        Dave Cross
version:       1.0.0.3

js:

/*
Based on Collapse Code Blocks by Michel de Ruiter

http://fogbugz.stackexchange.com/questions/6395

History:
1.0.0.3 - Dave: Updated CSS to work with the new UI in FogBugz 8.7.18
                Changed bg colour of collapsed email blocks to better indicate that some text is hidden
1.0.0.2 - Dave: Do not collapse by default, but remember individual users' collapse settings on a per-bug basis
1.0.0.1 - Dave: Added an expand/collapse all option at the top of the bug history panel
*/
var ixBug = $('div.ixBug div a').text();
var sCookie = "emgfb-" + ixBug + "-collapseEmailBlocks";
// determine if user has previously specified to collapse email blocks for this case
var bCollapseEmail = $.cookie(sCookie) || false;
var sEmailBlockCollapseText = "Collapse All Email Blocks";
function CollapseEmailBlocks(bCollapse)
{
    if (bCollapse) {
        $("div.bugevent.detailed.email").addClass("collapsed");
        $("div.bugevent.detailed.email").find(".emailHeader").addClass("collapsed");
    }
    else {
        $("div.bugevent.detailed.email").removeClass("collapsed");
        $("div.bugevent.detailed.email").find(".emailHeader").removeClass("collapsed");
    }
    sEmailBlockCollapseText = (bCollapse ? 'Expand All Email Blocks' : 'Collapse All Email Blocks');
}
CollapseEmailBlocks(bCollapseEmail);
$("div.bugevent.detailed.email").attr("title", "Double-click to collapse/expand")
.dblclick(function() {
  $(this).toggleClass("collapsed");
  $(this).find(".emailHeader").toggleClass("collapsed");
});
/* Add a link at the top of the case history to expand/collapse all email blocks */
if ( $("div.email").length > 0 ) {
    $("span#BugEvents").prepend('<p><a id="toggleEmailBlocks" class="dotted" href="javascript:;">' + sEmailBlockCollapseText + '</a></p>');
    $("#toggleEmailBlocks").click(function() {
        bCollapseEmail = !bCollapseEmail
        CollapseEmailBlocks(bCollapseEmail);
        $(this).text(sEmailBlockCollapseText);
    if (bCollapseEmail) {
        $.cookie(sCookie, true);
    }
    else {
        // Don't set the cookie false, delete it instead - to avoid cookie proliferation
        $.cookie(sCookie, null);;
    }
    });
}

css:

div.email.collapsed {
  height: 66px;
  background-color: #BBB;
  overflow: hidden !important;
  cursor: default;
}
div.emailHeader.collapsed {
  background-color: #AC9C98;
}
#toggleEmailBlocks {
  font-size: 11px;
}

Change Default “From” Value for Outgoing Emails

Originally posted by Michel de Ruiter.

This script will change the default selected value for the “from” when sending an email to the personal name variant.

name:          Reply as me by default
description:   Change the default From address to my personal name
author:        Michel de Ruiter
version:       1.1.1.0

js:

function ChangeFromToMe() {
  if ($("#sFrom").length &&
      $('#sFrom option:selected').text().indexOf($('#username').text()) == -1) {
    $('#sFrom option:selected + option').attr('selected', 'selected');
    DropListControl.refresh($("#sFrom")[0]);
  }
}
ChangeFromToMe();
$(window).on('BugViewChange', ChangeFromToMe);

Floating Top Nav

Originally posted by Dane Bertram.

Here’s a BugMonkey customization that will keep the “header” at the top of your screen even as you scroll the page:

name:        Floating top nav
description: Makes the top nav remain fixed at the top of the screen as you scroll
author:      Dane Bertram, Michel de Ruiter
version:     1.5.0.0

js:

function togglePin() {
  $('body').toggleClass('floatingTopNav');
}
if (!$.browser.msie || parseInt($.browser.version, 10) > 6) {
  $('<span id="pinTopNav">').attr('title', 'Pin navigation bar').click(togglePin)
    .append($('<span id="unpin">').text('u2612'))
    .append($('<span id="pin">').text('u2610'))
    .prependTo('#navTop nobr');
  togglePin(); // Default on
}

css:

#pinTopNav {
  cursor: pointer;
  margin-right: 6px;
}
#pinTopNav #pin, .floatingTopNav #pinTopNav #unpin {
  display: inline;
}
#pinTopNav #unpin, .floatingTopNav #pinTopNav #pin {
  display: none;
}
.floatingTopNav #navTopContainer {
  position: fixed;
  right: 0;
  left: 0;
  z-index: 1;
}
.floatingTopNav #banner {
  position: fixed;
  left: 0;
  right: 0;
  top: 21px;
  z-index: 2;
}
.floatingTopNav #belowBanner {
  position: fixed;
  right: 0;
  top: 63px;
  z-index: 1;
}
.floatingTopNav #appTabs {
  position: fixed;
  left: 0;
  z-index: 2;
}
.floatingTopNav #mainArea {
  margin-top: 89px;
}
.floatingTopNav #idPageNotifications table.notify-container {
  margin-top: 60px;
}
.floatingTopNav div.messageBar {
  position: fixed;
  z-index: 1;
}
.floatingTopNav .popupMenu {
  position: fixed !important;
}
.floatingTopNav #caseListCategoryPopup.popupMenu,
.floatingTopNav #emailActionsMorePopup.popupMenu {
  position: absolute !important;
}
.floatingTopNav .favoritesPopup {
  position: fixed !important;
  left: auto !important;
  right: 7px !important;
}
.floatingTopNav #idDropList_searchFor_oDropList {
  position: fixed !important;
  top:   54px !important;
  left:  auto !important;
  right:  7px !important;
  z-index: 12 !important;
}

See Time Estimates in Different Formats

Originally posted by Daniel LeCheminant.

You can try this bugmonkey script, which attempts to convert the hours listing into days/hours/minutes:
See Time Estimates in Different Formats


Save Email and Case Edit Drafts as You Type

Originally posted by Daniel LeCheminant.

The following BugMonkey script will give you some rudimentary save and restore functionality:

name:        Save Drafts
description: Save drafts of bug events (on modern browsers)
author:      Daniel LeCheminant
version:     1.0.0

js:
(function() {
    var storage = window.localStorage;
    var popup = window.api.PopupManager.newPopup("Drafts");

    var init = function() {
        var ixLastDraft = -1;

        setInterval(function() {
            $("div.body.editable textarea:not([ixDraft])").each(function() {
                var textarea = this;
                var ixDraft = ixLastDraft = nextDraft(ixLastDraft);

                $(this)
                .attr("ixDraft", ixDraft)
                .keyup(function() {
                    save(ixDraft, $(this).val());
                });

                $(this).closest(".editable").find("input.dlgButton")
                .click(function() {
                    deleteDraft(ixDraft);
                });

                var target1 = $(this).closest(".bugevent").find(".summary:last")
                var target2 = target1.find("div");

                $("<a>")
                .css({ cursor: "pointer", "text-decoration": "underline" })
                .addClass("action")
                .css($.browser.msie ? { "margin-left": "60px"} : { "float": "right" })
                .text("Drafts")
                .appendTo(target2.length ? target2 : target1)
                .click(function() {
                    var draftList = $("<div>");
                    for (var ixDraft = 0; ixDraft < 100; ixDraft++) {
                        var sText = load(ixDraft);
                        if (sText && sText.length > 0) {
                            var row = $("<div>")
                            .css({
                                "width": "230px",
                                "border": "1px dotted #ccc",
                                "background-color": "#f8f8f8",
                                padding: "4px",
                                margin: "2px"
                            })
                            .appendTo(draftList);

                            $("<img>")
                            .attr("src", StaticContentUrl("images/delete.gif"))
                            .addClass("draftDeleter")
                            .css({
                                "vertical-align": "top",
                                "margin-right": "8px",
                                "cursor": "pointer",
                                "float": "left"
                            })
                            .appendTo(row);

                            $("<div>")
                            .addClass("draftSelector")
                            .css({
                                "width": "200px",
                                "max-height": "100px",
                                "text-overflow": "ellipsis",
                                "font-size": "10px",
                                overflow: "hidden",
                                "cursor": "pointer",
                                "display": "inline-block",
                                "float": "right"
                            })
                            .attr("title", sText)
                            .text(sText)
                            .appendTo(row);

                            $("<div>").css("clear", "both").appendTo(row);

                            row.contents().attr("ixDraft", ixDraft);
                        }
                    };

                    if (!draftList.find("div").length) {
                        $("<div>").text("No Saved Drafts").appendTo(draftList);
                    }

                    popup.setHtml(draftList.html());
                    popup.showPopup(); popup.hide(); // Workaround positioning bug
                    popup.showPopup(this);

                    $("div.draftSelector")
                    .click(function() {
                        var ixDraft = $(this).attr("ixDraft");

                        $(textarea).val(load(ixDraft));
                        popup.hide();
                    })

                    $("img.draftDeleter")
                    .click(function() {
                        var ixDraft = $(this).attr("ixDraft");
                        deleteDraft(ixDraft);
                        popup.hide();
                    });
                });
            });
        }, 250);

        return true;
    };

    var save = function(ixDraft, text) {
        storage["draft_" + ixDraft] = text;
    };

    var load = function(ixDraft) {
        return storage["draft_" + ixDraft];
    };

    var deleteDraft = function(ixDraft) {
        storage.removeItem("draft_" + ixDraft);
    };

    var nextDraft = function(last) {
        for (var i = last + 1; i < 100; i++) {
            var sText = load(i);
            if (!sText || !sText) return i;
        }
        return -1;
    }

    $(function() {
        if (!$("#BugFields").length || !storage) return;
        /* Handle switching from view to edit */
        TabManager.renderView = (function(fnOld) {
            return function(F) {
                return fnOld.call(TabManager, F) && init();
            };
        })(TabManager.renderView);

        init();
    });
})();

Show Attachment File Sizes

Originally posted by Daniel LeCheminant.

This script will show you the file size of attachments, and will warn you if they are too large to be uploaded:
filesz

name:        Show attachment sizes
description: Show the size of files that are attached to a case before they are uploaded
author:      Daniel LeCheminant
version:     1.0.0.0

js:

$(function() {
    var cMaxBytes = 10000 * 1024;
    var sWarning = "File is larger than the maximum allowed attachment size";
    var rgPostFix = [["MB", 1024 * 1024], ["KB", 1024], ["B", 1]];

    if (!$("iframe[name=attachFrame]").length) return;

    setInterval(function() {
        $("#files_list").find("div.attachmentBlock:not(.hasSize)")
        .each(function() {
            var fs = this.element.files;
            if (!fs) return;

            var cBytes = fs[0].size;
            var sBytes;

            $.each(rgPostFix, function(ix, el) {
                if (cBytes >= el[1]) {
                    sBytes =
                        Math.round(cBytes / el[1] * 100) / 100 + " " + el[0];
                    return false;
                }
            });

            $("")
            .css("margin", "4px")
            .css("color", cBytes > cMaxBytes ? "#f00" : "#000")
            .attr("title", cBytes > cMaxBytes ? sWarning : "")
            .text("(" + sBytes + ")")
            .appendTo($(this).find("nobr"));
        })
        .addClass("hasSize");
    }, 1000);
});

Warning for Emails Missing Attachments

Originally posted by Daniel LeCheminant.

This customization warns you if you attempt to send an email that talks about attachments, but doesn’t include any.
warnl

name:        Warn about emails with missing attachments
description: Warns if your email says "attach" but you haven't attached anything
author:      Daniel LeCheminant
version:     1.0.0.0

js: 
   var sWarning = 
      "Your message mentions attachments, " +
      "but you haven't attached anything!nn" +
      "Do you still want to send the message?";

   if(!window.clickBugSubmit) return;
   window.clickBugSubmit = (function(fnOrig) {
      return function(e, elForm, fXMLSubmit, sValue, bOK) {
         if(bOK) {
            var fHasAttach = $("#files_list div").length;
            var sEmailText = $("#bugviewContainerEdit .emailHeader").siblings()
                             .find("textarea").val();
            var fSaysAttach = /battach/i.test(sEmailText);
            if(fSaysAttach && !fHasAttach && !confirm(sWarning)) {
               return cancel(e);
            } 
         }
         return fnOrig.apply(this, arguments);
      };
   })(window.clickBugSubmit);

Incorrect Mailbox Alert

Originally posted by Ben “Beastmaster” McCormack.

This is a script that lets you define a dictionary of user and email mappings. When the user sends or replies to an email, if the email selected for the user does not match what is in the dictionary, the user will be alerted to change the selected mailbox:
mailbox

name:          Alert If Email is Wrong
description:   Define a dictionary of users and email addresses and alert the user if they are using the wrong email address.
author:        Ben McCormack
version:       1.0.0.0

js:

var sUsers = {};
//define your user and mailbox associations here. The user name must match
//the full name of the user in FogBugz. The mailbox must match an available
//option in the From field when sending an email.
sUsers['Ben McCormack'] = '"Ben McCormack" <cases@benmtest.fogbugz.com>';
sUsers['Barney Rubble'] = '"FogBugz On Demand" <cases@benmtest.fogbugz.com>';
var sDefaultBackgroundCSS = $('div.body.editable div.emailHeader').css('background-color');
//main is called at the bottom
function main() {
  if (!replyingToEmail()){
    return;
  }
  if (!emailAddressMatchesUser()){
    notifyUserOfMismatch();
  }
}
//this function tells us if we're currently replying to an email
function replyingToEmail(){
  return $('div.body.editable .emailHeader').length !== 0;
}
//this function tells us if the "From" address matches the current
//user. You can define this however you want.
function emailAddressMatchesUser(){
  var sSelectedEmailAddress = $('select#sFrom option[selected="selected"]').text();
  var sCurrentUser = GetFullName();
  if (sUsers[sCurrentUser]===undefined){
    //this user didn't have a mailbox defined, so just return true
    return true;
  }
  return sUsers[sCurrentUser] === sSelectedEmailAddress;
}
//this function defines what happens when the From address doesn't
//match what is expected for this user. You can define this however
//you want.
function notifyUserOfMismatch(){
  if ($('div.emailMismatch').length !== 0) {
    return;
  }
  sUser = GetFullName();
  $('div.body.editable div.emailHeader').css('background-color','#CC5151');
  sMessage = 'ALERT! You should change the From address to: <br>' + htmlEncode(sUsers[sUser]);
  $('div.body.editable div.emailHeader').prepend('<div class="emailMismatch">' + sMessage + '</div>');
}
function clearNotification(){
  if ($('div.emailMismatch').length !== 0) {
    $('div.emailMismatch').remove();
    $('div.body.editable div.emailHeader').css('background-color',sDefaultBackgroundCSS)
  }  
}
function htmlEncode(value){
  return $('<div/>').text(value).html();
}
$(document).ready(function(){
  main();
});
$(window).on('BugViewChange', function(event) {
  main();
});

css:

div.emailMismatch{
  font-weight: bold;
  font-size: 120%;
}

Assign-back Case Link

Originally posted by adambox.

This script adds a link to quickly pass a case back to the person who assigned it to you.

name:          Assign-Back case link
description:   Adds a link in case view to assign the case back to the last assigner
author:        Adam Wishneusky, Michel de Ruiter
version:       1.1.0.0

js:

var ignoreMe = true; // Set to false to assign back to yourself as well.
if (!$('#bugviewContainer').length)
  return;
if (goBug.ixPersonAssignedTo != GetPersonID())
  return;
var lastAssigner =
  $('div.bugevents div.bugevent div.summary span.action:contains("ssigned to")' +
    (ignoreMe ? ':not(:contains("by ' + $(username).text() + '"))' : '') +
    ':' +(fMostRecentEventFirst ? 'first' : 'last') + ' a.person + a.person');
if (!lastAssigner.length)
  return;
window.AssignBack = function() {
  $('#edit0').click();
  $('select#ixPersonAssignedTo').val(lastAssigner.attr('data-ixperson')).change();
  DropListControl.refresh($('select#ixPersonAssignedTo')[0]);
}
var linkSwipeHtml = '<a onclick="javascript:AssignBack()" href="#" title="to ' +
  lastAssigner.text() + '">Assign Back</a>';
$('span.categoryAndAssignedTo').append(' ' + linkSwipeHtml);

Markdown in Case Events

Originally posted by John Gruber.

name:          Markdown
description:   Apply Markdown to case events
author:        John Gruber, John Fraser, Mike Wolfe, Michel de Ruiter
version:       0.2.0.0

js:
function decodeHtml(txt) {
  return txt
    .replace("&lt;pre&gt;",  "<pre>")
    .replace("&lt;/pre&gt;", "</pre>");
}
function markItDown() {
  var converter = new Markdown.Converter();
  $("div.bugevents div.body:not(.editable)").each(
    function() {
      var content = decodeHtml($(this).html());
      var replacetext = converter.makeHtml(content);
      $(this).html(replacetext);
    }
  );
}
$.getScript("http://pagedown.googlecode.com/hg/Markdown.Converter.js")
.done(function(script, textStatus) {
  markItDown();
  $(window).on('BugViewChange', markItDown);
  $(document).ajaxComplete(function(e, xhr, settings) {
    // if (settings.url.indexOf("_action=minimalUpdates") != -1)
    markItDown();
  });
})
.fail(function(jqxhr, settings, exception) {
  alert(exception.message);
});

Auto-Expand the Wiki Tree View

Originally posted by Max Kramer.

This script automatically expands the wiki tree view, showing more than the default 5 articles under the root article.

wiki_expand

name:          Expand Wiki Hierarchy
description:   Shows more articles in Wiki sidebar than the default 5
author:        Max Kramer
version:       1.0.0.0

js:

   var i = 2;                   // i is the number of times the view will be expanded, with each expansion being 10 more articles (or however many are left in the wiki if that number is less than 10) 

   function expandWiki () {           
        setTimeout(function () {    
        $(document).ready(function() {
            if ($('div .treeview-load-omitted-button')){
                $('div .treeview-load-omitted-button').click()
                };
            });  
        i--;                    
        if (i > 0) {           
             expandWiki();             
        }                        
        }, 1000)
    }

    expandWiki();  

css:

/* body { background-color: red !important; } */

Attach Javascript Events to FogBugz Dropdowns

Originally posted by Max Kramer.

Here’s a short script to attach a Javascript change event to dropdown in the FogBugz case view (the example shows an alert when the Priority dropdown is changed — make sure to target the element for the dropdown you want to use).

name:        Dropdown Alert
description: Doesnt do much, yet
author:      Max Kramer
version:    1.0.0.0
minApi:      1.0

js: 
    $(window).on('BugViewChange', function() {
        $('#ixPriority').change(function() {
          alert('Dropdown changed, validate me.');
        });
    });

css:

Customize Default Permissions for New Projects

Originally posted by Sonny.

name:          On Create New Project Default to a Particular Value
description:   When creating a new project, change the Initial Permissions to something else
author:        Sonny Kim
version:       1.0.0.0

js:

if (document.location.href.indexOf('pgEditProject')) { // if this is     the 'pgEditProject' page
  if ($("#idSelectTemp_0").val() == "-1") {
     $("#idSelectTemp_0 option[value='-1']").removeAttr("selected"); 
     $("#idSelectTemp_0 option[value='0']").attr("selected", "selected"); // this changes the selected option to 'value=0'
     DropListControl.refresh($("#idSelectTemp_0")[0]); // this refreshes the DropListControl to change the selected option
  }
}

Hide Certain Users from the “Assign To” Dropdown

Originally posted by Sonny.

name:          Prevent certain users from being assigned to cases
description:   Hide certain users in the "assign to" dropdown
author:        Adam Wishneusky and Sonny Kim
version:       1.0.0.0

js:

$(function(){
    // if we're not on the case page, don't do anything
    if (!$('#bugviewContainer').length) return;
    var arrExcludePerson = new Array();        
    // ******* YOU MUST EDIT THIS SECTION ******* 
    // array of ixPerson ids to remove from the drop-down list
    arrExcludePerson[0] = "2";
    arrExcludePerson[1] = "4";
    arrExcludePerson[2] = "6"; 
    // *******        END SECTION         ******* 
    var removeUsersFromDropDown = function(dropDownId, arrExclude) {
        if ($(dropDownId).length > 0) {
           for (var i = 0; i < arrExclude.length; i++) {
              var strConstructSelector = dropDownId + " option[value='" + arrExclude[i] + "']";
              $(strConstructSelector).remove();
           }
           DropListControl.refresh($(dropDownId)[0]);
        }
    }
    var oldShowAssignSpan = showAssignSpan;    // save the old 'showAssignSpan' function
    showAssignSpan = function(el, e) {         // overwrite 'showAssignSpan' function
        oldShowAssignSpan(el, e);              // call original 'showAssignSpan' function
        var dropDownIdParam = "#ixPersonAssignedToOverrideDropDown_assign0";
        removeUsersFromDropDown(dropDownIdParam, arrExcludePerson);
    }    
    var myFunction = function(sCommand) {
        // sCommand will specify the current action
        // (i.e., edit, resolve, assign, close, reply, forward, etc.)
        // iterate the array of persons to exclude and remove them from the dropdown list.
        if (sCommand == "new" || sCommand == "edit" || sCommand == "reopen" || sCommand == "assign") {
           var dropDownIdParam = "#ixPersonAssignedTo";
           removeUsersFromDropDown(dropDownIdParam, arrExcludePerson);
        }
        //console.log(sCommand);
    };
    if ($('#sEventEdit').length > 0)
    {
      myFunction('new');
    }
    else
    {
      myFunction('load');
    }
    // run it when the view changes and pass in the new view:
    $(window).on('BugViewChange', function(e, data) {
        myFunction(data.sCommand); 
    });
});

Set Default Mailbox for Email Replys by Project

Originally posted by Sonny.

This script is a shell to have a particular project default to a particular “from” mailbox address when replying. This code uses the Project ID, but you can switch to using the MAILBOX the case came into instead.

If a case is in a given project, you may want to use the code above to reply from a specific address. If you don’t care about the current project and want the reply set based on what address they emailed YOU at, you can change the above from using goBug.ixProject to using goBug.ixMailbox.

name:          Default the 'from' mailbox for certain project(s)
description:  Automatically change the 'from' address when on a particular project.
author:        Sonny Kim
version:      1.0.0.0

js:

$(function(){
    // if we're not on the case page, don't do anything
    if (!$('#bugviewContainer').length) return;
    var arrForThisProject = new Array(); 
    var arrUseThisMailbox = new Array(); 
    // ******* YOU MUST EDIT THIS SECTION ******* 
    // need a project to mailbox mapping. Add elements to the arrays for more projects - mailbox pairings.
    arrForThisProject[0] = '2'; // for this ixProject number
    arrUseThisMailbox[0] = '"some name" <some@mailbox.com>';  // use this mailbox (find this value by inspecting the from element in reply page and finding the 'select' value options
    // *******        END SECTION        ******* 
    var setTheFromMailboxAddress = function () {
      for (var i = 0; i < arrForThisProject.length && i < arrUseThisMailbox.length; i++) {
          if (window.goBug.ixProject == arrForThisProject[i]) {
              $('#sFrom option[value*="' + arrUseThisMailbox[i] + '"]:first').attr('selected','selected');
          }
      }
      DropListControl.refresh($('#sFrom')[0]);
    }

    var myFunction = function(sCommand) {
        if ($('div.body.editable .emailHeader').length < 1) return;
        setTheFromMailboxAddress();
    };
    if ($('#sEventEdit').length > 0)
    {
      myFunction('new');
    }
    else
    {
      myFunction('load');
    }
    // run it when the view changes and pass in the new view:
    $(window).on('BugViewChange', function(e, data) {
        myFunction(data.sCommand); 
    });
});

Highlight Overdue Relative Date

Originally posted by Ben McCormack.

For use with the Relative Time plugin:

eI4F3

name:        Make overdue relative date red
description: Checks the relative due date and if it is red, makes it red
author:      Ben McCormack
version:     1.0.0.0
minApi:      1.0

js: 
$('span.relative-time-due:not([data-time_span_seconds*="-"]):not([data-time_span_seconds*="null"])').css('color','red');

Toggle Quoted Text in Emails

Originally posted by Michel de Ruiter.

This script helps you easily toggle the visibility of all quoted texts in e-mails.

name:          Toggle quoted texts
description:   Adds a link to toggle hiding/showing all quoted texts
author:        Michel de Ruiter
version:       1.0.0.0

js:

if ($("div.emailBody > a.dotted[href='#'][onclick]").size() > 0) {
  $("<a>").css("border", "1px dotted #888")
  .insertBefore("#BugEvents").text("Toggle quoted texts")
  .attr("href", "javascript:void(0)")
  .click(function() {
    $("div.emailBody > a.dotted[href='#'][onclick]")
    .each(function(){ this.onclick(); });
  });
}

Set Hard Defaults for Project and Area in New Cases

Originally posted by Quentin Schroeder.

This script will set default values for Project and Area on all new cases, rather than using the most recently used value for those fields. Just change the defaultProject and defaultArea variables to names that match your system, and be sure to match them case sensitively.

name:        Override sticky defaults for new cases
description: Always use a given Project and Area for new cases
author:      Daniel LeCheminant
version:     1.0.0.0

js: 
if (goBug) {
    var defaultProject = "Inbox";
    var defaultArea = "Undecided";

    var setValue = function(target, value, callback) {
        var list = $("#ix" + target);
        var rgOpt = list.find("option");
        var opt = rgOpt.filter(function () {
            return $(this).text() == value;
        }).attr("selected", true);

        DropListControl.refresh(list[0]);

        if (callback) callback(rgOpt.index(opt));
    }

    if (goBug.ixProject == -1) {
        setValue("Project", defaultProject, projectChanged);
        setValue("Area", defaultArea);
    }
}

Disable Editing of Closed Cases

Originally posted by adambox.

This customization prevents editing closed cases, by removing the “Edit” button:

name:          Disable editing closed cases
description:   Removes the edit link on closed cases
author:        Adam Wishneusky
version:       1.0.0.0

js:

css:

  #editClosed0 {
    display: none !important;
  }
  #editClosed1 {
    display: none !important;
  }

Add “Projects” Dropdown to the Top Nav

Originally posted by Dane Bertram.

This script will add a “Projects” dropdown menu to the top navigation bar. The links in this menu serve as quick shortcuts for seeing all the cases in a given project.

name:          'Projects' main menu
description:   Adds a 'Projects' menu to the main navigation bar
author:        Dane Bertram
version:       1.0.0.0

js:

var nav = $('#mainnav');

var menu = $('<a>')
    .addClass('navlink menu')
    .attr('href', '#')
    .text('Projects')
    .append($('#Menu_Filter > img').clone())
    .on('click', function() {
        return theMgr.showPopup('projectFilterPopup', this, 0, this.offsetHeight + 2, null, true)
        || KeyManager.browseMenus('mainnav')
        || KeyManager.oMenuBrowser.setElCurrent(this)
        || KeyManager.browsePopup('projectFilterPopup');;
    })
    .appendTo(nav);

var popup = $('#filterPopup')
    .clone(false)
    .attr('id', 'projectFilterPopup')
    .appendTo(nav);

var linkDiv = popup.find('div:first').empty();

$.each(DB.Project, function(ix, project) {
    $('<a>')
        .attr('href', 'default.asp?pre=preSaveFilterProject&ixProject=' + project.ixProject)
        .text(project.sProject)
        .on('click', function() {
            return doPopupClick();
        })
        .appendTo(linkDiv);
});

theMgr.add('projectFilterPopup');

Customize the Community User Landing Page

Originally posted by Dane Bertram.

This customization adds a wiki page’s content as a third column to the Community User landing page.

Just a few notes:

  1. Make sure you edit the rules for this customization to make sure it’s required for all community users.
  2. Make sure you update the wikiPage variable to point to the wiki page you’d like to load on the Community User landing page (just look at the end of the URL after you’ve navigated to the wiki page you’d like to include on the landing page).
  3. Make sure the wiki page you’re trying to include on the landing page is part of a wiki that community users are allowed to read.
  4. The customization above assumes the wiki page is using the built-in FogBugz 8 Default Template. If that’s not the case, you might need to modify the var wikiContent = ... line to select the right portion of the wiki page.
name:          Wiki column on Community User landing page
description:   Modifies the community user landing page to include a wiki page as a third column
author:        Dane Bertram
version:       1.0.0.0

js:

var wikiPage = 'W19';
var columnWidth = '400';

if (!$('#idTeaserParagraph').length) return;

$('#mainArea > table')
    .attr('width', '100%')
    .find('td')
    .eq(1)
    .attr('width', '');

$.get('default.asp?' + wikiPage, function(data) {
    var wikiContent = $(data).find('#wiki-page-content').html();
    $('<td>')
        .attr('width', columnWidth)
        .html(wikiContent)
        .appendTo('#mainArea > table tr:first');
});

Make “My Filters” Come First

Originally posted by Dane Bertram.

This customization reorders the “Filters” menu to display your personal saved filters before “Shared Filters”:

name:          "My Filters" come first
description:   Moves the "My Filters" section above the "Shared Filter" section in the Filters menu
author:        Dane Bertram
version:       1.0.0.0

js:

var groups = [];

$("#filterPopup .popupHeadline").each(function(ix, el) {
    var jEl = $(el);
    groups.push({
        heading: jEl,
        filters: jEl.nextUntil('hr', 'a')
    });
});    

// swap heading text
var temp = groups[0].heading.text();
groups[0].heading.text(groups[1].heading.text());
groups[1].heading.text(temp);

// move filters under the updated headings
groups[0].heading.after(groups[1].filters.detach());
groups[1].heading.after(groups[0].filters.detach());

Automatically Correct Broken Links

Originally posted by Ben McCormack.

Say you moved your server and now Wiki images and links point to the old location. What can you do besides editing every wiki entry?

You can use a BugMonkey script (My Settings > Customizations) to automatically correct the links when users visit your pages.

Copy the following code to a new customization, and change the value of oldLocation to the beginning of the URL that you want to replace. This will effectively make all the links relative rather than the full hard-coded URL. This will even fix http links after changing benm to https.

name:          Replace local images and links
description:   Replaces hardcoded references to old server images with an empty string, forcing it to use relative resources.
author:        Ben McCormack
version:       1.2.0.0

js:

function replaceLinks(oldLocation) {
  $('img[src*="' + oldLocation + '"]').each(function() {
    $(this).attr('src',  $(this).attr('src').replace(oldLocation,  ''));
  });
  $('a[href*="'  + oldLocation + '"]').each(function() {
    $(this).attr('href', $(this).attr('href').replace(oldLocation, ''));
  });
}
$(document).ready(function() {
  replaceLinks('http://benm/');
});

Add Public Ticket URL to the Case Side Menu

Originally posted by adambox.

Here is a BugMonkey script that adds the ticket to the sidebar in a case, linked to the ticket URL.

name:          Public Ticket URL incase
description:   Show the public ticket url in cases
author:        Adam Wishneusky
version:       1.0.0.0

js:

  $(function(){
    // if we're not on the case page, don't do anything
    if(!$('#bugviewContainer').length)return;
    var myFunction =function(sCommand){
      var ticketurl = window.location.protocol +'//'+ window.location.hostname + window.location.pathname +'?'+ goBug.sTicket;
      var ticketurllink ="<div class="content"><a href=""+ ticketurl +"">"+ goBug.sTicket +"</a></div>";
      var sidebar = $('#bugviewContainerSide');
      var label = $('<label>').text('Public Ticket');
      sidebar.append(label).append(ticketurllink);
    };
    // run it on page-load:
    myFunction('load');
    // run it when the view changes and pass in the new view:
    $(window).on('BugViewChange',function(e, data){
        myFunction(data.sCommand); 
    });});

Cascading Filters

Originally posted by Rich Armstrong.

This customization lets you setup numbered filters that you work on in order every day. Create filters with numbers in the beginning of the name and when one filter is empty, it will load the next one.

For example, I have 1 – My Inbox Cases for cases I have to do today. When I close them all, FogBugz switches me automatically to 2 – Micro-manage My Colleagues

name:          Cascading Filters
description:   Loads the next filter when this one's empty. Must be named "0 - ...", "1 - ...", etc.
author:        Rich Armstrong
version:       1.0.0.1

js:

cascadingFilters = function() {
  var currentFilter = $("#idFilterTitle");
    var storage = window.localStorage;
    if (storage['CascadingFiltersArrived']) {
    if (storage['CascadingFiltersArrived'] == '1') {
        storage['CascadingFiltersArrived'] = '0';
        Info.show('Huzzah! Filters are cleared.');
        $('#loadingBar').html('Huzzah! Filters are cleared. ');
                 var toggleCascadingLink = $('<a>')
                    .text('Disable Cascading Filters.')
                    .attr('href', "#")
                    .click(function(event) {
                        if (storage) {
                            storage["CascadingFiltersEnabled"]=(storage["CascadingFiltersEnabled"]=="1"?"0":"1");
                        }
                        $(this).hide('fast');
                        Info.hide();
                    })
                    .appendTo($('#cascadingFiltersNotification'));
            setTimeout("$('#loadingBar a').hide();",5000);
        return;
    }
    }
  if (currentFilter.length > 0) {
    var filterName = currentFilter.text();
    if (filterName.match(/^[0-9] - /)){
        if (storage['CascadingFiltersEnabled']) {
            if (storage['CascadingFiltersEnabled'] == 0){
                Info.show('Cascading Filters disabled.');
                $('#loadingBar').html('Cascading Filters disabled. ');
                 var toggleCascadingLink = $('<a>')
                    .text('Enable.')
                    .attr('href', "/default.asp?pg=pgList")
                    .click(function(event) {
                        if (storage) {
                            storage["CascadingFiltersEnabled"]=(storage["CascadingFiltersEnabled"]=="1"?"0":"1");
                        }
                        $(this).hide('fast');
                        Info.hide();
                    })
                    .appendTo($('#cascadingFiltersNotification'));
                setTimeout("$('span#cascadingFiltersNotification').parent().effect('blind');",15000);
                return;
            }
        }

        // the filter names come back with a space at the front, which I'll just match, not bother to strip out.
        var filterNameRegexes = [
        /^[0] - /,
        /^[1] - /,
        /^[2] - /,
        /^[3] - /,
        /^[4] - /,
        /^[5] - /,
        /^[6] - /,
        /^[7] - /,
        /^[8] - /,
        /^[9] - /,
        /My Cases/
      ]
      var nextFilter = $("#filterPopup a").filter(function(){ return $(this).text().substring(1,100).match(filterNameRegexes[(filterName[0]*1) + 1]); });
      if (nextFilter.length == 0) {
        nextFilter = $("#filterPopup a").filter(function(){ return $(this).text().substring(1,100).match(/My Cases/); });
        nextFilter = nextFilter[0];
      }
      nextFilterHref = window.location.protocol + "//" + window.location.host
                     + window.location.pathname.replace("default.asp","")
                     + $(nextFilter).attr('href');
      nextFilterName = $(nextFilter).text().substring(1,100);
      if ($('#row_0').length == 0) {
        $('#bugListContainer').remove();
        Info.show('No cases here. Loading "' + nextFilterName + '"...');
        if (nextFilterName == "My Cases") {
            storage['CascadingFiltersArrived'] = '1';
        }
        window.location.href = nextFilterHref;
      }

    }
  }
  }

  cascadingFilters();

Required Fields

Originally posted by adambox.

I created a custom field called “Found In”. When I view the source of the new case page, I can see that Custom Fields’s internal unique name for it is “foundxinxg5c”. I put that value in my code here to create one required field. I also require the case title and a description (the main text field). On all types of case edits where the fields are available, they are required to have a value. If they don’t, each empty one is highlighted in red, the OK button is disabled with a tooltip instructing the user to complete the fields.

name:        Required fields
description: Disables the OK button if certain fields do not have values
author:      Adam Wishneusky
version:     1.0.0.0
minApi:      1.0

js: 

// required: idBugTitleEdit, foundxinxg5c, sEventEdit

window.disableSubmit = function() {
  $('#Button_OKEdit').attr("disabled","disabled").attr("title","please fill required fields to enable submission");
  $('#Button_Resolve').attr("disabled","disabled").attr("title","please fill required fields to enable submission");
  $('#Button_ResolveAndClose').attr("disabled","disabled").attr("title","please fill required fields to enable submission");
}

window.enableSubmit = function() {
  $('#Button_OKEdit').removeAttr("disabled").removeAttr("title");
  $('#Button_Resolve').removeAttr("disabled").removeAttr("title");
  $('#Button_ResolveAndClose').removeAttr("disabled").removeAttr("title");
}

window.checkAllFields = function() {
    if ((!$('#idBugTitleEdit').length || $('#idBugTitleEdit').val().length > 0) &&
        (!$('#foundxinxg5c').length || $('#foundxinxg5c').val().length > 0) &&
        (!$('#sEventEdit').length || $('#sEventEdit').val().length > 0)) {
        enableSubmit();
    }
}

window.setEnabledByContent = function() {
  if (this.value.length > 0) {
    $(this).removeClass("requiredfield");
    checkAllFields();
  }
  else {
    $(this).addClass("requiredfield");
    disableSubmit();
  }
}

$(function(){

    // if we're not on the case page, don't do anything
    if (!$('#bugviewContainer').length) return;

    var myFunction = function(sCommand) {
        // don't do anything when viewing a case
        if (sCommand == 'load' || sCommand == 'view') {
            return;
        }
        // if any field is present, disable it if it's empty and bind to the keyup event
        if ($('#idBugTitleEdit').length) {
            if ($('#idBugTitleEdit').val().length < 1) {
                $('#idBugTitleEdit').addClass("requiredfield");
                disableSubmit();
            }
            $('#idBugTitleEdit').keyup(setEnabledByContent);
        }
        if ($('#foundxinxg5c').length) {
            if ($('#foundxinxg5c').val().length < 1) {
                $('#foundxinxg5c').addClass("requiredfield");
                disableSubmit();
            }
            $('#foundxinxg5c').keyup(setEnabledByContent);
        }
        if ($('#sEventEdit').length) {
            if ($('#sEventEdit').val().length < 1) {
                $('#sEventEdit').addClass("requiredfield");
                disableSubmit();
            }
            $('#sEventEdit').keyup(setEnabledByContent);
        }
    };

    // this runs on full page load and determines if this is a new case or just viewing a case
    if ($('#sEventEdit').length > 0)
    {
      myFunction('new');
    }
    else
    {
      myFunction('load');
    }

    // run it when the view changes and pass in the new view:
    $(window).on('BugViewChange', function(e, data) {
        myFunction(data.sCommand); 
    });

});

css: 
  .requiredfield {
    border: 1px solid red !important;
  }

Here is a version that disables a single drop-down type field:

name:        Require Drop-Down Set
description: Requires that a custom drop-down field is changed from the default "--" value
author:      Adam Wishneusky
version:     1.0.0.0
minApi:      1.0

js: 
// set the value here to the id of the <select> for the dropdown. there is a text field
// with an id idDropList_SOMETHING_oText but you want just the SOMETHING part. that's the id
// of the actual <select> tag just after it in the DOM

window.requiredDropDownID = "likeitshotq03";

window.disableSubmit = function() {
  $('#Button_OKEdit').attr("disabled","disabled").attr("title","please fill required fields to enable submission");
  $('#Button_Resolve').attr("disabled","disabled").attr("title","please fill required fields to enable submission");
  $('#Button_ResolveAndClose').attr("disabled","disabled").attr("title","please fill required fields to enable submission");
}

window.enableSubmit = function() {
  $('#Button_OKEdit').removeAttr("disabled").removeAttr("title");
  $('#Button_Resolve').removeAttr("disabled").removeAttr("title");
  $('#Button_ResolveAndClose').removeAttr("disabled").removeAttr("title");
}

window.checkAllFields = function() {
    // find the <select> tag
    var theField = $('#' + requiredDropDownID);
    // if the tag isn't on the page, or the value is NOT the default of "--", enable submit
    if (( theField.length == 0) || 
        ( $('#' + requiredDropDownID + ' option:selected').text() != '--' )) {
        theField.prev().find('input[id*="' + requiredDropDownID + '"]').removeClass("requiredfield");
        enableSubmit();
    }
    // otherwise, disable
    else {
        theField.prev().find('input[id*="' + requiredDropDownID + '"]').addClass("requiredfield");
        disableSubmit();
    }
}

$(function(){
    // if we're not on the case page, don't do anything
    if (!$('#bugviewContainer').length) return;

    var myFunction = function(sCommand) {
        // don't do anything when viewing a case
        if (sCommand == 'load' || sCommand == 'view') {
            return;
        }
        // run our check when the drop-down is changed
        $('#' + requiredDropDownID).change(checkAllFields);
        $('#' + requiredDropDownID).change();
    };

    // this runs on full page load and determines if this is a new case or just viewing a case
    if ($('#sEventEdit').length > 0)
    {
      myFunction('new');
    }
    else
    {
      myFunction('load');
    }

    // run it when the view changes and pass in the new view:
    $(window).on('BugViewChange', function(e, data) {
        myFunction(data.sCommand); 
    });
});

css: 
  .requiredfield {
    border: 1px solid red !important;
  }

Add “Jump to Top” Link to Case View

Originally posted by Max Kramer.

This script adds a “Jump to Top” link on the right side of the FogBugz nav bar (with “Working On” and “Starred”).

name:        Jump To Top
description: Adds "Jump to Top" link to FogBugz nav
author:      Max Kramer
version:     1.0.0.0
minApi:      1.0

js: 

// Check to make sure we're on the case view
if (!window.goBug)
    return;

function addLink() {
    $('.navlink.menu#Jump_to_Top').remove();  // Remove existing jump link
    var belowNavToolbar = $('#belowBanner').html();
    var sNewLink = '<a class="navlink menu" id="Jump_to_Top" href="#BugFields" title="Jump to the top of the page">Jump to Top</a>';
    $('#belowBanner').html(sNewLink + belowNavToolbar); 
}
addLink();

Prevent Accidental Email Sends

Originally posted by adambox.

This script prevents accidentally sending an email in FogBugz by fat-fingering the backtick key for a snippet. What happens to me is I try to hit backtick and hit tab instead, then space or enter, sending my email before I finished writing.

This script disables tabbing out of the email body textarea.

name:          Prevent Accidental Send
description:   Tab key has no effect in the body of a bug event edit box
author:        Quentin Schroeder, Adam Wishneusky
version:       1.2.0.0

js:

$(function(){
  // if we're not on the case page, don't do anything
  if (!$('#bugviewContainer').length) return;

  function tabHandler(e) {
    if (!e) {
      e = window.event;
    }
    var TABKEY = 9;
    // this gets just the email sEvents, not the edit mode one
    var input_sEvent = $('#sEventForward, #sEventReply')[0];
    var srcEl = e.srcElement? e.srcElement : e.target;
    if(!e.shiftKey && e.keyCode == TABKEY && srcEl == input_sEvent) {
      if (e.preventDefault) {
        e.preventDefault();
      } 
      if (e.stopPropagation) {
        e.stopPropagation();
      } else {
        e.cancelBubble = true;
      }
      e.returnValue = false;
      return false;
    }
    return true;
  }

  var disableTab = function() {
    if (document.onkeydown) {
      var currHandler = document.onkeydown;
      var newHandler = function() {
        currHandler();
        tabHandler();
      };
      document.onkeydown = newHandler;
    } else {
      document.onkeydown = tabHandler;
    };
  }

  // we're just going to run on full page-load, not transitions to and from reply/forward mode, because the event handler already discriminates based on what textarea input you're in
  disableTab();

  // don't need to run on bugviewchange like many other scripts
  //$(window).on('BugViewChange', function(e, data) {
  //  disableTab(data.sCommand); 
  //});
});

Snippet Placeholders

Originally posted by Rich Armstrong.

This customization adds dynamic placeholders for snippets. With it, you can create snippets that fill in a customer’s first name for you from the case correspondent field.

name:          Custom Placeholders
description:   Adds some helpful values to the rgPlaceholders list.
author:        Rich Armstrong
version:       1.0.0.4

js:

$(function(){
  // if we're not on the case page, don't do anything
  if (!$('#bugviewContainer').length) return;

  /* To Title Case 1.1.1
   * David Gouch <http://individed.com>
   * 23 May 2008
   * License: http://individed.com/code/to-title-case/license.txt
   *
   * In response to John Gruber's call for a Javascript version of his script: 
   * http://daringfireball.net/2008/05/title_case
   */
  String.prototype.toTitleCase = function() {
    return this.replace(/([w&`'‘’"“.@:/{([<>_]+-? *)/g, function(match, p1, index, title) {
        if (index > 0 && title.charAt(index - 2) !== ":" &&
          match.search(/^(a(nd?|s|t)?|b(ut|y)|en|for|i[fn]|o[fnr]|t(he|o)|vs?.?|via)[ -]/i) > -1)
            return match.toLowerCase();
        if (title.substring(index - 1, index + 1).search(/['"_{([]/) > -1)
            return match.charAt(0) + match.charAt(1).toUpperCase() + match.substr(2);
        if (match.substr(1).search(/[A-Z]+|&|[w]+[._][w]+/) > -1 || 
          title.substring(index - 1, index + 1).search(/[])}]/) > -1)
            return match;
        return match.charAt(0).toUpperCase() + match.substr(1);
    });
  };
  var addCustomPlaceholders = function(){
    if (!window.goBug) return;
    var oMyFirstName = new Object();
    oMyFirstName.sPlaceHolder = "{myfirstname}";
    var oMyLastName = new Object();
    oMyLastName.sPlaceHolder = "{mylastname}";
    var oMyName = new Object();
    oMyName.sPlaceHolder = "{myname}";
    oMyName.sValue = GetFullName(); // this placeholder already exists as {username}
    // I just did this for consistency.
    sMyName = GetFullName().match(/(w+) (w+)/);
    if (sMyName[1]) { 
      oMyFirstName.sValue = sMyName[1];
      oMyLastName.sValue = sMyName[2];
    }
    rgPlaceHolders.push(oMyFirstName);
    rgPlaceHolders.push(oMyLastName);
    if (window['goBug'] != undefined) {
       if (goBug.sCustomerEmail == "") {
            return;
       }
       var oFirstName = new Object();
       oFirstName.sPlaceHolder = "{firstname}";
       sFirstName = goBug.sCustomerEmail.match(/^"?(([w'-()]+), )?([w'-]+)( [w'-().]+)*(, [SsJj]r.)?"? <[a-zA-Z0-9._+-]+@[a-zA-Z0-9.-]+.[a-zA-Z]{2,4}(.[a-zA-Z]{2,4})?>$/);
       if (sFirstName) { 
       oFirstName.sValue = sFirstName[3].toTitleCase();
       } else {
         oFirstName.sValue = "[[name]]";
       }
       rgPlaceHolders.push(oFirstName);
    }
  } //end function addCustomPlaceholders
  addCustomPlaceholders();
});

Add Custom Fields Link to Admin Dropdown

Originally posted by adambox.

Here is a script to add a link to the Custom Fields configuration page to the Admin drop-down menu.

name:        Custom Fields in Admin menu
description: Adds config link for Custom Fields to admin menu
author:      Adam Wishneusky
version:     1.0.0.0
minApi:      1.0

js: 
  if ($('#adminPopup').length == 0) return;
  $('#Menu_Admin')[0].onclick = function (event) {
    if ($('#cflink').length == 0) {
      window.setTimeout(
        function(){
          $('</a id="cflink" href="?pg=pgPluginConfig&sPluginId=customfields@fogcreek.com">Custom Fields</a>').insertBefore($('#adminPopup').find('hr'));
        }, 200
      );
    }
    return theMgr.showPopup('adminPopup',this,0,this.offsetHeight + 4,null,true) || KeyManager.browseMenus('navTop') || KeyManager.oMenuBrowser.setElCurrent(this) || KeyManager.browsePopup('adminPopup');
  }

List View Auto-Refresh

Originally posted by Daniel LeCheminant.

Here’s a script to make the case list auto-refresh itself periodically.

name:          Update List
description:   Add update functionality
author:        Daniel LeCheminant, Michel de Ruiter
version:       1.0.1.0

js:

$(function() {
  var storage = window.localStorage;
  if (!storage || $("#bugGrid").size() == 0)
    return;
  var getVal = function(key, def) {
    return (storage && storage[key]) || getCookie(key) || def;
  };
  var setVal = function(key, val) {
    storage ? storage[key] = val : setCookie(key, val);
  };
  var update = function() {
    if ($(".rCB:checked").length || $("#searchFor").val().length) {
      reset();
    } else {
      Info.show("Updating...")
      location = "?pg=pgList";
    }
  };
  var fixMins = function(sMins) {
    return Math.max(parseInt(sMins) || 5, 1);
  };
  var getMins = function() {
    return fixMins(getVal("cMinsRefresh", ""));
  };
  var timeout;
  var stop = function() {
    timeout = clearTimeout(timeout);
  };
  var reset = function(fStart) {
    if (timeout || fStart) {
      stop();
      timeout = setTimeout(update, getMins() * 60 * 1000);
    }
  };
  $(document).bind("click mousedown keydown", function() { reset(); });
  $(window).bind("scroll", function() { reset(); });
  var fAutoRefresh = getVal("fAutoRefresh", false) == "true";
  var span = $("").insertAfter("#versionFooter .version:last");
  $('')
  .attr("checked", fAutoRefresh ? "checked" : null)
  .click(function() {
    (fAutoRefresh = $(this).is(":checked")) ? reset(true) : stop();
    setVal("fAutoRefresh", fAutoRefresh);
  }).appendTo(span);
  span.append("Auto update every  minutes")
  .find("input:last").val(getMins())
  .keyup(function() {
    setVal("cMinsRefresh", fixMins($(this).val()));
    reset();
  })
  .blur(function() {
    $(this).val(getMins());
  });
  reset(fAutoRefresh);
});

css:

span.update                                  { float: right; }
span.update > input[type='checkbox'] + input { width: 20px; text-align: right; }

Resolve and Close a Case with One Click

Originally posted by adambox.

name:          Resolve and close in one click
description:   Adds a close button to active cases to resolve and close the case "won't respond" in one click
author:        Adam Wishneusky
version:       1.0.0.0

js:

$(function(){
    // if we're not on the case page, don't do anything
    if (!$('#bugviewContainer').length) return;

    var myFunction = function(sCommand) {
      if ($('li > a#resolve0').length > 0 && $('li > a#close0').length == 0) {
        var closelink = document.createElement("li");
        closelink.innerHTML = '</a class="actionButton2 icon-left close" onclick="javascript:a=function (){ $('#resolve0').click();$('select#ixStatus').val('12').change();$('select#ixPersonAssignedTo').val('15567').change(); $('#Button_ResolveAndClose').click();return;};a();" href="#" id="close0" command="close">Close!</a>';
        $('li > a#resolve0').after(closelink);
      }
    }
    if ($('#sEventEdit').length > 0)
    {
        myFunction('new');
    }
    else
    {
        myFunction('load');
    }
    // run it when the view changes and pass in the new view:
    $(window).on('BugViewChange', function(e, data) {
        myFunction(data.sCommand); 
    });
});

Linear Workflow

Originally posted by ben-mccormack for this blog post.

name:        "Next Status" Workflow
description: If you set up statuses with a step number and name convention,
             e.g. 'Active (1. Dev)', this script will automatically add breadcrumb
             links for navigating between workflow steps above the case header. The
             key is to make sure you have active statuses set up correctly to work
             with this script. For example:   

             Active
             Active (1. Dev)
             Active (2. QA)
             Active (3. Ready to Ship)

author:      Ben McCormack & Dane Bertram
version:     2.0.0.3
minApi:      1.0

js:

  // controls whether or not the default active status
  // is included as a step regardless of it's name
  // note: will always be included as the first step
  var fIncludeDefaultActive = true;

  function stepExtractor(ixCategory) {
    var ixStatusDefaultActive = -1;
    if (fIncludeDefaultActive) {
      var cat = DB.Category.firstMatch(function(cat) {
        return cat.ixCategory === ixCategory;
      });
      if (cat) {
        ixStatusDefaultActive = cat.ixStatusDefaultActive;
      }
    }

    return function(status) {

      if (status.fDeleted || status.ixCategory !== ixCategory) return null;

      var reStep = /((d+).s*(.*))/ // capture step # and step name
      var m = reStep.exec(status.sStatus);
      if (m) {
        return {
          step: parseInt(m[1], 10),
          label: m[2],
          status: status
        };
      }

      if (ixStatusDefaultActive > 0 && status.ixStatus === ixStatusDefaultActive) {
        var label = status.sStatus;
        var match = status.sStatus.match(/((.*))/);
        if (match) {
          label = $.trim(match[1]);
        }
        return {
          step: 0,
          label: label,
          status: status
        }
      }
      return null;
    }
  }

  function sortByStep(step1, step2) {
    if (step1.step === step2.step) {
      // defer to FogBugz status ordering
      return step1.status.iOrder - step2.status.iOrder;
    }
    if (step1.step > step2.step) return 1;
    return -1;
  }

  function getCurrentStep(ixStatus) {
    var status = DB.Status.firstMatch(function(status) {
      return status.ixStatus === ixStatus;
    });

    if (status) {
      return stepExtractor(status.ixCategory)(status);
    }
    return null;
  }

  function getSteps(ixCategory) {
    return $.map(DB.Status, stepExtractor(ixCategory)).sort(sortByStep);
  };

  function moveToStep(event) {
    if ($('#sEventEdit').length) {
      alert('Cannot navigate between steps while editing the case!');
      return;
    }
    var ixStatus = $(event.target).data('ixStatus');
    TabManager.clickChangeView(null, 'edit');
    var droplist = $('#ixStatus');
    droplist.val(ixStatus).change();
    DropListControl.refresh(droplist[0]);
    $('#Button_OKEdit').click();
  }

  function updateUI(steps, currStep, nextStep) {
    $('#BugBreadcrumbs ul.breadcrumbs').remove();
    $('ul.toolbar.buttons a.next').parent('li').remove();

    function decorateStepElement(el, step) {
      el
        .text(step.label)
        .data('ixStatus', step.status.ixStatus)
        .attr('title', 'Click to move to this step...')
        .click(moveToStep);
      return el;
    }

    var list = $('<ul>').addClass('breadcrumbs');

    $.each(steps, function(ix, step) {
      var link = decorateStepElement($('<li>'), step);
      if (currStep && currStep.step === step.step) {
        link
          .addClass('current')
          .attr('title', 'Currently working on this step')
          .unbind(); // no click handler
      }
      link.appendTo(list);
    });

    if ($('#BugBreadcrumbs > a.vb').length) {
      // add a little spacing if case hierarchy breadcrumbs are present
      list.css('margin-top', '0.5em');
    }

    list.appendTo('#BugBreadcrumbs');

    // add a new toolbar option to communicate moving to the next status
    if (nextStep !== null && !$('#sEventEdit').length) {
      var button = decorateStepElement($('<a>'), nextStep);
      button
        .addClass('actionButton2 icon-left next')
        .appendTo('ul.toolbar.buttons')
        .wrap('<li>');
    }
  }

  function statusSteps() {
    // this customization only applies to the case page
    if (!$('#bugviewContainer').length) return;

    //make sure we're dealing with an open and active case
    if (goBug.fResolved || !goBug.fOpen) { return; }

    var steps = getSteps(goBug.ixCategory);
    // we need at least 2 steps for the nav bar to make sense
    if (!steps.length || steps.length === 1) { return; }

    var nextStep = null;
    var currStep = getCurrentStep(goBug.ixStatus, goBug.ixCategory);
    if (currStep) {
      var currStepIndex = steps.firstMatchIndex(function(s) { return s.step === currStep.step; });
      if (currStepIndex !== undefined && currStepIndex + 1 < steps.length) {
        nextStep = steps[currStepIndex + 1];
      }
    }

    updateUI(steps, currStep, nextStep);
  }

  statusSteps();

  // run our code when the view changes without a page refresh
  $(window).on('BugViewChange', statusSteps);

css:

#BugBreadcrumbs .breadcrumbs {
  overflow: hidden;
  margin-left: -2px;
}
#BugBreadcrumbs .breadcrumbs li {
  float: left;
  padding: 5px 0 5px 30px;
  background: #eee;
  color: #999;
  position: relative;
  display: block;
  cursor: pointer;
}
#BugBreadcrumbs .breadcrumbs li:after {
  content: "";
  display: block;
  width: 0;
  height: 0;
  border-top: 30px solid transparent;
  border-bottom: 30px solid transparent;
  border-left: 15px solid #eee;
  position: absolute;
  top: 50%;
  margin-top: -30px;
  left: 100%;
  z-index: 2;
}
#BugBreadcrumbs .breadcrumbs li:before {
  content: "";
  display: block;
  width: 0;
  height: 0;
  border-top: 30px solid transparent;
  border-bottom: 30px solid transparent;
  border-left: 15px solid white;
  position: absolute;
  top: 50%;
  margin-top: -30px;
  margin-left: 2px;
  left: 100%;
  z-index: 1;
}

#BugBreadcrumbs .breadcrumbs li:first-child {
  padding-left: 10px;
  border-top-left-radius: 3px;
  border-bottom-left-radius: 3px;
}

#BugBreadcrumbs .breadcrumbs li:hover {
  background-color: #ddd;
  color: #666;
}
#BugBreadcrumbs .breadcrumbs li:hover:after {
  border-left-color: #ddd;
}

#BugBreadcrumbs .breadcrumbs li.current {
  cursor: default;
  color: #545a53;
  background-color: #e0f1df;
}
#BugBreadcrumbs .breadcrumbs li.current:after {
  border-left-color: #e0f1df;
}
#BugBreadcrumbs .breadcrumbs li.current:hover {
  background-color: #c0efbd;
  color: black;
}
#BugBreadcrumbs .breadcrumbs li.current:hover:after {
  border-left-color: #c0efbd;
}

a.actionButton2.next {
  cursor: pointer;
}