Adding a link item in the pull request overview panel

This tutorial will show how you can add some custom UI elements into the pull request overview. Given an example "personal TODO" plugin, we'll be adding a count of TODOs for a pull request near the JIRA issue and build counts.

The full source for this tutorial is available on Bitbucket as atlassian/bitbucket-example-pull-request-ui-plugin.

Discovering web fragment locations

The first step is to figure out where the available spots are to place your web fragment. This can be done by appending ?web.items&web.panels&web.sections to any URL to display the locations. Visit the Web Fragments documentation for more information.

Pull Request Overview panel locations

The one we want for this tutorial is bitbucket.pull-request.related-entities. You'll notice that it accepts Client Web Panels. That means two things:

  • As a panel location, it allows you to add arbitrary HTML to the page in that location.
  • As a client panel location, it will render your HTML in the user's browser, rather than on the server.

You'll also see that a pull-request object is provided to you to help you do your rendering.

Plugin descriptor - atlassian-plugin.xml

Great, so we know what we're doing! Let's get started then creating a Client Web Panel.

Here is the atlassian-plugin.xml for our panel:

<atlassian-plugin key="${project.groupId}.${project.artifactId}" name="${project.name}" plugins-version="2">
    <plugin-info>
        <description>${project.description}</description>
        <version>${project.version}</version>
        <vendor name="${project.organization.name}" url="${project.organization.url}" />
    </plugin-info>
    
    <client-resource key="pr-overview-resources" name="Pull Request Overview Resources">
        <directory location="/css/" />
        <directory location="/js/" />
        <directory location="/soy/" />
        
        <dependency>com.atlassian.auiplugin:ajs</dependency>
        <dependency>com.atlassian.bitbucket.server.bitbucket-web-api:pull-request-web-panels</dependency>
    </client-resource>

    <client-web-panel name="PR TODOs link" key="pr-overview-todo-panel" location="bitbucket.pull-request.related-entities" weight="2000">
        <client-context-provider>MyCompany.TODO.getTODOStats</client-context-provider>
        <resource name="view" type="soy" location="com.atlassian.bitbucket.server.bitbucket-example-pull-request-plugin:pr-overview-resources/com.mycompany.todo.prOverviewPanel" />
        <dependency>com.atlassian.bitbucket.server.bitbucket-example-pull-request-plugin:pr-overview-resources</dependency>
    </client-web-panel>

</atlassian-plugin>

You might notice some bits like ${project.artifactId} in this file. These are variables that are being populated automatically from values in our pom.xml file so that we can be certain the pom and plugin are in sync. The only variables you need to be concerned with for this tutorial are:

  • ${project.groupId} == "com.atlassian.bitbucket.server"
  • ${project.artifactId} == "bitbucket-example-pull-request-plugin"

These two are joined to generate the plugin's key - "com.atlassian.bitbucket.server.bitbucket-example-pull-request-plugin"

The plugin elements:

  • The <plugin-info /> describes the plugin as a whole.
  • The <client-resource /> describes the JS, LESS/CSS, and client-side Soy templates we'll need for our Client Web Panel. This particular resource encapsulates a JS file and a Soy file. You can learn more by checking out the Web Resource Plugin Module.
  • The <client-web-panel /> is the most interesting part for us. Let's dissect it.

client-web-panel

<client-web-panel name="PR TODOs link" key="pr-overview-todo-panel" location="bitbucket.pull-request.related-entities" weight="2000">

This line gives a human name of "PR TODOs link" to our panel, and is known for programmatic purposes as pr-overview-todo-panel. The location attribute specifies where the panel will be placed, and we've set it to the location "bitbucket.pull-request.related-entities" that we found earlier.

<dependency>com.atlassian.bitbucket.server.bitbucket-example-pull-request-plugin:pr-overview-resources</dependency>

This line says that whenever the pr-overview-todo-panel is shown, also include the resources in our "pr-overview-resources" <client-resource />

<client-context-provider>MyCompany.TODO.getTODOStats</client-context-provider>

This line references some JavaScript functions that will be used when rendering your web panel. The <client-context-provider/> will transform incoming data into the shape your template needs. This function is defined in a JavaScript file in the <client-resource/> dependency, and we'll look at it later.

<resource name="view" type="soy" location="com.atlassian.bitbucket.server.bitbucket-example-pull-request-plugin:pr-overview-resources/com.mycompany.todo.prOverviewPanel" />

Every Client Web Panel requires a view template that produces the HTML. Here we specify a <resource /> with the name "view". We specify that our resource is a Soy (Closure Templates) template with type="soy".

Then we provide the location of the Soy template. You can see we're referencing our <client-resource /> element via its fully qualified name (plugin key; colon; module key) in the location property: com.atlassian.bitbucket.server.bitbucket-example-pull-request-plugin:pr-overview-resources. The second half of the location parameter is the name of the Soy template to use within that resource. Let's write that template now.

The Soy template (Closure Template) - pull-request-overview.soy

{namespace com.mycompany.todo}

/**
 * @param count The count of TODOs in this PR.
 */
{template .prOverviewPanel}
    {call bitbucket.feature.pullRequest.relatedEntitiesWebPanel}
        {param linkText: $count == 1 ? 'TODO' : 'TODOs' /}
        {param linkHref: '#' /}
        {param iconClass: 'todo-icon' /}
        {param count: $count /}
        {param extraClasses: 'mycompany-todos-link'  /}
    {/call}
{/template}

This Soy file defines the prOverviewPanel template in the com.mycompany.todo namespace. This template takes in a single parameter count, and is very simple - it calls a Bitbucket Server template that will create the standard markup for the location we want. Most locations won't have a standard template like this and you will simply generate your own HTML. Read more about Soy at Closure Templates. Your Bitbucket Server version may not always be on the latest version of Closure Templates, so you'll find the most accurate documentation in the Internet Archive.

The JavaScript - pull-request-overview.js

(function($) {
    // Set up our namespace
    window.MyCompany = window.MyCompany || {};
    MyCompany.TODO = MyCompany.TODO || {};

    var storage = {
        getTODOs : function(pullRequestJson) {
            // ...
            return [];
        },
        putTODOs : function(pullRequestJson, todos) {
            // put(storageKey(pullRequestJson), JSON.stringify(todos));
        }
    };

    /**
     * The client-condition function takes in the context
     * before it is transformed by the client-context-provider.
     * If it returns a truthy value, the panel will be displayed.
     */
    function hasAnyTODOs(context) {
        var todos = storage.getTODOs(context['pullRequest']);
        return todos.length;
    }

    /**
     * The client-context-provider function takes in context and transforms
     * it to match the shape our template requires.
     */
    function getTODOStats(context) {
        var todos = storage.getTODOs(context['pullRequest']);
        return {
            count : todos.length
        };
    }

    /* Expose the client-context-provider function */
    MyCompany.TODO.getTODOStats = getTODOStats;

    /* use a live event to handle the link being clicked. */
    $(document).on('click', '.mycompany-todos-link', function(e) {
        e.preventDefault();

        // open a dialog to show the TODO details.
    });
}(AJS.$));

This file defines the implementation for our <client-context-provider/> by creating the globally accessible JavaScript function that we reference in our XML: MyCompany.TODO.getTODOStats.

It also adds a live event listener to the document for any clicks on our ".mycompany-todos-link".

It uses localStorage to hold the user's TODOs.