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

Discourse コードベースの継続的な改善の一環として、レガシーな「ウィジェット」レンダリングシステムの使用を廃止し、Glimmer コンポーネントに置き換えています。

最近、投稿メニューをモダン化し、glimmer_post_menu_mode 設定で Discourse 内で利用可能になりました。

この設定では、以下の 3 つの値を受け付けます。

  • disabled: レガシーな「ウィジェット」システムを使用
  • auto: 現在のプラグインとテーマの互換性を検出します。互換性がないものが含まれている場合はレガシーシステムを使用し、そうでない場合は新しいメニューを使用します。
  • enabled: 新しいメニューを使用します。互換性がないプラグインやテーマがある場合、サイトが破損する可能性があります。

公式プラグインはすでに新しいメニューと互換性があるように更新されていますが、まだ新しいメニューと互換性がないサードパーティ製プラグイン、テーマ、またはテーマコンポーネントがある場合は、それらのアップグレードが必要です。

ブラウザのコンソールには、互換性の問題の原因を特定する警告が出力されます。

:timer_clock: 展開スケジュール

これらは変更される可能性がある概算です

2024 年第 4 四半期:

  • :white_check_mark: コア実装完了
  • :white_check_mark: 公式プラグインの更新
  • :white_check_mark: Meta での有効化
  • :white_check_mark: glimmer_post_menu_mode のデフォルトを auto に設定、コンソールでの非推奨メッセージを有効化
  • :white_check_mark: アップグレードに関するアドバイスの公開

2025 年第 1 四半期:

  • :white_check_mark: サードパーティ製プラグインとテーマの更新完了
  • :white_check_mark: 非推奨メッセージの開始により、残存する問題に対して管理者警告バナーが表示される
  • :white_check_mark: 新しい投稿メニューのデフォルト有効化

2025 年第 2 四半期

  • :white_check_mark: 4 月 1 日 - 機能フラグ設定とレガシーコードの削除

:eyes: これは私にとって何を意味しますか?

プラグインやテーマが投稿メニューのカスタマイズに「ウィジェット」API を使用している場合、それらは新しいバージョンとの互換性のために更新する必要があります。

:person_tipping_hand: 新しい投稿メニューを試すには?

Discourse の最新バージョンでは、互換性のないプラグインやテーマがない場合、新しい投稿メニューが自動的に有効になります。

互換性のない拡張機能がインストールされている場合、管理者として設定を enabled に変更して、新しいメニューを強制的に使用することもできます。ただし、インストールされているカスタマイズによってはサイトが破損する可能性があるため、この設定は注意して使用してください。

稀なケースですが、この自動システムが期待通りに動作しない場合は、上記の設定を使用して一時的にこの「自動機能フラグ」をオーバーライドできます。その必要がある場合は、このトピックでお知らせください。

:technologist: プラグインやテーマを更新する必要がありますか?

以下のいずれかのカスタマイズを行っている場合、プラグインやテーマの更新が必要です。

  • 以下のウィジェットに対して decorateWidgetchangeWidgetSettingreopenWidget、または attachWidgetAction を使用する:

    • post-menu
    • post-user-tip-shim
    • small-user-list
  • 以下の API メソッドのいずれかを使用する:

    • addPostMenuButton
    • removePostMenuButton
    • replacePostMenuButton

:bulb: 上記のカスタマイズを実行する拡張機能がある場合、トピックページにアクセスすると、アップグレードが必要なプラグインやコンポーネントを特定する警告がコンソールに出力されます。

非推奨 ID は discourse.post-menu-widget-overrides です。

:warning: インスタンスで複数のテーマを使用している場合は、警告がアクティブなプラグインと現在使用されているテーマおよびテーマコンポーネントに対してのみ出力されるため、すべてのテーマを確認してください。

代替手段は何か?

投稿メニューのカスタマイズ用の新しい API として、値トランスフォーマー post-menu-buttons を導入しました。

この値トランスフォーマーは DAG オブジェクトを提供し、ボタンの追加、置換、削除、または並べ替えを可能にします。また、メニューに関連する投稿、表示されている投稿の状態、アイテムの配置を容易にするためのボタンキーなどのコンテキスト情報も提供します。

DAG API は、.add.replace のように新しいボタン定義が必要な場合、Ember コンポーネントを受け取ることを想定しています。

各カスタマイズは異なりますが、最も一般的なユースケースに対するガイダンスを以下に示します。

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

以後:

以下の例では、Ember の テンプレートタグ形式 (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 {
  // ボタンが即座に表示されるか、「もっと表示」ボタンの背後に隠されるかを示す
  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, // 2 番目に隠されたボタンのキー
        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) {
  // レガシーな「ウィジェット」コードのカスタマイズ
  ...
}

export default {
  name: "my-plugin",

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

:star: さらに多くの例

新しい API の使用方法の例については、公式プラグインを確認してください。

Glimmer Post Menu をデフォルトで enabled に切り替えました。

Discourse インスタンスがアップグレードされると、新しい API に更新されなかった既存のカスタマイズは適用されなくなります。

現時点では、管理者は残りのカスタマイズが更新される間、設定を disabled に戻すことができます。

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

コンポーネント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に追加する手間をかけたようですが、ドキュメントサイトは生成されませんでした。なぜですか?

もしよろしければ、ウィジェットレジストリの反復処理のためのプルリクエストを提出したいと思います。

また、新機能だけを実装したい場合、プラグインの互換性をどのDiscourse APIバージョンに設定すればよいですか?あなたはすべての例で 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をお試しになりましたか?これは、あなたが説明している問題を解決するはずであり、現在DOM要素をレンダリングしていないEmberコンポーネントも表示します。

ごく最近、このバージョン番号は不要になりました。以下のようにできます。

export default apiInitializer((api) => {

または

withPluginApi((api) => {

まもなく、その変更をドキュメントに反映させる予定です。バージョン間の互換性を管理するための最新の推奨事項は、@Arkshineが言及した.discourse-compatibilityファイルです。

これは本当に素晴らしいですね!毎回その番号を更新するのを忘れてしまいます :rofl:

@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) {
  // 機能へのアクセスを許可する
  ...
}

残念ながら、現在これらのための中心的なドキュメントはありません。アプリの一部の領域には、独自のドキュメントがあります(例:トピックリスト)。そこにはコンテキスト引数が記載されています。それ以外の場合は、コアのコードベースで applyTransformer の呼び出しを検索するか、console.log を使用するのが最善の方法です。

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

「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

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