今後の投稿メニュー変更 - テーマとプラグインの準備方法

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 に戻すことができます。

レガシーコードは第1四半期の初めに削除されます。

「いいね!」 2

コンポーネントAPI全体が利用可能であることの柔軟性に感謝しています。全体的にGlimmerコンポーネントの構文は気に入っており、コードベース内の複雑さを軽減するのに役立つ理由も理解できます。

しかし、基本的なユースケース(ボタンを追加してアイコンを付けたい場合)では、古いAPIメソッドの方が客観的に簡潔で理解しやすかった(インポートが少なく、操作が少なく、公開されるAPIフットプリントが少ない)です。古いAPIメソッドが引き続きサポートされない理由はあるのでしょうか?それらを便利な関数として使用し、基盤となる実装を新しいGlimmerコンポーネントで行うことで、APIメソッドもバージョン互換性のチェックを実行できると想像しています。

これにより、これらのメソッドを使用しているユーザーへの影響ははるかに少なくなり、プラグインエコシステム内の条件付きロジックのコード爆発も少なくなります。

既存のウィジェットに関する主な不満は、ドキュメントの不足です。それらの削除を発表するこの投稿は、これらの特定のメソッドが存在し、それらをどのように使用するかについての、私がこれまで見た中で最も明確なドキュメントの1つです。実際、古いAPIの使用方法を理解しようとしてここにたどり着きました。

トランスフォーマーが文字列リテラルで一元登録されているのは良いと思います。これにより、ドキュメント(およびプラグイン開発)の作業が大幅に楽になります。

ウィジェットでは、すべてcreateWidgetメソッドで登録され、その後createWidgetFromが呼び出されるようです。これに関する問題は、レジストリがAPIによって保護されたファイルスコープのグローバル変数であり、APIは反復処理を許可しないことです。ウィジェットレジストリに反復関数があれば、現在登録されているウィジェットをリアルタイムで検出できます。「ブラウザコンソールでこのJavaScript行を実行してAPIを呼び出し、レジストリを一覧表示する」と文書化されるべきです。そうすれば、トランスフォーマーレジストリが提供するものと非常に似たユーティリティを得ることができます。

プラグイン開発に役立つもう1つのことは、コンポーネント/ウィジェットによってレンダリングされたルートDOM要素に、どのコンポーネント/ウィジェットであるかを示す属性があることです。「data-widget=foo」のようなものです。これはデバッグ機能になることも、デフォルトで有効になることもあります。OSSなので、隠蔽によってセキュリティを確保しているわけではありません。

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 すごい、素晴らしい投稿、本当に役立ちます!

別の質問ですが、「context」が api.registerValueTransformer を呼び出す際に共有されることについて、どのようなコンテキストが渡されるかを知る方法はありますか?コンテキストをコンソールログに出力することはできますが、事前に何が利用可能かを知ることができれば便利です。

現在書いているプラグインでは、トピックの作成者に特別なモデレーション権限を与えています。これを行うには、「現在のトピックの作成者」、「現在のユーザー」、「ログインしているユーザーのグループメンバーシップ」を知る必要があります。

この具体的な例が、私の質問のコンテキストを理解するのに役立つかもしれません。

編集:
同様の興味を持つ他の人のために、最終的なコードは次のようになります。

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 を使用するのが最善の方法です。

トランスフォーマーに関する一般的なドキュメントは、こちらで見つけることができます:Using Transformers to customize client-side values and behavior

「いいね!」 1

「applyTransformer」を検索しても結果が0件でした。(Code search results · GitHub) 検索場所が間違っていますか?

「api.registerValueTransformer」を検索すると、参考になる例が見つかりました。(Code search results · GitHub) しかし、例ではコンテキストの包括的なドキュメントは提供されておらず、その特定の例で役立ったものしか示されていません。

私の特定の例で contextconsole.log すると、post は返されますが user は返されません。では、コンテキストに含まれていない他のアプリケーション状態にどのようにアクセスできますか?

以前は api.decorateWidget コンテキスト内で helper.getModel()helper.currentUser を呼び出すことができたと理解しています。同様の結果を得るための現在の方法があると仮定しています。

ご協力ありがとうございます。

ああ、自分で質問に答えたようです。この例ではapi.getCurrentUser()の使用が示されています。したがって、APIのその部分は実際には変更されておらず、Glimmerパラダイムと互換性があるままです。

彼が意図したのは applyValueTransformer または applyBehaviorTransformer だと思います。そのような関数は、次のファイルで見つけることができます: discourse/app/assets/javascripts/discourse/app/lib/transformer.js at main · discourse/discourse · GitHub

「いいね!」 4

2件の投稿が新しいトピックに分割されました:「ルートテンプレートに.gjsを使用できますか?」[/t/can-we-use-gjs-for-route-templates/357046]

レガシー投稿メニューコードは削除されました。テーマとプラグインの更新にご協力いただいた皆様、ありがとうございました :rocket:

「いいね!」 6