Próximos cambios en el menú de publicaciones: Cómo preparar temas y 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 Me gusta

Hemos cambiado el menú Publicación de Glimmer a habilitado por defecto.

Una vez que tu instancia de Discourse se actualice, esto provocará que las personalizaciones existentes que no se actualizaron a la nueva API no se apliquen.

Por ahora, los administradores aún pueden cambiar la configuración a deshabilitado mientras actualizan las personalizaciones restantes.

El código heredado se eliminará a principios del segundo trimestre.

2 Me gusta

Aprecio la flexibilidad de tener disponible la API completa del componente. Me gusta la sintaxis de los componentes de Glimmer en general, y veo por qué puede tener beneficios para reducir la complejidad dentro de la base de código.

Sin embargo, para casos de uso básicos (quiero agregar un botón y darle un ícono), los métodos de la API antigua eran objetivamente más concisos y fáciles de entender (menos importaciones, menos operaciones, menos huella de API expuesta). ¿Hay alguna razón por la que los métodos de la API antigua no pudieran tener soporte continuo? Me imagino que si los usara como funciones de conveniencia y realizara la implementación subyacente utilizando su nuevo componente Glimmer, entonces el método de la API también podría realizar la verificación de compatibilidad de versiones.

Esto sería mucho menos disruptivo para cualquiera que use estos métodos y crearía menos explosión de código de lógica condicional dentro del ecosistema de complementos.

Mi principal queja sobre los widgets existentes es su falta de documentación. Esta publicación, que anuncia su eliminación, es uno de los documentos más claros que he visto de que existen estos métodos particulares y cómo usarlos. De hecho, vine aquí tratando de descubrir cómo usar la API antigua.

Me gusta que los transformadores se registren en un solo lugar, mediante literales de cadena. Creo que eso facilita mucho el trabajo de documentación (y desarrollo de complementos).

Con los widgets, todos parecen registrarse mediante el método createWidget, que luego llama a createWidgetFrom. El problema que veo con esto es que el _registro es una variable global a nivel de archivo protegida por una API, y la API no permite ninguna iteración. Si pudiéramos obtener una función de iteración en el registro de widgets, podríamos descubrir en tiempo real los widgets registrados actualmente. Debería documentarse “ejecute esta línea de javascript en la consola de su navegador para llamar a la API y listar el registro”. Luego podríamos obtener una utilidad muy similar a la que proporciona el registro de transformadores.

Otra cosa que ayudaría en el desarrollo de complementos es ver un atributo en cualquier elemento DOM raíz renderizado por un componente/widget que le indique qué componente/widget está viendo. Como “data-widget=foo”. Esta podría ser una característica de depuración, o podría estar habilitada por defecto. Es OSS, por lo que no es como si estuviera logrando seguridad a través de la ofuscación.

Celebro el cambio hacia los componentes de Glimmer. Pero esto llevará tiempo, y hay muchos widgets con los que la gente necesita trabajar mientras tanto. Por lo tanto, creo que mejorar la visibilidad de los widgets, como se mencionó anteriormente, probablemente haría que el período de transición fuera más fácil para todos.

En cuanto a esos métodos de API… Parece que alguien se tomó la molestia de agregar comentarios detallados a la API de javascript, pero nunca se generó un sitio de documentación para ella. ¿Por qué no?

Estaría feliz de enviar una solicitud de extracción para iterar a través del registro de widgets, si eso es aceptable.

3 Me gusta

Además, si solo quiero implementar la nueva funcionalidad, ¿a qué versión de la API de Discourse debería fijar la compatibilidad de mi plugin? Utilizan withPluginApi("1.34.0", ...) en todos sus ejemplos. Creo que esta es una versión más antigua y no representa cuándo se hizo este cambio, ¿verdad? Pero por favor aclárenlo. ¡Gracias!

2 Me gusta

Es la versión correcta. Puedes consultar el registro de cambios aquí:

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

Además, esta función puede ayudar: Pinning plugin and theme versions for older Discourse installs (.discourse-compatibility)

1 me gusta

La eliminación de los widgets está en marcha desde hace un par de años y esperamos finalizarla en los próximos meses. Así que no creo que vayamos a hacer ningún cambio en el sistema subyacente antes de entonces.

¿Has probado el Inspector de Ember? Creo que debería resolver el problema que describes, e incluso mostrará componentes de Ember que actualmente no están renderizando ningún elemento DOM.

Muy recientemente, este número de versión ya no es obligatorio. Puedes hacer

export default apiInitializer((api) => {

o

withPluginApi((api) => {

Actualizaremos la documentación con ese cambio pronto. La preferencia moderna para gestionar la intercompatibilidad de versiones es el archivo .discourse-compatibility, que mencionó @Arkshine:

4 Me gusta

¡Esto es realmente genial! Cada vez que olvido actualizar ese número :rofl:.

1 me gusta

@david @Arkshine ¡vaya, grandes publicaciones, muy útiles!

Otra pregunta: con respecto al “contexto” que se comparte al llamar a api.registerValueTransformer, ¿cómo puedo saber qué contexto se me pasará? Supongo que puedo simplemente registrar el contexto en la consola, pero sería bueno saber de antemano qué hay disponible.

Para el plugin que estoy escribiendo ahora, estoy otorgando privilegios especiales de moderación al autor de un tema. Para hacer esto, necesito saber el “autor del tema actual”, el “usuario actual” y la “membresía del grupo del usuario conectado”.

Quizás ese ejemplo específico ayude a dar contexto a mi pregunta.

EDITAR:

Mi código final se ve así, para cualquier otra persona con intereses similares:

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

// si el usuario actual es el autor de la publicación, o está en un grupo privilegiado
if (post?.topic?.details?.created_by?.id === currentUser.id || validGroups.length > 0) {
  // dales acceso a la función
  ...
}
1 me gusta

Me temo que actualmente no tenemos ninguna documentación central para estos. Algunas áreas de la aplicación tienen su propia documentación específica por ejemplo, la lista de temas, que enumera los argumentos de contexto. Pero, por lo demás, la mejor opción es buscar la llamada a applyTransformer en el código base principal, o usar console.log.

La documentación general sobre transformadores se puede encontrar aquí: Using Transformers to customize client-side values and behavior

1 me gusta

Buscar “applyTransformer” devuelve 0 resultados. ¿Estoy buscando en el lugar equivocado?

Encuentro que buscar “api.registerValueTransformer” devuelve algunos ejemplos útiles. Pero, por supuesto, los ejemplos no proporcionan documentación completa del contexto que se devuelve; solo muestran los que fueron útiles para ese ejemplo en particular.

Desde console.log de context en mi ejemplo específico, veo que se devuelve post pero no user. Entonces, ¿cómo puedo acceder a otro estado de la aplicación que no está contenido dentro del contexto?

Entiendo que anteriormente se podría haber llamado a helper.getModel() o helper.currentUser dentro de un contexto api.decorateWidget. Supongo que existe algún método actual para obtener resultados similares.

Gracias por toda la ayuda aquí.

Oh, creo que respondí mi propia pregunta. Este ejemplo muestra el uso de api.getCurrentUser(). Así que, esencialmente, esa parte de la API no ha cambiado y sigue siendo compatible con el paradigma de Glimmer.

Creo que se refería a applyValueTransformer o applyBehaviorTransformer. Dichas funciones se pueden encontrar en el siguiente archivo: discourse/app/assets/javascripts/discourse/app/lib/transformer.js at main · discourse/discourse · GitHub

4 Me gusta

2 publicaciones se dividieron en un nuevo tema: ¿Podemos usar .gjs para las plantillas de ruta?

El código del menú de publicaciones heredado ha sido eliminado. Gracias a todos los que trabajaron en la actualización de sus temas y plugins :rocket:

6 Me gusta