
В этой статье расскажу, как мы реализовали гибкое многоэтапное согласование в Jira. Особенность подхода — все согласование зациклено в одном статусе, без громоздких схем workflow. Вся логика задается в Assets и управляется через Groovy‑скрипт.
Постановка задачи
Бизнес хотел видеть:
-
несколько последовательных этапов согласования;
-
возможность задавать количество этапов, согласующих, и условия этапа («все» или «любой»);
-
единый прозрачный результат в виде таблицы в задаче;
-
автоматический переход задачи в «Согласовано» или возврат в «В работу».
Инструменты и подход
-
Assets (бывший Insight) — хранение конфигурации согласования (этапы, согласующие, условия).
-
Groovy — основная логика.
-
JSON — два скрытых поля:
-
ApprovalConfig— структура согласования, формируется скриптом из Assets при старте процесса; -
ApprovalResult— динамический результат согласования (кто согласовал/отклонил, на каком этапе).
-
Архитектура решения
-
Согласование зациклено в одном статусе:
Задача не прыгает по workflow — все крутится внутри «Согласование». -
Цепочка согласования:
Выбирается пользователем из списка преднастроеных цепочек через поле справочник Assets object на экране создания задачи или на переходе в согласование
-
Два скрытых поля:
-
ApprovalConfig(JSON) — конфиг, формируется один раз на старте. Нужен чтобы скрипт переключал этапы. -
ApprovalResult(JSON) — результаты согласования, обновляются на каждом этапе. Используется для отрисовки таблицы результатов
-
-
Работа с пользователями:
-
В поле «Необходимо согласование» заносятся все согласующие текущего этапа.
-
Когда пользователь согласовал он удаляется оттуда и переносится в поле «Согласовано».
-
При переходе на следующий этап список «Необходимо согласование» обновляется.
-
-
Условия этапа:
-
«Все должны согласовать» или «достаточно одного». Настраивается в Assets.
-
-
Результат на экране:
-
Отдельный UI‑блок в задаче — табличка: Этап | Пользователь | Статус («Согласовано» / «Отказано»).
-
Реализация
1. Формирование цепочки и этапов согласования
Настройки в Assets (этапы, согласующие, условие ALL/ANY)
2. Формирование конфига
Groovy-скрипт берет данные из Assets и строит JSON-конфиг.
def getAllStepsData(){ def result = [steps: []] def stepCount = 1 //OBJECTID глобальная переменная, значение поля справочник Assets object if (!OBJECTID) return null //STEP_ATTR_IDS список из ID этапов на вкладке "цепочка согласования" в Assets STEP_ATTR_IDS.each { stepAttrId -> //assets - наша внутренняя библиотека реализующая //методы пакета com.riadalabs.jira.plugins.insight //в данном случае assets.getAttributeValue() //соответствует методу ObjectFacade.loadObjectAttributeBean() def stepIds = assets.getAttributeValue(OBJECTID, stepAttrId) if (!stepIds) return stepIds.each { stepId -> def type = assets.getAttributeValue(stepId, 26627)?.getAt(0) def status = assets.getAttributeValue(stepId, 26628)?.getAt(0) if (status != 1) return // обрабатываем только активные def approversList = [] def customApprover = assets.getAttributeValue(stepId, 26632)?.getAt(0) def approvers = assets.getAttributeValue(stepId, 26626) def stepName = assets.getAttributeValue(stepId, 26617)?.getAt(0) if (customApprover) { if (customApprover == "reporter") { approversList << issue.reporter.key } } else if (approvers) { approversList = approversList.plus(approvers) } if (approversList) { def stageStatus = (result.steps.isEmpty())?"in progress":"awaiting" result.steps << [ "step $stepCount": [ "approvers" : approversList, "stepName" : stepName, "type" : type, "stepStatus" : stageStatus ] ] } stepCount++ } } return result }
Итоговая структура записывается в поле ApprovalConfig
{ "steps": [ { "step 1": { "approvers": [ "fyulgushev", "iivanov" ], "stepName": "Согласующие первого этапа", "type": "Все", "stepStatus": "in progress" } }, { "step 2": { "approvers": [ "fyulgushev", "ppetrov" ], "stepName": "Согласующие второго этапа", "type": "Любой", "stepStatus": "awaiting" } }, { "step 3": { "approvers": [ "fyulgushev", "ssidorov" ], "stepName": "Согласующие третьего этапа", "type": "Все", "stepStatus": "awaiting" } }, { "step 4": { "approvers": [ "iivanov", "ppetrov" ], "stepName": "Согласующие четвертого этапа", "type": "Все", "stepStatus": "awaiting" } } ] }
3. Инициализация согласования
При старте заполняется поле «Необходимо согласование» пользователями из первого этапа.
В ApprovalResult создается структура для отрисовки таблицы согласования.
{ "steps": [ { "approverName": "fyulgushev", "dep": "Согласующие первого этапа", "status": "moved" }, { "approverName": "iivanov", "dep": "Согласующие первого этапа", "status": "moved" } ] }
4. Обработка согласования
Когда пользователь нажимает «Согласовать» или «Отклонить»:
-
его убираем из поля «Необходимо согласование»;
-
переносим в поле «Согласовано» (если согласовал) или в поле «Отказано» (если отказал);
-
обновляем
ApprovalResult.
def successJsonData(user){ JSONDATA["steps"].find{it["approverName"] == user.name && it.status == "moved"}.status = "success" //NEEDAPPROVE глобальная переменная со значением поля "Необходимо согласование" JSONDATA["steps"].removeIf { !(it["approverName"] in NEEDAPPROVE.collect{it.name}) && it["status"] == "moved"} //updateJsonField записывает структуру в поле ApprovalResult updateJsonField(ApprovalResultFieldID, JSONDATA) }
5. Проверка условий этапа
-
Если «ALL» → ждем всех согласующих.
-
Если «ANY» → достаточно одного согласия.
-
Если условие выполнено → этап закрывается, и переходим к следующему.
6. Переходы задачи
-
Если все этапы пройдены → заявка переводится в статус «Согласовано».
-
Если хоть один отказал → заявка возвращается в статус «В работе».
Визуализация результата
В задаче пользователи видят таблицу (рендерится из ApprovalResult):
JSONDATA = new JsonSlurper().parseText(DATA) def jsonSteps = JSONDATA.steps String html = """ <table class='aui myTable'> """ if (jsonSteps){ for (item in jsonSteps){ html += userData(item.approverName,item.dep,getStatus(item.status)) } } html += "</table>" return html def userData(userName,dep,status){ def user = getUser(userName) AvatarService avatarService = ComponentAccessor.getAvatarService() def userIconUrl = avatarService.getAvatarURL(ComponentAccessor.jiraAuthenticationContext.getLoggedInUser(), user.name).toString() def urlUser = baseURL+"/secure/ViewProfile.jspa?name=" def tr = "<tr>" tr += """ <th class="myTr"> <div> <div style="float: left;"> <img src=${userIconUrl} style="border-radius:50%;vertical-align:middle; width="32"; height="32";"> </div> <div style="margin-bottom: 10px; margin-left: 40px;"> <a href="${urlUser+user.name}">${user.displayName}</a> </div> </div> </th> """ tr += """<th class="myTr">${dep}</th>""" tr += """<th class="myTr">${status}</th>""" tr += "</tr>" return tr } def getStatus (key) { def map = [ success : "<span style=\"font-size: 12px;\" class=\"aui-lozenge aui-lozenge-success\">Согласовано</span>", moved : "<span style=\"font-size: 12px;\" class=\"aui-lozenge aui-lozenge-default\">Ожидает согласования</span>", error : "<span style=\"font-size: 12px;\" class=\"aui-lozenge aui-lozenge-removed\">Отклонено</span>", dialog : "<span style=\"font-size: 12px;\" class=\"aui-lozenge aui-lozenge-moved\">Обсуждение</span>" ] return map.get(key) }
Результат
-
Убрали громоздкий workflow.
-
Все согласование реализовано в одном статусе.
-
Настройки выносятся в Assets, администратор может управлять согласованием без переписывания скриптов.
-
Пользователи видят удобную табличку прогресса.
Ограничения и подводные камни
-
Придется поддерживать Groovy-код (проверка условий, обновление JSON, перезапуск согласования, обновление согласующих).
-
Визуализация требует кастомных скриптов.
Что можно улучшить
-
Объединить
ApprovalConfig и ApprovalResultв единую структуру -
Хранить JSON не в custom fields, а в issue properties
А как у вас организовано многоэтапное согласование? Используете Groovy/Assets или сторонние плагины с Marketplace?
ссылка на оригинал статьи https://habr.com/ru/articles/940626/
Добавить комментарий