diff --git a/app/assets/javascripts/discourse/app/lib/plugin-api.js b/app/assets/javascripts/discourse/app/lib/plugin-api.js index 71a6210d912..35877fa5aa1 100644 --- a/app/assets/javascripts/discourse/app/lib/plugin-api.js +++ b/app/assets/javascripts/discourse/app/lib/plugin-api.js @@ -86,6 +86,25 @@ import { addSearchSuggestion } from "discourse/widgets/search-menu-results"; // If you add any methods to the API ensure you bump up this number const PLUGIN_API_VERSION = "0.12.2"; +// This helper prevents us from applying the same `modifyClass` over and over in test mode. +function canModify(klass, type, resolverName, changes) { + if (!changes.pluginId) { + // eslint-disable-next-line no-console + console.warn( + "To prevent errors, add a `pluginId` key to your changes when calling `modifyClass`" + ); + return true; + } + + let key = "_" + type + "/" + changes.pluginId + "/" + resolverName; + if (klass.class[key]) { + return false; + } else { + klass.class[key] = 1; + return true; + } +} + class PluginApi { constructor(version, container) { this.version = version; @@ -138,10 +157,14 @@ class PluginApi { /** * Allows you to overwrite or extend methods in a class. * + * You should add a `pluginId` property to identify your plugin + * to help Discourse reload classes properly. + * * For example: * * ``` * api.modifyClass('controller:composer', { + * pluginId: 'my-plugin', * actions: { * newActionHere() { } * } @@ -150,9 +173,15 @@ class PluginApi { **/ modifyClass(resolverName, changes, opts) { const klass = this._resolveClass(resolverName, opts); - if (klass) { + if (!klass) { + return; + } + + if (canModify(klass, "member", resolverName, changes)) { + delete changes.pluginId; klass.class.reopen(changes); } + return klass; } @@ -169,9 +198,15 @@ class PluginApi { **/ modifyClassStatic(resolverName, changes, opts) { const klass = this._resolveClass(resolverName, opts); - if (klass) { + if (!klass) { + return; + } + + if (canModify(klass, "static", resolverName, changes)) { + delete changes.pluginId; klass.class.reopenClass(changes); } + return klass; } diff --git a/plugins/discourse-details/assets/javascripts/initializers/apply-details.js.es6 b/plugins/discourse-details/assets/javascripts/initializers/apply-details.js.es6 index a0867791311..11b2117f8f8 100644 --- a/plugins/discourse-details/assets/javascripts/initializers/apply-details.js.es6 +++ b/plugins/discourse-details/assets/javascripts/initializers/apply-details.js.es6 @@ -15,6 +15,7 @@ function initializeDetails(api) { }); api.modifyClass("controller:composer", { + pluginId: "discourse-details", actions: { insertDetails() { this.toolbarEvent.applySurround( diff --git a/plugins/discourse-local-dates/assets/javascripts/initializers/discourse-local-dates.js.es6 b/plugins/discourse-local-dates/assets/javascripts/initializers/discourse-local-dates.js.es6 index 68b94e7aebf..30c1b5c5103 100644 --- a/plugins/discourse-local-dates/assets/javascripts/initializers/discourse-local-dates.js.es6 +++ b/plugins/discourse-local-dates/assets/javascripts/initializers/discourse-local-dates.js.es6 @@ -82,6 +82,7 @@ function initializeDiscourseLocalDates(api) { }); api.modifyClass("component:d-editor", { + pluginId: "discourse-local-dates", actions: { insertDiscourseLocalDate(toolbarEvent) { showModal("discourse-local-dates-create-modal").setProperties({ diff --git a/plugins/discourse-narrative-bot/assets/javascripts/initializers/new-user-narrative.js.es6 b/plugins/discourse-narrative-bot/assets/javascripts/initializers/new-user-narrative.js.es6 index 156c4dd0cd2..89d8c460a1b 100644 --- a/plugins/discourse-narrative-bot/assets/javascripts/initializers/new-user-narrative.js.es6 +++ b/plugins/discourse-narrative-bot/assets/javascripts/initializers/new-user-narrative.js.es6 @@ -1,12 +1,15 @@ import { ajax } from "discourse/lib/ajax"; import { withPluginApi } from "discourse/lib/plugin-api"; +const PLUGIN_ID = "new-user-narrative"; + function initialize(api) { const messageBus = api.container.lookup("message-bus:main"); const currentUser = api.getCurrentUser(); const appEvents = api.container.lookup("service:app-events"); api.modifyClass("component:site-header", { + pluginId: PLUGIN_ID, didInsertElement() { this._super(...arguments); this.dispatch("header:search-context-trigger", "header"); @@ -14,6 +17,8 @@ function initialize(api) { }); api.modifyClass("controller:topic", { + pluginId: PLUGIN_ID, + _togglePostBookmark(post) { // if we are talking to discobot then any bookmarks should just // be created without reminder options, to streamline the new user diff --git a/plugins/poll/assets/javascripts/initializers/add-poll-ui-builder.js.es6 b/plugins/poll/assets/javascripts/initializers/add-poll-ui-builder.js.es6 index abeb3056c6e..ab5c59cfeae 100644 --- a/plugins/poll/assets/javascripts/initializers/add-poll-ui-builder.js.es6 +++ b/plugins/poll/assets/javascripts/initializers/add-poll-ui-builder.js.es6 @@ -4,6 +4,7 @@ import { withPluginApi } from "discourse/lib/plugin-api"; function initializePollUIBuilder(api) { api.modifyClass("controller:composer", { + pluginId: "discourse-poll-ui-builder", @discourseComputed( "siteSettings.poll_enabled", "siteSettings.poll_minimum_trust_level_to_create", diff --git a/plugins/poll/assets/javascripts/initializers/extend-for-poll.js.es6 b/plugins/poll/assets/javascripts/initializers/extend-for-poll.js.es6 index 6d9505704e6..ab0fb09570d 100644 --- a/plugins/poll/assets/javascripts/initializers/extend-for-poll.js.es6 +++ b/plugins/poll/assets/javascripts/initializers/extend-for-poll.js.es6 @@ -4,10 +4,30 @@ import { getRegister } from "discourse-common/lib/get-owner"; import { observes } from "discourse-common/utils/decorators"; import { withPluginApi } from "discourse/lib/plugin-api"; +const PLUGIN_ID = "discourse-poll"; +let _glued = []; +let _interval = null; + +function rerender() { + _glued.forEach((g) => g.queueRerender()); +} + +function cleanUpPolls() { + if (_interval) { + clearInterval(_interval); + _interval = null; + } + + _glued.forEach((g) => g.cleanUp()); + _glued = []; +} + function initializePolls(api) { const register = getRegister(api); + cleanUpPolls(); api.modifyClass("controller:topic", { + pluginId: PLUGIN_ID, subscribe() { this._super(...arguments); this.messageBus.subscribe("/polls/" + this.get("model.id"), (msg) => { @@ -23,14 +43,8 @@ function initializePolls(api) { }, }); - let _glued = []; - let _interval = null; - - function rerender() { - _glued.forEach((g) => g.queueRerender()); - } - api.modifyClass("model:post", { + pluginId: PLUGIN_ID, _polls: null, pollsObject: null, @@ -110,16 +124,6 @@ function initializePolls(api) { }); } - function cleanUpPolls() { - if (_interval) { - clearInterval(_interval); - _interval = null; - } - - _glued.forEach((g) => g.cleanUp()); - _glued = []; - } - api.includePostAttributes("polls", "polls_votes"); api.decorateCooked(attachPolls, { onlyStream: true, id: "discourse-poll" }); api.cleanupStream(cleanUpPolls);