
В этой статье разработаем плагин, который будет сохранять настройки плагина в Jira. Мы будем использовать библиотеки soy, requirejs, backbone js для отображения пользовательского интерфейса. Soy, requirejs, backbone js это встроенные в Jira библиотеки.
Цель статьи состоит в том, чтобы показать как можно использовать встроенные средства Jira для разработки пользовательского интерфейса.
Разработанный плагин будет содержать модуль webwork для сохранения параметров плагина в Jira. Параметры будут вводиться на двух экранах (по два параметра на каждом экране). Далее параметры будут упаковываться в json, который и будет сохраняться в Jira. Исходный код плагина можно посмотреть вот тут.
Создадим скелет плагина
Откроем терминал и выполним команду ниже:
atlas-create-jira-pluginОтветим на вопросы в терминале вот так:
Define value for groupId: : ru.matveev.alexey.jira.tutorial.webworkui Define value for artifactId: : webwork-soy-require-backbone Define value for version: 1.0.0-SNAPSHOT: : Define value for package: ru.matveev.alexey.jira.tutorial.webworkui: : Y: : Y
Внесем изменения в pom.xml
После создания скелета плагина необходимо внести изменения для корректной работы atlassian-spring-scanner 2.
Установим версию atlassian-spring-scanner в 2.0.0:
<atlassian.spring.scanner.version>2.0.0</atlassian.spring.scanner.version>
Изменим scope зависимости atlassian-spring-scanner-annotation с compile на provided:
<dependency> <groupId>com.atlassian.plugin</groupId> <artifactId>atlassian-spring-scanner-annotation</artifactId> <version>${atlassian.spring.scanner.version}</version> <scope>provided</scope> </dependency>
Удалим зависимость atlassian-spring-scanner-runtime.
Создадим сервис для получения и сохранения настроек плагина
Сначала создадим интерфейс для управления настройками плагина.
src/main/java/ru/matveev/alexey/jira/tutorial/webworkui/api/PluginSettingService.java
package ru.matveev.alexey.jira.tutorial.webworkui.api; public interface PluginSettingService { String getConfigJson(); void setConfigJson(String json); }
Теперь сделаем реализацию интерфейса.
src/main/java/ru/matveev/alexey/jira/tutorial/webworkui/impl/PluginSettingServiceImpl.java
package ru.matveev.alexey.jira.tutorial.webworkui.impl; import com.atlassian.plugin.spring.scanner.annotation.imports.ComponentImport; import com.atlassian.sal.api.pluginsettings.PluginSettings; import com.atlassian.sal.api.pluginsettings.PluginSettingsFactory; import ru.matveev.alexey.jira.tutorial.webworkui.api.PluginSettingService; import javax.inject.Inject; import javax.inject.Named; @Named public class PluginSettingServiceImpl implements PluginSettingService { public final PluginSettings pluginSettings; private static final String PLUGIN_STORAGE_KEY = "ru.matveev.alexey.jira.tutorial.webworkui."; private static final String CONFIG_JSON = "configjson"; @Inject public PluginSettingServiceImpl(@ComponentImport PluginSettingsFactory pluginSettingsFactory) { this.pluginSettings = pluginSettingsFactory.createGlobalSettings(); } private void setSettingValue(String settingKey, String settingValue) { this.pluginSettings.put(PLUGIN_STORAGE_KEY + settingKey, settingValue != null?settingValue:""); } private String getSettingValue(String settingKey) { return pluginSettings.get(PLUGIN_STORAGE_KEY + settingKey) != null?pluginSettings.get(PLUGIN_STORAGE_KEY + settingKey).toString():""; } @Override public String getConfigJson() { return getSettingValue(CONFIG_JSON); } @Override public void setConfigJson(String json) { setSettingValue(CONFIG_JSON, json); } }
Методы getConfigJson и setConfigJson отвечают за получение и сохранение параметра в формате json.
Создадим webwork для управления настройками плагина
О��кроем терминал в папке плагина и выполним команду ниже:
create-atlas-jira-plugin-moduleНа вопросы в терминале отвечаем следующим образом:
Choose a number (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/29/30/31/32/33/34): 31 Enter Plugin Module Name My Webwork Module: : Config Show Advanced Setup? (Y/y/N/n) N: : Y Module Key config: : webwork-config Module Description The Config Plugin: : i18n Name Key config.name: : i18n Description Key config.description: : Enter Action Classname MyActionClass: : ConfigWebwork Enter Package Name ru.matveev.alexey.jira.tutorial.webworkui.jira.webwork: :Enter Alias ConfigWebwork: : Enter View Name success: : success.soy Enter Template Path /templates/webwork-config/configwebwork/success.soy.vm: : /templates/webwork-config/configwebwork/success.soy Add Another View? (Y/y/N/n) N: : N Add Another Action? (Y/y/N/n) N: : N Add Another Plugin Module? (Y/y/N/n) N: : N
В результате будет создан файл src/main/java/ru/matveev/alexey/jira/tutorial/webworkui/jira/webwork/ConfigWebwork.java. Этот файл нужно изменить вот так:
src/main/java/ru/matveev/alexey/jira/tutorial/webworkui/jira/webwork/ConfigWebwork.java
package ru.matveev.alexey.jira.tutorial.webworkui.jira.webwork; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.atlassian.jira.web.action.JiraWebActionSupport; import ru.matveev.alexey.jira.tutorial.webworkui.api.PluginSettingService; import javax.inject.Inject; public class ConfigWebwork extends JiraWebActionSupport { private static final Logger log = LoggerFactory.getLogger(ConfigWebwork.class); private final PluginSettingService pluginSettingService; private String configJson; @Inject public ConfigWebwork(PluginSettingService pluginSettingService) { this.pluginSettingService = pluginSettingService; } @Override public String execute() throws Exception { super.execute(); return SUCCESS; } public void doSave() { pluginSettingService.setConfigJson(configJson); } @ActionViewData public String getConfigJson() { return pluginSettingService.getConfigJson().isEmpty()?"{}":pluginSettingService.getConfigJson(); } public void setConfigJson(String json) { this.configJson = json; } }
Аннотация @ActionViewData необходима для того, чтобы параметр configJson был доступен в soy шаблоне.
Создадим web section и web item
Мы добавили webwork. Теперь добавим пункт меню, из которого будет запускаться webwork.
Открываем терминал и выполняем следующую команду:
create-atlas-jira-plugin-module Отвечаем на вопросы следующим образом:
Choose a number (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/29/30/31/32/33/34): 30 Enter Plugin Module Name My Web Section: : Webwork Config Section Enter Location (e.g. system.admin/mynewsection): admin_plugins_menu Show Advanced Setup? (Y/y/N/n) N: : N Add Another Plugin Module? (Y/y/N/n) N: : Y Choose a number (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/29/30/31/32/33/34): 25 Enter Plugin Module Name My Web Item: : Webwork Config Item Enter Section (e.g. system.admin/globalsettings): admin_plugins_menu/webwork-config-section Enter Link URL (e.g. /secure/CreateIssue!default.jspa): /secure/ConfigWebwork.jspa? Show Advanced Setup? (Y/y/N/n) N: : N Add Another Plugin Module? (Y/y/N/n) N: : N
В результате мы создали пункт меню на странице Add-ons.
Создаем soy шаблон
Подробно про soy шаблоны можно почитать тут.
Мы создадим файл
src/main/resources/templates/webwork-config/configwebwork/success.soy.
src/main/resources/templates/webwork-config/configwebwork/success.soy
{namespace webwork.config} /** * This template is needed for drawing the formview. */ {template .formview} {@param configJson: string} {webResourceManager_requireResource('ru.matveev.alexey.jira.tutorial.webworkui.webwork-soy-require-backbone:webwork-soy-require-backbone-resources')} <html> <head> <meta charset="utf-8"/> <meta name="decorator" content="atl.admin"> <meta name="admin.active.section" content="admin_plugins_menu/telegram-config-section"> <meta name="admin.active.tab" content="telegram-general-config-item"> <title>my page page</title> </head> <body> <div id="container"> <form class="aui" action="ConfigWebwork!save.jspa" method="POST"> <div class="field-group"> <label for="configJson">Json</label> <input class="text long-field" type="text" id="configJson" name="configJson" placeholder="Json String" value="{$configJson}"> <div class="description">the configJson Parameter</div> </div> <div class="buttons-container"> <div class="buttons"> <input class="button submit" type="submit" value="Save" id="config-save-button"> <a class="cancel" href="#">Cancel</a> </div> </div> </form> </div> </body> </html> {/template}
В файле atlassian-plugin.xml в тег web-resource нужно добавить ссылку на созданный soy шаблон:
<resource type="soy" name="webwork-config" location="/templates/webwork-config/configwebwork/success.soy"/>
Теперь внесем изменения в atlassian-plugin.xml для того, чтобы при обращении к webwork был бы отображен созданный soy шаблон:
<view name="success" type="soy">:webwork-soy-require-backbone-resources/webwork.config.formview</view>
webwork-soy-require-backbone-resources — это атрибут name в теге web-resource, куда мы добавили ссылку на наш soy шаблон.
webwork.config.formview — namespace и название шаблона из soy файла.
Протестируем плагин
Откроем терминал в папке плагина и выполним следующую команду:
atlas-runПосле того как Jira запустится нужно перейти в браузере по следующей ссылке:
localhost:2990/jira/secure/ConfigWebwork.jspa
Экран будет выглядеть вот так:

Можно попробовать ввести данные в поле Json и сохранить. Webwork работает.
Теперь нам нужно сделать так, чтобы было два экрана для заполнения параметров, и на последнем экране кнопка Save должна приводить все параметры в json формат и сохранять в настройках плагина.
Для управления логикой перемещения по экранам и приведением параметров в json формат мы будем использовать backbone js. Про backbone js можно почитать тут.
Создадим backbone модель
src/main/resources/js/webwork-config-model.js
define('webwork/config/model', [
'jquery',
'backbone',
'underscore'
], function($, Backbone, _) {
var WebConfigModel = Backbone.Model.extend({
defaults: {
parameter1: '',
parameter2: '',
parameter3: '',
parameter4: ''
}
});
return {
Model: WebConfigModel
};
})
'jquery',
'backbone',
'underscore'
], function($, Backbone, _) {
var WebConfigModel = Backbone.Model.extend({
defaults: {
parameter1: '',
parameter2: '',
parameter3: '',
parameter4: ''
}
});
return {
Model: WebConfigModel
};
})
Для того, чтобы модель была доступна при загрузке soy шаблона, файл с моделью необходимо добавить в atlassian-plugin.xml в тег web-resource:
<resource type="download" name="webwork-config-model.js" location="/js/webwork-config-model.js"/>
Создадим backbone вью
Я написал в коде комментарии для важных моментов.
src/main/resources/js/webwork-config-view.js
//define это директива requirejs и определяет модель как модуль webwork/config/view. Это позволяет нам определять зависимости в других файлах от модели.
define('webwork/config/view', [
'jquery',
'backbone',
'underscore'
], function($, Backbone, _) {
«use strict»;
var AppView = Backbone.View.extend({
events: {
«click #config-save-button»: «saveConfig»,
«click #next-button»: «nextButton»,
«click #back-button»: «prevButton»
},
// функция, которая работает по кнопке Save. Сохраняет параметры с экрана в модель и преобразует параметры в json формат
saveConfig: function(){
this.model.set(«parameter3», $("#parameter3").val());
this.model.set(«parameter4», $("#parameter4").val());
$("#configJson").val(JSON.stringify(this.model));
},
// функция, которая работает по кнопке Next на первом экране. Сохраняет параметры с первого экрана в модель и рисует второй экран
nextButton: function(){
this.model.set(«parameter1», $("#parameter1").val());
this.model.set(«parameter2», $("#parameter2").val());
var template = webwork.config.page2({configJson:$("#configJson").val(), parameter3:this.model.get('parameter3'), parameter4:this.model.get('parameter4')});
$("#container").replaceWith(template);
$("#configJson").val(JSON.stringify(this.model));
},
// функция, которая работает по кнопке Back на втором экране. Сохраняет параметры со второго экрана в модель и рисует первый экран
prevButton: function(){
this.model.set(«parameter3», $("#parameter3").val());
this.model.set(«parameter4», $("#parameter4").val());
var template = webwork.config.page1({configJson:$("#configJson").val(), parameter1:this.model.get('parameter1'), parameter2:this.model.get('parameter2')});
$("#container").replaceWith(template);
$("#configJson").val(JSON.stringify(this.model));
},
initialize: function(){
this.render();
},
render: function(){
var template = webwork.config.page1({configJson:$("#configJson").val(), parameter1:this.model.get('parameter1'), parameter2:this.model.get('parameter2')});
$("#container").replaceWith(template);
},
// это ссылка на главный контейнер. Вью будет ловить все события от элементов ниже этого элемента
el: '#maincontainer'
});
return {
View: AppView
};
})
define('webwork/config/view', [
'jquery',
'backbone',
'underscore'
], function($, Backbone, _) {
«use strict»;
var AppView = Backbone.View.extend({
events: {
«click #config-save-button»: «saveConfig»,
«click #next-button»: «nextButton»,
«click #back-button»: «prevButton»
},
// функция, которая работает по кнопке Save. Сохраняет параметры с экрана в модель и преобразует параметры в json формат
saveConfig: function(){
this.model.set(«parameter3», $("#parameter3").val());
this.model.set(«parameter4», $("#parameter4").val());
$("#configJson").val(JSON.stringify(this.model));
},
// функция, которая работает по кнопке Next на первом экране. Сохраняет параметры с первого экрана в модель и рисует второй экран
nextButton: function(){
this.model.set(«parameter1», $("#parameter1").val());
this.model.set(«parameter2», $("#parameter2").val());
var template = webwork.config.page2({configJson:$("#configJson").val(), parameter3:this.model.get('parameter3'), parameter4:this.model.get('parameter4')});
$("#container").replaceWith(template);
$("#configJson").val(JSON.stringify(this.model));
},
// функция, которая работает по кнопке Back на втором экране. Сохраняет параметры со второго экрана в модель и рисует первый экран
prevButton: function(){
this.model.set(«parameter3», $("#parameter3").val());
this.model.set(«parameter4», $("#parameter4").val());
var template = webwork.config.page1({configJson:$("#configJson").val(), parameter1:this.model.get('parameter1'), parameter2:this.model.get('parameter2')});
$("#container").replaceWith(template);
$("#configJson").val(JSON.stringify(this.model));
},
initialize: function(){
this.render();
},
render: function(){
var template = webwork.config.page1({configJson:$("#configJson").val(), parameter1:this.model.get('parameter1'), parameter2:this.model.get('parameter2')});
$("#container").replaceWith(template);
},
// это ссылка на главный контейнер. Вью будет ловить все события от элементов ниже этого элемента
el: '#maincontainer'
});
return {
View: AppView
};
})
Для того, чтобы вью была доступна при загрузке soy шаблона, файл с вью необходимо добавить в atlassian-plugin.xml в тег web-resource:
<resource type="download" name="webwork-config-view.js" location="/js/webwork-config-view.js"/>
Создадим js file, чтобы настроить backbone модель и вью
src/main/resources/js/webwork-soy-require-backbone.js
require([
'webwork/config/view',
'webwork/config/model',
'jquery',
'backbone',
'underscore'
], function(webworkConfigView, webworkConfigModel, $, Backbone, _) {
var webworkConfigModel = new webworkConfigModel.Model(JSON.parse($("#configJson").val()));
var actionsView = new webworkConfigView.View({model: webworkConfigModel});
})
'webwork/config/view',
'webwork/config/model',
'jquery',
'backbone',
'underscore'
], function(webworkConfigView, webworkConfigModel, $, Backbone, _) {
var webworkConfigModel = new webworkConfigModel.Model(JSON.parse($("#configJson").val()));
var actionsView = new webworkConfigView.View({model: webworkConfigModel});
})
Наш js файл использует requirejs. Директива require позволяет добиться того, что файл будет загружен только после того, как все зависимости загружены. Мы определили следующие зависимости для нашего файла: webwork/config/view, webwork/config/model, query, backbone, underscore.
Добавим параметры необходимые для работы soy шаблонов
В тег web-resource в файле atlassian-plugin.xml нужно добавить:
<transformation extension="soy"> <transformer key="soyTransformer"/> </transformation> <resource name="success-soy.js" type="download" location="/templates/webwork-config/configwebwork/success.soy"/>
Эти параметры позволяют обращаться к soy шаблону в js файлах.
Внесем изменения в success.soy
Я добавил комментарии к важным моментам
src/main/resources/templates/webwork-config/configwebwork/success.soy
{namespace webwork.config} /** * Этот шаблон запускается сразу из webwork. Здесь выводится json параметр. Далее этот шаблон сразу перерисовывается шаблоном page1. Шаблон нужен для того, чтобы получить json параметр и заполнить backbone model. */ {template .formview} {@param configJson: string} {webResourceManager_requireResource('ru.matveev.alexey.jira.tutorial.webworkui.webwork-soy-require-backbone:webwork-soy-require-backbone-resources')} <html> <head> <meta charset="utf-8"/> <meta name="decorator" content="atl.admin"> <meta name="admin.active.section" content="admin_plugins_menu/telegram-config-section"> <meta name="admin.active.tab" content="telegram-general-config-item"> <title>my page page</title> </head> <body> <div id="maincontainer"> <div id="container"> <input class="text long-field hidden" type="text" id="configJson" name="configJson" placeholder="Json String" value="{$configJson}"> </div> </div> </body> </html> {/template} /** * Это шаблон первого экрана. Он содержит parameter1 и parameter2. */ {template .page1} {@param configJson: string} {@param parameter1: string} {@param parameter2: string} <div id="container"> <form class="aui"> <div class="field-group"> <label for="parameter1">Parameter 1</label> <input class="text long-field" type="text" id="parameter1" name="parameter1" placeholder="Parameter1 value" value="{$parameter1}"> <div class="description">Value of Parameter 1</div> </div> <div class="field-group"> <label for="parameter2">Parameter 2</label> <input class="text long-field" type="text" id="parameter2" name="parameter2" placeholder="Parameter2 value" value="{$parameter2}"> <div class="description">Value of Parameter 2</div> </div> <div class="field-group"> <input class="text long-field hidden" type="text" id="configJson" name="configJson" placeholder="Json String" value="{$configJson}"> </div> <div class="buttons-container"> <div class="buttons"> <a class="cancel" href="#">Cancel</a> <input class="button submit" type="submit" value="Next" id="next-button"> </div> </div> </form> </div> {/template} /** * Это шаблон второго экрана. Он содержит parameter3 и parameter4. */ {template .page2} {@param configJson: string} {@param parameter3: string} {@param parameter4: string} <div id="container"> <form class="aui" action="ConfigWebwork!save.jspa" method="POST"> <div class="field-group"> <label for="parameter1">Parameter 3</label> <input class="text long-field" type="text" id="parameter3" name="parameter3" placeholder="Parameter3 value" value="{$parameter3}"> <div class="description">Value of Parameter 3</div> </div> <div class="field-group"> <label for="parameter4">Parameter 4</label> <input class="text long-field" type="text" id="parameter4" name="parameter4" placeholder="Parameter4 value" value="{$parameter4}"> <div class="description">Value of Parameter 4</div> </div> <div class="field-group"> <input class="text long-field hidden" type="text" id="configJson" name="configJson" placeholder="Json String" value="{$configJson}"> </div> <div class="buttons-container"> <div class="buttons"> <input class="button submit" type="submit" value="Back" id="back-button"> <input class="button submit" type="submit" value="Save" id="config-save-button"> </div> </div> </form> </div> {/template}
Протестируем приложение
Открываем терминал в папке плагина и запускаем:
atlas-runПосле того как Jira запуститься, откройте браузер по ссылке:
http://localhost:2990/jira/secure/ConfigWebwork.jspaВы увидите следующий экран:

Заполните параметры и нажмите на кнопку Next. Появится следующий экран:

Заполните параметры 3 и 4 и нажмите на кнопку Save. Параметры будут сохранены в формате Json. Можно нажать на кнопку Back и вы перейдете к первому экрану.
Наш плагин работает.
