Browse Source

Feature: Mobile - Ticket overviews are not updated after the first query.

Dominik Klein 1 year ago
parent
commit
4c23881464

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

@@ -1,7 +1,7 @@
 <!-- Copyright (C) 2012-2023 Zammad Foundation, https://zammad-foundation.org/ -->
 
 <script setup lang="ts">
-import { onBeforeUnmount, onMounted } from 'vue'
+import { onBeforeUnmount, onMounted, watch } from 'vue'
 import { useRouter } from 'vue-router'
 import CommonNotifications from '@shared/components/CommonNotifications/CommonNotifications.vue'
 import { useApplicationStore } from '@shared/stores/application'
@@ -15,12 +15,15 @@ import useFormKitConfig from '@shared/composables/form/useFormKitConfig'
 import { useAppTheme } from '@shared/composables/useAppTheme'
 import useAuthenticationChanges from '@shared/composables/useAuthenticationUpdates'
 import DynamicInitializer from '@shared/components/DynamicInitializer/DynamicInitializer.vue'
-import CommonConfirmation from '@mobile/components/CommonConfirmation/CommonConfirmation.vue'
 import CommonImageViewer from '@shared/components/CommonImageViewer/CommonImageViewer.vue'
+import { useSessionStore } from '@shared/stores/session'
+import CommonConfirmation from '@mobile/components/CommonConfirmation/CommonConfirmation.vue'
+import { useTicketOverviewsStore } from './entities/ticket/stores/ticketOverviews'
 
 const router = useRouter()
 
 const authentication = useAuthenticationStore()
+const session = useSessionStore()
 
 useMetaTitle().initializeMetaTitle()
 
@@ -63,6 +66,18 @@ emitter.on('sessionInvalid', async () => {
   }
 })
 
+// Initialize the ticket overview store after a valid session is present on
+// the app level, so that the query keeps alive.
+watch(
+  () => session.initialized,
+  (newValue, oldValue) => {
+    if (!oldValue && newValue) {
+      useTicketOverviewsStore()
+    }
+  },
+  { immediate: true },
+)
+
 onBeforeUnmount(() => {
   emitter.off('sessionInvalid')
 })

+ 3 - 2
app/frontend/apps/mobile/components/CommonSectionMenu/CommonSectionMenu.vue

@@ -43,7 +43,8 @@ const slots = useSlots()
 
 const hasHelp = computed(() => slots.help || props.help)
 const showLabel = computed(() => {
-  if (!itemsWithPermission.value && !slots.default) return false
+  if (!itemsWithPermission.value && !slots.default && !slots['before-items'])
+    return false
   return slots.header || props.headerLabel || props.actionLabel
 })
 </script>
@@ -64,7 +65,7 @@ const showLabel = computed(() => {
     </component>
   </div>
   <div
-    v-if="itemsWithPermission || $slots.default"
+    v-if="itemsWithPermission || $slots.default || $slots['before-items']"
     class="flex w-full flex-col rounded-xl bg-gray-500 px-3 py-1 text-base text-white"
     :class="{ 'mb-6': !hasHelp }"
     v-bind="$attrs"

+ 32 - 0
app/frontend/apps/mobile/entities/ticket/composables/useTicketOverviews.ts

@@ -0,0 +1,32 @@
+// Copyright (C) 2012-2023 Zammad Foundation, https://zammad-foundation.org/
+
+import { onMounted } from 'vue'
+import { useTimeoutFn } from '@vueuse/shared'
+import { QueryHandler } from '@shared/server/apollo/handler'
+import { useTicketOverviewsStore } from '../stores/ticketOverviews'
+import { useTicketOverviewTicketCountLazyQuery } from '../graphql/queries/ticketOverviewTicketCount.api'
+
+const POLLING_INTERVAL = 60000
+
+export const useTicketOverviews = () => {
+  const overviews = useTicketOverviewsStore()
+
+  const ticketOverviewTicketCountHandler = new QueryHandler(
+    useTicketOverviewTicketCountLazyQuery({
+      pollInterval: POLLING_INTERVAL,
+    }),
+  )
+
+  onMounted(() => {
+    if (!overviews.loading) {
+      ticketOverviewTicketCountHandler.load()
+    } else {
+      useTimeoutFn(
+        () => ticketOverviewTicketCountHandler.load(),
+        POLLING_INTERVAL,
+      )
+    }
+  })
+
+  return overviews
+}

+ 31 - 0
app/frontend/apps/mobile/entities/ticket/graphql/queries/ticketOverviewTicketCount.api.ts

@@ -0,0 +1,31 @@
+import * as Types from '../../../../../../shared/graphql/types';
+
+import gql from 'graphql-tag';
+import * as VueApolloComposable from '@vue/apollo-composable';
+import * as VueCompositionApi from 'vue';
+export type ReactiveFunction<TParam> = () => TParam;
+
+export const TicketOverviewTicketCountDocument = gql`
+    query ticketOverviewTicketCount {
+  ticketOverviews {
+    edges {
+      node {
+        id
+        ticketCount
+      }
+      cursor
+    }
+    pageInfo {
+      endCursor
+      hasNextPage
+    }
+  }
+}
+    `;
+export function useTicketOverviewTicketCountQuery(options: VueApolloComposable.UseQueryOptions<Types.TicketOverviewTicketCountQuery, Types.TicketOverviewTicketCountQueryVariables> | VueCompositionApi.Ref<VueApolloComposable.UseQueryOptions<Types.TicketOverviewTicketCountQuery, Types.TicketOverviewTicketCountQueryVariables>> | ReactiveFunction<VueApolloComposable.UseQueryOptions<Types.TicketOverviewTicketCountQuery, Types.TicketOverviewTicketCountQueryVariables>> = {}) {
+  return VueApolloComposable.useQuery<Types.TicketOverviewTicketCountQuery, Types.TicketOverviewTicketCountQueryVariables>(TicketOverviewTicketCountDocument, {}, options);
+}
+export function useTicketOverviewTicketCountLazyQuery(options: VueApolloComposable.UseQueryOptions<Types.TicketOverviewTicketCountQuery, Types.TicketOverviewTicketCountQueryVariables> | VueCompositionApi.Ref<VueApolloComposable.UseQueryOptions<Types.TicketOverviewTicketCountQuery, Types.TicketOverviewTicketCountQueryVariables>> | ReactiveFunction<VueApolloComposable.UseQueryOptions<Types.TicketOverviewTicketCountQuery, Types.TicketOverviewTicketCountQueryVariables>> = {}) {
+  return VueApolloComposable.useLazyQuery<Types.TicketOverviewTicketCountQuery, Types.TicketOverviewTicketCountQueryVariables>(TicketOverviewTicketCountDocument, {}, options);
+}
+export type TicketOverviewTicketCountQueryCompositionFunctionResult = VueApolloComposable.UseQueryReturn<Types.TicketOverviewTicketCountQuery, Types.TicketOverviewTicketCountQueryVariables>;

+ 15 - 0
app/frontend/apps/mobile/entities/ticket/graphql/queries/ticketOverviewTicketCount.graphql

@@ -0,0 +1,15 @@
+query ticketOverviewTicketCount {
+  ticketOverviews {
+    edges {
+      node {
+        id
+        ticketCount
+      }
+      cursor
+    }
+    pageInfo {
+      endCursor
+      hasNextPage
+    }
+  }
+}

+ 30 - 0
app/frontend/apps/mobile/entities/ticket/graphql/subscriptions/ticketOverviewUpdates.api.ts

@@ -0,0 +1,30 @@
+import * as Types from '../../../../../../shared/graphql/types';
+
+import gql from 'graphql-tag';
+import { OverviewAttributesFragmentDoc } from '../../../../../../shared/entities/ticket/graphql/fragments/overviewAttributes.api';
+import * as VueApolloComposable from '@vue/apollo-composable';
+import * as VueCompositionApi from 'vue';
+export type ReactiveFunction<TParam> = () => TParam;
+
+export const TicketOverviewUpdatesDocument = gql`
+    subscription ticketOverviewUpdates($withTicketCount: Boolean!) {
+  ticketOverviewUpdates {
+    ticketOverviews {
+      edges {
+        node {
+          ...overviewAttributes
+        }
+        cursor
+      }
+      pageInfo {
+        endCursor
+        hasNextPage
+      }
+    }
+  }
+}
+    ${OverviewAttributesFragmentDoc}`;
+export function useTicketOverviewUpdatesSubscription(variables: Types.TicketOverviewUpdatesSubscriptionVariables | VueCompositionApi.Ref<Types.TicketOverviewUpdatesSubscriptionVariables> | ReactiveFunction<Types.TicketOverviewUpdatesSubscriptionVariables>, options: VueApolloComposable.UseSubscriptionOptions<Types.TicketOverviewUpdatesSubscription, Types.TicketOverviewUpdatesSubscriptionVariables> | VueCompositionApi.Ref<VueApolloComposable.UseSubscriptionOptions<Types.TicketOverviewUpdatesSubscription, Types.TicketOverviewUpdatesSubscriptionVariables>> | ReactiveFunction<VueApolloComposable.UseSubscriptionOptions<Types.TicketOverviewUpdatesSubscription, Types.TicketOverviewUpdatesSubscriptionVariables>> = {}) {
+  return VueApolloComposable.useSubscription<Types.TicketOverviewUpdatesSubscription, Types.TicketOverviewUpdatesSubscriptionVariables>(TicketOverviewUpdatesDocument, variables, options);
+}
+export type TicketOverviewUpdatesSubscriptionCompositionFunctionResult = VueApolloComposable.UseSubscriptionReturn<Types.TicketOverviewUpdatesSubscription, Types.TicketOverviewUpdatesSubscriptionVariables>;

+ 16 - 0
app/frontend/apps/mobile/entities/ticket/graphql/subscriptions/ticketOverviewUpdates.graphql

@@ -0,0 +1,16 @@
+subscription ticketOverviewUpdates($withTicketCount: Boolean!) {
+  ticketOverviewUpdates {
+    ticketOverviews {
+      edges {
+        node {
+          ...overviewAttributes
+        }
+        cursor
+      }
+      pageInfo {
+        endCursor
+        hasNextPage
+      }
+    }
+  }
+}

+ 7 - 1
app/frontend/apps/mobile/entities/ticket/helpers/ticketOverviewStorage.ts

@@ -5,13 +5,19 @@ import { useSessionStore } from '@shared/stores/session'
 export const getTicketOverviewStorage = () => {
   const session = useSessionStore()
 
-  const LOCAL_STORAGE_NAME = `ticket-overviews-${session.user?.id || '1'}`
+  const LOCAL_STORAGE_NAME = session.user?.id
+    ? `ticket-overviews-${session.user.id}`
+    : null
 
   const getOverviews = (): string[] => {
+    if (!LOCAL_STORAGE_NAME) return []
+
     return JSON.parse(localStorage.getItem(LOCAL_STORAGE_NAME) || '[]')
   }
 
   const saveOverviews = (overviews: string[]) => {
+    if (!LOCAL_STORAGE_NAME) return
+
     return localStorage.setItem(LOCAL_STORAGE_NAME, JSON.stringify(overviews))
   }
 

+ 41 - 7
app/frontend/apps/mobile/entities/ticket/stores/ticketOverviews.ts

@@ -1,14 +1,19 @@
 // Copyright (C) 2012-2023 Zammad Foundation, https://zammad-foundation.org/
 
+import { keyBy } from 'lodash-es'
 import { defineStore } from 'pinia'
+import { ref, computed } from 'vue'
+import { tryOnScopeDispose, watchOnce } from '@vueuse/core'
 import { QueryHandler } from '@shared/server/apollo/handler'
 import { useTicketOverviewsQuery } from '@shared/entities/ticket/graphql/queries/ticket/overviews.api'
-import type { TicketOverviewsQuery } from '@shared/graphql/types'
-import { ref, computed } from 'vue'
-import { keyBy } from 'lodash-es'
-import { watchOnce } from '@vueuse/core'
+import type {
+  TicketOverviewsQuery,
+  TicketOverviewUpdatesSubscription,
+  TicketOverviewUpdatesSubscriptionVariables,
+} from '@shared/graphql/types'
 import type { ConfidentTake } from '@shared/types/utils'
 import { getTicketOverviewStorage } from '../helpers/ticketOverviewStorage'
+import { TicketOverviewUpdatesDocument } from '../graphql/subscriptions/ticketOverviewUpdates.api'
 
 export type TicketOverview = ConfidentTake<
   TicketOverviewsQuery,
@@ -16,11 +21,35 @@ export type TicketOverview = ConfidentTake<
 >
 
 export const useTicketOverviewsStore = defineStore('ticketOverviews', () => {
-  const handler = new QueryHandler(
+  const ticketOverviewHandler = new QueryHandler(
     useTicketOverviewsQuery({ withTicketCount: true }),
   )
-  const overviewsRaw = handler.result()
-  const overviewsLoading = handler.loading()
+
+  // Updates the overviews when overviews got added, updated and/or deleted.
+  ticketOverviewHandler.subscribeToMore<
+    TicketOverviewUpdatesSubscriptionVariables,
+    TicketOverviewUpdatesSubscription
+  >({
+    document: TicketOverviewUpdatesDocument,
+    variables: {
+      withTicketCount: true,
+    },
+    updateQuery(_, { subscriptionData }) {
+      const ticketOverviews =
+        subscriptionData.data.ticketOverviewUpdates?.ticketOverviews
+      // if we return empty array here, the actual query will be aborted, because we have fetchPolicy "cache-and-network"
+      // if we return existing value, it will throw an error, because "overviews" doesn't exist yet on the query result
+      if (!ticketOverviews) {
+        return null as unknown as TicketOverviewsQuery
+      }
+      return {
+        ticketOverviews,
+      }
+    },
+  })
+
+  const overviewsRaw = ticketOverviewHandler.result()
+  const overviewsLoading = ticketOverviewHandler.loading()
 
   const overviews = computed(() => {
     if (!overviewsRaw.value?.ticketOverviews.edges) return []
@@ -66,8 +95,13 @@ export const useTicketOverviewsStore = defineStore('ticketOverviews', () => {
     }
   }
 
+  tryOnScopeDispose(() => {
+    ticketOverviewHandler.stop()
+  })
+
   return {
     overviews,
+    initializing: ticketOverviewHandler.operationResult.forceDisabled.value,
     loading: overviewsLoading,
     includedOverviews,
     includedIds,

+ 3 - 3
app/frontend/apps/mobile/initialize.ts

@@ -8,10 +8,10 @@ import '@mobile/styles/main.scss'
 
 import initializeStore from '@shared/stores'
 import initializeGlobalComponents from '@shared/initializer/globalComponents'
-import initializeForm from '@mobile/form'
-import initializeGlobalProperties from '@shared/initializer/globalProperties'
 import { initializeAppName } from '@shared/composables/useAppName'
-import { initializeObjectAttributes } from './object-attributes/initializeObjectAttributes'
+import initializeGlobalProperties from '@shared/initializer/globalProperties'
+import initializeForm from '@mobile/form'
+import { initializeObjectAttributes } from './initializer/objectAttributes'
 
 export default function initializeApp(app: App) {
   // TODO remove when Vue 3.3 released

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