Please note: BugMonkey is only supported in FogBugz For Your Server. If you are using FogBugz On Demand, BugMonkey is not supported in FogBugz Ocelot. In addition, BugMonkey does not reflect the strategic direction of FogBugz development, and we’d discourage developing new scripts.
BugMonkey is a FogBugz plug-in that allows you to customize your own FogBugz to suit your needs. The name comes from the popular GreaseMonkey extension for FireFox. You can find a bunch of BugMonkey scripts by visiting the BugMonkey Script Archive post.
You can add and manage BugMonkey scripts by going to My Settings > Customizations within FogBugz.
Click “New Customization…” to create a new script. Give it a name and description and fill in the JS and CSS sections. Make sure you save it and also enable it for your user. You can also enable scripts that others have shared in your FogBugz instance in the “Your Available Customizations” list.
Site administrators can configure the plugin in Admin > Plugins to set who can create customizations and who can import customizations from this site.
Site admins can edit any customization regardless of who created it, and can set whether it is on or off by default for certain users, or is required.
"Out, spam'd email! Out, I say!" — Lady Macbeth, if she worked a service desk
Spam has become a fact of life when you deal with large amounts of email. Fortunately, so have “Mark as Spam” buttons which makes the process of dealing with those items that sneak through filters that much easier. Before you celebrate too much, it might be helpful to know more about what this button does.
For projects whose mailboxes have Autosort enabled
Clicking the Spam button will move the case to the Spam area. This is required as part of the Autosort engine training. Mail in the Spam area will help identify future spam emails and sort them appropriately.
For sites using Manual sort, there is no need to move the mail to a new area. The Autosort engine does not perform any training. Instead, the email will be Resolved as “Resolved (SPAM)” and Closed.
In either case
By default, spam will be deleted after 7 days. This is controlled by the “Delete spam after” setting and can be disabled if you’d rather have the cases stay in your FogBugz account forever.
Deleting spam email is part of the daily cleanup tasks. When the cleanup task for spam emails runs, it first checks to see if the mailbox that received the message is configured to delete spam and if it is in the relevant Spam area or has the status “Resolved (SPAM)”. Cases that have been forwarded or replied to become ineligible for deletion as a spam case.
Any email that qualifies for spam deletion then gets its age checked. For projects with Autosort, age is considered the time from when the case was originally opened. For emails in manually sorted projects, age is considered from the date the case was closed. Remember, Autosort projects move spam to the Spam area; they don’t automatically close the case as well. Once a case’s age has exceeded the “Delete spam after” setting (measured in days), it will be deleted from the system.
Still have questions? Contact us, and we’ll be happy to help.
If you are using FogBugz On Demand or FogBugz On Site, then you may notice that many features that were previously provided by plugins are now being integrated into the core FogBugz product! You can read more about the progress of this integration on our FogBugz Ocelot page.
What are plugins?
Plugins are modules (.NET assemblies to be precise) which run inside of FogBugz to change or add to the look and feel and functionality of the application. The robust plugin architecture allows you to make changes to all areas of FogBugz: cases, wikis, discussion groups, email handling and time tracking. Plugins can add all new pages and tools to FogBugz or change existing interfaces such as the case view. Plugins can even create new APIs to allow connections to other applications.
Where do I get plugins and how do I install them?
Custom plugins are only available for FogBugz for your Server. FogBugz on Demand (except Ocelot!) has access to a pre-approved list of plugins which can be disabled/enabled by going to Admin -> Plugins.
If you are running FogBugz for your Server, then you can find custom plugins for your server by browsing the Plugin Gallery. You can also install one locally from a different source. If you don’t find the functionality you need, you might try writing your own in your favorite .NET language.
FogBugz comes many great plugins already installed (there are many more in the Plugin Gallery):
Custom Workflow Customize case categories, statuses and assignment
Project Backlog Use FogBugz in a Agile/Scrum development environment
URLTrigger Send custom HTTP requests triggered by FogBugz events
Wiki Table of Contents Insert a dynamically-updating table of contents into wiki pages
Enabling and Disabling
Plugins can be enabled or disabled by FogBugz administrators by clicking the “yes” or “no” link in the Enabled column on the plugins admin page, accessible by clicking Plugins from the Admin menu. Disabling a plugin preserves its data in the FogBugz database in case you need to enable it again later.
Configuring Plugins
Once installed, plugins are configured via the “Configure” icon next to their names on the Plugin admin page. The icon only appears next to plugins that have configurable options.
Note: The “tagrange” search axis and Kiln filters are not available for FogBugz Ocelot.
Here’s how to find FogBugz cases that have been closed between associated tags in Kiln!
Part of our new integration between Kiln and FogBugz is a FogBugz search axis which lets you specify two Tags from Kiln. Let’s say you have two Changesets tagged Build_1.0.3 and Build_1.0.4 simply search for
tagrange:"Build_1.0.3..Build_1.0.4"
The result set will be all cases which have been completed between those two tags.
The exact workings of this axis are as follows. If you search for tagrange:"tagA..tagB" first we will find every repository that contain both tags. Then we return all cases such that every associated changeset is an ancestor of tagB and at least 1 changeset is not an ancestor of tagA. Another way to think of this is: Bugs with changesets after tagA with no changesets after tagB. This set of cases is the set of cases which were completed between the two tags.
There is a corresponding filter “kiln – completed between” which returns an identical result set.
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!
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.
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.
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).
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”!
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
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'});
}
});
}
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.
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> | ')
.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.
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 /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;
}
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:
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.
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:
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("<pre>", "<pre>")
.replace("</pre>", "</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.
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: 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:
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:
Make sure you edit the rules for this customization to make sure it’s required for all community users.
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).
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.
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 anythingif(!$('#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: 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);
});
});
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;
}
If you add javascript to the page and just call it, it will run on every page-load. If you want to limit to just case edit, or assign, or another, use this code:
$(function(){
// if we're not on the case page, don't do anything
if (!$('#bugviewContainer').length) return;
var myFunction = function(sCommand) {
// you can put a conditional in here if you only want to
// run your code for certain modes by looking at sCommand.
// the possible values are:
// load, view, edit, assign, reply, forward, resolve,
// close, reactivate, reopen, new
// on initial page load, sCommand will be 'load'
// on viewing a case after canceling an AJAXy action, e.g.
// canceling an edit, sCommand will be 'view'
// on page load of the new case page, sCommand will be 'new'
};
if ($('#sEventEdit').length > 0)
{
myFunction('new');
}
else if ($('#sEventReply').length > 0)
{
myFunction('reply');
}
else if ($('#sEventForward').length > 0)
{
myFunction('forward');
}
else {
myFunction('load');
}
// run it when the view changes and pass in the new view:
$(window).on('BugViewChange', function(e, data) {
myFunction(data.sCommand);
});
});
To help figure out what commands are happening, put this as the first line inside of myFunction():
console.log(sCommand);
If you find that your code (myFunction) is not firing on some or all types of page-loads, you can add a delay:
$(function(){
// if we're not on the case page, don't do anything
if (!$('#bugviewContainer').length) return;
var myFunction = function(sCommand) {
// you can put a conditional in here if you only want to
// run your code for certain modes by looking at sCommand.
// the possible values are:
// load, view, edit, assign, reply, forward, resolve,
// close, reactivate, reopen, new
// on initial page load, sCommand will be 'load'
// on viewing a case after canceling an AJAXy action, e.g.
// canceling an edit, sCommand will be 'view'
// on page load of the new case page, sCommand will be 'new'
// run after a delay (in milliseconds)
setTimeout(doSomeStuff, 100);
};
var doSomeStuff = function() {
// do your work here
}
if ($('#sEventEdit').length > 0)
{
myFunction('new');
}
else if ($('#sEventReply').length > 0)
{
myFunction('reply');
}
else if ($('#sEventForward').length > 0)
{
myFunction('forward');
}
else {
myFunction('load');
}
// run it when the view changes and pass in the new view:
$(window).on('BugViewChange', function(e, data) {
myFunction(data.sCommand);
});
});
The “Site Timezone” is the default timezone for your FogBugz account. Generally, this should be set to the timezone that the majority of your users live in.
To set the “Site Timezone”, just go to Admin (In Kiln, click the picture of your Gravatar at the top right) > Site Configuration,
FogBugz -> Kiln ->
then click on the “Regional” tab
and then select the desired time-zone and click OK!
Overriding the Site Timezone – Setting your User Timezone
If you are one of the unlucky users that does not fall into the same timezone as your Site Timezone, then you will probably want to override this setting so that you can see all event messages with time stamps that make sense to you!
To set your User Timezone, just go to My Settings -> Options and change the value for the Time Zone setting. You should now see all events’ timestamps displayed relative to your timezone.