Useful Customizations For FogBugz


Follow

Overview

FogBugz allows you to add “customizations” to change the look and feel and even the behavior of the application.

While many FogBugz users like to make their 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! 

Consider this article as a kind-of a repository of FogBugz Customizations.


Introduction

Read first this article: Enabling, Configuring, and Creating New Customizations for FogBugz

Please also note the disclaimer mentioned in the article.


Description

 

Quick-add Subcases and Parent Cases

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
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;
} 

Back to the top


Removing Alert from FogBugz screen: Permanently 

This removes Alerts like expiry notification from the FogBugz page. This piece of code is to be added to the CSS Page. 

 Warning: This will remove all alerts Permanently. To restore the alerts, please remove this part from the CSS.

​.on-site-expiration-notice {
 display: none !important;
}​

Back to the top


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 web app looking up that value. To enable this customization to properly generate the URL, edit the variable linkBase, and update the line in the link. The Field 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
version:      2.0.0.0

js:

$(function(){
    // note: the bulk of this code (including comments) is based on this case page Customization template:
    // https://support.fogbugz.com/hc/en-us/articles/360011344013-Custom-JavaScript-and-CSS-Case-Page-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 -
    // ------ https://support.fogbugz.com/hc/en-us/articles/360011344013-Custom-JavaScript-and-CSS-Case-Page-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);
        });
    }
});

Back to the top


Tidy Case Events

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.
version:       2.0.1

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').not('.has-attachments').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:

Back to the top


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
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:

Back to the top


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.
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) "]";}

Back to the top


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.
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;
}

Back to the top



Additional From: addresses

This customization will add an additional From addresses to your options whenever replying to or forwarding an email.

name:          Additional From Addresses for Emails
description:   Provide additional options for the From: address when sending emails from FogBugz

version:       1.0.0.0

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 choices = fromField.droplist().droplistChooser.choices;
      var emailToAdd = { 
        value: "\"Another Email\" <someone@somedomain.com>",
        text: "\"Another Email\" <someone@somedomain.com>"
      };
      
      choices.push(emailToAdd);
    };
  };
  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:

Back to the top


Purple links for visited cases on the case list page

When developing FogBugz, we try to pay extra attention to accessibility in all forms. This includes making the colors easy to differentiate for users who have trouble visually distinguishing certain colors (including color blindness). If you’d prefer to instead use a color scheme where unvisited links on the case list page are blue and visited ones are purple, this customization will make it so.

Please note: we highly recommend not setting this customization to be required for all your users so that those users who need the more accessible default color scheme can use it.

name: Purple links for visited cases on the case list page
description: Changes the color of visited links to purple

version: 1.0.0.0

js:

// Nothing here


css:

a.grid-title-link.visited.case {
color: #8f09c4 !important;
}
div.grid-column-contents > a.visited.case {
color: #8f09c4 !important;
}

Back to the top


Custom links on the sidebar

This customization adds a new menu to the sidebar that supports any number of custom links, including sorting those links into sections.

name:          Links Menu
description:   Adds a menu to the sidebar with custom links

version:       2.0.0

js:

$((function($){
  if (!fb) { return }
  fb.customizations = fb.customizations || {};
  if (fb.customizations.linksMenu) { return }

  function addLink(section, text, url, func) {
    let linkId = section.toLowerCase().replace(/[^a-z0-9_\-.]/g, '-') + '-' + text.toLowerCase().replace(/[^a-z0-9_\-.]/g, '-');
    if ($('#' + linkId).length) {
      // An item in this section with the same name has already been added to the list
      return;
    }
    let menuSection;
    for (let existingSection of fb.customizations.linksMenu.sections) {
      if (existingSection.header.text.toLowerCase() == section.toLowerCase()) {
        menuSection = existingSection
        break;
      }
    }
    if (!menuSection) {
        menuSection = {header: {text: section}, items: []};
        fb.customizations.linksMenu.sections.push(menuSection);
    }
    menuSection.items.push({text: text, linkId: linkId, href: url});
  }
  fb.customizations.linksMenu = { sections: [], addLink: addLink };
  function getTemplateLists() {
    return fb.customizations.linksMenu.sections;
  }
  
  fb.sidebar._addCustomMenu('Links', getTemplateLists, '#icon-forward');
})(jQuery));

// Add additional calls here to add more links
// Parameters: Section, Link Title, Link URL
fb.customizations.linksMenu.addLink('FogBugz', 'Try FogBugz', 'https://www.fogbugz.com/try-fogbugz' );
fb.customizations.linksMenu.addLink('FogBugz', 'FogBugz API', 'https://support.fogbugz.com/hc/en-us/articles/360011242374-FogBugz-API-Introduction' );

css:

Back to the top


Related Articles