Extending FogBugz via GreaseMonkey

Tuesday, April 25, 2006 by Michael H. Pryor

This is a shout out to all the Web 2.0 kids.  If you like writing JavaScript and you want FogBugz to show some cool integration with whatever other systems you have, I have a neat trick for you.

The premise is after a FogBugz page loads, some JavaScript that you've written runs and uses the info on the case to send requests to your server.  You hack up a PHP or ASP or .NET script on your shop server, or your customer server to send back some interesting info to FogBugz, which you then display right inline in the page.

For example, here at Fog Creek, we get emails from our customers every day.  It helps to know whether that user has a support contract, what their orders numbers are, if they are currently in a trial, how long until that trial expires, etc.  Sure, we can look that information up on our internal control panel, but it would be nicer if it was all displayed inline anytime I looked at a case from that customer.

You have two choices to accomplish this.  You can download GreaseMonkey and install it on your machine and code up a GM script to do your dirty work.  The advantages are no code changes to FogBugz so you can run the upgrades without any problem, and if you need to send AJAX requests to a different machine than FogBugz is on, GM will let you do it.  The disadvantage is GreaseMonkey requires Firefox and any computer you want to use it, has to install the script you wrote. You can also write your own JavaScript file and modify FogBugz to include your file and run a function when the page loads.  The advantage here is one change on the server works for everyone.  The disadvantage is you have to reapply the change when you upgrade FogBugz.  Also, the built in AJAX object in JavaScript will only allow you to send requests back to the machine that FogBugz is on for security reasons.

Since only our support staff needed the extra info I added to FogBugz, I chose to use GreaseMonkey. 

The GreaseMonkey XML request function (the code that makes AJAX work) can be called like so:

  GM_xmlhttpRequest({
   method:"GET",
   url:"http://fogcreek.com/controlpanel/remote.aspx?pre=preTrial&sDomain=" + s,
   onload:insertTrialInfo
  });
 

More info on GM_xmlhttpRequest can be found here.

The 3 args are method, url, and onload. You can leave method as "GET".  url should be set to a script that returns the info you need from your server.  In our case, I wrote a small remote.aspx script which takes the emailer's domain name and does some lookups in the trial system to find their trial info.  The onload argument is the name of the function to call when this request returns.  You can also just write the onload function right there in the GM_xmlhttpRequest call.

  GM_xmlhttpRequest({
   method:"GET",
   url:"http://fogcreek.com/controlpanel/remote.aspx?pre=preOrders&sDomain=" + s,
   onload:function (responseDetails)
   {
    if ( responseDetails.responseText.length > 0 )
     insertRow("Orders: " + responseDetails.responseText);
   }
  });

Here I'm calling a function I wrote called insertRow which parses the HTML from FogBugz 4.0.35 and inserts some info on the right hand side of the page.

function insertRow( s )
{
 if ( s.length > 0 )
 {
  var oBox = document.getElementById("gmbox");
  if ( oBox )
  {
   oBox.innerHTML += "<br />" + s;
  }
  else
  {
   var tags = document.getElementsByTagName("label");
   for(var i=0; i<tags.length; i++)
   {
    if ( tags[i].innerHTML == "Correspondent:" )
    {
     var par = tags[i].parentNode.parentNode;
     if ( par.className == "middleLeft" )
     {
      var theTable = par.parentNode.parentNode;
      var oNode = theTable.cloneNode(false);
      var tablePar = theTable.parentNode;
      tablePar.insertBefore( oNode, theTable );
      oNode.setAttribute("style", "position:absolute; z-order: 1; top: 200px; left: 775px");
      oNode.innerHTML = "<tr><td id=\"gmbox\">" + s + "</td></tr>";
      break;
     }
    }
   }   
  }
 }
}

Another useful trick is if you want the information you show to actually do something when you click on it, you can add JavaScript functions to the global namespace.  For example, as part of the info I send back and display in the browser, I added some links so our support staff can extend a customer's trial by 15, 30, or 45 days.

I wrote this to the browser:

"<a href=\"javascript:extend('" + trialID + "', 30)\">30</a>"

which comes out to something like this in the browser

<a href="javascript:extend('12345ABCD', 30)">30</a>

The only problem is I need to make that extend function available to the global namespace so when the staff member clicks the 30 link, it can do its job.

unsafeWindow.extend = function ( trialID, numDays )
{
 GM_xmlhttpRequest({
  method:"GET",
  url:"http://fogcreek.com/controlpanel/remote.aspx?pre=preExtend&idTrial=" + trialID + "&nDays=" + numDays,
  onload: function(responseDetails) {
   var rg = document.evaluate( "//span[@exp]", document, null, XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE, null );
   for ( var i=0; i<rg.snapshotLength; i++ )
   {
    if ( rg.snapshotItem(i).getAttribute("exp") != null && rg.snapshotItem(i).getAttribute("exp") == trialID)
    {
     rg.snapshotItem(i).innerHTML = responseDetails.responseText;
    }
   }
  }
 });   
}
 

This assigns the function "extend" to the GreaseMonkey global object "unsafeWindow" which allows those scripts to be called.  Here's more info on the document.evaluate funkiness which allows you to loop over certain elements in the DOM.

I'll be taking any questions over here on the discussion forum.

Categories: FogBugz
Tags: ,
Actions: E-mail | Permalink