TicketDetailArticlesView.vue 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167
  1. <!-- Copyright (C) 2012-2023 Zammad Foundation, https://zammad-foundation.org/ -->
  2. <script setup lang="ts">
  3. import { computed, watch } from 'vue'
  4. import { useHeader } from '@mobile/composables/useHeader'
  5. import CommonLoader from '@mobile/components/CommonLoader/CommonLoader.vue'
  6. import { QueryHandler } from '@shared/server/apollo/handler'
  7. import { useApplicationStore } from '@shared/stores/application'
  8. import { convertToGraphQLId } from '@shared/graphql/utils'
  9. import type { TicketArticle } from '@shared/entities/ticket/types'
  10. import { useTicketView } from '@shared/entities/ticket/composables/useTicketView'
  11. import type {
  12. TicketArticleUpdatesSubscription,
  13. TicketArticleUpdatesSubscriptionVariables,
  14. } from '@shared/graphql/types'
  15. import { noop } from 'lodash-es'
  16. import TicketHeader from '../components/TicketDetailView/TicketDetailViewHeader.vue'
  17. import TicketTitle from '../components/TicketDetailView/TicketDetailViewTitle.vue'
  18. import TicketArticlesList from '../components/TicketDetailView/ArticlesList.vue'
  19. import TicketReplyButton from '../components/TicketDetailView/TicketDetailViewReplyButton.vue'
  20. import { useTicketArticlesQuery } from '../graphql/queries/ticket/articles.api'
  21. import { useTicketInformation } from '../composable/useTicketInformation'
  22. import { TicketArticleUpdatesDocument } from '../graphql/subscriptions/ticketArticlesUpdates.api'
  23. interface Props {
  24. internalId: string
  25. }
  26. const props = defineProps<Props>()
  27. const application = useApplicationStore()
  28. const articlesQuery = new QueryHandler(
  29. useTicketArticlesQuery(() => ({
  30. ticketId: convertToGraphQLId('Ticket', props.internalId),
  31. pageSize: Number(application.config.ticket_articles_min ?? 5),
  32. })),
  33. { errorShowNotification: false },
  34. )
  35. const result = articlesQuery.result()
  36. articlesQuery.subscribeToMore<
  37. TicketArticleUpdatesSubscriptionVariables,
  38. TicketArticleUpdatesSubscription
  39. >({
  40. document: TicketArticleUpdatesDocument,
  41. variables: {
  42. ticketId: convertToGraphQLId('Ticket', props.internalId),
  43. },
  44. onError: noop,
  45. updateQuery(previous, { subscriptionData }) {
  46. const updates = subscriptionData.data.ticketArticleUpdates
  47. if (updates.deletedArticleId) {
  48. const edges = previous.articles.edges.filter(
  49. (edge) => edge.node.id !== updates.deletedArticleId,
  50. )
  51. return {
  52. ...previous,
  53. articles: {
  54. ...previous.articles,
  55. edges,
  56. totalCount: previous.articles.totalCount - 1,
  57. },
  58. }
  59. }
  60. if (updates.createdArticle) {
  61. articlesQuery.fetchMore({
  62. variables: {
  63. pageSize: null,
  64. loadDescription: false,
  65. afterCursor: result.value?.articles.pageInfo.endCursor,
  66. },
  67. })
  68. }
  69. return previous
  70. },
  71. })
  72. const { ticket, liveUserList, ticketQuery } = useTicketInformation()
  73. const { isTicketEditable } = useTicketView(ticket)
  74. const isLoadingTicket = ticketQuery.loading()
  75. const totalCount = computed(() => result.value?.articles.totalCount || 0)
  76. const toMs = (date: string) => new Date(date).getTime()
  77. const articles = computed(() => {
  78. if (!result.value) {
  79. return []
  80. }
  81. const nodes = result.value.articles.edges.map(({ node }) => node) || []
  82. const totalCount = result.value.articles.totalCount || 0
  83. // description might've returned with "articles"
  84. const description = result.value.description.edges[0]?.node
  85. if (totalCount > nodes.length && description) {
  86. nodes.unshift(description)
  87. }
  88. return nodes
  89. .filter((a): a is TicketArticle => a != null)
  90. .sort((a, b) => toMs(a.createdAt) - toMs(b.createdAt))
  91. })
  92. useHeader({
  93. title: computed(() => {
  94. if (!ticket.value) return ''
  95. const { number, title } = ticket.value
  96. return `#${number} - ${title}`
  97. }),
  98. })
  99. watch(
  100. () => articles.value.length,
  101. (length) => {
  102. if (!length) return
  103. requestAnimationFrame(() => {
  104. window.scrollTo({
  105. behavior: 'smooth',
  106. top: window.innerHeight,
  107. })
  108. })
  109. },
  110. { immediate: true },
  111. )
  112. const loadPreviousArticles = async () => {
  113. await articlesQuery.fetchMore({
  114. variables: {
  115. pageSize: null,
  116. loadDescription: false,
  117. beforeCursor: result.value?.articles.pageInfo.startCursor,
  118. },
  119. })
  120. }
  121. </script>
  122. <template>
  123. <div class="flex min-h-[calc(100vh_-_5rem)] flex-col pb-20">
  124. <TicketHeader
  125. :ticket="ticket"
  126. :live-user-list="liveUserList"
  127. :loading-ticket="isLoadingTicket"
  128. />
  129. <CommonLoader
  130. :loading="isLoadingTicket"
  131. data-test-id="loader-title"
  132. class="flex border-b-[0.5px] border-white/10 bg-gray-600/90 py-5 px-4"
  133. >
  134. <TicketTitle v-if="ticket" :ticket="ticket" />
  135. </CommonLoader>
  136. <CommonLoader
  137. data-test-id="loader-list"
  138. :loading="isLoadingTicket"
  139. class="mt-2"
  140. >
  141. <TicketArticlesList
  142. v-if="ticket"
  143. :ticket="ticket"
  144. :articles="articles"
  145. :total-count="totalCount"
  146. @load-previous="loadPreviousArticles"
  147. />
  148. </CommonLoader>
  149. </div>
  150. <TicketReplyButton v-if="isTicketEditable" />
  151. </template>