Loading …
link-time Logo
Jira Plugin Entwicklung: Eingebettete Bilder im Anhang ausblenden
Jira Plugin Entwicklung: Eingebettete Bilder im Anhang ausblenden Jira Plugin Entwicklung: Eingebettete Bilder im Anhang ausblenden
Jira Plugin Entwicklung: Eingebettete Bilder im Anhang ausblenden
Jira Plugin Entwicklung: Eingebettete Bilder im Anhang ausblenden Jira Plugin Entwicklung: Eingebettete Bilder im Anhang ausblenden Jira Plugin Entwicklung: Eingebettete Bilder im Anhang ausblenden

Jira Plugin Entwicklung: Eingebettete Bilder im Anhang ausblenden

Werden Jira Vorgänge automatisch aus Emails erzeugt, enthalten diese häufig sehr viele Bilder, wie z.B. kleine Symbole von diversen sozialen Netzwerken oder Logos von Firmen.

Dadurch geht das wichtige, angehängte PDF Dokument gerne unter, da Jira alles gleichwertig als Anhang importiert und darstellt. Die Bildchen tauchen daher gleich doppelt auf. Sowohl eingebettet in der Beschreibung als auch als Anhang.

Obwohl Jira viele Möglichkeiten zur Anpassung bietet, gibt es leider keine Möglichkeit dieses Verhalten zu beeinflussen. Da bleibt nur selbst Hand anzulegen und ein eigenes Plugin zu schreiben. Das Ziel ist, Bilder, die bereits in der Beschreibung sichtbar sind, aus den Anhängen auszublenden. Mit diesem Blog schafft man das bequem an einem Vormittag.

Den kompletten Code gibt es auf GitHub: https://github.com/linked-planet/blog-plugin-hide-attachments

Wie erzeugt man ein neues Jira Plugin?

Als erstes erzeugen wir ein neues Plugin. Keine Angst! Wir haben bereits den Löwenanteil der Arbeit erledigt und stellen dafür ein Github-Projekt bereit, das einen passenden Maven Archetype enthält.

Den Archetype kann man mit drei Befehlen im Terminal installieren, wenn Maven bereits installiert ist:

  git clone git@github.com:linked-planet/atlassian-plugin-kotlin.git
  cd atlassian-plugin-kotlin
  install-latest-tag.sh

Als nächstes geht man in den Ordner in dem man das neue Plugin erzeugen möchte und führt folgenden Maven Befehl aus, wie es in der Anleitung zum Plugin erklärt wird:

  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 findet mein sein neues Plugin im Ordner "blog-plugin". Den Ordner kann man als Maven Projekt in Intellij öffnen. Derzeit muss man in den Projekteinstellungen noch Java 11 einstellen. Nun kann man Jira mit Hilfe der Intellij Run-Configuration starten.

Im Browser kann man sich mit admin/admin unter der Adresse jira:2990 einloggen.

Unter Jira-Administration -> Apps verwalten sollte unsere App bereits mit dem bei der Projekterstellung durch nameHumandReadable festgelegten Namen erscheinen, in unserem Beispiel also "Hide IssueAttachment Blog".

Javascript mit Hilfe des Plugins ausliefern.

Als nächstes bringen wir Jira dazu Javascript Code zu laden, sobald ein Vorgang (Issue) angezeigt wird.

Man fügt unter src/main/resources/js eine Datei namens hide-issue-attachments.js hinzu.
Die Datei enthält erstmal nur eine Zeile Code mit der wir überprüfen können, ob unser Code geladen wird.

console.info("[BLOG] hide-issue-attachments.js geladen")

Damit Jira weiss, dass es die Datei ausliefern soll, muss man 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 kann man die run configuration "package" ausführen und im Browser prüfen, ob das Modul geladen wurde.

Jira wird nun die hide-issue-attachments.js an den Browser übertragen. Der Kontext "jira.view.issue" teilt Jira mit, dass das Skript immer genau dann geladen werden soll, wenn ein Vorgang angezeigt wird. Dies ist perfekt für unseren Anwendungsfall, da wir nur die Vorgangs-Ansicht manipulieren wollen.

Mit Javascript den DOM manipulieren.

Nun, da unser Javascript geladen wird, kann der DOM nach herzenszlust manipuliert werden. Hier gibt es aber einiges zu beachten, damit Jira glücklich mit dem vorgefundenen Javascript Code ist.

  • Jira kapselt und komprimiert sämtlichen Javascript Code, bevor er als bulk.js ausgeliefert wird. Aus dem Grund können keine Klassen und einige andere Features, wie z.B. "optional chaining", nicht verwendet werden.
  • Sobald Jira die Seite geladen hat, wird die Methode AJS.toInit() aufgerufen.
  • Auch wenn AJS.toInit() aufgerufen wurde, können Teile der Seite fehlen oder sich später verändern.
  • Atlassian empfiehlt, dass Plugins klar kommunizieren, wenn sie Veränderungen am DOM vornehmen.

Da wir sowieso AJS.toInit() mit einem Lambda aufrufen müssen, definieren wir sämtlichen Code innerhalb dieses Lambdas, um ihn vor dem globalen Namensraum (Namespace) zu verstecken. Bei viel Code muss man natürlich kreativer werden.

Die Struktur in unserem JS sieht also so aus:

  AJS.toInit(() => {
    // ... lots of helper functions
    const observeContentAndHideAttachments = () => {
      // ...
    }
    observeContentAndHideAttachments()
  }

Unser Einstieg ist also die Funktion observeContentAndHideAttachments. Da sich der ganze DOM jederzeit ändern kann, insbesondere wenn Nutzer zwischen verschiedenen Vorgängen wechseln, müssen wir den ganzen DOM permanent beobachten.
Dazu bietet sich das content-element an, auf dem wir einen MutationObserver ansetzen, der alles unterhalb dieses Elements überwacht.

    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 ist also die Funktion observeContentNode. Diese schaut im wesentlichen, ob alle benötigten Bereiche bereits geladen wurden. Wir brauchen die Beschreibung und die Anhänge. Außerdem müssen wir uns merken ob der Code bereits ausgeführt wurde. Der Observer kann relativ oft aufgerufen werden, daher wollen wir aufwändige Operationen möglichst nur einmal ausführen.
Wir merken uns, ob der Code bereits ausgeführt wurde, in dem wir einen unsichtbaren Anhang mit einer speziellen ID einfügen. Ist der Anhang bereits da, können wir sofort stoppen.
Sind die Bedingungen erfüllt laden wir alle Attachments und alle Bilder in der Beschreibung und rufen die Funktion auf, die das eigentlich verstecken übernimmt.

    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 versteckt nun alle Attachments, deren ID einer ID eines Bildes im Dokument entspricht.

    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"
            }
        }
    }

Jetzt müssen wir nur noch die attachment Id für attachments und Bilder herausbekommen. Dazu wird die src URL der Bilder verwendet, die entweder den string "thumbnail" für eingebettete Bilder in der Beschreibung enthält, oder "attachment" für Bilder als Anhang, jeweils gefolgt von der gesuchten id. Das ganze wird mit Hilfe eines regulären Ausdrucks extrahiert.

    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 kann man erneut die run configuration "package" ausführen und im Browser prüfen ob die Bilder nicht mehr als Anhang auftauchen.

Fazit

Das Erzeugen von Jira Plugins mit den richtigen Werkzeugen geht sehr schnell. Auch das Einbinden von Javascript stellt keine große Hürde dar und mit ein paar Tipps lässt sich der DOM von jeder Jira Seite wie gewünscht manipulieren.

Den kompletten Code gibt es auf GitHub: https://github.com/linked-planet/blog-plugin-hide-attachments

 


Heiko Guckes

Heiko Guckes

› github.com/HighKo

› Alle Artikel anzeigen

Ihr Browser ist veraltet

Bitte aktualisieren Sie Ihren Browser, um diese Website korrekt darzustellen. Browser jetzt aktualisieren!

×