Грядущие изменения в меню постов: как подготовить темы и плагины

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:

Мы включили меню «Glimmer Post» по умолчанию.

После обновления вашего экземпляра Discourse это приведёт к тому, что существующие кастомизации, не обновлённые под новый API, не будут применены.

На данный момент администраторы могут вернуть настройку в состояние disabled, пока остальные кастомизации обновляются.

Устаревший код будет удалён в начале второго квартала.

Я ценю гибкость, которую даёт полный доступ к API компонентов. Мне нравится синтаксис компонентов Glimmer в целом, и я понимаю, почему он может снизить сложность кодовой базы.

Однако для базовых сценариев (например, когда нужно добавить кнопку и иконку) старые методы API были объективно более лаконичными и понятными (меньше импортов, меньше операций, меньше экспонируемого API). Есть ли причина, по которой старые методы не могли бы оставаться поддерживаемыми? Я представляю себе, что их можно было бы использовать как служебные функции, реализуя их внутреннюю логику на основе новых компонентов Glimmer, а сами методы API могли бы выполнять проверку совместимости версий.

Это было бы гораздо менее разрушительным для тех, кто использует эти методы, и предотвратило бы взрывной рост условной логики в экосистеме плагинов.

Моя главная претензия к существующим виджетам — отсутствие документации. Этот пост, объявляющий об их удалении, является одним из самых чётких документов, где упоминается существование этих конкретных методов и способы их использования. На самом деле я пришёл сюда именно потому, что пытался разобраться, как использовать старый API.

Мне нравится, что трансформеры регистрируются в одном месте с помощью строковых литералов. Это значительно упрощает работу по документации и разработку плагинов.

Что касается виджетов, то все они, похоже, регистрируются через метод createWidget, который затем вызывает createWidgetFrom. Проблема здесь в том, что _registry — это глобальная переменная в пределах файла, защищённая API, и этот API не позволяет выполнять итерацию. Если бы мы могли получить функцию итерации для реестра виджетов, то могли бы в реальном времени обнаруживать зарегистрированные виджеты. Должно быть документировано: «запустите эту строку JavaScript в консоли браузера, чтобы вызвать API и вывести реестр». Тогда мы получили бы утилиту, очень похожую на ту, что предоставляет реестр трансформеров.

Ещё одна вещь, которая помогла бы в разработке плагинов, — это наличие атрибута на любом корневом DOM-элементе, рендеримом компонентом или виджетом, который указывает, какой именно компонент или виджет вы просматриваете. Например, «data-widget=foo». Это может быть функцией отладки или просто включённой по умолчанию. Поскольку проект с открытым исходным кодом (OSS), речь не идёт о безопасности через неясность.

Я приветствую переход к компонентам Glimmer. Но это потребует времени, и в течение этого периода людям всё ещё придётся работать со множеством виджетов. Поэтому я считаю, что улучшение видимости виджетов, как описано выше, скорее всего, облегчит переходный период для всех.

Что касается этих методов API… Похоже, что кто-то потрудился добавить подробные комментарии к JavaScript API, но сайт документации для него так и не был сгенерирован. Почему?

Я с радостью подготовлю pull request для реализации итерации по реестру виджетов, если это приемлемо.

Кроме того, если я хочу реализовать только новый функционал, к какой версии API Discourse мне привязать совместимость моего плагина? Во всех ваших примерах используется withPluginApi("1.34.0", ...). Мне кажется, это более старая версия и она не отражает момент, когда было внесено это изменение? Пожалуйста, уточните. Спасибо!

Это правильная версия. Вы можете посмотреть список изменений здесь:

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

Кроме того, может помочь эта функция: Pinning plugin and theme versions for older Discourse installs (.discourse-compatibility)

Удаление виджетов уже идёт несколько лет, и мы надеемся завершить этот процесс в ближайшие несколько месяцев. Поэтому я не думаю, что до этого момента мы будем вносить какие-либо изменения в базовую систему.

Вы пробовали Ember Inspector? Думаю, он решит описанную вами проблему и даже покажет компоненты Ember, которые в данный момент не рендерят никаких DOM-элементов.

Совсем недавно эта версия больше не является обязательной. Вы можете написать:

export default apiInitializer((api) => {

или

withPluginApi((api) => {

Мы скоро обновим документацию с учётом этого изменения. Современным предпочтительным способом управления совместимостью версий является файл .discourse-compatibility, о котором упоминал @Arkshine:

Это на самом деле очень здорово! Каждый раз, когда я забываю обновить этот номер :rofl:.

@david @Arkshine вау, отличные посты, действительно полезные!

Ещё один вопрос — относительно «контекста», который передаётся при вызове api.registerValueTransformer: как узнать, какой контекст будет передан? Я полагаю, можно просто вывести контекст через console.log, но было бы неплохо заранее знать, что доступно.

Для плагина, который я сейчас пишу, я предоставляю особые права модерации автору темы. Чтобы это реализовать, мне нужно знать «автора текущей темы», «текущего пользователя» и «членство в группах авторизованного пользователя».

Возможно, этот конкретный пример поможет прояснить мой вопрос.

РЕДАКТИРОВАНИЕ:

Мой итоговый код выглядит так, на случай если кому-то ещё это интересно:

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

// если текущий пользователь — автор поста или состоит в привилегированной группе
if (post?.topic?.details?.created_by?.id === currentUser.id || validGroups.length > 0) {
  // предоставляем им доступ к функции
  ...
}

Боюсь, что у нас пока нет центральной документации по этому вопросу. В некоторых частях приложения есть собственная специализированная документация например, по списку тем, где перечислены аргументы контекста. Но в остальных случаях лучше всего поискать вызов applyTransformer в исходном коде ядра или использовать console.log.

Общая документация по трансформерам доступна здесь: Using Transformers to customize client-side values and behavior

Поиск «applyTransformer» вернул 0 результатов. Не туда ли я смотрю?

Я нашёл, что поиск «api.registerValueTransformer» выдаёт несколько полезных примеров. Но, конечно, эти примеры не содержат полной документации по возвращаемому контексту — они показывают только то, что было полезно в каждом конкретном случае.

Из вывода console.log для context в моём примере я вижу, что возвращается post, но не user. Так как же получить доступ к другому состоянию приложения, которое не входит в контекст?

Понимаю, что раньше можно было вызвать helper.getModel() или helper.currentUser в контексте api.decorateWidget. Предполагаю, что сейчас есть какой-то аналогичный метод для получения подобных данных.

Спасибо за всю помощь.

О, я думаю, я сам себе ответил. Этот пример демонстрирует использование api.getCurrentUser(). Таким образом, эта часть API не изменилась и по-прежнему совместима с парадигмой glimmer.

Думаю, он имел в виду applyValueTransformer или applyBehaviorTransformer. Такие функции можно найти в следующем файле: https://github.com/discourse/discourse/blob/main/app/assets/javascripts/discourse/app/lib/transformer.js

Код устаревшего меню постов теперь удален. Спасибо всем, кто работал над обновлением своих тем и плагинов :rocket: