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

В рамках наших непрерывных усилий по улучшению кодовой базы Discourse мы удаляем использование устаревшей системы рендеринга «widget» и заменяем её компонентами Glimmer.

Недавно мы модернизировали меню постов, и теперь оно доступно в Discourse при включении настройки glimmer_post_menu_mode.

Эта настройка принимает три возможных значения:

  • disabled: использовать устаревшую систему «widget»
  • auto: автоматически определяет совместимость ваших текущих плагинов и тем. Если какие-либо из них несовместимы, будет использована устаревшая система; в противном случае — новое меню.
  • enabled: использовать новое меню. Если у вас есть несовместимый плагин или тема, ваш сайт может перестать работать корректно.

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

В консоли браузера будут выводиться предупреждения, указывающие источник несовместимости.

:timer_clock: План внедрения

Эти сроки являются приблизительными и могут быть изменены

IV квартал 2024 года:

  • :white_check_mark: реализация в ядре завершена
  • :white_check_mark: официальные плагины обновлены
  • :white_check_mark: включено на Meta
  • :white_check_mark: glimmer_post_menu_mode по умолчанию установлено в auto; включены сообщения об устаревании в консоли
  • :white_check_mark: опубликованы рекомендации по обновлению

I квартал 2025 года:

  • :white_check_mark: сторонние плагины и темы должны быть обновлены
  • :white_check_mark: начнут выводиться сообщения об устаревании, вызывающие предупреждающий баннер для администратора при наличии оставшихся проблем
  • :white_check_mark: новое меню постов включено по умолчанию

II квартал 2025 года

  • :white_check_mark: 1 апреля — удаление настройки флага функции и устаревшего кода

:eyes: Что это значит для меня?

Если ваш плагин или тема использует какие-либо API «widget» для кастомизации меню постов, их необходимо обновить для совместимости с новой версией.

:person_tipping_hand: Как попробовать новое меню постов?

В последней версии Discourse новое меню постов будет включено, если у вас нет несовместимых плагинов или тем.

Если у вас установлены несовместимые расширения, вы как администратор всё ещё можете изменить настройку на enabled, чтобы принудительно использовать новое меню. Используйте это с осторожностью, так как ваш сайт может перестать работать корректно в зависимости от установленных кастомизаций.

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

:technologist: Нужно ли мне обновлять мой плагин или тему?

Вам потребуется обновить ваши плагины или темы, если они выполняют любую из следующих кастомизаций:

  • Используют decorateWidget, changeWidgetSetting, reopenWidget или attachWidgetAction для следующих виджетов:

    • post-menu
    • post-user-tip-shim
    • small-user-list
  • Используют любые из следующих методов API:

    • addPostMenuButton
    • removePostMenuButton
    • replacePostMenuButton

:bulb: Если у вас есть расширения, выполняющие одну из указанных выше кастомизаций, при доступе к странице темы в консоли будет выведено предупреждение, указывающее плагин или компонент, требующий обновления.

ID устаревания: discourse.post-menu-widget-overrides

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

Какие существуют замены?

Мы внедрили трансформатор значений post-menu-buttons как новый API для кастомизации меню постов.

Трансформатор значений предоставляет объект DAG, который позволяет добавлять, заменять, удалять или изменять порядок кнопок. Он также предоставляет контекстную информацию, такую как пост, связанный с меню, состояние отображаемого поста и ключи кнопок для упрощения размещения элементов.

API DAG ожидает получение компонентов Ember, если API требует новое определение кнопки, например .add и .replace.

Каждая кастомизация уникальна, но вот некоторые рекомендации для наиболее распространенных случаев использования:

addPostMenuButton

До:

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

После:

Приведенные ниже примеры используют формат шаблонов (gjs) в 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 {
  // указывает, будет ли кнопка отображаться немедленно или скрыта за кнопкой «показать ещё»
  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, // ключ первой кнопки
        secondLastHiddenButtonKey, // ключ второй последней скрытой кнопки
        lastHiddenButtonKey, // ключ последней скрытой кнопки
      },
    }) => {
        dag.add(
          "solved",
          SolvedAcceptAnswerButton,
          post.topic_accepted_answer
            ? {
                before: lastHiddenButtonKey,
                after: secondLastHiddenButtonKey,
              }
            : {
                before: [
                  "assign", // кнопка, добавленная плагином assign
                  firstButtonKey,
                ],
              }
        );
    }
  );
});

:bulb: Стилизация ваших кнопок

Рекомендуется включать ...attributes, как показано в примере выше, в ваш компонент.

В сочетании с использованием компонентов DButton или DMenu это обеспечит базовые классы и гарантирует, что ваша кнопка будет соответствовать форматированию других кнопок в меню постов.

Дополнительное форматирование можно указать с помощью ваших собственных классов.

replacePostMenuButton

  • До:
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;
    },
  });
});
  • После:
import ReactionsActionButton from "../components/discourse-reactions-actions-button";

...

withPluginApi("1.34.0", (api) => {
  api.registerValueTransformer(
    "post-menu-buttons",
    ({ value: dag, context: { buttonKeys } }) => {
      // ReactionsActionButton — это новый компонент кнопки
      dag.replace(buttonKeys.LIKE, ReactionsActionButton);
    }
  );
});

removePostMenuButton

  • До:
withPluginApi("1.34.0", (api) => {
  api.removePostMenuButton('like', (attrs, state, siteSettings, settings, currentUser) => {
    if (attrs.post_number === 1) {
      return true;
    }
  });
});
  • После:
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: А что насчет других кастомизаций?

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

:sparkles: Я автор плагина/темы. Как обновить тему/плагин для поддержки как старого, так и нового меню постов в период перехода?

Мы использовали следующую схему для поддержки обеих версий меню постов в наших плагинах:

function customizePostMenu(api) {
  const transformerRegistered = api.registerValueTransformer(
    "post-menu-buttons",
    ({ value: dag, context }) => {
      // новые кастомизации меню постов
      ...
    }
  );

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

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

function customizeWidgetPostMenu(api) {
  // здесь старая кастомизация кода «widget»
  ...
}

export default {
  name: "my-plugin",

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

:star: Дополнительные примеры

Вы можете проверить наши официальные плагины, чтобы увидеть примеры использования нового 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: