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