Jira plugins: some examples of a successful bicycle invention





We at Mail.ru Group put a lot of effort into the development of Atlassian products and, in particular, Jira. Thanks to our efforts, plug-ins MyGroovy, JsIncluder, My Calendar, My ToDo and others saw the light. We develop and actively use all these plugins within the company.



We receive a lot of requests from related departments to introduce new features. Sometimes this translates into new plugins, but more often we solve tasks using existing plugins, since most everyday tasks are easily covered by them.



To conduct excursions in the office, it was necessary to provide for the creation of requests with checking intersecting excursions. For testers - to make a mechanism for tracking the stages of testing with the person responsible for the implementation. Technical support wanted to automatically access the knowledge base.



Today I will tell you how, by combining plug-ins, I managed to solve these problems.



Request from the "guides"



Tools:





Problem



In the office of Mail.ru Group there are many "guides" who agree with the guests and then set tasks for the AXO. Sometimes it happens that several excursions can take place at the same time - then several groups go to the office at the same time, or one guide is refused, and he goes to negotiate with the guests.



Decision



  1. The appearance in the task of “slots” (date and time from a set of free options) for selection when creating an application for a tour For the day - 3 slots. For example:



    • 9 a.m. - 10 a.m.

    • 17: 30-18: 30

    • 20: 00-21: 00



    If a slot was selected in another task, you cannot offer it for selection in a new one. You also need the ability to remove the slots from the selection by hand (in the case, for example, when excursions in the office are impossible in principle).
  2. The emergence of a calendar, formed from free and busy slots, which can be shared on guides.


Implementation



Step 1 : add the required fields to the request creation screen.



To do this, create the “Date” field of the Date type and the “Tour time” field of the Radiobutton type to select one value from 3 options (9: 00-10: 00; 17: 30-18: 30; 20: 00-21: 00).



Step 2 : create a calendar.



Making a new calendar. We aim it through JQL to our project with excursions,

indicate Event start the “Date” field created earlier, and also add the “Excursion Time” field created earlier to the display.







Save the calendar. Now our tours can be viewed on the calendar.







Step 3 : we limit the creation of excursions and add a banner with a link to the calendar.



To achieve this, you need JS that will track the change in the Date field. When the date is selected, we must substitute it in the jql function and get all requests for this date, then we will find out what time is taken and hide these options on the screen to make it impossible to choose the time taken.





When there are no requests





When there are 2 requests at 9 am and at 20 pm



(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(` <div id="bannerWithInfo" class="aui-message info"> <p class="title">     </p> <p>   </p> <p>      </p> <p>         </p> <p><a href='https://jira.ru/secure/MailRuCalendar.jspa#calendars=492' target="_blank"> </a></p> </div> `); })(AJS.$);
      
      





Request from testers



Tool:





Problem



In the request, you need to configure the display of the testing stages with the indication of the employee responsible for the task. It should be seen that the stage has not yet been completed, or the stage is completed (and who conducted it).



Decision



Configure the scripted field type field to display the stages of testing and associate with the workflow, record in the transition responsible for the stage of the author.



Implementation



  1. Create a “Progress” field of type scripted field.
  2. Create fields of type UserPicker that correspond to the stages of testing.



    For example, define the following steps and create UserPicker fields with the same names:



    • Basic information collected

    • Localized

    • Logs collected

    • Played

    • Responsible found



  3. We configure the workflow so that responsible people fill in the transitions.



    For example, the “Localized” transition writes currentUser in the “Localized” UserPicker field.
  4. Customize the display using a scripted field.


Fill the groovy block:



 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 = "<table class='aui'>" approval.each{k,v-> html += """<tr> <td ${v?"bgcolor='${colorApprove}'":"bgcolor='${colorNotApprove}'"}>${k}</td> <td ${v?"bgcolor='${colorApprove}'":"bgcolor='${colorNotApprove}'"}>${v?displayUser(v):""}</td> </tr>""" } html += "</table>" return html } def displayUser(user){ "<a href=${baseUrl}/secure/ViewProfile.jspa?name=${user.name}>${user.displayName}</a>" } 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) }
      
      





In the velocity block, print $ value. We get the following result:







Support Request



Tools:





Problem



Technical support has its own knowledge base on Confluence. Need the ability to display issue-related knowledge base articles in a Jira query. We also need a mechanism to keep the database up to date - if the article was not useful, you need to request a technical writer in Jira to write the current article. When closing a request, only articles related to the request should remain. Links can only be visible to tech support.



Decision



When choosing a specific type of access in Jira (cascading type field), the query should display articles with Confluence that correspond to it in a separate field with wiki markup.



If used successfully, the article is selected as relevant using the checkbox mark.



When solving a problem, if it is not described in the attached article, a task should be created in Jira with the type “Documentation” associated with the current request.



Implementation



Step 1 : preparation



  1. Create a Text Field (multi-line) with wiki markup - Links.
  2. Create a field of type Select List (cascading) - "Call Type".



    For example, we use the following values:



    • ACCOUNT

    • Hardware

  3. We’ll prepare labels for articles that will connect articles on Confluence with queries in Jira:



    • Change AD group memberships - officeit_jira_ad_group_addresses_ad

    • Subscribing / unsubscribing from a newsletter - officeit_jira_subscription_subscription_of_submission

    • Granting access to a folder - officeit_jira_sharing_access_to_folder

    • Reset password from domain KM - officeit_jira_reset_password_of_domain_uz

    • Reset mail password - officeit_jira_reset_password_mail_post

    • Temporary equipment issuance - officeit_jira_ temporary equipment issuance

    • Issue of new equipment - officeit_jira_new_new_technique

    • Replacing the hard drive and installing the system from scratch - officeit_jira_replace_hard_drive_and_install_system_s_ zero

    • Replacing a hard drive with transferring information - officeit_jira_replacing_hard_drive_with_data transfer_information

    • Replacing defective / obsolete equipment - officeit_jira_replacing_ faulty_ obsolete_ equipment



    Next, you need to create articles on Confluence, put them labels.

  4. Preparing the workflow.



    The type of appeal will be filled when creating.



    Links are added to a separate screen and placed on the transition in close (in the example, the transition is called “Check actual Links”), we remember the transition id (necessary in the future to configure js).


Step 2 : MyGroovy post-function (add articles to the request)



 /* :   — 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<labels.size;i++){ if(i<labels.size-1){ result += "\"" +labels[i]+ "\"," }else{ result += "\"" +labels[i]+ "\"" } } result = URLEncoder.encode(result, "utf-8") return result } /*    —  . */ def wikiLinkFieldId = 50001L def requestTypeFieldValue = getCustomFieldValue(issue, 40001) if(!requestTypeFieldValue){ return "required field is empty" } def mainType = requestTypeFieldValue.getAt(null).toString() def subType = requestTypeFieldValue.getAt('1').toString() /*     ,       : [TEST    1 (    AD)|https://confluence.ru/pages/viewpage.action?pageId=500001]. */ String labels = getLabelFromMap(mainType,subType) if(labels==""){ return "no avalible position on LabelMap" } def api = "/rest/api/content/search?cql=label%20in(${labels})" def URL = (url+api) def wikiString = "" def resp = "curl -u ${usr}:${pas} -X GET ${URL}".execute().text def result = parseText(resp) def ids = result.results.id def title = result.results.title for (def i=0;i<ids.size;i++){ wikiString += "[${title[i]}|${url+browse+ids[i]}]\n" } updateCustomFieldValue(issue,wikiLinkFieldId,wikiString) return "Done"
      
      









Step 3 : JS script



 /* :  — 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 = '<div class="field-group"><label for="'+idname+'">' + labelTxt +'</label><div id="'+idname+'"></div></div>' 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; $('<input />', { type: 'checkbox', id: checkboxCounter+id, value: value }).appendTo(container); $('<label />', { for: checkboxCounter+id, text: ' ' }).appendTo(container); $('<a />', { href: link, text: name,target: "_blank" }).appendTo(container); $('<br>').appendTo(container); } /*       ,   : */ renameButtonNeedNewArticle(); $(document).ready(function() { var val = AJS.$('#customfield_'+customFieldId+'').val(); AJS.$('#customfield_'+customFieldId+'').val(''); if(val==""){return;} var i = val.split('\n'); i.forEach(function( index ) { if(index == ""){return;} var link = index.split('|'); addCheckbox(link); }); }); /*          Links. */ $('#'+idname+' input[type="checkbox"]').change(function() { var prevalue = []; AJS.$('#'+idname+' input:checkbox:checked').each(function(){ prevalue.push(this.value); }); AJS.$('#customfield_'+customFieldId+'').val(arrayToString(prevalue)); if(prevalue.length<1){ renameButtonNeedNewArticle(); }else{ renameButtonDeleteUnchecked(); } }); })(AJS.$);
      
      





This is what our transition looks like before JS processing.







This is the transition after processing.







And so, if one or more articles are selected.







After the transition is completed, the Links field will be overwritten with the new value.



Step 4 : MyGroovy post-function (create a request for a new article)



On the Check actual Links transition, we write a script that creates a request of the “Documentation” type if there are no values ​​in the Links field.



Finally



These solutions would not have appeared without the active participation of colleagues - primarily those who actively use ready-made tools or who are faced with tasks that need to be automated. It often turns out that an interesting task is already half the solution: then you just need to choose the tool that most efficiently, simply and easily (for the end user) satisfies your needs. Now, perhaps, you have questions and suggestions that could make the presented plugins even better - write in the comments.



All Articles