Jira Plugin-Entwicklung: Eingebettete Bilder aus Anhängen ausblenden
Jira-Issues enthalten oft doppelte Bilder aus E-Mails, was die Ansicht überlädt. Ein Plugin blendet diese im Anhang aus, belässt sie aber in der Beschreibung. Es nutzt JavaScript und einen MutationObserver zur dynamischen Aktualisierung der Seite – für bessere Usability.
Wenn Jira-Issues automatisch aus E-Mails erstellt werden, enthalten sie oft eine Vielzahl an Bildern – beispielsweise kleine Icons von sozialen Netzwerken oder Firmenlogos.
Das führt dazu, dass ein wichtiges PDF-Dokument leicht übersehen wird, da Jira alles gleichberechtigt als Anhang importiert und anzeigt. Die Bilder erscheinen somit doppelt: eingebettet in der Beschreibung und als Anhang.
Obwohl Jira viele Möglichkeiten zur Anpassung bietet, gibt es leider keine Option, dieses Verhalten direkt zu beeinflussen. Die einzige Lösung ist, ein eigenes Plugin zu schreiben. Ziel ist es, Bilder, die bereits in der Beschreibung sichtbar sind, aus der Anhänge-Liste auszublenden. Mit diesem Blog-Beitrag kannst du das bequem an einem Vormittag umsetzen.
Den fertigen Code findest du aufgithub.
Wie erstellt man ein neues Jira-Plugin?
Zuerst erstellen wir ein neues Plugin. Keine Sorge – wir haben den Löwenanteil bereits erledigt und stellen ein GitHub-Projekt bereit, das ein passendes Maven-Archetyp enthält.
Du kannst das Archetyp mit drei Befehlen im Terminal installieren (sofern Maven bereits installiert ist):
git clone git@github.com:linked-planet/atlassian-plugin-kotlin.git
cd atlassian-plugin-kotlin
install-latest-tag.sh
Gehe dann in das Verzeichnis, in dem du das neue Plugin erstellen willst, und führe folgenden Maven-Befehl aus (wie in der Plugin-Anleitung beschrieben):
mvn archetype:generate -B \
"-DarchetypeGroupId=com.linked-planet.maven.archetype" \
"-DarchetypeArtifactId=atlassian-plugin-kotlin" \
"-DarchetypeVersion=3.3.0" \
"-DatlassianApp=jira" \
"-DatlassianAppVersion=8.20.13" \
"-DgroupId=com.linked-planet.plugin.jira" \
"-DartifactId=blog-plugin" \
"-Dpackage=com.linkedplanet.plugin.jira.blog" \
"-DnameHumanReadable=Hide Issue Attachment Blog" \
"-Ddescription=Hides image attachments that are visible inside the description of an issue." \
"-DorganizationNameHumanReadable=linked-planet GmbH" \
"-DorganizationUrl=https://linked-planet.com" \
"-DinceptionYear=2023" \
"-DgenerateGithubActions=true" \
"-DgenerateDockerEnvironment=true" \
"-DgenerateStubs=true" \
"-DgenerateFrontend=false" \
"-DfrontendAppName=exampleApp" \
"-DfrontendAppNameUpperCamelCase=ExampleApp" \
"-DfrontendAppNameKebabCase=example-app" \
"-DhttpPort=2990" \
"-Dgoals=license:update-file-header"
Danach befindet sich dein neues Plugin im Ordner blog-plugin
. Du kannst diesen Ordner in Intellij als Maven-Projekt öffnen. Aktuell musst du in den Projekteinstellungen Java 11 setzen. Jetzt kannst du Jira mithilfe der Run-Konfiguration in Intellij starten.
Im Browser kannst du dich unter der Adresse jira:2990
mit dem Login admin/admin
anmelden.
Unter Jira Administration → Manage Apps sollte unsere App bereits mit dem im Feld nameHumanReadable
angegebenen Namen erscheinen – in unserem Beispiel: Hide Issue Attachment Blog.
JavaScript über das Plugin bereit stellen
Als Nächstes sorgen wir dafür, dass Jira JavaScript-Code lädt, sobald ein Issue angezeigt wird.
Lege eine Datei namens hide-issue-attachments.js
unter src/main/resources/js
an. Diese enthält vorerst nur eine Zeile zur Prüfung, ob unser Code geladen wurde:
console.info("[BLOG] hide-issue-attachments.js geladen")
Damit Jira die Datei ausliefert, musst du folgende Web-Resource in die atlassian-plugin.xml
einfügen:
<web-resource key="hide-issue-attachments">
<dependency>com.atlassian.auiplugin:ajs</dependency>
<resource type="download" name="hide-issue-attachments.js" location="js/hide-issue-attachments.js"/>
<context>jira.view.issue</context>
</web-resource>
Nun kannst du die Run-Konfiguration package
ausführen und im Browser prüfen, ob das Modul geladen wurde.
Jira liefert jetzt die hide-issue-attachments.js
an den Browser aus. Der Kontext jira.view.issue
sorgt dafür, dass das Script genau dann geladen wird, wenn ein Issue angezeigt wird – perfekt für unseren Anwendungsfall.
DOM mit JavaScript manipulieren
Da unser JavaScript nun geladen wird, können wir den DOM nach Belieben manipulieren. Dabei gilt es jedoch einige Dinge zu beachten, damit Jira mit unserem Code zufrieden ist:
- Jira kapselt und komprimiert allen JavaScript-Code, bevor er als
bulk.js
ausgeliefert wird. Daher sind Klassen und bestimmte Features wie "optional chaining" nicht nutzbar. - Nachdem Jira die Seite geladen hat, wird
AJS.toInit()
aufgerufen. - Auch wenn
AJS.toInit()
bereits durchgelaufen ist, können Teile der Seite später geladen oder verändert werden. - Atlassian empfiehlt, dass Plugins DOM-Manipulationen klar kennzeichnen.
Da wir sowieso eine Lambda-Funktion für AJS.toInit()
benötigen, definieren wir allen Code darin, um ihn aus dem globalen Namensraum herauszuhalten.
Unsere Struktur sieht also so aus:
AJS.toInit(() => {
// ... lots of helper functions
const observeContentAndHideAttachments = () => {
// ...
}
observeContentAndHideAttachments()
}
Der Einstiegspunkt ist die Funktion observeContentAndHideAttachments
. Da sich der gesamte DOM jederzeit ändern kann (z. B. durch das Umschalten zwischen Aktionen), müssen wir ihn dauerhaft beobachten. Dazu verwenden wir den Content-Knoten und platzieren darauf einen MutationObserver
.
const observeContentAndHideAttachments = () => {
const targetNode = document.getElementById("content")
if (!targetNode) {
console.info("[BLOG] hide-issue-attachments failed to load, because the document does not contain 'content' node.")
} else {
console.info("[BLOG] hide-issue-attachments modification loaded.")
const observer = new MutationObserver(observeContentNode)
observer.observe(targetNode, {
childList: true,
subtree: true,
})
}
}
Unser Observer ruft observeContentNode
auf. Diese prüft, ob alle nötigen Bereiche geladen sind – insbesondere die Beschreibung und die Anhänge. Außerdem merken wir uns, ob unser Code bereits ausgeführt wurde. Das tun wir, indem wir ein unsichtbares Element mit einer speziellen ID einfügen. Ist es bereits vorhanden, brechen wir sofort ab.
const alreadyExecutedId = "hide-issue-attachment-was-already-executed"
const addAlreadyExecutedLiElement = attachmentModule => {
const iWasHere = document.createElement("li")
iWasHere.id = alreadyExecutedId
iWasHere.style.display = "none"
attachmentModule.querySelector("#attachment_thumbnails").appendChild(iWasHere)
};
const observeContentNode = (/*mutationsList, observer*/) => {
if (document.getElementById(alreadyExecutedId)) return
const attachmentModule = document.querySelector("#attachmentmodule")
if (!attachmentModule) return
const descriptionModule = document.querySelector("#descriptionmodule")
if (!descriptionModule) return
const attachmentsList = attachmentModule.querySelectorAll("#attachment_thumbnails li")
addAlreadyExecutedLiElement(attachmentModule) // increases attachmentsList.length
if (attachmentsList.length === 0) return // no attachments to hide
const embeddedImages = document.querySelectorAll("#descriptionmodule img")
if (embeddedImages.length === 0) return
hideAttachmentsForEmbeddedImages(attachmentsList, embeddedImages)
}
Die Funktion hideAttachmentsForEmbeddedImages
blendet alle Anhänge aus, deren Bild-ID mit der eines Bildes in der Beschreibung übereinstimmt:
const hideAttachmentsForEmbeddedImages = (attachments /* : NodeList*/, embeddedImages /* : NodeList*/) => {
const embeddedImgIds = Array.from(embeddedImages)
.map(extractAttachmentIdFromImg)
for (const listItem of attachments) {
const attachmentImg = listItem.querySelector("img")
const attachmentImgId = extractAttachmentIdFromImg(attachmentImg)
if (!attachmentImgId) continue // attachment has no image
if (embeddedImgIds.includes(attachmentImgId)) {
console.info(`[BLOG] hid attachment from DOM with image id=${attachmentImgId} element:%o`, listItem)
listItem.style.display = "none"
}
}
}
Nun müssen wir nur noch die Attachment-ID aus den src
-URLs der Bilder extrahieren. Diese enthalten entweder thumbnail
(für eingebettete Bilder) oder attachment
, gefolgt von der ID:
const extractAttachmentIdFromImg = img => {
const src = img && img.src
if (!src) return undefined
let match = src.match(/[a-zA-Z]+:\/\/.*\/(thumbnail|attachment)\/(?<id>\d+)\/.*/)
if (match && match.length > 2) return match.groups.id
}
Nun kannst du die Run-Konfiguration package
erneut ausführen und im Browser prüfen, ob die Bilder nicht mehr als Anhang erscheinen.
Fazit
Mit den richtigen Werkzeugen lassen sich Jira-Plugins sehr schnell erstellen. Auch die Integration von JavaScript stellt keine große Hürde dar – und mit ein paar Tricks lässt sich das DOM jeder Jira-Seite ganz nach Wunsch anpassen.
Den fertigen Code findest du auf github.