ticketOverviews.ts 10.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341
  1. // Copyright (C) 2012-2025 Zammad Foundation, https://zammad-foundation.org/
  2. import { useLocalStorage } from '@vueuse/core'
  3. import { defaultsDeep, isEqual } from 'lodash-es'
  4. import { acceptHMRUpdate, defineStore, storeToRefs } from 'pinia'
  5. import {
  6. computed,
  7. effectScope,
  8. markRaw,
  9. onScopeDispose,
  10. ref,
  11. watch,
  12. type Raw,
  13. } from 'vue'
  14. import { useQueryPolling } from '#shared/composables/useQueryPolling.ts'
  15. import type {
  16. TicketsCachedByOverviewQuery,
  17. TicketsCachedByOverviewQueryVariables,
  18. } from '#shared/graphql/types.ts'
  19. import {
  20. MutationHandler,
  21. QueryHandler,
  22. } from '#shared/server/apollo/handler/index.ts'
  23. import { useApplicationStore } from '#shared/stores/application.ts'
  24. import { useSessionStore } from '#shared/stores/session.ts'
  25. import { useUserCurrentOverviewUpdateLastUsedMutation } from '#desktop/entities/ticket/graphql/mutations/userCurrentOverviewUpdateLastUsed.api.ts'
  26. import { useTicketsCountByOverview } from '#desktop/entities/ticket/stores/composables/useTicketsCountByOverview.ts'
  27. import { useUserCurrentTicketOverviews } from '#desktop/entities/ticket/stores/composables/useUserCurrentTicketOverviews.ts'
  28. import { useTicketsCachedByOverviewCache } from '../composables/useTicketsCachedByOverviewCache.ts'
  29. import { useTicketsCachedByOverviewLazyQuery } from '../graphql/queries/ticketsCachedByOverview.api.ts'
  30. import type {
  31. TicketsByOverviewHandlerItem,
  32. TicketOverviewQueryPollingConfig,
  33. } from './types.ts'
  34. const DEFAULT_CONFIG: TicketOverviewQueryPollingConfig = {
  35. enabled: true,
  36. page_size: 30,
  37. background: {
  38. calculation_count: 3,
  39. interval_sec: 10,
  40. cache_ttl_sec: 10,
  41. },
  42. foreground: {
  43. interval_sec: 5,
  44. cache_ttl_sec: 5,
  45. },
  46. counts: {
  47. interval_sec: 60,
  48. cache_ttl_sec: 60,
  49. },
  50. }
  51. export const useTicketOverviewsStore = defineStore('ticketOverviews', () => {
  52. const { user } = storeToRefs(useSessionStore())
  53. const { config } = storeToRefs(useApplicationStore())
  54. const localConfig = useLocalStorage(
  55. `${user.value?.id}-ticket-overview-query-polling`,
  56. {}, // no local overrides by default
  57. )
  58. const queryPollingConfig = computed<TicketOverviewQueryPollingConfig>(() => {
  59. const serverConfig = config.value?.ui_ticket_overview_query_polling ?? {}
  60. return defaultsDeep({}, localConfig.value, serverConfig, DEFAULT_CONFIG)
  61. })
  62. // Register window.setQueryPollingConfig to allow for manual override for debugging.
  63. window.setQueryPollingConfig = (
  64. c?: Partial<TicketOverviewQueryPollingConfig>,
  65. ): TicketOverviewQueryPollingConfig => {
  66. if (c) localConfig.value = c
  67. return queryPollingConfig.value
  68. }
  69. const {
  70. overviews,
  71. lastUsedOverviews,
  72. overviewsSortedByLastUsedIds,
  73. overviewsLoading,
  74. overviewsByLink,
  75. overviewIds,
  76. overviewsById,
  77. hasOverviews,
  78. lastTicketOverviewLink,
  79. currentTicketOverviewLink,
  80. setCurrentTicketOverviewLink,
  81. } = useUserCurrentTicketOverviews()
  82. const overviewBackgroundPollingIds = computed<ID[]>((currentIds) => {
  83. if (
  84. !hasOverviews.value ||
  85. !queryPollingConfig.value.enabled ||
  86. !queryPollingConfig.value.background.calculation_count
  87. )
  88. return []
  89. let backgroundIds = overviewsSortedByLastUsedIds.value.slice(
  90. 0,
  91. queryPollingConfig.value.background.calculation_count,
  92. )
  93. if (!backgroundIds.length && currentTicketOverviewLink.value) {
  94. backgroundIds.push(overviews.value[0].id)
  95. }
  96. backgroundIds = backgroundIds.filter(
  97. (id) => id !== overviewsByLink.value[currentTicketOverviewLink.value]?.id,
  98. )
  99. if (currentIds && isEqual(currentIds, backgroundIds)) return currentIds
  100. return backgroundIds
  101. })
  102. const overviewBackgroundCountPollingIds = computed<ID[]>((currentIds) => {
  103. if (!hasOverviews.value || !queryPollingConfig.value.enabled) return []
  104. const backgroundIds = overviewBackgroundPollingIds.value || []
  105. const remainingIds = overviewIds.value.filter(
  106. (id) =>
  107. !backgroundIds.includes(id) &&
  108. id !== overviewsByLink.value[currentTicketOverviewLink.value]?.id,
  109. )
  110. if (currentIds && isEqual(currentIds, remainingIds)) return currentIds
  111. return remainingIds
  112. })
  113. const { overviewsTicketCountById, overviewsTicketCount } =
  114. useTicketsCountByOverview(
  115. overviewIds,
  116. overviewBackgroundCountPollingIds,
  117. queryPollingConfig,
  118. )
  119. const ticketsByOverviewHandler = ref(
  120. new Map<ID, Raw<TicketsByOverviewHandlerItem>>(),
  121. )
  122. const { readTicketsByOverviewCache } = useTicketsCachedByOverviewCache()
  123. const addTicketByOverviewHandler = (overviewId: ID) => {
  124. const overview = overviewsById.value[overviewId]
  125. if (!overview) return
  126. const scope = effectScope(true)
  127. const result = scope.run(
  128. (): {
  129. handler: QueryHandler<
  130. TicketsCachedByOverviewQuery,
  131. TicketsCachedByOverviewQueryVariables
  132. >
  133. } => {
  134. // TODO: maybe we can use same variables here and afterwards?
  135. const cachedTickets = readTicketsByOverviewCache({
  136. overviewId,
  137. orderBy: overviewsById.value[overviewId].orderBy,
  138. orderDirection: overviewsById.value[overviewId].orderDirection,
  139. cacheTtl: queryPollingConfig.value.background.cache_ttl_sec,
  140. })
  141. const cachedCollectionSignature =
  142. cachedTickets?.ticketsCachedByOverview?.collectionSignature
  143. const ticketsQuery = new QueryHandler(
  144. useTicketsCachedByOverviewLazyQuery(
  145. () => ({
  146. pageSize: queryPollingConfig.value.page_size,
  147. overviewId,
  148. orderBy: overviewsById.value[overviewId].orderBy,
  149. orderDirection: overviewsById.value[overviewId].orderDirection,
  150. cacheTtl: queryPollingConfig.value.background.cache_ttl_sec,
  151. knownCollectionSignature: cachedCollectionSignature,
  152. }),
  153. {
  154. fetchPolicy: 'network-only',
  155. context: {
  156. batch: {
  157. active: false,
  158. },
  159. },
  160. },
  161. ),
  162. )
  163. if (
  164. lastTicketOverviewLink.value &&
  165. overviewsByLink.value[lastTicketOverviewLink.value]
  166. ) {
  167. // Delay the background polling when it was the previous foreground overview.
  168. const delayStartTimer = setTimeout(() => {
  169. ticketsQuery.load()
  170. }, queryPollingConfig.value.foreground.interval_sec * 1000)
  171. onScopeDispose(() => {
  172. clearTimeout(delayStartTimer)
  173. })
  174. } else {
  175. ticketsQuery.load()
  176. }
  177. const ticketsResult = ticketsQuery.result()
  178. const currentCollectionSignature = computed(() => {
  179. return ticketsResult.value?.ticketsCachedByOverview
  180. ?.collectionSignature
  181. })
  182. const { startPolling } = useQueryPolling(
  183. ticketsQuery,
  184. () => queryPollingConfig.value.background.interval_sec * 1000,
  185. () => ({
  186. knownCollectionSignature: currentCollectionSignature.value,
  187. }),
  188. {
  189. randomize: true,
  190. },
  191. )
  192. ticketsQuery.watchOnceOnResult(startPolling)
  193. return {
  194. handler: ticketsQuery,
  195. }
  196. },
  197. )
  198. if (!result) return scope.stop()
  199. ticketsByOverviewHandler.value.set(
  200. overview.id,
  201. markRaw({
  202. queryHandler: result.handler,
  203. scope,
  204. }),
  205. )
  206. }
  207. const removeTicketByOverviewHandler = (overviewId: ID) => {
  208. const handler = ticketsByOverviewHandler.value.get(overviewId)
  209. if (!handler) return
  210. handler.scope.stop()
  211. ticketsByOverviewHandler.value.delete(overviewId)
  212. }
  213. watch(
  214. overviewBackgroundPollingIds,
  215. (newBackgroundIds) => {
  216. // Get currently active handler IDs
  217. const currentIds = Array.from(ticketsByOverviewHandler.value.keys())
  218. // Remove handlers that are no longer in background polling
  219. currentIds.forEach((id) => {
  220. if (!newBackgroundIds.includes(id)) {
  221. removeTicketByOverviewHandler(id)
  222. }
  223. })
  224. // Add new handlers for IDs that aren't currently being handled
  225. newBackgroundIds.forEach((id) => {
  226. if (!ticketsByOverviewHandler.value.has(id)) {
  227. addTicketByOverviewHandler(id)
  228. }
  229. })
  230. },
  231. { immediate: true },
  232. )
  233. const useUserCurrentOverviewUpdateLastUsedMutationHandler =
  234. new MutationHandler(useUserCurrentOverviewUpdateLastUsedMutation())
  235. const updateLastUsedOverview = async (overviewId: ID) => {
  236. const newOverviewsLastUsed = {
  237. ...lastUsedOverviews.value,
  238. [overviewId]: new Date().toISOString(),
  239. }
  240. // Update user preferences with the new mapping using overviewsById
  241. user.value!.preferences.overviews_last_used = Object.fromEntries(
  242. Object.entries(newOverviewsLastUsed).map(([overviewId, lastUsedAt]) => [
  243. overviewsById.value[overviewId].internalId,
  244. lastUsedAt,
  245. ]),
  246. )
  247. const mappedOverviewsLastUsed = Object.entries(newOverviewsLastUsed).map(
  248. ([overviewId, lastUsedAt]) => ({
  249. overviewId,
  250. lastUsedAt,
  251. }),
  252. )
  253. await useUserCurrentOverviewUpdateLastUsedMutationHandler.send({
  254. overviewsLastUsed: mappedOverviewsLastUsed,
  255. })
  256. }
  257. watch(currentTicketOverviewLink, () => {
  258. const overviewId =
  259. overviewsByLink.value[currentTicketOverviewLink.value]?.id
  260. if (!overviewId) return
  261. updateLastUsedOverview(overviewId)
  262. })
  263. return {
  264. queryPollingConfig,
  265. overviews,
  266. overviewsTicketCountById,
  267. overviewsById,
  268. overviewsByLink,
  269. overviewsTicketCount,
  270. overviewsLoading,
  271. hasOverviews,
  272. currentTicketOverviewLink,
  273. setCurrentTicketOverviewLink,
  274. ticketsByOverviewHandler,
  275. lastUsedOverviews,
  276. overviewsSortedByLastUsedIds,
  277. overviewBackgroundPollingIds,
  278. overviewBackgroundCountPollingIds,
  279. updateLastUsedOverview,
  280. addTicketByOverviewHandler, // returned to be able to test them
  281. removeTicketByOverviewHandler, // returned to be able to test them
  282. }
  283. })
  284. if (import.meta.hot) {
  285. import.meta.hot.accept(
  286. acceptHMRUpdate(useTicketOverviewsStore, import.meta.hot),
  287. )
  288. }