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

As part of our continuous effort to improve the Discourse codebase, we’re removing uses of the legacy “widget” rendering system, and replacing them with Glimmer components.

Recently, we modernized the post menu, and is now available in Discourse behind the glimmer_post_menu_mode setting.

This setting accepts three possible values:

  • disabled: use the legacy “widget” system
  • auto: will detect the compatibility of your current plugins and themes. If any are incompatible, it will use the legacy system; otherwise it will use the new menu.
  • enabled: will use the new menu. If you have any incompatible plugin or theme, your site may be broken.

We already updated our official plugins to be compatible with the new menu, but if you still have any third-party plugin, theme, or theme component incompatible with the new menu, upgrading them will be required.

Warnings will be printed in the browser console identifying the source of the incompatibility.

:timer_clock: Roll-out Timeline

These are rough estimates subject to change

Q4 2024:

  • :white_check_mark: core implementation finished
  • :white_check_mark: official plugins updated
  • :white_check_mark: enabled on Meta
  • :white_check_mark: glimmer_post_menu_mode default to auto; console deprecation messages enabled
  • :white_check_mark: published upgrade advice

Q1 2025:

  • :white_check_mark: third-party plugins and themes should be updated
  • :white_check_mark: deprecation messages start, triggering an admin warning banner for any remaining issues
  • :white_check_mark: enabled the new post menu by default

Q2 2025

  • :white_check_mark: 1st April - removal of the feature flag setting and legacy code

:eyes: What does it mean for me?

If your plugin or theme uses any ‘widget’ APIs to customize the post menu, those will need to be updated for compatibility with the new version.

:person_tipping_hand: How do I try the new Post Menu?

In the latest version of Discourse, the new post menu will be enabled if you don’t have any incompatible plugin or theme.

If you do have incompatible extensions installed, as an admin, you can still change the setting to enabled to force using the new menu. Use this with caution as your site may be broken depending on the customizations you have installed.

In the unlikely event that this automatic system does not work as expected, you can temporarily override this ‘automatic feature flag’ using the setting above. If you need to that, please let us know in this topic.

:technologist: Do I need to update my plugin and theme?

You will need to update your plugins or themes if they perform any of the customizations below:

  • Use decorateWidget, changeWidgetSetting, reopenWidget or attachWidgetAction on these widgets:

    • post-menu
    • post-user-tip-shim
    • small-user-list
  • Use any of the API methods below:

    • addPostMenuButton
    • removePostMenuButton
    • replacePostMenuButton

:bulb: In case you have extensions that perform one of the customizations above, a warning will be printed in the console identifying the plugin or component that needs to be upgraded, when you access a topic page.

The deprecation ID is: discourse.post-menu-widget-overrides

:warning: If you use more than one theme in your instance, be sure to check all of them as the warnings will be printed only for the active plugins and currently used themes and theme-components.

What are the replacements?

We introduced the value transformer post-menu-buttons as the new API to customize the post menu.

The value transformer provides a DAG object which allows adding, replacing removing, or reordering the buttons. It also provides context information such as the post associated with the menu, the state of post being displayed and button keys to enable a easier placement of the items.

The DAG APIs expects to receive Ember components if the API needs a new button definition like .add and .replace

Each customization is different, but here is some guidance for the most common use cases:

addPostMenuButton

Before:

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",
      };
    }
  });
});

After:

The examples below use Ember’s Template Tag Format (gjs)

// 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 {
  // indicates if the button will be prompty displayed or hidden behind the show more button
  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, // key of the first button
        secondLastHiddenButtonKey, // key of the second last hidden button
        lastHiddenButtonKey, // key of the last hidden button
      },
    }) => {
        dag.add(
          "solved",
          SolvedAcceptAnswerButton,
          post.topic_accepted_answer
            ? {
                before: lastHiddenButtonKey,
                after: secondLastHiddenButtonKey,
              }
            : {
                before: [
                  "assign", // button added by the assign plugin
                  firstButtonKey,
                ],
              }
        );
    }
  );
});

:bulb: Styling your buttons

It’s recommended to include ...attributes as shown in the example above in your component.

When combined with the use of the components DButton or DMenu this will take care of the boilerplate classes and ensure your button follows the formatting of the other buttons in the post menu.

Additional formatting can be specified using your custom classes.

replacePostMenuButton

  • before:
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;
    },
  });
});
  • after:
import ReactionsActionButton from "../components/discourse-reactions-actions-button";

...

withPluginApi("1.34.0", (api) => {
  api.registerValueTransformer(
    "post-menu-buttons",
    ({ value: dag, context: { buttonKeys } }) => {
      // ReactionsActionButton is the bnew button component
      dag.replace(buttonKeys.LIKE, ReactionsActionButton);
    }
  );
});

removePostMenuButton

  • before:
withPluginApi("1.34.0", (api) => {
  api.removePostMenuButton('like', (attrs, state, siteSettings, settings, currentUser) => {
    if (attrs.post_number === 1) {
      return true;
    }
  });
});
  • after:
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: What about other customizations?

If your customization cannot be achieved using the new API we’ve introduced, please let us know by creating a new dev topic to discuss.

:sparkles: I am a plugin/theme author. How do I update a theme/plugin to support both old and new post menu during the transition?

We’ve used the pattern below to support both the old and new version of the post menu in our plugins:

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

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

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

function customizeWidgetPostMenu(api) {
  // old "widget" code customization here
  ...
}

export default {
  name: "my-plugin",

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

:star: More examples

You can check, our official plugins for examples on how to use the new API:

15 „Gefällt mir“

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.

2 „Gefällt mir“

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.

3 „Gefällt mir“

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!

2 „Gefällt mir“

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)

1 „Gefällt mir“

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:

4 „Gefällt mir“

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

1 „Gefällt mir“

@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
  ...
}
1 „Gefällt mir“

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

1 „Gefällt mir“

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

4 „Gefällt mir“

2 Beiträge wurden in ein neues Thema aufgeteilt: Können wir .gjs für Routen-Vorlagen verwenden?

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

6 „Gefällt mir“