diff --git a/app/assets/javascripts/discourse/app/lib/plugin-api.js b/app/assets/javascripts/discourse/app/lib/plugin-api.js
index fa3e255c498..ff69a5da8a7 100644
--- a/app/assets/javascripts/discourse/app/lib/plugin-api.js
+++ b/app/assets/javascripts/discourse/app/lib/plugin-api.js
@@ -8,6 +8,7 @@ import {
addButton,
apiExtraButtons,
removeButton,
+ replaceButton,
} from "discourse/widgets/post-menu";
import {
addExtraIconRenderer,
@@ -129,7 +130,7 @@ import { _addBulkButton } from "discourse/components/modal/topic-bulk-actions";
// based on Semantic Versioning 2.0.0. Please update the changelog at
// docs/CHANGELOG-JAVASCRIPT-PLUGIN-API.md whenever you change the version
// using the format described at https://keepachangelog.com/en/1.0.0/.
-export const PLUGIN_API_VERSION = "1.8.0";
+export const PLUGIN_API_VERSION = "1.8.1";
// This helper prevents us from applying the same `modifyClass` over and over in test mode.
function canModify(klass, type, resolverName, changes) {
@@ -656,6 +657,26 @@ class PluginApi {
removeButton(name, callback);
}
+ /**
+ * Replace an existing button with a widget
+ *
+ * Example:
+ * ```
+ * api.replacePostMenuButton("like", {
+ * name: "widget-name",
+ * buildAttrs: (widget) => {
+ * return { post: widget.findAncestorModel() };
+ * },
+ * shouldRender: (widget) => {
+ * const post = widget.findAncestorModel();
+ * return post.id === 1
+ * }
+ * });
+ **/
+ replacePostMenuButton(name, widget) {
+ replaceButton(name, widget);
+ }
+
/**
* A hook that is called when the editor toolbar is created. You can
* use this to add custom editor buttons.
diff --git a/app/assets/javascripts/discourse/app/widgets/post-menu.js b/app/assets/javascripts/discourse/app/widgets/post-menu.js
index 48d2da21cae..97570e78d16 100644
--- a/app/assets/javascripts/discourse/app/widgets/post-menu.js
+++ b/app/assets/javascripts/discourse/app/widgets/post-menu.js
@@ -20,6 +20,7 @@ const _builders = {};
export let apiExtraButtons = {};
let _extraButtons = {};
let _buttonsToRemoveCallbacks = {};
+let _buttonsToReplace = {};
export function addButton(name, builder) {
_extraButtons[name] = builder;
@@ -32,6 +33,7 @@ export function resetPostMenuExtraButtons() {
_extraButtons = {};
_buttonsToRemoveCallbacks = {};
+ _buttonsToReplace = {};
}
export function removeButton(name, callback) {
@@ -40,6 +42,10 @@ export function removeButton(name, callback) {
_buttonsToRemoveCallbacks[name].push(callback || (() => true));
}
+export function replaceButton(name, replaceWith) {
+ _buttonsToReplace[name] = replaceWith;
+}
+
function registerButton(name, builder) {
_builders[name] = builder;
}
@@ -47,17 +53,28 @@ function registerButton(name, builder) {
export function buildButton(name, widget) {
let { attrs, state, siteSettings, settings, currentUser } = widget;
- let shouldAddButton = true;
-
- if (_buttonsToRemoveCallbacks[name]) {
- shouldAddButton = !_buttonsToRemoveCallbacks[name].some((c) =>
+ // Return early if the button is supposed to be removed via the plugin API
+ if (
+ _buttonsToRemoveCallbacks[name] &&
+ _buttonsToRemoveCallbacks[name].some((c) =>
c(attrs, state, siteSettings, settings, currentUser)
- );
+ )
+ ) {
+ return;
+ }
+
+ // Look for a button replacement, build and return widget attrs if present
+ let replacement = _buttonsToReplace[name];
+ if (replacement && replacement?.shouldRender(widget)) {
+ return {
+ replaced: true,
+ name: replacement.name,
+ attrs: replacement.buildAttrs(widget),
+ };
}
let builder = _builders[name];
-
- if (shouldAddButton && builder) {
+ if (builder) {
let button = builder(attrs, state, siteSettings, settings, currentUser);
if (button && !button.id) {
button.id = name;
@@ -438,7 +455,7 @@ registerButton("delete", (attrs) => {
}
});
-function replaceButton(buttons, find, replace) {
+function _replaceButton(buttons, find, replace) {
const idx = buttons.indexOf(find);
if (idx !== -1) {
buttons[idx] = replace;
@@ -468,6 +485,13 @@ export default createWidget("post-menu", {
attachButton(name) {
let buttonAtts = buildButton(name, this);
+
+ // If the button is replaced via the plugin API, we need to render the
+ // replacement rather than a button
+ if (buttonAtts?.replaced) {
+ return this.attach(buttonAtts.name, buttonAtts.attrs);
+ }
+
if (buttonAtts) {
let button = this.attach(this.settings.buttonType, buttonAtts);
if (buttonAtts.before) {
@@ -509,8 +533,8 @@ export default createWidget("post-menu", {
// If the post is a wiki, make Edit more prominent
if (attrs.wiki && attrs.canEdit) {
- replaceButton(orderedButtons, "edit", "reply-small");
- replaceButton(orderedButtons, "reply", "wiki-edit");
+ _replaceButton(orderedButtons, "edit", "reply-small");
+ _replaceButton(orderedButtons, "reply", "wiki-edit");
}
orderedButtons.forEach((i) => {
diff --git a/app/assets/javascripts/discourse/tests/integration/components/widgets/post-menu-test.js b/app/assets/javascripts/discourse/tests/integration/components/widgets/post-menu-test.js
index 60f26df3f64..a4439503134 100644
--- a/app/assets/javascripts/discourse/tests/integration/components/widgets/post-menu-test.js
+++ b/app/assets/javascripts/discourse/tests/integration/components/widgets/post-menu-test.js
@@ -5,6 +5,8 @@ import { count, exists } from "discourse/tests/helpers/qunit-helpers";
import { hbs } from "ember-cli-htmlbars";
import { resetPostMenuExtraButtons } from "discourse/widgets/post-menu";
import { withPluginApi } from "discourse/lib/plugin-api";
+import { createWidget } from "discourse/widgets/widget";
+import { h } from "virtual-dom";
module("Integration | Component | Widget | post-menu", function (hooks) {
setupRenderingTest(hooks);
@@ -88,4 +90,51 @@ module("Integration | Component | Widget | post-menu", function (hooks) {
assert.ok(!exists(".actions .reply"), "it removes reply button");
});
+
+ createWidget("post-menu-replacement", {
+ html(attrs) {
+ return h("h1.post-menu-replacement", {}, attrs.id);
+ },
+ });
+
+ test("buttons are replaced when shouldRender is true", async function (assert) {
+ this.set("args", { id: 1, canCreatePost: true });
+
+ withPluginApi("0.14.0", (api) => {
+ api.replacePostMenuButton("reply", {
+ name: "post-menu-replacement",
+ buildAttrs: (widget) => {
+ return widget.attrs;
+ },
+ shouldRender: (widget) => widget.attrs.id === 1, // true!
+ });
+ });
+
+ await render(hbs``);
+
+ assert.ok(exists("h1.post-menu-replacement"), "replacement is rendered");
+ assert.ok(!exists(".actions .reply"), "reply button is replaced button");
+ });
+
+ test("buttons are not replaced when shouldRender is false", async function (assert) {
+ this.set("args", { id: 1, canCreatePost: true, canRemoveReply: false });
+
+ withPluginApi("0.14.0", (api) => {
+ api.replacePostMenuButton("reply", {
+ name: "post-menu-replacement",
+ buildAttrs: (widget) => {
+ return widget.attrs;
+ },
+ shouldRender: (widget) => widget.attrs.id === 102323948, // false!
+ });
+ });
+
+ await render(hbs``);
+
+ assert.ok(
+ !exists("h1.post-menu-replacement"),
+ "replacement is not rendered"
+ );
+ assert.ok(exists(".actions .reply"), "reply button is present");
+ });
});
diff --git a/docs/CHANGELOG-JAVASCRIPT-PLUGIN-API.md b/docs/CHANGELOG-JAVASCRIPT-PLUGIN-API.md
index 700cf004bb6..520221ff914 100644
--- a/docs/CHANGELOG-JAVASCRIPT-PLUGIN-API.md
+++ b/docs/CHANGELOG-JAVASCRIPT-PLUGIN-API.md
@@ -7,6 +7,20 @@ in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
+## [1.8.1] - 2023-08-08
+
+### Added
+
+- Adds `replacePostMenuButton` which allows plugins to replace a post menu button with a widget.
+
+## [1.8.0] - 2023-07-18
+
+### Added
+- Adds `addSidebarPanel` which is experimental, and adds a Sidebar panel by returning a class which extends from the
+ BaseCustomSidebarPanel class.
+
+- Adds `setSidebarPanel` which is experimental, and sets the current sidebar panel.
+
## [1.7.1] - 2023-07-18
### Added