تغييرات قائمة المشاركات القادمة - كيف تستعد للثيمات والإضافات

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 افتراضيًا.

بمجرد ترقية مثيل Discourse الخاص بك، سيؤدي هذا إلى عدم تطبيق التخصيصات الحالية التي لم يتم تحديثها إلى واجهة برمجة التطبيقات الجديدة.

حتى ذلك الحين، لا يزال بإمكان المسؤولين تغيير الإعداد مرة أخرى إلى “معطل” أثناء تحديث التخصيصات المتبقية.

ستتم إزالة التعليمات البرمجية القديمة في بداية الربع الثاني.

إعجابَين (2)

أقدر المرونة في توفر واجهة برمجة تطبيقات المكونات الكاملة. تعجبني بنية مكونات Glimmer بشكل عام، وأرى لماذا قد يكون لها فوائد في تقليل التعقيد داخل قاعدة التعليمات البرمجية.

ومع ذلك، بالنسبة لحالات الاستخدام الأساسية (أريد إضافة زر وإعطائه أيقونة)، كانت طرق واجهة برمجة التطبيقات القديمة أكثر إيجازًا وسهولة في الفهم بشكل موضوعي (أقل استيرادًا، عمليات أقل، بصمة واجهة برمجة تطبيقات أقل مكشوفة). هل هناك أي سبب لعدم استمرار دعم طرق واجهة برمجة التطبيقات القديمة؟ أتخيل أنه إذا استخدمتها كدوال مساعدة وقمت بتنفيذ التنفيذ الأساسي باستخدام مكون Glimmer الجديد الخاص بك، فيمكن لطريقة واجهة برمجة التطبيقات أيضًا إجراء فحص توافق الإصدار.

سيكون هذا أقل تعطيلًا بكثير لأي شخص يستخدم هذه الطرق، وسيؤدي إلى انفجار أقل في التعليمات البرمجية للمنطق الشرطي داخل نظام المكونات الإضافية.

شكواي الرئيسية بشأن الأدوات المصغرة الحالية هي افتقارها إلى التوثيق. هذه المشاركة، التي تعلن عن إزالتها، هي واحدة من أوضح المستندات التي رأيتها والتي تشير إلى وجود هذه الطرق المحددة وكيفية استخدامها. لقد جئت إلى هنا في الواقع، محاولًا معرفة كيفية استخدام واجهة برمجة التطبيقات القديمة.

أحب أن يتم تسجيل المحولات في مكان واحد، بواسطة ثوابت السلسلة النصية. أعتقد أن هذا يجعل مهمة التوثيق (وتطوير المكونات الإضافية) أسهل بكثير.

مع الأدوات المصغرة، يبدو أنها كلها مسجلة بواسطة طريقة createWidget، والتي بدورها تستدعي createWidgetFrom. المشكلة التي أراها في هذا هي أن _السجل هو متغير عام على نطاق الملف محمي بواسطة واجهة برمجة تطبيقات، ولا تسمح واجهة برمجة التطبيقات بأي تكرار. إذا كان بإمكاننا الحصول على دالة تكرار لسجل الأدوات المصغرة، فيمكننا اكتشاف الأدوات المصغرة المسجلة حاليًا في الوقت الفعلي. يجب توثيقها “تشغيل هذا السطر من جافاسكريبت في وحدة تحكم المتصفح الخاص بك لاستدعاء واجهة برمجة التطبيقات وسرد السجل”. عندها يمكننا الحصول على فائدة مشابهة جدًا لما يوفره سجل المحولات.

شيء آخر من شأنه أن يساعد في تطوير المكونات الإضافية هو رؤية سمة على أي عنصر DOM جذري يتم عرضه بواسطة مكون/أداة مصغرة تخبرك ما هو المكون/الأداة المصغرة التي تنظر إليها. مثل “data-widget=foo” يمكن أن تكون هذه ميزة تصحيح أخطاء، أو يمكن تمكينها افتراضيًا. إنها مفتوحة المصدر، لذا لا يبدو أنك تحقق الأمان من خلال الغموض.

أحتفي بالتحول نحو مكونات Glimmer. لكن هذا سيستغرق وقتًا، وهناك الكثير من الأدوات المصغرة التي يحتاج الناس إلى العمل معها في هذه الأثناء. لذلك أعتقد أن تحسين رؤية الأدوات المصغرة كما هو مذكور أعلاه من شأنه أن يجعل فترة الانتقال أسهل للجميع.

بالنسبة لطرق واجهة برمجة التطبيقات تلك… يبدو أن شخصًا ما بذل جهدًا لإضافة تعليقات مفصلة إلى واجهة برمجة تطبيقات جافاسكريبت، ولكن لم يتم إنشاء أي موقع توثيق لها على الإطلاق. لماذا لا؟

سأكون سعيدًا بتقديم طلب سحب للتكرار عبر سجل الأدوات المصغرة، إذا كان ذلك مقبولًا.

3 إعجابات

أيضًا، إذا أردت تنفيذ الوظيفة الجديدة فقط، إلى أي إصدار من واجهة برمجة تطبيقات Discourse يجب أن أُثبت توافق الإضافة الخاصة بي؟ أنت تستخدم 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? أعتقد أنه يجب أن يحل المشكلة التي تصفها، وسيعرض حتى مكونات Ember التي لا تعرض حاليًا أي عناصر DOM.

منذ وقت قريب جدًا، لم يعد هذا الرقم الإصدار مطلوبًا. يمكنك القيام بذلك

export default apiInitializer((api) => {

أو

withPluginApi((api) => {

سنقوم بتحديث المستندات بهذا التغيير قريبًا. التفضيل الحديث لإدارة التوافق بين الإصدارات هو ملف .discourse-compatibility الذي ذكره @Arkshine:

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.

يمكن العثور على وثائق عامة حول المحولات هنا: Using Transformers to customize client-side values and behavior

إعجاب واحد (1)

البحث عن “applyTransformer” يُرجع 0 نتائج. هل أبحث في المكان الخطأ؟

أجد أن البحث عن “api.registerValueTransformer” يُرجع بعض الأمثلة المفيدة. ولكن بالطبع الأمثلة لا توفر توثيقًا شاملاً للسياق الذي يتم إرجاعه - فهي تُظهر فقط تلك التي كانت مفيدة لهذا المثال المحدد.

من console.log لـ context في مثالي المحدد، أرى أن post يتم إرجاعه ولكن user لا يتم إرجاعه. فكيف يمكنني الوصول إلى حالة التطبيق الأخرى التي لا توجد ضمن السياق؟

أتفهم أنه في السابق ربما كان يمكن استدعاء helper.getModel() أو helper.currentUser ضمن سياق api.decorateWidget. أفترض أن هناك طريقة حالية للحصول على نتائج مماثلة.

شكرًا على كل المساعدة هنا.

أوه، أعتقد أنني أجبت على سؤالي بنفسي. هذا المثال يوضح استخدام api.getCurrentUser(). لذا، بشكل أساسي، لم يتغير هذا الجزء من واجهة برمجة التطبيقات، ولا يزال متوافقًا مع نموذج الـ glimmer.

أعتقد أنه كان يقصد applyValueTransformer أو applyBehaviorTransformer. يمكنك العثور على مثل هذه الدوال في الملف التالي: discourse/app/assets/javascripts/discourse/app/lib/transformer.js at main · discourse/discourse · GitHub

4 إعجابات

تم تقسيم منشورين إلى موضوع جديد: هل يمكننا استخدام .gjs لقوالب المسار؟

تمت إزالة رمز قائمة المشاركات القديمة الآن. شكرًا لكل من عمل على تحديث السمات والإضافات الخاصة بهم :rocket:

6 إعجابات