Browse Source

Feature: Mobile - Implemented the first version of the error handling layer.

Martin Gruner 3 years ago
parent
commit
919dea2592

+ 3 - 2
app/frontend/apps/mobile/App.vue

@@ -1,11 +1,12 @@
 <!-- Copyright (C) 2012-2022 Zammad Foundation, https://zammad-foundation.org/ -->
 
 <template>
-  <CommonNotifications />
+  <CommonNotifications v-if="applicationLoaded.value" />
   <div
+    v-if="applicationLoaded.value"
     class="min-h-screen min-w-full select-none bg-black text-center font-sans text-sm text-white antialiased"
   >
-    <router-view v-if="applicationLoaded.value" />
+    <router-view />
   </div>
 </template>
 

+ 9 - 1
app/frontend/common/composables/useNotifications.ts

@@ -1,9 +1,10 @@
 // Copyright (C) 2012-2022 Zammad Foundation, https://zammad-foundation.org/
 
 import { ref } from 'vue'
-import type {
+import {
   NewNotificationInterface,
   NotificationInterface,
+  NotificationTypes,
 } from '@common/types/notification'
 import getUuid from '@common/utils/getUuid'
 
@@ -40,10 +41,17 @@ export default function useNotifications() {
     return newNotification.id
   }
 
+  function hasErrors() {
+    return notifications.value.some((notification) => {
+      return notification.type === NotificationTypes.ERROR
+    })
+  }
+
   return {
     notify,
     notifications,
     removeNotification,
     clearAllNotifications,
+    hasErrors,
   }
 }

+ 6 - 8
app/frontend/common/server/apollo/handler/BaseHandler.ts

@@ -9,13 +9,13 @@ import {
   OperationReturn,
 } from '@common/types/server/apollo/handler'
 import { Ref } from 'vue'
-import useNotifications from '@common/composables/useNotifications'
 import { NotificationTypes } from '@common/types/notification'
 import {
   GraphQLErrorReport,
   GraphQLErrorTypes,
   GraphQLHandlerError,
 } from '@common/types/error'
+import useNotifications from '@common/composables/useNotifications'
 
 export default abstract class BaseHandler<
   TResult = OperationResult,
@@ -30,10 +30,10 @@ export default abstract class BaseHandler<
 
   protected baseHandlerOptions: BaseHandlerOptions = {
     errorShowNotification: true,
-    errorNotitifactionMessage: __(
+    errorNotificationMessage: __(
       'An error occured during the operation. Please contact your administrator.',
     ),
-    errorNotitifactionType: NotificationTypes.ERROR,
+    errorNotificationType: NotificationTypes.ERROR,
   }
 
   public handlerOptions!: CommonHandlerOptions<THandlerOptions>
@@ -67,11 +67,9 @@ export default abstract class BaseHandler<
     const options = this.handlerOptions
 
     if (options.errorShowNotification) {
-      const { notify } = useNotifications()
-
-      notify({
-        message: options.errorNotitifactionMessage,
-        type: options.errorNotitifactionType,
+      useNotifications().notify({
+        message: options.errorNotificationMessage,
+        type: options.errorNotificationType,
       })
     }
 

+ 2 - 0
app/frontend/common/server/apollo/link.ts

@@ -15,6 +15,7 @@ import consumer from '@common/server/action_cable/consumer'
 import { getMainDefinition } from '@apollo/client/utilities'
 import { BatchHttpLink } from '@apollo/client/link/batch-http'
 import { FragmentDefinitionNode, OperationDefinitionNode } from 'graphql'
+import connectedStateLink from '@common/server/apollo/link/connectedState'
 
 // Should subsequent HTTP calls be batched together?
 const enableBatchLink = false
@@ -76,6 +77,7 @@ const link = from([
   errorLink,
   setAuthorizationLink,
   debugLink,
+  connectedStateLink,
   splitLink,
 ])
 

+ 16 - 0
app/frontend/common/server/apollo/link/connectedState.ts

@@ -0,0 +1,16 @@
+// Copyright (C) 2012-2022 Zammad Foundation, https://zammad-foundation.org/
+
+import { ApolloLink } from '@apollo/client/core'
+import useApplicationConnectedStore from '@common/stores/application/connected'
+
+// This link is only there to look for received responses and set
+//  the applicationConnected state accordingly.
+const connectedStateLink = new ApolloLink((operation, forward) => {
+  return forward(operation).map((response) => {
+    // A response in this chain means there was no network error.
+    useApplicationConnectedStore().bringUp()
+    return response
+  })
+})
+
+export default connectedStateLink

+ 4 - 0
app/frontend/common/server/apollo/link/error.ts

@@ -8,6 +8,7 @@ import {
   GraphQLErrorTypes,
 } from '@common/types/error'
 import emitter from '@common/utils/emitter'
+import useApplicationConnectedStore from '@common/stores/application/connected'
 
 const errorLink = onError(({ graphQLErrors, networkError, operation }) => {
   const errorContext = getErrorContext(operation)
@@ -42,6 +43,9 @@ const errorLink = onError(({ graphQLErrors, networkError, operation }) => {
 
   if (networkError) {
     errorMessages.push(`[Network error]: ${networkError}`)
+    // Network error implies application connection problems.
+    // TODO: what's missing here is a detection of web socket disconnects.
+    useApplicationConnectedStore().takeDown()
   }
 
   if (errorContext.logLevel === 'silent') return

+ 42 - 0
app/frontend/common/stores/application/connected.ts

@@ -0,0 +1,42 @@
+// Copyright (C) 2012-2022 Zammad Foundation, https://zammad-foundation.org/
+
+import useNotifications from '@common/composables/useNotifications'
+import { NotificationTypes } from '@common/types/notification'
+import type { SingleValueStore } from '@common/types/store'
+import log from 'loglevel'
+import { defineStore } from 'pinia'
+
+let notificationId: string
+
+const notifications = useNotifications()
+
+// TODO: consider switching from notification to a modal dialog, and improving the message
+const useApplicationConnectedStore = defineStore('applicationConnected', {
+  state: (): SingleValueStore<boolean> => {
+    return {
+      value: false,
+    }
+  },
+  actions: {
+    bringUp(): void {
+      if (this.value) return
+      log.debug('Application connection just came up.')
+      if (notificationId) {
+        notifications.removeNotification(notificationId)
+      }
+      this.value = true
+    },
+    takeDown(): void {
+      if (!this.value) return
+      log.debug('Application connection just went down.')
+      notificationId = notifications.notify({
+        message: __('The connection to the server was lost.'),
+        type: NotificationTypes.ERROR,
+        persistent: true,
+      })
+      this.value = false
+    },
+  },
+})
+
+export default useApplicationConnectedStore

+ 12 - 2
app/frontend/common/stores/application/loaded.ts

@@ -1,5 +1,6 @@
 // Copyright (C) 2012-2022 Zammad Foundation, https://zammad-foundation.org/
 
+import useNotifications from '@common/composables/useNotifications'
 import type { SingleValueStore } from '@common/types/store'
 import testFlags from '@common/utils/testFlags'
 import { defineStore } from 'pinia'
@@ -17,10 +18,19 @@ const useApplicationLoadedStore = defineStore('applicationLoaded', {
   },
   actions: {
     setLoaded(): void {
-      this.value = true
-
       const loadingAppElement: Maybe<HTMLElement> =
         document.getElementById('loading-app')
+
+      if (useNotifications().hasErrors()) {
+        loadingAppElement
+          ?.getElementsByClassName('loading-failed')
+          .item(0)
+          ?.classList.add('active')
+        return
+      }
+
+      this.value = true
+
       if (loadingAppElement) {
         loadingAppElement.remove()
       }

+ 2 - 2
app/frontend/common/types/server/apollo/handler.ts

@@ -60,8 +60,8 @@ export type OperationResult =
 
 export interface BaseHandlerOptions {
   errorShowNotification: boolean
-  errorNotitifactionMessage: string
-  errorNotitifactionType: NotificationTypes
+  errorNotificationMessage: string
+  errorNotificationType: NotificationTypes
   errorCallback?: (error: GraphQLHandlerError) => void
 }
 

+ 4 - 4
app/frontend/tests/common/server/apollo/handler/MutationHandler.spec.ts

@@ -73,14 +73,14 @@ describe('MutationHandler', () => {
     })
 
     it('default handler options can be changed', () => {
-      const errorNotitifactionMessage = 'A test message.'
+      const errorNotificationMessage = 'A test message.'
 
       const mutationHandlerObject = new MutationHandler(sampleMutation(), {
-        errorNotitifactionMessage,
+        errorNotificationMessage,
       })
       expect(
-        mutationHandlerObject.handlerOptions.errorNotitifactionMessage,
-      ).toBe(errorNotitifactionMessage)
+        mutationHandlerObject.handlerOptions.errorNotificationMessage,
+      ).toBe(errorNotificationMessage)
     })
 
     it('given mutation function was executed', () => {

Some files were not shown because too many files changed in this diff