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 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ ' - ' }}
+
+
+
+
+ {{ $t('general.generic_error_details') }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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 @@
+
+
+
+
+ {{ content }}
+
+
+ {{ $t('general.generic_error_details') }}
+
+
+
+
+
+
+
+
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,