Some time has passed since I worked on this Plugin tutorial - it is mid 2018 now - and we need to catch up to some things.
1. State and future of AUI library:
2. AUI Redesign + JIRA version upgrade
pom.xml
accordingly.<project>
...
<build>
<plugins>
...
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.3</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>
</plugins>
</build>
<properties>
<jira.version>7.10.0</jira.version>
<amps.version>6.3.15</amps.version>
...
</properties>
</project>
Basically we want to first have the user select the week. Then we want to show a multi-select textfield containing existing users for that week and beeing able to add or remove users for that week. That means the template of the user selection loads conditionally by the week number.
There are now multiple soy templates as shown in the graphic above which we will later use from our JavaScript code to render the planning page.
{namespace JIRA.Templates.KDP}
{template .planningPage}
<form class="aui" id="kdp-user-select-form">
<div id="kdp-planning-page-week-container"></div>
<div id="kdp-planning-page-week-users-container"></div>
</form>
{/template}
{template .planningPageWeek}
<h4 class="kdp-step-headline">1. Select Week</h4>
<div class="kdp-step-content">
<input
class="aui-date-picker aui-button"
id="week-picker"
type="date"
max="2050-01-25"
min="2011-12-25"
/>
</div>
{/template}
{template .planningPageWeekUsers}
{@param? week: int}
<h4 class="kdp-step-headline">
2. Kitchen Duty for Week
{if $week != null }
<aui-badge class="aui-badge-primary">
<span id="week-label">{week}</span>
</aui-badge>
{else}
<aui-badge>
<span id="week-label">...</span>
</aui-badge>
{/if}
</h4>
<div class="kdp-step-content">
{if $week == null }
<div>- no week selected -</div>
{else}
<div>Users on kitchen duty for selected week:</div>
<div
id="kdp-user-select"
name="kdp-user-select"
class="kdp-aui-select"
multiple=""
placeholder="start typing a username ..."
></div>
<br/><br/>
<input
class="button submit"
type="submit"
id="kdp-user-select-save-button"
value="save"
/>
{/if}
</div>
{/template}
We will later use the templates from our JavaScript code but for now we need again do something in Java.
In step four we implemented logic to persist only one user for a week. We need to change that to multiple users per week because of the way the AUI Select 2 works.
Therefore I have changed the endpoints to do so. Basically I added just some for-each loops and some missing activeObjects.flush(entity)
statements that update the ID of the object after creating a new entity.
With the AUI Select 2 user field you can remove and add users. The HTTP PUT Request always contains a full list of users therefore we do not need the explicit delete-endpoint anymore.
We simply update the relationships in our @PUT
endpoint.
package io.codeclou.kitchen.duty.rest;
...
@Named
@Path("/planning")
public class KitchenDutyPlanningResource {
...
@GET
@Path("/week/{weekNumber}/users")
@Produces({MediaType.APPLICATION_JSON})
@AnonymousAllowed
public Response getUsersForWeek(@PathParam("weekNumber") final Integer weekNumber) {
Week week = activeObjects.executeInTransaction(new TransactionCallback<Week>() {
@Override
public Week doInTransaction() {
Week[] weeks = activeObjects.find(Week.class, Query.select().where("WEEK = ?", weekNumber));
if (weeks != null && weeks.length > 0) {
return weeks[0];
}
return null;
}
});
List<KitchenDutyPlanningResourceUserModel> users = new ArrayList<>();
if (week != null) {
UserToWeek[] relationships = activeObjects.executeInTransaction(new TransactionCallback<UserToWeek[]>() {
@Override
public UserToWeek[] doInTransaction() {
return KitchenDutyActiveObjectHelper.findAllRelationships(activeObjects, week);
}
});
if (relationships != null) {
for (UserToWeek userToWeek : relationships) {
users.add(new KitchenDutyPlanningResourceUserModel(userToWeek.getUser().getID(), userToWeek.getUser().getName()));
}
}
}
return Response.ok(users).build();
}
/*
* Add the Users to the Week
*/
@PUT
@Path("/week/{weekNumber}/users")
@Produces({MediaType.APPLICATION_JSON})
@AnonymousAllowed
public Response addUserToWeek(@PathParam("weekNumber") final Integer weekNumber,
final List<KitchenDutyPlanningResourceUserModel> userParams) {
activeObjects.executeInTransaction(new TransactionCallback<Void>() {
@Override
public Void doInTransaction() {
//
// WEEK
//
Week week = KitchenDutyActiveObjectHelper.findUniqueWeek(activeObjects, weekNumber);
if (week == null) {
week = activeObjects.create(Week.class, new DBParam("WEEK", weekNumber));
week.save();
activeObjects.flush(week);
}
//
// CLEANUP EXISTING RELATIONSHIPS
//
UserToWeek[] existingRelationships = KitchenDutyActiveObjectHelper.findAllRelationships(activeObjects, week);
if (existingRelationships != null) {
for (UserToWeek existingRelationship : existingRelationships) {
activeObjects.delete(existingRelationship);
activeObjects.flush(existingRelationship);
}
}
//
// USER
//
for (KitchenDutyPlanningResourceUserModel userParam : userParams) {
User user = KitchenDutyActiveObjectHelper.findUniqueUser(activeObjects, userParam.getUsername());
if (user == null) {
user = activeObjects.create(User.class, new DBParam("NAME", userParam.getUsername()));
user.save();
activeObjects.flush(user);
}
//
// Establish ManyToMany Relationship
//
UserToWeek relationship = KitchenDutyActiveObjectHelper.findRelationship(activeObjects, user, week);
if (relationship == null) {
relationship = activeObjects.create(UserToWeek.class);
relationship.setUser(user);
relationship.setWeek(week);
relationship.save();
activeObjects.flush(relationship);
}
}
return null;
}
});
return Response.ok().build();
}
KitchenDutyPlanningResourceUserModel
which procudes
the JSON [ { "id": 1, "username": "linda" }, { "id": 2, "username": "bob" } ]
activeObjects.flush(entity)
ensures that the ID gets updated for the week.
Having done that we can move on again to our frontend code.
Since we want to calcuclate the week number from our date selected in the date picker we need some libraries to do so. Date calculations
in JavaScript are best done with MomentJS.
We need to add the dependency for the AUI date picker to our atlassian-plugin.xml
and put a copy of moment.min.js
inside src/main/resources/js/3rdparty/
.
<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>
<dependency>com.atlassian.auiplugin:aui-date-picker</dependency>
<transformation extension="soy">
<transformer key="soyTransformer">
<functions>com.atlassian.confluence.plugins.soy:soy-core-functions</functions>
</transformer>
</transformation>
<resource type="download" name="momentjs.js"
location="/js/3rdparty/moment-2.22.2.min.js"/>
<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>
We edit our existing Planning Page JS-Controller file in /src/main/resources/js/kitchen-duty-plugin--planning-page-controller.js
.
I have extended the existing code to load and save the users per week. This JS code now uses the soy templates defined above.
var showSuccessFlag = function(message) {
require(['aui/flag'], function(flag) {
flag({
type: 'success',
title: 'Kitchen Duty Plugin',
close: 'auto',
body: message
});
});
};
var showErrorFlag = function(message) {
require(['aui/flag'], function(flag) {
flag({
type: 'error',
title: 'Kitchen Duty Plugin',
close: 'auto',
body: message
});
});
};
var initUserSearch = function(weekNumber) {
// Init SOY template
var planningPageWeekUsersTemplate = JIRA.Templates.KDP.planningPageWeekUsers({
week: weekNumber
});
AJS.$('#kdp-planning-page-week-users-container').html(planningPageWeekUsersTemplate);
// Init actions
var auiUserSelectOptions = {
ajax: {
url: function () { return window.KDPrestUrl + '/user/search'; },
dataType: 'json',
delay: 250,
data: function (searchTerm) { return { query: searchTerm }; },
results: function (data) { return { results: data }; },
cache: true
},
minimumInputLength: 1,
tags: 'true'
};
AJS.$('#kdp-user-select').auiSelect2(auiUserSelectOptions);
// Load initial values from REST API and set for aui-select
if (weekNumber !== null) {
AJS.$.ajax({
url: window.KDPrestUrl + '/planning/week/' + weekNumber + '/users',
dataType: 'json',
success: function(users) {
var selectedUserList = [];
if (users !== null) {
users.forEach(function(user) {
selectedUserList.push({ id: user.username, text: user.username });
});
AJS.$('#kdp-user-select').select2('data', selectedUserList);
}
}
});
}
// Save users on save button click
AJS.$('#kdp-user-select-form').off(); // remove previous listeners
AJS.$('#kdp-user-select-form').submit(function (e) {
e.preventDefault();
var selectedUserList = [];
AJS.$(AJS.$('#kdp-user-select').select2('data')).each(function () {
// we need to transform the JSON sent to the Endpoint since it
// has to be in specific format
selectedUserList.push({ username: this.text });
});
AJS.$.ajax({
url: window.KDPrestUrl + '/planning/week/' + weekNumber + '/users',
type: 'PUT',
contentType: 'application/json',
data: JSON.stringify(selectedUserList),
processData: false,
success: function() {
showSuccessFlag('Saved users for Week ' + weekNumber);
},
error: function() {
showErrorFlag('Failed to save users for Week ' + weekNumber);
}
});
});
};
var initWeekPicker = function() {
// Init SOY template
var planningPageWeekTemplate = JIRA.Templates.KDP.planningPageWeek();
AJS.$('#kdp-planning-page-week-container').html(planningPageWeekTemplate);
// Init actions
AJS.$('#week-picker').off(); // remove previous listeners
AJS.$('#week-picker').datePicker({'overrideBrowserDefault': true});
AJS.$('#week-picker').change(function() {
var week = moment(AJS.$('#week-picker').val()).week();
initUserSearch(week);
});
};
AJS.toInit(function(){
AJS.log('KDP: Planning Page Controller initializing ...');
var baseUrl = AJS.params.baseURL;
var restUrl = baseUrl + '/rest/kitchenduty/1.0';
window.KDPrestUrl = restUrl;
// set locale for moment-js so that week starts on sunday
// and week numbers are correctly calculated
moment.locale('en', {
week: {
dow: 0, // Sunday (0) is the first day of the week
doy: 1 // Week that contains Jan 1st is the first week of the year.
}
});
console.log('Week starts at: ' + moment().startOf('week').format('dddd'));
console.log('Current moment locale: ' + moment().locale());
// Init Base SOY template
var planningPageTemplate = JIRA.Templates.KDP.planningPage();
AJS.$('#kdp-planning-page-container').html(planningPageTemplate);
// Init child templates
initWeekPicker();
initUserSearch(null);
});
AJS.$('#id').select2('data', [ { id: 'foo', text: 'foo' } ]);
selectedUserList.push({ username: this.text });
.
Then we send the userlist via HTTP PUT to our Endpoint for the selected week.
AJS.toInit()
function (which is executed after the DOM has loaded),
we render the base planningPage soy template.
initWeekPicker()
and secondly
the initUserSearch()
with a null
parameter,
telling the function that the user has not yet selected a date.
That's it. Now we can run our plugin with atlas-run
and when navigating to http://localhost:2990/jira/secure/KitchenDutyPlanningWebworkAction.jspa
it should work as you can see below. We are completely done implementing the Kitchen Duty Planning Page.
The graphic shows marked green which components we implemented in this section.