useTicketEditForm.ts 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286
  1. // Copyright (C) 2012-2023 Zammad Foundation, https://zammad-foundation.org/
  2. import type { FormHandlerFunction } from '@shared/components/Form'
  3. import { FormHandlerExecution } from '@shared/components/Form'
  4. import { createArticleTypes } from '@shared/entities/ticket-article/action/plugins'
  5. import type { AppSpecificTicketArticleType } from '@shared/entities/ticket-article/action/plugins/types'
  6. import type { TicketById } from '@shared/entities/ticket/types'
  7. import { useTicketView } from '@shared/entities/ticket/composables/useTicketView'
  8. import { EnumObjectManagerObjects } from '@shared/graphql/types'
  9. import type { Ref } from 'vue'
  10. import { computed, shallowRef } from 'vue'
  11. import type {
  12. ChangedField,
  13. ReactiveFormSchemData,
  14. } from '@shared/components/Form/types'
  15. import type { FieldEditorContext } from '@shared/components/Form/fields/FieldEditor/types'
  16. import type { FormKitNode } from '@formkit/core'
  17. export const useTicketEditForm = (ticket: Ref<TicketById | undefined>) => {
  18. const ticketArticleTypes = computed(() => {
  19. return ticket.value ? createArticleTypes(ticket.value, 'mobile') : []
  20. })
  21. const currentArticleType = shallowRef<AppSpecificTicketArticleType>()
  22. const recipientContact = computed(
  23. () => currentArticleType.value?.recipientContact,
  24. )
  25. const editorType = computed(() => currentArticleType.value?.contentType)
  26. const editorMeta = computed(() => {
  27. return {
  28. mentionUser: {
  29. groupNodeId: 'group_id',
  30. },
  31. ...currentArticleType?.value?.editorMeta,
  32. }
  33. })
  34. const { isTicketCustomer } = useTicketView(ticket)
  35. const ticketSchema = {
  36. type: 'group',
  37. name: 'ticket', // will be flattened in the form submit result
  38. isGroupOrList: true,
  39. children: [
  40. {
  41. name: 'title',
  42. type: 'text',
  43. label: __('Ticket title'),
  44. required: true,
  45. },
  46. {
  47. screen: 'edit',
  48. object: EnumObjectManagerObjects.Ticket,
  49. },
  50. ],
  51. }
  52. const articleSchema = {
  53. if: '$newTicketArticleRequested || $newTicketArticlePresent',
  54. type: 'group',
  55. name: 'article',
  56. isGroupOrList: true,
  57. children: [
  58. {
  59. type: 'hidden',
  60. name: 'inReplyTo',
  61. },
  62. {
  63. if: '$fns.includes($currentArticleType.attributes, "subtype")',
  64. type: 'hidden',
  65. name: 'subtype',
  66. },
  67. {
  68. name: 'articleType',
  69. label: __('Article Type'),
  70. labelSrOnly: true,
  71. type: 'select',
  72. hidden: computed(() => ticketArticleTypes.value.length === 1),
  73. props: {
  74. options: ticketArticleTypes,
  75. },
  76. triggerFormUpdater: false,
  77. },
  78. {
  79. name: 'internal',
  80. label: __('Visibility'),
  81. labelSrOnly: true,
  82. hidden: isTicketCustomer,
  83. type: 'select',
  84. props: {
  85. options: [
  86. {
  87. value: true,
  88. label: __('Internal'),
  89. icon: 'mobile-lock',
  90. },
  91. {
  92. value: false,
  93. label: __('Public'),
  94. icon: 'mobile-unlock',
  95. },
  96. ],
  97. },
  98. triggerFormUpdater: false,
  99. },
  100. {
  101. if: '$fns.includes($currentArticleType.attributes, "to")',
  102. name: 'to',
  103. label: __('To'),
  104. type: 'recipient',
  105. props: {
  106. contact: recipientContact,
  107. multiple: true,
  108. },
  109. triggerFormUpdater: false,
  110. },
  111. {
  112. if: '$fns.includes($currentArticleType.attributes, "cc")',
  113. name: 'cc',
  114. label: __('CC'),
  115. type: 'recipient',
  116. props: {
  117. contact: recipientContact,
  118. multiple: true,
  119. },
  120. triggerFormUpdater: false,
  121. },
  122. {
  123. if: '$fns.includes($currentArticleType.attributes, "subject")',
  124. name: 'subject',
  125. label: __('Subject'),
  126. type: 'text',
  127. props: {
  128. maxlength: 200,
  129. },
  130. triggerFormUpdater: false,
  131. },
  132. {
  133. if: '$smimeIntegration === true && $fns.includes($currentArticleType.attributes, "security")',
  134. name: 'security',
  135. label: __('Security'),
  136. type: 'security',
  137. props: {
  138. // TODO ...
  139. },
  140. triggerFormUpdater: false,
  141. },
  142. {
  143. name: 'body',
  144. screen: 'edit',
  145. object: EnumObjectManagerObjects.TicketArticle,
  146. props: {
  147. contentType: editorType,
  148. meta: editorMeta,
  149. },
  150. triggerFormUpdater: false,
  151. required: true, // debug
  152. },
  153. {
  154. if: '$fns.includes($currentArticleType.attributes, "attachments")',
  155. type: 'file',
  156. name: 'attachments',
  157. props: {
  158. multiple: true,
  159. },
  160. },
  161. ],
  162. }
  163. const ticketEditSchema = [
  164. {
  165. isLayout: true,
  166. component: 'FormGroup',
  167. props: {
  168. style: {
  169. if: '$formLocation !== "[data-ticket-edit-form]"',
  170. then: 'display: none;',
  171. },
  172. showDirtyMark: true,
  173. },
  174. children: [ticketSchema],
  175. },
  176. {
  177. isLayout: true,
  178. component: 'FormGroup',
  179. props: {
  180. style: {
  181. if: '$formLocation !== "[data-ticket-article-reply-form]"',
  182. then: 'display: none;',
  183. },
  184. },
  185. children: [articleSchema],
  186. },
  187. ]
  188. const articleTypeChangeHandler = () => {
  189. const executeHandler = (
  190. execution: FormHandlerExecution,
  191. schemaData: ReactiveFormSchemData,
  192. changedField?: ChangedField,
  193. ) => {
  194. if (!schemaData.fields.articleType) return false
  195. if (
  196. execution === FormHandlerExecution.FieldChange &&
  197. (!changedField || changedField.name !== 'articleType')
  198. ) {
  199. return false
  200. }
  201. return true
  202. }
  203. const handleArticleType: FormHandlerFunction = (
  204. execution,
  205. formNode,
  206. values,
  207. changeFields,
  208. updateSchemaDataField,
  209. schemaData,
  210. changedField,
  211. ) => {
  212. if (
  213. !executeHandler(execution, schemaData, changedField) ||
  214. !ticket.value ||
  215. !formNode
  216. )
  217. return
  218. const body = formNode.find('body', 'name')
  219. const context = {
  220. body: body?.context as unknown as FieldEditorContext,
  221. }
  222. if (changedField?.newValue !== changedField?.oldValue) {
  223. currentArticleType.value?.onDeselected?.(ticket.value, context)
  224. }
  225. const newType = ticketArticleTypes.value.find(
  226. (type) => type.value === changedField?.newValue,
  227. )
  228. if (!newType) return
  229. if (!formNode.context?._open) {
  230. newType.onSelected?.(ticket.value, context)
  231. }
  232. currentArticleType.value = newType
  233. formNode.find('internal')?.input(newType.internal, false)
  234. }
  235. return {
  236. execution: [
  237. FormHandlerExecution.Initial,
  238. FormHandlerExecution.FieldChange,
  239. ],
  240. callback: handleArticleType,
  241. }
  242. }
  243. const articleTypeSelectHandler = (formNode: FormKitNode) => {
  244. // this is called only when user replied to an article, but the type inside form did not change
  245. // (because dialog was opened before, and type was changed then, but we still need to trigger select, because visually it's what happens)
  246. formNode.on('article-reply-open', ({ payload }) => {
  247. if (!payload || !ticket.value) return
  248. const articleType = ticketArticleTypes.value.find(
  249. (type) => type.value === payload,
  250. )
  251. if (!articleType) return
  252. const body = formNode.find('body', 'name') as FormKitNode
  253. const context = {
  254. body: body.context as unknown as FieldEditorContext,
  255. }
  256. articleType.onOpened?.(ticket.value, context)
  257. })
  258. }
  259. return {
  260. ticketEditSchema,
  261. currentArticleType,
  262. articleTypeHandler: articleTypeChangeHandler,
  263. articleTypeSelectHandler,
  264. }
  265. }