DEV: adds a new topic footer dropdown api (#14747)

DEV: adds a new topic footer dropdown api (#14747)

This api allows to add a dropdown at the bottom of a topic, note that this API is mobile only for now.

Also included in the commit:

  • various doc fixes
  • adding tests for both buttons and dropdowns APIs
  • uses thrown instead of @ember/error to ensure execution is halted when incorrect parameters are given
diff --git a/app/assets/javascripts/discourse/app/components/topic-footer-buttons.js b/app/assets/javascripts/discourse/app/components/topic-footer-buttons.js
index e0167eb..ab1430a 100644
--- a/app/assets/javascripts/discourse/app/components/topic-footer-buttons.js
+++ b/app/assets/javascripts/discourse/app/components/topic-footer-buttons.js
@@ -1,7 +1,9 @@
 import { alias, and, or } from "@ember/object/computed";
+import { computed } from "@ember/object";
 import Component from "@ember/component";
 import discourseComputed from "discourse-common/utils/decorators";
 import { getTopicFooterButtons } from "discourse/lib/register-topic-footer-button";
+import { getTopicFooterDropdowns } from "discourse/lib/register-topic-footer-dropdown";
 
 export default Component.extend({
   elementId: "topic-footer-buttons",
@@ -18,17 +20,25 @@ export default Component.extend({
     return this.siteSettings.enable_personal_messages && isPM;
   },
 
-  buttons: getTopicFooterButtons(),
-
-  @discourseComputed("buttons.[]")
-  inlineButtons(buttons) {
-    return buttons.filter((button) => !button.dropdown);
-  },
+  inlineButtons: getTopicFooterButtons(),
+  inlineDropdowns: getTopicFooterDropdowns(),
+
+  inlineActionables: computed(
+    "inlineButtons.[]",
+    "inlineDropdowns.[]",
+    function () {
+      return this.inlineButtons
+        .filterBy("dropdown", false)
+        .concat(this.inlineDropdowns)
+        .sortBy("priority")
+        .reverse();
+    }
+  ),
 
   // topic.assigned_to_user is for backward plugin support
-  @discourseComputed("buttons.[]", "topic.assigned_to_user")
-  dropdownButtons(buttons) {
-    return buttons.filter((button) => button.dropdown);
+  @discourseComputed("inlineButtons.[]", "topic.assigned_to_user")
+  dropdownButtons(inlineButtons) {
+    return inlineButtons.filter((button) => button.dropdown);
   },
 
   @discourseComputed("topic.isPrivateMessage")
diff --git a/app/assets/javascripts/discourse/app/lib/plugin-api.js b/app/assets/javascripts/discourse/app/lib/plugin-api.js
index d7ded31..8375bd1 100644
--- a/app/assets/javascripts/discourse/app/lib/plugin-api.js
+++ b/app/assets/javascripts/discourse/app/lib/plugin-api.js
@@ -81,6 +81,7 @@ import { registerCustomAvatarHelper } from "discourse/helpers/user-avatar";
 import { registerCustomPostMessageCallback as registerCustomPostMessageCallback1 } from "discourse/controllers/topic";
 import { registerHighlightJSLanguage } from "discourse/lib/highlight-syntax";
 import { registerTopicFooterButton } from "discourse/lib/register-topic-footer-button";
+import { registerTopicFooterDropdown } from "discourse/lib/register-topic-footer-dropdown";
 import { replaceFormatter } from "discourse/lib/utilities";
 import { replaceTagRenderer } from "discourse/lib/render-tag";
 import { setNewCategoryDefaultColors } from "discourse/routes/new-category";
@@ -93,7 +94,7 @@ import { CUSTOM_USER_SEARCH_OPTIONS } from "select-kit/components/user-chooser";
 import { downloadCalendar } from "discourse/lib/download-calendar";
 
 // If you add any methods to the API ensure you bump up this number
-const PLUGIN_API_VERSION = "0.13.0";
+const PLUGIN_API_VERSION = "0.13.1";
 
 // This helper prevents us from applying the same `modifyClass` over and over in test mode.
 function canModify(klass, type, resolverName, changes) {
@@ -735,18 +736,33 @@ class PluginApi {
   }
 
   /**
-   * Register a small icon to be used for custom small post actions
+   * Register a button to display at the bottom of a topic
    *
    * `‍``javascript
    * api.registerTopicFooterButton({
-   *   key: "flag"
-   *   icon: "flag"
-   *   action: (context) => console.log(context.get("topic.id"))
+   *   id: "flag",
+   *   icon: "flag",
+   *   action(context) { console.log(context.get("topic.id")) },
+   * });
+   * `‍``
+   **/
+  registerTopicFooterButton(buttonOptions) {
+    registerTopicFooterButton(buttonOptions);
+  }
+
+  /**
+   * Register a dropdown to display at the bottom of a topic, desktop only
+   *
+   * `‍``javascript
+   * api.registerTopicFooterDropdown({
+   *   id: "my-button",
+   *   content() { return [{id: 1, name: "foo"}] },
+   *   action(itemId) { console.log(itemId) },
    * });
    * `‍``
    **/
-  registerTopicFooterButton(action) {
-    registerTopicFooterButton(action);
+  registerTopicFooterDropdown(dropdownOptions) {
+    registerTopicFooterDropdown(dropdownOptions);
   }
 
   /**
diff --git a/app/assets/javascripts/discourse/app/lib/register-topic-footer-button.js b/app/assets/javascripts/discourse/app/lib/register-topic-footer-button.js
index ec3c019..0ceba7f 100644
--- a/app/assets/javascripts/discourse/app/lib/register-topic-footer-button.js
+++ b/app/assets/javascripts/discourse/app/lib/register-topic-footer-button.js
@@ -1,13 +1,11 @@
 import I18n from "I18n";
 import { computed } from "@ember/object";
-import error from "@ember/error";
 
 let _topicFooterButtons = {};
 
 export function registerTopicFooterButton(button) {
   if (!button.id) {
-    error(`Attempted to register a topic button: ${button} with no id.`);
-    return;
+    throw new Error(`Attempted to register a topic button with no id.`);
   }
 
   if (_topicFooterButtons[button.id]) {
@@ -15,6 +13,8 @@ export function registerTopicFooterButton(button) {
   }
 
   const defaultButton = {
+    type: "inline-button",
+
     // id of the button, required
     id: null,
 
@@ -60,10 +60,9 @@ export function registerTopicFooterButton(button) {
     !normalizedButton.title &&
     !normalizedButton.translatedTitle
   ) {
-    error(
+    throw new Error(
       `Attempted to register a topic button: ${button.id} with no icon or title.`
     );
-    return;
   }
 
   _topicFooterButtons[normalizedButton.id] = normalizedButton;
@@ -94,49 +93,48 @@ export function getTopicFooterButtons() {
       return Object.values(_topicFooterButtons)
         .filter((button) => _compute(button, "displayed"))
         .map((button) => {
-          const discourseComputedButon = {};
+          const discourseComputedButton = {};
 
-          discourseComputedButon.id = button.id;
+          discourseComputedButton.id = button.id;
+          discourseComputedButton.type = button.type;
 
           const label = _compute(button, "label");
-          discourseComputedButon.label = label
+          discourseComputedButton.label = label
             ? I18n.t(label)
             : _compute(button, "translatedLabel");
 
           const ariaLabel = _compute(button, "ariaLabel");
           if (ariaLabel) {
-            discourseComputedButon.ariaLabel = I18n.t(ariaLabel);
+            discourseComputedButton.ariaLabel = I18n.t(ariaLabel);
           } else {
             const translatedAriaLabel = _compute(button, "translatedAriaLabel");
-            discourseComputedButon.ariaLabel =
-              translatedAriaLabel || discourseComputedButon.label;
+            discourseComputedButton.ariaLabel =
+              translatedAriaLabel || discourseComputedButton.label;
           }
 
           const title = _compute(button, "title");
-          discourseComputedButon.title = title
+          discourseComputedButton.title = title
             ? I18n.t(title)
             : _compute(button, "translatedTitle");
 
-          discourseComputedButon.classNames = (
+          discourseComputedButton.classNames = (
             _compute(button, "classNames") || []
           ).join(" ");
 
-          discourseComputedButon.icon = _compute(button, "icon");
-          discourseComputedButon.disabled = _compute(button, "disabled");
-          discourseComputedButon.dropdown = _compute(button, "dropdown");
-          discourseComputedButon.priority = _compute(button, "priority");
+          discourseComputedButton.icon = _compute(button, "icon");
+          discourseComputedButton.disabled = _compute(button, "disabled");
+          discourseComputedButton.dropdown = _compute(button, "dropdown");

[... diff too long, it was truncated ...]

GitHub sha: 362c47ce6a87783808cb4417912a75250f003bf8

This commit appears in #14747 which was approved by eviltrout. It was merged by jjaffeux.