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

Come parte del nostro continuo sforzo per migliorare il codice di Discourse, stiamo rimuovendo l’uso del sistema di rendering “widget” legacy e lo stiamo sostituendo con i componenti Glimmer.

Recentemente, abbiamo modernizzato il menu dei post, ed è ora disponibile in Discourse dietro l’impostazione glimmer_post_menu_mode.

Questa impostazione accetta tre valori possibili:

  • disabled: utilizza il sistema legacy “widget”
  • auto: rileverà la compatibilità dei tuoi plugin e temi attuali. Se alcuni non sono compatibili, userà il sistema legacy; altrimenti userà il nuovo menu.
  • enabled: userà il nuovo menu. Se hai plugin o temi incompatibili, il tuo sito potrebbe non funzionare correttamente.

Abbiamo già aggiornato i nostri plugin ufficiali per renderli compatibili con il nuovo menu, ma se hai ancora plugin, temi o componenti di tema di terze parti incompatibili con il nuovo menu, sarà necessario aggiornarli.

Verranno visualizzati avvisi nella console del browser che identificano la fonte dell’incompatibilità.

:timer_clock: Cronologia di implementazione

Queste sono stime approssimative soggette a modifiche

Q4 2024:

  • :white_check_mark: implementazione core completata
  • :white_check_mark: plugin ufficiali aggiornati
  • :white_check_mark: abilitato su Meta
  • :white_check_mark: glimmer_post_menu_mode impostato su auto di default; messaggi di deprecazione nella console abilitati
  • :white_check_mark: pubblicati consigli per l’aggiornamento

Q1 2025:

  • :white_check_mark: plugin e temi di terze parti dovrebbero essere aggiornati
  • :white_check_mark: iniziano i messaggi di deprecazione, attivando un banner di avviso per l’amministratore per eventuali problemi residui
  • :white_check_mark: abilitato di default il nuovo menu dei post

Q2 2025

  • :white_check_mark: 1° aprile - rimozione dell’impostazione del feature flag e del codice legacy

:eyes: Cosa significa per me?

Se il tuo plugin o tema utilizza le API “widget” per personalizzare il menu dei post, dovrà essere aggiornato per essere compatibile con la nuova versione.

:person_tipping_hand: Come posso provare il nuovo menu dei post?

Nell’ultima versione di Discourse, il nuovo menu dei post sarà abilitato se non hai plugin o temi incompatibili.

Se hai installato estensioni incompatibili, come amministratore puoi comunque cambiare l’impostazione su enabled per forzare l’uso del nuovo menu. Usalo con cautela, poiché il tuo sito potrebbe non funzionare correttamente a seconda delle personalizzazioni installate.

Nel raro caso in cui questo sistema automatico non funzioni come previsto, puoi temporaneamente sovrascrivere questo “feature flag automatico” utilizzando l’impostazione sopra. Se devi farlo, faccelo sapere in questo argomento.

:technologist: Devo aggiornare il mio plugin o tema?

Dovrai aggiornare i tuoi plugin o temi se eseguono una delle personalizzazioni seguenti:

  • Utilizzano decorateWidget, changeWidgetSetting, reopenWidget o attachWidgetAction su questi widget:

    • post-menu
    • post-user-tip-shim
    • small-user-list
  • Utilizzano uno dei seguenti metodi API:

    • addPostMenuButton
    • removePostMenuButton
    • replacePostMenuButton

:bulb: Se hai estensioni che eseguono una delle personalizzazioni sopra, verrà visualizzato un avviso nella console che identifica il plugin o il componente che deve essere aggiornato quando accedi a una pagina di argomento.

L’ID di deprecazione è: discourse.post-menu-widget-overrides

:warning: Se utilizzi più di un tema nella tua istanza, assicurati di controllarli tutti, poiché gli avvisi verranno visualizzati solo per i plugin attivi e i temi e i componenti di tema attualmente in uso.

Quali sono le sostituzioni?

Abbiamo introdotto il value transformer post-menu-buttons come nuova API per personalizzare il menu dei post.

Il value transformer fornisce un oggetto DAG che consente di aggiungere, sostituire, rimuovere o riordinare i pulsanti. Fornisce anche informazioni sul contesto, come il post associato al menu, lo stato del post visualizzato e le chiavi dei pulsanti per facilitare il posizionamento degli elementi.

Le API DAG si aspettano di ricevere componenti Ember se l’API richiede una nuova definizione di pulsante, come .add e .replace.

Ogni personalizzazione è diversa, ma ecco alcune indicazioni per i casi d’uso più comuni:

addPostMenuButton

Prima:

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

Dopo:

Gli esempi seguenti utilizzano il Template Tag Format (gjs) di 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 {
  // indica se il pulsante verrà visualizzato immediatamente o nascosto dietro il pulsante "mostra altro"
  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, // chiave del primo pulsante
        secondLastHiddenButtonKey, // chiave del penultimo pulsante nascosto
        lastHiddenButtonKey, // chiave dell'ultimo pulsante nascosto
      },
    }) => {
        dag.add(
          "solved",
          SolvedAcceptAnswerButton,
          post.topic_accepted_answer
            ? {
                before: lastHiddenButtonKey,
                after: secondLastHiddenButtonKey,
              }
            : {
                before: [
                  "assign", // pulsante aggiunto dal plugin assign
                  firstButtonKey,
                ],
              }
        );
    }
  );
});

:bulb: Stile dei tuoi pulsanti

Si consiglia di includere ...attributes come mostrato nell’esempio sopra nel tuo componente.

Se combinato con l’uso dei componenti DButton o DMenu, questo gestirà le classi di base e garantirà che il tuo pulsante segua il formato degli altri pulsanti nel menu dei post.

È possibile specificare ulteriori formattazioni utilizzando le tue classi personalizzate.

replacePostMenuButton

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

...

withPluginApi("1.34.0", (api) => {
  api.registerValueTransformer(
    "post-menu-buttons",
    ({ value: dag, context: { buttonKeys } }) => {
      // ReactionsActionButton è il nuovo componente del pulsante
      dag.replace(buttonKeys.LIKE, ReactionsActionButton);
    }
  );
});

removePostMenuButton

  • prima:
withPluginApi("1.34.0", (api) => {
  api.removePostMenuButton('like', (attrs, state, siteSettings, settings, currentUser) => {
    if (attrs.post_number === 1) {
      return true;
    }
  });
});
  • dopo:
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: E le altre personalizzazioni?

Se la tua personalizzazione non può essere realizzata utilizzando la nuova API che abbiamo introdotto, faccelo sapere creando un nuovo argomento di sviluppo per discuterne.

:sparkles: Sono autore di plugin/temi. Come posso aggiornare un tema/plugin per supportare sia il vecchio che il nuovo menu dei post durante la transizione?

Abbiamo utilizzato il modello sottostante per supportare sia la vecchia che la nuova versione del menu dei post nei nostri plugin:

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

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

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

function customizeWidgetPostMenu(api) {
  // vecchia personalizzazione del codice "widget" qui
  ...
}

export default {
  name: "my-plugin",

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

:star: Altri esempi

Puoi consultare i nostri plugin ufficiali per esempi su come utilizzare la nuova API:

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.

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.

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!

È 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)

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:

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

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

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

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

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