← Назад к статьям

Создание кастомных полей через ScriptRunner

Стандартные типы кастомных полей в Jira покрывают большинство потребностей, но иногда нужны поля с более сложной логикой: вычисляемые значения, динамические опции на основе других полей, интеграции с внешними системами. ScriptRunner позволяет создавать полностью кастомные поля, которые делают практически всё, что можно представить. В этой статье разберу практические примеры создания таких полей.

Типы кастомных полей в ScriptRunner

ScriptRunner позволяет создавать несколько типов кастомных полей:

  • Scripted Fields — вычисляемые поля, значения которых рассчитываются автоматически
  • Scripted Fields (Read Only) — только для чтения, показывают вычисляемые значения
  • Scripted Fields (Editable) — можно редактировать, с кастомной логикой сохранения
  • Template Fields — поля на основе шаблонов

Наиболее часто используемые — Scripted Fields, которые вычисляют значения на основе других полей задачи.

Создание простого вычисляемого поля

Начнём с простого примера: поле, которое показывает количество дней с момента создания задачи.

Создайте новое поле через: Administration → Issues → Custom fields → Create Custom Field. Выберите тип "Scripted Field (read-only)" и добавьте скрипт:

import com.atlassian.jira.component.ComponentAccessor
import java.util.concurrent.TimeUnit

def created = issue.created
def now = new Date()
def diff = now.time - created.time
def days = TimeUnit.DAYS.convert(diff, TimeUnit.MILLISECONDS)

return "${days} дней"

Это поле будет автоматически вычисляться при каждом просмотре задачи и показывать количество дней с момента создания.

Вычисление на основе других полей

Часто нужно вычислять значение на основе других кастомных полей. Например, вычислять общее время на основе времени разработки и тестирования:

import com.atlassian.jira.component.ComponentAccessor

def customFieldManager = ComponentAccessor.getCustomFieldManager()

def devTimeField = customFieldManager.getCustomFieldObjectByName("Время разработки")
def testTimeField = customFieldManager.getCustomFieldObjectByName("Время тестирования")

def devTime = issue.getCustomFieldValue(devTimeField) as Integer ?: 0
def testTime = issue.getCustomFieldValue(testTimeField) as Integer ?: 0

def totalTime = devTime + testTime

return "${totalTime} часов"

Обратите внимание на использование as Integer ?: 0 — это безопасное приведение типа с значением по умолчанию, если поле пустое.

Работа с датами и временем

Работа с датами — частая задача в вычисляемых полях. Например, поле, показывающее дедлайн на основе даты создания и стандартного срока выполнения:

import java.util.Calendar

def created = issue.created
def standardDays = 14 // Стандартный срок выполнения в днях

def calendar = Calendar.getInstance()
calendar.setTime(created)
calendar.add(Calendar.DAY_OF_MONTH, standardDays)

def deadline = calendar.getTime()

// Форматирование даты
def formatter = new java.text.SimpleDateFormat("dd.MM.yyyy")
return formatter.format(deadline)

Можно также вычислять, просрочена ли задача:

import java.util.Calendar

def dueDateField = ComponentAccessor.getCustomFieldManager().getCustomFieldObjectByName("Due Date")
def dueDate = issue.getCustomFieldValue(dueDateField) as Date

if (!dueDate) {
    return "Нет дедлайна"
}

def now = new Date()
if (dueDate.before(now) && issue.resolution == null) {
    def diff = now.time - dueDate.time
    def daysOverdue = TimeUnit.DAYS.convert(diff, TimeUnit.MILLISECONDS)
    return "Просрочена на ${daysOverdue} дней"
} else if (issue.resolution) {
    return "Выполнена"
} else {
    return "В срок"
}

Агрегация данных из подзадач

Полезный пример — вычисление суммарного времени из подзадач:

import com.atlassian.jira.component.ComponentAccessor

def issueManager = ComponentAccessor.getIssueManager()
def subTasks = issueManager.getSubTaskObjects(issue.id)

if (subTasks.empty) {
    return "Нет подзадач"
}

def timeField = ComponentAccessor.getCustomFieldManager().getCustomFieldObjectByName("Время выполнения")
def totalTime = 0

subTasks.each { subTask ->
    def time = subTask.getCustomFieldValue(timeField) as Integer ?: 0
    totalTime += time
}

return "${totalTime} часов"

Аналогично можно вычислять процент выполнения на основе подзадач:

import com.atlassian.jira.component.ComponentAccessor

def issueManager = ComponentAccessor.getIssueManager()
def subTasks = issueManager.getSubTaskObjects(issue.id)

if (subTasks.empty) {
    return "0%"
}

def completed = subTasks.count { it.resolution != null }
def total = subTasks.size()
def percent = (completed * 100 / total) as Integer

return "${percent}% (${completed}/${total})"

Работа с связанными задачами

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

import com.atlassian.jira.component.ComponentAccessor

def issueLinkManager = ComponentAccessor.getIssueLinkManager()
def links = issueLinkManager.getInwardLinks(issue.id)

def blockers = links.findAll { link ->
    def linkedIssue = link.getSourceObject()
    link.issueLinkType.name == "Blocks" && linkedIssue.resolution == null
}

return blockers.size() > 0 ? "${blockers.size()} блокеров" : "Нет блокеров"

Кастомные поля с редактированием

Scripted Fields могут быть редактируемыми. В этом случае нужно определить логику получения и сохранения значения.

Для редактируемого поля нужно указать два скрипта: один для получения значения (getter), другой для сохранения (setter). Создайте поле типа "Scripted Field (editable)".

Getter (получение значения)

// Получаем сохранённое значение из другого поля или вычисляем
def valueField = ComponentAccessor.getCustomFieldManager().getCustomFieldObjectByName("Hidden Value Field")
def value = issue.getCustomFieldValue(valueField) as String

return value ?: ""

Setter (сохранение значения)

import com.atlassian.jira.issue.Issue
import com.atlassian.jira.issue.MutableIssue
import com.atlassian.jira.component.ComponentAccessor

def issueManager = ComponentAccessor.getIssueManager()
def mutableIssue = issueManager.getIssueObject(issue.id) as MutableIssue

// Сохраняем значение в скрытое поле для последующего использования
def valueField = ComponentAccessor.getCustomFieldManager().getCustomFieldObjectByName("Hidden Value Field")
mutableIssue.setCustomFieldValue(valueField, fieldValue)

def currentUser = ComponentAccessor.getJiraAuthenticationContext().getLoggedInUser()
issueManager.updateIssue(currentUser, mutableIssue, com.atlassian.jira.event.type.EventDispatchOption.DO_NOT_DISPATCH, false)

Интеграция с внешними системами

Кастомные поля могут получать данные из внешних систем. Например, показывать статус деплоя из CI/CD:

import groovy.json.JsonSlurper
import java.net.http.HttpClient
import java.net.http.HttpRequest
import java.net.http.HttpResponse
import java.net.URI

def issueKey = issue.key
def ciUrl = "https://ci.company.com/api/deploy-status/${issueKey}"

try {
    def client = HttpClient.newHttpClient()
    def request = HttpRequest.newBuilder()
        .uri(URI.create(ciUrl))
        .GET()
        .build()
    
    def response = client.send(request, HttpResponse.BodyHandlers.ofString())
    
    if (response.statusCode() == 200) {
        def json = new JsonSlurper().parseText(response.body())
        return json.status ?: "Неизвестно"
    } else {
        return "Ошибка получения статуса"
    }
} catch (Exception e) {
    log.error("Failed to get deploy status: ${e.message}")
    return "Недоступно"
}

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

Условная логика в полях

Поля могут показывать разное значение в зависимости от условий. Например, показывать разные форматы времени в зависимости от типа задачи:

def issueType = issue.issueType.name

if (issueType == "Bug") {
    def priority = issue.priority?.name ?: "Medium"
    return "Приоритет: ${priority}"
} else if (issueType == "Story") {
    def storyPoints = ComponentAccessor.getCustomFieldManager()
        .getCustomFieldObjectByName("Story Points")
    def points = issue.getCustomFieldValue(storyPoints)
    return "Story Points: ${points ?: 'Не оценено'}"
} else {
    return "Тип: ${issueType}"
}

Производительность вычисляемых полей

Вычисляемые поля выполняются при каждом просмотре задачи. Поэтому важно следить за производительностью:

  • Избегайте сложных операций — не делайте множественные запросы к БД или внешним API
  • Кэшируйте результаты — для тяжёлых вычислений используйте кэширование
  • Оптимизируйте запросы — если нужны данные из других задач, старайтесь минимизировать количество запросов

Пример кэширования результата:

import com.atlassian.cache.Cache
import com.atlassian.cache.CacheManager
import com.atlassian.jira.component.ComponentAccessor

def cacheManager = ComponentAccessor.getComponent(CacheManager)
def cache = cacheManager.getCache("scripted-field-cache", String.class, String.class)

def cacheKey = "field-${issue.id}-${issue.updated.time}"
def cachedValue = cache.get(cacheKey)

if (cachedValue) {
    return cachedValue
}

// Выполняем вычисление
def computedValue = performExpensiveCalculation()

// Кэшируем на 5 минут
cache.put(cacheKey, computedValue)

return computedValue

Типичные ошибки

Вот ошибки, которые часто допускают при создании кастомных полей:

Ошибка 1: Null pointer exceptions

Всегда проверяйте значения на null перед использованием. Используйте безопасный доступ (?.) и оператор Elvis (?:).

Ошибка 2: Медленные вычисления

Вычисления выполняются при каждом просмотре задачи. Если вычисление тяжёлое, пользователи будут ждать. Оптимизируйте или кэшируйте.

Ошибка 3: Изменение задачи в getter

В getter (скрипт получения значения) нельзя изменять задачу. Это приведёт к ошибкам и может вызвать бесконечные циклы. Изменения делайте только в setter или через слушатели событий.

Выводы

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

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

Используйте кэширование для тяжёлых вычислений, всегда проверяйте значения на null, тестируйте поля на разных типах задач и в разных контекстах.

Если нужна помощь с созданием кастомных полей — свяжитесь со мной.