
Mail.ru集团的我们在Atlassian产品(特别是Jira)的开发上投入了大量精力。 由于我们的努力,插件MyGroovy,JsIncluder,“我的日历”,“我的待办事项”和
其他插件得到了启发。 我们在公司内部开发并积极使用所有这些插件。
我们收到了相关部门的大量要求以介绍新功能。 有时这会转化为新的插件,但是由于我们日常的大部分任务很容易被它们覆盖,因此我们通常会使用现有的插件来解决任务。
为了在办公室进行短途旅行,必须规定创建请求并验证相交的短途旅行。 对于测试人员-建立一种与实施负责人一起监视测试阶段的机制。 技术支持希望自动访问知识库。
今天,我将告诉您如何通过组合插件来解决这些问题。
来自“指南”的请求
工具:
问题
Mail.ru集团办公室中有很多“向导”会与客人安排在一起,然后为AXO设置任务。 有时可能会发生几次短途旅行-然后几个小组同时去办公室,或者一位向导被拒绝,他去与客人谈判。
解决方案
- 创建一日游应用程序时,在任务中出现的“时段”(一组免费选项中的日期和时间)供选择-3个时段。 例如:
- 上午9点-上午10点
- 17:30-18:30
- 20:00-21:00
如果在另一个任务中选择了插槽,则无法在新任务中提供该插槽供选择。 您还需要能够从选择中手动删除插槽(例如,在原则上不可能进行办公室旅行的情况下)。 - 日历的外观,由空闲和忙碌的时段组成,可以在指南上共享。
实作
步骤1 :将必填字段添加到请求创建屏幕。
为此,请创建日期类型的“日期”字段和单选按钮类型的“游览时间”字段,以从3个选项(9:00-10:00; 17:30-18:30; 20:00-21:00)中选择一个值。
第2步 :创建日历。
制作新的日历。 我们会通过JQL将其瞄准我们的项目,
表示事件开始于较早创建的“日期”字段,并且还将较早创建的“游览时间”字段添加到显示中。

保存日历。 现在,我们的旅行可以在日历上查看了。
步骤3 :我们限制游览的创建,并添加带有指向日历链接的横幅。
为此,您需要JS,它将跟踪Date字段中的更改。 选择日期后,我们必须将其替换为jql函数并获取该日期的所有请求,然后我们才能确定花费了什么时间,并将这些选项隐藏在屏幕上以使其无法选择花费的时间。
没有请求时
当上午9点和晚上20点有2个请求时(function($){ $("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(); } 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(); var params = { jql: "issuetype = Events and cf[52500] is not EMPTY and cf[19620] = 20" + result, fields: "customfield_52500" }; $.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.$);
测试人员的要求
工具:
问题
在请求中,您需要配置测试阶段的显示以及负责该任务的员工的指示。 应该看到,该阶段尚未完成,或者该阶段已经完成(以及执行者)。
解决方案
配置脚本化的字段类型字段以显示测试阶段并与工作流程相关联,在负责作者阶段的过渡中进行记录。
实作
- 创建类型为脚本字段的“进度”字段。
- 创建与测试阶段相对应的UserPicker类型的字段。
例如,定义以下步骤并创建具有相同名称的UserPicker字段:
- 我们设置了工作流程,以便负责任的人员填写过渡。
例如,“本地化”过渡将currentUser写入“本地化” UserPicker字段。 - 使用脚本字段自定义显示。
填写常规块:
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'){
在速度块中,打印$值。 我们得到以下结果:

支持请求
工具:
问题
技术支持在Confluence方面拥有自己的知识库。 需要能够在Jira查询中显示与问题相关的知识库文章。 我们还需要一种使数据库保持最新状态的机制-如果本文没有用,则需要请Jira的技术作家来撰写当前文章。 关闭请求时,仅应保留与该请求相关的文章。 链接仅对技术支持可见。
解决方案
在Jira(级联类型字段)中选择特定的访问类型时,查询应在带有Wiki标记的单独字段中显示与Confluence相对应的文章。
如果成功使用,则使用复选框标记将文章选择为相关。
解决问题时,如果未在所附文章中进行说明,则应在Jira中创建一个任务,其类型与当前请求关联。
实作
步骤1 :准备
- 创建带有Wiki标记的文本字段(多行)-链接。
- 创建类型的字段选择列表(级联)-“呼叫类型”。
例如,我们使用以下值:
- 我们将为文章准备标签,以将Confluence上的文章与Jira中的查询联系起来:
- 更改广告组的成员身份-officeit_jira_ad_group_addresses_ad
- 订阅/取消订阅新闻通讯-officeit_jira_subscription_subscription_of_subscription
- 授予对文件夹的访问权限-officeit_jira_sharing_access_to_folder
- 从域KM重置密码-officeit_jira_reset_password_of_domain_uz
- 重置邮件密码-officeit_jira_reset_password_mail_post
- 临时设备发行-Officeit_jira_临时设备发行
- 发行新设备-Officeit_jira_new__new_technique
- 从头开始更换硬盘驱动器并安装系统-officeit_jira_replace_hard_drive_and_install_system_s_零
- 用传输信息替换硬盘驱动器-officeit_jira_replacing_hard_drive_with_data transfer_information
- 更换有缺陷/陈旧的设备-Officeit_jira_replacing_ faulty_作废_设备
接下来,您需要在Confluence上创建文章,并为其添加标签。
- 准备工作流程。
申诉类型将在创建时填写。
链接被添加到一个单独的屏幕中,并以紧密的方式放置在过渡中(在该示例中,过渡称为“检查实际链接”),我们会记住过渡ID(将来需要配置js)。
步骤2 :MyGroovy后期功能(在请求中添加文章)
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() 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"
第三步 :JS脚本
(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); 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); }); }); $('#'+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.$);
这就是我们在进行JS处理之前的过渡效果。

这是处理后的过渡。

因此,如果选择了一篇或多篇文章。

转换完成后,“链接”字段将被新值覆盖。
步骤4 :MyGroovy后期功能(创建对新文章的请求)
在“检查实际链接”过渡中,如果“链接”字段中没有任何值,我们将编写一个脚本来创建“文档”类型的请求。
总结
如果没有同事的积极参与,这些解决方案就不会出现-主要是那些积极使用现成工具或面临需要在工作中实现自动化的任务的同事。 通常,有趣的任务已经解决了一半:然后,您只需要选择最有效,最简单,最轻松(对于最终用户)即可满足您需求的工具。 现在,也许您有问题和建议可以使所展示的插件变得更好-在评论中写下。