123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420 |
- <!-- Copyright (C) 2012-2024 Zammad Foundation, https://zammad-foundation.org/ -->
- <script setup lang="ts">
- import { isEqual } from 'lodash-es'
- import { computed, markRaw, reactive } from 'vue'
- import { useRouter, useRoute } from 'vue-router'
- import Form from '#shared/components/Form/Form.vue'
- import type { FormSubmitData } from '#shared/components/Form/types.ts'
- import { useForm } from '#shared/components/Form/useForm.ts'
- import { useConfirmation } from '#shared/composables/useConfirmation.ts'
- import { useTicketSignature } from '#shared/composables/useTicketSignature.ts'
- import { useTicketCreate } from '#shared/entities/ticket/composables/useTicketCreate.ts'
- import { useTicketCreateArticleType } from '#shared/entities/ticket/composables/useTicketCreateArticleType.ts'
- import { useTicketCreateView } from '#shared/entities/ticket/composables/useTicketCreateView.ts'
- import { useTicketFormOrganizationHandler } from '#shared/entities/ticket/composables/useTicketFormOrganizationHandler.ts'
- import type { TicketFormData } from '#shared/entities/ticket/types.ts'
- import { defineFormSchema } from '#shared/form/defineFormSchema.ts'
- import {
- EnumFormUpdaterId,
- EnumObjectManagerObjects,
- EnumTaskbarEntity,
- } from '#shared/graphql/types.ts'
- import { useWalker } from '#shared/router/walker.ts'
- import { useApplicationStore } from '#shared/stores/application.ts'
- import CommonButton from '#desktop/components/CommonButton/CommonButton.vue'
- import CommonContentPanel from '#desktop/components/CommonContentPanel/CommonContentPanel.vue'
- import LayoutContent from '#desktop/components/layout/LayoutContent.vue'
- import { useTaskbarTab } from '#desktop/entities/user/current/composables/useTaskbarTab.ts'
- import { useTaskbarTabStateUpdates } from '#desktop/entities/user/current/composables/useTaskbarTabStateUpdates.ts'
- import type { TaskbarTabContext } from '#desktop/entities/user/current/types.ts'
- import ApplyTemplate from '../components/ApplyTemplate.vue'
- import TicketDuplicateDetectionAlert from '../components/TicketDuplicateDetectionAlert.vue'
- import TicketSidebar from '../components/TicketSidebar.vue'
- import {
- useProvideTicketSidebar,
- useTicketSidebar,
- } from '../composables/useTicketSidebar.ts'
- import {
- TicketSidebarScreenType,
- type TicketSidebarContext,
- } from '../types/sidebar.ts'
- interface Props {
- tabId?: string
- }
- defineOptions({
- beforeRouteEnter(to) {
- const { ticketCreateEnabled, checkUniqueTicketCreateRoute } =
- useTicketCreateView()
- // TODO: Add real handling, when error page is available (see mobile).
- if (!ticketCreateEnabled.value) return '/error'
- return checkUniqueTicketCreateRoute(to)
- },
- beforeRouteUpdate(to) {
- // When route is updated we need to check again of the unique identifier.
- const { checkUniqueTicketCreateRoute } = useTicketCreateView()
- return checkUniqueTicketCreateRoute(to)
- },
- })
- defineProps<Props>()
- const router = useRouter()
- const walker = useWalker()
- const route = useRoute()
- const {
- form,
- isDisabled,
- isDirty,
- isInitialSettled,
- formNodeId,
- values,
- triggerFormUpdater,
- } = useForm()
- const application = useApplicationStore()
- const redirectAfterCreate = (internalId?: number) => {
- if (internalId) {
- router.replace(`/tickets/${internalId}`)
- } else {
- router.replace({ name: 'Dashboard' }) // TODO: check...?
- }
- }
- const goBack = () => {
- walker.back('/') // TODO: check what is the best fallback route path.
- }
- const { ticketArticleSenderTypeField } = useTicketCreateArticleType()
- const { createTicket, isTicketCustomer } = useTicketCreate(
- form,
- redirectAfterCreate,
- )
- const defaultTitle = __('New Ticket')
- const formSchema = defineFormSchema([
- {
- isLayout: true,
- component: 'CommonContentPanel',
- children: [
- {
- isLayout: true,
- element: 'h1',
- attrs: {
- class:
- 'py-2.5 text-center text-xl font-medium leading-snug text-black dark:text-white',
- ariaCurrent: 'page',
- },
- children: '$values.title || $t($defaultTitle)',
- },
- {
- if: '$isTicketCustomer === false',
- ...ticketArticleSenderTypeField,
- outerClass: 'flex justify-center',
- },
- {
- isLayout: true,
- element: 'div',
- attrs: {
- class: 'grid grid-cols-1 gap-2.5',
- role: 'tabpanel',
- ariaLabelledby: '$getTabLabel($values.articleSenderType)',
- id: '$getTabPanelId($values.articleSenderType)',
- },
- children: [
- {
- if: '$existingAdditionalCreateNotes() && $getAdditionalCreateNote($values.articleSenderType) !== undefined',
- isLayout: true,
- component: 'CommonAlert',
- props: {
- variant: 'warning',
- },
- children: '$t($getAdditionalCreateNote($values.articleSenderType))',
- },
- {
- if: '$values.ticket_duplicate_detection.count > 0',
- isLayout: true,
- component: 'TicketDuplicateDetectionAlert',
- props: {
- tickets: '$values.ticket_duplicate_detection.items',
- },
- children: '',
- },
- {
- screen: 'create_top',
- object: EnumObjectManagerObjects.Ticket,
- },
- // Because of the current field screen settings in the backend
- // seed we need to add this manually.
- {
- if: '$values.articleSenderType === "email-out"',
- name: 'cc',
- label: __('CC'),
- type: 'recipient',
- props: {
- multiple: true,
- clearable: true,
- },
- },
- {
- if: '$securityIntegration === true && $values.articleSenderType === "email-out"',
- name: 'security',
- label: __('Security'),
- type: 'security',
- },
- {
- name: 'body',
- screen: 'create_top',
- object: EnumObjectManagerObjects.TicketArticle,
- required: true,
- props: {
- meta: {
- mentionText: {
- customerNodeName: 'customer_id',
- },
- mentionUser: {
- groupNodeName: 'group_id',
- },
- mentionKnowledgeBase: {
- attachmentsNodeName: 'attachments',
- },
- },
- },
- },
- {
- type: 'file',
- name: 'attachments',
- label: __('Attachment'),
- labelSrOnly: true,
- props: {
- multiple: true,
- },
- },
- {
- name: 'ticket_duplicate_detection',
- type: 'hidden',
- value: {
- count: 0,
- items: [],
- },
- },
- {
- name: 'link_ticket_id',
- type: 'hidden',
- },
- {
- name: 'shared_draft_id',
- type: 'hidden',
- },
- {
- name: 'externalReferences',
- type: 'hidden',
- },
- ],
- },
- ],
- },
- {
- isLayout: true,
- component: 'CommonContentPanel',
- children: [
- {
- isLayout: true,
- element: 'div',
- attrs: {
- class: 'grid grid-cols-2-uneven gap-2.5',
- },
- children: [
- {
- screen: 'create_middle',
- object: EnumObjectManagerObjects.Ticket,
- },
- ],
- },
- {
- screen: 'create_bottom',
- object: EnumObjectManagerObjects.Ticket,
- },
- ],
- },
- ])
- const securityIntegration = computed<boolean>(
- () =>
- (application.config.smime_integration ||
- application.config.pgp_integration) ??
- false,
- )
- const additionalCreateNotes = computed(
- () =>
- (application.config.ui_ticket_create_notes as Record<string, string>) || {},
- )
- const schemaData = reactive({
- defaultTitle,
- isTicketCustomer,
- securityIntegration,
- getTabLabel: (value: string) => `tab-label-${value}`,
- getTabPanelId: (value: string) => `tab-panel-${value}`,
- existingAdditionalCreateNotes: () => {
- return Object.keys(additionalCreateNotes).length > 0
- },
- getAdditionalCreateNote: (value: string) => {
- return additionalCreateNotes.value[value]
- },
- })
- const changedFields = reactive({
- // Workaround until the object attribute for body is required so core worklow is returning it correctly.
- body: {
- required: true,
- },
- })
- const { signatureHandling } = useTicketSignature()
- const sidebarContext = computed<TicketSidebarContext>(() => ({
- screenType: TicketSidebarScreenType.TicketCreate,
- form: form.value,
- formValues: values.value,
- }))
- useProvideTicketSidebar(sidebarContext)
- const { hasSidebar } = useTicketSidebar()
- const tabContext = computed<TaskbarTabContext>((currentContext) => {
- if (!isInitialSettled.value) return {}
- const newContext = {
- formValues: values.value,
- formIsDirty: isDirty.value,
- }
- if (currentContext && isEqual(newContext, currentContext))
- return currentContext
- return newContext
- })
- const { activeTaskbarTab, activeTaskbarTabFormId, activeTaskbarTabDelete } =
- useTaskbarTab(EnumTaskbarEntity.TicketCreate, tabContext)
- const { setSkipNextStateUpdate } = useTaskbarTabStateUpdates(triggerFormUpdater)
- const { waitForVariantConfirmation } = useConfirmation()
- const discardChanges = async () => {
- const confirm = await waitForVariantConfirmation('unsaved')
- if (confirm) {
- goBack()
- activeTaskbarTabDelete()
- }
- }
- const applyTemplate = (templateId: string) => {
- // Skip subscription for the current tab, to avoid not needed form updater requests.
- setSkipNextStateUpdate(true)
- triggerFormUpdater({
- includeDirtyFields: true,
- additionalParams: {
- templateId,
- },
- })
- }
- const formAdditionalRouteQueryParams = computed(() => ({
- taskbarId: activeTaskbarTab.value?.taskbarTabId,
- ...(route.query || {}),
- }))
- const submitCreateTicket = async (event: FormSubmitData<TicketFormData>) => {
- return createTicket(event).then((result) => {
- if (!result || result === null || result === undefined) return
- if (typeof result === 'function') result()
- activeTaskbarTabDelete()
- })
- }
- </script>
- <template>
- <LayoutContent
- name="ticket-create"
- background-variant="primary"
- content-alignment="center"
- :show-sidebar="hasSidebar"
- >
- <div class="w-full max-w-screen-xl px-28 py-3.5">
- <Form
- id="ticket-create"
- ref="form"
- :key="tabId"
- :form-id="activeTaskbarTabFormId"
- :schema="formSchema"
- :schema-component-library="{
- CommonContentPanel: markRaw(CommonContentPanel),
- TicketDuplicateDetectionAlert: markRaw(TicketDuplicateDetectionAlert),
- }"
- :schema-data="schemaData"
- :form-updater-id="EnumFormUpdaterId.FormUpdaterUpdaterTicketCreate"
- :handlers="[
- useTicketFormOrganizationHandler(),
- signatureHandling('body'),
- ]"
- :change-fields="changedFields"
- :form-updater-additional-params="formAdditionalRouteQueryParams"
- use-object-attributes
- form-class="flex flex-col gap-3"
- @submit="submitCreateTicket($event as FormSubmitData<TicketFormData>)"
- @changed="setSkipNextStateUpdate(true)"
- />
- </div>
- <template #sideBar="{ isCollapsed, toggleCollapse }">
- <TicketSidebar
- :context="sidebarContext"
- :is-collapsed="isCollapsed"
- :toggle-collapse="toggleCollapse"
- />
- </template>
- <template #bottomBar>
- <template v-if="isInitialSettled">
- <CommonButton
- v-if="isDirty"
- size="large"
- variant="danger"
- :disabled="isDisabled"
- @click="discardChanges"
- >{{ __('Discard Changes') }}</CommonButton
- >
- <CommonButton v-else size="large" variant="secondary" @click="goBack">{{
- __('Cancel & Go Back')
- }}</CommonButton>
- </template>
- <ApplyTemplate @select-template="applyTemplate" />
- <CommonButton
- size="large"
- variant="submit"
- type="submit"
- :form="formNodeId"
- :disabled="isDisabled"
- >{{ __('Create') }}</CommonButton
- >
- </template>
- </LayoutContent>
- </template>
|