diff --git a/app/src/api/errors.ts b/app/src/api/errors.ts index 7c0056ec..9b8c7d09 100644 --- a/app/src/api/errors.ts +++ b/app/src/api/errors.ts @@ -3,15 +3,16 @@ * @module api/errors */ +import type { APIRequest } from '@/composables/useRequests' import i18n from '@/i18n' -import type { APIErrorData, RequestMethod, APIRequest } from './api' +import type { APIErrorData, RequestMethod } from './api' class APIError extends Error { name = 'APIError' code: number status: string method: RequestMethod - request: APIRequest + requestId: string path: string constructor( @@ -28,7 +29,7 @@ class APIError extends Error { this.code = status this.status = statusText this.method = request.method - this.request = request + this.requestId = request.id this.path = urlObj.pathname + urlObj.search } @@ -152,13 +153,13 @@ const errors = { } export { - errors as default, - APIError, - APIErrorLog, APIBadRequestError, APIConnexionError, + APIError, + APIErrorLog, APIInternalError, APINotFoundError, APINotRespondingError, APIUnauthorizedError, + errors as default, } diff --git a/app/src/api/handlers.ts b/app/src/api/handlers.ts index 48fa7970..68e3a235 100644 --- a/app/src/api/handlers.ts +++ b/app/src/api/handlers.ts @@ -3,7 +3,7 @@ * @module api/handlers */ -import errors, { APIError } from '@/api/errors' +import errors from '@/api/errors' import { STATUS_VARIANT, type APIRequest, @@ -98,39 +98,6 @@ export function getError( errorCode = 'log' } - // This error can be catched by a view otherwise it will be catched by the `onUnhandledAPIError` handler. + // This error can be catched by a view otherwise it will be catched by the global error handler. return new errors[errorCode](request, response, errorData) } - -/** - * If an APIError is not catched by a view it will be dispatched to the store so the - * error can be displayed in the error modal. - */ -export function onUnhandledAPIError(error: APIError) { - error.log() - store.dispatch('HANDLE_ERROR', error) -} - -/** - * Global catching of unhandled promise's rejections. - * Those errors (thrown or rejected from inside a promise) can't be catched by - * `window.onerror`. - */ -export function registerGlobalErrorHandlers() { - window.addEventListener('unhandledrejection', (e) => { - const error = e.reason - if (error instanceof APIError) { - onUnhandledAPIError(error) - // Seems like there's a bug in Firefox and the error logging in not prevented. - e.preventDefault() - } - }) - - // Keeping this in case it is needed. - - // Global catching of errors occuring inside vue components. - // Vue.config.errorHandler = (err, vm, info) => {} - - // Global catching of regular js errors. - // window.onerror = (message, source, lineno, colno, error) => {} -} diff --git a/app/src/api/index.ts b/app/src/api/index.ts index b959b5a5..3659bfe2 100644 --- a/app/src/api/index.ts +++ b/app/src/api/index.ts @@ -1,2 +1,2 @@ export { default, objectToParams } from './api' -export { getError, registerGlobalErrorHandlers } from './handlers' +export { getError } from './handlers' diff --git a/app/src/composables/useRequests.ts b/app/src/composables/useRequests.ts index e1cd9e8c..7848e2be 100644 --- a/app/src/composables/useRequests.ts +++ b/app/src/composables/useRequests.ts @@ -1,11 +1,13 @@ import { createGlobalState } from '@vueuse/core' import { v4 as uuid } from 'uuid' import { computed, reactive, shallowRef } from 'vue' +import { useRouter } from 'vue-router' import type { APIQuery, RequestMethod } from '@/api/api' -import { type APIError } from '@/api/errors' +import { APIErrorLog, type APIError } from '@/api/errors' import { isObjectLiteral } from '@/helpers/commons' import i18n from '@/i18n' +import store from '@/store' import type { StateVariant } from '@/types/commons' export type RequestStatus = 'pending' | 'success' | 'warning' | 'error' @@ -47,6 +49,8 @@ export const STATUS_VARIANT = { } as const export const useRequests = createGlobalState(() => { + const router = useRouter() + const requests = shallowRef([]) const currentRequest = computed(() => { return requests.value.find((r) => r.showModal) @@ -146,6 +150,41 @@ export const useRequests = createGlobalState(() => { }, 350) } + function handleAPIError(err: APIError) { + err.log() + if (err.code === 401) { + // Unauthorized + store.dispatch('DISCONNECT') + } else if (err instanceof APIErrorLog) { + // Errors that have produced logs + router.push({ name: 'tool-log', params: { name: err.logRef } }) + } else { + const request = requests.value.find((r) => r.id === err.requestId)! + request.err = err + } + } + + function showModal(requestId: APIRequest['id']) { + const request = requests.value.find((r) => r.id === requestId)! + request.showModal = true + } + + function dismissModal(requestId: APIRequest['id']) { + const request = requests.value.find((r) => r.id === requestId)! + + if (request.err && request.initial) { + // In case of an initial request (data that is needed by a view to render itself), + // try to go back so the user doesn't get stuck at a never ending skeleton view. + if (history.length > 2) { + history.back() + } else { + // if the url was opened in a new tab, return to home + router.push({ name: 'home' }) + } + } + request.showModal = false + } + return { requests, historyList, @@ -153,5 +192,8 @@ export const useRequests = createGlobalState(() => { locked, startRequest, endRequest, + handleAPIError, + dismissModal, + showModal, } }) diff --git a/app/src/main.ts b/app/src/main.ts index 42ea1428..92e1252d 100644 --- a/app/src/main.ts +++ b/app/src/main.ts @@ -1,15 +1,15 @@ -import { createApp, type Component } from 'vue' -import App from './App.vue' -import { createBootstrap } from 'bootstrap-vue-next' -import { VueShowdownPlugin } from 'vue-showdown' import { watchOnce } from '@vueuse/core' +import { createBootstrap } from 'bootstrap-vue-next' +import { createApp, type Component } from 'vue' +import { VueShowdownPlugin } from 'vue-showdown' +import App from './App.vue' +import { APIError } from './api/errors' +import { useRequests } from './composables/useRequests' import { useSettings } from './composables/useSettings' -import store from './store' -import router from './router' import i18n from './i18n' - -import { registerGlobalErrorHandlers } from './api' +import router from './router' +import store from './store' import '@/scss/main.scss' @@ -17,6 +17,24 @@ type Module = { default: Component } const app = createApp(App) +// Error catching +function onError(err: unknown) { + if (err instanceof APIError) { + useRequests().handleAPIError(err) + } else { + // FIXME Error modal for internal code error? + throw err + } +} +app.config.errorHandler = (err) => onError(err) +window.addEventListener('unhandledrejection', (e) => { + // Global catching of unhandled promise's rejections. + // Those errors (thrown or rejected from inside a promise) can't be catched by + // `window.onerror` or vue. + e.preventDefault() + onError(e.reason) +}) + app.use(store) app.use(router) app.use(i18n) @@ -46,7 +64,5 @@ Object.values(globalComponentsModules).forEach( }, ) -registerGlobalErrorHandlers() - // Load default locales translations files then mount the app watchOnce(useSettings().localesLoaded, () => app.mount('#app')) diff --git a/app/src/router/index.ts b/app/src/router/index.ts index a824634b..95b7e1e9 100644 --- a/app/src/router/index.ts +++ b/app/src/router/index.ts @@ -1,8 +1,9 @@ import { createRouter, createWebHashHistory } from 'vue-router' +import { useRequests } from '@/composables/useRequests' import { useSettings } from '@/composables/useSettings' -import routes from './routes' import store from '@/store' +import routes from './routes' const router = createRouter({ history: createWebHashHistory(import.meta.env.BASE_URL), @@ -32,8 +33,10 @@ router.beforeEach((to, from, next) => { updateTransitionName({ to, from }) } - if (store.getters.error) { - store.dispatch('DISMISS_ERROR', true) + const { currentRequest, dismissModal } = useRequests() + if (currentRequest.value?.err) { + // In case an error is still present after code route change + dismissModal(currentRequest.value.id) } if (to.name === 'post-install' && store.getters.installed) { diff --git a/app/src/store/info.ts b/app/src/store/info.ts index 3bc2d81e..97dc15c0 100644 --- a/app/src/store/info.ts +++ b/app/src/store/info.ts @@ -10,7 +10,6 @@ export default { connected: localStorage.getItem('connected') === 'true', // Boolean yunohost: null, // Object { version, repo } reconnecting: null, // null|Object { attemps, delay, initialDelay } - error: null, // null || request routerKey: undefined, // String if current route has params breadcrumb: [], // Array of routes transitionName: null, // String of CSS class if transitions are enabled @@ -34,14 +33,6 @@ export default { state.reconnecting = args }, - SET_ERROR(state, request) { - if (request) { - state.error = request - } else { - state.error = null - } - }, - SET_ROUTER_KEY(state, key) { state.routerKey = key }, @@ -134,48 +125,6 @@ export default { }) }, - HANDLE_ERROR({ commit, dispatch }, error) { - if (error.code === 401) { - // Unauthorized - dispatch('DISCONNECT') - } else if (error.logRef) { - // Errors that have produced logs - router.push({ name: 'tool-log', params: { name: error.logRef } }) - } else { - // The request is temporarely stored in the error for reference, but we reverse - // the ownership to stay generic. - const request = error.request - delete error.request - request.error = error - // Display the error in a modal on the current view. - commit('SET_ERROR', request) - } - }, - - REVIEW_ERROR({ commit }, request) { - request.review = true - commit('SET_ERROR', request) - }, - - DISMISS_ERROR({ commit, state }, { initial, review = false }) { - if (initial && !review) { - // In case of an initial request (data that is needed by a view to render itself), - // try to go back so the user doesn't get stuck at a never ending skeleton view. - if (history.length > 2) { - history.back() - } else { - // if the url was opened in a new tab, return to home - router.push({ name: 'home' }) - } - } - commit('SET_ERROR', null) - }, - - DISMISS_WARNING({ commit, state }, request) { - commit('SET_WAITING', false) - delete request.showWarningMessage - }, - UPDATE_ROUTER_KEY({ commit }, { to, from }) { if (isEmptyValue(to.params)) { commit('SET_ROUTER_KEY', undefined) @@ -257,11 +206,8 @@ export default { installed: (state) => state.installed, connected: (state) => state.connected, yunohost: (state) => state.yunohost, - error: (state) => state.error, reconnecting: (state) => state.reconnecting, history: (state) => state.history, - lastAction: (state) => state.history[state.history.length - 1], - currentRequest: (state) => state.currentRequest, routerKey: (state) => state.routerKey, breadcrumb: (state) => state.breadcrumb, transitionName: (state) => state.transitionName,