Browse Source

Feature: Desktop View - Add an explicit discard article button to the article reply form

Benjamin Scharf 5 months ago
parent
commit
4a7e36bd6f

+ 276 - 0
app/frontend/apps/desktop/pages/ticket/__tests__/ticket-detail-view-edit.spec.ts

@@ -674,5 +674,281 @@ describe('Ticket detail view', () => {
         view.queryByRole('textbox', { name: 'Text' }),
         view.queryByRole('textbox', { name: 'Text' }),
       ).not.toBeInTheDocument()
       ).not.toBeInTheDocument()
     })
     })
+
+    it('discards reply form and it keeps the ticket attribute fields state', async () => {
+      mockTicketQuery({
+        ticket: createDummyTicket({
+          articleType: 'phone',
+          defaultPolicy: {
+            update: true,
+            agentReadAccess: true,
+          },
+        }),
+      })
+
+      mockTicketArticlesQuery({
+        articles: {
+          totalCount: 1,
+          edges: [
+            {
+              node: createDummyArticle({
+                articleType: 'phone',
+                internal: false,
+              }),
+            },
+          ],
+        },
+      })
+
+      mockFormUpdaterQuery({
+        formUpdater: {
+          fields: {
+            group_id: {
+              options: [
+                {
+                  value: 1,
+                  label: 'Users',
+                },
+                {
+                  value: 2,
+                  label: 'test group',
+                },
+              ],
+            },
+            owner_id: {
+              options: [
+                {
+                  value: 3,
+                  label: 'Test Admin Agent',
+                },
+              ],
+            },
+            state_id: {
+              options: [
+                {
+                  value: 4,
+                  label: 'closed',
+                },
+                {
+                  value: 2,
+                  label: 'open',
+                },
+                {
+                  value: 6,
+                  label: 'pending close',
+                },
+                {
+                  value: 3,
+                  label: 'pending reminder',
+                },
+              ],
+            },
+            pending_time: {
+              show: false,
+            },
+            priority_id: {
+              options: [
+                {
+                  value: 1,
+                  label: '1 low',
+                },
+                {
+                  value: 2,
+                  label: '2 normal',
+                },
+                {
+                  value: 3,
+                  label: '3 high',
+                },
+              ],
+            },
+          },
+          flags: {
+            newArticlePresent: false,
+          },
+        },
+      })
+
+      const view = await visitView('/tickets/1')
+
+      // Discard changes inside the reply form
+      await view.events.click(
+        view.getByRole('button', { name: 'Add phone call' }),
+      )
+
+      expect(
+        await view.findByRole('heading', { level: 2, name: 'Reply' }),
+      ).toBeInTheDocument()
+
+      // Sets dirty set for a ticket attribute
+      await view.events.click(view.getByLabelText('State'))
+      await view.events.click(
+        await view.findByRole('option', { name: 'closed' }),
+      )
+
+      await view.events.click(
+        view.getByRole('button', { name: 'Discard unsaved reply' }),
+      )
+
+      expect(
+        await view.findByRole('dialog', { name: 'Unsaved Changes' }),
+      ).toBeInTheDocument()
+
+      await view.events.click(
+        view.getByRole('button', { name: 'Discard Changes' }),
+      )
+
+      // Verify that ticket attributes state is not lost
+      expect(view.getByLabelText('State')).toHaveTextContent('closed')
+    })
+
+    // TODO: Currently we have a problem in our resetForm-Function but also Formkit has an bug inside the own reset handling
+    //  (null / false will currently ignored when setting back the initial value).
+    // So we will improve our own reset function and create an issue on FormKit side to fix this.
+    it.skip('discards complete form with an reply and afterwards only the reply directly', async () => {
+      mockTicketQuery({
+        ticket: createDummyTicket({
+          group: {
+            id: convertToGraphQLId('Group', 1),
+            emailAddress: {
+              name: 'Zammad Helpdesk',
+              emailAddress: 'zammad@localhost',
+            },
+          },
+          defaultPolicy: {
+            update: true,
+            agentReadAccess: true,
+          },
+        }),
+      })
+
+      mockTicketArticlesQuery({
+        articles: {
+          totalCount: 1,
+          edges: [
+            {
+              node: createDummyArticle({
+                articleType: 'phone',
+                internal: false,
+              }),
+            },
+          ],
+        },
+      })
+
+      mockFormUpdaterQuery({
+        formUpdater: {
+          fields: {
+            group_id: {
+              options: [
+                {
+                  value: 1,
+                  label: 'Users',
+                },
+                {
+                  value: 2,
+                  label: 'test group',
+                },
+              ],
+            },
+            owner_id: {
+              options: [
+                {
+                  value: 3,
+                  label: 'Test Admin Agent',
+                },
+              ],
+            },
+            state_id: {
+              options: [
+                {
+                  value: 4,
+                  label: 'closed',
+                },
+                {
+                  value: 2,
+                  label: 'open',
+                },
+                {
+                  value: 6,
+                  label: 'pending close',
+                },
+                {
+                  value: 3,
+                  label: 'pending reminder',
+                },
+              ],
+            },
+            pending_time: {
+              show: false,
+            },
+            priority_id: {
+              options: [
+                {
+                  value: 1,
+                  label: '1 low',
+                },
+                {
+                  value: 2,
+                  label: '2 normal',
+                },
+                {
+                  value: 3,
+                  label: '3 high',
+                },
+              ],
+            },
+          },
+          flags: {
+            newArticlePresent: false,
+          },
+        },
+      })
+
+      const view = await visitView('/tickets/1')
+
+      // Discard changes inside the reply form
+      await view.events.click(view.getByRole('button', { name: 'Add reply' }))
+
+      await view.events.click(
+        await view.findByRole('button', {
+          name: 'Discard your unsaved changes',
+        }),
+      )
+
+      expect(
+        await view.findByRole('dialog', { name: 'Unsaved Changes' }),
+      ).toBeInTheDocument()
+
+      await view.events.click(
+        view.getByRole('button', { name: 'Discard Changes' }),
+      )
+
+      expect(
+        view.queryByRole('button', {
+          name: 'Discard your unsaved changes',
+        }),
+      ).not.toBeInTheDocument()
+
+      await view.events.click(view.getByRole('button', { name: 'Add reply' }))
+
+      await view.events.click(
+        view.getByRole('button', { name: 'Discard unsaved reply' }),
+      )
+
+      expect(
+        await view.findByRole('dialog', { name: 'Unsaved Changes' }),
+      ).toBeInTheDocument()
+
+      await view.events.click(
+        view.getByRole('button', { name: 'Discard Changes' }),
+      )
+
+      expect(
+        view.queryByRole('button', {
+          name: 'Discard your unsaved changes',
+        }),
+      ).not.toBeInTheDocument()
+    })
   })
   })
 })
 })

+ 10 - 2
app/frontend/apps/desktop/pages/ticket/components/TicketDetailView/ArticleReply.vue

@@ -31,6 +31,7 @@ const emit = defineEmits<{
     articleType: string,
     articleType: string,
     performReply: AppSpecificTicketArticleType['performReply'],
     performReply: AppSpecificTicketArticleType['performReply'],
   ]
   ]
+  'discard-form': []
 }>()
 }>()
 
 
 const currentTicketArticleType = computed(() => {
 const currentTicketArticleType = computed(() => {
@@ -218,7 +219,7 @@ defineExpose({
         }"
         }"
       >
       >
         <div
         <div
-          class="flex h-10 items-center justify-between p-3"
+          class="flex h-10 items-center p-3"
           :class="{
           :class="{
             'bg-neutral-50 dark:bg-gray-500': pinned,
             'bg-neutral-50 dark:bg-gray-500': pinned,
             'border-b border-b-transparent': pinned && articleFormReachedTop,
             'border-b border-b-transparent': pinned && articleFormReachedTop,
@@ -228,12 +229,19 @@ defineExpose({
         >
         >
           <CommonLabel
           <CommonLabel
             id="article-reply-form-title"
             id="article-reply-form-title"
-            class="text-stone-200 dark:text-neutral-500"
+            class="text-stone-200 ltr:mr-auto rtl:ml-auto dark:text-neutral-500"
             tag="h2"
             tag="h2"
             size="small"
             size="small"
           >
           >
             {{ $t('Reply') }}
             {{ $t('Reply') }}
           </CommonLabel>
           </CommonLabel>
+          <CommonButton
+            v-tooltip="$t('Discard unsaved reply')"
+            class="text-red-500 ltr:mr-2 rtl:ml-2"
+            variant="none"
+            icon="trash"
+            @click="$emit('discard-form')"
+          />
           <CommonButton
           <CommonButton
             v-tooltip="pinned ? $t('Unpin this panel') : $t('Pin this panel')"
             v-tooltip="pinned ? $t('Unpin this panel') : $t('Pin this panel')"
             :icon="pinned ? 'pin' : 'pin-angle'"
             :icon="pinned ? 'pin' : 'pin-angle'"

+ 1 - 1
app/frontend/apps/desktop/pages/ticket/composables/useTicketArticleReply.ts

@@ -11,7 +11,7 @@ export const useTicketArticleReply = (
   initialNewTicketArticlePresent: Ref<boolean | undefined>,
   initialNewTicketArticlePresent: Ref<boolean | undefined>,
 ) => {
 ) => {
   const localNewTicketArticlePresent = ref<boolean>()
   const localNewTicketArticlePresent = ref<boolean>()
-  // TODO: swichting tabs when you added a new article is shortly showing the buttons (because taskbar tab don't has the information yet?)
+  // TODO: switching tabs when you added a new article is shortly showing the buttons (because taskbar tab don't has the information yet?)
   const newTicketArticlePresent = computed({
   const newTicketArticlePresent = computed({
     get: () => {
     get: () => {
       if (localNewTicketArticlePresent.value !== undefined)
       if (localNewTicketArticlePresent.value !== undefined)

+ 3 - 3
app/frontend/apps/desktop/pages/ticket/views/TicketCreate.vue

@@ -47,9 +47,6 @@ interface Props {
   tabId?: string
   tabId?: string
 }
 }
 
 
-// - handover context to useTaskbarTab composable
-// - Default output for TicketCreate-TabEntity without a "entity/state"
-
 defineOptions({
 defineOptions({
   beforeRouteEnter(to) {
   beforeRouteEnter(to) {
     const { ticketCreateEnabled, checkUniqueTicketCreateRoute } =
     const { ticketCreateEnabled, checkUniqueTicketCreateRoute } =
@@ -324,6 +321,9 @@ const discardChanges = async () => {
 }
 }
 
 
 const applyTemplate = (templateId: string) => {
 const applyTemplate = (templateId: string) => {
+  // Skip subscription for the current tab, to avoid not needed form updater requests.
+  setSkipNextStateUpdate(true)
+
   triggerFormUpdater({
   triggerFormUpdater({
     includeDirtyFields: true,
     includeDirtyFields: true,
     additionalParams: {
     additionalParams: {

+ 20 - 0
app/frontend/apps/desktop/pages/ticket/views/TicketDetailView.vue

@@ -257,6 +257,10 @@ const discardChanges = async () => {
     newTicketArticlePresent.value = false
     newTicketArticlePresent.value = false
 
 
     await nextTick()
     await nextTick()
+
+    // Skip subscription for the current tab, to avoid not needed form updater requests.
+    setSkipNextStateUpdate(true)
+
     formReset()
     formReset()
   }
   }
 }
 }
@@ -490,6 +494,21 @@ const submitEditTicket = async (
     })
     })
 }
 }
 
 
+const discardReplyForm = async () => {
+  const confirm = await waitForVariantConfirmation('unsaved')
+
+  if (!confirm) return
+
+  newTicketArticlePresent.value = false
+
+  await nextTick()
+
+  // Skip subscription for the current tab, to avoid not needed form updater requests.
+  setSkipNextStateUpdate(true)
+
+  return triggerFormUpdater()
+}
+
 const handleShowArticleForm = (
 const handleShowArticleForm = (
   articleType: string,
   articleType: string,
   performReply: AppSpecificTicketArticleType['performReply'],
   performReply: AppSpecificTicketArticleType['performReply'],
@@ -552,6 +571,7 @@ watch(ticketId, () => {
           :has-internal-article="hasInternalArticle"
           :has-internal-article="hasInternalArticle"
           :parent-reached-bottom-scroll="reachedBottom"
           :parent-reached-bottom-scroll="reachedBottom"
           @show-article-form="handleShowArticleForm"
           @show-article-form="handleShowArticleForm"
+          @discard-form="discardReplyForm"
         />
         />
 
 
         <div id="wrapper-form-ticket-edit" class="hidden" aria-hidden="true">
         <div id="wrapper-form-ticket-edit" class="hidden" aria-hidden="true">

+ 15 - 11
i18n/zammad.pot

@@ -1085,7 +1085,7 @@ msgstr ""
 msgid "Add phone call"
 msgid "Add phone call"
 msgstr ""
 msgstr ""
 
 
-#: app/frontend/apps/desktop/pages/ticket/components/TicketDetailView/ArticleReply.vue:63
+#: app/frontend/apps/desktop/pages/ticket/components/TicketDetailView/ArticleReply.vue:64
 #: app/frontend/apps/mobile/pages/ticket/components/TicketDetailView/ArticleReplyDialog.vue:37
 #: app/frontend/apps/mobile/pages/ticket/components/TicketDetailView/ArticleReplyDialog.vue:37
 #: app/frontend/apps/mobile/pages/ticket/components/TicketDetailView/TicketDetailViewActions.vue:88
 #: app/frontend/apps/mobile/pages/ticket/components/TicketDetailView/TicketDetailViewActions.vue:88
 msgid "Add reply"
 msgid "Add reply"
@@ -4982,6 +4982,10 @@ msgstr ""
 msgid "Discard changes"
 msgid "Discard changes"
 msgstr ""
 msgstr ""
 
 
+#: app/frontend/apps/desktop/pages/ticket/components/TicketDetailView/ArticleReply.vue:239
+msgid "Discard unsaved reply"
+msgstr ""
+
 #: app/frontend/apps/desktop/pages/ticket/components/TicketDetailView/TicketDetailBottomBar/TicketDetailBottomBar.vue:136
 #: app/frontend/apps/desktop/pages/ticket/components/TicketDetailView/TicketDetailBottomBar/TicketDetailBottomBar.vue:136
 #: app/frontend/apps/mobile/pages/ticket/components/TicketDetailView/ArticleReplyDialog.vue:146
 #: app/frontend/apps/mobile/pages/ticket/components/TicketDetailView/ArticleReplyDialog.vue:146
 #: app/frontend/apps/mobile/pages/ticket/views/TicketInformation/TicketInformationDetails.vue:161
 #: app/frontend/apps/mobile/pages/ticket/views/TicketInformation/TicketInformationDetails.vue:161
@@ -7542,7 +7546,7 @@ msgid "Including private key."
 msgstr ""
 msgstr ""
 
 
 #: app/assets/javascripts/app/controllers/ticket_zoom/checklist_modal.coffee:10
 #: app/assets/javascripts/app/controllers/ticket_zoom/checklist_modal.coffee:10
-#: app/frontend/apps/desktop/pages/ticket/views/TicketDetailView.vue:340
+#: app/frontend/apps/desktop/pages/ticket/views/TicketDetailView.vue:341
 msgid "Incomplete Ticket Checklist"
 msgid "Incomplete Ticket Checklist"
 msgstr ""
 msgstr ""
 
 
@@ -9869,7 +9873,7 @@ msgid "No x-zammad-session-user-id, no sender set!"
 msgstr ""
 msgstr ""
 
 
 #: app/assets/javascripts/app/controllers/ticket_zoom/checklist_modal.coffee:8
 #: app/assets/javascripts/app/controllers/ticket_zoom/checklist_modal.coffee:8
-#: app/frontend/apps/desktop/pages/ticket/views/TicketDetailView.vue:343
+#: app/frontend/apps/desktop/pages/ticket/views/TicketDetailView.vue:344
 msgid "No, just close the ticket"
 msgid "No, just close the ticket"
 msgstr ""
 msgstr ""
 
 
@@ -10877,7 +10881,7 @@ msgstr ""
 msgid "Pick a name for the application, and we'll give you a unique token."
 msgid "Pick a name for the application, and we'll give you a unique token."
 msgstr ""
 msgstr ""
 
 
-#: app/frontend/apps/desktop/pages/ticket/components/TicketDetailView/ArticleReply.vue:238
+#: app/frontend/apps/desktop/pages/ticket/components/TicketDetailView/ArticleReply.vue:246
 msgid "Pin this panel"
 msgid "Pin this panel"
 msgstr ""
 msgstr ""
 
 
@@ -11631,7 +11635,7 @@ msgstr ""
 msgid "Replacement agent"
 msgid "Replacement agent"
 msgstr ""
 msgstr ""
 
 
-#: app/frontend/apps/desktop/pages/ticket/components/TicketDetailView/ArticleReply.vue:235
+#: app/frontend/apps/desktop/pages/ticket/components/TicketDetailView/ArticleReply.vue:236
 #: app/frontend/shared/entities/ticket-article/action/plugins/email.ts:87
 #: app/frontend/shared/entities/ticket-article/action/plugins/email.ts:87
 #: app/frontend/shared/entities/ticket-article/action/plugins/facebook.ts:20
 #: app/frontend/shared/entities/ticket-article/action/plugins/facebook.ts:20
 #: app/frontend/shared/entities/ticket-article/action/plugins/sms.ts:22
 #: app/frontend/shared/entities/ticket-article/action/plugins/sms.ts:22
@@ -11787,7 +11791,7 @@ msgstr ""
 msgid "Resetting the order of your ticket overviews failed."
 msgid "Resetting the order of your ticket overviews failed."
 msgstr ""
 msgstr ""
 
 
-#: app/frontend/apps/desktop/pages/ticket/components/TicketDetailView/ArticleReply.vue:201
+#: app/frontend/apps/desktop/pages/ticket/components/TicketDetailView/ArticleReply.vue:202
 msgid "Resize article panel"
 msgid "Resize article panel"
 msgstr ""
 msgstr ""
 
 
@@ -15617,12 +15621,12 @@ msgstr ""
 msgid "Ticket update"
 msgid "Ticket update"
 msgstr ""
 msgstr ""
 
 
-#: app/frontend/apps/desktop/pages/ticket/views/TicketDetailView.vue:485
+#: app/frontend/apps/desktop/pages/ticket/views/TicketDetailView.vue:486
 msgid "Ticket update failed."
 msgid "Ticket update failed."
 msgstr ""
 msgstr ""
 
 
 #: app/frontend/apps/desktop/pages/ticket/components/TicketDetailView/TicketDetailTopBar/useTicketEditTitle.ts:28
 #: app/frontend/apps/desktop/pages/ticket/components/TicketDetailView/TicketDetailTopBar/useTicketEditTitle.ts:28
-#: app/frontend/apps/desktop/pages/ticket/views/TicketDetailView.vue:437
+#: app/frontend/apps/desktop/pages/ticket/views/TicketDetailView.vue:438
 #: app/frontend/apps/mobile/pages/ticket/views/TicketDetailView.vue:171
 #: app/frontend/apps/mobile/pages/ticket/views/TicketDetailView.vue:171
 msgid "Ticket updated successfully."
 msgid "Ticket updated successfully."
 msgstr ""
 msgstr ""
@@ -16335,7 +16339,7 @@ msgstr ""
 msgid "Unlock"
 msgid "Unlock"
 msgstr ""
 msgstr ""
 
 
-#: app/frontend/apps/desktop/pages/ticket/components/TicketDetailView/ArticleReply.vue:238
+#: app/frontend/apps/desktop/pages/ticket/components/TicketDetailView/ArticleReply.vue:246
 msgid "Unpin this panel"
 msgid "Unpin this panel"
 msgstr ""
 msgstr ""
 
 
@@ -17285,7 +17289,7 @@ msgid "Yes, add attachments now"
 msgstr ""
 msgstr ""
 
 
 #: app/assets/javascripts/app/controllers/ticket_zoom/checklist_modal.coffee:5
 #: app/assets/javascripts/app/controllers/ticket_zoom/checklist_modal.coffee:5
-#: app/frontend/apps/desktop/pages/ticket/views/TicketDetailView.vue:342
+#: app/frontend/apps/desktop/pages/ticket/views/TicketDetailView.vue:343
 msgid "Yes, open the checklist"
 msgid "Yes, open the checklist"
 msgstr ""
 msgstr ""
 
 
@@ -17503,7 +17507,7 @@ msgstr ""
 msgid "You have unchecked items in the checklist."
 msgid "You have unchecked items in the checklist."
 msgstr ""
 msgstr ""
 
 
-#: app/frontend/apps/desktop/pages/ticket/views/TicketDetailView.vue:337
+#: app/frontend/apps/desktop/pages/ticket/views/TicketDetailView.vue:338
 msgid "You have unchecked items in the checklist. Do you want to handle them before closing this ticket?"
 msgid "You have unchecked items in the checklist. Do you want to handle them before closing this ticket?"
 msgstr ""
 msgstr ""