Bevorstehende Änderungen im Beitragsmenü - So bereiten Sie Themes und Plugins vor

Als Teil unserer kontinuierlichen Bemühungen zur Verbesserung der Discourse-Codebasis entfernen wir die Verwendung des veralteten „Widget“-Rendersystems und ersetzen es durch Glimmer-Komponenten.

Kürzlich haben wir das Beitragsmenü modernisiert, das jetzt in Discourse über die Einstellung glimmer_post_menu_mode verfügbar ist.

Diese Einstellung akzeptiert drei mögliche Werte:

  • disabled: Verwendet das veraltete „Widget“-System.
  • auto: Erkennt die Kompatibilität Ihrer aktuellen Plugins und Themes. Wenn welche inkompatibel sind, wird das veraltete System verwendet; andernfalls wird das neue Menü verwendet.
  • enabled: Verwendet das neue Menü. Wenn Sie ein inkompatibles Plugin oder Theme haben, kann Ihre Seite beschädigt werden.

Wir haben unsere offiziellen Plugins bereits aktualisiert, um mit dem neuen Menü kompatibel zu sein. Wenn Sie jedoch noch ein Drittanbieter-Plugin, -Theme oder eine -Theme-Komponente haben, die mit dem neuen Menü inkompatibel ist, ist ein Upgrade erforderlich.

Warnungen werden in der Browserkonsole ausgegeben, die die Quelle der Inkompatibilität identifizieren.

:timer_clock: Zeitplan für die Einführung

Dies sind grobe Schätzungen, die Änderungen unterliegen

Q4 2024:

  • :white_check_mark: Kernimplementierung abgeschlossen
  • :white_check_mark: Offizielle Plugins aktualisiert
  • :white_check_mark: Auf Meta aktiviert
  • :white_check_mark: glimmer_post_menu_mode standardmäßig auf auto; Deprecation-Meldungen in der Konsole aktiviert
  • :white_check_mark: Upgrade-Empfehlungen veröffentlicht

Q1 2025:

  • :white_check_mark: Drittanbieter-Plugins und Themes sollten aktualisiert sein
  • :white_check_mark: Deprecation-Meldungen beginnen und lösen eine Warnbanner für Administratoren für verbleibende Probleme aus
  • :white_check_mark: Das neue Beitragsmenü standardmäßig aktiviert

Q2 2025

  • :white_check_mark: 1. April - Entfernung der Feature-Flag-Einstellung und des veralteten Codes

:eyes: Was bedeutet das für mich?

Wenn Ihr Plugin oder Theme „Widget“-APIs verwendet, um das Beitragsmenü anzupassen, müssen diese aktualisiert werden, um mit der neuen Version kompatibel zu sein.

:person_tipping_hand: Wie kann ich das neue Beitragsmenü testen?

In der neuesten Version von Discourse wird das neue Beitragsmenü aktiviert, wenn Sie keine inkompatiblen Plugins oder Themes haben.

Wenn Sie inkompatible Erweiterungen installiert haben, können Sie als Administrator die Einstellung trotzdem auf enabled ändern, um das neue Menü zu erzwingen. Verwenden Sie dies mit Vorsicht, da Ihre Seite je nach installierten Anpassungen beschädigt sein könnte.

In dem unwahrscheinlichen Fall, dass dieses automatische System nicht wie erwartet funktioniert, können Sie diesen „automatischen Feature-Flag“ vorübergehend mit der oben genannten Einstellung überschreiben. Falls Sie dies tun müssen, teilen Sie uns dies bitte in diesem Thema mit.

:technologist: Muss ich mein Plugin oder Theme aktualisieren?

Sie müssen Ihre Plugins oder Themes aktualisieren, wenn sie eine der folgenden Anpassungen durchführen:

  • Verwendung von decorateWidget, changeWidgetSetting, reopenWidget oder attachWidgetAction an diesen Widgets:

    • post-menu
    • post-user-tip-shim
    • small-user-list
  • Verwendung einer der folgenden API-Methoden:

    • addPostMenuButton
    • removePostMenuButton
    • replacePostMenuButton

:bulb: Falls Sie Erweiterungen haben, die eine der oben genannten Anpassungen durchführen, wird eine Warnung in der Konsole ausgegeben, die das Plugin oder die Komponente identifiziert, die aktualisiert werden muss, wenn Sie eine Topic-Seite aufrufen.

Die Deprecation-ID lautet: discourse.post-menu-widget-overrides

:warning: Wenn Sie mehr als ein Theme in Ihrer Instanz verwenden, überprüfen Sie bitte alle, da Warnungen nur für aktive Plugins und derzeit verwendete Themes sowie Theme-Komponenten ausgegeben werden.

Was sind die Ersatzlösungen?

Wir haben den Value Transformer post-menu-buttons als neue API zur Anpassung des Beitragsmenüs eingeführt.

Der Value Transformer bietet ein DAG-Objekt, das das Hinzufügen, Ersetzen, Entfernen oder Neuordnen von Schaltflächen ermöglicht. Es stellt zudem Kontextinformationen bereit, wie den Beitrag, der mit dem Menü verknüpft ist, den Status des angezeigten Beitrags und Schaltflächenschlüssel, um eine einfachere Platzierung der Elemente zu ermöglichen.

Die DAG-API erwartet Ember-Komponenten, wenn die API eine neue Schaltflächendefinition wie .add oder .replace benötigt.

Jede Anpassung ist anders, aber hier sind einige Richtlinien für die häufigsten Anwendungsfälle:

addPostMenuButton

Vorher:

withPluginApi("1.34.0", (api) => {
  api.addPostMenuButton("solved", (attrs) => {
    if (attrs.can_accept_answer) {
      const isOp = currentUser?.id === attrs.topicCreatedById;
      return {
        action: "acceptAnswer",
        icon: "far-check-square",
        className: "unaccepted",
        title: "solved.accept_answer",
        label: isOp ? "solved.solution" : null,
        position: attrs.topic_accepted_answer ? "second-last-hidden" : "first",
      };
    }
  });
});

Nachher:

Die folgenden Beispiele verwenden das Template Tag Format (gjs) von Ember.

// components/solved-accept-answer-button.gjs
import Component from "@glimmer/component";
import { action } from "@ember/object";
import { inject as service } from "@ember/service";
import DButton from "discourse/components/d_button";
import { ajax } from "discourse/lib/ajax";
import { popupAjaxError } from "discourse/lib/ajax-error";

export default class SolvedAcceptAnswerButton extends Component {
  // Gibt an, ob die Schaltfläche sofort angezeigt oder hinter der „Mehr anzeigen“-Schaltfläche versteckt wird
  static hidden(args) { 
    return args.post.topic_accepted_answer;
  }

  ...

  <template>
    <DButton
      class="post-action-menu__solved-unaccepted unaccepted"
      ...attributes
      @action={{this.acceptAnswer}}
      @icon="far-check-square"
      @label={{if this.showLabel "solved.solution"}}
      @title="solved.accept_answer"
    />
  </template>
}

// initializer.js
import SolvedAcceptAnswerButton from "../components/solved-accept-answer-button";

...
withPluginApi("1.34.0", (api) => {
  api.registerValueTransformer(
    "post-menu-buttons",
    ({
      value: dag, 
      context: {
        post,
        firstButtonKey, // Schlüssel der ersten Schaltfläche
        secondLastHiddenButtonKey, // Schlüssel der zweiten vorletzten versteckten Schaltfläche
        lastHiddenButtonKey, // Schlüssel der letzten versteckten Schaltfläche
      },
    }) => {
        dag.add(
          "solved",
          SolvedAcceptAnswerButton,
          post.topic_accepted_answer
            ? {
                before: lastHiddenButtonKey,
                after: secondLastHiddenButtonKey,
              }
            : {
                before: [
                  "assign", // vom assign-Plugin hinzugefügte Schaltfläche
                  firstButtonKey,
                ],
              }
        );
    }
  );
});

:bulb: Styling Ihrer Schaltflächen

Es wird empfohlen, ...attributes wie im obigen Beispiel in Ihrer Komponente einzufügen.

In Kombination mit der Verwendung der Komponenten DButton oder DMenu übernimmt dies die Boilerplate-Klassen und stellt sicher, dass Ihre Schaltfläche dem Format der anderen Schaltflächen im Beitragsmenü folgt.

Zusätzliche Formatierungen können mit Ihren benutzerdefinierten Klassen angegeben werden.

replacePostMenuButton

  • Vorher:
withPluginApi("1.34.0", (api) => {
  api.replacePostMenuButton("like", {
    name: "discourse-reactions-actions",
    buildAttrs: (widget) => {
      return { post: widget.findAncestorModel() };
    },
    shouldRender: (widget) => {
      const post = widget.findAncestorModel();
      return post && !post.deleted_at;
    },
  });
});
  • Nachher:
import ReactionsActionButton from "../components/discourse-reactions-actions-button";

...

withPluginApi("1.34.0", (api) => {
  api.registerValueTransformer(
    "post-menu-buttons",
    ({ value: dag, context: { buttonKeys } }) => {
      // ReactionsActionButton ist die neue Schaltflächenkomponente
      dag.replace(buttonKeys.LIKE, ReactionsActionButton);
    }
  );
});

removePostMenuButton

  • Vorher:
withPluginApi("1.34.0", (api) => {
  api.removePostMenuButton('like', (attrs, state, siteSettings, settings, currentUser) => {
    if (attrs.post_number === 1) {
      return true;
    }
  });
});
  • Nachher:
withPluginApi("1.34.0", (api) => {
  api.registerValueTransformer(
    "post-menu-buttons",
    ({ value: dag, context: { post, buttonKeys } }) => {
      if (post.post_number === 1) {
        dag.delete(buttonKeys.LIKE);
      }
    }
  );
});

:sos: Was ist mit anderen Anpassungen?

Wenn Ihre Anpassung nicht mit der neuen API erreicht werden kann, die wir eingeführt haben, teilen Sie uns dies bitte mit, indem Sie ein neues Entwickler-Thema erstellen, um dies zu diskutieren.

:sparkles: Ich bin Autor eines Plugins/Themes. Wie aktualisiere ich ein Theme/Plugin, um während der Übergangszeit sowohl das alte als auch das neue Beitragsmenü zu unterstützen?

Wir haben das folgende Muster verwendet, um sowohl die alte als auch die neue Version des Beitragsmenüs in unseren Plugins zu unterstützen:

function customizePostMenu(api) {
  const transformerRegistered = api.registerValueTransformer(
    "post-menu-buttons",
    ({ value: dag, context }) => {
      // neue Beitragsmenü-Anpassungen
      ...
    }
  );

  const silencedKey =
    transformerRegistered && "discourse.post-menu-widget-overrides";

  withSilencedDeprecations(silencedKey, () => customizeWidgetPostMenu(api));
}

function customizeWidgetPostMenu(api) {
  // alte „Widget“-Code-Anpassung hier
  ...
}

export default {
  name: "my-plugin",

  initialize(container) {
    withPluginApi("1.34.0", customizePostMenu);
  }
};

:star: Weitere Beispiele

Sie können unsere offiziellen Plugins auf Beispiele für die Verwendung der neuen API überprüfen:

Wir haben das Glimmer Post Menü standardmäßig auf enabled umgestellt.

Sobald Ihre Discourse-Instanz aktualisiert wurde, führt dies dazu, dass bestehende Anpassungen, die nicht an die neue API angepasst wurden, nicht angewendet werden.

Vorerst können Administratoren die Einstellung immer noch auf disabled zurücksetzen, während die restlichen Anpassungen aktualisiert werden.

Der Legacy-Code wird Anfang Q2 entfernt.

Ich schätze die Flexibilität, die volle Komponenten-API zur Verfügung zu haben. Mir gefällt die Syntax von Glimmer-Komponenten insgesamt, und ich sehe, warum sie Vorteile bei der Reduzierung der Komplexität des Codebestands haben mag.

Für einfache Anwendungsfälle (ich möchte einen Button hinzufügen und ihm ein Icon geben) waren die alten API-Methoden jedoch objektiv kürzer und leichter verständlich (weniger Importe, weniger Operationen, weniger exponierter API-Fußabdruck). Gibt es einen Grund, warum die alten API-Methoden keine fortgesetzte Unterstützung genießen könnten? Ich stelle mir vor, wenn Sie sie als Komfortfunktionen verwenden und die zugrunde liegende Implementierung mit Ihrer neuen Glimmer-Komponente durchführen würden, dann könnte die API-Methode auch die Versionskompatibilitätsprüfung durchführen.

Dies wäre für jeden, der diese Methoden verwendet, weitaus weniger störend und würde zu weniger Code-Explosionen von bedingter Logik im Plugin-Ökosystem führen.

Meine Hauptbeschwerde über die vorhandenen Widgets ist deren mangelnde Dokumentation. Dieser Beitrag, der ihre Entfernung ankündigt, ist eines der klarsten Dokumente, die ich gesehen habe, dass diese speziellen Methoden existieren und wie man sie verwendet. Ich bin tatsächlich hierher gekommen, um herauszufinden, wie man die alte API verwendet.

Mir gefällt, dass Transformer an einem Ort, mit String-Literalen registriert werden. Ich denke, das erleichtert die Dokumentation (und die Plugin-Entwicklung) erheblich.

Bei Widgets scheinen sie alle über die Methode createWidget registriert zu werden, die dann createWidgetFrom aufruft. Das Problem, das ich hier sehe, ist, dass das _Registry eine globale Variable im Dateibereich ist, die durch eine API geschützt ist, und die API keine Iteration zulässt. Wenn wir eine Iterationsfunktion für das Widget-Registry erhalten könnten, könnten wir in Echtzeit die aktuell registrierten Widgets entdecken. Es sollte dokumentiert werden: “Führen Sie diese Zeile JavaScript in Ihrer Browserkonsole aus, um die API aufzurufen und das Registry aufzulisten”. Dann könnten wir eine sehr ähnliche Funktionalität erhalten, wie sie das Transformer-Registry bietet.

Eine weitere Sache, die bei der Plugin-Entwicklung helfen würde, wäre, ein Attribut auf jedem Root-DOM-Element zu sehen, das von einer Komponente/einem Widget gerendert wird und Ihnen mitteilt, welche Komponente/welches Widget Sie gerade betrachten. Wie z.B. “data-widget=foo”. Dies könnte eine Debugging-Funktion sein oder einfach standardmäßig aktiviert sein. Es ist OSS, also ist es nicht so, als würden Sie Sicherheit durch Obskurität erreichen.

Ich feiere den Wandel hin zu Glimmer-Komponenten. Aber das wird Zeit brauchen, und es gibt viele Widgets, mit denen die Leute in der Zwischenzeit arbeiten müssen. Daher denke ich, dass die Verbesserung der Sichtbarkeit von Widgets, wie oben erwähnt, den Übergangszeitraum für alle wahrscheinlich erleichtern würde.

Was diese API-Methoden betrifft… Es sieht so aus, als ob jemand sich die Mühe gemacht hat, detaillierte Kommentare zur JavaScript-API hinzuzufügen, aber es wurde nie eine Dokumentationsseite dafür generiert. Warum nicht?

Ich würde gerne einen Pull-Request zum Iterieren durch das Widget-Registry einreichen, wenn das in Ordnung ist.

Außerdem, wenn ich nur die neue Funktionalität implementieren möchte, auf welche Version der Discourse API sollte ich die Plugin-Kompatibilität festlegen? Du verwendest withPluginApi("1.34.0", ...) in all deinen Beispielen. Ich denke, das ist eine ältere Version und nicht repräsentativ dafür, wann diese Änderung vorgenommen wurde? Bitte kläre das. Danke!

Es ist die richtige Version. Sie können das Änderungsprotokoll hier einsehen:

https://github.com/discourse/discourse/blob/main/docs/CHANGELOG-JAVASCRIPT-PLUGIN-API.md?plain=1#L64-L67

Außerdem kann diese Funktion helfen: Pinning plugin and theme versions for older Discourse installs (.discourse-compatibility)

Die Entfernung von Widgets läuft seit einigen Jahren, und wir hoffen, sie in den nächsten Monaten abzuschließen. Daher denke ich nicht, dass wir vorher noch Änderungen am zugrunde liegenden System vornehmen werden.

Haben Sie den Ember Inspector ausprobiert? Ich denke, er sollte das von Ihnen beschriebene Problem lösen und zeigt sogar Ember-Komponenten an, die derzeit keine DOM-Elemente rendern.

Seit sehr kurzem ist diese Versionsnummer nicht mehr erforderlich. Sie können tun

export default apiInitializer((api) => {

oder

withPluginApi((api) => {

Wir werden die Dokumentation bald mit dieser Änderung aktualisieren. Die moderne bevorzugte Methode zur Verwaltung der Versionskompatibilität ist die Datei .discourse-compatibility, die @Arkshine erwähnt hat:

Das ist wirklich schön! Jedes Mal, wenn ich vergesse, diese Nummer zu aktualisieren :rofl:.

@david @Arkshine wow, tolle Beiträge, wirklich hilfreich!

Eine weitere Frage – bezüglich des „Kontexts“, der beim Aufruf von api.registerValueTransformer geteilt wird, wie kann ich herausfinden, welcher Kontext mir übergeben wird? Ich nehme an, ich kann den Kontext einfach per Konsolenprotokoll ausgeben, aber es wäre schön, vorher zu wissen, was verfügbar ist.

Für das Plugin, das ich gerade schreibe, gebe ich dem Autor eines Themas spezielle Moderationsprivilegien. Dazu muss ich den „aktuellen Themaautor“, den „aktuellen Benutzer“ und die „Gruppenzugehörigkeit des angemeldeten Benutzers“ kennen.

Vielleicht hilft dieses spezielle Beispiel, meine Frage zu verdeutlichen.

EDIT:

Mein endgültiger Code sieht für alle anderen mit ähnlichen Interessen wie folgt aus:

const currentUser = api.getCurrentUser()
const validGroups = currentUser.groups.filter(g => ['admins', 'staff', 'moderators'].includes(g.name))

// if current user is the post author, or they are in a privileged group
if (post?.topic?.details?.created_by?.id === currentUser.id || validGroups.length > 0) {
  // give them access to the feature
  ...
}

Ich fürchte, wir haben derzeit keine zentrale Dokumentation dafür. Einige Bereiche der App haben ihre eigenen spezifischen Dokumente z. B. die Themenliste, die die Kontextargumente auflisten. Ansonsten ist es am besten, den applyTransformer-Aufruf in der Codebasis des Kerns zu suchen oder console.log zu verwenden.

Allgemeine Dokumente zu Transformer finden Sie hier: Using Transformers to customize client-side values and behavior

Die Suche nach „applyTransformer“ liefert 0 Ergebnisse. Suche ich am falschen Ort?

Ich stelle fest, dass die Suche nach „api.registerValueTransformer“ einige hilfreiche Beispiele liefert. Aber natürlich bieten die Beispiele keine umfassende Dokumentation des zurückgegebenen Kontexts – sie zeigen nur diejenigen, die für dieses spezielle Beispiel nützlich waren.

Aus console.log von context in meinem spezifischen Beispiel sehe ich, dass post zurückgegeben wird, user jedoch nicht. Wie kann ich also auf andere Anwendungszustände zugreifen, die nicht im Kontext enthalten sind?

Ich verstehe, dass man zuvor helper.getModel() oder helper.currentUser in einem api.decorateWidget-Kontext aufrufen konnte. Ich gehe davon aus, dass es eine aktuelle Methode gibt, um ähnliche Ergebnisse zu erzielen.

Vielen Dank für all die Hilfe hier.

Oh, ich glaube, ich habe meine eigene Frage beantwortet. Dieses Beispiel zeigt die Verwendung von api.getCurrentUser(). Dieser Teil der API hat sich also im Wesentlichen nicht geändert und ist weiterhin mit dem Glimmer-Paradigma kompatibel.

Ich glaube, er meinte applyValueTransformer oder applyBehaviorTransformer. Solche Funktionen finden Sie in der folgenden Datei: discourse/app/assets/javascripts/discourse/app/lib/transformer.js at main · discourse/discourse · GitHub

Der Code für das Legacy-Post-Menü wurde jetzt entfernt. Danke an alle, die an der Aktualisierung ihrer Themes & Plugins gearbeitet haben :rocket: