Last updated Apr 19, 2024

JIRA Agile ADG Migration Guidelines

Introduction

This document is a case study of how the JIRA Agile team built JIRA Agile 2.6.1 (note, JIRA Agile was previously named Bonfire), which is the release where they started their Atlassian Design Guidelines (ADG) compliancy work. At the time of this writing, JIRA Agile supported 5.x and 6.x versions of JIRA. This will not always be the case.

This document is aimed at external plugin developers and explains the strategies the JIRA Agile team is using to work in both pre-ADG and post-ADG JIRAs.

Approach

The JIRA Agile team is not currently trying to make their plugin 100% ADG compliant. That will be done during a later release. Any work currently undertaken by the JIRA Agile team as a result of the ADG changes in JIRA is to fix elements that are broken.

The changes to the plugin need to work in JIRA 5.x and JIRA 6.x and should all work with a single jar. This solution does not require a different artifact for a different version of JIRA.

When an element is broken as a result of this change, the markup used by JIRA Agile is updated to the ADG compliant markup. This means that they get the styling coming from JIRA with very little additional work. This also means that the JIRA Agile plugin is using the latest markup patterns that take advantage of styles used in JIRA. Older versions of JIRA will not have styles for those markup patterns and so, where possible, the styles are backported to older versions of JIRA using conditional resource loading.

Conditional Resource Loading

This section explains how the JIRA Agile team conditionally loaded resources into the page. Two methods are described in this section. The first method loads in the resources per action and is good if you only want to do the conditional loading on a specific page. The other loads in the resources in a servlet-filter, which is good if you want the resource loading to happen on several pages. The source code for the Version Kit has also been included to assist in parsing and comparing JIRA versions.

JIRA Agile contains the following files:

  • bonfire-overrides-5.0.css - loaded for all JIRA instances 5.0.x or older
  • bonfire-overrides-5.1.css  - loaded for all JIRA instances 5.1.x or older
  • bonfire-overrides-5.2.css - loaded for all JIRA instances 5.2.x or older

These classes contain the styles used for certain versions of JIRA. JIRA Agile loads these by defining a web-resource for each of them in the atlassian-plugin.xml like this:

1
2
<web-resource key="bonfire-legacy-five-zero" name="Bonfire Legacy 5.0 Resources">
    <resource type="download" name="bf-legacy-5.0.css" location="includes/css/legacy/bonfire-overrides-5.0.css"/>
</web-resource>

When support for JIRA 5.x is eventually dropped, the legacy css files will be deleted.

Method 1: In the Web Actions

Originally, JIRA Agile conditionally required the resources in the web actions for each page based on the JIRA version. The JIRA version can be obtained from the BuildUtilsInfo:

1
2
public void doDefault()
{
    includeVersionSpecificResources();

    return SUCCESS;
}

private void includeVersionSpecificResources()
{
    VersionKit.SoftwareVersion five1 = VersionKit.version(5, 1);
    VersionKit.SoftwareVersion five2 = VersionKit.version(5, 2);
    VersionKit.SoftwareVersion six0 = VersionKit.version(6, 0);
    VersionKit.SoftwareVersion jiraVersion = VersionKit.parse(buildUtilsInfo.getVersion());
    if (jiraVersion.isLessThan(six0))
    {
        webResourceManager.requireResource("com.atlassian.bonfire.plugin:bonfire-legacy-five-two");
    }
    if (jiraVersion.isLessThan(five2))
    {
        webResourceManager.requireResource("com.atlassian.bonfire.plugin:bonfire-legacy-five-one");
    }
    if (jiraVersion.isLessThan(five1))
    {
        webResourceManager.requireResource("com.atlassian.bonfire.plugin:bonfire-legacy-five-zero");
    }
} 

This method is useful if you want to explicitly include the legacy resources into the page.

Method 2: Using a Servlet-filter

The method that JIRA Agile uses is to have a servlet-filter that conditionally adds in the resources on all of JIRA Agile's pages. Using this approach means that you don't need to have the conditionally added resources included on every action.

JIRA Agile declares the servlet filter in the atlassian-plugin.xml like this:

1
2
<servlet-filter name="Bonfire Legacy Resource Filter" key="bonLegacyResources" location="before-dispatch" class="com.atlassian.bonfire.web.filters.BonfireLegacyResourceFilter" weight="501">
    <url-pattern>/secure/SessionNavigator.jspa*</url-pattern>
    <url-pattern>/secure/ViewSession.jspa*</url-pattern>
    <url-pattern>/browse/*</url-pattern>

    <dispatcher>REQUEST</dispatcher>
    <dispatcher>FORWARD</dispatcher>
</servlet-filter> 

All the URLs required for resource loading are included in the url-pattern section.

The filter itself looks something like the conditional check done in the web actions:

1
2
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException
{
    includeVersionSpecificResources();

    chain.doFilter(request, response);
}

private void includeVersionSpecificResources()
{
    VersionKit.SoftwareVersion five1 = VersionKit.version(5, 1);
    VersionKit.SoftwareVersion five2 = VersionKit.version(5, 2);
    VersionKit.SoftwareVersion six0 = VersionKit.version(6, 0);
    VersionKit.SoftwareVersion jiraVersion = VersionKit.parse(buildUtilsInfo.getVersion());
    if (jiraVersion.isLessThan(six0))
    {
        webResourceManager.requireResource("com.atlassian.bonfire.plugin:bonfire-legacy-five-two");
    }
    if (jiraVersion.isLessThan(five2))
    {
        webResourceManager.requireResource("com.atlassian.bonfire.plugin:bonfire-legacy-five-one");
    }
    if (jiraVersion.isLessThan(five1))
    {
        webResourceManager.requireResource("com.atlassian.bonfire.plugin:bonfire-legacy-five-zero");
    }
} 

The main difference is that it is done as part of the filter chain and not in the individual actions.

Version Kit

Here is the code from the Version Kit, which handles multiple versions of JIRA.

VersionKit.java

1
2
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
 * Helps with versions comparisons
 */
public class VersionKit
{
    private static final Pattern VERSION_PATTERN = Pattern.compile("^(\\d+)\\.(\\d+)\\.?(\\d+)?");
    public static class SoftwareVersion
    {
        private final int majorVersion;
        private final int minorVersion;
        private final int bugFixVersion;
        private final String dottedVersionString;
        public SoftwareVersion(final String dottedVersionString)
        {
            this.dottedVersionString = dottedVersionString;
            Matcher versionMatcher = VERSION_PATTERN.matcher(dottedVersionString);
            if (versionMatcher.find())
            {
                majorVersion = decode(versionMatcher, 1, 0);
                minorVersion = decode(versionMatcher, 2, 0);
                bugFixVersion = decode(versionMatcher, 3, 0);
            }
            else
            {
                throw new IllegalArgumentException("The dotted version string is not in the expected format");
            }
        }
        public SoftwareVersion(final int majorVersion, final int minorVersion, final int bugfixVersion)
        {
            this.majorVersion = majorVersion;
            this.minorVersion = minorVersion;
            this.bugFixVersion = bugfixVersion;
            this.dottedVersionString = "" + majorVersion + "." + minorVersion + "." + bugfixVersion;
        }
        public SoftwareVersion(final int majorVersion, final int minorVersion)
        {
            this.majorVersion = majorVersion;
            this.minorVersion = minorVersion;
            this.bugFixVersion = 0;
            this.dottedVersionString = "" + majorVersion + "." + minorVersion;
        }
        private int decode(Matcher versionMatcher, int i, int defaultVal)
        {
            if (versionMatcher.group(i) != null)
            {
                return Integer.decode(versionMatcher.group(i));
            }
            return defaultVal;
        }
        public int getMajorVersion()
        {
            return majorVersion;
        }
        public int getMinorVersion()
        {
            return minorVersion;
        }
        public int getBugFixVersion()
        {
            return bugFixVersion;
        }
        /**
         * Returns true if this version is greater than if equal to the specified version
         *
         * @param that the specified version to compare against
         *
         * @return true if this version is greater than if equal to the specified version
         */
        public boolean isGreaterThanOrEqualTo(SoftwareVersion that)
        {
            if (this.equals(that))
            {
                return true;
            }
            if (this.majorVersion < that.majorVersion)
            {
                return false;
            }
            if (this.majorVersion == that.majorVersion)
            {
                if (this.minorVersion < that.minorVersion)
                {
                    return false;
                }
                if (this.minorVersion == that.minorVersion)
                {
                    if (this.bugFixVersion < that.bugFixVersion)
                    {
                        return false;
                    }
                }
            }
            return true;
        }
        /**
         * Returns true if this version is less than or equal to the specified version
         *
         * @param that the specified version to compare against
         *
         * @return true if this version is less than or equal to the specified version
         */
        public boolean isLessThanOrEqualTo(SoftwareVersion that)
        {
            if (this.equals(that))
            {
                return true;
            }
            if (this.majorVersion > that.majorVersion)
            {
                return false;
            }
            if (this.majorVersion == that.majorVersion)
            {
                if (this.minorVersion > that.minorVersion)
                {
                    return false;
                }
                if (this.minorVersion == that.minorVersion)
                {
                    if (this.bugFixVersion > that.bugFixVersion)
                    {
                        return false;
                    }
                }
            }
            return true;
        }
        /**
         * Returns true if this version is greater than the specified version
         *
         * @param that the specified version to compare against
         *
         * @return true if this version is greater than to the specified version
         */
        public boolean isGreaterThan(SoftwareVersion that)
        {
            if (this.majorVersion > that.majorVersion)
            {
                return true;
            }
            if (this.majorVersion == that.majorVersion)
            {
                if (this.minorVersion > that.minorVersion)
                {
                    return true;
                }
                if (this.minorVersion == that.minorVersion)
                {
                    if (this.bugFixVersion > that.bugFixVersion)
                    {
                        return true;
                    }
                }
            }
            return false;
        }
        /**
         * Returns true if this version is less than the specified version
         *
         * @param that the specified version to compare against
         *
         * @return true if this version is less than to the specified version
         */
        public boolean isLessThan(SoftwareVersion that)
        {
            if (this.majorVersion < that.majorVersion)
            {
                return true;
            }
            if (this.majorVersion == that.majorVersion)
            {
                if (this.minorVersion < that.minorVersion)
                {
                    return true;
                }
                if (this.minorVersion == that.minorVersion)
                {
                    if (this.bugFixVersion < that.bugFixVersion)
                    {
                        return true;
                    }
                }
            }
            return false;
        }
        @Override
        public boolean equals(Object o)
        {
            if (this == o)
            {
                return true;
            }
            if (o == null || getClass() != o.getClass())
            {
                return false;
            }
            SoftwareVersion that = (SoftwareVersion) o;
            if (bugFixVersion != that.bugFixVersion)
            {
                return false;
            }
            if (majorVersion != that.majorVersion)
            {
                return false;
            }
            if (minorVersion != that.minorVersion)
            {
                return false;
            }
            return true;
        }
        @Override
        public int hashCode()
        {
            int result = majorVersion;
            result = 31 * result + minorVersion;
            result = 31 * result + bugFixVersion;
            return result;
        }
        @Override
        public String toString()
        {
            return dottedVersionString;
        }
    }

    /**
     * Parses and returns a SoftwareVersion object representing the dotted number string.
     *
     * @param dottedVersionString the input version
     *
     * @return a version domain object
     *
     * @throws IllegalArgumentException if the string is not N.N.N
     */
    public static SoftwareVersion parse(final String dottedVersionString)
    {
        return new SoftwareVersion(dottedVersionString);
    }
    public static SoftwareVersion version(final int majorVersion, final int... versions)
    {
        int minorVersion = readArray(versions, 0, 0);
        int bugFixVersion = readArray(versions, 1, 0);
        return new SoftwareVersion(majorVersion, minorVersion, bugFixVersion);
    }
    private static int readArray(int[] versions, int index, int defaultVal)
    {
        if (index >= versions.length)
        {
            return defaultVal;
        }
        return versions[index];
    }
}

Examples

Form in a Pop-up Dialog

Most of the time, elements that were already using AUI styles get JIRA ADG markup changes for free. This example shows a form in a pop-up dialog. Everything in the form is just using AUI styles, so the pop-up gets the updated look without any additional work.

JIRA 5.2
JIRA 6.0

The markup remains the same and looks something like this:

1
2
<h1 class="dialog-title">Assign Test Session</h1>
<form class="aui" action="action/to/do/when/post/is/made" method="post">
    <div class="form-body">
        <div class="field-group">
            <label for="ex-assignee">Assignee:</label>
            <input class="text" id="ex-assignee" name="assignee"/>
        </div>
    </div>
    <div class="buttons-container form-footer">
        <div class="buttons">
            <input id="ex-submit" type="submit" class="submit button" value="Assign">
            <a href="#close" class="cancel">Close</a>
        </div>
    </div>
</form>

Dealing with Custom Styles

Sometimes little things break because a margin or head was changed upstream. The style that causes the breakage is due to ADG, but the elements that have been affected aren't using AUI styles and have custom styles defined by the plugin. To fix it so that it looks the same both times, first fix the styles so they look fine in 6.0. Then you can add in the changed styles to the 5.2-only stylesheet.

JIRA 5.2
JIRA 6.0

Broken

Fixed

The change fixes the look in 6.0 and the styles remain the same in older versions since the 5.2 stylesheet is only included in the older versions.

main css file

1
2
 .bfq-button-container {
     float: right;
-    margin-right: 13px;
     display: inline-block;
 }

The offending line is removed from the main css file.

bonfire-overrides-5.2.css

1
2
+.bfq-button-container {
+    margin-right: 13px;
+}

The offending line is then added to the stylesheet that is only included in JIRA 5.2 and older, so things will be as they were.

Replacing Headers

This example deals with replacing headers that break when the patterns used in JIRA are no longer there. To fix this, you need to update the header code to the latest defined by AUI.

JIRA 5.2

Before

After Markup Update

After Backporting Styles

 

JIRA 6.0

Before

After Markup Update

The new markup looks like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<div class="bf-header-wrapper">
    <div class="aui-page-header-inner">
        <div class="aui-page-header-image">
            <div class="aui-avatar aui-avatar-large">
                <div class="aui-avatar-inner">
                    <img src="/jira/download/resources/com.atlassian.bonfire.plugin/images/bonfire_icon_whiteonblue_48.png" alt="Bonfire Logo">
                </div>
            </div>
        </div>
        <div class="aui-page-header-main">
            <ol class="aui-nav aui-nav-breadcrumbs">
                <li><a href="/jira/browse/MARS">Mission To Mars</a></li>
            </ol>
            <h1>Shared Suit Session - Test The Look and Feel</h1>
            <span class="aui-lozenge">Shared</span>
        </div>
        <div class="aui-page-header-actions">
            <div class="aui-buttons">
                <a class="aui-button" href="/jira/secure/SessionNavigator.jspa"><span>Back to test sessions</span></a>
                <a class="aui-button bf-create-session no-refresh" href="/jira/secure/CreateSession.jspa?projectKey=MARS&projectId=19000">
                    <span class="aui-icon aui-icon-small aui-iconfont-add">Add</span>
                    Create Session
                </a>
            </div>
        </div>
    </div>
</div>

This conforms to the latest markup patterns in AUI and the styles are all inherited from JIRA. As expected, the markup changes break the header in older versions of JIRA. The new ADG components are designed to fit visually in older versions of JIRA, so you can backport some of the styles so that the new markup still looks acceptable. You can get these styles from AUI.

JIRA Agile includes the styles for the aui-page-header, aui-lozenge, aui-nav-breadcrumbs and aui-buttons. Styles for aui-button already exist within older versions of JIRA and + icons also already exist.

The following styles were backported from AUI:

bonfire-overrides-5.2.css

1
2
/**
 * AUI Page Header
 */
.aui-page-header-inner {
    border-spacing: 0;
    -moz-box-sizing: border-box;
    box-sizing: border-box;
    display: table;
    table-layout: auto;
    width: 100%;
}
.aui-page-header-image,
.aui-page-header-main,
.aui-page-header-actions {
    -moz-box-sizing: border-box;
    box-sizing: border-box;
    display: table-cell;
    margin: 0;
    padding: 0;
    text-align: left;
    vertical-align: top;
}
/* collapse the cell to fit its content */
.aui-page-header-image {
    white-space: nowrap;
    width: 1px;
}
.aui-page-header-main {
    vertical-align: middle;
}
.aui-page-header-image + .aui-page-header-main {
    padding-left: 10px;
}
.aui-page-header-actions {
    padding-left: 20px;
    text-align: right;
    vertical-align: middle;
}
.aui-page-header-main > h1,
.aui-page-header-main > h2,
.aui-page-header-main > h3,
.aui-page-header-main > h4,
.aui-page-header-main > h5,
.aui-page-header-main > h6 {
    margin: 0;
}
.aui-page-header-actions > .aui-buttons {
    margin: 5px 0; /* spaces out button groups when they wrap to 2 lines */
    vertical-align: top;
    white-space: nowrap;
}
/*! AUI Lozenge */
.aui-lozenge {
    background: #ccc;
    border: 1px solid #ccc;
    border-radius: 3px;
    color: #333;
    display: inline-block;
    font-size: 11px;
    font-weight: bold;
    line-height: 1;
    margin: 0;
    padding: 2px 5px 1px 5px;
    text-align: center;
    text-decoration: none;
    text-transform: uppercase;
}
/*! AUI Navigation */
/* Nav defaults - put very little here!
-------------------- */
.aui-nav,
.aui-nav > li {
    margin: 0;
    padding: 0;
    list-style: none;
}
/* Horizontal, breadcrumbs and pagination are all horizontal */
.aui-nav-breadcrumbs:after,
.aui-nav-pagination:after,
.aui-nav-horizontal:after,
.aui-navgroup-horizontal .aui-nav:after,
.aui-navgroup-horizontal .aui-navgroup-inner:after {
    clear: both;
    content: " ";
    display: table;
}
.aui-nav-breadcrumbs > li,
.aui-nav-pagination > li,
.aui-nav-horizontal > li,
.aui-navgroup-horizontal .aui-nav > li {
    float: left;
}
/* Navigation headings
-------------------- */
.aui-nav-heading {
    color: #707070;
    font-size: 12px;
    font-weight: bold;
    line-height: 1.66666666666667; /* 20px */
    text-transform: uppercase;
}
/* Breadcrumb navigation
-------------------- */
.aui-nav-breadcrumbs > li {
    padding: 0 10px 0 0;
}
.aui-nav-breadcrumbs > li + li:before {
    content: "/";
    padding-right: 10px
}
/* last of type for where it works */
.aui-nav-breadcrumbs > li.aui-nav-selected a,
.aui-nav-breadcrumbs > li:last-child:not(:first-child) a {
    color: #333;
}
/* Slip in the old icon for older versions */
.aui-iconfont-add {
    background: url('../../../images/icons/create_16.png') no-repeat 0px 0px;
    padding-left: 1px;
    padding-bottom: 1px;
}
.session-page-wrapper .aui-button {
    text-shadow: 0 0 0 black;
}

bonfire-overrides-5.1.css

1
2
.aui-icon.aui-icon-small.aui-iconfont-add {
    display: inline-block;
    padding-left: 4px;
    padding-bottom: 3px;
}

Rate this page: