Using FogBugz On Demand? We've recently rolled out a new sidebar as part of taking FogBugz forward. Please see this article for details on what's new, what's changed, and where you can find all your favorite things.
FogBugz On Demand and On Site allow you to add “customizations” to change the look and feel and even the behavior of the application. This post describes how to enable and work with customizations in your account. While many FogBugz users like to make their own special changes to the app, we have found that many users have similar needs. Therefore, we maintain this list of customizations in the hopes that one or more of them might be useful for you! We also offer a template which makes handling case-page transitions easier.

The FogBugz user interface, including global javascript objects and DOM elements, can and does change without warning. We intend to keep these scripts updated to work with the current version of FogBugz On Demand; if we’ve missed something, please let us know!

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 and Parent case links on case view
description: Adds links to create a sub- or parent-case right from a case view
author: Adam Wishneusky, Chad McElligott, Michel de Ruiter
version: 2.0.0.0

js:

$(function(){
 var isOcelot = function() {
  return (typeof fb.config != 'undefined');
 };
 // if this isn't Ocelot, and we're not on the oldbugz case page, don't run
 if (!isOcelot() && !$('#bugviewContainer').length) {
  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 makeButtonHtml(sLinkUrl, sLinkText, sClass, fIsOcelot) {
  var sHtml = "";
  if (fIsOcelot) {
   sHtml = '<a class="control ' + sClass + '" name="' + sClass + '" href="' + sLinkUrl + '"><span class="icon icon-' + sClass + '"></span>' + sLinkText + '</a>';
  }
  else {
   sHtml = '<li><a class="actionButton2 icon-left ' + sClass + '" href="' + sLinkUrl + '">' + sLinkText + '</a></li>';
  }
   return sHtml;
 }

 function addButtons(fIsOcelot) {
  // array of buttons jQuery finds, using a different selector for each UI
  var buttons;
  // the bug JS object is goBug in the old UI and fb.cases.current.bug in Ocelot
  var bug;
  // tags differ in the two UIs: bug.tags in Ocelot and goBug.ListTagsAsArray() in the old UI
  var rgTags;
  // when using url parameters to populate case form fields on page-load, Ocelot and the
  // old UI differ in one or more. In this instance, the tag list is "tags" in Ocelot
  // and "sTags" in the old UI
  var sTagParamName;
  // based on the UI, remove the existing buttons and set the above vars
  if (fIsOcelot){
   $('a.control.addsubcase,a.control.addparent').remove(); // Remove existing buttons
   buttons = $('span.controls');
   bug = fb.cases.current.bug;
   rgTags = bug.tags;
   sTagParamName = "tags";
  }
  else {
   $('.icon-left.addsubcase,.icon-left.addparent').remove(); // Remove existing buttons
   buttons = $("ul.buttons");
   bug = goBug;
   rgTags = goBug.ListTagsAsArray();
   sTagParamName = "sTags";
  }
  var sLinkStart = '/default.asp?command=new&pg=pgEditBug' +
   '&ixCategory=' + bug.ixCategory +
   '&ixProject=' + bug.ixProject +
   '&ixArea=' + bug.ixArea +
   '&ixFixFor=' + bug.ixFixFor +
   '&ixPersonAssignedTo=' + bug.ixPersonAssignedTo +
   '&sCustomerEmail=' + encodeURIComponent(bug.sCustomerEmail) +
   '&ixPriority=' + bug.ixPriority +
   '&' + sTagParamName + '=' + encodeURIComponent(rgTags) +
   '&sEvent='; // To be updated dynamically.
  var sButtonsHtml = makeButtonHtml(sLinkStart + '&ixBugParent=' + bug.ixBug + '&b=c',
   "Subcase",
   "addsubcase",
   fIsOcelot);
  // ixBugChildren in the request doesn't put the value in the subcases box in Ocelot
  // like it did in the old UI 🙁
  if (bug.ixBugParent == 0) {
   sButtonsHtml += makeButtonHtml(sLinkStart + '&ixBugChildren=' + bug.ixBug + '&b=c',
    "Parent",
    "addparent",
    fIsOcelot);
  }
  buttons.prepend(sButtonsHtml);
 }

 // this needs to be idempotent. in ocelot, we don't know on a full page
 // load if it was run on the /nav/end event so we run it again after subscribing
 var runThisWhenCasePageModeChanges = function(sCommand, fIsOcelot) {
  if (sCommand == 'load' || sCommand == 'view') {
   // show the buttons on case view, but not the bulk case view:
   if ($('#bulkBugItems').length > 0) {
    return;
   }
   addButtons(fIsOcelot);
  }
  else {
  // do something on case edit in one of the various modes: edit,
  // assign, resolve, close, reactivate, reopen, open, email, reply,
  // forward, and new
  }
 };

 // ------ set up to call your code when the case view changes -------------------

 // Bug Monkey customizations are run only on full page-load. In ocelot, this only
 // happens when you go direclty to a URL or when you refresh the whole page.
 // Therefore, we just want to subscribe to the navigation event that fires whenever
 // the view is changed, as well as subscribe to any other events we care about
 // and run our code one initial time.
 if (isOcelot()) {
  // /nav/end fires at the end of every single-page-app navigation, e.g. when
  // the list page is done displaying or when the case page is done changing from
  // view to edit mode. the event param contains some useful info e.g.
  // event.route is something like '/cases/234/case-title-here' and event.url
  // has the entire url of the page
  // on the case page, fb.cases.current.sAction is the mode of the page just like
  // in the old UI: view, edit, assign, resolve, close, reactivate, reopen,
  // open, email, reply, forward and new
  fb.pubsub.subscribe({
   '/nav/end': function(event) {
    //console.log("navigated to url " + event.url);
    //console.log("route: " + event.route);
    // if it's the case page, add the onkeyup for the title field and
    // run an initial check of the fields
    if (typeof fb.cases.current.sAction != 'undefined') {
     runThisWhenCasePageModeChanges(fb.cases.current.sAction, true);
    }
    else {
     //console.log('ocelot page but not the case page');
    }
   }
  });
  // depending on timing, if you go directly to a case page, the pubsub might not
  // finish before /nav/end is called the first time, so run the function once
  // after a delay. remember your code should be idempotent
  setTimeout(function() { runThisWhenCasePageModeChanges(fb.cases.current.sAction, true) },100);
 }
 else {
  // the old UI case page is a full page load in many situations, but does use ajax
  // transitions for certain things, like clicking edit while viewing a case or
  // canceling an edit in progress.
  
  // this runs on full page load and determines which action is occurring based on
  // what case-page html elements are present
  if ($('#bugviewContainerEdit textarea').length > 0) {
   if ($('div.ixBug a').length > 0) {
    if ($('#sTo').length > 0) {
     // warning: not localized:
     if ($('#bugviewContainerEdit div.bugevent div.summary span.action:contains("Replied by")').length > 0) {
      runThisWhenCasePageModeChanges('reply', false);
     }
     // warning: not localized:
     else if ($('#bugviewContainerEdit div.bugevent div.summary span.action:contains("Forwarded by")').length > 0) {
      runThisWhenCasePageModeChanges('forward', false);
     }
     else {
      runThisWhenCasePageModeChanges('email', false);
     }
    }
    // warning: not localized:
    else if ($('#bugviewContainerEdit div.bugevent div.summary span.action:contains("Assigned by")').length > 0) {
     runThisWhenCasePageModeChanges('assign', false);
    }
    // warning: not localized:
    else if ($('#bugviewContainerEdit div.bugevent div.summary span.action:contains("Closed by")').length > 0) {
     runThisWhenCasePageModeChanges('close', false);
    }
    // warning: not localized:
    else if ($('#bugviewContainerEdit div.bugevent div.summary span.action:contains("Reactivated by")').length > 0) {
     runThisWhenCasePageModeChanges('reactivate');
    }
    else if ($('#Button_Resolve').length > 0) {
     runThisWhenCasePageModeChanges('resolve', false);
    }
    else {
     runThisWhenCasePageModeChanges('edit', false);
    }
    // add to this: direct link could be to edit or reply or...
   }
   else if ($('#bulkBugItems').length > 0) {
    // bulk action
    runThisWhenCasePageModeChanges('edit', false);
    // add to this: direct link could be to edit or reply or...
   }
   else {
    runThisWhenCasePageModeChanges('new', false);
   }
  }
  else {
   // this is the case view page on a full page-load. The sCommand value
   // on an ajax load is 'view' If you do not need to distinguish
   // the two, change the string 'load' here to 'view'.
   // Note that in Ocelot, the sAction is always 'view' and never 'load'
   runThisWhenCasePageModeChanges('load', false);
  }
  // full page loads are handled above. To handle the ajax
  // transitions, hook into the BugViewChange event:
  $(window).on('BugViewChange', function(e, data) {
   runThisWhenCasePageModeChanges(data.sCommand, false);
  });
 }

 $(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.addsubcase:before,
.icon-left.addparent:before {
 background-position: 0px -374px;
 height: 16px;
 width: 16px;
}
/* plus used in the Iteration Planner */
.icon-addparent:before {
 content: "\e61d";
 color: #80a1bd;
}
.icon-addsubcase:before {
 content: "\e61d";
 color: #80a1bd;
}

#bugviewContainer .buttonbar ul.toolbar.buttons {
 white-space: nowrap;
}

Auto-link a custom field to an external system

This customization looks for a custom text field, grabs the value from it and formats it as a link to another webapp looking up that value. To enable this customization to properly generate the URL, edit the variable linkBase and update the line in the linkTheField function which formats the URL.

name:         Create custom link from text field
description:  turns a specific custom text field into a link to another site
author:       Adam Wishneusky
version:      2.0.0.0

js:

$(function(){
    // note: the bulk of this code (including comments) is based on this case page Customization template:
    // http://help.fogcreek.com/11580/case-page-customization-template
    var isOcelot = function() {
        return (typeof fb.config != 'undefined');
    };
    // if this isn't Ocelot, and we're not on the old UI case page, don't run
    if (!isOcelot() && !$('#bugviewContainer').length) {
        return;
    }
    // configure the field to work with:
    // note: does not work with fields containing single quotes
    // 2.0.0.0 of this Customization is only tested with text-type custom fields containing a int w/ no spaces
    var fieldName = 'Other Ticket #';
    // put the link base here
    var linkBase = "http://some.other.tracker/";

    // in the old UI:
    // in both views, the label containing the field name is inside a div.dialog-item
    // in view mode, the field value text is in a div.content next to the label inside the div.dialog-item
    // in edit mode, the input tag is also in a div.content next to the label inside the div.dialog-item

    // in ocelot:
    // in view mode, the label containing the field name is inside a div.field.customfield.view
    //               the field value text is in a div.content next to the label inside the div.field.customfield.view
    // in edit mode, the label containing the field name is inside a div.contains-customfield
    //               the input tag is inside the label

    var linkTheField = function(fIsOcelot) {
        var customFieldLabelTag = $('label:contains("' + fieldName + '")');
        var customField = customFieldLabelTag.parent().find('div.content');
        if (customField.length == 1)
        {
            var fieldText = customField.text();
            var otherTicketNumber = parseInt(fieldText);
            if (!isNaN(otherTicketNumber))
            {
                customField.html('' + otherTicketNumber + '');
            }
        }
    };

    // this needs to be idempotent. in ocelot, we don't know on a full page
    // load if it was run on the /nav/end event so we run it again after subscribing
    // this was called runLinkConversion in the previous version of the script
    // but changed when I used the 'case page hook.js' as a starting point for a new
    // version with ocelot support
    var runThisWhenCasePageModeChanges = function(sCommand, fIsOcelot) {
        if(sCommand == 'view' || sCommand == 'load' || sCommand == 'email' || sCommand == 'reply' || sCommand == 'forward')
        {
            linkTheField(fIsOcelot);
        }
    };

    // ------ set up to call your code when the case view changes -------------------
    // ------ everything below is straight from the 'case page hook.js' sample code -
    // ------ http://help.fogcreek.com/11580/case-page-customization-template -------

    // Bug Monkey customizations are run only on full page-load. In ocelot, this only
    // happens when you go direclty to a URL or when you refresh the whole page.
    // Therefore, we just want to subscribe to the navigation event that fires whenever
    // the view is changed, as well as subscribe to any other events we care about
    // and run our code one initial time.
    if (isOcelot()) {
        // /nav/end fires at the end of every single-page-app navigation, e.g. when
        // the list page is done displaying or when the case page is done changing from
        // view to edit mode. the event param contains some useful info e.g.
        // event.route is something like '/cases/234/case-title-here' and event.url
        // has the entire url of the page
        // on the case page, fb.cases.current.sAction is the mode of the page just like
        // in the old UI: view, edit, assign, resolve, close, reactivate, reopen,
        // open, email, reply, forward and new
        fb.pubsub.subscribe({
            '/nav/end': function(event) {
                //console.log("navigated to url " + event.url);
                //console.log("route: " + event.route);
                // if it's the case page, add the onkeyup for the title field and
                // run an initial check of the fields
                if (typeof fb.cases.current.sAction != 'undefined') {
                    runThisWhenCasePageModeChanges(fb.cases.current.sAction, true);
                }
                else {
                    //console.log('ocelot page but not the case page');
                }
            }
        });
        // depending on timing, if you go directly to a case page, the pubsub might not
        // finish before /nav/end is called the first time, so run the function once
        // after a delay. remember your code should be idempotent
        setTimeout(function() { runThisWhenCasePageModeChanges(fb.cases.current.sAction, true) },100);
    }
    else {
        // the old UI case page is a full page load in many situations, but does use ajax
        // transitions for certain things, like clicking edit while viewing a case or
        // canceling an edit in progress.
        
        // this runs on full page load and determines which action is occurring based on
        // what case-page html elements are present
        if ($('#bugviewContainerEdit textarea').length > 0) {
            if ($('div.ixBug a').length > 0) {
                if ($('#sTo').length > 0) {
                    // warning: not localized:
                    if ($('#bugviewContainerEdit div.bugevent div.summary span.action:contains("Replied by")').length > 0) {
                        runThisWhenCasePageModeChanges('reply', false);
                    }
                    // warning: not localized:
                    else if ($('#bugviewContainerEdit div.bugevent div.summary span.action:contains("Forwarded by")').length > 0) {
                        runThisWhenCasePageModeChanges('forward', false);
                    }
                    else {
                        runThisWhenCasePageModeChanges('email', false);
                    }
                }
                // warning: not localized:
                else if ($('#bugviewContainerEdit div.bugevent div.summary span.action:contains("Assigned by")').length > 0) {
                    runThisWhenCasePageModeChanges('assign', false);
                }
                // warning: not localized:
                else if ($('#bugviewContainerEdit div.bugevent div.summary span.action:contains("Closed by")').length > 0) {
                    runThisWhenCasePageModeChanges('close', false);
                }
                // warning: not localized:
                else if ($('#bugviewContainerEdit div.bugevent div.summary span.action:contains("Reactivated by")').length > 0) {
                    runThisWhenCasePageModeChanges('reactivate');
                }
                else if ($('#Button_Resolve').length > 0) {
                    runThisWhenCasePageModeChanges('resolve', false);
                }
                else {
                    runThisWhenCasePageModeChanges('edit', false);
                }
                // add to this: direct link could be to edit or reply or...
            }
            else if ($('#bulkBugItems').length > 0) {
                // bulk action
                runThisWhenCasePageModeChanges('edit', false);
                // add to this: direct link could be to edit or reply or...
            }
            else {
                runThisWhenCasePageModeChanges('new', false);
            }
        }
        else {
            // this is the case view page on a full page-load. The sCommand value
            // on an ajax load is 'view' If you do not need to distinguish
            // the two, change the string 'load' here to 'view'.
            // Note that in Ocelot, the sAction is always 'view' and never 'load'
            runThisWhenCasePageModeChanges('load', false);
        }
        // full page loads are handled above. To handle the ajax
        // transitions, hook into the BugViewChange event:
        $(window).on('BugViewChange', function(e, data) {
            runThisWhenCasePageModeChanges(data.sCommand, false);
        });
    }
});


Tidy Case Events

Formerly a FogBugz plugin. Converted to a Customization by Jude Allred.

Minor edits to a case (those with no comment text) can clutter up the case view. This customization hides them and provides a link in the left sidebar to restore them.

name:          Tidy Case Events
description:   Hides all brief edited-by events; adds a link in the left sidebar to restore them.
author:        Fog Creek Software / Jude Allred
version:       2.0.0.0

js:

$(function(){

    
    var isOcelot = function() {
        return (typeof fb.config != 'undefined');
    };

    var runThisWhenCasePageModeChanges = function(sCommand, fIsOcelot) {
        if (sCommand == 'load' || sCommand == 'view') {
            var c = 0;
            $('section.case .event.brief').each(function(){
                var ev = $(this);
                if(ev.find('.action').text().indexOf('Edited') !== -1) {
                    ev.hide();
                    c++;
                }
            });
            if(c > 0) {
                $('#tceLabel').remove();
                $('section.case .left:not(.corner)').append('<label id="tceLabel" class="field">Tidy Case Events<div class="content"><span class="dotted href" id="tceToggle">Show ' + c + ' minor edits.</span></div></label>');
            }
        }
    };

    if (isOcelot()) {

        fb.pubsub.subscribe({
            '/nav/end': function(event) {
                if (typeof fb.cases.current.sAction != 'undefined') {
                    runThisWhenCasePageModeChanges(fb.cases.current.sAction, true);
                }
            }
        });

        $('body').on('click', '.case #tceToggle', function() {
            $('#tceLabel').remove();
            $('section.case .event.brief').show();
        });

        setTimeout(function() { runThisWhenCasePageModeChanges(fb.cases.current.sAction, true) },100);
    }
});

css:

Reply As Me

This customization will change the From name to yours whenever replying to or forwarding an email.

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

js:

$(function() {
  var isOcelot = function() {
    return (typeof fb.config != 'undefined');
  };
  var runThisWhenCasePageModeChanges = function(sCommand, fIsOcelot) {
    if (sCommand == 'reply' || sCommand == 'forward') {
      var fromField = $('#email-from-droplist');
      var dropList = fromField.droplist();
      var currentFrom = dropList.val();
      var newFrom = "";
      for (i=0; i < dropList.config.choices.length; i++) {
        if (dropList.config.choices[i].value == currentFrom && i % 2 == 0) {
          newFrom = dropList.config.choices[i+1].value;
        }
      }
      dropList.val(newFrom);
    };
  };
  if (isOcelot()) {
    fb.pubsub.subscribe({
      '/nav/end': function(event) {
        if (typeof fb.cases.current.sAction != 'undefined') {
            runThisWhenCasePageModeChanges(fb.cases.current.sAction, true);
        }
      }
    });
    setTimeout(function() { runThisWhenCasePageModeChanges(fb.cases.current.sAction, true) }, 100);
  };
});

css:

Show Keyboard Shortcuts/Access Keys in Ocelot

This script will label all of the Access Keys in Ocelot.

name:          Show Keyboard Shortcuts/Access Keys in Ocelot
description:   Show Keyboard Shortcuts (access keys) in Ocelot. Access keys are show in pink text in format of '[a]' where 'a' is the access key for the action or button.
author:        Quentin Schroeder; Derrick Miller
version:       1.0.1.0

js:

function js() {
	init();
	function init() {
		appendAccessKeyCharacter();
		fb.pubsub.subscribe("/nav/end", appendAccessKeyCharacter);
	}
	function appendAccessKeyCharacter() {
		$('[accesskey]').each(function(){
			var title = $(this).attr('title')
			var appendKey = ' [' + $(this).attr('accesskey') + ']';
			if (title && title.indexOf(appendKey) == -1) { // doesn't already have it
				$(this).attr('title', title + appendKey);
			}
		});
	}
}


css:

/* body { background-color: red !important; } */
a[accesskey]:after, button[accesskey]:after, input[accesskey]:after, label[accesskey]:after, legend[accesskey]:after, textarea[accesskey]:after { margin-left: 0.3em; color: Plum; content: "[" attr(accesskey) "]";}

Printer-friendly lightboxes

This script places a convenient “Print” button on top of the lightbox and also works with Cmd/Ctrl-p. Makes FogBugz cases more printer friendly.

name: Print button
description: Print lightboxes nicely. This places a convenient "Print" button at the top of the lightbox but also works with Cmd/Ctrl-p in at least Chrome and Firefox. Does not work when selecting "Print" from the file menu.
author: Daniel Lieberman
version: 1.0.0

js:

var printLightbox = function() {
 const printWindow = window.open(window.location.href);
 printWindow.onload = function() {
 setTimeout(function() {
 const header = printWindow.$('article').find('.top').find('h1');
 const title = printWindow.$(header).text();
 printWindow.$(header).text(fb.cases.current.bug.ixBug + ': ' + title);
 let extraOffset = 0;
 if (printWindow.$('.left')[1].style.width) {
 extraOffset = parseInt(printWindow.$('.left')[1].style.width) - 185;
 }
 printWindow.$('.left')[1].style.width = null;
 printWindow.$('.left').hide();
 printWindow.$('nav').hide();
 printWindow.$('#header').hide();
 printWindow.$('header')[0].style.borderColor = '#fff';
 printWindow.$('header')[1].style.borderColor = '#fff';
 let caseHeaderInfo = printWindow.$('.case-header-info');
 for (let i = 0; i < caseHeaderInfo.length; i += 1) {
 caseHeaderInfo[i].style.borderColor = '#fff';
 }
 printWindow.$('.top')[0].style.borderColor = '#fff';
 printWindow.$('article')[0].style.borderColor = '#fff';
 printWindow.$('header')[0].style.position = 'relative';
 printWindow.$('header')[0].style.right = '90px';
 printWindow.$('header')[1].style.position = 'relative';
 printWindow.$('header')[1].style.right = '90px';
 printWindow.$('.events')[0].style.position = 'relative';
 const eventOffset = 90 + extraOffset;
 printWindow.$('.events')[0].style.right = eventOffset + 'px';
 printWindow.focus();
 printWindow.print();
 setTimeout(function() { printWindow.close(); }, 100);
 }, 1000);
 };
};
$(function(){
 var isOcelot = function() {
 return (typeof fb.config != 'undefined');
 };
 // if this isn't Ocelot, and we're not on the oldbugz case page, don't run
 if (!isOcelot() && !$('#bugviewContainer').length) {
 return;
 }
 // this needs to be idempotent. in ocelot, we don't know on a full page
 // load if it was run on the /nav/end event so we run it again after subscribing
 var runThisWhenCasePageModeChanges = function(sCommand, fIsOcelot) {
 if (!fIsOcelot || $('.print').length) {
 return;
 }
 const isLightbox = !!$('.case-lightbox').length
 if (sCommand == 'load' || sCommand == 'view') {
 const buttonHTML = '<a class="control print" name="print" href="default.asp"><span class="icon icon-print"></span>Print</a>';
 const controls = $('span.controls');
 controls.prepend(buttonHTML);
 $('.print').click( (e) => {
 e.preventDefault();
 printLightbox();
 });
 $(window).keydown( (e) => {
 if ((e.ctrlKey || e.metaKey) && e.keyCode == 80 && fb.cases.current.bug /* check whether we're actually looking at a case */) {
 e.preventDefault();
 printLightbox();
 }
 });
 }
 };
 
 // ------ set up to call your code when the case view changes -------------------
 
 // Customizations are run only on full page-load. In ocelot, this only
 // happens when you go direclty to a URL or when you refresh the whole page.
 // Therefore, we just want to subscribe to the navigation event that fires whenever
 // the view is changed, as well as subscribe to any other events we care about
 // and run our code one initial time.
 if (isOcelot()) {
 // /nav/end fires at the end of every single-page-app navigation, e.g. when
 // the list page is done displaying or when the case page is done changing from
 // view to edit mode. the event param contains some useful info e.g.
 // event.route is something like '/cases/234/case-title-here' and event.url
 // has the entire url of the page
 // on the case page, fb.cases.current.sAction is the mode of the page just like
 // in the old UI: view, edit, assign, resolve, close, reactivate, reopen,
 // open, email, reply, forward and new
 fb.pubsub.subscribe({
 '/nav/end': function(event) {
 //console.log("navigated to url " + event.url);
 //console.log("route: " + event.route);
 // if it's the case page, add the onkeyup for the title field and
 // run an initial check of the fields
 if (typeof fb.cases.current.sAction != 'undefined') {
 runThisWhenCasePageModeChanges(fb.cases.current.sAction, true);
 }
 else {
 //console.log('ocelot page but not the case page');
 }
 }
 });
 // depending on timing, if you go directly to a case page, the pubsub might not
 // finish before /nav/end is called the first time, so run the function once
 // after a delay. remember your code should be idempotent
 setTimeout(function() { runThisWhenCasePageModeChanges(fb.cases.current.sAction, true) }, 100);
 }
 else {
 // the old UI case page is a full page load in many situations, but does use ajax
 // transitions for certain things, like clicking edit while viewing a case or
 // canceling an edit in progress.
 
 // this runs on full page load and determines which action is occurring based on
 // what case-page html elements are present
 if ($('#bugviewContainerEdit textarea').length > 0) {
 if ($('div.ixBug a').length > 0) {
 if ($('#sTo').length > 0) {
 // warning: not localized:
 if ($('#bugviewContainerEdit div.bugevent div.summary span.action:contains("Replied by")').length > 0) {
 runThisWhenCasePageModeChanges('reply', false);
 }
 // warning: not localized:
 else if ($('#bugviewContainerEdit div.bugevent div.summary span.action:contains("Forwarded by")').length > 0) {
 runThisWhenCasePageModeChanges('forward', false);
 }
 else {
 runThisWhenCasePageModeChanges('email', false);
 }
 }
 // warning: not localized:
 else if ($('#bugviewContainerEdit div.bugevent div.summary span.action:contains("Assigned by")').length > 0) {
 runThisWhenCasePageModeChanges('assign', false);
 }
 // warning: not localized:
 else if ($('#bugviewContainerEdit div.bugevent div.summary span.action:contains("Closed by")').length > 0) {
 runThisWhenCasePageModeChanges('close', false);
 }
 // warning: not localized:
 else if ($('#bugviewContainerEdit div.bugevent div.summary span.action:contains("Reactivated by")').length > 0) {
 runThisWhenCasePageModeChanges('reactivate');
 }
 else if ($('#Button_Resolve').length > 0) {
 runThisWhenCasePageModeChanges('resolve', false);
 }
 else {
 runThisWhenCasePageModeChanges('edit', false);
 }
 // add to this: direct link could be to edit or reply or...
 }
 else if ($('#bulkBugItems').length > 0) {
 // bulk action
 runThisWhenCasePageModeChanges('edit', false);
 // add to this: direct link could be to edit or reply or...
 }
 else {
 runThisWhenCasePageModeChanges('new', false);
 }
 }
 else {
 // this is the case view page on a full page-load. The sCommand value
 // on an ajax load is 'view' If you do not need to distinguish
 // the two, change the string 'load' here to 'view'.
 // Note that in Ocelot, the sAction is always 'view' and never 'load'
 runThisWhenCasePageModeChanges('load', false);
 }
 // full page loads are handled above. To handle the ajax
 // transitions, hook into the BugViewChange event:
 $(window).on('BugViewChange', function(e, data) {
 runThisWhenCasePageModeChanges(data.sCommand, false);
 });
 }
});


css:

.icon-print:before {
 content: "\2399";
 color: #80a1bd;
}
#bugviewContainer .buttonbar ul.toolbar.buttons {
 white-space: nowrap;
}


More coming soon!


License

Copyright (c) 2017 Fog Creek Software

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.