Próximas mudanças no menu de posts - Como preparar temas e 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 curtidas

Trocamos o Glimmer Post Menu para enabled (ativado) por padrão.

Assim que sua instância do Discourse for atualizada, isso fará com que personalizações existentes que não foram atualizadas para a nova API não sejam aplicadas.

Por enquanto, os administradores ainda podem reverter a configuração para disabled (desativado) enquanto atualizam as personalizações restantes.

O código legado será removido no início do segundo trimestre.

2 curtidas

Aprecio a flexibilidade de ter a API completa do componente disponível. Gosto da sintaxe dos componentes Glimmer em geral, e vejo por que ela pode ter benefícios em reduzir a complexidade dentro da base de código.

No entanto, para casos de uso básicos (quero adicionar um botão e dar a ele um ícone), os métodos antigos da API eram objetivamente mais concisos e fáceis de entender (menos importações, menos operações, menos pegada de API exposta). Existe alguma razão pela qual os métodos antigos da API não poderiam ter suporte contínuo? Imagino que se você os usasse como funções de conveniência e realizasse a implementação subjacente usando seu novo componente Glimmer, o método da API também poderia realizar a verificação de compatibilidade de versão.

Isso seria muito menos disruptivo para qualquer pessoa que use esses métodos e criaria menos explosão de código de lógica condicional no ecossistema de plugins.

Minha principal reclamação sobre os widgets existentes é a falta de documentação. Este post, anunciando sua remoção, é um dos documentos mais claros que vi de que esses métodos específicos existem e como usá-los. Na verdade, vim aqui tentando descobrir como usar a API antiga.

Gosto que os transformadores sejam registrados em um só lugar, por literais de string. Acho que isso torna o trabalho de documentação (e desenvolvimento de plugins) muito mais fácil.

Com os widgets, todos parecem ser registrados pelo método createWidget, que então chama createWidgetFrom. O problema que vejo nisso é que o _registry é uma variável global de escopo de arquivo protegida por uma API, e a API não permite nenhuma iteração. Se pudéssemos apenas obter uma função de iteração no registro de widgets, poderíamos descobrir em tempo real os widgets atualmente registrados. Deveria ser documentado “execute esta linha de javascript no console do seu navegador para chamar a API e listar o registro”. Então, poderíamos obter uma utilidade muito semelhante à que o registro de transformadores está fornecendo.

Outra coisa que ajudaria no desenvolvimento de plugins é ver um atributo em qualquer elemento DOM raiz renderizado por um componente/widget que diga qual componente/widget você está olhando. Como “data-widget=foo”. Isso poderia ser um recurso de depuração, ou poderia simplesmente estar habilitado por padrão. É OSS, então não é como se você estivesse alcançando segurança por obscuridade.

Celebro a mudança para componentes Glimmer. Mas isso levará tempo, e há muitos widgets com os quais as pessoas precisam trabalhar nesse ínterim. Portanto, acho que melhorar a visibilidade dos widgets, como mencionado acima, provavelmente tornaria o período de transição mais fácil para todos.

Quanto a esses métodos de API… Parece que alguém se deu ao trabalho de adicionar comentários detalhados à API javascript, mas nenhum site de documentação foi gerado para isso. Por que não?

Ficarei feliz em enviar um pull request para iterar pelo registro de widgets, se isso for aceitável.

3 curtidas

Além disso, se eu quiser implementar apenas a nova funcionalidade, para qual versão da API do Discourse devo ajustar a compatibilidade do meu plugin? Você usa withPluginApi("1.34.0", ...) em todos os seus exemplos. Acho que essa é uma versão mais antiga e não representa quando essa mudança foi feita? Mas, por favor, esclareça. Obrigado!

2 curtidas

É a versão correta. Você pode consultar o changelog aqui:

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

Além disso, este recurso pode ajudar: Pinning plugin and theme versions for older Discourse installs (.discourse-compatibility)

1 curtida

A remoção de widgets está em andamento há alguns anos e esperamos finalizá-la nos próximos meses. Portanto, não acho que faremos nenhuma alteração no sistema subjacente antes disso.

Você já experimentou o Ember Inspector? Acho que ele deve resolver o problema que você descreve e ainda mostrará componentes Ember que não estão renderizando nenhum elemento DOM no momento.

Muito recentemente, este número de versão não é mais necessário. Você pode fazer

export default apiInitializer((api) => {

ou

withPluginApi((api) => {

Atualizaremos a documentação com essa alteração em breve. A preferência moderna para gerenciar a intercompatibilidade de versões é o arquivo .discourse-compatibility, que @Arkshine mencionou:

4 curtidas

Isso é muito bom! Toda vez que eu esqueço de atualizar esse número :rofl:.

1 curtida

@david @Arkshine uau, ótimos posts, muito úteis!

Outra pergunta - em relação ao “contexto” que é compartilhado ao chamar api.registerValueTransformer, como posso descobrir qual contexto será passado para mim? Suponho que eu possa simplesmente usar console.log no contexto, mas seria bom saber com antecedência o que está disponível.

Para o plugin que estou escrevendo agora, estou concedendo privilégios especiais de moderação ao autor de um tópico. Para fazer isso, preciso saber o “autor do tópico atual”, o “usuário atual” e a “associação de grupo do usuário logado”.

Talvez esse exemplo específico ajude a dar contexto à minha pergunta.

EDIT:

Meu código final se parece com isto, para qualquer outra pessoa com interesses semelhantes:

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

// se o usuário atual for o autor da postagem, ou estiver em um grupo privilegiado
if (post?.topic?.details?.created_by?.id === currentUser.id || validGroups.length > 0) {
  // conceda a eles acesso ao recurso
  ...
}
1 curtida

Receio que não tenhamos nenhuma documentação central para estes no momento. Algumas áreas do aplicativo têm sua própria documentação específica por exemplo, a lista de tópicos, que lista os argumentos de contexto. Mas, caso contrário, a melhor opção é procurar pela chamada applyTransformer no código-fonte do core, ou usar console.log.

Documentação geral sobre transformadores pode ser encontrada aqui: Using Transformers to customize client-side values and behavior

1 curtida

Pesquisar por "applyTransformer" retorna 0 resultados. Estou procurando no lugar errado?

Descobri que pesquisar por "api.registerValueTransformer" retorna alguns exemplos úteis. Mas, é claro, os exemplos não fornecem documentação abrangente do contexto retornado - eles mostram apenas aqueles que foram úteis para aquele exemplo específico.

Do console.log de context no meu exemplo específico, vejo que post está sendo retornado, mas user não. Então, como posso ter acesso a outro estado da aplicação que não está contido no contexto?

Entendo que anteriormente se poderia ter chamado helper.getModel() ou helper.currentUser dentro de um contexto api.decorateWidget. Presumo que exista algum método atual para obter resultados semelhantes.

Obrigado por toda a ajuda aqui.

Ah, acho que respondi à minha própria pergunta. Este exemplo mostra o uso de api.getCurrentUser(). Então, essencialmente, essa parte da API não mudou e ainda é compatível com o paradigma glimmer.

Acredito que ele quis dizer applyValueTransformer ou applyBehaviorTransformer. Tais funções podem ser encontradas no seguinte arquivo: discourse/app/assets/javascripts/discourse/app/lib/transformer.js at main · discourse/discourse · GitHub

4 curtidas

2 posts foram divididos para um novo tópico: Podemos usar .gjs para templates de rota?

O código do menu de posts legados foi removido. Obrigado a todos que trabalharam na atualização de seus temas e plugins :rocket:

6 curtidas