Outlook Add-In for Replicon

My employer uses Replicon to track hours against projects and tasks. Its web-based user interface is…adequate. Entering time in an online calendar is fine but I already have an electronic calendar open all day: Microsoft Outlook.

I started a project to create an add-in that adds an “Add to Replicon” menu item to the Outlook calendar. The Replicon Repliconnect API is used to get project and task information and to send the timesheet information back to the server.

It’s still a work in progress. The initial code is up here.

TFS Colorizer Extension for Firefox

tfspreview

At least one of my colleagues uses Firefox, so I created the TFS colorizer as a Firefox extension.

The extension uses the new Firefox Add-On SDK.  Specifically, the extension uses the page-mod API to detect when a TFS board-like page is being displayed and then applies modifications to it.  I found it more difficult to create the extension for Firefox than I did for Chrome.  There were two tricky parts:

Options

There is no easy way to create a nice Options page with the Add-On SDK.  There’s a way to create an ugly one: the simple-prefs API.  It can list out some settings easily, but that’s not what I want.

In the end, I had to create a toolbar button and have that launch my options page.  This code sets up the button in the action bar and opens the options page when it’s clicked.  There’s some code in there to make sure the tab is only ever opened once.

var button = buttons.ActionButton( {
	id: "colorizer-options",
	label:	"TFS Colorizer",
	icon: {
		"16": "./icon_16.png",
		"32": "./icon_32.png",
		"64": "./icon_64.png"
	},
	/**
	 * This is called when our toolbar button is clicked.
	 */ 
	onClick: function ( state ) {
		try {
			// Get the options URL
			var url = self.data.url( "options.html" );
			// For each open tab...
			for ( var i = 0; i < tabs.length; i++ ) {
				var tab = tabs[i];
				// If this is our options tab...
				if ( tab.url == url ) {
					// Activate the tab
					tab.activate();
					// We're done
					return ;
				}
			}
			// We didn't find the options page open, above, so open it.
			tabs.open( url );
		} catch ( e ) {
			console.error( e.message );
		}
	}
});

I would have preferred that this show up in the Add-Ons “Options” sections since my extension doesn’t really require a button cluttering up the toolbar, but I didn’t see a better way.

Accessing Storage

Getting the user’s settings in and out of storage was also a trial.  In Firefox add-ons, you have two types of javascript files:

  • lib/main.js: the main entry point to the add-on.  This has access to the add-on APIs, such as the ones that store data, but does not have access to the browser page.
  • data/*.js a.k.a. content scripts: These have access to the browser page but they don’t have access to the APIs.

So you have a script that can load and store your user settings but won’t affect the page and a script that runs on the page but can’t access the user settings.  How to communicate between the two?  What I ended up using, and this was from the documentation, was the “port.on” and “port.emit” functionality to pass data between the two in a request/response model.  When the content page wanted to load settings from file, it would make a request via:

// Load settings
self.port.emit( "loadSettingsRequest", null );

Main.js would catch that request and issue a response:

pageMod.PageMod( {
	include: self.data.url( "options.html" ),
	contentScriptFile: [
		self.data.url("jquery-2.1.0.min.js"),
		self.data.url("common.js"),
		self.data.url("options.js")
	],
	contentScript: "TfsOptions.initialize();",
	onAttach: function (worker) {
		worker.port.on( "loadSettingsRequest",  function () {
			// Pull the settings from storage
			var json = ss.storage.settings;
			// Parse the json
			var settings;
			if ( null != json ) {
				settings = JSON.parse( json );
			} else {
				settings = null;
			}
			// Send the response
			worker.port.emit( "loadSettingsResponse", settings );
		} );

The content page would catch the response and take action:

self.port.on( "loadSettingsResponse",  function ( settings ) {
	try {
		// If the settings are null, use the defaults
		if ( null == settings ) {
			settings = TfsDefaults.defaultSettings;
		}
		// Populate the table with the settings
		TfsOptions.populateTable( $table, settings );
	} catch( e ) {
		console.error( e.message );
	}
} );

Note that in order to use the port.on/emit code, the content script uses “self” and the main script uses “worker”.  With these two things worked out, the rest was pretty straightforward and I was able re-use most of the existing code.

The code for the FireFox add-on is up on github.  I’m trying to get the extension into the Mozilla add-ons repository.  It’s currently undergoing their review process.