useTicketEditForm.ts 9.3 KB

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