Le plugin Structure pour Jira est très utile pour le travail quotidien avec les tâches et leur analyse ; il porte la visualisation et la structuration des tickets Jira à un nouveau niveau, et ce, dès le départ.
Et tout le monde ne le sait pas, mais la fonctionnalité des formules de structure peut tout simplement vous époustoufler. À l'aide de formules, vous pouvez créer des tableaux extrêmement utiles qui peuvent grandement simplifier le travail avec les tâches et, plus important encore, ils sont utiles pour effectuer une analyse plus approfondie des versions, des épopées et des projets.
Dans cet article, vous verrez comment créer vos propres formules, en commençant par les exemples les plus simples et en terminant par des cas complexes, mais plutôt utiles.
Alors, à qui s’adresse ce texte ? On pourrait se demander pourquoi écrire un article alors que la documentation officielle sur le site Web d'ALM Works est là, attendant que les lecteurs s'y penchent. C'est vrai. Cependant, je fais partie de ces personnes qui n'avaient même pas la moindre idée que Structure cachait des fonctionnalités aussi étendues : "Attendez, c'était une option depuis le début ?!" Cette prise de conscience m'a fait réfléchir : il se peut qu'il y ait d'autres personnes qui ne savent toujours pas le genre de choses qu'elles peuvent faire avec les formules et la structure.
Cet article sera également utile à ceux qui connaissent déjà les formules. Vous apprendrez quelques options pratiques intéressantes pour utiliser les champs personnalisés et, peut-être, en emprunterez quelques-unes pour vos projets . À propos, si vous avez vos propres exemples intéressants, je serai heureux que vous les partagiez dans les commentaires. .
Chaque exemple est analysé en détail, depuis la description du problème jusqu'à l'explication du code, de manière suffisamment approfondie pour qu'il n'y ait plus de questions. Bien entendu, parallèlement aux explications, chaque exemple est illustré par un code que vous pouvez essayer par vous-même sans vous lancer dans l'analyse.
Si vous n'avez pas envie de lire, mais que les formules vous intéressent, consultez les webinaires ALM Works . Ceux-ci expliquent les bases en 40 minutes ; les informations y sont présentées de manière très compressée.
Vous n'avez besoin d'aucune connaissance supplémentaire pour comprendre les exemples, donc toute personne ayant travaillé avec Jira et Structure pourra répéter les exemples dans ses tableaux sans aucun problème.
Les développeurs ont fourni une syntaxe assez souple avec leur langage Expr. Fondamentalement, la philosophie ici est « écrivez comme vous voulez et ça marchera ».
Alors, commençons!
Alors, pourquoi voudrions-nous utiliser des formules ? Eh bien, il s'avère parfois que nous n'avons pas suffisamment de champs Jira standards, tels que « Assignee », « Story Points », etc. Ou nous devons calculer un certain montant pour certains champs, afficher la capacité restante par version et savoir combien de fois la tâche a changé de statut. Peut-être souhaitons-nous même fusionner plusieurs champs en un seul afin de rendre notre structure plus facile à lire.
Pour résoudre ces problèmes, nous avons besoin de formules et nous les utiliserons pour créer des champs personnalisés.
La première chose que nous devons faire est de comprendre comment fonctionne une formule. Cela nous permet d'appliquer une sorte d'opération à une chaîne. Étant donné que nous téléchargeons de nombreuses tâches dans la structure, la formule est appliquée à chaque ligne du tableau entier. Habituellement, toutes ses opérations visent à travailler avec des tâches dans ces lignes.
Ainsi, si nous demandons à la formule d'afficher un champ Jira, par exemple « Destinataire », alors la formule sera appliquée pour chaque tâche, et nous aurons une autre colonne « Destinataire ».
Les formules se composent de plusieurs entités de base :
Nous nous familiariserons davantage avec les formules et leur syntaxe à travers quelques exemples, et nous allons parcourir six cas pratiques.
Avant d'examiner chaque exemple, nous indiquerons quelles fonctionnalités de structure nous utilisons ; les nouvelles fonctionnalités qui n'ont pas encore été expliquées seront en gras. Chacun des exemples suivants aura un niveau de complexité croissant. Ils sont organisés de manière à vous présenter progressivement les fonctionnalités importantes de la formule.
Voici la structure de base que vous verrez à chaque fois :
Ces exemples couvrent des sujets allant du mappage de variables aux tableaux complexes :
Voyons d’abord comment créer des champs personnalisés avec des formules. Dans la partie supérieure droite de Structure, à la fin de toutes les colonnes, il y a une icône « + » — cliquez dessus. Dans le champ qui apparaît, écrivez « Formule… » et sélectionnez l'élément approprié.
Discutons de la sauvegarde d'une formule. Malheureusement, il n'est toujours pas possible de sauvegarder une formule spécifique séparément quelque part (uniquement dans votre cahier, comme je le fais). Lors du webinaire ALM Works, l'équipe a mentionné qu'elle travaillait sur une banque de formules, mais pour l'instant, la seule façon de les enregistrer est d'enregistrer l'intégralité de la vue avec la formule.
Lorsque nous avons fini de travailler sur une formule, nous devons cliquer sur la vue de notre structure (elle sera probablement marquée d'un astérisque bleu) et cliquer sur « Enregistrer » pour écraser la vue actuelle. Ou vous pouvez cliquer sur « Enregistrer sous… » pour créer une nouvelle vue. (N'oubliez pas de le rendre disponible aux autres utilisateurs de Jira car les nouvelles vues sont privées par défaut.)
La formule sera enregistrée dans le reste des champs dans une vue particulière, et vous pourrez la voir dans l'onglet « Avancé » du menu « Afficher les détails ».
À partir de la version 8.2, Structure a désormais la possibilité d'enregistrer des formules en 3 clics rapides.
La boîte de dialogue d'enregistrement est disponible dans la fenêtre d'édition de formule. Si cette fenêtre n'est pas ouverte, cliquez simplement sur l'icône triangle ▼ dans la colonne souhaitée.
Dans la fenêtre d'édition, nous voyons le champ « Colonne enregistrée », à droite il y a une icône avec une notification bleue, ce qui signifie que les modifications dans la formule n'ont pas été enregistrées. Cliquez sur cette icône et sélectionnez l'option « Enregistrer sous… ».
Entrez ensuite les noms de notre colonne (formule) et choisissez dans quel espace la sauvegarder. «Mes colonnes» si nous voulons l'enregistrer dans une liste personnelle. « Global », pour que la formule soit enregistrée dans la liste générale, où elle pourra être modifiée par tous les utilisateurs de votre Structure. Cliquez sur « Enregistrer ».
Notre formule est désormais enregistrée. Nous pouvons le charger dans n’importe quelle structure ou le réenregistrer de n’importe où. En réenregistrant la formule, elle sera mise à jour dans toutes les structures dans lesquelles elle est utilisée.
Le mappage des variables est également enregistré avec la formule, mais nous parlerons du mappage plus tard.
Passons maintenant à nos exemples !
Nous avons besoin d'un tableau avec une liste de tâches, ainsi que les dates de début et de fin pour travailler sur ces tâches. Nous avons également besoin du tableau pour l'exporter vers un Gantt Excel distinct. Malheureusement, Jira et Structure ne savent pas comment fournir de telles dates dès le départ.
Les dates de début et de fin sont les dates de transition vers des statuts spécifiques, dans notre cas ce sont « En cours » et « Fermé ». Nous devons prendre ces dates et afficher chacune d'elles dans un champ séparé (cela est nécessaire pour une exportation ultérieure vers le Gantt). Nous aurons donc deux champs (deux formules).
Les fonctionnalités de la structure utilisées
Un exemple de code
Champ pour la date de début :
firstTransitionToStart
Champ pour la date de fin :
latestTransitionToDone
Dans ce cas, le code est une variable unique, firstTransitionToStart pour le champ de date de début et lastTransitionToDone pour le deuxième champ.
Concentrons-nous sur le champ de date de début pour l'instant. Notre objectif est d'obtenir la date à laquelle la tâche est passée au statut « En cours » (cela correspond au début logique de la tâche), de sorte que la variable est nommée, de manière assez explicite pour éviter d'avoir à deviner ultérieurement, comme « première transition vers commencer".
Pour transformer une date en variable, nous nous tournons vers le mappage de variables. Sauvons notre formule en cliquant sur le bouton « Enregistrer ».
Notre variable est apparue dans la section « Variables », avec un point d'exclamation à côté. La structure indique qu'il ne peut pas lier une variable à un champ dans Jira, et nous devrons le faire nous-mêmes (c'est-à-dire le mapper).
Cliquez sur la variable et accédez à l'interface de cartographie. Sélectionnez le champ ou l'opération nécessaire — recherchez l'opération « Date de transition… ». Pour ce faire, écrivez « transition » dans le champ de sélection. Plusieurs options vous seront proposées à la fois, et l'une d'elles nous convient : « Première transition vers En cours ». Mais afin de démontrer le fonctionnement du mappage, choisissons l'option « Date de transition… ».
Après cela, vous devez choisir le statut dans lequel la transition s'est produite et l'ordre de cette transition : la première ou la dernière.
Sélectionnez ou saisissez dans « Statut » — « Statut : En cours » (ou le statut correspondant dans votre Workflow), et dans « Transition » — « Première transition vers le statut », puisque le début du travail sur une tâche est la toute première transition au statut correspondant.
Si au lieu de « Date de transition… » nous choisissions l'option initialement proposée « Première transition vers En cours », alors le résultat serait presque le même : la structure choisirait pour nous les paramètres nécessaires. La seule chose est qu'au lieu de « Statut : En cours », nous aurions « Catégorie : En cours ».
Permettez-moi de noter une caractéristique importante : un statut et une catégorie sont deux choses différentes. Un statut est un statut spécifique, c'est sans ambiguïté, mais une catégorie peut regrouper plusieurs statuts. Il n'y a que trois catégories : « À faire », « En cours » et « Terminé ». Dans Jira, ils sont généralement marqués respectivement de couleurs grise, bleue et verte. Le statut doit appartenir à l'une de ces catégories.
Je recommande d'indiquer un statut spécifique dans des cas comme celui-ci afin d'éviter toute confusion avec des statuts de même catégorie. Par exemple, nous avons deux statuts de la catégorie « To Do » sur le projet, « Open » et « QA Queue ».
Revenons à notre exemple.
Une fois que nous avons sélectionné les options nécessaires, nous pouvons cliquer sur « < Retour à la liste des variables » pour compléter les options de mappage pour la première variable TransitionToStart. Si nous faisons tout correctement, nous verrons une coche verte.
En même temps, dans notre champ personnalisé, nous voyons des nombres étranges qui ne ressemblent pas du tout à une date. Dans notre cas, le résultat de la formule sera la valeur de la variable firstTransitionToStart, et sa valeur est en millisecondes depuis janvier 1970. Afin d'obtenir la date correcte, nous devons choisir un format d'affichage de formule spécifique.
La sélection du format est située tout en bas de la fenêtre d'édition. « Général » y est sélectionné par défaut. Nous avons besoin de « Date/Heure » pour afficher correctement la date.
Pour le deuxième champ, lastTransitionToDone, nous ferons de même. La seule différence est que lors du mappage, nous pouvons déjà sélectionner la catégorie « Terminé », et non le statut (puisqu'il n'y a généralement qu'un seul statut d'achèvement de tâche sans ambiguïté). Nous sélectionnons « Dernière transition » comme paramètre de transition, car nous nous intéressons à la transition la plus récente vers la catégorie « Terminé ».
Le résultat final pour les deux champs ressemblera à ceci.
Voyons maintenant comment obtenir le même résultat, mais avec notre propre format d'affichage.
Nous ne sommes pas satisfaits du format d'affichage de la date de l'exemple précédent, car nous en avons besoin d'un format spécial pour le tableau du Gantt : « 01.01.2022 ».
Affichons les dates à l'aide des fonctions intégrées à Structure, en précisant le format qui nous convient.
Caractéristiques structurelles utilisées
Un exemple de code
FORMAT_DATETIME(firstTransitionToStart;"dd.MM.yyyy")
Les développeurs ont fourni de nombreuses fonctions différentes, dont une distincte pour afficher la date dans notre propre format : FORMAT_DATETIME ; c'est ce que nous allons utiliser. La fonction utilise deux arguments : une date et une chaîne du format souhaité.
Nous configurons la variable firstTransitionToStart (premier argument) en utilisant les mêmes règles de mappage que dans l'exemple précédent. Le deuxième argument est une chaîne spécifiant le format, et nous le définissons comme ceci : « jj.MM.aaaa ». Cela correspond au formulaire souhaité, « 01.01.2022 ».
Ainsi, notre formule donnera immédiatement un résultat sous la forme souhaitée. Ainsi, nous pouvons conserver l’option « Général » dans les paramètres du champ.
Le deuxième champ avec la date de fin des travaux se fait de la même manière. En conséquence, la structure devrait ressembler à l’image ci-dessous.
En principe, il n'y a pas de difficultés significatives à travailler avec la syntaxe des formules. Si vous avez besoin d'une variable, écrivez son nom ; si vous avez besoin d'une fonction, encore une fois, écrivez simplement son nom et transmettez les arguments (s'ils sont requis).
Lorsque Structure rencontre un nom inconnu, elle suppose qu'il s'agit d'une variable et essaie de la mapper elle-même, ou nous demande de l'aide.
À propos, remarque importante : la structure n'est pas sensible à la casse, donc firstTransitionToStart, firsttransitiontostart et firSttrAnsItiontOSarT sont la même variable. La même règle s'applique aux fonctions. Afin d'obtenir un style de code sans ambiguïté, dans les exemples, nous essaierons de respecter les règles des conventions de capitalisation de MSDN.
Examinons maintenant la syntaxe et examinons un format spécial pour afficher le résultat.
Nous travaillons avec des tâches régulières (Tâche, Bug, etc.) et avec des tâches de type Story qui comportent des sous-tâches. À un moment donné, nous devons découvrir sur quelles tâches et sous-tâches l'employé a travaillé pendant une certaine période.
Le problème est que de nombreuses sous-tâches ne fournissent pas d'informations sur l'histoire elle-même, car elles sont appelées « travailler sur l'histoire », « mettre en place » ou, par exemple, « activer l'effet ». Et si nous demandons une liste de tâches pour une certaine période, nous obtiendrons une douzaine de tâches avec le nom « travailler sur l'histoire » sans aucune autre information utile.
Nous aimerions avoir une vue avec une liste divisée en deux colonnes : une tâche et une tâche parent, afin qu'à l'avenir il soit possible de regrouper une telle liste par employés.
Sur notre projet, nous avons deux options lorsqu'une tâche peut avoir un parent :
Il faut donc :
Pour simplifier la perception de l'information, nous colorerons le texte du type de tâche : c'est-à-dire soit « [Histoire] » soit « [Epic] ».
Ce que nous utiliserons :
Un exemple de code
if( Parent.Issuetype = "Story"; """{color:green}[${Parent.Issuetype}]{color} ${Parent.Summary}"""; EpicLink; """{color:#713A82}[${EpicLink.Issuetype}]{color} ${EpicLink.EpicName}""" )
Pourquoi la formule commence-t-elle par une condition if, si nous avons simplement besoin de générer une chaîne et d'y insérer le type et le nom de la tâche ? N'existe-t-il pas un moyen universel d'accéder aux champs de tâches ? Oui, mais pour les tâches et les epics, ces champs sont nommés différemment et vous devez également y accéder différemment, c'est une fonctionnalité de Jira.
Les différences commencent au niveau de la recherche parent. Pour une sous-tâche, le parent réside dans le champ Jira « Parent Issue », et pour une tâche normale, l'epic sera le parent, situé dans le champ « Epic Link ». En conséquence, nous devrons écrire deux options différentes pour accéder à ces champs.
C'est là que nous avons besoin d'une condition if. Le langage Expr a différentes manières de gérer les conditions. Le choix entre eux est une question de goût.
Il existe une méthode « de type Excel » :
if (condition1; result1; condition2; result2 … )
Ou une méthode plus « code-like » :
if condition1 : result1 else if condition2 : result2 else result3
Dans l'exemple, j'ai utilisé la première option ; regardons maintenant notre code de manière simplifiée :
if( Parent.Issuetype = "Story"; Some kind of result 1; EpicLink; Some kind of result 2 )
Nous voyons deux conditions évidentes :
Voyons ce qu'ils font et commençons par le premier, Parent.Issuetype=”Story”.
Dans ce cas, Parent est une variable qui est automatiquement mappée au champ « Parent Issue ». C'est là que, comme nous l'avons vu ci-dessus, le parent de la sous-tâche devrait vivre. Grâce à la notation par points (.), on accède à la propriété de ce parent, notamment à la propriété Issuetype, qui correspond au champ Jira « Issue Type ». Il s'avère que toute la ligne Parent.Issuetype nous renvoie le type de la tâche parent, si une telle tâche existe.
De plus, nous n'avons pas eu besoin de définir ou de cartographier quoi que ce soit, car les développeurs ont déjà fait de leur mieux pour nous. Ici, par exemple, vous trouverez un lien vers toutes les propriétés (y compris les champs Jira) prédéfinies dans le langage, et ici vous pouvez voir une liste de toutes les variables standard, qui peuvent également être consultées en toute sécurité sans paramètres supplémentaires.
Ainsi, la première condition est de voir si le type de la tâche parent est Story. Si la première condition n’est pas satisfaite, alors le type de la tâche parent n’est pas Story, ou n’existe pas du tout. Et cela nous amène à la deuxième condition : EpicLink.
En fait, c’est à ce moment-là que l’on vérifie si le champ Jira « Epic Link » est renseigné (c’est-à-dire que l’on vérifie son existence). La variable EpicLink est également standard et n'a pas besoin d'être mappée. Il s'avère que notre condition est satisfaite si la tâche possède Epic Link.
Et la troisième option est lorsqu'aucune des conditions n'est remplie, c'est-à-dire que la tâche n'a ni parent ni Epic Link. Dans ce cas, nous n'affichons rien et laissons le champ vide. Cela se fait automatiquement puisque nous n’obtiendrons aucun résultat.
Nous avons compris les conditions, passons maintenant aux résultats. Dans les deux cas, il s'agit d'une chaîne avec du texte et une mise en forme spéciale.
Résultat 1 (si le parent est Story) :
"""{color:green}[${Parent.Issuetype}]{color} ${Parent.Summary}"""
Résultat 2 (s'il y a Epic Link) :
"""{color:#713A82}[${EpicLink.Issuetype}]{color} ${EpicLink.EpicName}"""
Les deux résultats ont une structure similaire : ils sont tous deux constitués de guillemets triples « » » au début et à la fin de la chaîne de sortie, de spécifications de couleur dans les blocs d'ouverture {color: COLOR} et de fermeture {color}, ainsi que d'opérations effectuées via le Symbole $. Les guillemets triples indiquent à la structure qu'à l'intérieur il y aura des variables, des opérations ou des blocs de formatage (tels que des couleurs).
Pour le résultat de la première condition, nous :
Ainsi, nous obtenons la chaîne « [Histoire] Un nom de tâche ». Comme vous l'avez peut-être deviné, Summary est également une variable standard. Pour rendre le schéma de construction de telles chaînes plus clair, permettez-moi de partager une image tirée de la documentation officielle.
De la même manière, nous collectons la chaîne pour le deuxième résultat, mais définissons la couleur via le code hexadécimal. J'ai compris que la couleur de l'Epic était « #713A82 » (dans les commentaires, d'ailleurs, vous pouvez suggérer une couleur plus précise pour Epic). N'oubliez pas les champs (propriétés) qui changent pour Epic. Au lieu de « Résumé », utilisez « EpicName », au lieu de « Parent », utilisez « EpicLink ».
En conséquence, le schéma de notre formule peut être représenté comme un tableau de conditions.
Condition : la tâche parent existe et son type est Story.
Résultat : Ligne avec le type vert de la tâche parent et son nom.
Condition : Le champ Epic Link est rempli.
Résultat : Ligne avec la couleur épique du type et son nom.
Par défaut, l'option d'affichage « Général » est sélectionnée dans le champ, et si vous ne la modifiez pas, le résultat ressemblera à du texte brut sans changer la couleur ni identifier les blocs. Si vous changez le format d'affichage en « Wiki Markup », le texte sera transformé.
Faisons maintenant connaissance avec les variables qui ne sont pas liées aux champs Jira : les variables locales.
Grâce à l'exemple précédent, vous avez appris que nous travaillons avec des tâches de type Story, qui comportent des sous-tâches. Cela donne lieu à un cas particulier avec les estimations. Pour obtenir un score Story, nous résumons les scores de ses sous-tâches, qui sont estimés en Story points abstraits.
L'approche est inhabituelle, mais elle fonctionne pour nous. Ainsi, lorsque Story n'a pas d'estimation, mais que les sous-tâches en ont, il n'y a pas de problème, mais lorsque Story et les sous-tâches ont une estimation, l'option standard de Structure, « Σ Story Points », ne fonctionne pas correctement.
En effet, l'estimation de Story s'ajoute à la somme des sous-tâches. En conséquence, un mauvais montant est affiché dans Story. Nous aimerions éviter cela et ajouter une indication d'incohérence avec l'estimation établie dans Story et la somme des sous-tâches.
Nous avons besoin de plusieurs conditions, car tout dépend si l'estimation est définie dans Story.
Les conditions sont donc :
Lorsque Story n'a pas d'estimation , nous affichons l'estimation de la somme des sous-tâches en orange pour indiquer que cette valeur n'a pas encore été définie dans Story.
Si Story a une estimation , alors vérifiez si elle correspond à l'estimation de la somme des sous-tâches :
La formulation de ces conditions peut prêter à confusion, exprimons-les donc sous forme de schéma.
Caractéristiques structurelles utilisées
Un exemple de code
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}"""
Avant de plonger dans le code, transformons notre schéma en une manière plus « semblable à du code » pour comprendre de quelles variables nous avons besoin.
À partir de ce schéma, nous voyons que nous aurons besoin de :
Variables de condition :
Une variable de couleur du texte – couleur
Deux variables d'estimation :
De plus, la variable de couleur dépend également d'un certain nombre de conditions, par exemple de la disponibilité d'un devis et du type de tâche dans la ligne (voir le schéma ci-dessous).
Ainsi, pour déterminer la couleur, nous aurons besoin d'une autre variable de condition, isStory, qui indique si le type de tâche est Story.
La variable sp (storypoints) sera standard, ce qui signifie qu'elle sera automatiquement mappée au champ Jira approprié. Nous devons définir le reste des variables par nous-mêmes et elles seront locales pour nous.
Essayons maintenant d'implémenter les schémas dans le code. Tout d’abord, définissons toutes les variables.
with isEstimated = storypoints != undefined: with childrenSum = sum#children{storypoints}: with isStory = issueType = "Story": with isErr = isStory AND childrenSum != storypoints:
Les lignes sont unies par le même schéma syntaxique : le mot-clé with, le nom de la variable et le symbole deux-points « : » à la fin de la ligne.
Le mot-clé with est utilisé pour désigner les variables locales (et les fonctions personnalisées, mais plus à ce sujet dans un exemple séparé). Il indique à la formule qui va ensuite une variable qui n'a pas besoin d'être mappée. Les deux points « : » marquent la fin de la définition de la variable.
Ainsi, nous créons la variable isEstimated (rappel, ce cas n'est pas important). Nous y stockerons soit 1, soit 0, selon que le champ story points est rempli ou non. La variable storypoints est mappée automatiquement car nous n'avons jamais créé de variable locale du même nom auparavant (par exemple, avec storypoints = … :).
La variable non définie dénote la non-existence de quelque chose (comme null, NaN et autres dans d'autres langues). Ainsi, l’expression storypoints != undefined peut être lue comme une question : « Le champ story points est-il rempli ?
Ensuite, nous devons déterminer la somme des points d’histoire de toutes les tâches enfants. Pour ce faire, nous créons une variable locale : childrenSum.
with childrenSum = sum#children{storypoints}:
Cette somme est calculée via la fonction d'agrégation. (Vous pouvez en savoir plus sur des fonctions comme celle-ci dans la documentation officielle .) En un mot, Structure peut effectuer diverses opérations avec des tâches, en tenant compte de la hiérarchie de la vue actuelle.
Nous utilisons la fonction somme, et en plus, en utilisant le symbole « # », nous transmettons la clarification aux enfants, ce qui limite le calcul de la somme uniquement à toutes les tâches enfants de la ligne actuelle. Entre accolades, nous indiquons quel champ nous voulons résumer — nous avons besoin d'une estimation en storypoints.
La variable locale suivante, isStory, stocke une condition : si le type de tâche dans la ligne actuelle est une Story.
with isStory = issueType = "Story":
Nous nous tournons vers la variable issueType, familière de l'exemple précédent, c'est-à-dire le type de tâche qui correspond au champ souhaité par lui-même. Nous faisons cela parce qu'il s'agit d'une variable standard et que nous ne l'avons pas définie auparavant avec.
Définissons maintenant la variable isErr — elle signale un écart entre la somme de la sous-tâche et l'estimation de l'histoire.
with isErr = isStory AND childrenSum != storypoints:
Ici, nous utilisons les variables locales isStory et childrenSum que nous avons créées précédemment. Pour signaler une erreur, nous avons besoin que deux conditions soient remplies simultanément : le type de problème est Story (isStory) et (ET) la somme des points enfants (childrenSum) n'est pas égale (!=) à l'estimation définie dans la tâche (storypoints ). Tout comme en JQL, nous pouvons utiliser des mots de lien lors de la création de conditions, comme AND ou OR.
Notez que pour chacune des variables locales, il y a un symbole « : » à la fin de la ligne. Cela devrait être à la fin, après toutes les opérations qui définissent la variable. Par exemple, si nous devons diviser la définition d'une variable en plusieurs lignes, alors les deux points « : » ne sont placés qu'après la dernière opération. Comme dans l'exemple avec la variable color - la couleur du texte.
with color = if isStory : if isEstimated : if isErr : "red" else "green" else "orange":
Ici, nous voyons beaucoup de « : », mais ils jouent des rôles différents. Les deux points après if isStory sont le résultat de la condition isStory. Rappelons la construction : si condition : résultat. Présentons cette construction sous une forme plus complexe, qui définit une variable.
with variable = (if condition: (if condition2 : result2 else result3) ):
Il s'avère que si condition2 : résultat2 sinon résultat3 est en quelque sorte le résultat de la première condition, et à la toute fin il y a deux points « : », qui complète la définition de la variable.
À première vue, la définition de la couleur peut sembler compliquée, même si, en fait, nous avons décrit ici le schéma de définition des couleurs présenté au début de l'exemple. C'est juste qu'à la suite de la première condition, une autre condition commence – une condition imbriquée et une autre dans celle-ci.
Mais le résultat final est légèrement différent du schéma présenté précédemment.
if isEstimated : """{color:$color}$storypoints{color} ${if isErr :""" ($childrenSum)"""}""" else """{color:$color}$childrenSum{color}"""
Nous n'avons pas besoin d'écrire « {color}$sp » deux fois dans le code, comme c'était le cas dans le schéma ; nous serons plus intelligents dans les choses. Dans la branche, si la tâche a une estimation, nous afficherons toujours {color: $color}$storypoints{color} (c'est-à-dire juste une estimation en story points dans la couleur nécessaire), et s'il y a une erreur, alors après un espace, nous compléterons la ligne avec la somme des sous-tâches estimées : ($childrenSum).
S'il n'y a pas d'erreur, il ne sera pas ajouté. J'attire également votre attention sur le fait qu'il n'y a pas de symbole « : », puisque nous ne définissons pas de variable, mais affichons le résultat final à travers une condition.
Nous pouvons évaluer notre travail dans l'image ci-dessous dans le champ « ∑SP (mod) ». La capture d'écran montre spécifiquement deux champs supplémentaires :
À l'aide de ces exemples, nous avons analysé les principales fonctionnalités du langage structuré qui vous aideront à résoudre la plupart des problèmes. Examinons maintenant deux autres fonctionnalités utiles, nos fonctions et nos tableaux. Nous verrons comment créer notre propre fonction personnalisée.
Parfois, il y a de nombreuses tâches dans un sprint et nous pouvons manquer de petits changements. Par exemple, nous pouvons manquer une nouvelle sous-tâche ou le fait que l'une des histoires est passée à l'étape suivante. Ce serait bien d'avoir un outil nous informant des derniers changements importants dans les tâches.
Nous nous intéressons à trois types de changements de statut de tâche survenus depuis hier : nous avons commencé à travailler sur la tâche, une nouvelle tâche est apparue, la tâche est fermée. De plus, il sera utile de voir que la tâche est clôturée avec la résolution « Won't Do ».
Pour ce faire, nous allons créer un champ avec une chaîne d’émojis responsables des dernières modifications. Par exemple, si une tâche a été créée hier et que nous avons commencé à travailler dessus, alors elle sera marquée de deux emojis : « En cours » et « Nouvelle tâche ».
Pourquoi avons-nous besoin d'un tel champ personnalisé, si plusieurs champs supplémentaires peuvent être affichés, par exemple la date de passage au statut « En cours » ou un champ « Résolution » distinct ? La réponse est simple : les gens perçoivent les emojis plus facilement et plus rapidement que le texte, qui se trouve dans différents champs et doit être analysé. La formule rassemblera tout en un seul endroit et l'analysera pour nous, ce qui nous fera gagner du temps et des efforts pour des choses plus utiles.
Déterminons de quoi les différents emoji seront responsables :
Caractéristiques structurelles utilisées
Un exemple de code
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 : "❌")
Une analyse de la solution
Pour commencer, réfléchissons aux variables globales dont nous avons besoin pour déterminer les événements qui nous intéressent. Il faut savoir, si depuis hier :
L'utilisation de variables déjà existantes aux côtés de nouvelles variables de mappage nous aidera à vérifier toutes ces conditions.
Passons au code. La première ligne commence par une condition qui vérifie si le type de tâche existe.
if defined(issueType):
Cela se fait via la fonction définie intégrée, qui vérifie l'existence du champ spécifié. Le contrôle est effectué pour optimiser le calcul de la formule.
Nous ne chargerons pas Structure avec des calculs inutiles, si la ligne n'est pas une tâche. Il s'avère que tout le code après if est le résultat, je veux dire, la deuxième partie de la construction if (condition : result). Et si la condition n’est pas remplie, le code ne fonctionnera pas non plus.
La ligne suivante avec now = now(): est également nécessaire pour optimiser les calculs. Plus loin dans le code, nous devrons comparer plusieurs fois différentes dates avec la date actuelle. Afin de ne pas faire plusieurs fois le même calcul, nous allons calculer cette date une seule fois et en faire maintenant une variable locale.
Ce serait aussi bien de garder notre « hier » séparément. Un « hier » pratique s'est transformé empiriquement en 1,3 jour. Faisons cela en variable : avec dayScope = 1.3 :.
Nous devons maintenant calculer plusieurs fois le nombre de jours entre deux dates. Par exemple, entre la date du jour et la date de début des travaux. Bien sûr, il existe une fonction DAYS_BETWEEN intégrée, qui semble nous convenir. Mais si la tâche, par exemple, a été créée vendredi, nous ne verrons pas d'avis de nouvelle tâche lundi, car en fait plus de 1,3 jours se sont écoulés. De plus, la fonction DAYS_BETWEEN ne compte que le nombre total de jours (c'est-à-dire que 0,5 jour se transformera en 0 jour), ce qui ne nous convient pas non plus.
Nous avons formulé une exigence : nous devons calculer le nombre exact de jours ouvrables entre ces dates ; et une fonction personnalisée nous y aidera.
Sa syntaxe de définition est très similaire à la syntaxe de définition d'une variable locale. La seule différence et le seul ajout sont l'énumération facultative des arguments entre les premières parenthèses. Les secondes parenthèses contiennent les opérations qui seront effectuées lorsque notre fonction sera appelée. Cette définition de la fonction n'est pas la seule possible, mais nous utiliserons celle-ci (d'autres peuvent être trouvées dans la documentation officielle ).
with workDaysBetween(today, from)= ( with weekends = (Weeknum(today) - Weeknum(from)) * 2: HOURS_BETWEEN(from;today)/24 - weekends ):
Notre fonction personnalisée workDaysBetween calculera les jours ouvrables entre aujourd'hui et les dates de début, qui sont passées en arguments. La logique de la fonction est très simple : on compte le nombre de jours de congé et on les soustrait du nombre total de jours entre les dates.
Pour calculer le nombre de jours de congé, nous devons savoir combien de semaines se sont écoulées entre aujourd'hui et à partir de. Pour ce faire, on calcule la différence entre les numéros de chacune des semaines. Nous obtiendrons ce numéro grâce à la fonction Weeknum, qui nous fournit le numéro de la semaine du début de l'année. En multipliant cette différence par deux, on obtient le nombre de jours de congé écoulés.
Ensuite, la fonction HOURS_BETWEEN compte le nombre d'heures entre nos dates. Nous divisons le résultat par 24 pour obtenir le nombre de jours, et soustrayons les jours de congé de ce nombre, afin d'obtenir les jours ouvrables entre les dates.
À l'aide de notre nouvelle fonction, définissons un ensemble de variables auxiliaires. Notez que certaines dates dans les définitions sont des variables globales, dont nous avons parlé au début de l'exemple.
with daysAfterCreated = workDaysBetween(now,created): with daysAfterStart = workDaysBetween(now,latestTransitionToProgress): with daysAfterDone = workDaysBetween(now, resolutionDate):
Pour rendre le code facile à lire, définissons des variables qui stockent les résultats des conditions.
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 :
Pour la variable isRecentCreated, j'ai ajouté une condition facultative et not(resolution), ce qui m'aide à simplifier la future ligne, car si la tâche est déjà fermée, alors je ne suis pas intéressé par les informations sur sa création récente.
Le résultat final est construit via la fonction concat, concaténant les lignes.
concat( if isRecentCreated : "*️⃣", if isRecentWork : "🚀", if isRecentDone : "✅", if isWontDo : "❌")
Il s'avère que l'emoji ne sera dans la ligne que lorsque la variable dans la condition est égale à 1. Ainsi, notre ligne peut afficher simultanément des modifications indépendantes de la tâche.
Nous avons abordé le sujet du comptage des jours ouvrables sans jours de congé. Il existe un autre problème lié à cela, que nous analyserons dans notre dernier exemple tout en nous familiarisant avec les tableaux.
Parfois, nous souhaitons savoir depuis combien de temps une tâche est exécutée, sans compter les jours de congé. Cela est nécessaire, par exemple, pour analyser la version publiée. Comprendre pourquoi nous avons besoin de jours de congé. Sauf que l’un circulait du lundi au jeudi et l’autre du vendredi au lundi. Dans une telle situation, on ne peut pas affirmer que les tâches sont équivalentes, même si la différence en jours calendaires nous dit le contraire.
Malheureusement, la structure prête à l'emploi ne sait pas comment ignorer les jours de congé, et le champ avec l'option « Temps en statut… » produit un résultat quels que soient les paramètres Jira, même si samedi et dimanche sont spécifiés comme jours de congé.
De ce fait, notre objectif est de calculer le nombre exact de jours ouvrés, en ignorant les jours chômés, et de prendre en compte l'impact des transitions de statut sur ce temps.
Et qu’est-ce que les statuts ont à voir là-dedans ? Laissez-moi répondre. Supposons que nous ayons calculé qu'entre le 10 et le 20 mars, la tâche a été à l'œuvre pendant trois jours. Mais sur ces 3 jours, il a été en pause pendant un jour et en revue pendant un jour et demi. Il s'avère que la tâche n'a duré qu'une demi-journée.
La solution de l'exemple précédent ne nous convient pas en raison du problème de basculement entre les statuts, car la fonction personnalisée workDaysBetween ne prend en compte que le temps entre deux dates sélectionnées.
Ce problème peut être résolu de différentes manières. La méthode de l'exemple est la plus coûteuse en termes de performances, mais la plus précise en termes de comptage des jours de congés et des statuts. A noter que son implémentation ne fonctionne que dans la version Structure antérieure à 7.4 (décembre 2021).
Ainsi, l’idée derrière la formule est la suivante :
Ainsi, nous obtiendrons l'heure exacte de travail sur la tâche, en ignorant les jours de congé et les transitions entre les statuts supplémentaires.
Caractéristiques structurelles utilisées
Un exemple de code
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}"""
Une analyse de la solution
Avant de transférer notre algorithme dans le code, facilitons les calculs pour Structure.
if defined(issueType) : if status != "Open" :
Si la ligne n'est pas une tâche ou si son statut est « Ouvert », alors nous ignorerons ces lignes. Nous ne nous intéressons qu'aux tâches qui ont été lancées pour fonctionner.
Pour calculer le nombre de jours entre des dates, il faut d'abord déterminer ces dates : finishDate et startDate.
with finishDate = if toQA != Undefined : toQA else if toDone != Undefined : toDone else now(): with startDate = DEFAULT(toProgress, toDone):
Nous supposerons que la date d'achèvement de la tâche (finishDate) est :
La date de début des travaux startDate est déterminée par la date de passage au statut « En cours ». Il existe des cas où la tâche est clôturée sans passer à l'étape en cours de travail. Dans de tels cas, nous considérons la date de clôture comme la date de début, le résultat est donc 0 jour.
Comme vous l'avez peut-être deviné, toQA, toDone et toProgress sont des variables qui doivent être mappées aux statuts appropriés comme dans le premier exemple et les exemples précédents.
Nous voyons également la nouvelle fonction DEFAULT(toProgress, toDone). Il vérifie si toProgress a une valeur, et sinon, il utilise la valeur de la variable toDone.
Vient ensuite la définition de la fonction personnalisée statusWeekendsCount, mais nous y reviendrons plus tard, car elle est étroitement liée aux listes de dates. Il est préférable d'aller directement à la définition de cette liste, pour que nous puissions comprendre plus tard comment lui appliquer notre fonction.
Nous souhaitons obtenir une liste de dates sous la forme suivante : [startDate (disons 11.03), 12.03, 13.03, 14.03… finishDate]. Il n’existe pas de fonction simple qui ferait tout le travail à notre place dans Structure. Alors recourons à une astuce :
Voyons maintenant comment nous pouvons l'implémenter dans le code. Nous travaillerons avec des tableaux.
with overallDays = round(hours_between(startDate,finishDate)/24): with sequenceArray = SEQUENCE(0,overallDays): with datesArray = sequenceArray.map(DATE_ADD(startDate,$,"day")):
Nous comptons combien de jours le travail sur une tâche prendra. Comme dans l'exemple précédent, par division par 24 et par la fonction hours_between(startDate,finishDate). Le résultat est écrit dans la variable globalDays.
Nous créons un tableau de la séquence de nombres sous la forme de la variable séquenceArray. Ce tableau est construit via la fonction SEQUENCE(0,overallDays), qui crée simplement un tableau de la taille souhaitée avec une séquence de 0 à globalDays.
Vient ensuite la magie. L'une des fonctions du tableau est map. Il applique l'opération spécifiée à chaque élément du tableau.
Notre tâche est d'ajouter la date de début à chaque numéro (c'est-à-dire le numéro du jour). La fonction DATE_ADD peut faire cela, elle ajoute un certain nombre de jours, de mois ou d'années à la date spécifiée.
Sachant cela, décryptons la chaîne :
with datesArray = sequenceArray.map(DATE_ADD(startDate, $,"day"))
À chaque élément du séquenceArray, la fonction .map() applique DATE_ADD(startDate, $, « day »).
Voyons ce qui est passé dans les arguments de DATE_ADD. La première chose est startDate, la date à laquelle le numéro souhaité sera ajouté. Ce nombre est spécifié par le deuxième argument, mais nous voyons $.
Le symbole $ désigne un élément du tableau. La structure comprend que la fonction DATE_ADD est appliquée à un tableau, et donc à la place de $ il y aura l'élément de tableau souhaité (c'est-à-dire 0, 1, 2…).
Le dernier argument « jour » indique que nous ajoutons un jour, puisque la fonction peut ajouter un jour, un mois et une année, en fonction de ce que nous spécifions.
Ainsi, la variable datesArray stockera un tableau de dates depuis le début des travaux jusqu'à leur fin.
Revenons à la fonction personnalisée que nous avons manquée. Il filtrera les jours supplémentaires et calculera le reste. Nous avons décrit cet algorithme au tout début de l'exemple, avant d'analyser le code, notamment dans les paragraphes 3 et 4 sur le filtrage des jours de congés et des statuts.
with statusWeekendsCount(dates, status) = ( dates.filter(x -> weekday(x) > 5 and historical_value(this,"status",x)=status).size() ):
Nous allons transmettre deux arguments à la fonction personnalisée : un tableau de dates, appelons-le dates, et le statut requis : statut. Nous appliquons la fonction .filter() au tableau de dates transférées, qui conserve uniquement les enregistrements du tableau qui ont passé par la condition de filtre. Dans notre cas, il y en a deux, et ils se combinent par et. Après le filtre, nous voyons .size(), il renvoie la taille du tableau une fois toutes les opérations effectuées.
Si nous simplifions l'expression, nous obtenons quelque chose comme ceci : array.filter(condition1 and condition2).size(). Ainsi, nous avons obtenu le nombre de jours de congé qui nous convenait, c'est-à-dire les jours de congé qui remplissaient les conditions.
Examinons de plus près les deux conditions :
x -> weekday(x) > 5 and historical_value(this,"status",x)=status
L'expression x -> fait simplement partie de la syntaxe du filtre, indiquant que nous appellerons l'élément du tableau x . Par conséquent, x apparaît dans chaque condition (de la même manière que c'était le cas avec $). Il s'avère que x correspond à chaque date du tableau de dates transféré.
La première condition, weekday(x) > 5, requiert que le jour de la semaine de la date x (c'est-à-dire chaque élément) soit supérieur à 5 : il s'agit soit du samedi (6), soit du dimanche (7).
La deuxième condition utilise historic_value.
historical_value(this,"status",x) = status
C'est une fonctionnalité de Structure de la version 7.4.
La fonction accède à l'historique de la tâche et recherche une date précise dans le champ spécifié. Dans ce cas, nous recherchons la date x dans le champ « statut ». La variable this n'est qu'une partie de la syntaxe de la fonction, elle est mappée automatiquement et représente la tâche en cours dans la ligne.
Ainsi, dans la condition, nous comparons l'argument de statut transféré et le champ « status », qui est renvoyé par la fonction historic_value pour chaque date x du tableau. S'ils correspondent, l'entrée reste dans la liste.
La touche finale est l'utilisation de notre fonction pour compter le nombre de jours dans le statut souhaité :
with progressWeekends = statusWeekendsCount(datesArray, "in Progress"): with progressDays = (timeInProgress/86400000 - progressWeekends).round(1):
Tout d'abord, découvrons combien de jours de congé avec le statut « en cours » ont eu dans notre datesArray. Autrement dit, nous transmettons notre liste de dates et le statut souhaité à la fonction personnalisée statusWeekendsCount. La fonction supprime tous les jours de la semaine et tous les jours chômés dont le statut de la tâche diffère du statut « en cours » et renvoie le nombre de jours restants dans la liste.
Ensuite, nous soustrayons ce montant de la variable timeInProgress, que nous mappons via l'option « Time in status… ».
Le nombre 86400000 est le diviseur qui transformera les millisecondes en jours. La fonction .round(1) est nécessaire pour arrondir le résultat aux dixièmes, par exemple à « 4.1 », sinon vous pouvez obtenir ce type d'entrée : « 4.0999999… ».
Pour indiquer la durée de la tâche, nous introduisons la variable couleur. Nous le modifierons en fonction du nombre de jours passés sur la tâche.
with color = if( progressDays = 0 ; "gray" ; progressDays > 0 and progressDays <= 2.5; "green" ; progressDays > 2.5 and progressDays <= 4; "orange" ; progressDays > 4; "red" ):
Et la dernière ligne avec le résultat des jours calculés :
"""{color:$color}$progressDays d{color}"""
Notre résultat ressemblera à l’image ci-dessous.
À propos, dans la même formule, vous pouvez afficher l'heure de n'importe quel statut. Si, par exemple, nous transmettons le statut « Pause » à notre fonction personnalisée et mappons la variable timeInProgress via « Time in… — Pause », alors nous calculerons l'heure exacte de la pause.
Vous pouvez combiner les statuts et faire une entrée comme « wip : 3.2d | rev: 12d", c'est-à-dire calculer le temps de travail et le temps de révision. Vous n'êtes limité que par votre imagination et votre flux de travail.
Nous avons présenté un nombre exhaustif de fonctionnalités de ce langage de formule qui vous aideront à faire quelque chose de similaire ou à écrire quelque chose de complètement nouveau et intéressant pour analyser les tâches Jira.
J'espère que l'article vous a aidé à comprendre les formules, ou du moins vous a intéressé à ce sujet. Je ne prétends pas avoir « le meilleur code et le meilleur algorithme », donc si vous avez des idées sur la façon d'améliorer les exemples, je serais heureux si vous les partagez !
Bien sûr, vous devez comprendre que personne ne vous parlera mieux des formules que les développeurs d'ALM Works. Par conséquent, je joins des liens vers leur documentation et leurs webinaires. Et si vous commencez à travailler avec des champs personnalisés, consultez-les souvent pour voir quelles autres fonctionnalités vous pouvez utiliser.