User Search JS Controller

In the previous section we implemented the User Search REST Endpoint which we will now use from our JS Controller. You might want to check the Story Workshop again to see what we want to build here.

AUI and JIRA Version?

Ok so we want to use the Select2 Widget from the AUI Framework. First you have to know that each version of JIRA has AUI prepackaged in a certain version. Therefore we will use that and not pull in a second version of that (really, don't do that).

So we do a atlas-run and wait for JIRA to startup. Now open the Browser Console and type:

AJS.version
5.7.31

It will print the exact version used in the JIRA target version we code our plugin for. At this point you really have to think about the range of versions you want to support with your plugin. If you want to support older JIRA versions you need to know the lowest version of AUI availabe and code with backwardscompatibility in mind. To make things simpler we assume that our plugin will be compatible starting from JIRA 7.0 up to the latest JIRA version (currently 7.1.x).

Alright now open the AUI documentation for 5.7.31 and select AUI Select 2 documentation. Ok no offense to the people writing the docs but it is a little thin and it took me some time to come up with a hassle-free way to use the Select 2 the way I want it to behave. You just read the doc and I will provide you some code you should like.

What about JS Frameworks, Template Engines and jQuery?

Alright that is it about our Widget. But how do we write our JavaScript code? Is there any template engine? Any watchers and/or frameworks? You have good point there. Let me tell you a short tale about JIRA and JavaScript...

Ok basically we already used AJS.version before, so you might have already wondered what that actually was. JIRA bundles a lightweight jQuery which is bound to AJS there is no $ or jQuery and even if there were you are advised to use AJS prefix. So here is a basic example:

var theFooElement = AJS.$('#foo');
undefined

It will select the DOM element with id foo (when there is no such element it will return undefined). Ok just take that in for now, I know it is unfamiliar but we will get used to it.

So what about template Engines? There is soy which is basically the Google Closure Template Framework which can be used server-side and client-side. We will stick with velocity for server-side templates (you could switch to soy completely). And we will use soy-templates on the client-side.

Small is beautiful - Avoid using big JavaScript frameworks that will register globals, bring in dependencies and tend to overwrite stuff that might break JIRA functionality. In most cases AUI already provides a way to solve your problem.

Alright let's resume what we are going to use:

Basic setup of JS Dependencies and Controller

Now we create our JS-Controller file in /src/main/resources/js/kitchen-duty-plugin--planning-page-controller.js. We put some simple dummy code inside which will trigger a message flag on pageload just to see if everything is working.

src/main/resources/js/.../kitchen-duty-plugin--planning-page-controller.js
AJS.toInit(function(){
   require(['aui/flag'], function(flag) {
       var myFlag = flag({
           type: 'success',
           title: 'Kitchen Duty Plugin',
           body: 'JS Controller is working'
       });
   });
});

Our new JS Controller will not just work out of the box. We need to define a resource in atlassian-plugin.xml for our controller.

src/main/resources/.../atlassian-plugin.xml
<web-resource key="kitchen-duty-plugin-resources--planning-page" name="kitchen-duty-plugin Web Resources for Planning Page">
	<dependency>com.atlassian.auiplugin:ajs</dependency>
	<resource type="download" name="kitchen-duty-plugin--planning-page-controller.js" location="/js/kitchen-duty-plugin--planning-page-controller.js"/>
	<context>kitchen-duty-plugin</context>
</web-resource>

We have defined the resource now but JIRA does not know when to use (load) the resources we defined. We will use the PageBuilderService to tell JIRA when to load our resources. (You might want to try #require in your velocity templates instead, but PageBuilderService always worked best for me)

We pull in some additional dependencies to be able to use PageBuilderService. Add these dependencies to your pom.xml.

pom.xml
<dependency>
	<groupId>com.atlassian.templaterenderer</groupId>
	<artifactId>atlassian-template-renderer-api</artifactId>
	<version>1.5.7</version>
	<scope>provided</scope>
</dependency>
<dependency>
	<groupId>com.atlassian.plugins</groupId>
	<artifactId>atlassian-plugins-webresource</artifactId>
	<version>3.3.3</version>
	<scope>provided</scope>
</dependency>

We already learned that we need to do a @ComponentImport somewhere in our application to pull dependencies from other OSGi Bundles. That is why we simply do that in our MyPluginComponentImpl.java file which we will (mis)use for that purpose from now on.

src/main/java/com/codeclou/kitchen/duty/impl/.../MyPluginComponentImpl.java
import com.atlassian.webresource.api.assembler.PageBuilderService;

...

@ComponentImport
private PageBuilderService pageBuilderService;

Now that the PageBuilderService is usable we need to tell our Webwork Action to load our resources.

src/main/java/com/codeclou/kitchen/duty/webwork/.../KitchenDutyPlanningWebworkAction.java
package io.codeclou.kitchen.duty.webwork;

import com.atlassian.webresource.api.assembler.PageBuilderService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.atlassian.jira.web.action.JiraWebActionSupport;

import javax.inject.Inject;
import javax.inject.Named;

@Named 
public class KitchenDutyPlanningWebworkAction extends JiraWebActionSupport
{
    private static final Logger log = LoggerFactory.getLogger(KitchenDutyPlanningWebworkAction.class);

    @Inject 
    private PageBuilderService pageBuilderService;

    @Override
    public String execute() throws Exception {
        pageBuilderService.assembler().resources().requireWebResource(
           "io.codeclou.kitchen-duty-plugin:kitchen-duty-plugin-resources" 
        ).requireWebResource(
           "io.codeclou.kitchen-duty-plugin:kitchen-duty-plugin-resources--planning-page"
        );

        return "kitchen-duty-planning-success";
    }

    public void setPageBuilderService(PageBuilderService pageBuilderService) { 
        this.pageBuilderService = pageBuilderService;
    }
}
Define our KitchenDutyPlanningWebworkAction as @Named-component.
Use @Inject to wire the PageBuilderService.
We tell the PageBuilderService via requireWebResource() to load our shared resource bundle and our planning-page bundle containing the controller. The pattern for require is "pluginKey:resourceKey".
And of course a setter for the setter injection of PageBuilderService.

After starting up JIRA via atlas-run (you should restart atlas-run because we added something to pom.xml) you should see a green success message popping up on the top right when surfing to planning page: http://localhost:2990/jira/secure/KitchenDutyPlanningWebworkAction.jspa

Setting up Select2 Widget to find Users

Before we start you might (not necessary) want to read in depth about AUI Layout, AUI Forms and Soy.

What is soy? It is a templating framework with templates we can use server-side and client-side. Cool! But we will need a transformer to transform the soy templates into JavaScript to be able to use them in our JS-Controller.

Here is a short summary of what we will be doing now:

  • Create a soy template for the user search.
  • define a transformer to be able to use soy template in JS Controller.
  • write some JS code to use AUI Select2 widget with our User Search REST Resource (see previous chapter).

Create the soy template file in src/main/resources/templates-soy/kitchen-duty-planning.soy and paste the following code there.

This file will later contain more than one template for each use case. But we will have one file per page.

src/main/resources/templates-soy/.../kitchen-duty-planning.soy
{namespace JIRA.Templates.KDP} 
/**
 * Kitchen Duty Planning Page - User Search Template
 */
{template .userSearch} 
<form class="aui" id="kdp-user-select-form"> 

    <div class="field-group">
        <label for="kdp-user-select">Users</label>
        <div id="kdp-user-select"  
                 name="kdp-user-select"
                 class="kdp-aui-select"
                 multiple=""
                 placeholder="start typing a username ...">
        </div>
        <div class="description">The users that should have kitchen duty.</div>
    </div>
    <div class="buttons-container">
        <div class="buttons">
            <input class="button submit" type="submit" 
                   id="kdp-user-select-save-button"
                   value="show"/>
        </div>
    </div>

</form>
{/template}
We define a namespace called JIRA.Templates.KDP for our templates. Later we will access all out templates with namespace.templateName.
Our first actual template will be called .userSearch and contains HTML code we will later use from our JS Controller.
The input-fields and AUI widgets will only render correctly if we surround them with a form that has a class called aui. We define an id on the form to be able to access it via jQuery later.
This is the container for the AUI Select 2 widget. We use a div with id kdp-user-select. Don't forget multiple to tell the widget on init that this field will accept multiple values.
The last thing we need is a button to submit the form. We also give the button a unique id to use it via jQuery later.

Change velocity template so that we have one div container with an id we can use to dynamically insert our content which is rendered by JS-Controller.

src/main/resources/templates/kitchen-duty-planning-webwork-module/.../kitchen-duty-planning-success.vm
<html>
<head>
    <title>$i18n.getText("kitchen-duty-plugin.admin.planning.page.title")</title>
    <meta name="decorator" content="atl.admin">
</head>
<body>
<h1>$i18n.getText("kitchen-duty-plugin.admin.planning.page.headline")</h1>

<div id="kdp-planning-page-container"></div> 

</body>
</html>
The only thing we change in our Velocity template is to place on div container which we will use to append the rendered template(s) to.

Change the JS-Controller to render the soy template, initialize the AUI Select 2 widget to work with our User Search REST Resource.

src/main/resources/js/.../kitchen-duty-plugin--planning-page-controller.js
var showSuccessFlag = function(message) { 
    require(['aui/flag'], function(flag) {
        var myFlag = flag({
            type: 'success',
            title: 'Kitchen Duty Plugin',
            close: 'auto',
            body: message
        });
    });
};

var initUserSearch = function(restUrl) { 
    var templateUserSearch = JIRA.Templates.KDP.userSearch(); 

    var auiUserSelectOptions = {
        ajax: {
            url: function () {
                return restUrl + '/user/search'; 
            },
            dataType: 'json',
            delay: 250,
            data: function (searchTerm) {
                return {
                    query: searchTerm
                };
            },
            results: function (data) {
                return {
                    results: data
                };
            },
            cache: true
        },
        minimumInputLength: 1,
        tags: 'true' 
    };

    /* INIT TEMPLATES AND WIDGETS */

    AJS.$('#kdp-planning-page-container').append(templateUserSearch); 
    AJS.$('#kdp-user-select').auiSelect2(auiUserSelectOptions); 
    AJS.$('#kdp-user-select-form').submit(function (e) { 
        e.preventDefault();
        AJS.$(AJS.$('#kdp-user-select').select2('data')).each(function () { 
            showSuccessFlag(this.id);
        });
    });
};

AJS.toInit(function(){ 
    AJS.log('KDP: Planning Page Controller initializing ...');
    var baseUrl = AJS.params.baseURL; 
    var restUrl = baseUrl + '/rest/kitchenduty/1.0';

    initUserSearch(restUrl); 
});
We put the message flag into a function so that we can easily create success flags everywhere we want.
We define an init function for our user search code. Inside there we will initialize everything we need for AUI Select 2.
The first thing we do is get our userSearch soy template and have it ready to use.
The auiUserSelectOptions are the options of AUI Select 2 which you can read in the docs. We define the URL here where the AUI Select 2 field should query for usernames. The rest of the options is necessary and should be self explanatory.
This option tells the AUI Select 2 that we want to have tags which should be removable.
Now we append our rendered userSearch soy template to the DOM into our special div-container we defined earlier.
Right after that we initialize the AUI Select 2 field for userSearch with the previously defined options.
Now we hook into the submit button of the form and prevent the default-form-submit. Otherwise the form would trigger a POST/GET request which we don't want.
Later we will hook the code to save the selection in here, but that will happen in future chapters. For now we are fine with getting each selected username displayed as a success flag. select2('data') returns an array with all selected elements.
The AJS.toInit function is a little helper that executes its callback when the AUI Framework has loaded. We put all calls to our init functions there.
We get the baseUrl from the AUI Framework. If you call the AJS.params.baseURL to early outside the AJS.init helper you will get undefined errors. Depending on the baseUrl we build our base restUrl which all Endpoints have in common.
Finally we call the initUserSearch function and pass the restUrl.

 

Now we need to teach JIRA that we want to use soy in our client-side code and define a transformer and the soy template to load. Also we need to define AUI dependencies to be loaded.

src/main/resources/.../atlassian-plugin.xml
<?xml version="1.0" encoding="UTF-8"?>

<atlassian-plugin key="${atlassian.plugin.key}" name="${project.name}" plugins-version="2">
...
  <web-resource key="kitchen-duty-plugin-resources--planning-page" name="kitchen-duty-plugin Web Resources for Planning Page">
    <dependency>com.atlassian.auiplugin:ajs</dependency>
    <dependency>com.atlassian.auiplugin:aui-select2</dependency> 
    <dependency>com.atlassian.auiplugin:aui-experimental-soy-templates</dependency> 
    <transformation extension="soy"> 
      <transformer key="soyTransformer">
        <functions>com.atlassian.confluence.plugins.soy:soy-core-functions</functions>
      </transformer>
    </transformation>
    <resource type="download" name="kitchen-duty-planning-soy.js" 
              location="templates-soy/kitchen-duty-planning.soy"/>
    <resource type="download" name="kitchen-duty-plugin--planning-page-controller.js"
              location="/js/kitchen-duty-plugin--planning-page-controller.js"/>
    <context>kitchen-duty-plugin</context>
  </web-resource>
...
</atlassian-plugin>
We specify teh dependency to AUI Select 2 widget so that JIRA provides it when our bundle is loaded.
We also specify the special soy dependency as it is stated in the docs (even though I think this does nothing)
Now we specify a transformer which transforms the soy templates into JavaScript. It transforms all files with soy extension to JavaScript. (But not really all, see next point)
We specify another download resource. This is the JavaScript file which contains all our soy templates for the planning page. location is the path to the source file which is a soy file. The soy file will be dynamically transformed to JavaScript by the previously defined transformer.

Now restart atlas-run and refresh (shift-reload) the Page and you should see this:

Ok we can search for users now and add or remove them to or from our input field. In the next chapter we tend to the actual Kitchen Duty Planning REST Resources that save our selection. Further chapters will tend to the JS Controller saving the selection and loading the AUI Select 2 with our saved selection.

You have completed Step 3. Good Job! You can check out this step's solution on GitHub

The graphic shows marked green which components we implemented in this section.