diff --git a/package-lock.json b/package-lock.json index ae6799dd5cb4006c2b35ed0f8e731019c2b3dfff..7e63fe57104ff692f203149b34f440e2cd169539 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "dependencies": { "axios": "^1.6.8", "bootstrap": "^5.3.3", + "oh-vue-icons": "^1.0.0-rc3", "pinia": "^2.1.7", "vue": "^3.4.21", "vue-router": "^4.3.0", @@ -6063,6 +6064,48 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/oh-vue-icons": { + "version": "1.0.0-rc3", + "resolved": "https://registry.npmjs.org/oh-vue-icons/-/oh-vue-icons-1.0.0-rc3.tgz", + "integrity": "sha512-+k2YC6piK7sEZnwbkQF3UokFPMmgqpiLP6f/H0ovQFLl20QA5V4U8EcI6EclD2Lt5NMQ3k6ilLGo8XyXqdVSvg==", + "dependencies": { + "vue-demi": "^0.12.5" + }, + "peerDependencies": { + "@vue/composition-api": "^1.0.0-rc.1", + "vue": "^2.0.0 || >=3.0.0" + }, + "peerDependenciesMeta": { + "@vue/composition-api": { + "optional": true + } + } + }, + "node_modules/oh-vue-icons/node_modules/vue-demi": { + "version": "0.12.5", + "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.12.5.tgz", + "integrity": "sha512-BREuTgTYlUr0zw0EZn3hnhC3I6gPWv+Kwh4MCih6QcAeaTlaIX0DwOVN0wHej7hSvDPecz4jygy/idsgKfW58Q==", + "hasInstallScript": true, + "bin": { + "vue-demi-fix": "bin/vue-demi-fix.js", + "vue-demi-switch": "bin/vue-demi-switch.js" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "@vue/composition-api": "^1.0.0-rc.1", + "vue": "^3.0.0-0 || ^2.6.0" + }, + "peerDependenciesMeta": { + "@vue/composition-api": { + "optional": true + } + } + }, "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", diff --git a/package.json b/package.json index 9f52cb1c7a23f36ca3194ffe34a78eb863858028..7f62401e3056a470b8fb54afb0fc7f6634551a86 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,7 @@ "dependencies": { "axios": "^1.6.8", "bootstrap": "^5.3.3", + "oh-vue-icons": "^1.0.0-rc3", "pinia": "^2.1.7", "vue": "^3.4.21", "vue-router": "^4.3.0", diff --git a/src/App.vue b/src/App.vue index 461e3d52db193794b9b516bce015595a8eeb985d..1a140ee58a2d942ddacb457b5fbd6f5b5ad23e0b 100644 --- a/src/App.vue +++ b/src/App.vue @@ -1,9 +1,18 @@ <script setup lang="ts"> import { RouterView } from 'vue-router' +import ErrorBoundaryCatcher from '@/components/Exceptions/ErrorBoundaryCatcher.vue'; </script> <template> <main> - <RouterView /> + <error-boundary-catcher> + <RouterView /> + </error-boundary-catcher> </main> -</template> \ No newline at end of file +</template> + +<style> + main { + font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; + } +</style> \ No newline at end of file diff --git a/src/components/Exceptions/ErrorBoundaryCatcher.vue b/src/components/Exceptions/ErrorBoundaryCatcher.vue new file mode 100644 index 0000000000000000000000000000000000000000..3bc046fb271f7d3b3ce3acee559329d18dce53bb --- /dev/null +++ b/src/components/Exceptions/ErrorBoundaryCatcher.vue @@ -0,0 +1,19 @@ +<template> + <error-box :error-message="errorStore.getFirstError" @update:errorMessage="errorStore.removeCurrentError" /> + <slot /> + </template> + + <script setup lang="ts"> + import { onErrorCaptured } from 'vue'; + import { useErrorStore } from '@/stores/ErrorStore'; + import ErrorBox from '@/components/Exceptions/ErrorBox.vue'; + import handleUnknownError from '@/components/Exceptions/unkownErrorHandler'; + + const errorStore = useErrorStore(); + + onErrorCaptured((err, _vm, _info): boolean => { + const message = handleUnknownError(err.message); + errorStore.addError(message); //If no openAPI axios error, use err.message + return false; + }); + </script> \ No newline at end of file diff --git a/src/components/Exceptions/ErrorBox.vue b/src/components/Exceptions/ErrorBox.vue new file mode 100644 index 0000000000000000000000000000000000000000..f6dbe0d4adfb07e7ae1b4720ecf322b08fa3de71 --- /dev/null +++ b/src/components/Exceptions/ErrorBox.vue @@ -0,0 +1,90 @@ +<template> + <div class="error-box" v-if="errorMessage" data-testid="error-box"> + <span> + <v-icon name="bi-exclamation-triangle" /> + <button @click="wrapText = !wrapText" class="error-message-button"> + <h4>{{ errorMessage }}</h4> + </button> + <button class="error-remove-button" @click="$emit('update:errorMessage', '')" data-testid="hide-button"> + <v-icon scale="2" name="bi-dash" /> + </button> + </span> + </div> + </template> + + <script lang="ts"> + import { defineComponent } from 'vue'; + import { OhVueIcon, addIcons } from 'oh-vue-icons'; + import { BiDash, BiExclamationTriangle } from 'oh-vue-icons/icons'; + + addIcons(BiDash, BiExclamationTriangle); + + export default defineComponent({ + components: { + 'v-icon': OhVueIcon, + }, + props: { + errorMessage: { + type: String, + default: '', + }, + }, + data() { + return { + wrapText: false, + }; + }, + }); + </script> + + <style scoped> + .error-box { + position: fixed; + top: 2em; + left: 50%; + transform: translate(-50%, 0); + width: min(100%, 700px); + background-color: var(--red-color); + padding: 7px; + border-radius: 5px; + z-index: 1000; + } + + .error-box span { + display: flex; + justify-content: space-between; + align-items: center; + } + + .error-message-button { + white-space: v-bind('wrapText ? "normal" : "nowrap"'); + overflow: hidden; + border: none; + } + + .error-message-button h4 { + overflow: hidden; + text-overflow: ellipsis; + } + + .error-box * { + margin: 2px; + } + + .error-box button { + background-color: transparent; + box-shadow: none; + color: black; + cursor: pointer; + } + + .error-remove-button { + color: rgb(79, 77, 77); + border: solid black 0.5px; + padding: 0.3em; + } + + .error-box button:hover { + color: rgb(40, 38, 38); + } + </style> \ No newline at end of file diff --git a/src/components/Exceptions/unkownErrorHandler.ts b/src/components/Exceptions/unkownErrorHandler.ts new file mode 100644 index 0000000000000000000000000000000000000000..6f7b6d914edc88b03029a68b907f7ef45f05ee7e --- /dev/null +++ b/src/components/Exceptions/unkownErrorHandler.ts @@ -0,0 +1,19 @@ +import { ApiError as BackendApiError } from '@/api'; +import { AxiosError } from 'axios'; + +/** + * Finds the correct error message for the given error + * The message is then put into the error store + * which an error component can then display + * @param error The unknown error to handle + */ +const handleUnknownError = (error: any): string => { + if (error instanceof AxiosError) { + return error.code!!; + } else if (error instanceof BackendApiError) { + return error.body.detail ?? error.body; + } + return 'ContextErrorMessage'; +}; + +export default handleUnknownError; \ No newline at end of file diff --git a/src/router/index.ts b/src/router/index.ts index 83cc091e2226f9bdedf4ff60d45c2b9b83317778..98101c6423782c3fa0ebfca00d156b93fac31f5a 100644 --- a/src/router/index.ts +++ b/src/router/index.ts @@ -24,6 +24,11 @@ const routes = [ name: 'news', component: () => import('@/views/NewsView.vue'), }, + { + path: 'test', + name: 'test', + component: () => import('@/views/TestView.vue'), + }, ] }, { diff --git a/src/stores/ErrorStore.ts b/src/stores/ErrorStore.ts new file mode 100644 index 0000000000000000000000000000000000000000..32ffb91af9d857c3ad747853265724d3843f4ea4 --- /dev/null +++ b/src/stores/ErrorStore.ts @@ -0,0 +1,33 @@ +import { defineStore } from 'pinia'; + +// A userstore which can be used to store several errors at the same time +export const useErrorStore = defineStore('ErrorStore', { + state: () => ({ + errors: [] as string[], + }), + actions: { + addError(error: string) { + console.log(error); + this.errors = [error]; + }, + removeCurrentError() { + if (this.errors.length > 0) { + this.errors.shift(); + } + }, + }, + getters: { + getFirstError(): string { + if (this.errors.length > 0) { + return `Exceptions.${this.errors[0]}`; + } + return ''; + }, + getLastError(): string { + if (this.errors.length > 0) { + return `Exceptions.${this.errors[this.errors.length - 1]}`; + } + return ''; + }, + }, +}); \ No newline at end of file diff --git a/src/views/TestView.vue b/src/views/TestView.vue new file mode 100644 index 0000000000000000000000000000000000000000..a7fce444288c4df4c5f9f531c3e92e3da43935a4 --- /dev/null +++ b/src/views/TestView.vue @@ -0,0 +1,11 @@ +<template> + <div> + <Button1 :buttonText="hallo"></Button1> + </div> +</template> + +<script setup lang="ts"> + import Button1 from '@/components/Buttons/Button1.vue' + + const hallo = 'Hallo' +</script> \ No newline at end of file