Zwei-Wege Kommunikation eines Miro Plugins: Aufruf von Methoden auf dem globalen Miro Objekt und Registrierung für Events mit addHandler.

Miro ohne Grenzen – Wie man eigene Plugins für Miro entwickelt

Keine Kommentare

In den letzten zwei Jahren haben sich viele der Aktivitäten in der Business-Welt zu Remote-Aktivitäten verändert. Für viele von uns sind dadurch neue Tools in den Fokus gerückt.

Aber auch wenn diese Werkzeuge sich enorm weiterentwickelt haben, irgendwann stoßen sie an ihre Grenzen. Und was läge einem Entwickler in so einem Fall näher als zu schauen, wie man diese Grenzen verschieben kann?

Eines der Werkzeuge, die in meinem Alltag eine deutlich größere Rolle einnehmen als früher, ist Miro, eine Online-Whiteboard-Lösung.

In diesem Blog-Artikel beleuchte ich, wie man Miro durch ein eigenes Plugin funktional erweitern kann.

Projektinitialisierung

Miro bietet unter https://developers.miro.com/docs eine Übersicht der zur Verfügung stehenden APIs und SDKs. Für diesen Artikel konzentriere ich mich auf Plugins zur Erweiterung des Miro User Interfaces.

Um mit einem eigenen Plugin loszulegen, sind einige Schritte erforderlich. Als Startpunkt empfehle ich die Seite: https://developers.miro.com/docs/build-your-first-hello-world-app. Ich werde mich in diesem Artikel auf das Anlegen des Entwicklungsprojektes konzentrieren.

Anlegen des Entwicklungsprojektes

Ein initiales Projekt lässt sich über das Command Line Interface create-miro-app erzeugen. Mit dem Befehl:

yarn create miro-app

  • erzeugt man sich ein initiales Projekt. Dabei wird man interaktiv durch ein paar Fragen geführt:
  • Wie soll der Name des Plugins sein?
  • Möchte man mit VanillaJS oder React arbeiten?
  • Soll das Projekt als JavaScript oder TypeScript angelegt werden?

Das API

Als ich angefangen habe, zu recherchieren, wie man Miro erweitern kann, hatte ich zunächst keine Vorstellung davon, wie die API für ein Plugin aussehen würde. Ich war also von den Fragen durchaus überrascht, da ich mit serverseitigem Code und nicht mit einem eigenen Frontend-Projekt gerechnet hätte.
Das Plugin läuft also als eigene Anwendung in einem iFrame innerhalb von Miro. Kommuniziert wird mit der Hauptanwendung über Events. Dies wird allerdings vom Entwickler weggekapselt. Stattdessen programmiert man gegen ein Miro-Objekt, das automatisch global zur Verfügung steht.

Hinweis:
Wer nur schnell mal etwas ausprobieren möchte, erreicht mit create-miro-app schnell sein Ziel. Wer aber wirklich ein Plugin produktreif entwickeln möchte, dem lege ich ans Herz, einen Blick auf mein im Rahmen dieses Blogposts entstandenes Repo bei GitHub zu werfen:
https://github.com/stefan-spittank/miro-affinity-diagram-tools
Dort findet ihr ein Projekt inkl. Test-Setup mit Jest und react-testing-library, JSlint und Prettier.

Das Miro-Objekt erlaubt dem Entwickler die Kommunikation in zwei Richtungen. Zum einen kann er über Methoden Einfluss auf die Inhalte des geöffneten Miro Boards nehmen, zum anderen kann sich über Events über Änderungen im Miro Board benachrichtigen lassen:
Kommunikation mit dem miro Objekt. Änderungen über den Aufruf von Methoden, Events registrieren mit addListener

Aktivierung des Plugins

Damit ein Miro-Plugin aktiv werden kann, muss es sich in die Benutzeroberfläche von Miro einklinken. Dies geschieht in der
index.ts-Datei, die über ein unsichtbares iFrame aus der index.html geladen wird.
In der index.ts kann man sich beim Miro-Objekt für das onReady Callback von miro anmelden. Dieses Callback wird ausgeführt, wenn Miro geladen wurde. Ab diesem Zeitpunkt ist es zulässig, die SDK-Methoden zu verwenden.
In diesem Callback kann nun das eigene Plugin mit der Methode initialize initialisiert werden. Dieser Methode übergibt man ein Konfigurationsobjekt, um sich für verschiedene Extension Points zu registrieren. Extension Points sind dabei Elemente in der Miro-Benutzeroberfläche, die man als Plugin erweitern kann. Beispiele für Extension Points sind die Toolbar, die Bottom Bar oder das Export Menu.
Ein Beispiel für eine Plugin-Initialisierung:

miro.onReady(async () => {
  await miro.initialize({
    extensionPoints: {
      toolbar: {
        title: "Test",
        toolbarSvgIcon: affinityDiagramIcon,
        librarySvgIcon: affinityDiagramIcon,
        async onClick() {
          const isAuthorized = await miro.isAuthorized();
 
          if (!isAuthorized) {
            // Ask the user to authorize the app.
            await miro.requestAuthorization();
          }
          await miro.board.ui.openLeftSidebar(
            "src/SidebarApp/SidebarApp.html"
          );
        },
      },
    },
  });
});

Im Beispiel registrieren wir einen Button in der Toolbar mit dem Titel „Test“ und zwei Icons (Miro unterscheidet hier zwischen zwei Zuständen, die für das Beispiel hier keine Relevanz haben). Wird der Button in der Toolbar angeklickt, erfolgt zunächst eine Überprüfung, ob das Plugin bereits für den Benutzer aktiviert ist. Ist dies nicht der Fall, wird der Benutzer nun zunächst durch den Aufruf von requestAuthorization dazu aufgefordert, das Plugin zu installieren.
Screenshot des Dialogs in Miro der den Benutzer fragt, ob er das Plugin autorisieren möchte.

Hinweis: in diesem Dialog werden dem Benutzer auch die Berechtigungen angezeigt, die das Plugin einfordert. Diese werden allerdings nicht beim Aufruf von requestAuthorization festgelegt, sondern bereits beim Anlegen der App in Miro. Über das Berechtigungskonzept wird gesteuert, auf welche Aspekte eines Miro Boards das Plugin Lese- oder Schreibzugriff hat. Mehr zum Berechtigungskonzept findet ihr hier: https://developers.miro.com/docs/sdk#scopes

Ist das Plugin autorisiert, öffnet es über den Befehl miro.board.ui.openLeftSidebar eine HTML-Seite des Plugins in die linke Seitenleiste von Miro geladen. Diese Seitenleiste rendert wiederum eine React-Anwendung als User Interface des Plugins.

Zugriff auf Elemente des Miro Boards

Nachdem das Plugin sich in Miro registriert und der Benutzer es autorisiert hat, ist nun ein programmatischer Zugriff auf das Board möglich.
Was wäre naheliegender als zunächst ein einfaches Widget mit dem Text “Hello World” anzulegen? Hier der dazu notwendige Code:

await miro.board.widgets.create({
  type: "STICKER",
  text: "Hello World",
  x: 0,
  y: 0
});

Die Erzeugung eines Widgets ist also denkbar einfach. Angegeben werden muss mindestens: der Typ des Widgets, der Inhalt und die Position.
Der Typ des Widgets ist im Beispiel “STICKER”, wodurch ein typisches Post-it-Widget erzeugt wird. Es existieren aber auch eine Reihe weiterer Widget-Typen, wie Text- oder Shape-Widgets.
Der obige Code erzeugt das Widget bei den Koordinaten (0, 0). Dies ist aber vermutlich keine gute Idee, da diese Position nicht unbedingt im für den Benutzer sichtbaren Bereich liegen muss. Der für den aktuellen Benutzer sichtbaren Bereich kann über die Miro-API ausgelesen werden. Im Beispiel würde das so aussehen:

const { x, y, width, height } = await miro.board.viewport.get();

Der Viewport liefert ein Rectangle-Objekt zurück mit x- und y-Koordinaten und Höhe sowie Breite. Die Informationen können nun genutzt werden, um das neue Widget im sichtbaren Bereich zu positionieren.

Reaktion auf Ereignisse

Der obige Code demonstriert sowohl den lesenden als auch den schreibenden Zugriff auf Elemente im Miro Board. Ein weiterer wichtiger Baustein für die Entwicklung eines Miro-Plugins ist die Möglichkeit, auf Ereignisse zu reagieren. Mögliche Ereignisse, auf die ein Plugin reagieren kann, sind:

  • SELECTION_UPDATED
  • WIDGETS_CREATED
  • WIDGETS_DELETED
  • WIDGETS_TRANSFORMATION_UPDATED
  • ALL_WIDGETS_LOADED

Um auf eines dieser Events zu reagieren, muss ein Event Handler registriert werden. Auch dies erfolgt über das globale Miro-Objekt:

miro.addListener('SELECTION_UPDATED', widgets => {
  console.log('Selected widgets:', widgets)
});

Abschluss

Miro hat mit seiner API eine Möglichkeit geschaffen, den Funktionsumfang um Fähigkeiten zu erweitern, die dem Benutzer vielleicht bislang gefehlt haben. Aber wie gut funktioniert das in der Praxis? Mein Fazit:

Was mir gut gefallen hat

Es gab einige Punkte, die mir wirklich gut gefallen haben:

  • Die API war für mich überraschend einfach. Durch das Command-Line Tool create-miro-app habe ich sehr schnell ein erstes Erfolgserlebnis verbuchen können. Ein erster Prototyp war in ein weniger als einem Tag realisierbar.
  • create-miro-app wird aktiv weiterentwickelt. In dem Zeitraum, in dem dieser Blogeintrag entstand, gab es mehrere Releases.
  • Die Möglichkeit, eine React-Anwendung als Miro-Plugin einzusetzen, hat den Vorteil, dass es direkt eine breite Entwicklerbasis gibt, die potenziell Miro-Plugins umsetzen könnte.

Was mir (noch) nicht so gut gefallen hat

Bei der Entwicklung bin ich aber auch auf einige Punkte gestoßen, die aus meiner Sicht noch nicht rund sind:

  • Die Möglichkeit, das Plugin in TypeScript zu entwickeln, ist zwar gegeben, diese wird von Miro aber recht stiefmütterlich behandelt. Zum einen hat sich das Entwicklungsteam gegen den – recht verbreiteten – Weg entschieden, eine eigenes @typings-Modul zu veröffentlichen. Stattdessen wird dem generierten Code eine miro.d.ts-Datei spendiert. In meinem Fall konnte IntelliJ mit der Datei aber nichts anfangen, sodass der initial von create-miro-app erzeugte Zustand für mich nicht nutzbar war (falls euch das auch so geht: in meinem Repo findet liegt die miro.d.ts nun im Ordner @types – das funktioniert).
    Zum anderen ist die Miro-API nicht wirklich gut getypt. An diversen Stellen bekommt man dann doch ein Array von any zurück und muss selbst testen, wie die Objekte wohl aussehen mögen, die man von der API erhält. Beispiel: Das SELECTION_UDPATED-Event liefert ein Event-Objekt mit einer data-Property vom Typ any. Zur Laufzeit ist any mal ein Objekt, mal ein Array. Die Objekte sind Widgets, aber nicht mit allen Eigenschaften, die ein Widget hat, das man über miro.ui.board.widgets.get erhält. Der Typ scheint also zu sein: Partial<SDK.IWidget> | Partial<SDK.IWidget>[].
  • Wenn man nicht nur etwas ausprobieren möchte, sondern ernsthaft ein Plugin entwickelt, sollte man sich früh Gedanken über Testbarkeit machen. Das globale Miro-Objekt ist der Testbarkeit des Codes nicht zuträglich. Ich habe mich für mein Plugin entschieden, das Miro-Objekt zu kapseln und einen eigenen Miro-Provider zu implementieren.
  • Wer ein eigenes Plugin entwickelt, wird früher oder später zu dem Punkt kommen, an dem er das Plugin mit anderen Benutzern verproben möchte. Dann benötigt man neben dem lokal gestarteten Plugin noch eine deployte Variante. Mehrere Stages für Plugins sind aber nicht vorgesehen. Ich habe mich hier so beholfen, dass ich in Miro mehrere Plugins für DEV und PROD registriert habe, die dann zur Laufzeit aber optisch nicht unterscheidbar sind. Also benötigt man auch hier ein eigenes Konzept.

Fazit

Bei aller Kritik an der API bin ich doch sehr dankbar für die Möglichkeit, die Miro hier geschaffen hat. Im Rahmen dieses Artikels konnte ich ein Plugin für die Auswertung von Benutzer-Interviews realisieren, das mir bei meiner täglichen Arbeit eine große Hilfe sein wird. Die Schwächen in der API wird Miro ja vielleicht noch ausmerzen.
Gibt es für euch Dinge, die ihr gerne in Miro erweitern würdet? Oder habt ihr gar schon eigene Erfahrungen in der Entwicklung von Miro-Plugins? Dann lasst doch einen Kommentar da. Ich bin sehr auf eure Anmerkungen gespannt.

Stefan ist seit 2016 für die codecentric AG am Standort Solingen tätig.
Anwendungen nutzbar zu machen und eine gute „User Experience“ zu erreichen ist sein täglich Brot. Dabei helfen ihm auch seine langjährigen Erfahrungen als Verantwortlicher für User Interfaces in der IT-Branche.

Über 1.000 Abonnenten sind up to date!

Die neuesten Tipps, Tricks, Tools und Technologien.
Jede Woche direkt in deine Inbox.

Kostenfrei anmelden und immer auf dem neuesten Stand bleiben!
(Keine Sorge, du kannst dich jederzeit abmelden.)

Kommentieren

Deine E-Mail-Adresse wird nicht veröffentlicht.