From 670edf3006bd77ba1c7a708b5fd3ac6cd4b62637 Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Fri, 26 Jun 2026 17:43:57 +0300 Subject: [PATCH] Added error handling and handle async component failing --- src/App.js | 2 + src/App.scss | 13 +++ src/App.vue | 1 + src/components/error_modal/error_modal.js | 36 +++++++ src/components/error_modal/error_modal.vue | 100 +++++++++++++++++++ src/components/global_error/global_error.js | 52 ++++++++++ src/components/global_error/global_error.vue | 23 +++++ src/i18n/en.json | 4 + src/stores/interface.js | 25 ++++- 9 files changed, 254 insertions(+), 2 deletions(-) create mode 100644 src/components/error_modal/error_modal.js create mode 100644 src/components/error_modal/error_modal.vue create mode 100644 src/components/global_error/global_error.js create mode 100644 src/components/global_error/global_error.vue diff --git a/src/App.js b/src/App.js index 7de7a0696..934b96713 100644 --- a/src/App.js +++ b/src/App.js @@ -4,6 +4,7 @@ import { defineAsyncComponent } from 'vue' import DesktopNav from 'src/components/desktop_nav/desktop_nav.vue' import FeaturesPanel from 'src/components/features_panel/features_panel.vue' +import GlobalError from 'src/components/global_error/global_error.vue' import GlobalNoticeList from 'src/components/global_notice_list/global_notice_list.vue' import InstanceSpecificPanel from 'src/components/instance_specific_panel/instance_specific_panel.vue' import MobileNav from 'src/components/mobile_nav/mobile_nav.vue' @@ -68,6 +69,7 @@ export default { () => import('src/components/status_history_modal/status_history_modal.vue'), ), + GlobalError, GlobalNoticeList, }, data: () => ({ diff --git a/src/App.scss b/src/App.scss index 9618a2ac4..02db8f531 100644 --- a/src/App.scss +++ b/src/App.scss @@ -144,6 +144,19 @@ h4 { margin: 0; } +code { + background: var(--bg); + border: 1px solid var(--fg); + border-radius: var(--roundness); + margin: 0.2em; + padding: 0 0.2em; + + &.pre { + white-space: pre; + display: block; + } +} + .iconLetter { display: inline-block; text-align: center; diff --git a/src/App.vue b/src/App.vue index c1a7199d7..84a6f7d3d 100644 --- a/src/App.vue +++ b/src/App.vue @@ -73,6 +73,7 @@ + diff --git a/src/components/error_modal/error_modal.js b/src/components/error_modal/error_modal.js new file mode 100644 index 000000000..aaa9f99ec --- /dev/null +++ b/src/components/error_modal/error_modal.js @@ -0,0 +1,36 @@ +import DialogModal from 'src/components/dialog_modal/dialog_modal.vue' + +import { library } from '@fortawesome/fontawesome-svg-core' +import { faCircleXmark } from '@fortawesome/free-solid-svg-icons' + +library.add(faCircleXmark) + +/** + * This component emits the following events: + * cancelled, emitted when the action should not be performed; + * accepted, emitted when the action should be performed; + * + * The caller should close this dialog after receiving any of the two events. + */ +const ErrorModal = { + components: { + DialogModal, + }, + props: { + title: { + type: String, + }, + clearText: { + type: String, + }, + recoverText: { + type: String, + }, + error: { + type: Error, + }, + }, + emits: ['clear', 'recover'] +} + +export default ErrorModal diff --git a/src/components/error_modal/error_modal.vue b/src/components/error_modal/error_modal.vue new file mode 100644 index 000000000..7429e8bdc --- /dev/null +++ b/src/components/error_modal/error_modal.vue @@ -0,0 +1,100 @@ + + + + diff --git a/src/components/global_error/global_error.js b/src/components/global_error/global_error.js new file mode 100644 index 000000000..0f826f4c4 --- /dev/null +++ b/src/components/global_error/global_error.js @@ -0,0 +1,52 @@ +import ErrorModal from 'src/components/error_modal/error_modal.vue' + +import { useInterfaceStore } from 'src/stores/interface.js' + +import { mapState, mapActions } from 'pinia' + +const GlobalError = { + components: { + ErrorModal, + }, + computed: { + title() { + if (this.globalError == null) return null + return this.globalError.title && this.$t(this.globalError.title) + }, + content() { + if (this.globalError == null) return null + if (this.globalError.content) { + return this.$t(this.globalError.content, [this.globalError.error]) + } else { + return null + } + }, + details() { + if (this.globalError == null) return null + if (this.globalError.error != null) { + return this.globalError.error.toString() + '\n\n' + this.globalError.error.stack + } else { + return this.globalError.details + } + }, + recoverText() { + if (this.globalError == null) return null + if (this.globalError.recoverText == null) return null + return this.$t(this.globalError.recoverText) + }, + ...mapState(useInterfaceStore, ['globalError']), + }, + methods: { + clear() { + this.globalError.clear?.() + this.clearGlobalError() + }, + recover() { + this.globalError.recover?.() + this.clearGlobalError() + }, + ...mapActions(useInterfaceStore, ['clearGlobalError']), + }, +} + +export default GlobalError diff --git a/src/components/global_error/global_error.vue b/src/components/global_error/global_error.vue new file mode 100644 index 000000000..3c3901d4f --- /dev/null +++ b/src/components/global_error/global_error.vue @@ -0,0 +1,23 @@ + + + diff --git a/src/i18n/en.json b/src/i18n/en.json index 10ce5704b..62cce21e8 100644 --- a/src/i18n/en.json +++ b/src/i18n/en.json @@ -90,7 +90,11 @@ "loading": "Loading…", "generic_error": "An error occured", "generic_error_message": "An error occured: {0}", + "generic_error_details": "Technical info:", "error_retry": "Please try again", + "refresh_required": "Refresh required", + "refresh_required_content": "Frontend was updated on server, you need to refresh the page.", + "refresh_required_refresh": "Refresh", "retry": "Try again", "optional": "optional", "show_more": "Show more", diff --git a/src/stores/interface.js b/src/stores/interface.js index 4d5d2a8e4..e76a23cc2 100644 --- a/src/stores/interface.js +++ b/src/stores/interface.js @@ -177,8 +177,29 @@ export const useInterfaceStore = defineStore('interface', { removeGlobalNotice(notice) { this.globalNotices = this.globalNotices.filter((n) => n !== notice) }, - setGlobalError(data) { - this.globalError = data + setGlobalError({ error, instance, info }) { + console.log(info) + switch (info) { + case 'https://vuejs.org/error-reference/#runtime-13': { + this.globalError = { + title: 'general.refresh_required', + content: 'general.refresh_required_content', + // `true` disables cache on Firefox (non-standard) + recover: () => window.location.reload(true), + recoverText: 'general.refresh_required_refresh', + error, + } + break + } + default: { + this.globalError = { error } + break + } + } + console.log(this.globalError) + }, + clearGlobalError() { + this.globalError = null; }, pushGlobalNotice({ messageKey,