useArticleDataHandler.ts 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173
  1. // Copyright (C) 2012-2024 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. ticketId: ticketId.value,
  39. pageSize: options.pageSize || 20,
  40. firstArticlesCount: firstArticlesCount.value,
  41. })),
  42. )
  43. const articleResult = articlesQuery.result()
  44. const articleData = computed(() => articleResult.value)
  45. const allArticleLoaded = computed(() => {
  46. if (!articleResult.value?.articles.totalCount) return false
  47. return (
  48. articleResult.value?.articles.edges.length <
  49. articleResult.value?.articles.totalCount
  50. )
  51. })
  52. const refetchArticlesQuery = (pageSize: Maybe<number>) => {
  53. articlesQuery.refetch({
  54. ticketId: ticketId.value,
  55. pageSize,
  56. })
  57. }
  58. const articleLoading = articlesQuery.loading()
  59. const isLoadingArticles = computed(() => {
  60. // Return already true when a article result already exists from the cache, also
  61. // when maybe a loading is in progress(because of cache + network).
  62. if (articleData.value !== undefined) return false
  63. return articleLoading.value
  64. })
  65. const adjustPageInfoAfterDeletion = (nextEndCursorEdge?: Maybe<string>) => {
  66. const newPageInfo: Pick<PageInfo, 'startCursor' | 'endCursor'> = {}
  67. if (nextEndCursorEdge) {
  68. newPageInfo.endCursor = nextEndCursorEdge
  69. } else {
  70. newPageInfo.startCursor = null
  71. newPageInfo.endCursor = null
  72. }
  73. return newPageInfo
  74. }
  75. articlesQuery.subscribeToMore<
  76. TicketArticleUpdatesSubscriptionVariables,
  77. TicketArticleUpdatesSubscription
  78. >(() => ({
  79. document: TicketArticleUpdatesDocument,
  80. variables: {
  81. ticketId: ticketId.value,
  82. },
  83. onError: noop,
  84. updateQuery(previous, { subscriptionData }) {
  85. const updates = subscriptionData.data.ticketArticleUpdates
  86. if (!previous.articles || updates.updateArticle) return previous
  87. const previousArticlesEdges = previous.articles.edges
  88. const previousArticlesEdgesCount = previousArticlesEdges.length
  89. if (updates.removeArticleId) {
  90. const edges = previousArticlesEdges.filter(
  91. (edge) => edge.node.id !== updates.removeArticleId,
  92. )
  93. const removedArticleVisible =
  94. edges.length !== previousArticlesEdgesCount
  95. if (removedArticleVisible && !allArticleLoaded.value) {
  96. refetchArticlesQuery(firstArticlesCount.value)
  97. return previous
  98. }
  99. const result = {
  100. ...previous,
  101. articles: {
  102. ...previous.articles,
  103. edges,
  104. totalCount: previous.articles.totalCount - 1,
  105. },
  106. }
  107. if (removedArticleVisible) {
  108. const nextEndCursorEdge =
  109. previousArticlesEdges[previousArticlesEdgesCount - 2]
  110. result.articles.pageInfo = {
  111. ...previous.articles.pageInfo,
  112. ...adjustPageInfoAfterDeletion(nextEndCursorEdge.cursor),
  113. }
  114. }
  115. // Trigger cache garbage collection after the returned article deletion subscription
  116. // updated the article list.
  117. nextTick(() => {
  118. getApolloClient().cache.gc()
  119. })
  120. return result
  121. }
  122. if (updates.addArticle) {
  123. options?.onAddArticleCallback?.({
  124. updates,
  125. previousArticlesEdges,
  126. previousArticlesEdgesCount,
  127. articlesQuery,
  128. result: articleResult,
  129. allArticleLoaded,
  130. refetchArticlesQuery,
  131. })
  132. }
  133. return previous
  134. },
  135. }))
  136. return {
  137. articlesQuery,
  138. articleResult,
  139. articleData,
  140. allArticleLoaded,
  141. isLoadingArticles,
  142. refetchArticlesQuery,
  143. }
  144. }