Changements de menu des publications à venir - Comment préparer thèmes et plugins

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 « J'aime »

Nous avons activé le menu Glimmer Post par défaut.

Une fois votre instance Discourse mise à niveau, cela entraînera l’application des personnalisations existantes qui n’ont pas été mises à jour vers la nouvelle API.

Pour l’instant, les administrateurs peuvent toujours désactiver le paramètre tout en mettant à jour les personnalisations restantes.

Le code hérité sera supprimé au début du deuxième trimestre.

2 « J'aime »

J’apprécie la flexibilité d’avoir l’API complète des composants disponible. J’aime la syntaxe des composants Glimmer dans l’ensemble, et je vois pourquoi cela pourrait présenter des avantages pour réduire la complexité du code.

Cependant, pour des cas d’utilisation simples (je veux ajouter un bouton et lui donner une icône), les anciennes méthodes d’API étaient objectivement plus concises et faciles à comprendre (moins d’importations, moins d’opérations, moins d’empreinte API exposée). Y a-t-il une raison pour laquelle les anciennes méthodes d’API ne pourraient pas continuer à être prises en charge ? J’imagine que si vous les utilisiez comme fonctions de commodité et que vous effectuiez l’implémentation sous-jacente à l’aide de votre nouveau composant Glimmer, la méthode d’API pourrait également effectuer la vérification de compatibilité des versions.

Cela serait beaucoup moins perturbateur pour quiconque utilise ces méthodes et créerait moins d’explosion de code de logique conditionnelle au sein de l’écosystème des plugins.

Ma principale plainte concernant les widgets existants est leur manque de documentation. Ce post, annonçant leur suppression, est l’un des documents les plus clairs que j’ai vus indiquant que ces méthodes particulières existent et comment les utiliser. Je suis venu ici en fait, en essayant de comprendre comment utiliser l’ancienne API.

J’aime que les transformateurs soient enregistrés en un seul endroit, par des littéraux de chaîne. Je pense que cela facilite grandement la documentation (et le développement de plugins).

Avec les widgets, ils semblent tous être enregistrés par la méthode createWidget, qui appelle ensuite createWidgetFrom (discourse/app/assets/javascripts/discourse/app/widgets/widget.js at a86590ffd6069a939d58002050ef0e9b92889a2e · discourse/discourse · GitHub). Le problème que je vois avec cela est que le _registry est une variable globale de portée de fichier protégée par une API, et l’API ne permet aucune itération. Si nous pouvions simplement obtenir une fonction d’itération sur le registre des widgets, nous pourrions découvrir en temps réel les widgets actuellement enregistrés. Il devrait être documenté “exécutez cette ligne de javascript dans la console de votre navigateur pour appeler l’API et lister le registre”. Ensuite, nous pourrions obtenir une utilité très similaire à ce que fournit le registre des transformateurs.

Une autre chose qui aiderait dans le développement de plugins est de voir un attribut sur tout élément DOM racine rendu par un composant/widget qui vous indique quel composant/widget vous regardez. Comme “data-widget=foo”. Cela pourrait être une fonctionnalité de débogage, ou cela pourrait simplement être activé par défaut. C’est de l’OSS, donc ce n’est pas comme si vous obteniez la sécurité par l’obscurité.

Je célèbre le passage aux composants Glimmer. Mais cela prendra du temps, et il y a beaucoup de widgets avec lesquels les gens doivent travailler entre-temps. Je pense donc qu’améliorer la visibilité des widgets, comme mentionné ci-dessus, rendrait probablement la période de transition plus facile pour tout le monde.

Quant à ces méthodes d’API… Il semble que quelqu’un ait pris la peine d’ajouter des commentaires détaillés à l’API javascript, mais aucun site de documentation n’a jamais été généré pour cela. Pourquoi pas ?

Je serais heureux de soumettre une pull request pour itérer sur le registre des widgets, si cela est acceptable.

3 « J'aime »

De plus, si je souhaite uniquement implémenter la nouvelle fonctionnalité, à quelle version de l’API Discourse dois-je fixer la compatibilité de mon plugin ? Vous utilisez withPluginApi("1.34.0", ...) dans tous vos exemples. Je pense que c’est une version plus ancienne et qu’elle ne représente pas le moment où ce changement a été effectué. Mais veuillez clarifier. Merci !

2 « J'aime »

C’est la bonne version. Vous pouvez consulter le journal des modifications ici :

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

De plus, cette fonctionnalité peut vous aider : Pinning plugin and theme versions for older Discourse installs (.discourse-compatibility)

1 « J'aime »

La suppression des widgets est en cours depuis quelques années, et nous espérons la finaliser dans les prochains mois. Je ne pense donc pas que nous apporterons des modifications au système sous-jacent avant cela.

Avez-vous essayé l’inspecteur Ember? Je pense qu’il devrait résoudre le problème que vous décrivez, et il montrera même les composants Ember qui ne rendent actuellement aucun élément DOM.

Depuis très récemment, ce numéro de version n’est plus requis. Vous pouvez faire

export default apiInitializer((api) => {

ou

withPluginApi((api) => {

Nous mettrons bientôt à jour la documentation avec ce changement. La préférence moderne pour gérer l’intercompatibilité des versions est le fichier .discourse-compatibility mentionné par @Arkshine :

4 « J'aime »

C’est vraiment sympa ! Chaque fois que j’oublie de mettre à jour ce numéro :rofl:.

1 « J'aime »

@david @Arkshine wow, super publications, très utiles !

Autre question - concernant le « contexte » partagé lors de l’appel à api.registerValueTransformer, comment puis-je savoir quel contexte me sera transmis ? Je suppose que je peux simplement faire un console.log du contexte, mais ce serait bien de savoir à l’avance ce qui est disponible.

Pour le plugin que j’écris actuellement, j’accorde des privilèges de modération spéciaux à l’auteur d’un sujet. Pour ce faire, j’ai besoin de connaître « l’auteur du sujet actuel », « l’utilisateur actuel » et « l’appartenance aux groupes de l’utilisateur connecté ».

Peut-être que cet exemple spécifique aide à contextualiser ma question.

EDIT :
Mon code final ressemble à ceci, pour quiconque aurait des intérêts similaires :

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

// si l'utilisateur actuel est l'auteur du post, ou s'il fait partie d'un groupe privilégié
if (post?.topic?.details?.created_by?.id === currentUser.id || validGroups.length > 0) {
  // donnez-lui accès à la fonctionnalité
  ...
}
1 « J'aime »

Je crains que nous n’ayons actuellement aucune documentation centrale pour ceux-ci. Certaines zones de l’application ont leur propre documentation spécifique par exemple, la liste des sujets, qui répertorie les arguments de contexte. Mais sinon, le mieux est de rechercher l’appel applyTransformer dans le code source du noyau, ou d’utiliser console.log.

La documentation générale sur les transformateurs se trouve ici : Using Transformers to customize client-side values and behavior

1 « J'aime »

La recherche de « applyTransformer » ne renvoie aucun résultat. Est-ce que je cherche au mauvais endroit ?

Je constate que la recherche de « api.registerValueTransformer » renvoie des exemples utiles. Mais bien sûr, les exemples ne fournissent pas une documentation complète du contexte renvoyé - ils ne montrent que ceux qui ont été utiles pour cet exemple particulier.

D’après console.log de context dans mon exemple spécifique, je vois que post est renvoyé mais user ne l’est pas. Alors, comment puis-je accéder à d’autres états de l’application qui ne sont pas contenus dans le contexte ?

Je comprends qu’auparavant, on pouvait appeler helper.getModel() ou helper.currentUser dans un contexte api.decorateWidget. Je suppose qu’il existe une méthode actuelle pour obtenir des résultats similaires.

Merci pour toute l’aide apportée ici.

Oh, je pense que j’ai répondu à ma propre question. Cet exemple montre l’utilisation de api.getCurrentUser(). Donc, essentiellement, cette partie de l’API n’a pas changé et est toujours compatible avec le paradigme glimmer.

Je crois qu’il voulait dire applyValueTransformer ou applyBehaviorTransformer. Vous pouvez trouver de telles fonctions dans le fichier suivant : discourse/app/assets/javascripts/discourse/app/lib/transformer.js at main · discourse/discourse · GitHub

4 « J'aime »

2 messages ont été divisées dans un nouveau sujet : Pouvons-nous utiliser .gjs pour les modèles de route ?

Le code du menu de publication hérité a maintenant été supprimé. Merci à tous ceux qui ont travaillé à la mise à jour de leurs thèmes et plugins :rocket:

6 « J'aime »