useTicketEditForm.ts 8.5 KB

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