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' }),
       ).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,
     performReply: AppSpecificTicketArticleType['performReply'],
   ]
+  'discard-form': []
 }>()
 
 const currentTicketArticleType = computed(() => {
@@ -218,7 +219,7 @@ defineExpose({
         }"
       >
         <div
-          class="flex h-10 items-center justify-between p-3"
+          class="flex h-10 items-center p-3"
           :class="{
             'bg-neutral-50 dark:bg-gray-500': pinned,
             'border-b border-b-transparent': pinned && articleFormReachedTop,
@@ -228,12 +229,19 @@ defineExpose({
         >
           <CommonLabel
             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"
             size="small"
           >
             {{ $t('Reply') }}
           </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
             v-tooltip="pinned ? $t('Unpin this panel') : $t('Pin this panel')"
             :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>,
 ) => {
   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({
     get: () => {
       if (localNewTicketArticlePresent.value !== undefined)

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

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

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

@@ -257,6 +257,10 @@ const discardChanges = async () => {
     newTicketArticlePresent.value = false
 
     await nextTick()
+
+    // Skip subscription for the current tab, to avoid not needed form updater requests.
+    setSkipNextStateUpdate(true)
+
     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 = (
   articleType: string,
   performReply: AppSpecificTicketArticleType['performReply'],
@@ -552,6 +571,7 @@ watch(ticketId, () => {
           :has-internal-article="hasInternalArticle"
           :parent-reached-bottom-scroll="reachedBottom"
           @show-article-form="handleShowArticleForm"
+          @discard-form="discardReplyForm"
         />
 
         <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"
 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/TicketDetailViewActions.vue:88
 msgid "Add reply"
@@ -4982,6 +4982,10 @@ msgstr ""
 msgid "Discard changes"
 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/mobile/pages/ticket/components/TicketDetailView/ArticleReplyDialog.vue:146
 #: app/frontend/apps/mobile/pages/ticket/views/TicketInformation/TicketInformationDetails.vue:161
@@ -7542,7 +7546,7 @@ msgid "Including private key."
 msgstr ""
 
 #: 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"
 msgstr ""
 
@@ -9869,7 +9873,7 @@ msgid "No x-zammad-session-user-id, no sender set!"
 msgstr ""
 
 #: 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"
 msgstr ""
 
@@ -10877,7 +10881,7 @@ msgstr ""
 msgid "Pick a name for the application, and we'll give you a unique token."
 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"
 msgstr ""
 
@@ -11631,7 +11635,7 @@ msgstr ""
 msgid "Replacement agent"
 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/facebook.ts:20
 #: 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."
 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"
 msgstr ""
 
@@ -15617,12 +15621,12 @@ msgstr ""
 msgid "Ticket update"
 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."
 msgstr ""
 
 #: 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
 msgid "Ticket updated successfully."
 msgstr ""
@@ -16335,7 +16339,7 @@ msgstr ""
 msgid "Unlock"
 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"
 msgstr ""
 
@@ -17285,7 +17289,7 @@ msgid "Yes, add attachments now"
 msgstr ""
 
 #: 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"
 msgstr ""
 
@@ -17503,7 +17507,7 @@ msgstr ""
 msgid "You have unchecked items in the checklist."
 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?"
 msgstr ""