Плагины Jira: несколько примеров успешного изобретения велосипеда

zkqcts9wiogkowchg77ul7e3xqi.jpeg

Мы в Mail.ru Group вкладываем много сил в развитие продуктов компании Atlassian и, в частности, Jira. Благодаря нашим усилиям свет увидели плагины My Groovy, JS Includer, My Calendar, My ToDo. Все эти плагины мы развиваем и активно используем внутри компании.

Мы получаем массу запросов от смежных подразделений по внедрению новых фич. Иногда это выливается в новые плагины, но чаще мы решаем поставленные задачи используя уже существующие плагины, так как большинство повседневных задач ими легко покрываются.

Для проведения экскурсий в офисе нужно было предусмотреть создание запросов с проверкой пересекающихся экскурсий. Для тестировщиков — сделать механизм отслеживания этапов тестирования с ответственным за выполнение. Техподдержка хотела получить автоматический доступ к базе знаний.

Сегодня я расскажу, как путем комбинирования плагинов удалось решить эти задачи.


Инструменты:

  • My Calendar
  • JS Includer


Проблема


В офисе Mail.ru Group много «экскурсоводов», которые договариваются с гостями и затем ставят задачи на АХО. Иногда случается так, что несколько экскурсий могут образоваться в одно и тоже время — тогда по офису одновременно ходят несколько групп, либо одному экскурсоводу отказывают, и он идет передоговариваться с гостями.

Решение


  1. Появление в задаче «слотов» (даты и времени из набора свободных вариантов) для выбора при создании заявки на экскурсию На день — 3 слота. Например:
    • 9:00–10:00
    • 17:30–18:30
    • 20:00–21:00

    Если слот был выбран в другой задаче, нельзя предлагать его для выбора в новой. Также нужна возможность убрать слоты из выбора руками (в случае, например, когда экскурсии в офисе невозможны в принципе).
  2. Появление календаря, формируемого из свободных и занятых слотов, который можно расшарить на экскурсоводов.


Реализация


Шаг 1: добавляем необходимые поля на экран создания запроса.

Для этого создадим поле «Дата» типа Date и поле «Время экскурсии» типа Radiobutton для выбора одного значения из 3 вариантов (9:00–10:00; 17:30–18:30; 20:00–21:00).

Шаг 2: создаем календарь.

Делаем новый календарь. Нацеливаем через JQL его на наш проект с экскурсиями,
указываем Event start созданное ранее поле «Дата», а так же добавляем в отображение созданное ранее поле «Время экскурсии».

daddc5487b6b205cd687a872c1736694.png

Сохраняем календарь. Теперь наши экскурсии можно просматривать в календаре.

b04cbf0f948315ea51b06a240aad90c3.png

Шаг 3: ограничиваем создание экскурсий и добавляем баннер с ссылкой на календарь.

Чтобы этого добиться, потребуется JS, который будет отслеживать изменение в поле «Дата». Когда выбрана дата, мы должны подставить ее в jql-функцию и получить все запросы на эту дату, затем узнаем какое время занято и прячем эти варианты на экране, чтобы лишить возможности выбрать занятое время.

6024d2f46d629821518c82f42da7e6a5.png
Когда нет запросов

5c1ed57b57361925498de2f40666d8d9.png
Когда есть 2 запроса на 9 утра и на 20 вечера

(function($){
/*
   Пояснение:

   Дата — customfield_19620
   Время экскурсии — customfield_52500
   Опции поля «Время экскурсии»:
   9:00-10:00 — 47611
   17:30-18:30 — 47612
   20:00-21:00 — 47613
*/

/*
   Сначала добавляем проверку значения в поле дата.
   Весь дальнейший код будет внутри этого блока.
*/
   $("input[name=customfield_19620]").on("click change", function(e) {
       var idOptions = [];
       var url = "/rest/api/latest/search";

/*
   Если «Дата» не выбрана, то скрываем выбор времени.
*/
       if (!$("#customfield_19620").val()) {
           $('input:radio[name=customfield_52500]').closest('.group').hide();
       }
/*
   Иначе берем значение из поля даты и переводим в удобный для подстановки в jql вид, так же выводим на экран все значения времени.
*/
       else {
           var temp = $("#customfield_19620").val();
           var arrDate = temp.split('.');
           var result = "" + arrDate[2].trim() + "-" + arrDate[1].trim() + "-" + arrDate[0].trim();
           $('input:radio[name=customfield_52500][value="-1"]').parent().remove();
           $('input:radio[name=customfield_52500]').closest('.group').show();
           $('input:radio[name=customfield_52500][value="47611"]').parent().show();
           $('input:radio[name=customfield_52500][value="47612"]').parent().show();
           $('input:radio[name=customfield_52500][value="47613"]').parent().show();
/*
   Затем подставляем в jql.
*/
           var params = {
               jql: "issuetype = Events and cf[52500] is not EMPTY and cf[19620] = 20" + result,
               fields: "customfield_52500"
           };
/*
   Далее в полученном JSON находим все запросы и скрываем использованное в них время с экрана.
*/
           $.getJSON(url, params, function (data) {
               var issues = data.issues
               for (var i = 0; i < issues.length; i++) {
                   idOptions.push(issues[i].fields.customfield_52500.id)
               }
               for (var k = 0; k < idOptions.length; k++) {
                   $('input:radio[name=customfield_52500][value=' + idOptions[k] + ']').parent().hide();
               }
           });
       }
   });
/*
   Добавляем баннер с ссылкой на календарь.
*/
   $('div.field-group:has(#customfield_19620)').last().before(`
       

Как работать с календарем

Выберите дату планируемой экскурсии

Затем выберите время экскурсии из доступных вариантов

По ссылке ниже вы можете посмотреть запланированные экскурсии в календаре

Календарь экскурсий

`); })(AJS.$);


Инструмент:

Проблема


В запросе нужно настроить отображение этапов тестирования с указанием ответственного за задачу сотрудника. Должно быть видно, что этап еще не завершен, либо этап завершен (и кто его проводил).

Решение


Настроить поле типа scripted field на отображение этапов тестирования и связать с workflow, записывать в ответственных за этап автора перехода.

Реализация


  1. Создаем поле «Ход выполнения» типа scripted field.
  2. Создаем поля типа UserPicker, соответствующие этапам тестирования.

    Для примера определим следующие этапы и создадим поля UserPicker с теми же названиями:

    • Базовая информация собрана
    • Локализовано
    • Логи собраны
    • Воспроизведено
    • Ответственный найден

  3. Настраиваем workflow так, чтобы на переходах заполнялись ответственные.

    Например переход «Локализовано» записывает currentUser в поле UserPicker «Локализовано».

  4. Настраиваем отображение при помощи scripted field.


Заполняем блок groovy:

import com.atlassian.jira.component.ComponentAccessor
import com.atlassian.jira.config.properties.APKeys

baseUrl = ComponentAccessor.getApplicationProperties().getString(APKeys.JIRA_BASEURL)
colorApprove = "#D2F0C2"
colorNotApprove = "#FDACAC"
return getHTMLApproval()

def getHTMLApproval(){
   def approval = getApproval()
   def html = ""
   approval.each{k,v->
       html += """"""
   }
   html += "
${k} ${v?displayUser(v):""}
" return html } def displayUser(user){ "${user.displayName}" } def getApproval(){ def approval = [:] as LinkedHashMap if (issue.getIssueTypeId() == '10001'){ //Тип запроса - Тестирование approval.put("Базовая информация собрана", getCfValue(54407)) approval.put("Логи собраны", getCfValue(54409)) approval.put("Воспроизведено", getCfValue(54410)) approval.put("Ответственный найден", getCfValue(54411)) approval.put("Локализовано", getCfValue(54408)) } return approval } def getCfValue(id){ ComponentAccessor.customFieldManager.getCustomFieldObject(id).getValue(issue) }


В блоке velocity выводим $value. Получаем такой результат:

e9747c0fe4fa0077c378e049d5f5643c.png


Инструменты:

  • JS Includer
  • My Groovy


Проблема


У техподдержки есть своя база знаний на Confluence. Нужна возможность отображать связанные с проблемой статьи из базы знаний в запросе Jira. Так же нужен механизм поддержки базы в актуальном состоянии — если статья не была полезной, нужно поставить запрос техническому писателю в Jira на написание актуальной статьи. При закрытии запроса должны остаться только статьи относящиеся к запросу. Ссылки могут быть видны только техподдержке.

Решение


При выборе определенного типа обращения в Jira (поле каскадного типа) в запросе должны отображаться статьи с Confluence, которые ему соответствуют в отдельном поле с wiki разметкой.

Статья при успешном использовании выбирается как актуальная с помощью отметки чекбокса.

При решении задачи, если оно не описано в прикрепленной статье, должна создаваться задача в Jira с типом «Документация», связанная с текущим запросом.

Реализация


Шаг 1: подготовка

  1. Создаем поле Text Field (multi-line) с wiki разметкой — Links.
  2. Создаем поле типа Select List (cascading) — «Тип обращения».

    Для примера используем следующие значения:

    • ACCOUNT
    • HARDWARE
  3. Заготовим лейблы для статей, которыми будем связывать статьи на Confluence с запросами в Jira:
    • Изменение членства в группах AD — officeit_jira_изменение_членства_в_группах_ad
    • Подписка/отписка от рассылки — officeit_jira_подписка_отписка_от_рассылки
    • Предоставление доступа к папке — officeit_jira_предоставление_доступа_к_папке
    • Сброс пароля от доменной УЗ — officeit_jira_сброс_пароля_от_доменной_уз
    • Сброс пароля от почты — officeit_jira_сброс_пароля_от_почты
    • Выдача временного оборудования — officeit_jira_выдача_временного_оборудования
    • Выдача новой техники — officeit_jira_выдача_новой_техники
    • Замена жесткого диска и установка системы с нуля — officeit_jira_замена_жесткого_диска_и_установка_системы_с_нуля
    • Замена жесткого диска с переносом информации — officeit_jira_замена_жесткого_диска_с_переносом_информации
    • Замена неисправного/устаревшего оборудования — officeit_jira_замена_неисправного_устаревшего_оборудования

    Далее необходимо создать статьи на Confluence, проставить им лейблы.
  4. Подготавливаем workflow.

    Тип обращения будем заполнять при создании.

    Links добавляем на отдельный экран и помещаем на переход в закрыть (в примере переход называется «Check actual Links»), запоминаем id перехода (необходимо в дальнейшем для настройки js).


Шаг 2: MyGroovy post-function (добавляем статьи в запрос)

/*
Пояснение:

Тип обращения — customfield_40001
Links — customfield_50001
*/

/*
Указываем куда, под кем и как будем подключаться.
*/

def usr = "bot"
def pas = "qwerty"
def url = "https://confluence.ru"
def browse = "/pages/viewpage.action?pageId="

/*
Добавляем методы
*/

def updateCustomFieldValue(issue, Long customFieldId, newValue) {
   def customField = ComponentAccessor.customFieldManager.getCustomFieldObject(customFieldId)
   customField.updateValue(null, issue, new ModifiedValue(customField.getValue(issue), newValue), new DefaultIssueChangeHolder())
   return issue
}
def getCustomFieldObject(Long fieldId) {
   ComponentAccessor.customFieldManager.getCustomFieldObject(fieldId)
}
def parseText(text) {
   def jsonSlurper = new JsonSlurper()
   return jsonSlurper.parseText(text)
}
def getCustomFieldValue(issue, Long fieldId) {
   issue.getCustomFieldValue(ComponentAccessor.customFieldManager.getCustomFieldObject(fieldId))
}

/*
Указываем скрипту, как соотносить типы обращения с лейблами.
*/

def getLabelFromMap(String main, String sub){
   def mapLabels = [
           "ACCOUNT": [
                   "Изменение членства в группах AD"        :["officeit_jira_изменение_членства_в_группах_ad"],
                   "Подписка/отписка от рассылки"        :["officeit_jira_подписка_отписка_от_рассылки"],
                   "Предоставление доступа к папке"        :["officeit_jira_предоставление_доступа_к_папке"],
                   "Сброс пароля от доменной УЗ"            :["officeit_jira_сброс_пароля_от_доменной_уз"],
                   "Сброс пароля от почты"                :["officeit_jira_сброс_пароля_от_почты"]
           ],
           "HARDWARE": [
                   "Выдача временного оборудования"        :["officeit_jira_выдача_временного_оборудования"],
                   "Выдача новой техники"                :["officeit_jira_выдача_новой_техники"],
                   "Замена жесткого диска и установка системы с нуля":["officeit_jira_замена_жесткого_диска_и_установка_системы_с_нуля"],
                   "Замена жесткого диска с переносом информации":["officeit_jira_замена_жесткого_диска_с_переносом_информации"],
                   "Замена неисправного/устаревшего оборудования":["officeit_jira_замена_неисправного_устаревшего_оборудования"]
           ]
   ]
   def labels = mapLabels[main][sub]
   def result = ""

   if(!labels){
       return ""
   }

   for (def i=0;i


959a98eac782a2e53bf6a8c37d7b9a12.png

Шаг 3: JS-скрипт

/*
Пояснение:

Переход — Check actual Links
id перехода — 10
Links — customfield_50001
*/

(function($){
  
   /*
   Вначале объявляем переменные, с которыми будем работать, прячем ненужное от посторонних глаз и делаем проверку что код будет выполняться для нужного нам перехода.
   */
  
   var buttonNewArticle = 'Необходима новая статья';
   var buttonDeleteUnchecked = 'Сохранить отмеченные';
   var buttonNewArticleTitle = 'Автоматически будет создан таск на новую статью';
   var buttonDeleteUncheckedTitle = 'Все неотмеченные статьи будут удалены.';
   var avalibleTransitions = [10];
   var currentTransition = parseInt(AJS.$('.hidden input[name^="action"]').val());
  
   if(avalibleTransitions.indexOf(currentTransition)==-1){
       console.log('Error: transition ' + currentTransition + ' is not avalible');
       return;
   }
  
   var customFieldId = 50001;
   var labelTxt = 'Выберите актуальные статьи';
   var idname = 'cblist';
   var checkboxCounter = 'cbsq';
   var text = '
' AJS.$('.field-group label[for^="customfield_'+customFieldId+'"]').parent().hide(); AJS.$('.field-group label[for^="comment"]').parent().hide(); $('.jira-dialog-content div.form-body').prepend(text); /* Далее пишем следующие функции: */ /* renameButtonNeedNewArticle и renameButtonDeleteUnchecked — меняем кнопку « Закрыть» в зависимости от того выбраны ли статьи или нужно создать новую addCheckbox — рисуем чекбокс напротив каждой статьи. */ function arrayToString(arrays) { return arrays.join('\n'); } function renameButtonNeedNewArticle() { $('#issue-workflow-transition-submit').val(buttonNewArticle); $('#issue-workflow-transition-submit').attr("title",buttonNewArticleTitle); } function renameButtonDeleteUnchecked() { $('#issue-workflow-transition-submit').val(buttonDeleteUnchecked); $('#issue-workflow-transition-submit').attr("title",buttonDeleteUncheckedTitle); } function addCheckbox(array) { var value = array.join('|'); var name = array[0].replace('[',''); var link = array[1].replace(']',''); var container = $('#'+idname); var inputs = container.find('input'); var id = inputs.length+1; $('', { type: 'checkbox', id: checkboxCounter+id, value: value }).appendTo(container); $('


Так выглядит наш переход до обработки JS.

662deb79f92137320bc5135130ccefdb.png

Так выглядит переход после обработки.

d4d71ec1f7ccef66c8d659b2c250a6de.png

И так, если выбрана одна или несколько статей.

cdc703ef953ed3247eaa66b8c0cbb37f.png

После выполнения перехода поле Links будет перезаписано новым значением.

Шаг 4: MyGroovy post-function (создаем запрос на новую статью)

На переходе Check actual Links пишем скрипт, который создает запрос с типом «Документация», если в поле Links нет значений.

В заключение


Эти решения не появились бы без активного участия коллег — в первую очередь тех, кто активно пользуется готовыми инструментами или сталкивается в своей работе с задачами, которые нужно автоматизировать. Часто оказывается, что интересная задача — это уже половина решения: далее нужно лишь подобрать инструмент, который наиболее эффективно, просто и легко (для конечного пользователя) удовлетворяет поставленным запросам. Теперь, возможно, у вас появились вопросы и предложения, которые могли бы сделать представленные плагины ещё лучше — пишите в комментариях.

© Habrahabr.ru