Browse Source

Feature: Mobile - Cleanup private frontend stores after logout.

Dominik Klein 2 years ago
parent
commit
0f82a4844c

+ 3 - 16
app/frontend/apps/mobile/modules/home/stores/ticketOverviews.ts

@@ -15,23 +15,10 @@ export type TicketOverview = ConfidentTake<
   'ticketOverviews.edges.node'
 >
 
-let overviewHandler: QueryHandler<
-  TicketOverviewsQuery,
-  { withTicketCount: boolean }
->
-
-const getOverviewHandler = () => {
-  if (!overviewHandler) {
-    overviewHandler = new QueryHandler(
-      useTicketOverviewsQuery({ withTicketCount: true }),
-    )
-  }
-
-  return overviewHandler
-}
-
 export const useTicketsOverviews = defineStore('tickets-overview', () => {
-  const handler = getOverviewHandler()
+  const handler = new QueryHandler(
+    useTicketOverviewsQuery({ withTicketCount: true }),
+  )
   const overviewsRaw = handler.result()
   const overviewsLoading = handler.loading()
 

+ 6 - 2
app/frontend/apps/mobile/modules/ticket/__tests__/tickets-view.spec.ts

@@ -2,10 +2,11 @@
 
 import { EnumOrderDirection } from '@shared/graphql/types'
 import { waitFor } from '@testing-library/vue'
+import { getTestRouter } from '@tests/support/components/renderComponent'
 import { visitView } from '@tests/support/components/visitView'
 import { mockTicketOverviews } from '@tests/support/mocks/ticket-overviews'
 import { waitForNextTick } from '@tests/support/utils'
-import { stringifyQuery } from 'vue-router'
+import { stringifyQuery, useRoute } from 'vue-router'
 import { mockTicketsByOverview, ticketDefault } from './mocks/overview'
 
 beforeEach(() => {
@@ -129,7 +130,10 @@ it('takes filter from query', async () => {
     column: 'number',
     direction: EnumOrderDirection.Ascending,
   })
-  await visitView(`/tickets/view?${query}`)
+
+  const view = await visitView(`/tickets/view?${query}`)
+
+  await view.findByTestId('overview')
 
   await waitFor(() => {
     expect(ticketsMock.spies.resolve).toHaveBeenCalledWith(

+ 1 - 1
app/frontend/apps/mobile/modules/ticket/views/TicketOverview.vue

@@ -82,7 +82,7 @@ const orderColumnLabels = computed(() => {
 
 // Check that the given order by column is really a valid column and otherwise
 // reset query parameter.
-watchOnce(orderColumnLabels, () => {
+watch(selectedOverview, () => {
   if (userOrderBy.value && !orderColumnLabels.value[userOrderBy.value]) {
     userOrderBy.value = undefined
   }

+ 0 - 3
app/frontend/shared/components/Form/fields/FieldAutoComplete/FieldAutoComplete.stories.ts

@@ -156,12 +156,9 @@ const mockQueryResult = (
 const mockClient = () => {
   const mockApolloClient = createMockClient()
 
-  console.log('mockApolloClient', mockApolloClient)
-
   mockApolloClient.setRequestHandler(
     AutocompleteSearchUserDocument,
     (variables) => {
-      console.log('VARIABLES', variables)
       return Promise.resolve({
         data: mockQueryResult(variables.query, variables.limit),
       })

+ 8 - 0
app/frontend/shared/server/apollo/handler/SubscriptionHandler.ts

@@ -25,6 +25,14 @@ export default class SubscriptionHandler<
     return this.operationResult.options
   }
 
+  public start(): void {
+    this.operationResult.start()
+  }
+
+  public stop(): void {
+    this.operationResult.stop()
+  }
+
   public result(): Ref<Maybe<TResult> | undefined> {
     return this.operationResult.result
   }

+ 1 - 1
app/frontend/shared/stores/__tests__/session.spec.ts

@@ -31,7 +31,7 @@ const userData = {
   },
 }
 
-describe('Translations Store', () => {
+describe('Session Store', () => {
   beforeEach(() => {
     setActivePinia(createPinia())
   })

+ 96 - 89
app/frontend/shared/stores/application.ts

@@ -42,111 +42,118 @@ let connectionNotificationId: string
 // TODO: consider switching from notification to a modal dialog, and improving the message
 const notifications = useNotifications()
 
-const useApplicationStore = defineStore('applicationLoaded', () => {
-  const loaded = ref(false)
-  const loading = computed(() => !loaded.value)
-
-  const setLoaded = (): void => {
-    const loadingAppElement: Maybe<HTMLElement> =
-      document.getElementById('loading-app')
-
-    if (useNotifications().hasErrors()) {
-      loadingAppElement
-        ?.getElementsByClassName('loading-failed')
-        .item(0)
-        ?.classList.add('active')
-      return
+const useApplicationStore = defineStore(
+  'application',
+  () => {
+    const loaded = ref(false)
+    const loading = computed(() => !loaded.value)
+
+    const setLoaded = (): void => {
+      const loadingAppElement: Maybe<HTMLElement> =
+        document.getElementById('loading-app')
+
+      if (useNotifications().hasErrors()) {
+        loadingAppElement
+          ?.getElementsByClassName('loading-failed')
+          .item(0)
+          ?.classList.add('active')
+        return
+      }
+
+      loaded.value = true
+
+      if (loadingAppElement) {
+        loadingAppElement.remove()
+      }
+
+      testFlags.set('applicationLoaded.loaded')
     }
 
-    loaded.value = true
+    const connected = ref(false)
 
-    if (loadingAppElement) {
-      loadingAppElement.remove()
+    const bringConnectionUp = (): void => {
+      if (connected.value) return
+
+      log.debug('Application connection just came up.')
+
+      if (connectionNotificationId) {
+        notifications.removeNotification(connectionNotificationId)
+      }
+      connected.value = true
     }
 
-    testFlags.set('applicationLoaded.loaded')
-  }
+    const takeConnectionDown = (): void => {
+      if (!connected.value) return
 
-  const connected = ref(false)
+      log.debug('Application connection just went down.')
 
-  const bringConnectionUp = (): void => {
-    if (connected.value) return
+      connectionNotificationId = notifications.notify({
+        message: __('The connection to the server was lost.'),
+        type: NotificationTypes.Error,
+        persistent: true,
+      })
+      connected.value = false
+    }
 
-    log.debug('Application connection just came up.')
+    const config = ref<ConfigList>({})
 
-    if (connectionNotificationId) {
-      notifications.removeNotification(connectionNotificationId)
+    const initializeConfigUpdateSubscription = (): void => {
+      const configUpdatesSubscription = new SubscriptionHandler(
+        useConfigUpdatesSubscription(),
+      )
+
+      configUpdatesSubscription.onResult((result) => {
+        const updatedSetting = result.data?.configUpdates.setting
+        if (updatedSetting) {
+          config.value[updatedSetting.key] = updatedSetting.value
+        } else {
+          testFlags.set('useConfigUpdatesSubscription.subscribed')
+        }
+      })
+
+      configUpdatesSubscriptionInitialized = true
     }
-    connected.value = true
-  }
-
-  const takeConnectionDown = (): void => {
-    if (!connected.value) return
-
-    log.debug('Application connection just went down.')
-
-    connectionNotificationId = notifications.notify({
-      message: __('The connection to the server was lost.'),
-      type: NotificationTypes.Error,
-      persistent: true,
-    })
-    connected.value = false
-  }
-
-  const config = ref<ConfigList>({})
-
-  const initializeConfigUpdateSubscription = (): void => {
-    const configUpdatesSubscription = new SubscriptionHandler(
-      useConfigUpdatesSubscription(),
-    )
-
-    configUpdatesSubscription.onResult((result) => {
-      const updatedSetting = result.data?.configUpdates.setting
-      if (updatedSetting) {
-        config.value[updatedSetting.key] = updatedSetting.value
-      } else {
-        testFlags.set('useConfigUpdatesSubscription.subscribed')
+
+    const getConfig = async (): Promise<void> => {
+      const configQuery = getApplicationConfigQuery()
+
+      const result = await configQuery.loadedResult(true)
+      if (result?.applicationConfig) {
+        result.applicationConfig.forEach((item) => {
+          config.value[item.key] = item.value
+        })
+
+        // app/assets/javascripts/app/config.coffee
+        config.value.api_path = '/api/v1'
       }
-    })
 
-    configUpdatesSubscriptionInitialized = true
-  }
+      if (!configUpdatesSubscriptionInitialized) {
+        initializeConfigUpdateSubscription()
+      }
+    }
 
-  const getConfig = async (): Promise<void> => {
-    const configQuery = getApplicationConfigQuery()
+    const resetAndGetConfig = async (): Promise<void> => {
+      config.value = {}
 
-    const result = await configQuery.loadedResult(true)
-    if (result?.applicationConfig) {
-      result.applicationConfig.forEach((item) => {
-        config.value[item.key] = item.value
-      })
-      // app/assets/javascripts/app/config.coffee
-      config.value.api_path = '/api/v1'
+      await getConfig()
     }
 
-    if (!configUpdatesSubscriptionInitialized) {
-      initializeConfigUpdateSubscription()
+    return {
+      loaded,
+      loading,
+      setLoaded,
+      connected,
+      bringConnectionUp,
+      takeConnectionDown,
+      config,
+      initializeConfigUpdateSubscription,
+      getConfig,
+      resetAndGetConfig,
     }
-  }
-
-  const resetAndGetConfig = async (): Promise<void> => {
-    config.value = {}
-
-    await getConfig()
-  }
-
-  return {
-    loaded,
-    loading,
-    setLoaded,
-    connected,
-    bringConnectionUp,
-    takeConnectionDown,
-    config,
-    initializeConfigUpdateSubscription,
-    getConfig,
-    resetAndGetConfig,
-  }
-})
+  },
+  {
+    requiresAuth: false,
+  },
+)
 
 export default useApplicationStore

+ 4 - 0
app/frontend/shared/stores/authentication.ts

@@ -10,6 +10,7 @@ import useFingerprint from '@shared/composables/useFingerprint'
 import testFlags from '@shared/utils/testFlags'
 import { useSessionStore } from './session'
 import useApplicationStore from './application'
+import { resetAndDisposeStores } from '.'
 
 const useAuthenticationStore = defineStore(
   'authentication',
@@ -24,6 +25,8 @@ const useAuthenticationStore = defineStore(
       session.resetCurrentSession()
       authenticated.value = false
 
+      resetAndDisposeStores(true)
+
       // Refresh the config after logout, to have only the non authenticated version.
       await useApplicationStore().resetAndGetConfig()
 
@@ -97,6 +100,7 @@ const useAuthenticationStore = defineStore(
     shareState: {
       enabled: true,
     },
+    requiresAuth: false,
   },
 )
 

+ 35 - 6
app/frontend/shared/stores/index.ts

@@ -1,15 +1,44 @@
 // Copyright (C) 2012-2022 Zammad Foundation, https://zammad-foundation.org/
 
 import type { App } from 'vue'
-import type { Pinia } from 'pinia'
-import { createPinia } from 'pinia'
+import { createPinia, type Pinia } from 'pinia'
+import type { UsedStore } from '@shared/types/store'
 import PiniaSharedState from './plugins/sharedState'
 
-const store: Pinia = createPinia()
-store.use(PiniaSharedState({ enabled: false }))
+declare module 'pinia' {
+  // eslint-disable-next-line @typescript-eslint/no-unused-vars
+  export interface DefineStoreOptionsBase<S, Store> {
+    requiresAuth?: boolean
+  }
+}
+
+const usedStores = new Set<UsedStore>()
+
+const pinia: Pinia = createPinia()
+pinia.use(PiniaSharedState({ enabled: false }))
+
+// Remember all stores, for example to cleanup the private stores after logout.
+pinia.use((context) => {
+  usedStores.add({
+    store: context.store,
+    requiresAuth: context.options.requiresAuth ?? true,
+  })
+})
 
 export default function initializeStore(app: App) {
-  app.use(store)
+  app.use(pinia)
+}
+
+export const resetAndDisposeStores = (requiresAuth?: boolean) => {
+  usedStores.forEach((usedStore) => {
+    if (requiresAuth !== undefined && usedStore.requiresAuth !== requiresAuth) {
+      return
+    }
+
+    usedStore.store.$dispose()
+    delete pinia.state.value[usedStore.store.$id]
+    usedStores.delete(usedStore)
+  })
 }
 
-export { store }
+export { pinia }

+ 39 - 33
app/frontend/shared/stores/locale.ts

@@ -11,48 +11,54 @@ import useTranslationsStore from './translations'
 
 type Locale = LastArrayElement<LocalesQuery['locales']>
 
-const useLocaleStore = defineStore('locale', () => {
-  const localeData = ref<Maybe<Locale>>(null)
-  const locales = ref<Maybe<LocalesQuery['locales']>>(null)
+const useLocaleStore = defineStore(
+  'locale',
+  () => {
+    const localeData = ref<Maybe<Locale>>(null)
+    const locales = ref<Maybe<LocalesQuery['locales']>>(null)
 
-  const loadLocales = async (): Promise<void> => {
-    if (locales.value) return
+    const loadLocales = async (): Promise<void> => {
+      if (locales.value) return
 
-    locales.value = await getAvailableLocales()
-  }
+      locales.value = await getAvailableLocales()
+    }
 
-  const setLocale = async (locale?: string): Promise<void> => {
-    await loadLocales()
+    const setLocale = async (locale?: string): Promise<void> => {
+      await loadLocales()
 
-    let newLocaleData
+      let newLocaleData
 
-    if (locale) {
-      newLocaleData = locales.value?.find((elem) => {
-        return elem.locale === locale
-      })
-    }
+      if (locale) {
+        newLocaleData = locales.value?.find((elem) => {
+          return elem.locale === locale
+        })
+      }
 
-    if (!newLocaleData)
-      newLocaleData = localeForBrowserLanguage(locales.value || [])
+      if (!newLocaleData)
+        newLocaleData = localeForBrowserLanguage(locales.value || [])
 
-    log.debug('localeStore.setLocale()', newLocaleData)
+      log.debug('localeStore.setLocale()', newLocaleData)
 
-    // Update the translation store, when the locale is different.
-    if (localeData.value?.locale !== newLocaleData.locale) {
-      await useTranslationsStore().load(newLocaleData.locale)
-      localeData.value = newLocaleData
+      // Update the translation store, when the locale is different.
+      if (localeData.value?.locale !== newLocaleData.locale) {
+        await useTranslationsStore().load(newLocaleData.locale)
+        localeData.value = newLocaleData
+
+        document.documentElement.setAttribute('dir', newLocaleData.dir)
+        document.documentElement.setAttribute('lang', newLocaleData.locale)
+      }
+    }
 
-      document.documentElement.setAttribute('dir', newLocaleData.dir)
-      document.documentElement.setAttribute('lang', newLocaleData.locale)
+    return {
+      locales,
+      localeData,
+      setLocale,
+      loadLocales,
     }
-  }
-
-  return {
-    locales,
-    localeData,
-    setLocale,
-    loadLocales,
-  }
-})
+  },
+  {
+    requiresAuth: false,
+  },
+)
 
 export default useLocaleStore

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