useTicketArticleContext.ts 5.5 KB

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