You are viewing an old version of this page. View the current version.

Compare with Current View Page History

« Previous Version 2 Next »

JIRA has a built-in Due Date field. But what happens when you don't know the exact day – you know, at best, the year quarter (Q1–Q4) in which the issue is due.

A client of ours solved this by defining a custom field, showing the quarter (and optionally, year) to which the issue is committed to be delivered in:

 

This works, but the result is a little hard for humans to parse, as there is very little visual distinction between different values:

Solution – an easy-to-visualize 'view'

We solved this by creating a read-only graphical "timeline" view of the field, here seen alongside the text view:

I think you'll agree, it's much easier to see see what's going on. The field begins with the current quarter (it's May 2016 at time of writing, so Q2), and displays 4 quarters into the future. The last issue, with the grey ellipsis, shows an issue committed to before the current quarter (i.e. overdue). A mouseover explains what's going on:

Likewise for issues committed to more than one year in the future.

Implementation

We utilized the indispensable ScriptRunner for JIRA plugin, and created a 'script field', i.e. a field whose value is calculated programmatically. The implementation is as follows:

/**
 * An alternative timeline view of the 'Quarter Commit' field.
 * jeff@redradishtech.com, 4/May/16.
 */
import com.atlassian.jira.component.ComponentAccessor
import com.atlassian.jira.issue.customfields.option.Option
import com.atlassian.jira.issue.fields.CustomField

// ID of the cascading select custom field storing the Quarter and Year.
def QUARTER_COMMIT_CUSTOMFIELDID = 14100;

/** Get the year in which a customfield was last set in this issue, or null if never set. */
def Integer getFieldSetYear(CustomField customField) {
    def chm = ComponentAccessor.getChangeHistoryManager();
    def historyItems = chm.getChangeItemsForField(issue, customField.getName());
    if (historyItems.empty) return null;
    fieldSetDate = historyItems.reverse().first().getCreated();
    def fieldSetCal = Calendar.getInstance();
    fieldSetCal.setTimeInMillis(fieldSetDate.getTime());
    year = fieldSetCal.get(Calendar.YEAR)
    return year;
}

cf = ComponentAccessor.customFieldManager.getCustomFieldObject(QUARTER_COMMIT_CUSTOMFIELDID);
Map<String, Option> cfVal = issue.getCustomFieldValue(cf);

if (!cfVal) return;
currentYear = Calendar.getInstance().get(Calendar.YEAR) as Integer;
currentQuarter = Calendar.getInstance().get(Calendar.MONTH).intdiv(3) as Integer; // 0 to 3
 
issueQuarter = cfVal[null]?.getValue()?.replaceFirst("[qQ]", "")?.toInteger() - 1; // 0 to 3. E.g. "Q1" becomes 0
issueYear = cfVal["1"]?.getValue()?.toInteger(); // e.g. 2017

if (!issueYear) {
    // It's possible for the field's year to be unset. In that case we cunningly infer the year from the 
    // date at which 'Quarter Commit' was last modified. 
    issueYear = getFieldSetYear(cf);
    if (!issueYear) {
        // If for some bizarre reason we can't tell when Quarter Commit was set, default to current year.
        issueYear = currentYear
    };
};

QUARTERS = ["Q1", "Q2", "Q3", "Q4"]

/** 
 * Display the 'before current quarter' block, grey if our issue was scheduled any time
 * before the current quarter, white otherwise.
 **/
def beforeNowBlock() {
    if ((issueYear < currentYear) ||
            (issueYear == currentYear && issueQuarter < currentQuarter))
    {
        "<th style='padding-left: 0px; padding-right: 0px; background: lightgrey' title='Committed to Q${issueQuarter+1} ${issueYear?issueYear:''} (in the past)'>&hellip;</th>"
    } else {
        "<td style='padding-left: 0px; padding-right: 0px;'>&hellip;</td>"
    }
}

/** 
 * Display a quarter's block. If the issue's year + quarter equals ours, display green, otherwise white.
 *
 * @param quarterOffset Display the n'th from current quarter (0 to 3). I.e. 0 means "this quarter", 1 means "next qurater" etc.
 * 
 */
def quarterBlock(quarterOffset) {
    def q = QUARTERS[quarterOffset % 4]
    // If the year is unset (implying current), or is equal to our year, display just the quarter. Otherwise display quarter plus year.
//    log.error("Considering ${q}: does issue year ${issueYear} equal ${currentYear + ((quarterOffset).intdiv(4))}, and does issue quarter ${issueQuarter} equal our quarter ${quarterOffset}?")
    output = (issueYear == currentYear + ((quarterOffset).intdiv(4)) && issueQuarter == quarterOffset%4) ?
            "<th style='background-color:lightgreen' title='Committed to Q${issueQuarter+1} ${issueYear}'>${q}</th>" :
            "<td style='color:grey'>${q}</td>";
    output
}

/* Display the 'after our 4-quarter window' block. */
def afterQuartersBlock() {
    // E.g. for Q1 2017 when we're in Q2, don't show (currentQuarter=1, issueQuarter=0)
    if ((issueYear > currentYear) &&
            issueQuarter >= currentQuarter) {
        "<th style='padding-left: 0px; padding-right: 0px; background-color: lightgreen' title='Committed to Q${issueQuarter+1} ${issueYear}'>&hellip;</th>"
    } else {
        "<td style='padding-left: 0px; padding-right: 0px;'>&hellip;</td>"
    }
}

return """
<table class="aui">
    <tbody>
        <tr>
${beforeNowBlock()}
${quarterBlock(currentQuarter)}
${quarterBlock(currentQuarter+1)}
${quarterBlock(currentQuarter+2)}
${quarterBlock(currentQuarter+3)}
${afterQuartersBlock()}
        </tr>
    </tbody>
</table>
""" as String;
  • No labels