Upcoming post menu changes - How to prepare themes and plugins

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

  • 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 Likes

We’ve switched the Glimmer Post Menu to enabled by default.

Once your Discourse instance is upgraded, this will cause existing customizations that weren’t updated to the new API to not be applied.

For now, admins can still change the setting back to disabled while getting the remaining customizations updated.

The legacy code will be removed at the beginning of Q2.

2 Likes

I appreciate the flexibility of having the full component API available. I like the syntax of glimmer components overall, and I see why it may have benefits in reducing complexity within the codebase.

However, for basic use cases (I want to add a button and give it an icon), the old API methods were objectively more terse and easy to understand (less imports, less operations, less API footprint exposed). Is there any reason why the old API methods could not enjoy continued support? I’m imagining if you used them as convenience functions and performed the underlying implementation using your new glimmer component, then the API method could also perform the version compatibility check.

This would be far less disruptive to anyone using these methods, and would create less of a code explosion of conditional logic within the plugin ecosystem.

My main complaint about the existing widgets is their lack of documentation. This post, announcing their removal, is one of the clearest documents I have seen that these particular methods exist and how to use them. I came here in fact, trying to figure out how to use the old API.

I like that transformers are registered in one place, by string literals. I think that makes the documentation (and plugin development) job a whole lot easier.

With widgets, they all seem to be registered by the createWidget method, which then calls createWidgetFrom. The issue I see with this is that the _registry is a file scope global variable protected by an API, and the API doesn’t allow any iteration. If we could just get an iteration function on the widget registry, then we could discover in real time the currently registered widgets. It should be documented “run this line of javascript in your browser console to call the API and list the registry”. Then we could get a very similar utility to what the transformer registry is providing.

Another thing that would help in plugin development is to see an attribute on any root DOM element rendered by a component/widget which tells you what component/widget you are looking at. Like “data-widget=foo” This could be a debug feature, or it could just be enabled by default. It’s OSS, so it’s not like you’re achieving security through obscurity.

I celebrate the shift towards glimmer components. But this will take time, and there’s a lot of widgets that people need to work with in the meantime. So I think improving the visibility of widgets as mentioned above would likely make the transition period easier for everyone.

As for those API methods… It looks like someone went to the trouble to add detailed comments to the javascript API, but no documentation site was ever generated for it. Why not?

I’d be happy to submit a pull request for iterating through the widget registry, if that’s agreeable.

3 Likes

Also, if I want to only implement the new functionality, what Discourse API version should I peg my plugin compatibility to? You use withPluginApi("1.34.0", ...) in all of your examples. I think this is an older version and not representative of when this change was made? But please clarify. Thank you!

2 Likes

It’s the correct version. You can look at the changelog here:

Also, this feature can help: Pinning plugin and theme versions for older Discourse installs (.discourse-compatibility)

1 Like

The removal of widgets has been underway for a couple of years, and we hope to finalize it in the next few months. So I don’t think we’ll be making any changes to the underlying system before then.

Have you tried the Ember Inspector? I think it should solve the problem you describe, and will even show Ember components that aren’t currently rendering any DOM elements.

As of very recently, this version number is not required. You can do

export default apiInitializer((api) => {

or

withPluginApi((api) => {

We’ll be getting the docs updated with that change soon. The modern preference for managing version intercompatibility is the .discourse-compatibility file which @Arkshine mentioned:

4 Likes

This is actually really nice! Every time I forget to update that number :rofl:.

1 Like

@david @Arkshine wow, great posts, really helpful!

Another question - regarding the “context” that is shared when calling api.registerValueTransformer, how can I find out what context is going to get passed to me? I suppose I can just console.log the context, but it would be nice to know before hand what is available.

For the plugin I’m writing now I am giving special moderation privileges to the author of a topic. In order to do this I need to know the “current topic author” and “current user” and “group membership of the logged in user”.

Maybe that specific example helps give context for my question.

1 Like

I’m afraid we don’t currently have any central documentation for these. Some areas of the app have their own specific docs e.g. the topic list, which list the context arguments. But otherwise, the best bet is to search for the applyTransformer call in core’s codebase, or use console.log.

General docs on transformers can be found here: Using Transformers to customize client-side values and behavior

1 Like

Searching “applyTransformer” returns 0 results. Am I looking in the wrong place?

I do find that searching for “api.registerValueTransformer” returns some helpful examples. But of course the examples don’t provide comprehensive documentation of the context being returned - they only show the ones that were useful for that particular example.

From console.log of context in my specific example, I see that post is being returned but user is not. So how can I get access to other application state which is not contained within the context?

I understand that previously one might have called helper.getModel() or helper.currentUser within an api.decorateWidget context. I assume there’s some present method for getting similar results.

Thanks for all the help here.

Oh, I think I answered my own question. This example shows use of api.getCurrentUser(). So essentially that portion of the API hasn’t changed, and is still compatible with the glimmer paradigm.

I believe he meant applyValueTransformer or applyBehaviorTransformer. Such functions you can find in the following file: discourse/app/assets/javascripts/discourse/app/lib/transformer.js at main · discourse/discourse · GitHub

4 Likes