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

Dans le cadre de notre effort continu pour améliorer la base de code de Discourse, nous supprimons l’utilisation de l’ancien système de rendu « widget » et le remplaçons par des composants Glimmer.

Récemment, nous avons modernisé le menu des publications, qui est désormais disponible dans Discourse via le paramètre glimmer_post_menu_mode.

Ce paramètre accepte trois valeurs possibles :

  • disabled : utilise l’ancien système « widget »
  • auto : détecte la compatibilité de vos plugins et thèmes actuels. Si l’un d’eux est incompatible, il utilisera l’ancien système ; sinon, il utilisera le nouveau menu.
  • enabled : utilise le nouveau menu. Si vous avez un plugin ou un thème incompatible, votre site peut être endommagé.

Nous avons déjà mis à jour nos plugins officiels pour qu’ils soient compatibles avec le nouveau menu, mais si vous avez toujours des plugins, thèmes ou composants de thème tiers incompatibles avec le nouveau menu, leur mise à jour sera nécessaire.

Des avertissements seront affichés dans la console du navigateur pour identifier la source de l’incompatibilité.

:timer_clock: Calendrier de déploiement

Ces estimations sont approximatives et sujettes à modification

T4 2024 :

  • :white_check_mark: implémentation principale terminée
  • :white_check_mark: plugins officiels mis à jour
  • :white_check_mark: activé sur Meta
  • :white_check_mark: glimmer_post_menu_mode défini par défaut sur auto ; messages de dépréciation dans la console activés
  • :white_check_mark: conseils de mise à niveau publiés

T1 2025 :

  • :white_check_mark: les plugins et thèmes tiers doivent être mis à jour
  • :white_check_mark: les messages de dépréciation commencent, déclenchant une bannière d’avertissement pour l’administrateur pour tout problème restant
  • :white_check_mark: le nouveau menu des publications activé par défaut

T2 2025

  • :white_check_mark: 1er avril - suppression du paramètre de feature flag et du code legacy

:eyes: Qu’est-ce que cela signifie pour moi ?

Si votre plugin ou thème utilise des API « widget » pour personnaliser le menu des publications, celles-ci devront être mises à jour pour être compatibles avec la nouvelle version.

:person_tipping_hand: Comment essayer le nouveau menu des publications ?

Dans la dernière version de Discourse, le nouveau menu des publications sera activé si vous n’avez aucun plugin ou thème incompatible.

Si vous avez des extensions incompatibles installées, en tant qu’administrateur, vous pouvez toujours changer le paramètre sur enabled pour forcer l’utilisation du nouveau menu. Utilisez cette option avec prudence car votre site peut être endommagé en fonction des personnalisations installées.

Dans le cas peu probable où ce système automatique ne fonctionne pas comme prévu, vous pouvez temporairement outrepasser ce « feature flag automatique » en utilisant le paramètre ci-dessus. Si vous devez le faire, veuillez nous en informer dans ce sujet.

:technologist: Dois-je mettre à jour mon plugin ou mon thème ?

Vous devrez mettre à jour vos plugins ou thèmes s’ils effectuent l’une des personnalisations suivantes :

  • Utiliser decorateWidget, changeWidgetSetting, reopenWidget ou attachWidgetAction sur ces widgets :

    • post-menu
    • post-user-tip-shim
    • small-user-list
  • Utiliser l’une des méthodes API suivantes :

    • addPostMenuButton
    • removePostMenuButton
    • replacePostMenuButton

:bulb: Si vous avez des extensions qui effectuent l’une des personnalisations ci-dessus, un avertissement sera affiché dans la console pour identifier le plugin ou le composant qui doit être mis à jour lorsque vous accédez à une page de sujet.

L’ID de dépréciation est : discourse.post-menu-widget-overrides

:warning: Si vous utilisez plusieurs thèmes sur votre instance, assurez-vous de vérifier tous car les avertissements ne seront affichés que pour les plugins actifs et les thèmes et composants de thème actuellement utilisés.

Quelles sont les alternatives ?

Nous avons introduit le transformateur de valeur post-menu-buttons comme nouvelle API pour personnaliser le menu des publications.

Le transformateur de valeur fournit un objet DAG qui permet d’ajouter, remplacer, supprimer ou réorganiser les boutons. Il fournit également des informations contextuelles telles que la publication associée au menu, l’état de la publication affichée et les clés de bouton pour faciliter le placement des éléments.

Les API DAG s’attendent à recevoir des composants Ember si l’API nécessite une nouvelle définition de bouton comme .add et .replace.

Chaque personnalisation est différente, mais voici quelques conseils pour les cas d’utilisation les plus courants :

addPostMenuButton

Avant :

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

Après :

Les exemples ci-dessous utilisent le format de balise de modèle d’Ember (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 {
  // indique si le bouton sera affiché immédiatement ou caché derrière le bouton "voir plus"
  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, // clé du premier bouton
        secondLastHiddenButtonKey, // clé du deuxième dernier bouton caché
        lastHiddenButtonKey, // clé du dernier bouton caché
      },
    }) => {
        dag.add(
          "solved",
          SolvedAcceptAnswerButton,
          post.topic_accepted_answer
            ? {
                before: lastHiddenButtonKey,
                after: secondLastHiddenButtonKey,
              }
            : {
                before: [
                  "assign", // bouton ajouté par le plugin assign
                  firstButtonKey,
                ],
              }
        );
    }
  );
});

:bulb: Style de vos boutons

Il est recommandé d’inclure ...attributes comme montré dans l’exemple ci-dessus dans votre composant.

Combiné à l’utilisation des composants DButton ou DMenu, cela gérera les classes de base et assurera que votre bouton suit le formatage des autres boutons du menu des publications.

Un formatage supplémentaire peut être spécifié en utilisant vos classes personnalisées.

replacePostMenuButton

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

...

withPluginApi("1.34.0", (api) => {
  api.registerValueTransformer(
    "post-menu-buttons",
    ({ value: dag, context: { buttonKeys } }) => {
      // ReactionsActionButton est le nouveau composant de bouton
      dag.replace(buttonKeys.LIKE, ReactionsActionButton);
    }
  );
});

removePostMenuButton

  • Avant :
withPluginApi("1.34.0", (api) => {
  api.removePostMenuButton('like', (attrs, state, siteSettings, settings, currentUser) => {
    if (attrs.post_number === 1) {
      return true;
    }
  });
});
  • Après :
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: Et pour les autres personnalisations ?

Si votre personnalisation ne peut pas être réalisée avec la nouvelle API que nous avons introduite, veuillez nous en informer en créant un nouveau sujet de développement pour en discuter.

:sparkles: Je suis l’auteur d’un plugin/thème. Comment mettre à jour un thème/plugin pour prendre en charge à la fois l’ancien et le nouveau menu des publications pendant la transition ?

Nous avons utilisé le modèle ci-dessous pour prendre en charge à la fois l’ancienne et la nouvelle version du menu des publications dans nos plugins :

function customizePostMenu(api) {
  const transformerRegistered = api.registerValueTransformer(
    "post-menu-buttons",
    ({ value: dag, context }) => {
      // nouvelles personnalisations du menu des publications
      ...
    }
  );

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

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

function customizeWidgetPostMenu(api) {
  // ancienne personnalisation du code "widget" ici
  ...
}

export default {
  name: "my-plugin",

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

:star: Plus d’exemples

Vous pouvez consulter nos plugins officiels pour des exemples sur l’utilisation de la nouvelle API :

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.

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.

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 !

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)

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 :

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

@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é
  ...
}

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

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

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: