即将上线的帖子菜单更改——如何准备主题和插件

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 个赞

我们将 Glimmer Post Menu 默认设置为 enabled

一旦您的 Discourse 实例升级,这将导致未更新到新 API 的现有自定义设置将不被应用。

目前,管理员在更新剩余自定义设置的同时,仍可以将设置改回 disabled

旧代码将在第一季度初被移除。

2 个赞

我赞赏拥有完整组件 API 的灵活性。我喜欢 Glimmer 组件的整体语法,并且我明白它可能在降低代码库复杂性方面具有优势。

但是,对于基本用例(我想添加一个按钮并为其添加图标),旧的 API 方法在客观上更简洁易懂(导入更少,操作更少,暴露的 API 占用空间更小)。旧的 API 方法是否有任何理由不能继续支持?我想象如果将它们用作便捷函数,并使用新的 Glimmer 组件执行底层实现,那么 API 方法也可以执行版本兼容性检查。

这将对使用这些方法的任何人造成的干扰要小得多,并且可以减少插件生态系统中条件逻辑的代码爆炸。

我对现有组件的主要抱怨是它们缺乏文档。这篇宣布移除它们的帖子是我见过的关于这些特定方法存在以及如何使用它们的 सर्वात清晰的文档之一。事实上,我来这里是为了弄清楚如何使用旧的 API。

我喜欢转换器在一个地方,通过字符串字面量进行注册。我认为这使得文档(和插件开发)工作变得容易得多。

对于组件,它们似乎都通过 createWidget 方法注册,该方法然后调用 createWidgetFrom。我在这里看到的问题是,_注册表是一个文件作用域的全局变量,由一个 API 保护,而该 API 不允许任何迭代。如果我们能在组件注册表中获得一个迭代函数,那么我们就可以实时发现当前注册的组件。应该记录“在浏览器控制台中运行此行 JavaScript 来调用 API 并列出注册表”。然后,我们可以获得与 transformer 注册表提供的功能非常相似的功能。

在插件开发方面,另一件有帮助的事情是,在组件/组件渲染的任何根 DOM 元素上看到一个属性,告诉我们您正在查看哪个组件/组件。例如“data-widget=foo”。这可以是一个调试功能,也可以默认启用。它是开源的,所以您并不是通过隐藏来实现安全性。

我赞扬向 Glimmer 组件的转变。但这需要时间,在此期间,人们需要处理很多组件。因此,我认为如上所述改进组件的可视性可能会让每个人都更容易过渡。

至于那些 API 方法……看起来有人费尽心思为 JavaScript API 添加了详细的注释,但从未为此生成文档站点。为什么不呢?

如果可行,我很乐意提交一个用于迭代组件注册表的拉取请求。

3 个赞

此外,如果我只想实现新的功能,我应该将我的插件兼容性锁定到哪个Discourse API版本?你在所有示例中都使用了 withPluginApi("1.34.0" ...)。我认为这是一个较旧的版本,并不代表这一变更何时进行的?但请澄清。谢谢!

2 个赞

这是正确的版本。您可以在此处查看更新日志:

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)

1 个赞

小部件的移除已经进行了一两年,我们希望在未来几个月内完成。所以我认为在此之前我们不会对底层系统进行任何更改。

您试过 Ember Inspector 了吗?我认为它应该能解决您描述的问题,甚至还能显示当前未渲染任何 DOM 元素的 Ember 组件。

非常近期,这个版本号不再是必需的。你可以这样做

export default apiInitializer((api) => {

或者

withPluginApi((api) => {

我们很快就会更新文档以反映这一变化。管理版本互通性的现代首选是 @Arkshine 提到的 .discourse-compatibility 文件:

4 个赞

这真是太好了!每次我都会忘记更新那个数字 :rofl:

1 个赞

@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) {
  // 授予他们访问该功能的权限
  ...
}
1 个赞

恐怕我们目前没有任何中央文档。应用程序的某些区域有自己的特定文档例如,主题列表,其中列出了上下文参数。但除此之外,最好的办法是在核心代码库中搜索 applyTransformer 调用,或者使用 console.log

关于 transformer 的一般文档可以在这里找到:Using Transformers to customize client-side values and behavior

1 个赞

搜索“applyTransformer”返回 0 个结果。是我找错地方了吗?

我确实发现搜索“api.registerValueTransformer”返回了一些有用的示例。但这些示例当然没有提供返回的上下文的全面文档——它们只显示了对该特定示例有用的那些。

从我特定示例中 contextconsole.log 输出来看,我看到返回了 post 但没有返回 user。那么,我该如何访问不包含在上下文中的其他应用程序状态呢?

我了解到之前可能会在 api.decorateWidget 上下文中调用 helper.getModel()helper.currentUser。我假设现在有一些现成的方法可以获得类似的结果。

感谢您的所有帮助。

哦,我想我找到了答案。 这个例子 展示了 api.getCurrentUser() 的用法。所以,API 的那部分实际上没有改变,仍然与 Glimmer 范式兼容。

我认为他指的是 applyValueTransformerapplyBehaviorTransformer。您可以在以下文件中找到这些函数:discourse/app/assets/javascripts/discourse/app/lib/transformer.js at main · discourse/discourse · GitHub

4 个赞

2 帖已拆分到新主题:我们可以在路由模板中使用 .gjs 吗?

旧帖子菜单代码已被移除。感谢所有更新了他们的主题和插件 :rocket: 的人。

6 个赞