Jira Plugins: alguns exemplos da invenção bem sucedida da bicicleta



Nós do Mail.ru Group nos esforçamos muito no desenvolvimento de produtos Atlassian e, em particular, Jira. Graças aos nossos esforços, os plug-ins MyGroovy, JsIncluder, My Calendar, My ToDo e outros viram a luz. Desenvolvemos e usamos ativamente todos esses plugins na empresa.

Recebemos muitas solicitações de departamentos relacionados para introduzir novos recursos. Às vezes, isso se traduz em novos plugins, mas com mais freqüência resolvemos tarefas usando os plugins existentes, pois a maioria das tarefas diárias é facilmente coberta por eles.

Para realizar excursões no escritório, era necessário prever a criação de solicitações com verificação das excursões cruzadas. Para testadores - criar um mecanismo para monitorar as etapas dos testes com a pessoa responsável pela implementação. O suporte técnico queria acessar automaticamente a base de conhecimento.

Hoje vou contar como, combinando plug-ins, consegui resolver esses problemas.

Pedido dos "guias"


Ferramentas:

  • Minha agenda
  • Js includer

O problema


Existem muitos “guias” no escritório do Grupo Mail.ru que combinam com os convidados e depois definem tarefas para o AXO. Às vezes acontece que várias excursões podem ocorrer ao mesmo tempo - então vários grupos vão ao escritório ao mesmo tempo, ou um guia é recusado e ele negocia com os convidados.

Solução


  1. A aparência na tarefa de "slots" (data e hora de um conjunto de opções gratuitas) para seleção ao criar um aplicativo para um tour do dia - 3 slots. Por exemplo:

    • 9h - 10h
    • 17: 30-18: 30
    • 20: 00-21: 00

    Se um slot foi selecionado em outra tarefa, você não pode oferecê-lo para seleção em um novo. Você também precisa remover os slots da seleção manualmente (no caso, por exemplo, quando excursões no escritório são impossíveis em princípio).
  2. A aparência de um calendário, formado a partir de slots livres e ocupados, que podem ser compartilhados em guias.

Implementação


Etapa 1 : adicione os campos obrigatórios à tela de criação de solicitação.

Para fazer isso, crie o campo "Data" do tipo Data e o campo "Hora da turnê" do tipo Botão de opção de rádio para selecionar um valor entre 3 opções (9: 00-10: 00; 17: 30-18: 30; 20: 00-21: 00).

Etapa 2 : crie um calendário.

Fazendo um novo calendário. O objetivo é através do JQL em nosso projeto com excursões,
indicar Início do evento no campo "Data" criado anteriormente e também adicionar o campo "Hora da excursão" criado anteriormente ao visor.



Salve o calendário. Agora, nossos passeios podem ser vistos no calendário.



Etapa 3 : limitamos a criação de excursões e adicionamos um banner com um link para o calendário.

Para conseguir isso, você precisa de JS, que acompanhará a alteração no campo Data. Quando a data é selecionada, devemos substituí-la na função jql e obter todas as solicitações para essa data. Descobriremos a que horas são necessárias e ocultamos essas opções na tela para tornar impossível a escolha da hora.


Quando não há solicitações


Quando existem 2 pedidos às 9 da manhã e às 20 da noite

(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.$); 

Pedido dos testadores


Ferramenta:

  • Meu groovy

O problema


Na solicitação, você precisa configurar a exibição dos estágios de teste com a indicação do funcionário responsável pela tarefa. Deve-se observar que a etapa ainda não foi concluída ou a fase foi concluída (e quem a conduziu).

Solução


Configure o campo do tipo de campo com script para exibir os estágios dos testes e associar-se ao fluxo de trabalho, registre a transição responsável pelo estágio do autor.

Implementação


  1. Crie um campo "Progresso" do tipo campo com script.
  2. Crie campos do tipo UserPicker que correspondem aos estágios do teste.

    Por exemplo, defina as seguintes etapas e crie campos UserPicker com os mesmos nomes:

    • Informações básicas coletadas
    • Localizada
    • Logs coletados
    • Jogado
    • Responsável encontrado

  3. Configuramos o fluxo de trabalho para que as pessoas responsáveis ​​preencham as transições.

    Por exemplo, a transição "Localizada" grava currentUser no campo UserPicker "Localizada".
  4. Personalize a exibição usando um campo com script.

Preencha o bloco de 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 = "<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) } 

No bloco de velocidade, imprima $ value. Temos o seguinte resultado:



Pedido de Suporte


Ferramentas:

  • Js includer
  • Meu groovy

O problema


O suporte técnico possui sua própria base de conhecimento sobre o Confluence. Precisa da capacidade de exibir artigos da base de conhecimento relacionados a problemas em uma consulta Jira. Também precisamos de um mecanismo para manter o banco de dados atualizado - se o artigo não foi útil, você precisa solicitar um escritor técnico em Jira para escrever o artigo atual. Ao fechar uma solicitação, apenas os artigos relacionados à solicitação devem permanecer. Os links só podem ser visíveis para o suporte técnico.

Solução


Ao escolher um tipo específico de acesso no Jira (campo do tipo em cascata), a consulta deve exibir artigos com o Confluence que correspondam a ele em um campo separado com marcação do wiki.

Se usado com sucesso, o artigo é selecionado como relevante usando a marca de caixa de seleção.

Ao resolver um problema, se ele não estiver descrito no artigo em anexo, uma tarefa deve ser criada no Jira com o tipo "Documentação" associado à solicitação atual.

Implementação


Etapa 1 : preparação

  1. Crie um campo de texto (várias linhas) com marcação wiki - Links.
  2. Crie um campo do tipo Selecione Lista (em cascata) - "Tipo de chamada".

    Por exemplo, usamos os seguintes valores:

    • CONTA
    • Hardware
  3. Prepararemos rótulos para artigos que conectarão artigos sobre o Confluence às consultas em Jira:

    • Alterar associações ao grupo do AD - officeit_jira_ad_group_addresses_ad
    • Assinando / cancelando a inscrição em um boletim informativo - officeit_jira_subscription_subscription_subscription
    • Concedendo acesso à pasta - officeit_jira_sharing_access_to_folder
    • Redefinir senha do KM do domínio - officeit_jira_reset_password_of_domain_uz
    • Redefinir senha do correio - officeit_jira_reset_password_mail_post
    • Emissão temporária de equipamentos - officeit_jira_ emissão temporária de equipamentos
    • Emissão de novos equipamentos - officeit_jira_new__new_technique
    • Substituindo o disco rígido e instalando o sistema a partir do zero - officeit_jira_replace_hard_drive_and_install_system_s_ zero
    • Substituindo um disco rígido pela transferência de informações - officeit_jira_replacing_hard_drive_with_data transfer_information
    • Substituindo equipamento defeituoso / obsoleto - officeit_jira_replacing_ defeituoso_ obsoleto_ equipamento

    Em seguida, você precisa criar artigos sobre o Confluence, colocar etiquetas para eles.
  4. Preparando o fluxo de trabalho.

    O tipo de apelação será preenchido ao criar.

    Os links são adicionados a uma tela separada e colocados na transição de perto (no exemplo, a transição é chamada “Verificar links reais”), lembramos o ID da transição (necessário no futuro para configurar js).

Etapa 2 : Função pós-MyGroovy (adicione artigos à solicitação)

 /* :   — 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" 



Etapa 3 : script 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 = '<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.$); 

É assim que nossa transição se parece antes do processamento do JS.



Esta é a transição após o processamento.



E assim, se um ou mais artigos forem selecionados.



Após a transição, o campo Links será substituído pelo novo valor.

Etapa 4 : Função pós-MyGroovy (crie uma solicitação para um novo artigo)

Na transição Verificar links reais, escrevemos um script que cria uma solicitação do tipo "Documentação" se não houver valores no campo Links.

Em conclusão


Essas soluções não apareceriam sem a participação ativa dos colegas - principalmente aqueles que usam ativamente ferramentas prontas ou que enfrentam tarefas que precisam ser automatizadas em seu trabalho. Muitas vezes acontece que uma tarefa interessante já é metade da solução: basta escolher a ferramenta que mais eficientemente, de forma simples e fácil (para o usuário final) satisfaça suas necessidades. Agora, talvez, você tenha perguntas e sugestões que poderiam tornar os plugins apresentados ainda melhores - escreva nos comentários.

Source: https://habr.com/ru/post/pt476446/


All Articles