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