useTicketArticleContext.ts 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168
  1. // Copyright (C) 2012-2023 Zammad Foundation, https://zammad-foundation.org/
  2. import type { PopupItem } from '@mobile/components/CommonSectionPopup'
  3. import { useDialog } from '@shared/composables/useDialog'
  4. import { computed, nextTick, ref, shallowRef } from 'vue'
  5. import type { TicketArticle, TicketById } from '@shared/entities/ticket/types'
  6. import { createArticleActions } from '@shared/entities/ticket-article/action/plugins'
  7. import type { TicketArticlePerformOptions } from '@shared/entities/ticket-article/action/plugins/types'
  8. import type {
  9. EditorContentType,
  10. FieldEditorContext,
  11. } from '@shared/components/Form/fields/FieldEditor/types'
  12. import { getArticleSelection } from '@shared/entities/ticket-article/composables/getArticleSelection'
  13. import type { SelectionData } from '@shared/utils/selection'
  14. import type { FormKitNode } from '@formkit/core'
  15. import { useTicketInformation } from './useTicketInformation'
  16. export const useTicketArticleContext = () => {
  17. const articleForContext = shallowRef<TicketArticle>()
  18. const ticketForContext = shallowRef<TicketById>()
  19. const selectionData = ref<SelectionData>()
  20. const metadataDialog = useDialog({
  21. name: 'article-metadata',
  22. component: () =>
  23. import('../components/TicketDetailView/ArticleMetadataDialog.vue'),
  24. })
  25. const { showArticleReplyDialog, form } = useTicketInformation()
  26. const triggerId = ref(0)
  27. const recalculate = () => {
  28. triggerId.value += 1
  29. }
  30. const disposeCallbacks: (() => unknown)[] = []
  31. const onDispose = (callback: () => unknown) => {
  32. disposeCallbacks.push(callback)
  33. }
  34. const openReplyDialog: TicketArticlePerformOptions['openReplyDialog'] =
  35. async (values = {}) => {
  36. const formNode = form.value?.formNode as FormKitNode
  37. await showArticleReplyDialog()
  38. const { articleType, ...otherOptions } = values
  39. const typeNode = formNode.find('articleType', 'name')
  40. if (formNode.context) {
  41. Object.assign(formNode.context, { _open: true })
  42. }
  43. typeNode?.input(articleType, false)
  44. // trigger new fields that depend on the articleType
  45. await nextTick()
  46. for (const [key, value] of Object.entries(otherOptions)) {
  47. const node = formNode.find(key, 'name')
  48. node?.input(value, false)
  49. // TODO: make handling more generic(?)
  50. if (node && (key === 'to' || key === 'cc')) {
  51. const options = Array.isArray(value)
  52. ? value.map((v) => ({ value: v, label: v }))
  53. : [{ value, label: value }]
  54. node.emit('prop:options', options)
  55. }
  56. }
  57. formNode.emit('article-reply-open', articleType)
  58. const context = formNode.find('body', 'name')?.context as
  59. | FieldEditorContext
  60. | undefined
  61. context?.focus()
  62. nextTick(() => {
  63. if (formNode.context) {
  64. Object.assign(formNode.context, { _open: false })
  65. }
  66. })
  67. }
  68. const getNewArticleBody = (type: EditorContentType): string => {
  69. const bodyElement = form.value?.formNode.find('body', 'name')
  70. if (!bodyElement) return ''
  71. const getEditorValue = bodyElement.context?.getEditorValue
  72. return typeof getEditorValue === 'function' ? getEditorValue(type) : ''
  73. }
  74. const contextOptions = computed<PopupItem[]>(() => {
  75. const ticket = ticketForContext.value
  76. const article = articleForContext.value
  77. // trigger ID cannot be less than 0, so it's just a hint for vue to recalculate computed
  78. if (!article || !ticket || triggerId.value < 0) return []
  79. // clear all side effects before recalculating
  80. disposeCallbacks.forEach((callback) => callback())
  81. disposeCallbacks.length = 0
  82. const actions = createArticleActions(ticket, article, 'mobile', {
  83. recalculate,
  84. onDispose,
  85. }).map((action) => {
  86. const { perform, link, label } = action
  87. if (!perform) return action
  88. return {
  89. label,
  90. link,
  91. onAction: () =>
  92. perform(ticket, article, {
  93. selection: selectionData.value,
  94. openReplyDialog,
  95. getNewArticleBody,
  96. }),
  97. }
  98. })
  99. return [
  100. ...actions,
  101. {
  102. label: __('Show meta data'),
  103. onAction() {
  104. metadataDialog.open({
  105. name: metadataDialog.name,
  106. article,
  107. ticketInternalId: ticket.internalId,
  108. })
  109. },
  110. },
  111. ]
  112. })
  113. const articleContextShown = computed({
  114. get: () => articleForContext.value != null,
  115. set: (value) => {
  116. // we don't care for "true", because to make it truthy we
  117. // call showArticleContext
  118. // setting it to "false" is done via "update:modelValue"
  119. if (!value) {
  120. articleForContext.value = undefined
  121. // TODO: add tests, when we have an action that uses it
  122. disposeCallbacks.forEach((callback) => callback())
  123. disposeCallbacks.length = 0
  124. }
  125. },
  126. })
  127. const showArticleContext = (article: TicketArticle, ticket: TicketById) => {
  128. metadataDialog.prefetch()
  129. articleForContext.value = article
  130. ticketForContext.value = ticket
  131. try {
  132. // can throw RangeError
  133. selectionData.value = getArticleSelection(article.internalId)
  134. } catch {
  135. selectionData.value = undefined
  136. }
  137. }
  138. return {
  139. contextOptions,
  140. articleContextShown,
  141. showArticleContext,
  142. }
  143. }