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.
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:
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.
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:
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:
AJS
prefixAJS.getI18nText
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.
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.
<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
.
<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.
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.
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;
}
}
KitchenDutyPlanningWebworkAction
as @Named
-component.
@Inject
to wire the PageBuilderService
.
requireWebResource()
to load our shared resource bundle and our planning-page bundle containing the controller.
The pattern for require is "pluginKey:resourceKey"
.
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
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 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.
{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}
JIRA.Templates.KDP
for our templates.
Later we will access all out templates with namespace.templateName
.
.userSearch
and contains HTML code we will later
use from our JS Controller.
aui
.
We define an id on the form to be able to access it via jQuery later.
div
with id kdp-user-select
.
Don't forget multiple
to tell the widget on init that this field will accept multiple values.
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.
<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>
Change the JS-Controller to render the soy template, initialize the AUI Select 2 widget to work with our User Search REST Resource.
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);
});
userSearch
soy template and have it ready to use.
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.
select2('data')
returns an array with all selected elements.
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.
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.
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.
<?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>
soy
extension to JavaScript. (But not really all, see next point)
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.
The graphic shows marked green which components we implemented in this section.