Próximas mudanças no menu de posts - Como preparar temas e plugins

Como parte de nosso esforço contínuo para melhorar a base de código do Discourse, estamos removendo o uso do sistema de renderização legado “widget” e substituindo-o por componentes Glimmer.

Recentemente, modernizamos o menu de postagem, e ele já está disponível no Discourse por meio da configuração glimmer_post_menu_mode.

Essa configuração aceita três valores possíveis:

  • disabled: usa o sistema legado “widget”
  • auto: detectará a compatibilidade dos seus plugins e temas atuais. Se houver algum incompatível, usará o sistema legado; caso contrário, usará o novo menu.
  • enabled: usará o novo menu. Se você tiver algum plugin ou tema incompatível, seu site pode quebrar.

Já atualizamos nossos plugins oficiais para serem compatíveis com o novo menu, mas se você ainda tiver algum plugin, tema ou componente de tema de terceiros incompatível com o novo menu, será necessário atualizá-los.

Avisos serão exibidos no console do navegador, identificando a origem da incompatibilidade.

:timer_clock: Cronograma de Implantação

Estas são estimativas aproximadas, sujeitas a alterações

Q4 2024:

  • :white_check_mark: implementação principal concluída
  • :white_check_mark: plugins oficiais atualizados
  • :white_check_mark: ativado no Meta
  • :white_check_mark: glimmer_post_menu_mode padrão definido como auto; mensagens de depreciação no console ativadas
  • :white_check_mark: conselho de atualização publicado

Q1 2025:

  • :white_check_mark: plugins e temas de terceiros devem ser atualizados
  • :white_check_mark: mensagens de depreciação começam, acionando um banner de aviso para administradores sobre quaisquer problemas remanescentes
  • :white_check_mark: novo menu de postagem ativado por padrão

Q2 2025

  • :white_check_mark: 1º de abril - remoção da configuração de recurso (feature flag) e do código legado

:eyes: O que isso significa para mim?

Se o seu plugin ou tema usa alguma API de “widget” para personalizar o menu de postagem, eles precisarão ser atualizados para serem compatíveis com a nova versão.

:person_tipping_hand: Como posso testar o novo Menu de Postagem?

Na versão mais recente do Discourse, o novo menu de postagem será ativado se você não tiver nenhum plugin ou tema incompatível.

Se você tiver extensões incompatíveis instaladas, como administrador, ainda pode alterar a configuração para enabled para forçar o uso do novo menu. Use isso com cautela, pois seu site pode quebrar dependendo das personalizações que você instalou.

No improvável caso de esse sistema automático não funcionar como esperado, você pode substituir temporariamente essa “feature flag automática” usando a configuração acima. Se precisar fazer isso, por favor, nos avise neste tópico.

:technologist: Preciso atualizar meu plugin ou tema?

Você precisará atualizar seus plugins ou temas se eles realizarem alguma das personalizações abaixo:

  • Usar decorateWidget, changeWidgetSetting, reopenWidget ou attachWidgetAction nos seguintes widgets:

    • post-menu
    • post-user-tip-shim
    • small-user-list
  • Usar qualquer um dos métodos de API abaixo:

    • addPostMenuButton
    • removePostMenuButton
    • replacePostMenuButton

:bulb: Caso você tenha extensões que realizam uma das personalizações acima, um aviso será exibido no console identificando o plugin ou componente que precisa ser atualizado ao acessar uma página de tópico.

O ID de depreciação é: discourse.post-menu-widget-overrides

:warning: Se você usar mais de um tema na sua instância, verifique todos eles, pois os avisos serão exibidos apenas para os plugins ativos e os temas e componentes de tema atualmente em uso.

Quais são as substituições?

Introduzimos o transformador de valor post-menu-buttons como a nova API para personalizar o menu de postagem.

O transformador de valor fornece um objeto DAG que permite adicionar, substituir, remover ou reordenar os botões. Ele também fornece informações de contexto, como a postagem associada ao menu, o estado da postagem exibida e as chaves dos botões para facilitar o posicionamento dos itens.

As APIs do DAG esperam receber componentes Ember se a API precisar de uma nova definição de botão, como .add e .replace.

Cada personalização é diferente, mas aqui estão algumas orientações para os casos de uso mais comuns:

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

Depois:

Os exemplos abaixo usam o Formato de Tag de Template (gjs) do 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 o botão será exibido imediatamente ou oculto atrás do botão "mostrar mais"
  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, // chave do primeiro botão
        secondLastHiddenButtonKey, // chave do segundo último botão oculto
        lastHiddenButtonKey, // chave do último botão oculto
      },
    }) => {
        dag.add(
          "solved",
          SolvedAcceptAnswerButton,
          post.topic_accepted_answer
            ? {
                before: lastHiddenButtonKey,
                after: secondLastHiddenButtonKey,
              }
            : {
                before: [
                  "assign", // botão adicionado pelo plugin assign
                  firstButtonKey,
                ],
              }
        );
    }
  );
});

:bulb: Estilizando seus botões

É recomendado incluir ...attributes conforme mostrado no exemplo acima em seu componente.

Quando combinado com o uso dos componentes DButton ou DMenu, isso cuidará das classes de boilerplate e garantirá que seu botão siga o formato dos outros botões no menu de postagem.

Formatações adicionais podem ser especificadas usando suas classes 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;
    },
  });
});
  • Depois:
import ReactionsActionButton from "../components/discourse-reactions-actions-button";

...

withPluginApi("1.34.0", (api) => {
  api.registerValueTransformer(
    "post-menu-buttons",
    ({ value: dag, context: { buttonKeys } }) => {
      // ReactionsActionButton é o novo componente de botão
      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;
    }
  });
});
  • Depois:
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 quanto a outras personalizações?

Se sua personalização não puder ser realizada usando a nova API que introduzimos, por favor, nos avise criando um novo tópico de desenvolvimento para discutir.

:sparkles: Sou autor de plugin/tema. Como atualizo um tema/plugin para suportar tanto o menu de postagem antigo quanto o novo durante a transição?

Usamos o padrão abaixo para suportar ambas as versões antiga e nova do menu de postagem em nossos plugins:

function customizePostMenu(api) {
  const transformerRegistered = api.registerValueTransformer(
    "post-menu-buttons",
    ({ value: dag, context }) => {
      // personalizações do novo menu de postagem
      ...
    }
  );

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

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

function customizeWidgetPostMenu(api) {
  // personalização do código antigo "widget" aqui
  ...
}

export default {
  name: "my-plugin",

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

:star: Mais exemplos

Você pode verificar nossos plugins oficiais para exemplos de como usar a nova API:

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.

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.

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!

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

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:

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

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

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

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

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