From 7bf45d6a25c3ca066eef1ed429e28ca2766a4ab3 Mon Sep 17 00:00:00 2001
From: Simon Knox <psimyn@gmail.com>
Date: Wed, 11 May 2022 10:04:47 +1000
Subject: [PATCH] Update issue description when deleting work item

Add different mutation when deleting a task from the
modal, to trigger update of the parent issue description
Also redirect to issue page when deleting work item from
the detail page
---
 .../issues/show/components/description.vue    | 33 ++++++---
 .../components/work_item_actions.vue          | 44 ++++--------
 .../components/work_item_detail.vue           |  7 +-
 .../components/work_item_detail_modal.vue     | 71 +++++++++++++++++--
 .../work_items/components/work_item_state.vue |  2 +
 .../work_items/components/work_item_title.vue |  1 +
 ...elete_task_from_work_item.mutation.graphql |  9 +++
 .../graphql/work_item.fragment.graphql        |  1 +
 app/assets/javascripts/work_items/index.js    |  3 +-
 .../work_items/pages/work_item_root.vue       | 45 ++++++++++--
 app/views/projects/work_items/index.html.haml |  2 +-
 .../show/components/description_spec.js       |  8 ++-
 .../components/work_item_actions_spec.js      | 49 +------------
 .../components/work_item_detail_modal_spec.js | 56 +++++++++++++--
 .../components/work_item_state_spec.js        |  9 +++
 .../components/work_item_title_spec.js        |  9 +++
 spec/frontend/work_items/mock_data.js         |  3 +
 .../work_items/pages/work_item_detail_spec.js | 14 ++++
 .../work_items/pages/work_item_root_spec.js   | 56 ++++++++++++++-
 spec/frontend/work_items/router_spec.js       |  1 +
 20 files changed, 313 insertions(+), 110 deletions(-)
 create mode 100644 app/assets/javascripts/work_items/graphql/delete_task_from_work_item.mutation.graphql

diff --git a/app/assets/javascripts/issues/show/components/description.vue b/app/assets/javascripts/issues/show/components/description.vue
index 6d310ed973cfd..831cef6683615 100644
--- a/app/assets/javascripts/issues/show/components/description.vue
+++ b/app/assets/javascripts/issues/show/components/description.vue
@@ -8,7 +8,7 @@ import {
 } from '@gitlab/ui';
 import $ from 'jquery';
 import Vue from 'vue';
-import { convertToGraphQLId } from '~/graphql_shared/utils';
+import { getIdFromGraphQLId, convertToGraphQLId } from '~/graphql_shared/utils';
 import { TYPE_WORK_ITEM } from '~/graphql_shared/constants';
 import createFlash from '~/flash';
 import { isPositiveInteger } from '~/lib/utils/number_utils';
@@ -140,7 +140,10 @@ export default {
     }
 
     if (this.workItemId) {
-      this.$refs.detailsModal.show();
+      const taskLink = this.$el.querySelector(
+        `.gfm-issue[data-issue="${getIdFromGraphQLId(this.workItemId)}"]`,
+      );
+      this.openWorkItemDetailModal(taskLink);
     }
   },
   methods: {
@@ -216,7 +219,7 @@ export default {
           this.addHoverListeners(taskLink, workItemId);
           taskLink.addEventListener('click', (e) => {
             e.preventDefault();
-            this.$refs.detailsModal.show();
+            this.openWorkItemDetailModal(taskLink);
             this.workItemId = workItemId;
             this.updateWorkItemIdUrlQuery(issue);
             this.track('viewed_work_item_from_modal', {
@@ -248,7 +251,7 @@ export default {
           </svg>
         `;
         button.setAttribute('aria-label', s__('WorkItem|Convert to work item'));
-        button.addEventListener('click', () => this.openCreateTaskModal(button.id));
+        button.addEventListener('click', () => this.openCreateTaskModal(button));
         item.prepend(button);
       });
     },
@@ -265,20 +268,29 @@ export default {
         }
       });
     },
-    openCreateTaskModal(id) {
-      const { parentElement } = this.$el.querySelector(`#${id}`);
+    setActiveTask(el) {
+      const { parentElement } = el;
       const lineNumbers = parentElement.getAttribute('data-sourcepos').match(/\b\d+(?=:)/g);
       this.activeTask = {
-        id,
         title: parentElement.innerText,
         lineNumberStart: lineNumbers[0],
         lineNumberEnd: lineNumbers[1],
       };
+    },
+    openCreateTaskModal(el) {
+      this.setActiveTask(el);
       this.$refs.modal.show();
     },
     closeCreateTaskModal() {
       this.$refs.modal.hide();
     },
+    openWorkItemDetailModal(el) {
+      if (!el) {
+        return;
+      }
+      this.setActiveTask(el);
+      this.$refs.detailsModal.show();
+    },
     closeWorkItemDetailModal() {
       this.workItemId = undefined;
       this.updateWorkItemIdUrlQuery(undefined);
@@ -287,7 +299,8 @@ export default {
       this.$emit('updateDescription', description);
       this.closeCreateTaskModal();
     },
-    handleDeleteTask() {
+    handleDeleteTask(description) {
+      this.$emit('updateDescription', description);
       this.$toast.show(s__('WorkItem|Work item deleted'));
     },
     updateWorkItemIdUrlQuery(workItemId) {
@@ -353,6 +366,10 @@ export default {
       ref="detailsModal"
       :can-update="canUpdate"
       :work-item-id="workItemId"
+      :issue-gid="issueGid"
+      :lock-version="lockVersion"
+      :line-number-start="activeTask.lineNumberStart"
+      :line-number-end="activeTask.lineNumberEnd"
       @workItemDeleted="handleDeleteTask"
       @close="closeWorkItemDetailModal"
     />
diff --git a/app/assets/javascripts/work_items/components/work_item_actions.vue b/app/assets/javascripts/work_items/components/work_item_actions.vue
index 701cb84df59e3..31e4a932c5af6 100644
--- a/app/assets/javascripts/work_items/components/work_item_actions.vue
+++ b/app/assets/javascripts/work_items/components/work_item_actions.vue
@@ -1,7 +1,7 @@
 <script>
 import { GlDropdown, GlDropdownItem, GlModal, GlModalDirective } from '@gitlab/ui';
 import { s__ } from '~/locale';
-import deleteWorkItemMutation from '../graphql/delete_work_item.mutation.graphql';
+import Tracking from '~/tracking';
 
 export default {
   i18n: {
@@ -15,6 +15,7 @@ export default {
   directives: {
     GlModal: GlModalDirective,
   },
+  mixins: [Tracking.mixin({ label: 'actions_menu' })],
   props: {
     workItemId: {
       type: String,
@@ -27,36 +28,16 @@ export default {
       default: false,
     },
   },
-  emits: ['workItemDeleted', 'error'],
+  emits: ['deleteWorkItem'],
   methods: {
-    deleteWorkItem() {
-      this.$apollo
-        .mutate({
-          mutation: deleteWorkItemMutation,
-          variables: {
-            input: {
-              id: this.workItemId,
-            },
-          },
-        })
-        .then(({ data: { workItemDelete, errors } }) => {
-          if (errors?.length) {
-            throw new Error(errors[0].message);
-          }
-
-          if (workItemDelete?.errors.length) {
-            throw new Error(workItemDelete.errors[0]);
-          }
-
-          this.$emit('workItemDeleted');
-        })
-        .catch((e) => {
-          this.$emit(
-            'error',
-            e.message ||
-              s__('WorkItem|Something went wrong when deleting the work item. Please try again.'),
-          );
-        });
+    handleDeleteWorkItem() {
+      this.track('click_delete_work_item');
+      this.$emit('deleteWorkItem');
+    },
+    handleCancelDeleteWorkItem({ trigger }) {
+      if (trigger !== 'ok') {
+        this.track('cancel_delete_work_item');
+      }
     },
   },
 };
@@ -81,7 +62,8 @@ export default {
       :title="$options.i18n.deleteWorkItem"
       :ok-title="$options.i18n.deleteWorkItem"
       ok-variant="danger"
-      @ok="deleteWorkItem"
+      @ok="handleDeleteWorkItem"
+      @hide="handleCancelDeleteWorkItem"
     >
       {{
         s__(
diff --git a/app/assets/javascripts/work_items/components/work_item_detail.vue b/app/assets/javascripts/work_items/components/work_item_detail.vue
index 33d49583b04aa..4222ffe42fe73 100644
--- a/app/assets/javascripts/work_items/components/work_item_detail.vue
+++ b/app/assets/javascripts/work_items/components/work_item_detail.vue
@@ -67,11 +67,6 @@ export default {
       return this.workItem?.userPermissions?.deleteWorkItem;
     },
   },
-  methods: {
-    handleWorkItemDeleted() {
-      this.$emit('workItemDeleted');
-    },
-  },
 };
 </script>
 
@@ -101,7 +96,7 @@ export default {
           :work-item-id="workItem.id"
           :can-delete="canDelete"
           class="gl-ml-auto gl-mt-5"
-          @workItemDeleted="handleWorkItemDeleted"
+          @deleteWorkItem="$emit('deleteWorkItem')"
           @error="error = $event"
         />
       </div>
diff --git a/app/assets/javascripts/work_items/components/work_item_detail_modal.vue b/app/assets/javascripts/work_items/components/work_item_detail_modal.vue
index 693a7649508ed..172a40a6e56fd 100644
--- a/app/assets/javascripts/work_items/components/work_item_detail_modal.vue
+++ b/app/assets/javascripts/work_items/components/work_item_detail_modal.vue
@@ -1,5 +1,7 @@
 <script>
 import { GlAlert, GlModal } from '@gitlab/ui';
+import { s__ } from '~/locale';
+import deleteWorkItemFromTaskMutation from '../graphql/delete_task_from_work_item.mutation.graphql';
 import WorkItemDetail from './work_item_detail.vue';
 
 export default {
@@ -14,17 +16,72 @@ export default {
       required: false,
       default: null,
     },
+    issueGid: {
+      type: String,
+      required: false,
+      default: '',
+    },
+    lockVersion: {
+      type: Number,
+      required: false,
+      default: null,
+    },
+    lineNumberStart: {
+      type: String,
+      required: false,
+      default: null,
+    },
+    lineNumberEnd: {
+      type: String,
+      required: false,
+      default: null,
+    },
   },
-  emits: ['workItemDeleted', 'close'],
+  emits: ['workItemDeleted', 'workItemUpdated', 'close'],
   data() {
     return {
       error: undefined,
     };
   },
   methods: {
-    handleWorkItemDeleted() {
-      this.$emit('workItemDeleted');
-      this.closeModal();
+    deleteWorkItem() {
+      this.$apollo
+        .mutate({
+          mutation: deleteWorkItemFromTaskMutation,
+          variables: {
+            input: {
+              id: this.issueGid,
+              lockVersion: this.lockVersion,
+              taskData: {
+                id: this.workItemId,
+                lineNumberStart: Number(this.lineNumberStart),
+                lineNumberEnd: Number(this.lineNumberEnd),
+              },
+            },
+          },
+        })
+        .then(
+          ({
+            data: {
+              workItemDeleteTask: {
+                workItem: { descriptionHtml },
+                errors,
+              },
+            },
+          }) => {
+            if (errors?.length) {
+              throw new Error(errors[0].message);
+            }
+
+            this.$emit('workItemDeleted', descriptionHtml);
+            this.$refs.modal.hide();
+          },
+        )
+        .catch((e) => {
+          this.error =
+            e.message ||
+            s__('WorkItem|Something went wrong when deleting the work item. Please try again.');
+        });
     },
     closeModal() {
       this.error = '';
@@ -46,7 +103,11 @@ export default {
       {{ error }}
     </gl-alert>
 
-    <work-item-detail :work-item-id="workItemId" @workItemDeleted="handleWorkItemDeleted" />
+    <work-item-detail
+      :work-item-id="workItemId"
+      @deleteWorkItem="deleteWorkItem"
+      @workItemUpdated="$emit('workItemUpdated')"
+    />
   </gl-modal>
 </template>
 
diff --git a/app/assets/javascripts/work_items/components/work_item_state.vue b/app/assets/javascripts/work_items/components/work_item_state.vue
index 7d4b48f847f95..51db4c804eb3e 100644
--- a/app/assets/javascripts/work_items/components/work_item_state.vue
+++ b/app/assets/javascripts/work_items/components/work_item_state.vue
@@ -75,6 +75,8 @@ export default {
         if (workItemUpdate?.errors?.length) {
           throw new Error(workItemUpdate.errors[0]);
         }
+
+        this.$emit('updated');
       } catch (error) {
         this.$emit('error', i18n.updateError);
         Sentry.captureException(error);
diff --git a/app/assets/javascripts/work_items/components/work_item_title.vue b/app/assets/javascripts/work_items/components/work_item_title.vue
index 73b46bb06d227..d2e6d3c0bbf2d 100644
--- a/app/assets/javascripts/work_items/components/work_item_title.vue
+++ b/app/assets/javascripts/work_items/components/work_item_title.vue
@@ -52,6 +52,7 @@ export default {
           },
         });
         this.track('updated_title');
+        this.$emit('updated');
       } catch {
         this.$emit('error', i18n.updateError);
       }
diff --git a/app/assets/javascripts/work_items/graphql/delete_task_from_work_item.mutation.graphql b/app/assets/javascripts/work_items/graphql/delete_task_from_work_item.mutation.graphql
new file mode 100644
index 0000000000000..32c07ed48c72d
--- /dev/null
+++ b/app/assets/javascripts/work_items/graphql/delete_task_from_work_item.mutation.graphql
@@ -0,0 +1,9 @@
+mutation workItemDeleteTask($input: WorkItemDeleteTaskInput!) {
+  workItemDeleteTask(input: $input) {
+    workItem {
+      id
+      descriptionHtml
+    }
+    errors
+  }
+}
diff --git a/app/assets/javascripts/work_items/graphql/work_item.fragment.graphql b/app/assets/javascripts/work_items/graphql/work_item.fragment.graphql
index ca5ba7a7d8eec..e25fd102699b7 100644
--- a/app/assets/javascripts/work_items/graphql/work_item.fragment.graphql
+++ b/app/assets/javascripts/work_items/graphql/work_item.fragment.graphql
@@ -2,6 +2,7 @@ fragment WorkItem on WorkItem {
   id
   title
   state
+  description
   workItemType {
     id
     name
diff --git a/app/assets/javascripts/work_items/index.js b/app/assets/javascripts/work_items/index.js
index 10fae9b9cc002..e39b0d6a3539c 100644
--- a/app/assets/javascripts/work_items/index.js
+++ b/app/assets/javascripts/work_items/index.js
@@ -5,7 +5,7 @@ import { createApolloProvider } from './graphql/provider';
 
 export const initWorkItemsRoot = () => {
   const el = document.querySelector('#js-work-items');
-  const { fullPath } = el.dataset;
+  const { fullPath, issuesListPath } = el.dataset;
 
   return new Vue({
     el,
@@ -13,6 +13,7 @@ export const initWorkItemsRoot = () => {
     apolloProvider: createApolloProvider(),
     provide: {
       fullPath,
+      issuesListPath,
     },
     render(createElement) {
       return createElement(App);
diff --git a/app/assets/javascripts/work_items/pages/work_item_root.vue b/app/assets/javascripts/work_items/pages/work_item_root.vue
index b8ce6d641a9ea..6dc3dc3b3c9c7 100644
--- a/app/assets/javascripts/work_items/pages/work_item_root.vue
+++ b/app/assets/javascripts/work_items/pages/work_item_root.vue
@@ -1,33 +1,70 @@
 <script>
+import { GlAlert } from '@gitlab/ui';
 import { TYPE_WORK_ITEM } from '~/graphql_shared/constants';
 import { convertToGraphQLId } from '~/graphql_shared/utils';
+import { visitUrl } from '~/lib/utils/url_utility';
 import { s__ } from '~/locale';
 import WorkItemDetail from '../components/work_item_detail.vue';
+import deleteWorkItemMutation from '../graphql/delete_work_item.mutation.graphql';
 
 export default {
   components: {
+    GlAlert,
     WorkItemDetail,
   },
+  inject: ['issuesListPath'],
   props: {
     id: {
       type: String,
       required: true,
     },
   },
+  data() {
+    return {
+      error: '',
+    };
+  },
   computed: {
     gid() {
       return convertToGraphQLId(TYPE_WORK_ITEM, this.id);
     },
   },
   methods: {
-    handleWorkItemDeleted() {
-      this.$root.$toast.show(s__('WorkItem|Work item deleted'));
-      this.$router.push('/');
+    deleteWorkItem() {
+      this.$apollo
+        .mutate({
+          mutation: deleteWorkItemMutation,
+          variables: {
+            input: {
+              id: this.gid,
+            },
+          },
+        })
+        .then(({ data: { workItemDelete, errors } }) => {
+          if (errors?.length) {
+            throw new Error(errors[0].message);
+          }
+
+          if (workItemDelete?.errors.length) {
+            throw new Error(workItemDelete.errors[0]);
+          }
+
+          this.$toast.show(s__('WorkItem|Work item deleted'));
+          visitUrl(this.issuesListPath);
+        })
+        .catch((e) => {
+          this.error =
+            e.message ||
+            s__('WorkItem|Something went wrong when deleting the work item. Please try again.');
+        });
     },
   },
 };
 </script>
 
 <template>
-  <work-item-detail :work-item-id="gid" @workItemDeleted="handleWorkItemDeleted" />
+  <div>
+    <gl-alert v-if="error" variant="danger" @dismiss="error = ''">{{ error }}</gl-alert>
+    <work-item-detail :work-item-id="gid" @deleteWorkItem="deleteWorkItem" />
+  </div>
 </template>
diff --git a/app/views/projects/work_items/index.html.haml b/app/views/projects/work_items/index.html.haml
index 0efd7a740d39f..356f93c6ed50a 100644
--- a/app/views/projects/work_items/index.html.haml
+++ b/app/views/projects/work_items/index.html.haml
@@ -1,3 +1,3 @@
 - page_title s_('WorkItem|Work Items')
 
-#js-work-items{ data: { full_path: @project.full_path } }
+#js-work-items{ data: { full_path: @project.full_path, issues_list_path: project_issues_path(@project) } }
diff --git a/spec/frontend/issues/show/components/description_spec.js b/spec/frontend/issues/show/components/description_spec.js
index c08453530e5ef..1ae04531a6b82 100644
--- a/spec/frontend/issues/show/components/description_spec.js
+++ b/spec/frontend/issues/show/components/description_spec.js
@@ -37,6 +37,7 @@ const showDetailsModal = jest.fn();
 const $toast = {
   show: jest.fn(),
 };
+
 const workItemQueryResponse = {
   data: {
     workItem: null,
@@ -319,8 +320,10 @@ describe('Description component', () => {
       });
 
       it('shows toast after delete success', async () => {
-        findWorkItemDetailModal().vm.$emit('workItemDeleted');
+        const newDesc = 'description';
+        findWorkItemDetailModal().vm.$emit('workItemDeleted', newDesc);
 
+        expect(wrapper.emitted('updateDescription')).toEqual([[newDesc]]);
         expect($toast.show).toHaveBeenCalledWith('Work item deleted');
       });
     });
@@ -381,7 +384,8 @@ describe('Description component', () => {
       describe('when url query `work_item_id` exists', () => {
         it.each`
           behavior           | workItemId     | modalOpened
-          ${'opens'}         | ${'123'}       | ${1}
+          ${'opens'}         | ${'2'}         | ${1}
+          ${'does not open'} | ${'123'}       | ${0}
           ${'does not open'} | ${'123e'}      | ${0}
           ${'does not open'} | ${'12e3'}      | ${0}
           ${'does not open'} | ${'1e23'}      | ${0}
diff --git a/spec/frontend/work_items/components/work_item_actions_spec.js b/spec/frontend/work_items/components/work_item_actions_spec.js
index 286c8180e160b..137a0a7326d44 100644
--- a/spec/frontend/work_items/components/work_item_actions_spec.js
+++ b/spec/frontend/work_items/components/work_item_actions_spec.js
@@ -1,29 +1,17 @@
 import { GlDropdownItem, GlModal } from '@gitlab/ui';
 import { shallowMount } from '@vue/test-utils';
-import Vue from 'vue';
-import VueApollo from 'vue-apollo';
-import waitForPromises from 'helpers/wait_for_promises';
-import createMockApollo from 'helpers/mock_apollo_helper';
 import WorkItemActions from '~/work_items/components/work_item_actions.vue';
-import deleteWorkItem from '~/work_items/graphql/delete_work_item.mutation.graphql';
-import { deleteWorkItemResponse, deleteWorkItemFailureResponse } from '../mock_data';
 
 describe('WorkItemActions component', () => {
   let wrapper;
   let glModalDirective;
 
-  Vue.use(VueApollo);
-
   const findModal = () => wrapper.findComponent(GlModal);
   const findDeleteButton = () => wrapper.findComponent(GlDropdownItem);
 
-  const createComponent = ({
-    canDelete = true,
-    deleteWorkItemHandler = jest.fn().mockResolvedValue(deleteWorkItemResponse),
-  } = {}) => {
+  const createComponent = ({ canDelete = true } = {}) => {
     glModalDirective = jest.fn();
     wrapper = shallowMount(WorkItemActions, {
-      apolloProvider: createMockApollo([[deleteWorkItem, deleteWorkItemHandler]]),
       propsData: { workItemId: '123', canDelete },
       directives: {
         glModal: {
@@ -54,43 +42,12 @@ describe('WorkItemActions component', () => {
     expect(glModalDirective).toHaveBeenCalled();
   });
 
-  it('calls delete mutation when clicking OK button', () => {
-    const deleteWorkItemHandler = jest.fn().mockResolvedValue(deleteWorkItemResponse);
-
-    createComponent({
-      deleteWorkItemHandler,
-    });
-
-    findModal().vm.$emit('ok');
-
-    expect(deleteWorkItemHandler).toHaveBeenCalled();
-    expect(wrapper.emitted('error')).toBeUndefined();
-  });
-
-  it('emits event after delete success', async () => {
+  it('emits event when clicking OK button', () => {
     createComponent();
 
     findModal().vm.$emit('ok');
 
-    await waitForPromises();
-
-    expect(wrapper.emitted('workItemDeleted')).not.toBeUndefined();
-    expect(wrapper.emitted('error')).toBeUndefined();
-  });
-
-  it('emits error event after delete failure', async () => {
-    createComponent({
-      deleteWorkItemHandler: jest.fn().mockResolvedValue(deleteWorkItemFailureResponse),
-    });
-
-    findModal().vm.$emit('ok');
-
-    await waitForPromises();
-
-    expect(wrapper.emitted('error')[0]).toEqual([
-      "The resource that you are attempting to access does not exist or you don't have permission to perform this action",
-    ]);
-    expect(wrapper.emitted('workItemDeleted')).toBeUndefined();
+    expect(wrapper.emitted('deleteWorkItem')).toEqual([[]]);
   });
 
   it('does not render when canDelete is false', () => {
diff --git a/spec/frontend/work_items/components/work_item_detail_modal_spec.js b/spec/frontend/work_items/components/work_item_detail_modal_spec.js
index 67d794519b61f..aaabdbc82d978 100644
--- a/spec/frontend/work_items/components/work_item_detail_modal_spec.js
+++ b/spec/frontend/work_items/components/work_item_detail_modal_spec.js
@@ -1,21 +1,51 @@
-import { GlModal, GlAlert } from '@gitlab/ui';
+import { GlAlert } from '@gitlab/ui';
 import { shallowMount } from '@vue/test-utils';
 import Vue, { nextTick } from 'vue';
 import VueApollo from 'vue-apollo';
+import waitForPromises from 'helpers/wait_for_promises';
+import createMockApollo from 'helpers/mock_apollo_helper';
 import WorkItemDetail from '~/work_items/components/work_item_detail.vue';
 import WorkItemDetailModal from '~/work_items/components/work_item_detail_modal.vue';
+import deleteWorkItemFromTaskMutation from '~/work_items/graphql/delete_task_from_work_item.mutation.graphql';
 
 describe('WorkItemDetailModal component', () => {
   let wrapper;
 
   Vue.use(VueApollo);
 
+  const hideModal = jest.fn();
+  const GlModal = {
+    template: `
+    <div>
+      <slot></slot>
+    </div>
+  `,
+    methods: {
+      hide: hideModal,
+    },
+  };
+
   const findModal = () => wrapper.findComponent(GlModal);
   const findAlert = () => wrapper.findComponent(GlAlert);
   const findWorkItemDetail = () => wrapper.findComponent(WorkItemDetail);
 
   const createComponent = ({ workItemId = '1', error = false } = {}) => {
+    const apolloProvider = createMockApollo([
+      [
+        deleteWorkItemFromTaskMutation,
+        jest.fn().mockResolvedValue({
+          data: {
+            workItemDeleteTask: {
+              workItem: { id: 123, descriptionHtml: 'updated work item desc' },
+              errors: [],
+            },
+          },
+        }),
+      ],
+    ]);
+
     wrapper = shallowMount(WorkItemDetailModal, {
+      apolloProvider,
       propsData: { workItemId },
       data() {
         return {
@@ -35,7 +65,9 @@ describe('WorkItemDetailModal component', () => {
   it('renders WorkItemDetail', () => {
     createComponent();
 
-    expect(findWorkItemDetail().props()).toEqual({ workItemId: '1' });
+    expect(findWorkItemDetail().props()).toEqual({
+      workItemId: '1',
+    });
   });
 
   it('renders alert if there is an error', () => {
@@ -65,10 +97,24 @@ describe('WorkItemDetailModal component', () => {
     expect(wrapper.emitted('close')).toBeTruthy();
   });
 
-  it('emits `workItemDeleted` event on deleting work item', () => {
+  it('emits `workItemUpdated` event on updating work item', () => {
     createComponent();
-    findWorkItemDetail().vm.$emit('workItemDeleted');
+    findWorkItemDetail().vm.$emit('workItemUpdated');
+
+    expect(wrapper.emitted('workItemUpdated')).toBeTruthy();
+  });
+
+  describe('delete work item', () => {
+    it('emits workItemDeleted and closes modal', async () => {
+      createComponent();
+      const newDesc = 'updated work item desc';
+
+      findWorkItemDetail().vm.$emit('deleteWorkItem');
 
-    expect(wrapper.emitted('workItemDeleted')).toBeTruthy();
+      await waitForPromises();
+
+      expect(wrapper.emitted('workItemDeleted')).toEqual([[newDesc]]);
+      expect(hideModal).toHaveBeenCalled();
+    });
   });
 });
diff --git a/spec/frontend/work_items/components/work_item_state_spec.js b/spec/frontend/work_items/components/work_item_state_spec.js
index 6584d197206da..9e48f56d9e930 100644
--- a/spec/frontend/work_items/components/work_item_state_spec.js
+++ b/spec/frontend/work_items/components/work_item_state_spec.js
@@ -81,6 +81,15 @@ describe('WorkItemState component', () => {
       });
     });
 
+    it('emits updated event', async () => {
+      createComponent();
+
+      findItemState().vm.$emit('changed', STATE_CLOSED);
+      await waitForPromises();
+
+      expect(wrapper.emitted('updated')).toEqual([[]]);
+    });
+
     it('emits an error message when the mutation was unsuccessful', async () => {
       createComponent({ mutationHandler: jest.fn().mockRejectedValue('Error!') });
 
diff --git a/spec/frontend/work_items/components/work_item_title_spec.js b/spec/frontend/work_items/components/work_item_title_spec.js
index afde0d9ec4549..19b56362ac0a2 100644
--- a/spec/frontend/work_items/components/work_item_title_spec.js
+++ b/spec/frontend/work_items/components/work_item_title_spec.js
@@ -57,6 +57,15 @@ describe('WorkItemTitle component', () => {
       });
     });
 
+    it('emits updated event', async () => {
+      createComponent();
+
+      findItemTitle().vm.$emit('title-changed', 'new title');
+      await waitForPromises();
+
+      expect(wrapper.emitted('updated')).toEqual([[]]);
+    });
+
     it('does not call a mutation when the title has not changed', () => {
       createComponent();
 
diff --git a/spec/frontend/work_items/mock_data.js b/spec/frontend/work_items/mock_data.js
index 0c50a3aa50a0b..f348355001311 100644
--- a/spec/frontend/work_items/mock_data.js
+++ b/spec/frontend/work_items/mock_data.js
@@ -5,6 +5,7 @@ export const workItemQueryResponse = {
       id: 'gid://gitlab/WorkItem/1',
       title: 'Test',
       state: 'OPEN',
+      description: 'description',
       workItemType: {
         __typename: 'WorkItemType',
         id: 'gid://gitlab/WorkItems::Type/5',
@@ -27,6 +28,7 @@ export const updateWorkItemMutationResponse = {
         id: 'gid://gitlab/WorkItem/1',
         title: 'Updated title',
         state: 'OPEN',
+        description: 'description',
         workItemType: {
           __typename: 'WorkItemType',
           id: 'gid://gitlab/WorkItems::Type/5',
@@ -65,6 +67,7 @@ export const createWorkItemMutationResponse = {
         id: 'gid://gitlab/WorkItem/1',
         title: 'Updated title',
         state: 'OPEN',
+        description: 'description',
         workItemType: {
           __typename: 'WorkItemType',
           id: 'gid://gitlab/WorkItems::Type/5',
diff --git a/spec/frontend/work_items/pages/work_item_detail_spec.js b/spec/frontend/work_items/pages/work_item_detail_spec.js
index 39fe7aed0eac2..9f87655175cc9 100644
--- a/spec/frontend/work_items/pages/work_item_detail_spec.js
+++ b/spec/frontend/work_items/pages/work_item_detail_spec.js
@@ -104,4 +104,18 @@ describe('WorkItemDetail component', () => {
       issuableId: workItemQueryResponse.data.workItem.id,
     });
   });
+
+  it('emits workItemUpdated event when fields updated', async () => {
+    createComponent();
+
+    await waitForPromises();
+
+    findWorkItemState().vm.$emit('updated');
+
+    expect(wrapper.emitted('workItemUpdated')).toEqual([[]]);
+
+    findWorkItemTitle().vm.$emit('updated');
+
+    expect(wrapper.emitted('workItemUpdated')).toEqual([[], []]);
+  });
 });
diff --git a/spec/frontend/work_items/pages/work_item_root_spec.js b/spec/frontend/work_items/pages/work_item_root_spec.js
index 81d01a0cb4576..85096392e8408 100644
--- a/spec/frontend/work_items/pages/work_item_root_spec.js
+++ b/spec/frontend/work_items/pages/work_item_root_spec.js
@@ -1,21 +1,45 @@
+import { GlAlert } from '@gitlab/ui';
 import { shallowMount } from '@vue/test-utils';
 import Vue from 'vue';
 import VueApollo from 'vue-apollo';
+import waitForPromises from 'helpers/wait_for_promises';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import { visitUrl } from '~/lib/utils/url_utility';
 import WorkItemDetail from '~/work_items/components/work_item_detail.vue';
 import WorkItemsRoot from '~/work_items/pages/work_item_root.vue';
+import deleteWorkItem from '~/work_items/graphql/delete_work_item.mutation.graphql';
+import { deleteWorkItemResponse, deleteWorkItemFailureResponse } from '../mock_data';
+
+jest.mock('~/lib/utils/url_utility', () => ({
+  visitUrl: jest.fn(),
+}));
 
 Vue.use(VueApollo);
 
 describe('Work items root component', () => {
   let wrapper;
+  const issuesListPath = '/-/issues';
+  const mockToastShow = jest.fn();
 
   const findWorkItemDetail = () => wrapper.findComponent(WorkItemDetail);
+  const findAlert = () => wrapper.findComponent(GlAlert);
 
-  const createComponent = () => {
+  const createComponent = ({
+    deleteWorkItemHandler = jest.fn().mockResolvedValue(deleteWorkItemResponse),
+  } = {}) => {
     wrapper = shallowMount(WorkItemsRoot, {
+      apolloProvider: createMockApollo([[deleteWorkItem, deleteWorkItemHandler]]),
+      provide: {
+        issuesListPath,
+      },
       propsData: {
         id: '1',
       },
+      mocks: {
+        $toast: {
+          show: mockToastShow,
+        },
+      },
     });
   };
 
@@ -30,4 +54,34 @@ describe('Work items root component', () => {
       workItemId: 'gid://gitlab/WorkItem/1',
     });
   });
+
+  it('deletes work item when deleteWorkItem event emitted', async () => {
+    const deleteWorkItemHandler = jest.fn().mockResolvedValue(deleteWorkItemResponse);
+
+    createComponent({
+      deleteWorkItemHandler,
+    });
+
+    findWorkItemDetail().vm.$emit('deleteWorkItem');
+
+    await waitForPromises();
+
+    expect(deleteWorkItemHandler).toHaveBeenCalled();
+    expect(mockToastShow).toHaveBeenCalled();
+    expect(visitUrl).toHaveBeenCalledWith(issuesListPath);
+  });
+
+  it('shows alert if delete fails', async () => {
+    const deleteWorkItemHandler = jest.fn().mockRejectedValue(deleteWorkItemFailureResponse);
+
+    createComponent({
+      deleteWorkItemHandler,
+    });
+
+    findWorkItemDetail().vm.$emit('deleteWorkItem');
+
+    await waitForPromises();
+
+    expect(findAlert().exists()).toBe(true);
+  });
 });
diff --git a/spec/frontend/work_items/router_spec.js b/spec/frontend/work_items/router_spec.js
index 7e68c5e4f0ee3..99dcd886f7bd2 100644
--- a/spec/frontend/work_items/router_spec.js
+++ b/spec/frontend/work_items/router_spec.js
@@ -17,6 +17,7 @@ describe('Work items router', () => {
       router,
       provide: {
         fullPath: 'full-path',
+        issuesListPath: 'full-path/-/issues',
       },
       mocks: {
         $apollo: {
-- 
GitLab