paint-brush
Экономия времени и нервов с помощью формул и плагина Jira Structureк@ipolubentcev
944 чтения
944 чтения

Экономия времени и нервов с помощью формул и плагина Jira Structure

к Ivan Polubentsev36m2023/10/29
Read on Terminal Reader

Слишком долго; Читать

Формулы с плагином Jira Structure могут быть ошеломляющими: улучшите свою игру, создавая таблицы, упрощая работу с задачами и анализируя релизы и проекты.
featured image - Экономия времени и нервов с помощью формул и плагина Jira Structure
Ivan Polubentsev HackerNoon profile picture
0-item
1-item

Плагин Structure для Jira очень полезен для повседневной работы с задачами и их анализа; он выводит визуализацию и структурирование заявок Jira на новый уровень и делает все это прямо из коробки.


И, не все это знают, но функциональность формул Структуры может просто поразить воображение. С помощью формул можно создавать чрезвычайно полезные таблицы, которые позволяют существенно упростить работу с задачами, а главное — пригодятся для более глубокого анализа релизов, эпиков и проектов.


Как насчет отображения диаграммы Burndown или отображения состояния заявки в таблице с задачами?


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


Итак, для кого этот текст? Кто-то может задаться вопросом, зачем писать статью, когда официальная документация на веб-сайте ALM Works прямо здесь и ждет, пока читатели покопаются в ней. Это правда. Однако я из тех людей, которые даже не имели ни малейшего представления о том, что за Structure скрывается такой широкий функционал: «Подожди, это же была опция с самого начала?!» Это осознание заставило меня задуматься: могут быть и другие люди, которые до сих пор не знают, что можно делать с формулами и структурой.


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


Каждый пример подробно разбирается, от описания проблемы до объяснения кода, достаточно тщательно, чтобы не оставалось вопросов. Разумеется, помимо пояснений, каждый пример иллюстрируется кодом, который вы можете опробовать самостоятельно, не углубляясь в анализ.


Если вам не хочется читать, но вас интересуют формулы, посетите вебинары ALM Works . Они объясняют основы за 40 минут; информация там представлена в очень сжатом виде.


Для понимания примеров не нужны никакие дополнительные знания, поэтому любой, кто работал с Jira и Structure, сможет без проблем повторить примеры в своих таблицах.


Разработчики предоставили в своем языке Expr довольно гибкий синтаксис. По сути, философия здесь такая: «пишите, как хотите, и это сработает».


Итак, начнем!


Зачем нам нужны формулы?

Итак, зачем нам вообще использовать формулы? Ну, иногда оказывается, что нам не хватает стандартных полей Jira, таких как «Правопреемник», «Story Points» и так далее. Или нам нужно посчитать какую-то сумму по определенным полям, отобразить оставшуюся мощность по версии и узнать, сколько раз задача меняла свой статус. Возможно, мы даже захотим объединить несколько полей в одно, чтобы нашу структуру было легче читать.


Чтобы решить эти проблемы, нам нужны формулы, и мы будем использовать их для создания настраиваемых полей.


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


Итак, если мы попросим формулу отобразить какое-нибудь поле Jira, например «Исполнитель», то формула будет применяться для каждой задачи, и у нас будет еще один столбец «Исполнитель».


Формулы состоят из нескольких основных сущностей:

  • Переменные — для доступа к полям Jira и сохранения промежуточных результатов.
  • Встроенные функции — выполняют предопределенную операцию, например, подсчитывают количество часов между датами или фильтруют данные в массиве.
  • Пользовательские функции — если нам нужны уникальные вычисления
  • Различные формы отображения результата, например, «Дата/Время», «Продолжительность», «Число» или «Вики-разметка» на ваш выбор.


Знакомство с формулами.

Мы познакомимся с формулами и их синтаксисом на некоторых примерах и рассмотрим шесть практических случаев.


Прежде чем рассматривать каждый пример, мы укажем, какие функции структуры мы используем; новые функции, которые еще не объяснены, будут выделены жирным шрифтом. Каждый из следующих примеров будет иметь возрастающий уровень сложности. Они расположены так, чтобы постепенно познакомить вас с важными особенностями формул.


Вот базовая структура, которую вы будете видеть каждый раз:

  • Проблема
  • Предлагаемое решение
  • Используемые особенности структуры
  • Пример кода
  • Анализ решения


Эти примеры охватывают самые разные темы: от сопоставления переменных до сложных массивов:

  • Два примера отображения дат начала и окончания работы над задачей (варианты с разным отображением)
  • Родительская задача — отображение типа и названия родительской задачи.
  • Сумма Story Points подзадач и статус этих оценок
  • Индикация последних изменений в статусе задачи.
  • Расчет рабочего времени без учета выходных (выходных) и дополнительных статусов


Создание формул

Для начала давайте разберемся, как создавать настраиваемые поля с помощью формул. В правой верхней части Структуры, в конце всех столбцов, есть значок «+» — нажмите на него. В появившемся поле напишите «Формула…» и выберите соответствующий пункт.


Создание формул


Сохранение формул

Давайте обсудим сохранение формулы. К сожалению, сохранить где-то отдельно конкретную формулу пока нет возможности (только в своем блокноте, как я). На вебинаре ALM Works команда упомянула, что работает над банком формул, но на данный момент единственный способ их сохранить — сохранить все представление вместе с формулой.


Когда мы закончим работу над формулой, нам нужно щелкнуть представление нашей структуры (скорее всего, оно будет отмечено синей звездочкой) и нажать «Сохранить», чтобы перезаписать текущее представление. Или вы можете нажать «Сохранить как…», чтобы создать новое представление. (Не забудьте сделать его доступным для других пользователей Jira, поскольку новые представления по умолчанию являются частными.)


Формула будет сохранена в остальных полях конкретного представления, и вы сможете увидеть ее на вкладке «Дополнительно» меню «Просмотр подробностей».


Начиная с версии 8.2, Structure теперь имеет возможность сохранять формулы в три быстрых клика.

Диалог сохранения доступен из окна редактирования формулы. Если это окно не открыто, просто нажмите на значок треугольника ▼ в нужном столбце.


Сохранение формул


В окне редактирования видим поле «Сохраненный столбец», справа значок с синим уведомлением, который означает, что изменения в формуле не сохранились. Нажмите на этот значок и выберите опцию «Сохранить как…».


Сохраненный столбец


Затем введите названия нашего столбца (формулы) и выберите, в каком месте его сохранить. «Мои колонки», если мы хотим сохранить их в личном списке. «Глобальный», чтобы формула сохранялась в общем списке, где ее смогут редактировать все пользователи вашей Структуры. Нажмите «Сохранить».


Нажмите «Сохранить»


Теперь наша формула сохранена. Мы можем загрузить его в любую структуру или пересохранить откуда угодно. При пересохранении формулы она обновится во всех структурах, в которых она используется.


Сопоставление переменных также сохраняется вместе с формулой, но о сопоставлении мы поговорим позже.


Теперь перейдем к нашим примерам!


Отображение дат начала и окончания работы над задачей

Пользовательские даты в последних двух столбцах

Проблема

Нам нужна таблица со списком задач, а также датами начала и окончания работы над этими задачами. Еще нам нужна таблица, чтобы экспортировать ее в отдельный Excel-Gantt. К сожалению, Jira и Structure не умеют предоставлять такие даты «из коробки».

Предложенное решение

Даты начала и окончания — это даты перехода в конкретные статусы, в нашем случае это «В работе» и «Закрыто». Нам нужно взять эти даты и отобразить каждую из них в отдельном поле (это необходимо для дальнейшего экспорта в Гантт). Итак, у нас будет два поля (две формулы).


Используемые особенности структуры

  1. Сопоставление переменных
  2. Возможность настройки формата отображения.


Пример кода

Поле для даты начала:

 firstTransitionToStart


Поле для даты окончания:

 latestTransitionToDone


Анализ решения

В этом случае код представляет собой одну переменную firstTransitionToStart для поля даты начала и lateTransitionToDone для второго поля.


Давайте сейчас сосредоточимся на поле даты начала. Наша цель — получить дату, когда задача перешла в статус «В работе» (это соответствует логическому началу задачи), поэтому переменная названа, совершенно явно, чтобы исключить необходимость последующих угадываний, как «первый переход в начинать".


Чтобы превратить дату в переменную, мы обратимся к сопоставлению переменных. Сохраним нашу формулу, нажав кнопку «Сохранить».


Нажмите, чтобы сохранить формулу


Наша переменная появилась в разделе «Переменные», рядом с ней появился восклицательный знак. Структура указывает, что она не может связать переменную с полем в Jira, и нам придется сделать это самостоятельно (т. е. сопоставить ее).


Нажмите на переменную и перейдите в интерфейс сопоставления. Выберите поле или нужную операцию — ищите операцию «Дата перехода…». Для этого в поле выбора напишите «переход». Вам будет предложено сразу несколько вариантов, и нам подойдет один из них: «Первый переход в Выполняется». Но чтобы продемонстрировать, как работает сопоставление, давайте выберем опцию «Дата перехода…».


Конфигурация сопоставления


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


Выберите или введите в «Статус» — «Статус: В работе» (или соответствующий статус в вашем Workflow), а в «Переходе» — «Первый переход в статус», так как начало работы над задачей — это самый первый переход. в соответствующий статус.


Выберите нужную категорию



Если бы вместо «Даты перехода…» мы выбрали изначально предложенный вариант «Первый переход в Выполняется», то результат был бы практически таким же — Структура сама бы выбрала за нас необходимые параметры. Единственное, вместо «Статус: В процессе» у нас будет «Категория: В процессе».


Разница между категорией статуса и статусом


Отмечу важную особенность: статус и категория – это две разные вещи. Статус — это конкретный статус, он однозначен, но категория может включать в себя несколько статусов. Всего три категории: «Сделать», «В процессе» и «Готово». В Jira они обычно обозначаются серым, синим и зеленым цветами соответственно. Статус должен принадлежать к одной из этих категорий.

Рекомендую в таких случаях указывать конкретный статус, чтобы не путать со статусами одной категории. Например, у нас есть два статуса категории «Сделать» на проекте: «Открыт» и «Очередь QA».


Вернемся к нашему примеру.


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


Общие показывает значение по умолчанию (в миллисекундах).


В то же время в нашем настраиваемом поле мы видим какие-то странные цифры, совсем не похожие на дату. В нашем случае результатом формулы будет значение переменной firstTransitionToStart, а ее значение — миллисекунды с января 1970 года. Чтобы получить правильную дату, нам нужно выбрать определенный формат отображения формулы.


Выбор формата находится в самом низу окна редактирования. По умолчанию там выбрано «Общее». Нам нужно «Дата/Время», чтобы дата корректно отображалась.


Выберите дату/время вместо общего.


Для второго поля, lateTransitionToDone, мы сделаем то же самое. Разница лишь в том, что при отображении мы уже можем выбрать категорию «Выполнено», а не статус (поскольку однозначный статус завершения задачи обычно один). В качестве параметра перехода выбираем «Последний переход», так как нас интересует самый последний переход в категорию «Готово».


Окончательный результат для двух полей будет выглядеть следующим образом.


Окончательный вид с датами


Теперь посмотрим, как добиться того же результата, но с собственным форматом отображения.


Отображение даты в нашем собственном формате

Пример пользовательского формата


Проблема

Нас не устраивает формат отображения даты из предыдущего примера, так как для таблицы Ганта нам нужен специальный — «01.01.2022».


Предложенное решение

Отобразим даты с помощью встроенных в Structure функций, указав подходящий нам формат.


Используемые особенности конструкции

  1. Сопоставление переменных
  2. Функции выражения


Пример кода

 FORMAT_DATETIME(firstTransitionToStart;"dd.MM.yyyy")


Анализ решения

Разработчики предусмотрели множество различных функций, в том числе отдельную для отображения даты в нашем формате: FORMAT_DATETIME; это то, что мы собираемся использовать. Функция использует два аргумента: дату и строку нужного формата.


Мы настраиваем переменную firstTransitionToStart (первый аргумент), используя те же правила сопоставления, что и в предыдущем примере. Второй аргумент — это строка, определяющая формат, и мы определяем его следующим образом: «дд.ММ.гггг». Это соответствует желаемой форме «01.01.2022».


Таким образом, наша формула сразу даст результат в нужном виде. Таким образом, мы можем оставить опцию «Общие» в настройках поля.


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


Финальный корм после трансформации


В принципе, существенных сложностей при работе с синтаксисом формул нет. Если вам нужна переменная, напишите ее имя; если вам нужна функция, опять же, просто напишите ее имя и передайте аргументы (если они необходимы).


Когда Structure встречает неизвестное имя, он предполагает, что это переменная, и пытается отобразить ее самостоятельно или обращается к нам за помощью.


Кстати, важное замечание: структура не чувствительна к регистру, поэтому firstTransitionToStart, firsttransitiontostart и firSttrAnsItiontOStarT — это одна и та же переменная. То же правило применимо и к функциям. Чтобы добиться однозначного стиля кода, в примерах мы постараемся придерживаться правил использования заглавных букв MSDN.


Теперь углубимся в синтаксис и рассмотрим специальный формат отображения результата.


Отображение имени родительской задачи

Имя родителя отображается перед сводкой.


Проблема

Мы работаем с обычными задачами (Задача, Ошибка и т. д.) и с задачами типа Story, имеющими подзадачи. В какой-то момент нам необходимо узнать, над какими задачами и подзадачами работал сотрудник в течение определенного периода.


Проблема в том, что многие подзадачи не дают информации о самой истории, так как они называются «работа над историей», «настройка» или, например, «активация эффекта». А если мы запросим список задач на определенный период, то получим десяток задач с названием «работа над историей» без какой-либо другой полезной информации.


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


Предложенное решение

В нашем проекте у нас есть два варианта, когда задача может иметь родителя:

  1. Задача является подзадачой, а ее родительским элементом является только история.
  2. Задача — это обычная задача (Задача, Ошибка и т. д.), и она может иметь или не иметь Epic, и в этом случае задача вообще не имеет родительского элемента.


Итак, мы должны:

  1. Узнайте, есть ли у задачи родительский элемент
  2. Узнайте тип этого родителя
  3. Определите тип и название этой задачи по следующей схеме: «[Родитель-тип] Родитель-имя».


Для упрощения восприятия информации раскрасим текст типа задания: то есть либо «[История]», либо «[Эпопея]».


Что мы будем использовать:

  1. Сопоставление переменных
  2. Состояние
  3. Доступ к полям задач
  4. Формат отображения — вики-разметка


Пример кода

 if( Parent.Issuetype = "Story"; """{color:green}[${Parent.Issuetype}]{color} ${Parent.Summary}"""; EpicLink; """{color:#713A82}[${EpicLink.Issuetype}]{color} ${EpicLink.EpicName}""" )


Анализ решения

Почему формула начинается с условия if, если нам нужно просто вывести строку и вставить туда тип и название задачи? Нет ли какого-нибудь универсального способа доступа к полям задач? Да, но для задач и эпиков эти поля называются по-другому и доступ к ним тоже нужен другой, это особенность Jira.


Различия начинаются на уровне родительского поиска. Для подзадачи родительский элемент находится в поле «Родительская задача» Jira, а для обычной задачи родителем будет эпик, расположенный в поле «Эпическая ссылка». Соответственно, нам придется написать два разных варианта доступа к этим полям.


Здесь нам нужно условие if. В языке Expr есть разные способы работы с условиями. Выбор между ними – дело вкуса.


Есть метод, похожий на Excel:

 if (condition1; result1; condition2; result2 … )


Или более «кодовый» метод:

 if condition1 : result1 else if condition2 : result2 else result3


В примере я использовал первый вариант; теперь давайте посмотрим на наш код упрощенно:

 if( Parent.Issuetype = "Story"; Some kind of result 1; EpicLink; Some kind of result 2 )


Мы видим два очевидных условия:

  • Parent.Issuetype = «История»
  • ЭпикЛинк


Давайте разберемся, что они делают, и начнем с первого, Parent.Issuetype=”Story”.


В данном случае «Родительский» — это переменная, которая автоматически сопоставляется с полем «Родительский выпуск». Здесь, как мы обсуждали выше, должен находиться родительский элемент подзадачи. Используя точечную запись (.), мы получаем доступ к свойству этого родителя, в частности, к свойству Issuetype, которое соответствует полю Jira «Тип задачи». Получается, что вся строка Parent.Issuetype возвращает нам тип родительской задачи, если такая задача существует.


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


Таким образом, первое условие — посмотреть, является ли тип родительской задачи Story. Если первое условие не выполнено, то тип родительской задачи не Story или она вообще не существует. И это подводит нас ко второму условию: EpicLink.


По сути, это когда мы проверяем, заполнено ли поле «Epic Link» Jira (то есть проверяем его наличие). Переменная EpicLink также является стандартной и не нуждается в сопоставлении. Получается, что наше условие выполняется, если в задаче есть Epic Link.


И третий вариант — когда ни одно из условий не выполнено, то есть у задачи нет ни родителя, ни Epic Link. В этом случае мы ничего не показываем и оставляем поле пустым. Это делается автоматически, поскольку мы не получим никаких результатов.


С условиями разобрались, теперь перейдем к результатам. В обоих случаях это строка с текстом и специальным форматированием.


Результат 1 (если родитель — Story):

 """{color:green}[${Parent.Issuetype}]{color} ${Parent.Summary}"""


Результат 2 (если есть Epic Link):

 """{color:#713A82}[${EpicLink.Issuetype}]{color} ${EpicLink.EpicName}"""


Оба результата схожи по структуре: оба состоят из тройных кавычек «»» в начале и конце выходной строки, указания цвета в открывающем {color: COLOR} и закрывающем {color} блоках, а также операциях, выполняемых через символ $. Тройные кавычки сообщают структуре, что внутри будут переменные, операции или блоки форматирования (например, цвета).


Для результата первого условия мы:

  1. Передача типа родительской задачи ${Parent.Issuetype}
  2. Заключите его в квадратные скобки «[…]»
  3. Выделите все зеленым, обернув это выражение [${Parent.Issuetype}] в блок выбора цвета {color:green}…{color}, где мы написали «зеленый»
  4. И последнее: добавьте имя родительской задачи через пробел ${Parent.Summary}.


Таким образом, мы получаем строку «[История] Имя какой-то задачи». Как вы уже могли догадаться, Summary также является стандартной переменной. Чтобы схема построения таких строк была более понятной, поделюсь изображением из официальной документации.


Пользовательская схема строк из официальной документации


Аналогичным образом собираем строку для второго результата, но цвет задаем через шестнадцатеричный код. Я разобрался, что цвет эпика — «#713A82» (в комментариях, кстати, можно подсказать более точный цвет для эпика). Не забывайте про поля (свойства), которые меняются для Epic. Вместо «Сводка» используйте «EpicName», вместо «Родитель» используйте «EpicLink».


В результате схему нашей формулы можно представить в виде таблицы условий.


Условие: родительская задача существует, ее тип — История.

Результат: строка с зеленым типом родительской задачи и ее именем.

Условие: поле Epic Link заполнено.

Результат: Строка с эпическим цветом шрифта и его названием.


По умолчанию в поле выбран вариант отображения «Общие», и если вы его не измените, результат будет выглядеть как обычный текст без изменения цвета и идентификации блоков. Если вы измените формат отображения на «Вики-разметка», текст преобразуется.


1) Отображение общего — по умолчанию отображается обычный текст в том виде, в каком он есть. 2) Замените «Общее» на Wiki-разметку.



Теперь познакомимся с переменными, не связанными с полями Jira — локальными переменными.


Подсчет количества Story Points с цветовой индикацией

Суммы Story Point выделены разными цветами.


Проблема

Из предыдущего примера вы узнали, что мы работаем с задачами типа «История», у которых есть подзадачи. Это приводит к частному случаю с оценками. Чтобы получить оценку Story, мы суммируем оценки ее подзадач, которые оцениваются в абстрактных баллах Story.


Подход необычный, но у нас он работает. Итак, когда у Story нет оценки, а у подзадач есть, проблем нет, но когда оценка есть и у Story, и у подзадач, то стандартная опция из Structure «Σ Story Points» работает некорректно.


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


Предложенное решение

Нам понадобится несколько условий, так как все зависит от того, выставлена ли оценка в Story.


Итак, условия:


Если в Story нет оценки , мы отображаем сумму оценок подзадач оранжевым цветом, чтобы указать, что это значение еще не установлено в Story.


Если у Story есть оценка , то проверьте, соответствует ли она сумме оценок подзадач:

  • Если она не совпадает, раскрасьте оценку красным цветом и напишите рядом с ней в скобках правильную сумму.
  • Если оценка и сумма совпадают, просто напишите оценку зеленым цветом.


Формулировки этих условий могут сбить с толку, поэтому изложим их в виде схемы.


Алгоритм выбора варианта отображения текста


Используемые особенности конструкции

  1. Сопоставление переменных
  2. Локальные переменные
  3. Методы агрегирования
  4. Условия
  5. Текст с форматированием


Пример кода

 with isEstimated = storypoints != undefined: with childrenSum = sum#children{storypoints}: with isStory = issueType = "Story": with isErr = isStory AND childrenSum != storypoints: with color = if isStory : if isEstimated : if isErr : "red" else "green" else "orange": if isEstimated : """{color:$color}$storypoints{color} ${if isErr :""" ($childrenSum)"""}""" else """{color:$color}$childrenSum{color}"""


Анализ решения

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


Тот же алгоритм, переписанный с переменными


Из этой схемы мы видим, что нам понадобится:


Условные переменные:

  • isEstimated (наличие оценки)
  • isError (соответствие оценки Story и суммы)


Одна переменная цвета текста — цвет


Две переменные оценки:

  • sum (сумма оценок подзадач)
  • sp (Сюжетные очки)


Причем переменная цвета зависит еще и от ряда условий, например, от наличия оценки и от типа задачи в строке (см. схему ниже).


Алгоритм выбора цвета


Итак, чтобы определить цвет, нам понадобится еще одна переменная условия — isStory, которая указывает, является ли тип задачи «История».


Переменная sp (storypoints) будет стандартной, то есть она будет автоматически сопоставляться с соответствующим полем Jira. Остальные переменные мы должны определить сами, и они будут для нас локальными.


Теперь попробуем реализовать схемы в коде. Сначала давайте определим все переменные.


 with isEstimated = storypoints != undefined: with childrenSum = sum#children{storypoints}: with isStory = issueType = "Story": with isErr = isStory AND childrenSum != storypoints:


Строки объединены одной синтаксической схемой: ключевое слово with, имя переменной и двоеточие «:» в конце строки.


Синтаксис объявления локальных переменных


Ключевое слово with используется для обозначения локальных переменных (и пользовательских функций, но об этом в отдельном примере). Он сообщает формуле, что следующей идет переменная, которую не нужно сопоставлять. Двоеточие «:» указывает на конец определения переменной.


Таким образом, мы создаем переменную isEstimated (напоминаем, регистр не важен). Мы будем хранить в нем либо 1, либо 0, в зависимости от того, заполнено ли поле Story Points. Переменная Storypoints отображается автоматически, поскольку мы ранее не создавали локальную переменную с таким же именем (например, с помощью Storypoints = … :).


Неопределенная переменная обозначает отсутствие чего-либо (как null, NaN и тому подобное в других языках). Поэтому выражение Storypoints != undefined можно прочитать как вопрос: «Заполнено ли поле Storypoints?».


Далее нам следует определить сумму сюжетных баллов всех дочерних задач. Для этого мы создаем локальную переменную ChildrenSum.


 with childrenSum = sum#children{storypoints}:


Эта сумма рассчитывается с помощью функции агрегирования. (О таких функциях вы можете прочитать в официальной документации .) Если в двух словах, то Structure может выполнять различные операции с задачами с учетом иерархии текущего представления.


Мы используем функцию sum и дополнительно к ней с помощью символа «#» передаем уточняющие дочерние элементы, что ограничивает вычисление суммы только любыми дочерними задачами текущей строки. В фигурных скобках указываем, какое поле мы хотим суммировать — нам нужна оценка в сторипойнтах.


Следующая локальная переменная isStory хранит условие: является ли тип задачи в текущей строке Story.


 with isStory = issueType = "Story":


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


Теперь определим переменную isErr — она сигнализирует о несоответствии суммы подзадачи оценке Story.


 with isErr = isStory AND childrenSum != storypoints:


Здесь мы используем локальные переменные isStory и ChildrenSum, которые мы создали ранее. Чтобы сигнализировать об ошибке, нам необходимо одновременное выполнение двух условий: тип задачи — Story (isStory) и (И) сумма дочерних баллов (childrenSum) не равна (!=) установленной оценке в задании (storypoints ). Как и в JQL, при создании условий мы можем использовать слова-ссылки, например AND или OR.


Обратите внимание, что для каждой локальной переменной в конце строки стоит символ «:». Оно должно быть в конце, после всех операций, определяющих переменную. Например, если нам нужно разбить определение переменной на несколько строк, то двоеточие «:» ставится только после последней операции. Как и в примере с переменной color — цвет текста.


 with color = if isStory : if isEstimated : if isErr : "red" else "green" else "orange":


Здесь мы видим много «:», но они играют разные роли. Двоеточие после if isStory является результатом условия isStory. Напомним конструкцию: если условие: результат. Представим эту конструкцию в более сложном виде, определяющем переменную.


 with variable = (if condition: (if condition2 : result2 else result3) ):


Получается, что если условие2:результат2, то результат3 — это как бы результат первого условия, а в самом конце стоит двоеточие «:», завершающее определение переменной.


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


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


 if isEstimated : """{color:$color}$storypoints{color} ${if isErr :""" ($childrenSum)"""}""" else """{color:$color}$childrenSum{color}"""


Нам не нужно дважды писать в коде «{color}$sp», как это было в схеме; мы будем умнее во всем. В ветке, если у задачи есть оценка, мы всегда будем отображать {color: $color}$storypoints{color} (то есть просто оценку в стори-пойнтах в нужном цвете), а если есть ошибка, то после пробела дополним строку суммой оценки подзадач: ($childrenSum).


Если ошибки нет, то она не будет добавлена. Также обращаю внимание на то, что здесь нет символа «:», так как мы не определяем переменную, а отображаем конечный результат через условие.


Оценить нашу работу мы можем на изображении ниже в поле «∑SP (mod)». На снимке экрана конкретно показаны два дополнительных поля:


  • «Story Points» — оценка в Story Points (стандартное Jira-поле).
  • «∑ Story Points» — стандартное настраиваемое поле Структуры, которое неправильно рассчитывает сумму.


Окончательный вид поля и сравнение со стандартными полями Story Points и ∑ Story Points.


С помощью этих примеров мы проанализировали основные особенности языка структур, которые помогут вам решить большинство задач. Давайте теперь рассмотрим еще две полезные функции: наши функции и массивы. Мы увидим, как создать нашу собственную функцию.


Последние изменения

Обратите внимание на смайлик слева — он представляет собой настраиваемое поле.


Проблема

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


Предложенное решение

Нас интересуют три типа изменения статуса задачи, произошедшие со вчерашнего дня: мы начали работу над задачей, появилась новая задача, задача закрыта. Дополнительно будет полезно увидеть, что задача закрывается с резолюцией «Не буду делать».


Для этого мы создадим поле со строкой смайлов, отвечающих за последние изменения. Например, если задача была создана вчера и мы начали над ней работать, то она будет отмечена двумя смайлами: «В работе» и «Новая задача».


Зачем нам такое настраиваемое поле, если можно отображать несколько дополнительных полей, например, дату перехода в статус «В работе» или отдельное поле «Решение»? Ответ прост — люди воспринимают смайлы проще и быстрее, чем текст, который находится в разных полях и требует анализа. Формула соберет все в одном месте и проанализирует за нас, что сэкономит нам силы и время для более полезных дел.


Давайте определим, за что будут отвечать разные смайлы:

  • *️⃣ — самый распространенный способ отметить новую задачу.
  • ✅ отмечает выполненную задачу
  • ❌ указывает на задачу, которую вы решили отменить («Не буду делать»)
  • 🚀 означает, что мы решили начать работу над задачей (этот смайлик подойдет нашей команде, у вас он может быть другим)


Используемые особенности конструкции

  1. Сопоставление переменных
  2. Методы языка выражения
  3. Локальные переменные
  4. Условия
  5. Наша собственная функция


Пример кода

 if defined(issueType): with now = now(): with daysScope = 1.3: with workDaysBetween(today, from)= ( with weekends = (Weeknum(today) - Weeknum(from)) * 2: HOURS_BETWEEN(from;today)/24 - weekends ): with daysAfterCreated = workDaysBetween(now,created): with daysAfterStart = workDaysBetween(now,latestTransitionToProgress): with daysAfterDone = workDaysBetween(now, resolutionDate): with isWontDo = resolution = "Won't Do": with isRecentCreated = daysAfterCreated >= 0 and daysAfterCreated <= daysScope and not(resolution): with isRecentWork = daysAfterStart >= 0 and daysAfterStart <= daysScope : with isRecentDone = daysAfterDone >= 0 and daysAfterDone <= daysScope : concat( if isRecentCreated : "*️⃣", if isRecentWork : "🚀", if isRecentDone : "✅", if isWontDo : "❌")

Анализ решения


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

  • Задача создана
  • Статус изменился на «В работе».
  • Решение найдено (и какое)


Использование уже существующих переменных вместе с новыми переменными сопоставления поможет нам проверить все эти условия.

  • создано — дата создания задачи
  • LatestTransitionToProgress — самая поздняя дата перехода в статус «В процессе» (сопоставляем как в предыдущем примере)
  • разрешенияДата — дата завершения задачи
  • разрешение — текст разрешения


Перейдем к коду. Первая строка начинается с условия, которое проверяет, существует ли тип задачи.


 if defined(issueType):


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


Мы не будем нагружать Structure бесполезными вычислениями, если строка не является задачей. Получается, что весь код после if — это результат, я имею в виду вторую часть конструкции if (условие:результат). А если условие не выполнено, то код тоже не будет работать.


Следующая строка с now = now(): также необходима для оптимизации вычислений. Далее в коде нам придется несколько раз сравнивать разные даты с текущей датой. Чтобы не делать один и тот же расчет несколько раз, мы вычислим эту дату один раз и теперь сделаем ее локальной переменной.


Еще было бы неплохо сохранить наше «вчера» отдельно. Удобное «вчера» эмпирически превратилось в 1,3 дня. Давайте превратим это в переменную: с DaysScope = 1.3:.


Теперь нам нужно несколько раз посчитать количество дней между двумя датами. Например, между текущей датой и датой начала работы. Конечно, есть встроенная функция DAYS_BETWEEN, которая нам вроде бы подходит. Но, если задача, например, была создана в пятницу, то в понедельник мы не увидим уведомления о новой задаче, так как по факту прошло более 1,3 дней. Кроме того, функция DAYS_BETWEEN считает только общее количество дней (то есть 0,5 дня превратятся в 0 дней), что нас тоже не устраивает.


Мы сформировали требование — нам нужно посчитать точное количество рабочих дней между этими датами; и в этом нам поможет пользовательская функция.


Синтаксис объявления локальных функций


Его определяющий синтаксис очень похож на синтаксис определения локальной переменной. Единственное отличие и единственное дополнение — необязательное перечисление аргументов в первых скобках. Во вторых скобках указаны операции, которые будут выполняться при вызове нашей функции. Данное определение функции не единственно возможное, но мы воспользуемся этим (другие можно найти в официальной документации ).


 with workDaysBetween(today, from)= ( with weekends = (Weeknum(today) - Weeknum(from)) * 2: HOURS_BETWEEN(from;today)/24 - weekends ):


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


Чтобы подсчитать количество выходных дней, нам нужно узнать, сколько недель прошло между сегодняшним днем и с. Для этого посчитаем разницу между числами каждой из недель. Мы получим этот номер из функции Weeknum, которая предоставляет нам номер недели с начала года. Умножив эту разницу на два, получим количество пройденных выходных.


Далее функция HOURS_BETWEEN подсчитывает количество часов между нашими датами. Делим результат на 24, чтобы получить количество дней, и вычитаем из этого числа выходные дни, получая рабочие дни между датами.


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


 with daysAfterCreated = workDaysBetween(now,created): with daysAfterStart = workDaysBetween(now,latestTransitionToProgress): with daysAfterDone = workDaysBetween(now, resolutionDate):


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


 with isWontDo = resolution = "Won't Do": with isRecentCreated = daysAfterCreated >= 0 and daysAfterCreated <= daysScope and not(resolution): with isRecentWork = daysAfterStart >= 0 and daysAfterStart <= daysScope : with isRecentDone = daysAfterDone >= 0 and daysAfterDone <= daysScope :


Для переменной isRecentCreated я добавил необязательное условие, а не(разрешение), что помогает мне упростить будущую строку, ведь если задача уже закрыта, то информация о ее недавнем создании меня не интересует.


Конечный результат создается с помощью функции concat, объединяющей строки.


 concat( if isRecentCreated : "*️⃣", if isRecentWork : "🚀", if isRecentDone : "✅", if isWontDo : "❌")


Получается, что смайлик будет в строке только тогда, когда переменная в условии равна 1. Таким образом, наша строка может одновременно отображать независимые изменения задачи.


Итоговый вид колонны с изменениями (левая сторона)


Мы затронули тему подсчета рабочих дней без выходных. С этим связана еще одна проблема, которую мы разберем в нашем последнем примере и заодно познакомимся с массивами.


Расчет рабочего времени без учета выходных

Пример финального отображения


Проблема

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


К сожалению, Structure «из коробки» не умеет игнорировать выходные дни, и поле с опцией «Время в статусе…» выдает результат независимо от настроек Jira — даже если в качестве выходных указаны суббота и воскресенье.


В результате наша цель — посчитать точное количество рабочих дней, не учитывая выходных, и учесть влияние переходов статусов на это время.


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


Решение из предыдущего примера нам не подходит из-за проблемы переключения между статусами, поскольку пользовательская функция workDaysBetween учитывает только время между двумя выбранными датами.


Предложенное решение

Эту проблему можно решить разными способами. Способ в примере самый затратный по производительности, но самый точный с точки зрения подсчета выходных и статусов. Обратите внимание, что его реализация работает только в версии Structure старше 7.4 (декабрь 2021 г.).


Итак, идея формулы заключается в следующем:


  1. Нам нужно узнать, сколько дней прошло от начала до завершения задачи.
  2. Делаем из этого массив, то есть список дней между началом и окончанием нашей работы над задачей
  3. Оставлять в списке только выходные дни


Фильтрация только выходных из всех дат (они могут иметь разные статусы)


  1. Из этих выходных мы оставляем только те, когда задача находилась в статусе «В работе» (здесь нам поможет функция из версии 7.4 «Историческая ценность»)


Удаление ненужных статусов с сохранением статуса «в работе».


  1. Теперь в списке у нас есть только те выходные дни, которые совпали с периодом «В работе».
  2. Отдельно узнаем общую длительность статуса «В работе» (через встроенную опцию структуры «Время в статусе…»);
  3. Вычтите из этого времени количество ранее полученных выходных.


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


Используемые особенности конструкции

  1. Сопоставление переменных
  2. Методы языка выражения
  3. Локальные переменные
  4. Условия
  5. Внутренний метод (наша собственная функция)
  6. Массивы
  7. Доступ к истории выполнения задачи
  8. Текст с форматированием


Пример кода

 if defined(issueType) : if status != "Open" : with finishDate = if toQA != Undefined : toQA else if toDone != Undefined : toDone else now(): with startDate = DEFAULT(toProgress, toDone): with statusWeekendsCount(dates, status) = ( dates.filter(x -> weekday(x) > 5 and historical_value(this,"status",x)=status).size() ): with overallDays = round(hours_between(startDate,finishDate)/24): with sequenceArray = SEQUENCE(0,overallDays): with datesArray = sequenceArray.map(DATE_ADD(startDate,$,"day")): with progressWeekends = statusWeekendsCount(datesArray, "in Progress"): with progressDays = (timeInProgress/86400000 - progressWeekends).round(1): with color = if( progressDays = 0 ; "gray" ; progressDays > 0 and progressDays <= 2.5; "green" ; progressDays > 2.5 and progressDays <= 4; "orange" ; progressDays > 4; "red" ): """{color:$color}$progressDays d{color}"""


Анализ решения


Прежде чем переносить наш алгоритм в код, давайте облегчим расчеты для Структуры.


 if defined(issueType) : if status != "Open" :


Если строка не является задачей или ее статус «Открыта», мы пропустим эти строки. Нас интересуют только те задачи, которые запущены в работу.


Чтобы вычислить количество дней между датами, мы должны сначала определить эти даты: FinishDate и startDate.


 with finishDate = if toQA != Undefined : toQA else if toDone != Undefined : toDone else now(): with startDate = DEFAULT(toProgress, toDone): 


Определение статусов, обозначающих логическое завершение работы


Предположим, что дата завершения задачи (finishDate):

  • Либо дата перевода задачи в статус «QA»
  • Либо дата перехода на «Закрыто»
  • Или если задача еще в «В работе», то сегодняшняя дата (чтобы понять, сколько времени прошло)


Дата начала работ startDate определяется датой перехода в статус «В работе». Бывают случаи, когда задача закрывается, не переходя в стадию в работе. В таких случаях мы считаем дату закрытия датой начала, поэтому результат равен 0 дней.


Как вы уже могли догадаться, toQA, toDone и toProgress — это переменные, которым необходимо сопоставить соответствующие статусы, как в первом и предыдущем примерах.


Мы также видим новую функцию DEFAULT(toProgress, toDone). Он проверяет, имеет ли toProgress значение, а если нет, то использует значение переменной toDone.


Далее следует определение пользовательской функции statusWeekendsCount, но мы вернемся к ней позже, поскольку она тесно связана со списками дат. Лучше сразу перейти к определению этого списка, чтобы потом понять, как к нему применить нашу функцию.


Мы хотим получить список дат в следующем виде: [startDate (скажем, 11.03), 12.03, 13.03, 14.03…finishDate]. Не существует простой функции, которая сделала бы за нас всю работу в Structure. Итак, прибегнем к хитрости:


  1. Создадим простой список из последовательности чисел от 0 до количества дней в работе, то есть [0, 1, 2, 3…n дней в работе]
  2. К каждому числу (т.е. дню) добавьте дату начала задачи. В результате получаем список (массив) нужного типа: [старт + 0 дней, старт + 1 день, старт + 2 дня… старт + n дней работы].


Создание исходного массива дат от даты начала до логического конца


Теперь давайте посмотрим, как мы можем реализовать это в коде. Мы будем работать с массивами.


 with overallDays = round(hours_between(startDate,finishDate)/24): with sequenceArray = SEQUENCE(0,overallDays): with datesArray = sequenceArray.map(DATE_ADD(startDate,$,"day")):


Считаем, сколько дней займет работа над задачей. Как и в предыдущем примере, посредством деления на 24 и функцииhours_between(startDate,finishDate). Результат записывается в переменную commonDays.


Мы создаем массив последовательности чисел в виде переменной SequenceArray. Этот массив создается с помощью функции SEQUENCE(0,overallDays), которая просто создает массив желаемого размера с последовательностью от 0 до commonDays.


Дальше происходит волшебство. Одна из функций массива — карта. Он применяет указанную операцию к каждому элементу массива.


Наша задача — к каждому числу (то есть номеру дня) добавить дату начала. Это умеет функция DATE_ADD, она добавляет к указанной дате определенное количество дней, месяцев или лет.


Зная это, давайте расшифруем строку:


 with datesArray = sequenceArray.map(DATE_ADD(startDate, $,"day"))


К каждому элементу в SequenceArray функция .map() применяет DATE_ADD(startDate, $, «day»).


Давайте посмотрим, что передается в аргументах для DATE_ADD. Первое — startDate, дата, к которой будет добавлен нужный номер. Это число указывается вторым аргументом, но мы видим $.


Символ $ обозначает элемент массива. Структура понимает, что функция DATE_ADD применяется к массиву, а значит вместо $ будет нужный элемент массива (то есть 0, 1, 2…).


Последний аргумент «день» указывает на то, что мы добавляем день, поскольку функция может добавлять день, месяц и год, в зависимости от того, что мы указываем.


Таким образом, переменная dateArray будет хранить массив дат от начала работы до ее завершения.


Давайте вернемся к пользовательской функции, которую мы пропустили. Он отфильтрует лишние дни и рассчитает остаток. Этот алгоритм мы описали в самом начале примера, перед разбором кода, а именно в пунктах 3 и 4 про фильтрацию выходных и статусов.


 with statusWeekendsCount(dates, status) = ( dates.filter(x -> weekday(x) > 5 and historical_value(this,"status",x)=status).size() ):


Пользовательской функции мы передадим два аргумента: массив дат, назовем его датами, и необходимый статус — status. Мы применяем функцию .filter() к переданному массиву дат, которая сохраняет в массиве только те записи, которые прошли через условие фильтра. В нашем случае их два, и они объединяются через и . После фильтра мы видим .size(), он возвращает размер массива после выполнения всех операций над ним.


Если упростить выражение, то получим что-то вроде этого: array.filter(condition1 и Condition2).size(). Итак, в результате мы получили подходящее для нас количество выходных, то есть тех выходных, которые прошли условия.


Рассмотрим подробнее оба условия:


 x -> weekday(x) > 5 and historical_value(this,"status",x)=status


Выражение x -> — это всего лишь часть синтаксиса фильтра, указывающая, что мы будем вызывать элемент массива x. Поэтому в каждом условии появляется x (аналогично тому, как это было с $). Оказывается, x — это каждая дата из переданного массива дат.


Первое условие, день недели(x) > 5, требует, чтобы день недели даты x (то есть каждого элемента) был больше 5 — это либо суббота (6), либо воскресенье (7).


Второе условие использует историческое_значение.


 historical_value(this,"status",x) = status


Это особенность Структуры версии 7.4.


Функция обращается к истории задачи и ищет конкретную дату в указанном поле. В данном случае мы ищем дату x в поле «статус». Эта переменная является лишь частью синтаксиса функции, она отображается автоматически и представляет текущую задачу в строке.


Таким образом, в условии мы сравниваем переданный аргумент статуса и поле «статус», которое возвращает функция Historical_value для каждой даты x в массиве. Если они совпадают, то запись остается в списке.


Последний штрих — использование нашей функции для подсчета количества дней в нужном статусе:


 with progressWeekends = statusWeekendsCount(datesArray, "in Progress"): with progressDays = (timeInProgress/86400000 - progressWeekends).round(1):


Для начала давайте выясним, сколько выходных со статусом «в работе» попало в наш dateArray. То есть мы передаем в пользовательскую функцию statusWeekendsCount наш список дат и желаемый статус. Функция забирает все будние и все выходные дни, в которых статус задачи отличается от статуса «в работе», и возвращает количество дней, оставшихся в списке.


Затем мы вычитаем эту сумму из переменной timeInProgress, которую отображаем через опцию «Время в статусе…».


Число 86400000 — это делитель, который преобразует миллисекунды в дни. Функция .round(1) нужна для округления результата до десятых, например до «4,1», в противном случае вы можете получить запись такого типа: «4,0999999…».


Чтобы указать длину задачи, мы вводим переменную цвета. Мы будем менять его в зависимости от количества дней, потраченных на выполнение задачи.


  • Серый — 0 дней
  • Зеленый — больше 0, но меньше 2,5 дней.
  • Красный — от 2,5 до 4 дней
  • Красный — более 4 дней


 with color = if( progressDays = 0 ; "gray" ; progressDays > 0 and progressDays <= 2.5; "green" ; progressDays > 2.5 and progressDays <= 4; "orange" ; progressDays > 4; "red" ):


И финальная строка с результатом рассчитанных дней:


 """{color:$color}$progressDays d{color}"""


Наш результат будет выглядеть как на изображении ниже.


Окончательный вид поля «Время в работе»


Кстати, в этой же формуле можно вывести время любого статуса. Если, например, мы передаем в нашу пользовательскую функцию статус «Пауза», а переменную timeInProgress сопоставляем через «Время в… — Пауза», то мы рассчитаем точное время в паузе.


Вы можете объединить статусы и сделать запись типа «wip: 3.2d | rev: 12d», то есть посчитать время в работе и время в просмотре. Вы ограничены только вашим воображением и рабочим процессом.


Заключение

Мы представили исчерпывающее количество возможностей этого языка формул, которые помогут вам сделать что-то подобное или написать что-то совершенно новое и интересное для анализа задач Jira.


Надеюсь, статья помогла вам разобраться с формулами или хотя бы заинтересовала эту тему. Я не претендую на то, что у меня «лучший код и алгоритм», поэтому если у вас есть идеи, как улучшить примеры, буду рад, если вы ими поделитесь!


Конечно, нужно понимать, что лучше разработчиков ALM Works о формулах вам никто не расскажет. Поэтому прилагаю ссылки на их документацию и вебинары. А если вы начнете работать с настраиваемыми полями, почаще проверяйте их, чтобы узнать, какие еще функции вы можете использовать.