3

3

We have a workflow within our organization where bugs from from:

Dev > QA > Ready for Release

Is there a way to encourage that workflow within FogBugz? I see that the workflow plugin allows you to associate statuses with users, but how about encouraging a workflow to a next status rather than a particular user?

flag

1 Answer

10

Yes! While the workflow plugin doesn't support this out of the box, you can use conventions in your statuses and a sweet BugMonkey script to give users a single button to push to move their case to the next status. Here's what the finished result looks like:

screenshot of the finished product

The key is, you need to set up multiple Active statuses using the following convention (you'll need to do this for each type of status e.g. Bug, Inquiry etc.):

Active
Active (1. Dev)
Active (2. QA)
Active (3. Ready to Ship)

For each status, the 'next' status is one with the next highest number. That's why it's important to use the convention above e.g. Active (#. Desc). You can optionally have one default Active status, which is set up to automatically go to the 1 item in the list.

Also worth noting, in true FogBugz fashion, we're not preventing users from being able to choose a different status via the traditional Edit dialog, but we are making it super easy for them to choose the next expected action.

Here's the code:

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;
}
link|flag
Great CSS trick with the arrows! – Michel de Ruiter Sep 22 at 21:29
Dane's pretty slick like that :-) – Ben McCormack Sep 24 at 13:30
Very nice. I like it. – Nick Sep 27 at 13:16
This works great. There is just one thing it doesn't seem to do... When you select a status, it doesn't automatically assigned it to the correct person based on your workflow. Would this be possible in the bugmonkey script? – Nick Oct 12 at 21:42
1 
@Nick thanks for trying that. I had a minute to look at this script this morning and it looks like we weren't calling .change() on the droplist, which is required to trigger the assigned-to to update. I updated the script with the change and it should work now. – Ben McCormack Oct 16 at 14:30
show 9 more comments

Your Answer

Not the answer you're looking for? Browse other questions tagged or ask your own question.