diff --git a/app/assets/javascripts/discourse/app/instance-initializers/boot-client-error-handler.js b/app/assets/javascripts/discourse/app/instance-initializers/boot-client-error-handler.js new file mode 100644 index 00000000000..d9f4480b9f4 --- /dev/null +++ b/app/assets/javascripts/discourse/app/instance-initializers/boot-client-error-handler.js @@ -0,0 +1,5 @@ +export default { + initialize(owner) { + owner.lookup("service:client-error-handler"); + }, +}; diff --git a/app/assets/javascripts/discourse/app/instance-initializers/theme-errors-handler.js b/app/assets/javascripts/discourse/app/services/client-error-handler.js similarity index 77% rename from app/assets/javascripts/discourse/app/instance-initializers/theme-errors-handler.js rename to app/assets/javascripts/discourse/app/services/client-error-handler.js index 9ca286b26da..8575153202d 100644 --- a/app/assets/javascripts/discourse/app/instance-initializers/theme-errors-handler.js +++ b/app/assets/javascripts/discourse/app/services/client-error-handler.js @@ -1,4 +1,3 @@ -import { isTesting } from "discourse-common/config/environment"; import { getAndClearUnhandledThemeErrors } from "discourse/app"; import getURL from "discourse-common/lib/get-url"; import I18n from "I18n"; @@ -9,26 +8,34 @@ import identifySource, { getThemeInfo, } from "discourse/lib/source-identifier"; import Ember from "ember"; +import { disableImplicitInjections } from "discourse/lib/implicit-injections"; +import Service, { inject as service } from "@ember/service"; +import { getOwner } from "@ember/application"; const showingErrors = new Set(); -export default { - initialize(owner) { - if (isTesting()) { - return; - } +@disableImplicitInjections +export default class ClientErrorHandlerService extends Service { + @service currentUser; - this.currentUser = owner.lookup("service:current-user"); + constructor() { + super(...arguments); getAndClearUnhandledThemeErrors().forEach((e) => this.reportThemeError(e)); document.addEventListener("discourse-error", this.handleDiscourseError); - }, + } - teardown() { + get rootElement() { + return document.querySelector(getOwner(this).rootElement); + } + + willDestroy() { document.removeEventListener("discourse-error", this.handleDiscourseError); - delete this.currentUser; - }, + this.rootElement + .querySelectorAll(".broken-theme-alert-banner") + .forEach((e) => e.remove()); + } @bind handleDiscourseError(e) { @@ -39,7 +46,7 @@ export default { } e.preventDefault(); // Mark as handled - }, + } reportThemeError(e) { const { themeId, error } = e.detail; @@ -53,7 +60,7 @@ export default { const message = I18n.t("themes.broken_theme_alert"); this.displayErrorNotice(message, source); - }, + } reportGenericError(e) { const { messageKey, error } = e.detail; @@ -67,14 +74,14 @@ export default { showingErrors.add(messageKey); this.displayErrorNotice(message, source); } - }, + } displayErrorNotice(message, source) { if (!this.currentUser?.admin) { return; } - let html = `⚠️ ${message}`; + let html = `⚠️ ${escape(message)}`; if (source && source.type === "theme") { html += `
${I18n.t("themes.error_caused_by", { @@ -88,11 +95,11 @@ export default { )}`; const alertDiv = document.createElement("div"); - alertDiv.classList.add("broken-theme-alert"); + alertDiv.classList.add("broken-theme-alert-banner"); alertDiv.innerHTML = html; - document.body.prepend(alertDiv); - }, -}; + this.rootElement.prepend(alertDiv); + } +} function reportToLogster(name, error) { const data = { diff --git a/app/assets/stylesheets/common/base/discourse.scss b/app/assets/stylesheets/common/base/discourse.scss index d0616c4467d..80175967e73 100644 --- a/app/assets/stylesheets/common/base/discourse.scss +++ b/app/assets/stylesheets/common/base/discourse.scss @@ -777,7 +777,7 @@ table { } } -.broken-theme-alert { +.broken-theme-alert-banner { font-size: var(--base-font-size); font-weight: bold; padding: 5px 0;