useArticleDataHandler.ts 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182
  1. // Copyright (C) 2012-2025 Zammad Foundation, https://zammad-foundation.org/
  2. import { noop } from 'lodash-es'
  3. import { computed, type ComputedRef, nextTick, type Ref } from 'vue'
  4. import { useTicketArticlesQuery } from '#shared/entities/ticket/graphql/queries/ticket/articles.api.ts'
  5. import { TicketArticleUpdatesDocument } from '#shared/entities/ticket/graphql/subscriptions/ticketArticlesUpdates.api.ts'
  6. import type {
  7. PageInfo,
  8. TicketArticlesQuery,
  9. TicketArticleUpdatesSubscription,
  10. TicketArticleUpdatesSubscriptionVariables,
  11. } from '#shared/graphql/types.ts'
  12. import { getApolloClient } from '#shared/server/apollo/client.ts'
  13. import { QueryHandler } from '#shared/server/apollo/handler/index.ts'
  14. export interface AddArticleCallbackArgs {
  15. updates: TicketArticleUpdatesSubscription['ticketArticleUpdates']
  16. previousArticlesEdges: TicketArticlesQuery['articles']['edges']
  17. previousArticlesEdgesCount: number
  18. articlesQuery: unknown // :TODO type this query sustainable
  19. result: Ref<TicketArticlesQuery | undefined>
  20. allArticleLoaded: ComputedRef<boolean>
  21. refetchArticlesQuery: (pageSize: Maybe<number>) => void
  22. }
  23. export const useArticleDataHandler = (
  24. ticketId: Ref<string>,
  25. options: {
  26. pageSize: number
  27. firstArticlesCount?: Ref<number>
  28. onAddArticleCallback?: (args: AddArticleCallbackArgs) => void
  29. } = {
  30. pageSize: 20,
  31. },
  32. ) => {
  33. const firstArticlesCount = computed(
  34. () => options.firstArticlesCount?.value || 5,
  35. )
  36. const articlesQuery = new QueryHandler(
  37. useTicketArticlesQuery(
  38. () => ({
  39. ticketId: ticketId.value,
  40. pageSize: options.pageSize || 20,
  41. firstArticlesCount: firstArticlesCount.value,
  42. }),
  43. {
  44. context: {
  45. batch: {
  46. active: false,
  47. },
  48. },
  49. },
  50. ),
  51. )
  52. const articleResult = articlesQuery.result()
  53. const articleData = computed(() => articleResult.value)
  54. const allArticleLoaded = computed(() => {
  55. if (!articleResult.value?.articles.totalCount) return false
  56. return (
  57. articleResult.value?.articles.edges.length <
  58. articleResult.value?.articles.totalCount
  59. )
  60. })
  61. const refetchArticlesQuery = (pageSize: Maybe<number>) => {
  62. articlesQuery.refetch({
  63. ticketId: ticketId.value,
  64. pageSize,
  65. })
  66. }
  67. const articleLoading = articlesQuery.loading()
  68. const isLoadingArticles = computed(() => {
  69. // Return already true when a article result already exists from the cache, also
  70. // when maybe a loading is in progress(because of cache + network).
  71. if (articleData.value !== undefined) return false
  72. return articleLoading.value
  73. })
  74. const adjustPageInfoAfterDeletion = (nextEndCursorEdge?: Maybe<string>) => {
  75. const newPageInfo: Pick<PageInfo, 'startCursor' | 'endCursor'> = {}
  76. if (nextEndCursorEdge) {
  77. newPageInfo.endCursor = nextEndCursorEdge
  78. } else {
  79. newPageInfo.startCursor = null
  80. newPageInfo.endCursor = null
  81. }
  82. return newPageInfo
  83. }
  84. articlesQuery.subscribeToMore<
  85. TicketArticleUpdatesSubscriptionVariables,
  86. TicketArticleUpdatesSubscription
  87. >(() => ({
  88. document: TicketArticleUpdatesDocument,
  89. variables: {
  90. ticketId: ticketId.value,
  91. },
  92. onError: noop,
  93. updateQuery(previous, { subscriptionData }) {
  94. const updates = subscriptionData.data.ticketArticleUpdates
  95. if (!previous.articles || updates.updateArticle) return previous
  96. const previousArticlesEdges = previous.articles.edges
  97. const previousArticlesEdgesCount = previousArticlesEdges.length
  98. if (updates.removeArticleId) {
  99. const edges = previousArticlesEdges.filter(
  100. (edge) => edge.node.id !== updates.removeArticleId,
  101. )
  102. const removedArticleVisible =
  103. edges.length !== previousArticlesEdgesCount
  104. if (removedArticleVisible && !allArticleLoaded.value) {
  105. refetchArticlesQuery(firstArticlesCount.value)
  106. return previous
  107. }
  108. const result = {
  109. ...previous,
  110. articles: {
  111. ...previous.articles,
  112. edges,
  113. totalCount: previous.articles.totalCount - 1,
  114. },
  115. }
  116. if (removedArticleVisible) {
  117. const nextEndCursorEdge =
  118. previousArticlesEdges[previousArticlesEdgesCount - 2]
  119. result.articles.pageInfo = {
  120. ...previous.articles.pageInfo,
  121. ...adjustPageInfoAfterDeletion(nextEndCursorEdge.cursor),
  122. }
  123. }
  124. // Trigger cache garbage collection after the returned article deletion subscription
  125. // updated the article list.
  126. nextTick(() => {
  127. getApolloClient().cache.gc()
  128. })
  129. return result
  130. }
  131. if (updates.addArticle) {
  132. options?.onAddArticleCallback?.({
  133. updates,
  134. previousArticlesEdges,
  135. previousArticlesEdgesCount,
  136. articlesQuery,
  137. result: articleResult,
  138. allArticleLoaded,
  139. refetchArticlesQuery,
  140. })
  141. }
  142. return previous
  143. },
  144. }))
  145. return {
  146. articlesQuery,
  147. articleResult,
  148. articleData,
  149. allArticleLoaded,
  150. isLoadingArticles,
  151. refetchArticlesQuery,
  152. }
  153. }