Cards are the new Issues

I’m going to start taking this blog in a slightly different direction from now on. Up to now I’ve been giving a step-by-step account of development of the PixelBugs project. Since my last post, I’ve made quite a few changes and to describe them all in detail here would take up too much of my very limited time. So from now on I’ll be highlighting important changes, decisions and interesting snippets of code rather that the full commentary. Of course the source is still all available on Google Code as before so you can always check it out. I’ll still be developing in the same TDD way:

  1. Write tests which may not compile
  2. Write the minimal amount of code so the solution builds but tests fail
  3. Add the implementation by any means necessary so the tests pass
  4. Refactor the code to remove duplication and improve design with tests still passing

This is know as red-green-refactor. So on that note, what’s changed since last time?

Cards
The first major change is the move to cards rather than issues. The reason for this is that we have just started to introduce agile development at work at I’m really happy with the way that is going. So I have decided to turn this project into an agile project management tool rather than a traditional issue tracker. I’m not sure what that means for the name, but we’ll see what happens.

So to introduce cards I had to refactor the existing tests that used issues then refactored the issue domain/service with their new names. That was a pretty straight forward and all tests passed after this was done.

Next I extended the domain to include things like card status, priority, type and comments. I also added some other properties for things like story points:

Once these extra properties were added I had to refactor the new card view to have status, priority and type selects and a new story points input box. In addition to this, I refactored the card index view to display cards in lanes by status just like a card wall in real life might be (not styled yet):

I’ve added a show (details) page for a card when it is clicked on the wall and also a new card editing view. I found one interesting bug when editing an existing card. There is a select for the card owner that has an option added to the start of the list so that you can select “No Owner”. To do this I used the firstoption parameter of the $FormHelper.Select method. This worked fine if you are setting it to a user, but if you set a card owner to “No Owner” from a user, it never updated. I tracked this down to the fact that I’m using Guid’s as my primary keys and the default value for the firstoption is 0 (zero). Now because the databinder doesn’t know what to do in this case it doesn’t change the owner value even though I had AutoLoadBehavior.NullIfInvalidKey set. The fix was pretty easy, just add a firstoptionvalue parameter and set it to empty string. This equates to null when databinding and so fixes the bug. Here is the NVelocity:

<div class="control">
    $Form.LabelFor("card.owner.id", $strings.Labels_OwnedBy)
    $Form.Select("card.owner.id", $users, "%{value='Id', text='FullName', firstoption='$strings.Options_NoOwner', firstoptionvalue=''}")
</div>

jQuery in, prototypejs out
In the past and for the start of this project I used prototypejs which is a great JavaScript library. I’ve been hearing a lot of positive things about jQuery and so I thought this was the perfect project to give it a go. So I’ve removed prototypejs in favour of jQuery and refactored the small amount of JavaScript which was very simple. The first piece of jQuery worth sharing here is the drag and drop functionality I’ve added to the card wall. This enables the user to drag a card from one lane to another and when the card is dropped an Ajax call is made which updates the card status. Here is the code:

$("div.card").draggable({
    helper: 'clone',
    ghosting: true,
    opacity: 0.5,
    fx: 300,
    handle: 'div.handle'
});
$(".lane").droppable({
    accept: "div.card",
    hoverClass: "droppableHover",
    drop: function(ev, ui) {
        //Show the wait animation
        $("#wait").show();
        //Get the id of the card and the new status id
        var card = ui.draggable[0];
        var statusId = this.id;
        //Use ajax to update the card status
        jQuery.ajax({
            url: 'UpdateStatus.ashx',
            type: 'POST',
            data: { cardId: card.id, statusId: statusId },
            success: function(data) {
                card.setAttribute("status", statusId);
                $("div.card").each(function() {
                    $("#" + this.getAttribute("status")).append(this);
                });
                $("#wait").hide();
            },
            error: function() {
                $("#wait").hide();
                alert("Unable to change the card status");
            }
        });
    }
});

So how does this work? Well, it’s actually very straight forward. The first block simply initialises the cards to be draggable. This is done with the CSS selector “div.card”, we then define what should happen when dragging: we’d like to drag a clone rather that the actual card div, we’d like it to be 50% transparent and we’d like to only drag from a handle (not the whole div). The reason for the handle is to fix the conflict between clicking a card to see its details and clicking a card to start the drag.

The second block deals with where the cards can be dropped. So we use another CSS selector to define the lanes a droppable. We specify that they will only accept cards and the CSS class to add when hovering over a lane. Then, when we drop a card we show the wait animation and fire an Ajax call to an action in the controller to update the card status. If this succeeds we move the card in the UI, if it fails we leave the card where it is and show an error message.

WYSIWYG editor
The next change was to refactor the new and edit views to remove some duplication. The main part of the form is the same for both views, so I’ve brought that out into it’s own partial view and then use this in both the new and edit views.

Now that we have removed this duplication I wanted to add a WYSIWYG editor instead of the standard textarea control. I looked into TinyMCE but this seemed to be really heavyweight compared to my current needs. After some more searching I found NicEdit which has all the features I currently need and is very lightweight. To add NicEdit to our existing textarea we just use the following:

bkLib.onDomLoaded(function() {
    new nicEditor({
        iconsPath: '$siteroot/Content/js/nicEdit/nicEditorIcons.gif',
        buttonList: ['bold','italic','underline','forecolor','left','center','right','ol','ul','fontSize','fontFamily','fontFormat','indent','outdent']
    }).panelInstance('card_body');
});

This code allows us to specify where the icons are located and we can pick which buttons display, very easy to setup and configure.

Live Writer
You may have noticed that I’m not using screenshots for code in this post. That’s because I’ve switched to using Microsoft Live Writer which has a plug-in for formatting code snippets. This has really helped and has reduced the amount of time I spend writing posts which means I can spend more time coding, wahoo!

That’s for for today, except to say that we now have 53 passing tests and 100% code coverage in our web and core assemblies. PixelDragons.Commons has no test coverage at the moment and that’s because this was created before I started TTD. As I touch Commons, I’ll add tests to rectify this. As usual the full source code is available on Google Code:

Url: http://code.google.com/p/pixelbugs/
Svn Revision: r19

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google+ photo

You are commenting using your Google+ account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

%d bloggers like this: