Add a direct button to canned replies in editor

I use the ‘canned replies’ plugin quite often. They’re now 2 clicks deep (gear → canned replies); is there a way to customise my editor toolbar to have direct access, while still respecting user permissions?

1 Like

2 clicks is not really that much work, but this is a good opportunity for a general explanation that documents how to work with composer buttons with a focus on what you’re trying to do.

Adding buttons to the toolbar is done with a different pluginAPI method.

The toolbar uses this method plugin-api.js.es6#L375-L391 while the popup menu uses this one plugin-api.js.es6#L396-L411

The canned replies plugin uses the pop-up menu method like so

https://github.com/discourse/discourse-canned-replies/blob/master/assets/javascripts/initializers/add-canned-replies-ui-builder.js.es6#L18-L25

You cannot move the button unless you fork the plugin - highly not recommended.

What you can do is add another button in the toolbar that does the same thing and hide the old one. In order to create a button in the toolbar, you’d need to look at how some other buttons are added.

There are two types of buttons in the toolbar. The first type is buttons that handle formatting like Bold and Italics. Since they’re not similar to what you want to achieve, let’s ignore those for now.

The other type is buttons insert things like dates and emojis. Let’s look at the date - calendar - button.

https://github.com/discourse/discourse/blob/c322cccd53b432f5d9e467daf7bee5b164f66b4d/plugins/discourse-local-dates/assets/javascripts/initializers/discourse-local-dates.js.es6#L12-L21

We now have an example to fallback to. So let’s try to create the new button.

We start with this

api.onToolbarCreate(toolbar => {
  toolbar.addButton({
  
  });
});

and add the attributes from the canned reply button one by one. For reference those are

id: "canned_replies_button",
icon: "far-clipboard",
action: "showCannedRepliesButton",
label: "canned_replies.composer_button_text"

id: this allows you to add CSS classes to the button - we’ll use custom-canned-button

icon: the icon the button will use - we’ll keep it the same

label: the popup buttons have text while the composer button do not, so we need to change that to title and use the same value.

action: this is where you define what the button does. Let’s put all of that together

api.onToolbarCreate(toolbar => {
  toolbar.addButton({
    id: "custom-canned-replies",
    icon: "far-clipboard",
    action: "showCannedRepliesButton",
    title: "canned_replies.composer_button_text"
  });
});

If you try this, you’ll get a button in the toolbar but clicking it won’t do anything. This happens because the action showCannedRepliesButton is not defined. This happens because of the different context - since you’re doing this in a theme.

If you look at the canned reply plugin, you’ll notice that that action is defined in the composer controller

https://github.com/discourse/discourse-canned-replies/blob/master/assets/javascripts/initializers/add-canned-replies-ui-builder.js.es6#L5-L16

So the next step is for you to reference the composer controller in order to be able to send that action when the button is clicked. You can do that like so

const composerController = api.container.lookup("controller:composer");

api.onToolbarCreate(toolbar => {
  toolbar.addButton({
    title: "canned_replies.composer_button_text",
    id: "custom-canned-replies",
    group: "extras",
    icon: "far-clipboard",
    sendAction: () => composerController.send("showCannedRepliesButton")
  });
});

Notice that we used the same pattern from the calendar button for sendAction. The only two exceptions is instead of this

toolbar.context.send we use composerController.send

and we’re not passing the event as I don’t think that’s needed.

This should give you a fully functional button in the toolbar

but we’re still not done yet as this button is now visible to all members. The usage permissions still apply and if a user who doesn’t have permission tries to click it, they’ll just get an error. However, a broken button is not good so let’s fix that

The permissions for using canned replies are here

https://github.com/discourse/discourse-canned-replies/blob/master/assets/javascripts/initializers/add-canned-replies-ui-builder.js.es6#L34-L38

So we only need to replicate those as conditions for the button being added. So something like this

const currentUser = api.getCurrentUser();
const canUseCannedReplies = currentUser
  ? currentUser.can_use_canned_replies
  : false;

if (!canUseCannedReplies) return;

and this would ensure that the button is only displayed if you have the required permissions.

So, let’s put everything together

import { withPluginApi } from "discourse/lib/plugin-api";

export default {
  name: "move-canned-button",
  initialize() {
    withPluginApi("0.8.7", api => {
      const currentUser = api.getCurrentUser();
      const canUseCannedReplies = currentUser
        ? currentUser.can_use_canned_replies
        : false;

      if (!canUseCannedReplies) return;

      const composerController = api.container.lookup("controller:composer");

      api.onToolbarCreate(toolbar => {
        toolbar.addButton({
          title: "canned_replies.composer_button_text",
          id: "custom-canned-replies",
          group: "extras",
          icon: "far-clipboard",
          sendAction: () => composerController.send("showCannedRepliesButton")
        });
      });
    });
  }
};

This goes into this file in a theme component

javascripts/discourse/initializers/move-canned-button.js.es6

if you’re using the new Splitting up theme Javascript into multiple files feature - highly recomneded

Or you can just add this script to the header tab in your theme if you’re doing this in the admin

Old syntax
<script type="text/discourse-plugin"
        version="0.8">
const currentUser = api.getCurrentUser();
const canUseCannedReplies = currentUser
  ? currentUser.can_use_canned_replies
  : false;

if (!canUseCannedReplies) return;

const composerController = api.container.lookup("controller:composer");

api.onToolbarCreate(toolbar => {
  toolbar.addButton({
    title: "canned_replies.composer_button_text",
    id: "custom-canned-replies",
    group: "extras",
    icon: "far-clipboard",
    sendAction: () => composerController.send("showCannedRepliesButton")
  });
});
</script>

The last thing you’d need is to hide the old button and you can do that like so

.toolbar-popup-menu-options {
  [data-name="Canned replies"] {
    display: none;
  }
}
29 Likes

I did NOT expect such an in depth reply, that’s fantastic! I’ll need some time tonight to digest this, thanks :pray:

15 Likes

Welcome to @johani :tada:

13 Likes