Prossimi cambiamenti nel menu dei post - Come preparare temi e plugin

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 Mi Piace

Abbiamo impostato il menu Glimmer Post su enabled per impostazione predefinita.

Una volta aggiornata la tua istanza Discourse, ciò causerà il mancato applicamento delle personalizzazioni esistenti che non sono state aggiornate alla nuova API.

Per ora, gli amministratori possono ancora reimpostare l’impostazione su disabled mentre aggiornano le restanti personalizzazioni.

Il codice legacy verrà rimosso all’inizio del secondo trimestre.

2 Mi Piace

Apprezzo la flessibilità di avere a disposizione l’intera API dei componenti. Mi piace la sintassi dei componenti Glimmer nel complesso e capisco perché possa offrire vantaggi nel ridurre la complessità del codebase.

Tuttavia, per casi d’uso basilari (voglio aggiungere un pulsante e dargli un’icona), i vecchi metodi API erano oggettivamente più concisi e facili da capire (meno importazioni, meno operazioni, meno impronta dell’API esposta). C’è qualche motivo per cui i vecchi metodi API non potrebbero continuare a essere supportati? Immagino che se li utilizzaste come funzioni di convenienza ed eseguiste l’implementazione sottostante utilizzando il vostro nuovo componente Glimmer, il metodo API potrebbe anche eseguire il controllo di compatibilità delle versioni.

Ciò sarebbe molto meno dirompente per chiunque utilizzi questi metodi e creerebbe meno esplosione di codice di logica condizionale all’interno dell’ecosistema dei plugin.

La mia principale lamentela riguardo ai widget esistenti è la loro mancanza di documentazione. Questo post, che annuncia la loro rimozione, è uno dei documenti più chiari che abbia visto riguardo all’esistenza di questi particolari metodi e a come utilizzarli. Sono arrivato qui, infatti, cercando di capire come utilizzare la vecchia API.

Mi piace che i transformer siano registrati in un unico posto, tramite letterali di stringa. Penso che questo renda il lavoro di documentazione (e di sviluppo dei plugin) molto più semplice.

Con i widget, sembrano tutti essere registrati tramite il metodo createWidget, che poi chiama createWidgetFrom. Il problema che vedo in questo è che il _registro è una variabile globale a livello di file protetta da un’API, e l’API non consente alcuna iterazione. Se potessimo semplicemente ottenere una funzione di iterazione sul registro dei widget, potremmo scoprire in tempo reale i widget attualmente registrati. Dovrebbe essere documentato “esegui questa riga di javascript nella console del tuo browser per chiamare l’API ed elencare il registro”. Quindi potremmo ottenere un’utilità molto simile a quella che fornisce il registro dei transformer.

Un’altra cosa che aiuterebbe nello sviluppo dei plugin è vedere un attributo su qualsiasi elemento DOM radice renderizzato da un componente/widget che ti dica di quale componente/widget si tratta. Come “data-widget=foo”. Questa potrebbe essere una funzionalità di debug, o potrebbe essere semplicemente abilitata per impostazione predefinita. È OSS, quindi non è come se si stesse ottenendo sicurezza tramite l’oscurità.

Celebro il passaggio ai componenti Glimmer. Ma questo richiederà tempo, e nel frattempo ci sono molti widget con cui le persone devono lavorare. Quindi penso che migliorare la visibilità dei widget, come menzionato sopra, probabilmente renderà il periodo di transizione più facile per tutti.

Per quanto riguarda quei metodi API… Sembra che qualcuno si sia preso la briga di aggiungere commenti dettagliati all’API Javascript, ma non è mai stato generato un sito di documentazione per essa. Perché no?

Sarei felice di inviare una pull request per l’iterazione attraverso il registro dei widget, se ciò fosse accettabile.

3 Mi Piace

Inoltre, se voglio implementare solo la nuova funzionalità, a quale versione dell’API di Discourse dovrei fissare la compatibilità del mio plugin? Tu usi withPluginApi("1.34.0", ...) in tutti i tuoi esempi. Penso che questa sia una versione più vecchia e non rappresentativa di quando questa modifica è stata apportata? Ma per favore chiarisci. Grazie!

2 Mi Piace

È la versione corretta. Puoi consultare il changelog qui:

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

Inoltre, questa funzionalità può essere d’aiuto: Pinning plugin and theme versions for older Discourse installs (.discourse-compatibility)

1 Mi Piace

La rimozione dei widget è in corso da un paio d’anni e speriamo di finalizzarla nei prossimi mesi. Quindi non penso che apporteremo modifiche al sistema sottostante prima di allora.

Hai provato l’Ember Inspector? Penso che dovrebbe risolvere il problema che descrivi, e mostrerà anche i componenti Ember che attualmente non stanno renderizzando alcun elemento DOM.

Molto recentemente, questo numero di versione non è più richiesto. Puoi fare

export default apiInitializer((api) => {

o

withPluginApi((api) => {

Aggiorneremo presto la documentazione con questa modifica. La preferenza moderna per la gestione dell’intercompatibilità delle versioni è il file .discourse-compatibility menzionato da @Arkshine:

4 Mi Piace

Questo è davvero fantastico! Ogni volta dimentico di aggiornare quel numero :rofl:.

1 Mi Piace

@david @Arkshine wow, ottimi post, davvero utili!

Un’altra domanda: riguardo al “contesto” che viene condiviso quando si chiama api.registerValueTransformer, come posso scoprire quale contesto mi verrà passato? Suppongo di poter semplicemente fare un console.log del contesto, ma sarebbe bello saperlo in anticipo cosa è disponibile.

Per il plugin che sto scrivendo ora sto dando privilegi di moderazione speciali all’autore di un argomento. Per fare ciò ho bisogno di conoscere l’“autore dell’argomento corrente”, l’“utente corrente” e l’“appartenenza al gruppo dell’utente connesso”.

Forse quell’esempio specifico aiuta a dare contesto alla mia domanda.

MODIFICA:

Il mio codice finale è questo, per chiunque altro abbia interessi simili:

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

// se l'utente corrente è l'autore del post, o fa parte di un gruppo privilegiato
if (post?.topic?.details?.created_by?.id === currentUser.id || validGroups.length > 0) {
  // concedi loro l'accesso alla funzionalità
  ...
}
1 Mi Piace

Temo che al momento non abbiamo alcuna documentazione centrale per questi. Alcune aree dell’app hanno la loro documentazione specifica ad esempio, l’elenco degli argomenti, che elenca gli argomenti del contesto. Ma altrimenti, la cosa migliore è cercare la chiamata applyTransformer nel codebase principale, o usare console.log.

La documentazione generale sui transformer si trova qui: Using Transformers to customize client-side values and behavior

1 Mi Piace

La ricerca di “applyTransformer” restituisce 0 risultati. Sto cercando nel posto sbagliato?

Trovo che la ricerca di “api.registerValueTransformer” restituisca alcuni esempi utili. Ma ovviamente gli esempi non forniscono una documentazione completa del contesto restituito, mostrano solo quelli che sono stati utili per quel particolare esempio.

Dal console.log di context nel mio esempio specifico, vedo che post viene restituito ma user no. Quindi, come posso accedere ad altro stato dell’applicazione che non è contenuto nel contesto?

Capisco che in precedenza si potesse chiamare helper.getModel() o helper.currentUser in un contesto api.decorateWidget. Presumo ci sia un metodo attuale per ottenere risultati simili.

Grazie per tutto l’aiuto.

Oh, penso di aver risposto alla mia domanda. Questo esempio mostra l’uso di api.getCurrentUser(). Quindi essenzialmente quella parte dell’API non è cambiata ed è ancora compatibile con il paradigma glimmer.

Credo intendesse applyValueTransformer o applyBehaviorTransformer. Tali funzioni si possono trovare nel seguente file: discourse/app/assets/javascripts/discourse/app/lib/transformer.js at main · discourse/discourse · GitHub

4 Mi Piace

2 post sono stati divisi in un nuovo argomento: Possiamo usare .gjs per i template di route?

Il codice del menu post legacy è stato rimosso. Grazie a tutti coloro che hanno lavorato all’aggiornamento dei loro temi e plugin :rocket:

6 Mi Piace