From 6405159484af54ac37a9cce962c5d43c4b09634b Mon Sep 17 00:00:00 2001 From: Joffrey JAFFEUX Date: Thu, 13 Feb 2020 23:44:34 +0100 Subject: [PATCH] FEATURE: adds a new plugin api to decorate plugin outlets (#8937) ``` api.decoratePluginOutlet( "discovery-list-container-top", elem => { if (elem.classList.contains("foo")) { elem.style.backgroundColor = "yellow"; } } ); ``` --- .../components/plugin-connector.js.es6 | 26 ++++++++ .../discourse/lib/plugin-api.js.es6 | 26 +++++++- .../discourse/lib/plugin-connectors.js.es6 | 1 + .../plugin-outlet-decorator-test.js.es6 | 61 +++++++++++++++++++ test/javascripts/helpers/qunit-helpers.js.es6 | 2 + 5 files changed, 115 insertions(+), 1 deletion(-) create mode 100644 test/javascripts/acceptance/plugin-outlet-decorator-test.js.es6 diff --git a/app/assets/javascripts/discourse/components/plugin-connector.js.es6 b/app/assets/javascripts/discourse/components/plugin-connector.js.es6 index 4fbb83e30e2..b89e242763b 100644 --- a/app/assets/javascripts/discourse/components/plugin-connector.js.es6 +++ b/app/assets/javascripts/discourse/components/plugin-connector.js.es6 @@ -2,6 +2,19 @@ import Component from "@ember/component"; import { defineProperty, computed } from "@ember/object"; import deprecated from "discourse-common/lib/deprecated"; import { buildArgsWithDeprecations } from "discourse/lib/plugin-connectors"; +import { afterRender } from "discourse-common/utils/decorators"; + +let _decorators = {}; + +// Don't call this directly: use `plugin-api/decoratePluginOutlet` +export function addPluginOutletDecorator(outletName, callback) { + _decorators[outletName] = _decorators[outletName] || []; + _decorators[outletName].push(callback); +} + +export function resetDecorators() { + _decorators = {}; +} export default Component.extend({ init() { @@ -45,6 +58,19 @@ export default Component.extend({ connectorClass.setupComponent.call(this, merged, this); }, + didReceiveAttrs() { + this._super(...arguments); + + this._decoratePluginOutlets(); + }, + + @afterRender + _decoratePluginOutlets() { + (_decorators[this.connector.outletName] || []).forEach(dec => + dec(this.element, this.args) + ); + }, + willDestroyElement() { this._super(...arguments); diff --git a/app/assets/javascripts/discourse/lib/plugin-api.js.es6 b/app/assets/javascripts/discourse/lib/plugin-api.js.es6 index 09ebe31f43e..2fcedd76b99 100644 --- a/app/assets/javascripts/discourse/lib/plugin-api.js.es6 +++ b/app/assets/javascripts/discourse/lib/plugin-api.js.es6 @@ -1,6 +1,7 @@ import deprecated from "discourse-common/lib/deprecated"; import { iconNode } from "discourse-common/lib/icon-library"; import { addDecorator } from "discourse/widgets/post-cooked"; +import { addPluginOutletDecorator } from "discourse/components/plugin-connector"; import ComposerEditor from "discourse/components/composer-editor"; import DiscourseBanner from "discourse/components/discourse-banner"; import { addButton } from "discourse/widgets/post-menu"; @@ -51,7 +52,7 @@ import Composer from "discourse/models/composer"; import { on } from "@ember/object/evented"; // If you add any methods to the API ensure you bump up this number -const PLUGIN_API_VERSION = "0.8.37"; +const PLUGIN_API_VERSION = "0.8.38"; class PluginApi { constructor(version, container) { @@ -983,6 +984,29 @@ class PluginApi { addGlobalNotice(id, text, options) { addGlobalNotice(id, text, options); } + + /** + * Used for decorating the rendered HTML content of a plugin-outlet after it's been rendered + * + * `callback` will be called when it is time to decorate it. + * + * For example, to add a yellow background to a connector: + * + * ``` + * api.decoratePluginOutlet( + * "discovery-list-container-top", + * (elem, args) => { + * if (elem.classList.contains("foo")) { + * elem.style.backgroundColor = "yellow"; + * } + * } + * ); + * ``` + * + **/ + decoratePluginOutlet(outletName, callback, opts) { + addPluginOutletDecorator(outletName, callback, opts || {}); + } } let _pluginv01; diff --git a/app/assets/javascripts/discourse/lib/plugin-connectors.js.es6 b/app/assets/javascripts/discourse/lib/plugin-connectors.js.es6 index 20c356e99d9..8c6efa86a2e 100644 --- a/app/assets/javascripts/discourse/lib/plugin-connectors.js.es6 +++ b/app/assets/javascripts/discourse/lib/plugin-connectors.js.es6 @@ -73,6 +73,7 @@ function buildConnectorCache() { _connectorCache[outletName] = _connectorCache[outletName] || []; _connectorCache[outletName].push({ + outletName, templateName: resource.replace("javascripts/", ""), template: Ember.TEMPLATES[resource], classNames: `${outletName}-outlet ${uniqueName}`, diff --git a/test/javascripts/acceptance/plugin-outlet-decorator-test.js.es6 b/test/javascripts/acceptance/plugin-outlet-decorator-test.js.es6 new file mode 100644 index 00000000000..38600b6e43d --- /dev/null +++ b/test/javascripts/acceptance/plugin-outlet-decorator-test.js.es6 @@ -0,0 +1,61 @@ +import { acceptance } from "helpers/qunit-helpers"; +import { withPluginApi } from "discourse/lib/plugin-api"; + +const PREFIX = "javascripts/single-test/connectors"; +acceptance("Plugin Outlet - Decorator", { + loggedIn: true, + + beforeEach() { + Ember.TEMPLATES[ + `${PREFIX}/discovery-list-container-top/foo` + ] = Ember.HTMLBars.compile("FOO"); + Ember.TEMPLATES[ + `${PREFIX}/discovery-list-container-top/bar` + ] = Ember.HTMLBars.compile("BAR"); + + withPluginApi("0.8.38", api => { + api.decoratePluginOutlet( + "discovery-list-container-top", + (elem, args) => { + if (elem.classList.contains("foo")) { + elem.style.backgroundColor = "yellow"; + + if (args.category) { + elem.classList.add("in-category"); + } else { + elem.classList.remove("in-category"); + } + } + }, + { id: "yellow-decorator" } + ); + }); + }, + + afterEach() { + delete Ember.TEMPLATES[`${PREFIX}/discovery-list-container-top/foo`]; + delete Ember.TEMPLATES[`${PREFIX}/discovery-list-container-top/bar`]; + } +}); + +QUnit.test( + "Calls the plugin callback with the rendered outlet", + async assert => { + await visit("/"); + + const fooConnector = find(".discovery-list-container-top-outlet.foo ")[0]; + const barConnector = find(".discovery-list-container-top-outlet.bar ")[0]; + + assert.ok(exists(fooConnector)); + assert.equal(fooConnector.style.backgroundColor, "yellow"); + assert.equal(barConnector.style.backgroundColor, ""); + + await visit("/c/bug"); + + assert.ok(fooConnector.classList.contains("in-category")); + + await visit("/"); + + assert.notOk(fooConnector.classList.contains("in-category")); + } +); diff --git a/test/javascripts/helpers/qunit-helpers.js.es6 b/test/javascripts/helpers/qunit-helpers.js.es6 index 25dcb2a3d8d..34d92149b7c 100644 --- a/test/javascripts/helpers/qunit-helpers.js.es6 +++ b/test/javascripts/helpers/qunit-helpers.js.es6 @@ -18,6 +18,7 @@ import { initSearchData } from "discourse/widgets/search-menu"; import { resetDecorators } from "discourse/widgets/widget"; import { resetWidgetCleanCallbacks } from "discourse/components/mount-widget"; import { resetDecorators as resetPostCookedDecorators } from "discourse/widgets/post-cooked"; +import { resetDecorators as resetPluginOutletDecorators } from "discourse/components/plugin-connector"; import { resetCache as resetOneboxCache } from "pretty-text/oneboxer"; import { resetCustomPostMessageCallbacks } from "discourse/controllers/topic"; import User from "discourse/models/user"; @@ -128,6 +129,7 @@ export function acceptance(name, options) { initSearchData(); resetDecorators(); resetPostCookedDecorators(); + resetPluginOutletDecorators(); resetOneboxCache(); resetCustomPostMessageCallbacks(); Discourse._runInitializer("instanceInitializers", function(