Próximos cambios en el menú de publicaciones: Cómo preparar temas y plugins

Como parte de nuestro esfuerzo continuo por mejorar la base de código de Discourse, estamos eliminando el uso del sistema de renderizado heredado de “widget” y reemplazándolo con componentes Glimmer.

Recientemente, modernizamos el menú de publicaciones, y ahora está disponible en Discourse mediante la configuración glimmer_post_menu_mode.

Esta configuración acepta tres valores posibles:

  • disabled: utiliza el sistema heredado de “widget”.
  • auto: detectará la compatibilidad de tus plugins y temas actuales. Si alguno no es compatible, se utilizará el sistema heredado; de lo contrario, se utilizará el nuevo menú.
  • enabled: utilizará el nuevo menú. Si tienes algún plugin o tema incompatible, es posible que tu sitio deje de funcionar.

Ya hemos actualizado nuestros plugins oficiales para que sean compatibles con el nuevo menú, pero si aún tienes algún plugin, tema o componente de tema de terceros incompatible con el nuevo menú, será necesario actualizarlos.

Se imprimirán advertencias en la consola del navegador que identificarán el origen de la incompatibilidad.

:timer_clock: Cronograma de implementación

Estas son estimaciones aproximadas sujetas a cambios

Q4 2024:

  • :white_check_mark: implementación central finalizada
  • :white_check_mark: plugins oficiales actualizados
  • :white_check_mark: habilitado en Meta
  • :white_check_mark: glimmer_post_menu_mode configurado por defecto en auto; mensajes de obsolescencia en consola habilitados
  • :white_check_mark: publicado consejo de actualización

Q1 2025:

  • :white_check_mark: los plugins y temas de terceros deberían estar actualizados
  • :white_check_mark: los mensajes de obsolescencia comenzarán, activando un banner de advertencia para administradores para cualquier problema restante
  • :white_check_mark: el nuevo menú de publicaciones se habilitará por defecto

Q2 2025

  • :white_check_mark: 1 de abril - eliminación de la configuración de la bandera de función y del código heredado

:eyes: ¿Qué significa esto para mí?

Si tu plugin o tema utiliza alguna API de “widget” para personalizar el menú de publicaciones, deberá actualizarse para ser compatible con la nueva versión.

:person_tipping_hand: ¿Cómo puedo probar el nuevo menú de publicaciones?

En la última versión de Discourse, el nuevo menú de publicaciones se habilitará si no tienes ningún plugin o tema incompatible.

Si tienes extensiones incompatibles instaladas, como administrador, aún puedes cambiar la configuración a enabled para forzar el uso del nuevo menú. Usa esto con precaución, ya que tu sitio podría dejar de funcionar dependiendo de las personalizaciones que tengas instaladas.

En el improbable caso de que este sistema automático no funcione como se espera, puedes anular temporalmente esta “bandera de función automática” utilizando la configuración anterior. Si necesitas hacerlo, por favor avísanos en este tema.

:technologist: ¿Necesito actualizar mi plugin o tema?

Necesitarás actualizar tus plugins o temas si realizan alguna de las personalizaciones siguientes:

  • Utilizan decorateWidget, changeWidgetSetting, reopenWidget o attachWidgetAction en estos widgets:

    • post-menu
    • post-user-tip-shim
    • small-user-list
  • Utilizan cualquiera de los siguientes métodos de la API:

    • addPostMenuButton
    • removePostMenuButton
    • replacePostMenuButton

:bulb: En caso de que tengas extensiones que realicen una de las personalizaciones anteriores, se imprimirá una advertencia en la consola que identificará el plugin o componente que necesita ser actualizado al acceder a una página de tema.

El ID de obsolescencia es: discourse.post-menu-widget-overrides

:warning: Si utilizas más de un tema en tu instancia, asegúrate de revisar todos ellos, ya que las advertencias solo se imprimirán para los plugins activos y los temas y componentes de tema actualmente en uso.

¿Cuáles son las alternativas?

Hemos introducido el transformador de valores post-menu-buttons como la nueva API para personalizar el menú de publicaciones.

El transformador de valores proporciona un objeto DAG que permite agregar, reemplazar, eliminar o reordenar los botones. También proporciona información contextual, como la publicación asociada con el menú, el estado de la publicación mostrada y las claves de los botones para facilitar la colocación de los elementos.

Las APIs de DAG esperan recibir componentes Ember si la API necesita una nueva definición de botón, como .add y .replace.

Cada personalización es diferente, pero aquí hay algunas orientaciones para los casos de uso más comunes:

addPostMenuButton

Antes:

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

Después:

Los ejemplos a continuación utilizan el Formato de etiqueta de plantilla (gjs) de 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 si el botón se mostrará inmediatamente o se ocultará detrás del botón "mostrar más"
  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, // clave del primer botón
        secondLastHiddenButtonKey, // clave del penúltimo botón oculto
        lastHiddenButtonKey, // clave del último botón oculto
      },
    }) => {
        dag.add(
          "solved",
          SolvedAcceptAnswerButton,
          post.topic_accepted_answer
            ? {
                before: lastHiddenButtonKey,
                after: secondLastHiddenButtonKey,
              }
            : {
                before: [
                  "assign", // botón agregado por el plugin assign
                  firstButtonKey,
                ],
              }
        );
    }
  );
});

:bulb: Estilizando tus botones

Se recomienda incluir ...attributes como se muestra en el ejemplo anterior en tu componente.

Al combinarse con el uso de los componentes DButton o DMenu, esto se encargará de las clases de plantilla y asegurará que tu botón siga el formato de los demás botones del menú de publicaciones.

El formato adicional se puede especificar utilizando tus clases personalizadas.

replacePostMenuButton

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

...

withPluginApi("1.34.0", (api) => {
  api.registerValueTransformer(
    "post-menu-buttons",
    ({ value: dag, context: { buttonKeys } }) => {
      // ReactionsActionButton es el nuevo componente de botón
      dag.replace(buttonKeys.LIKE, ReactionsActionButton);
    }
  );
});

removePostMenuButton

  • Antes:
withPluginApi("1.34.0", (api) => {
  api.removePostMenuButton('like', (attrs, state, siteSettings, settings, currentUser) => {
    if (attrs.post_number === 1) {
      return true;
    }
  });
});
  • Despué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: ¿Qué pasa con otras personalizaciones?

Si tu personalización no puede lograrse utilizando la nueva API que hemos introducido, por favor avísanos creando un nuevo tema de desarrollo para discutirlo.

:sparkles: Soy autor de un plugin/tema. ¿Cómo actualizo un tema/plugin para que sea compatible con el menú de publicaciones antiguo y nuevo durante la transición?

Hemos utilizado el siguiente patrón para soportar tanto la versión antigua como la nueva del menú de publicaciones en nuestros plugins:

function customizePostMenu(api) {
  const transformerRegistered = api.registerValueTransformer(
    "post-menu-buttons",
    ({ value: dag, context }) => {
      // personalizaciones del nuevo menú de publicaciones
      ...
    }
  );

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

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

function customizeWidgetPostMenu(api) {
  // código de personalización antiguo de "widget" aquí
  ...
}

export default {
  name: "my-plugin",

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

:star: Más ejemplos

Puedes consultar nuestros plugins oficiales para ver ejemplos de cómo utilizar la nueva API:

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.

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.

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!

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)

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:

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

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

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

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

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: