From beea6ce03917bad62be6f9a3794aa3b944962604 Mon Sep 17 00:00:00 2001 From: Bastian Allgeier Date: Wed, 5 Apr 2023 16:06:34 +0200 Subject: [PATCH 1/4] Notification module --- panel/src/panel/module.js | 102 ++++++++++++ panel/src/panel/module.test.js | 68 ++++++++ panel/src/panel/notification.js | 229 +++++++++++++++++++++++++++ panel/src/panel/notification.test.js | 115 ++++++++++++++ panel/src/panel/timer.js | 38 +++++ 5 files changed, 552 insertions(+) create mode 100644 panel/src/panel/module.js create mode 100644 panel/src/panel/module.test.js create mode 100644 panel/src/panel/notification.js create mode 100644 panel/src/panel/notification.test.js create mode 100644 panel/src/panel/timer.js diff --git a/panel/src/panel/module.js b/panel/src/panel/module.js new file mode 100644 index 0000000000..e5be9df32d --- /dev/null +++ b/panel/src/panel/module.js @@ -0,0 +1,102 @@ +import { isObject } from "@/helpers/object"; + +/** + * Panel modules represent a particular part of state + * for the panel. I.e. $system, $translation. + * Features are built upon such state modules + * + * The inheritance cascade is: + * Module -> Feature -> Island + * + * @param {Object} panel The panel singleton + * @param {String} key Sets the $key for the module. Backend responses use this key. + * @param {Object} defaults Sets the default state of the module + */ +export default (key, defaults = {}) => { + return { + /** + * Module defaults will be reactive and + * must be present immediately in the object + * to get reactivity out of the box. + */ + ...defaults, + + /** + * The key is used to place the module + * state in the right place within the global + * panel state + * + * @returns {String} + */ + key() { + return key; + }, + + /** + * Returns all default values. + * This will be used to restore the state + * and fetch the existing state. + * + * @returns {Object} + */ + defaults() { + return defaults; + }, + + /** + * Restores the default state + */ + reset() { + return this.set(this.defaults()); + }, + + /** + * Sets a new state for the module + * + * @param {Object} state + */ + set(state) { + this.validateState(state); + + // merge the new state with the defaults + // to always get a full object with all props + for (const prop in this.defaults()) { + this[prop] = state[prop] ?? this.defaults()[prop]; + } + + return this.state(); + }, + + /** + * Returns the current state. The defaults + * object is used to fetch all keys from the object + * Keys which are not defined in the defaults + * object will also not be in the final state + * + * @returns {Object} + */ + state() { + const state = {}; + + for (const prop in this.defaults()) { + state[prop] = this[prop] ?? this.defaults()[prop]; + } + + return state; + }, + + /** + * Validates the state object + * + * @param {Object} state + * @returns {Boolean} + */ + validateState(state) { + if (isObject(state) === false) { + throw new Error(`Invalid ${this.key()} state`); + } + + return true; + } + }; +}; diff --git a/panel/src/panel/module.test.js b/panel/src/panel/module.test.js new file mode 100644 index 0000000000..9ac1baa019 --- /dev/null +++ b/panel/src/panel/module.test.js @@ -0,0 +1,68 @@ +/** + * @vitest-environment node + */ + +import { describe, expect, it } from "vitest"; +import Module from "./module.js"; + +describe.concurrent("module", () => { + it("should set & get a key", async () => { + const mod = Module("$test"); + expect(mod.key()).toStrictEqual("$test"); + }); + + it("should set & get defaults", async () => { + const defaults = { + message: null + }; + + const mod = Module("$test", defaults); + + expect(mod.defaults()).toStrictEqual(defaults); + expect(mod.state()).toStrictEqual(defaults); + expect(mod.message).toStrictEqual(null); + }); + + it("should set new state", async () => { + const defaults = { + message: null, + isOpen: false + }; + + const mod = Module("$test", defaults); + + mod.set({ message: "Hello" }); + + expect(mod.state()).toStrictEqual({ message: "Hello", isOpen: false }); + expect(mod.message).toStrictEqual("Hello"); + expect(mod.isOpen).toStrictEqual(false); + }); + + it("should set default state", async () => { + const defaults = { + message: null + }; + + const mod = Module("$test", defaults); + + mod.set({ message: "Hello" }); + mod.reset(); + + expect(mod.message).toStrictEqual(null); + }); + + it("should validate state", async () => { + const mod = Module("$test"); + + // invalid state + try { + mod.validateState("foo"); + } catch (error) { + expect(error.message).toStrictEqual("Invalid $test state"); + } + + // valid state + const validation = mod.validateState({ message: "Yay" }); + expect(validation).toStrictEqual(true); + }); +}); diff --git a/panel/src/panel/notification.js b/panel/src/panel/notification.js new file mode 100644 index 0000000000..247463a040 --- /dev/null +++ b/panel/src/panel/notification.js @@ -0,0 +1,229 @@ +import JsonRequestError from "@/errors/JsonRequestError.js"; +import RequestError from "@/errors/RequestError.js"; +import Module from "./module.js"; +import Timer from "./timer.js"; + +export const defaults = () => { + return { + isOpen: false, + message: null, + timeout: null, + type: null + }; +}; + +export default (panel) => { + const parent = Module("$notification", defaults()); + + return { + ...parent, + + /** + * Closes the notification by + * setting the inactive state (defaults) + * + * @returns {Object} The inactive state + */ + close() { + // stop any previous timers + this.timer.stop(); + + // reset the defaults + this.reset(); + + // return the closed state + return this.state(); + }, + + /** + * Checks where it should be displayed. + * When a drawer or dialog is open, it's + * displayed there instead of the topbar + * + * @returns {String} dialog|drawer|view + */ + get context() { + if (panel.dialog.isOpen) { + return "dialog"; + } + + if (panel.drawer.isOpen) { + return "drawer"; + } + + return "view"; + }, + + /** + * Sends a deprecation warning to the console + * + * @param {String} message + */ + deprecated(message) { + console.warn("Deprecated: " + message); + }, + + /** + * Converts an error object or string + * into an error notification + * + * @param {Error|Object|String} error + * @returns {Object} The notification state + */ + error(error) { + if (error instanceof JsonRequestError) { + return this.fatal(error); + } + + if (error instanceof RequestError) { + // get the broken element in the response json that + // has an error message. This can be deprecated later + // when the server always sends back a simple error + // response without nesting it in $dropdown, $dialog, etc. + const broken = Object.values(error.response.json).find( + (element) => typeof element.error === "string" + ); + + if (broken) { + error.message = broken.error; + } + } + + if (typeof error === "string") { + return this.open({ + message: error, + type: "error" + }); + } + + if (panel.debug) { + console.error(error); + } + + return this.open({ + message: error.message || "Something went wrong", + type: "error" + }); + }, + + /** + * Getter that converts the notification type + * into the matching icon + * + * @returns {String} + */ + get icon() { + return this.type === "success" ? "check" : "alert"; + }, + + /** + * Checks if the notification is a fatal + * error. Those are displayed in the + * component which sends them to an isolated + * iframe. This will happen when API responses + * cannot be parsed at all. + */ + get isFatal() { + return this.type === "fatal"; + }, + + /** + * Creates a fatal error based on an + * Error object or string + * + * @param {Error|Object|String} error + * @returns {Object} The notification state + */ + fatal(error) { + if (typeof error === "string") { + return this.open({ + message: error, + type: "fatal" + }); + } + + if (error instanceof JsonRequestError) { + return this.open({ + message: error.response.text, + type: "fatal" + }); + } + + return this.open({ + message: error.message || "Something went wrong", + type: "fatal" + }); + }, + + /** + * Opens the notification + * The context will determine where it will + * be shown. + * + * @param {Error|Object|String} notification + * @returns {Object} The notification state + */ + open(notification) { + // stop any previous timers + this.timer.stop(); + + // simple success notifications + if (typeof notification === "string") { + return this.success(notification); + } + + // set the new state + this.set(notification); + + // open the notification + this.isOpen = true; + + // start a timer to auto-close the notification + this.timer.start(this.timeout, () => { + this.close(); + }); + + // returns the new open state + return this.state(); + }, + + /** + * Shortcut to create a success + * notification. You can pass a simple + * string or a state object. + * + * @param {Object|String} success + * @returns {Object} The notification state + */ + success(success) { + if (!success) { + success = {}; + } + + if (typeof success === "string") { + success = { message: success }; + } + + return this.open({ + timeout: 4000, + type: "success", + ...success + }); + }, + + /** + * Getter that converts the notification type + * into the matching notification component theme + * + * @returns {String} + */ + get theme() { + return this.type === "error" ? "negative" : "positive"; + }, + + /** + * Holds the timer object + */ + timer: Timer + }; +}; diff --git a/panel/src/panel/notification.test.js b/panel/src/panel/notification.test.js new file mode 100644 index 0000000000..2a6c0a186f --- /dev/null +++ b/panel/src/panel/notification.test.js @@ -0,0 +1,115 @@ +/** + * @vitest-environment node + */ + +import { describe, expect, it } from "vitest"; +import Notification from "./notification.js"; + +describe.concurrent("panel.notification", () => { + it("should have a default state", async () => { + const notification = Notification(); + + const state = { + isOpen: false, + message: null, + timeout: null, + type: null + }; + + expect(notification.state()).toStrictEqual(state); + }); + + it("should open & close", async () => { + const notification = Notification(); + + notification.open({ + message: "Hello world" + }); + + expect(notification.message).toStrictEqual("Hello world"); + expect(notification.isOpen).toStrictEqual(true); + + notification.close(); + + expect(notification.isOpen).toStrictEqual(false); + }); + + it("should return the correct context", async () => { + const panel = { + dialog: {}, + drawer: {} + }; + + const notification = Notification(panel); + + expect(notification.context).toStrictEqual("view"); + + panel.drawer.isOpen = true; + + expect(notification.context).toStrictEqual("drawer"); + + panel.dialog.isOpen = true; + + expect(notification.context).toStrictEqual("dialog"); + }); + + it("should return the right icon", async () => { + const notification = Notification(); + + notification.success("Test"); + + expect(notification.icon).toStrictEqual("check"); + + notification.error("Test"); + + expect(notification.icon).toStrictEqual("alert"); + }); + + it("should return the right theme", async () => { + const notification = Notification(); + + notification.success("Test"); + + expect(notification.theme).toStrictEqual("positive"); + + notification.error("Test"); + + expect(notification.theme).toStrictEqual("negative"); + }); + + it("should set a timer for success notifications", async () => { + const notification = Notification(); + + notification.success("Test"); + expect(notification.timeout).toStrictEqual(4000); + }); + + it("should not set a timer for error notifications", async () => { + const notification = Notification(); + + notification.error("Test"); + expect(notification.timeout).toStrictEqual(null); + }); + + it("should reset the timer when closing notifications", async () => { + const notification = Notification(); + + notification.success("Test"); + expect(notification.timer.interval).toBeTypeOf("object"); + + notification.close(); + expect(notification.timer.interval).toStrictEqual(null); + }); + + it("should convert Error objects", async () => { + const notification = Notification({ + // switch off debugging to avoid console output + debug: false + }); + + notification.error(new Error("test")); + + expect(notification.type).toStrictEqual("error"); + expect(notification.message).toStrictEqual("test"); + }); +}); diff --git a/panel/src/panel/timer.js b/panel/src/panel/timer.js new file mode 100644 index 0000000000..19578e1a58 --- /dev/null +++ b/panel/src/panel/timer.js @@ -0,0 +1,38 @@ +/** + * Simple timer implementation to + * help with starting and stopping timers + * + * @example + * timer.start(100, () => {}); + * timer.stop(); + */ +export default { + interval: null, + + /** + * Starts the timer if a timeout is defined + * + * @param {Integer} timeout + * @param {Function} callback + */ + start(timeout, callback) { + // stop any previous timers + this.stop(); + + // don't set a new one without a timeout + if (!timeout) { + return; + } + + // set a new timer + this.interval = setInterval(callback, timeout); + }, + + /** + * Stops the timer + */ + stop() { + clearInterval(this.interval); + this.interval = null; + } +}; From 0eaf1606f0c6df12e7968670fd3adba524e9b081 Mon Sep 17 00:00:00 2001 From: Bastian Allgeier Date: Wed, 5 Apr 2023 17:13:04 +0200 Subject: [PATCH 2/4] Refactor store calls --- panel/src/components/Dialogs/Dialog.vue | 2 +- .../components/Forms/Field/StructureField.vue | 2 +- panel/src/components/Forms/FormButtons.vue | 6 +- panel/src/components/Forms/LoginCode.vue | 2 +- panel/src/components/Layout/Panel.vue | 5 +- panel/src/components/Misc/Fatal.vue | 2 +- panel/src/components/Navigation/Topbar.vue | 28 +++-- .../src/components/Sections/FilesSection.vue | 6 +- .../src/components/Sections/PagesSection.vue | 4 +- panel/src/components/Views/FileView.vue | 2 +- .../src/components/Views/InstallationView.vue | 4 +- .../components/Views/ResetPasswordView.vue | 2 +- panel/src/components/Views/UserView.vue | 4 +- panel/src/config/api.js | 4 +- panel/src/config/errorhandling.js | 8 +- panel/src/fiber/app.js | 7 +- panel/src/index.js | 17 ++- panel/src/panel/notification.js | 22 +++- panel/src/panel/notification.test.js | 5 +- panel/src/store/modules/notification.js | 103 ++++-------------- panel/src/store/store.js | 28 +---- 21 files changed, 109 insertions(+), 154 deletions(-) diff --git a/panel/src/components/Dialogs/Dialog.vue b/panel/src/components/Dialogs/Dialog.vue index 85b2594b24..be132be221 100644 --- a/panel/src/components/Dialogs/Dialog.vue +++ b/panel/src/components/Dialogs/Dialog.vue @@ -199,7 +199,7 @@ export default { // send a global success notification if (success.message) { - this.$store.dispatch("notification/success", success.message); + this.$panel.notification.success(success.message); } // dispatch store actions that might have been defined in diff --git a/panel/src/components/Forms/Field/StructureField.vue b/panel/src/components/Forms/Field/StructureField.vue index 1651b06271..dbdbea886d 100644 --- a/panel/src/components/Forms/Field/StructureField.vue +++ b/panel/src/components/Forms/Field/StructureField.vue @@ -542,7 +542,7 @@ export default { return true; } catch (errors) { - this.$store.dispatch("notification/error", { + this.$panel.notification.error({ message: this.$t("error.form.incomplete"), details: errors }); diff --git a/panel/src/components/Forms/FormButtons.vue b/panel/src/components/Forms/FormButtons.vue index a6232868ca..89b33869cb 100644 --- a/panel/src/components/Forms/FormButtons.vue +++ b/panel/src/components/Forms/FormButtons.vue @@ -234,19 +234,19 @@ export default { try { await this.$store.dispatch("content/save"); this.$events.$emit("model.update"); - this.$store.dispatch("notification/success", ":)"); + this.$panel.notification.success(); } catch (response) { if (response.code === 403) { return; } if (this.$helper.object.length(response.details) > 0) { - this.$store.dispatch("notification/error", { + this.$panel.notification.error({ message: this.$t("error.form.incomplete"), details: response.details }); } else { - this.$store.dispatch("notification/error", { + this.$panel.notification.error({ message: this.$t("error.form.notSaved"), details: [ { diff --git a/panel/src/components/Forms/LoginCode.vue b/panel/src/components/Forms/LoginCode.vue index 190a6275ed..89ae1c15ee 100644 --- a/panel/src/components/Forms/LoginCode.vue +++ b/panel/src/components/Forms/LoginCode.vue @@ -67,7 +67,7 @@ export default { try { await this.$api.auth.verifyCode(this.code); - this.$store.dispatch("notification/success", this.$t("welcome")); + this.$panel.notification.success(this.$t("welcome")); if (this.mode === "password-reset") { this.$go("reset-password"); diff --git a/panel/src/components/Layout/Panel.vue b/panel/src/components/Layout/Panel.vue index 3b59ff973b..687f717ef9 100644 --- a/panel/src/components/Layout/Panel.vue +++ b/panel/src/components/Layout/Panel.vue @@ -18,7 +18,10 @@ - + diff --git a/panel/src/components/Misc/Fatal.vue b/panel/src/components/Misc/Fatal.vue index dd048fbf41..38b25fdd29 100644 --- a/panel/src/components/Misc/Fatal.vue +++ b/panel/src/components/Misc/Fatal.vue @@ -9,7 +9,7 @@ diff --git a/panel/src/components/Navigation/Topbar.vue b/panel/src/components/Navigation/Topbar.vue index 66df0eaeec..e22e3c16da 100644 --- a/panel/src/components/Navigation/Topbar.vue +++ b/panel/src/components/Navigation/Topbar.vue @@ -30,11 +30,12 @@
@@ -70,13 +71,20 @@ export default { }, computed: { notification() { - if ( - this.$store.state.notification.type && - this.$store.state.notification.type !== "error" - ) { - return this.$store.state.notification; - } else { - return null; + return this.$panel.notification.context === "view" + ? this.$panel.notification + : null; + } + }, + watch: { + notification(notification) { + // send the notification to the dialog instead + // of the topbar. + if (notification.type === "error") { + this.$dialog({ + component: "k-error-dialog", + props: notification.state() + }); } } } diff --git a/panel/src/components/Sections/FilesSection.vue b/panel/src/components/Sections/FilesSection.vue index 0675bdd4ca..fad805e8ba 100644 --- a/panel/src/components/Sections/FilesSection.vue +++ b/panel/src/components/Sections/FilesSection.vue @@ -86,11 +86,11 @@ export default { files: items.map((item) => item.id), index: this.pagination.offset }); - this.$store.dispatch("notification/success", ":)"); + this.$panel.notification.success(); this.$events.$emit("file.sort"); } catch (error) { this.reload(); - this.$store.dispatch("notification/error", error.message); + this.$panel.notification.error(error.message); } finally { this.isProcessing = false; } @@ -98,7 +98,7 @@ export default { onUpload() { this.$events.$emit("file.create"); this.$events.$emit("model.update"); - this.$store.dispatch("notification/success", ":)"); + this.$panel.notification.success(); }, replace(file) { this.$refs.upload.open({ diff --git a/panel/src/components/Sections/PagesSection.vue b/panel/src/components/Sections/PagesSection.vue index 2b50bec90b..9eae95a20f 100644 --- a/panel/src/components/Sections/PagesSection.vue +++ b/panel/src/components/Sections/PagesSection.vue @@ -82,10 +82,10 @@ export default { try { await this.$api.pages.changeStatus(element.id, "listed", position); - this.$store.dispatch("notification/success", ":)"); + this.$panel.notification.success(); this.$events.$emit("page.sort", element); } catch (error) { - this.$store.dispatch("notification/error", { + this.$panel.notification.error({ message: error.message, details: error.details }); diff --git a/panel/src/components/Views/FileView.vue b/panel/src/components/Views/FileView.vue index e5ff7730e5..b21271da4a 100644 --- a/panel/src/components/Views/FileView.vue +++ b/panel/src/components/Views/FileView.vue @@ -83,7 +83,7 @@ export default { } }, onUpload() { - this.$store.dispatch("notification/success", ":)"); + this.$panel.notification.success(); this.$reload(); } } diff --git a/panel/src/components/Views/InstallationView.vue b/panel/src/components/Views/InstallationView.vue index 334e461095..910edb468a 100644 --- a/panel/src/components/Views/InstallationView.vue +++ b/panel/src/components/Views/InstallationView.vue @@ -152,9 +152,9 @@ export default { globals: ["$system", "$translation"] }); - this.$store.dispatch("notification/success", this.$t("welcome") + "!"); + this.$panel.notification.success(this.$t("welcome") + "!"); } catch (error) { - this.$store.dispatch("notification/error", error); + this.$panel.notification.error(error); } } } diff --git a/panel/src/components/Views/ResetPasswordView.vue b/panel/src/components/Views/ResetPasswordView.vue index 835f5a9c8c..f55a121094 100644 --- a/panel/src/components/Views/ResetPasswordView.vue +++ b/panel/src/components/Views/ResetPasswordView.vue @@ -87,7 +87,7 @@ export default { this.values.password ); - this.$store.dispatch("notification/success", ":)"); + this.$panel.notification.success(); this.$go("/"); } catch (error) { this.issue = error.message; diff --git a/panel/src/components/Views/UserView.vue b/panel/src/components/Views/UserView.vue index 3d2c527eb5..7d17ec7c95 100644 --- a/panel/src/components/Views/UserView.vue +++ b/panel/src/components/Views/UserView.vue @@ -139,7 +139,7 @@ export default { async deleteAvatar() { await this.$api.users.deleteAvatar(this.model.id); this.avatar = null; - this.$store.dispatch("notification/success", ":)"); + this.$panel.notification.success(); this.$reload(); }, onAvatar() { @@ -150,7 +150,7 @@ export default { } }, uploadedAvatar() { - this.$store.dispatch("notification/success", ":)"); + this.$panel.notification.success(); this.$reload(); } } diff --git a/panel/src/config/api.js b/panel/src/config/api.js index e5fe5b5553..d091b4a8e7 100644 --- a/panel/src/config/api.js +++ b/panel/src/config/api.js @@ -29,8 +29,8 @@ export default { window.console.error(error); } }, - onParserError: ({ html, silent }) => { - store.dispatch("fatal", { html, silent }); + onParserError: ({ html }) => { + window.panel.notification.fatal(html); }, onPrepare: (options) => { // if language set, add to headers diff --git a/panel/src/config/errorhandling.js b/panel/src/config/errorhandling.js index 6737bc3e0d..d1344930e2 100644 --- a/panel/src/config/errorhandling.js +++ b/panel/src/config/errorhandling.js @@ -1,5 +1,3 @@ -import store from "@/store/store.js"; - export default { install(app) { window.panel = window.panel || {}; @@ -12,12 +10,12 @@ export default { */ window.onunhandledrejection = (event) => { event.preventDefault(); - store.dispatch("notification/error", event.reason); + window.panel.notification.error(event.reason); }; // global deprecation handler window.panel.deprecated = (message) => { - store.dispatch("notification/deprecated", message); + window.panel.notification.deprecated(message); }; /** @@ -26,7 +24,7 @@ export default { * @param {Error} error */ window.panel.error = app.config.errorHandler = (error) => { - store.dispatch("notification/error", error); + window.panel.notification.error(error); }; } }; diff --git a/panel/src/fiber/app.js b/panel/src/fiber/app.js index 657c0cea54..49477ac291 100644 --- a/panel/src/fiber/app.js +++ b/panel/src/fiber/app.js @@ -29,11 +29,8 @@ export default { * * @param {object} */ - onFatal({ text, options }) { - this.$store.dispatch("fatal", { - html: text, - silent: options.silent - }); + onFatal({ text }) { + window.panel.notification.fatal(text); }, /** * Is being called when a Fiber request diff --git a/panel/src/index.js b/panel/src/index.js index c00aa8fc69..5df24c9576 100644 --- a/panel/src/index.js +++ b/panel/src/index.js @@ -1,4 +1,4 @@ -import Vue, { h } from "vue"; +import Vue, { h, reactive } from "vue"; import Api from "./config/api.js"; import App from "./fiber/app.js"; @@ -9,6 +9,7 @@ import Fiber from "./fiber/plugin.js"; import Helpers from "./helpers/index.js"; import I18n from "./config/i18n.js"; import Libraries from "./libraries/index.js"; +import Notification from "./panel/notification.js"; import Plugins from "./config/plugins.js"; import store from "./store/store.js"; import Vuelidate from "vuelidate"; @@ -19,9 +20,21 @@ Vue.config.devtools = true; const app = new Vue({ store, created() { - window.panel.$vue = window.panel.app = this; window.panel.plugins.created.forEach((plugin) => plugin(this)); + window.panel.$vue = window.panel.app = this; this.$store.dispatch("content/init"); + + /** + * This is temporary panel setup + * code until the entire panel.js class is there + */ + window.panel.notification = Notification({ + debug: window.panel.$config.debug + }); + + reactive(window.panel.notification); + + Vue.prototype.$panel = window.panel; }, render: () => h(App) }); diff --git a/panel/src/panel/notification.js b/panel/src/panel/notification.js index 247463a040..a9213e43b9 100644 --- a/panel/src/panel/notification.js +++ b/panel/src/panel/notification.js @@ -5,6 +5,7 @@ import Timer from "./timer.js"; export const defaults = () => { return { + details: null, isOpen: false, message: null, timeout: null, @@ -12,7 +13,7 @@ export const defaults = () => { }; }; -export default (panel) => { +export default (panel = {}) => { const parent = Module("$notification", defaults()); return { @@ -40,14 +41,26 @@ export default (panel) => { * When a drawer or dialog is open, it's * displayed there instead of the topbar * - * @returns {String} dialog|drawer|view + * @returns {false|String} false|dialog|drawer|fatal|error|view */ get context() { - if (panel.dialog.isOpen) { + // no notifications, no context + if (this.isOpen === false) { + return false; + } + + // show notifications in the fatal overlay + if (this.type === "fatal") { + return "fatal"; + } + + // show notifications in the dialog + if (panel.dialog?.isOpen) { return "dialog"; } - if (panel.drawer.isOpen) { + // show notifications in the drawer + if (panel.drawer?.isOpen) { return "drawer"; } @@ -102,6 +115,7 @@ export default (panel) => { return this.open({ message: error.message || "Something went wrong", + details: error.details || {}, type: "error" }); }, diff --git a/panel/src/panel/notification.test.js b/panel/src/panel/notification.test.js index 2a6c0a186f..c04a673bf4 100644 --- a/panel/src/panel/notification.test.js +++ b/panel/src/panel/notification.test.js @@ -102,10 +102,7 @@ describe.concurrent("panel.notification", () => { }); it("should convert Error objects", async () => { - const notification = Notification({ - // switch off debugging to avoid console output - debug: false - }); + const notification = Notification(); notification.error(new Error("test")); diff --git a/panel/src/store/modules/notification.js b/panel/src/store/modules/notification.js index 6171df8acc..fe94149f59 100644 --- a/panel/src/store/modules/notification.js +++ b/panel/src/store/modules/notification.js @@ -1,93 +1,38 @@ +/** + * @deprecated Use window.panel.notification instead + */ export default { - timer: null, namespaced: true, - state: { - type: null, - message: null, - details: null, - timeout: null - }, - mutations: { - SET(state, notification) { - state.type = notification.type; - state.message = notification.message; - state.details = notification.details; - state.timeout = notification.timeout; - }, - UNSET(state) { - state.type = null; - state.message = null; - state.details = null; - state.timeout = null; - } - }, actions: { - close(context) { - clearTimeout(this.timer); - context.commit("UNSET"); + /** + * @deprecated Use window.panel.notification.close() instead + */ + close() { + window.panel.notification.close(); }, + /** + * @deprecated Use window.panel.notification.deprecated() instead + */ deprecated(context, message) { - console.warn("Deprecated: " + message); + window.panel.notification.deprecated(message); }, + /** + * @deprecated Use window.panel.notification.error() instead + */ error(context, error) { - // props for the dialog - let props = error; - - // handle when a simple string is thrown as error - // we should avoid that whenever possible - if (typeof error === "string") { - props = { - message: error - }; - } - - // handle proper Error instances - if (error instanceof Error) { - // convert error objects to props for the dialog - props = { - message: error.message - }; - - // only log errors to the console in debug mode - if (window.panel.$config.debug) { - window.console.error(error); - } - } - - // show the error dialog - context.dispatch( - "dialog", - { - component: "k-error-dialog", - props: props - }, - { root: true } - ); - - // remove the notification from store - // to avoid showing it in the topbar - context.dispatch("close"); + window.panel.notification.error(error); }, + /** + * @deprecated Use window.panel.notification.open() instead + */ open(context, payload) { - context.dispatch("close"); - context.commit("SET", payload); - - if (payload.timeout) { - this.timer = setTimeout(() => { - context.dispatch("close"); - }, payload.timeout); - } + window.panel.notification.open(payload); }, + /** + * @deprecated Use window.panel.notification.success() instead + */ success(context, payload) { - if (typeof payload === "string") { - payload = { message: payload }; - } - - context.dispatch("open", { - type: "success", - timeout: 4000, - ...payload - }); + window.panel.notification.success(payload); } } }; diff --git a/panel/src/store/store.js b/panel/src/store/store.js index 3e27ca1736..bf4900e0b5 100644 --- a/panel/src/store/store.js +++ b/panel/src/store/store.js @@ -14,7 +14,6 @@ export default new Vuex.Store({ state: { dialog: null, drag: null, - fatal: false, isLoading: false }, mutations: { @@ -24,9 +23,6 @@ export default new Vuex.Store({ SET_DRAG(state, drag) { state.drag = drag; }, - SET_FATAL(state, html) { - state.fatal = html; - }, SET_LOADING(state, loading) { state.isLoading = loading; } @@ -38,27 +34,11 @@ export default new Vuex.Store({ drag(context, drag) { context.commit("SET_DRAG", drag); }, + /** + * @deprecated Use window.panel.notification.fatal() + */ fatal(context, options) { - // close the fatal window if false - // is passed as options - if (options === false) { - context.commit("SET_FATAL", false); - return; - } - - console.error("The JSON response could not be parsed"); - - // show the full response in the console - // if debug mode is enabled - if (window.panel.$config.debug) { - console.info(options.html); - } - - // only show the fatal dialog if the silent - // option is not set to true - if (!options.silent) { - context.commit("SET_FATAL", options.html); - } + window.panel.notification.fatal(options); }, isLoading(context, loading) { context.commit("SET_LOADING", loading === true); From 50bf749a5ac44f76ce2ec7da676cfd9fd36d2b04 Mon Sep 17 00:00:00 2001 From: Bastian Allgeier Date: Wed, 5 Apr 2023 18:10:40 +0200 Subject: [PATCH 3/4] Remove unnecessary error logging and fix unit tests --- panel/src/panel/notification.js | 4 ---- panel/src/panel/notification.test.js | 5 +++++ 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/panel/src/panel/notification.js b/panel/src/panel/notification.js index a9213e43b9..98a3daf14a 100644 --- a/panel/src/panel/notification.js +++ b/panel/src/panel/notification.js @@ -109,10 +109,6 @@ export default (panel = {}) => { }); } - if (panel.debug) { - console.error(error); - } - return this.open({ message: error.message || "Something went wrong", details: error.details || {}, diff --git a/panel/src/panel/notification.test.js b/panel/src/panel/notification.test.js index c04a673bf4..bb0b126177 100644 --- a/panel/src/panel/notification.test.js +++ b/panel/src/panel/notification.test.js @@ -10,6 +10,7 @@ describe.concurrent("panel.notification", () => { const notification = Notification(); const state = { + details: null, isOpen: false, message: null, timeout: null, @@ -42,6 +43,10 @@ describe.concurrent("panel.notification", () => { const notification = Notification(panel); + expect(notification.context).toStrictEqual(false); + + notification.isOpen = true; + expect(notification.context).toStrictEqual("view"); panel.drawer.isOpen = true; From 089bd74202cc6731f7d65a56aa67639967147684 Mon Sep 17 00:00:00 2001 From: Bastian Allgeier Date: Wed, 5 Apr 2023 18:13:09 +0200 Subject: [PATCH 4/4] Fixe broken type check --- panel/src/components/Navigation/Topbar.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/panel/src/components/Navigation/Topbar.vue b/panel/src/components/Navigation/Topbar.vue index e22e3c16da..e5d05d9a72 100644 --- a/panel/src/components/Navigation/Topbar.vue +++ b/panel/src/components/Navigation/Topbar.vue @@ -80,7 +80,7 @@ export default { notification(notification) { // send the notification to the dialog instead // of the topbar. - if (notification.type === "error") { + if (notification?.type === "error") { this.$dialog({ component: "k-error-dialog", props: notification.state()