Commit 9b9dbd4f authored by Coung Ngo's avatar Coung Ngo Committed by Natalia Tepluhina

Fix issue blocked modal

There are two "blocked by" modals on the issue page, and the older
one is not functional due to a recent update of the issue header
from Haml to Vue. This commit fixes this by making the issue page
use the same modal and sharing more state and behaviour between
different Vue apps on the issue page.
parent f5b96fb2
<script>
import { GlButton, GlDropdown, GlDropdownItem, GlIcon, GlLink, GlModal } from '@gitlab/ui';
import { mapGetters } from 'vuex';
import { mapActions, mapGetters, mapState } from 'vuex';
import createFlash, { FLASH_TYPES } from '~/flash';
import { IssuableType } from '~/issuable_show/constants';
import { IssuableStatus, IssueStateEvent } from '~/issue_show/constants';
import { capitalizeFirstCharacter } from '~/lib/utils/text_utility';
import { visitUrl } from '~/lib/utils/url_utility';
import { __, sprintf } from '~/locale';
import eventHub from '~/notes/event_hub';
import promoteToEpicMutation from '../queries/promote_to_epic.mutation.graphql';
import updateIssueMutation from '../queries/update_issue.mutation.graphql';
......@@ -72,15 +73,11 @@ export default {
default: '',
},
},
data() {
return {
isUpdatingState: false,
};
},
computed: {
...mapGetters(['getNoteableData']),
...mapState(['isToggleStateButtonLoading']),
...mapGetters(['openState', 'getBlockedByIssues']),
isClosed() {
return this.getNoteableData.state === IssuableStatus.Closed;
return this.openState === IssuableStatus.Closed;
},
buttonText() {
return this.isClosed
......@@ -107,9 +104,16 @@ export default {
return canClose || canReopen;
},
},
created() {
eventHub.$on('toggle.issuable.state', this.toggleIssueState);
},
beforeDestroy() {
eventHub.$off('toggle.issuable.state', this.toggleIssueState);
},
methods: {
...mapActions(['toggleStateButtonLoading']),
toggleIssueState() {
if (!this.isClosed && this.getNoteableData?.blocked_by_issues?.length) {
if (!this.isClosed && this.getBlockedByIssues.length) {
this.$refs.blockedByIssuesModal.show();
return;
}
......@@ -117,7 +121,7 @@ export default {
this.invokeUpdateIssueMutation();
},
invokeUpdateIssueMutation() {
this.isUpdatingState = true;
this.toggleStateButtonLoading(true);
this.$apollo
.mutate({
......@@ -148,11 +152,11 @@ export default {
})
.catch(() => createFlash({ message: __('Update failed. Please try again.') }))
.finally(() => {
this.isUpdatingState = false;
this.toggleStateButtonLoading(false);
});
},
promoteToEpic() {
this.isUpdatingState = true;
this.toggleStateButtonLoading(true);
this.$apollo
.mutate({
......@@ -179,7 +183,7 @@ export default {
})
.catch(() => createFlash({ message: this.$options.i18n.promoteErrorMessage }))
.finally(() => {
this.isUpdatingState = false;
this.toggleStateButtonLoading(false);
});
},
},
......@@ -191,7 +195,7 @@ export default {
<gl-dropdown class="gl-display-block gl-display-sm-none!" block :text="dropdownText">
<gl-dropdown-item
v-if="showToggleIssueStateButton"
:disabled="isUpdatingState"
:disabled="isToggleStateButtonLoading"
@click="toggleIssueState"
>
{{ buttonText }}
......@@ -199,7 +203,11 @@ export default {
<gl-dropdown-item v-if="canCreateIssue" :href="newIssuePath">
{{ newIssueTypeText }}
</gl-dropdown-item>
<gl-dropdown-item v-if="canPromoteToEpic" :disabled="isUpdatingState" @click="promoteToEpic">
<gl-dropdown-item
v-if="canPromoteToEpic"
:disabled="isToggleStateButtonLoading"
@click="promoteToEpic"
>
{{ __('Promote to epic') }}
</gl-dropdown-item>
<gl-dropdown-item v-if="!isIssueAuthor" :href="reportAbusePath">
......@@ -220,7 +228,7 @@ export default {
class="gl-display-none gl-display-sm-inline-flex!"
category="secondary"
:data-qa-selector="qaSelector"
:loading="isUpdatingState"
:loading="isToggleStateButtonLoading"
:variant="buttonVariant"
@click="toggleIssueState"
>
......@@ -243,7 +251,7 @@ export default {
</gl-dropdown-item>
<gl-dropdown-item
v-if="canPromoteToEpic"
:disabled="isUpdatingState"
:disabled="isToggleStateButtonLoading"
data-testid="promote-button"
@click="promoteToEpic"
>
......@@ -272,7 +280,7 @@ export default {
>
<p>{{ __('This issue is currently blocked by the following issues:') }}</p>
<ul>
<li v-for="issue in getNoteableData.blocked_by_issues" :key="issue.iid">
<li v-for="issue in getBlockedByIssues" :key="issue.iid">
<gl-link :href="issue.web_url">#{{ issue.iid }}</gl-link>
</li>
</ul>
......
......@@ -3,23 +3,23 @@ import $ from 'jquery';
import { mapActions, mapGetters, mapState } from 'vuex';
import { isEmpty } from 'lodash';
import Autosize from 'autosize';
import { GlAlert, GlIntersperse, GlLink, GlSprintf, GlButton, GlIcon } from '@gitlab/ui';
import { GlButton, GlIcon } from '@gitlab/ui';
import { __, sprintf } from '~/locale';
import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue';
import { deprecatedCreateFlash as Flash } from '../../flash';
import Autosave from '../../autosave';
import { deprecatedCreateFlash as Flash } from '~/flash';
import Autosave from '~/autosave';
import {
capitalizeFirstCharacter,
convertToCamelCase,
splitCamelCase,
slugifyWithUnderscore,
} from '../../lib/utils/text_utility';
} from '~/lib/utils/text_utility';
import { refreshUserMergeRequestCounts } from '~/commons/nav/user_merge_requests';
import * as constants from '../constants';
import eventHub from '../event_hub';
import NoteableWarning from '../../vue_shared/components/notes/noteable_warning.vue';
import markdownField from '../../vue_shared/components/markdown/field.vue';
import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue';
import NoteableWarning from '~/vue_shared/components/notes/noteable_warning.vue';
import markdownField from '~/vue_shared/components/markdown/field.vue';
import userAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue';
import noteSignedOutWidget from './note_signed_out_widget.vue';
import discussionLockedWidget from './discussion_locked_widget.vue';
import issuableStateMixin from '../mixins/issuable_state';
......@@ -34,10 +34,6 @@ export default {
userAvatarLink,
GlButton,
TimelineEntryItem,
GlAlert,
GlIntersperse,
GlLink,
GlSprintf,
GlIcon,
},
mixins: [issuableStateMixin],
......@@ -63,9 +59,8 @@ export default {
'getNoteableDataByProp',
'getNotesData',
'openState',
'getBlockedByIssues',
]),
...mapState(['isToggleStateButtonLoading', 'isToggleBlockedIssueWarning']),
...mapState(['isToggleStateButtonLoading']),
noteableDisplayName() {
return splitCamelCase(this.noteableType).toLowerCase();
},
......@@ -143,8 +138,8 @@ export default {
? __('merge request')
: __('issue');
},
isIssueType() {
return this.noteableDisplayName === constants.ISSUE_NOTEABLE_TYPE;
isMergeRequest() {
return this.noteableType === constants.MERGE_REQUEST_NOTEABLE_TYPE;
},
trackingLabel() {
return slugifyWithUnderscore(`${this.commentButtonTitle} button`);
......@@ -172,11 +167,9 @@ export default {
'stopPolling',
'restartPolling',
'removePlaceholderNotes',
'closeIssue',
'reopenIssue',
'closeMergeRequest',
'reopenMergeRequest',
'toggleIssueLocalState',
'toggleStateButtonLoading',
'toggleBlockedIssueWarning',
]),
setIsSubmitButtonDisabled(note, isSubmitting) {
if (!isEmpty(note) && !isSubmitting) {
......@@ -186,8 +179,6 @@ export default {
}
},
handleSave(withIssueAction) {
this.isSubmitting = true;
if (this.note.length) {
const noteData = {
endpoint: this.endpoint,
......@@ -210,9 +201,10 @@ export default {
this.resizeTextarea();
this.stopPolling();
this.isSubmitting = true;
this.saveNote(noteData)
.then(() => {
this.enableButton();
this.restartPolling();
this.discard();
......@@ -221,7 +213,6 @@ export default {
}
})
.catch(() => {
this.enableButton();
this.discard(false);
const msg = __(
'Your comment could not be submitted! Please check your network connection and try again.',
......@@ -229,64 +220,31 @@ export default {
Flash(msg, 'alert', this.$el);
this.note = noteData.data.note.note; // Restore textarea content.
this.removePlaceholderNotes();
})
.finally(() => {
this.isSubmitting = false;
});
} else {
this.toggleIssueState();
}
},
enableButton() {
this.isSubmitting = false;
},
toggleIssueState() {
if (
this.noteableType.toLowerCase() === constants.ISSUE_NOTEABLE_TYPE &&
this.isOpen &&
this.getBlockedByIssues &&
this.getBlockedByIssues.length > 0
) {
this.toggleBlockedIssueWarning(true);
if (!this.isMergeRequest) {
eventHub.$emit('toggle.issuable.state');
return;
}
if (this.isOpen) {
this.forceCloseIssue();
} else {
this.reopenIssue()
.then(() => {
this.enableButton();
refreshUserMergeRequestCounts();
})
.catch(({ data }) => {
this.enableButton();
this.toggleStateButtonLoading(false);
let errorMessage = sprintf(
__('Something went wrong while reopening the %{issuable}. Please try again later'),
{ issuable: this.noteableDisplayName },
);
if (data) {
errorMessage = Object.values(data).join('\n');
}
const toggleMergeRequestState = this.isOpen
? this.closeMergeRequest
: this.reopenMergeRequest;
Flash(errorMessage);
});
}
},
forceCloseIssue() {
this.closeIssue()
.then(() => {
this.enableButton();
refreshUserMergeRequestCounts();
})
.catch(() => {
this.enableButton();
this.toggleStateButtonLoading(false);
Flash(
sprintf(
__('Something went wrong while closing the %{issuable}. Please try again later'),
{ issuable: this.noteableDisplayName },
),
);
});
const errorMessage = this.isOpen
? __('Something went wrong while closing the merge request. Please try again later')
: __('Something went wrong while reopening the merge request. Please try again later');
toggleMergeRequestState()
.then(refreshUserMergeRequestCounts)
.catch(() => Flash(errorMessage));
},
discard(shouldClear = true) {
// `blur` is needed to clear slash commands autocomplete cache if event fired.
......@@ -384,6 +342,7 @@ export default {
name="note[note]"
class="note-textarea js-vue-comment-form js-note-text js-gfm-input js-autosize markdown-area"
data-qa-selector="comment_field"
data-testid="comment-field"
data-supports-quick-actions="true"
:aria-label="__('Description')"
:placeholder="__('Write a comment or drag your files here…')"
......@@ -392,36 +351,7 @@ export default {
@keydown.ctrl.enter="handleSave()"
></textarea>
</markdown-field>
<gl-alert
v-if="isToggleBlockedIssueWarning"
class="gl-mt-5"
:title="__('Are you sure you want to close this blocked issue?')"
:primary-button-text="__('Yes, close issue')"
:secondary-button-text="__('Cancel')"
variant="warning"
:dismissible="false"
@primaryAction="toggleBlockedIssueWarning(false) && forceCloseIssue()"
@secondaryAction="toggleBlockedIssueWarning(false) && enableButton()"
>
<p>
<gl-sprintf
:message="
__('This issue is currently blocked by the following issues: %{issues}.')
"
>
<template #issues>
<gl-intersperse>
<gl-link
v-for="blockingIssue in getBlockedByIssues"
:key="blockingIssue.web_url"
:href="blockingIssue.web_url"
>#{{ blockingIssue.iid }}</gl-link
>
</gl-intersperse>
</template>
</gl-sprintf>
</p>
</gl-alert>
<div class="note-form-actions">
<div
class="btn-group gl-mr-3 comment-type-dropdown js-comment-type-dropdown droplab-dropdown"
......@@ -430,6 +360,7 @@ export default {
:disabled="isSubmitButtonDisabled"
class="js-comment-button js-comment-submit-button"
data-qa-selector="comment_button"
data-testid="comment-button"
type="submit"
category="primary"
variant="success"
......@@ -488,15 +419,13 @@ export default {
</div>
<gl-button
v-if="canToggleIssueState && !isToggleBlockedIssueWarning"
v-if="canToggleIssueState"
:loading="isToggleStateButtonLoading"
category="secondary"
:variant="buttonVariant"
:class="[
actionButtonClassNames,
'btn-comment btn-comment-and-close js-action-button',
]"
:disabled="isToggleStateButtonLoading || isSubmitting"
:class="[actionButtonClassNames, 'btn-comment btn-comment-and-close']"
:disabled="isSubmitting"
data-testid="close-reopen-button"
@click="handleSave(true)"
>{{ issueActionButtonTitle }}</gl-button
>
......
......@@ -244,21 +244,7 @@ export const toggleResolveNote = ({ commit, dispatch }, { endpoint, isResolved,
});
};
export const toggleBlockedIssueWarning = ({ commit }, value) => {
commit(types.TOGGLE_BLOCKED_ISSUE_WARNING, value);
// Hides Close issue button at the top of issue page
const closeDropdown = document.querySelector('.js-issuable-close-dropdown');
if (closeDropdown) {
closeDropdown.classList.toggle('d-none');
} else {
const closeButton = document.querySelector(
'.detail-page-header-actions .btn-close.btn-grouped',
);
closeButton.classList.toggle('d-md-block');
}
};
export const closeIssue = ({ commit, dispatch, state }) => {
export const closeMergeRequest = ({ commit, dispatch, state }) => {
dispatch('toggleStateButtonLoading', true);
return axios.put(state.notesData.closePath).then(({ data }) => {
commit(types.CLOSE_ISSUE);
......@@ -267,7 +253,7 @@ export const closeIssue = ({ commit, dispatch, state }) => {
});
};
export const reopenIssue = ({ commit, dispatch, state }) => {
export const reopenMergeRequest = ({ commit, dispatch, state }) => {
dispatch('toggleStateButtonLoading', true);
return axios.put(state.notesData.reopenPath).then(({ data }) => {
commit(types.REOPEN_ISSUE);
......
......@@ -26,7 +26,6 @@ export default () => ({
// View layer
isToggleStateButtonLoading: false,
isToggleBlockedIssueWarning: false,
isNotesFetched: false,
isLoading: true,
isLoadingDescriptionVersion: false,
......
......@@ -43,7 +43,6 @@ export const SET_FETCHING_DISCUSSIONS = 'SET_FETCHING_DISCUSSIONS';
export const CLOSE_ISSUE = 'CLOSE_ISSUE';
export const REOPEN_ISSUE = 'REOPEN_ISSUE';
export const TOGGLE_STATE_BUTTON_LOADING = 'TOGGLE_STATE_BUTTON_LOADING';
export const TOGGLE_BLOCKED_ISSUE_WARNING = 'TOGGLE_BLOCKED_ISSUE_WARNING';
export const SET_ISSUE_CONFIDENTIAL = 'SET_ISSUE_CONFIDENTIAL';
export const SET_ISSUABLE_LOCK = 'SET_ISSUABLE_LOCK';
......
......@@ -301,10 +301,6 @@ export default {
Object.assign(state, { isToggleStateButtonLoading: value });
},
[types.TOGGLE_BLOCKED_ISSUE_WARNING](state, value) {
Object.assign(state, { isToggleBlockedIssueWarning: value });
},
[types.SET_NOTES_FETCHED_STATE](state, value) {
Object.assign(state, { isNotesFetched: value });
},
......
......@@ -88,7 +88,7 @@ export default {
};
</script>
<template>
<div class="issuable-note-warning">
<div class="issuable-note-warning" data-testid="confidential-warning">
<gl-icon v-if="!isLockedAndConfidential" :name="warningIcon" :size="16" class="icon inline" />
<span v-if="isLockedAndConfidential" ref="lockedAndConfidential">
......
---
title: Fix issue blocked-by modal
merge_request: 48273
author:
type: fixed
......@@ -14,13 +14,39 @@ RSpec.describe 'Related issues', :js do
let_it_be(:issue_project_b_a) { create(:issue, project: project_b) }
let_it_be(:issue_project_unauthorized_a) { create(:issue, project: project_unauthorized) }
shared_examples 'issue closed by modal' do |selector|
it 'shows a modal to confirm closing the issue' do
# Workaround for modal not showing when issue is first added
visit project_issue_path(project, issue_a)
wait_for_requests
within(selector) do
click_button 'Close issue'
end
within('.modal-content', visible: true) do
expect(page).to have_text 'Are you sure you want to close this blocked issue?'
expect(page).to have_link("##{issue_b.iid}", href: project_issue_path(project, issue_b))
click_button 'Yes, close issue'
end
wait_for_requests
expect(page).not_to have_selector('.modal-content', visible: true)
within(first('.status-box', visible: :all)) do
expect(page).to have_text 'Closed'
end
end
end
context 'when user has permission to manage related issues' do
before do
stub_feature_flags(vue_issue_header: false)
project.add_maintainer(user)
project_b.add_maintainer(user)
gitlab_sign_in(user)
sign_in(user)
end
context 'with "Relates to", "Blocks", "Is blocked by" groupings' do
......@@ -97,29 +123,12 @@ RSpec.describe 'Related issues', :js do
expect(find('.js-related-issues-header-issue-count')).to have_content('1')
end
it 'hides the modal when issue is closed' do
# Workaround for modal not showing when issue is first added
visit project_issue_path(project, issue_a)
wait_for_requests
within('.new-note') do
button = find(:button, 'Close issue')
scroll_to(button)
button.click
end
click_button 'Yes, close issue'
wait_for_requests
find(:button, 'Yes, close issue', visible: false)
status_box = first('.status-box', visible: :all)
scroll_to(status_box)
context 'when clicking the top `Close issue` button in the issue header', :aggregate_failures do
it_behaves_like 'issue closed by modal', '.detail-page-header'
end
within(status_box) do
expect(page).to have_content 'Closed'
end
context 'when clicking the bottom `Close issue` button below the comment textarea', :aggregate_failures do
it_behaves_like 'issue closed by modal', '.new-note'
end
end
......
/* eslint-disable one-var */
import MockAdapter from 'axios-mock-adapter';
import $ from 'jquery';
import Issue from '~/issue';
import axios from '~/lib/utils/axios_utils';
import '~/lib/utils/text_utility';
describe('Issue', () => {
let testContext;
beforeEach(() => {
testContext = {};
});
let $btn, $dropdown, $alert, $boxOpen, $boxClosed;
preloadFixtures('ee/issues/blocked-issue.html');
describe('with blocked issue', () => {
let mock;
function setup() {
testContext.issue = new Issue();
testContext.$projectIssuesCounter = $('.issue_counter').first();
testContext.$projectIssuesCounter.text('1,001');
}
function mockCloseButtonResponseSuccess(url, response) {
mock.onPut(url).reply(() => [200, response]);
}
beforeEach(() => {
loadFixtures('ee/issues/blocked-issue.html');
mock = new MockAdapter(axios);
mock.onGet(/(.*)\/related_branches$/).reply(200, {});
jest.spyOn(axios, 'get');
});
afterEach(() => {
mock.restore();
});
it(`displays warning when attempting to close the issue`, done => {
setup();
$btn = $('.js-issuable-close-button');
$dropdown = $('.js-issuable-close-dropdown ');
$alert = $('.js-close-blocked-issue-warning');
expect($btn).toExist();
expect($btn).toHaveClass('btn-issue-blocked');
expect($dropdown).not.toHaveClass('hidden');
expect($alert).toHaveClass('hidden');
testContext.$triggeredButton = $btn;
testContext.$triggeredButton.trigger('click');
setImmediate(() => {
expect($alert).not.toHaveClass('hidden');
expect($dropdown).toHaveClass('hidden');
done();
});
});
it(`hides warning when cancelling closing the issue`, done => {
setup();
$btn = $('.js-issuable-close-button');
$alert = $('.js-close-blocked-issue-warning');
testContext.$triggeredButton = $btn;
testContext.$triggeredButton.trigger('click');
setImmediate(() => {
expect($alert).not.toHaveClass('hidden');
const $cancelbtn = $('.js-close-blocked-issue-warning .js-cancel-blocked-issue-warning');
$cancelbtn.trigger('click');
expect($alert).toHaveClass('hidden');
done();
});
});
it('closes the issue when clicking alert close button', done => {
$btn = $('.js-issuable-close-button');
$boxOpen = $('div.status-box-open');
$boxClosed = $('div.status-box-issue-closed');
expect($boxOpen).not.toHaveClass('hidden');
expect($boxOpen).toHaveText('Open');
expect($boxClosed).toHaveClass('hidden');
testContext.$triggeredButton = $btn;
mockCloseButtonResponseSuccess(testContext.$triggeredButton.data('endpoint'), {
id: 34,
});
setup();
testContext.$triggeredButton.trigger('click');
const $btnCloseAnyway = $('.js-close-blocked-issue-warning .btn-close-anyway');
$btnCloseAnyway.trigger('click');
setImmediate(() => {
expect($btn).toHaveText('Reopen');
expect($boxOpen).toHaveClass('hidden');
expect($boxClosed).not.toHaveClass('hidden');
expect($boxClosed).toHaveText('Closed');
done();
});
});
});
});
import { mount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils';
import CommentForm from '~/notes/components/comment_form.vue';
import createStore from '~/notes/stores';
import {
notesDataMock,
userDataMock,
noteableDataMock,
} from '../../../../../spec/frontend/notes/mock_data';
jest.mock('autosize');
jest.mock('~/commons/nav/user_merge_requests');
jest.mock('~/gl_form');
describe('issue_comment_form component', () => {
let store;
let wrapper;
let axiosMock;
const setupStore = (userData, noteableData) => {
store.dispatch('setUserData', userData);
store.dispatch('setNoteableData', noteableData);
store.dispatch('setNotesData', notesDataMock);
};
const mountComponent = (noteableType = 'issue') => {
wrapper = mount(CommentForm, {
propsData: {
noteableType,
},
store,
});
};
const findCloseBtn = () => wrapper.find('.btn-comment-and-close');
beforeEach(() => {
axiosMock = new MockAdapter(axios);
store = createStore();
// This is necessary as we query Close issue button at the top of issue page when clicking bottom button
setFixtures(
'<div class="detail-page-header-actions"><button class="btn-close btn-grouped"></button></div>',
);
});
afterEach(() => {
axiosMock.restore();
wrapper.destroy();
});
describe('when issue is not blocked by other issues', () => {
beforeEach(() => {
setupStore(userDataMock, noteableDataMock);
mountComponent();
});
afterEach(() => {
wrapper.destroy();
});
it('should close the issue when clicking close issue button', done => {
jest.spyOn(wrapper.vm, 'closeIssue').mockResolvedValue();
findCloseBtn().trigger('click');
wrapper.vm.$nextTick(() => {
expect(wrapper.vm.closeIssue).toHaveBeenCalled();
done();
});
});
});
describe('when issue is blocked by other issues', () => {
let noteableDataMockBlocked;
beforeEach(() => {
noteableDataMockBlocked = Object.assign(noteableDataMock, {
blocked_by_issues: [
{
iid: 1,
web_url: 'path/to/issue',
},
],
});
setupStore(userDataMock, noteableDataMockBlocked);
mountComponent();
});
afterEach(() => {
wrapper.destroy();
});
it('should display alert warning when attempting to close issue, close button is hidden', done => {
findCloseBtn().trigger('click');
wrapper.vm.$nextTick(() => {
const warning = wrapper.find('.gl-alert-warning');
expect(warning.exists()).toBe(true);
expect(warning.text()).toContain('Are you sure you want to close this blocked issue?');
const linkToBlockingIssue = warning.find('.gl-link');
expect(linkToBlockingIssue.text()).toContain(
noteableDataMockBlocked.blocked_by_issues[0].iid,
);
done();
});
});
it('should close the issue when clicking close issue button in alert', done => {
jest.spyOn(wrapper.vm, 'closeIssue').mockResolvedValue();
findCloseBtn().trigger('click');
wrapper.vm.$nextTick(() => {
expect(findCloseBtn().exists()).toBe(false);
const warning = wrapper.find('.gl-alert-warning');
const primaryButton = warning.find('.gl-alert-actions .gl-button');
expect(primaryButton.text()).toEqual('Yes, close issue');
primaryButton.trigger('click');
wrapper.vm.$nextTick(() => {
expect(warning.exists()).toBe(false);
done();
});
setTimeout(() => {
expect(wrapper.vm.closeIssue).toHaveBeenCalled();
done();
}, 1000);
done();
});
});
it('should dismiss alert warning when clicking cancel button in alert', done => {
findCloseBtn().trigger('click');
wrapper.vm.$nextTick(() => {
const warning = wrapper.find('.gl-alert-warning');
const secondaryButton = warning.find('.gl-alert-actions .btn-default');
expect(secondaryButton.text()).toEqual('Cancel');
secondaryButton.trigger('click');
wrapper.vm.$nextTick(() => {
expect(warning.exists()).toBe(false);
done();
});
});
});
});
});
......@@ -25422,7 +25422,7 @@ msgstr ""
msgid "Something went wrong while archiving a requirement."
msgstr ""
msgid "Something went wrong while closing the %{issuable}. Please try again later"
msgid "Something went wrong while closing the merge request. Please try again later"
msgstr ""
msgid "Something went wrong while creating a requirement."
......@@ -25509,7 +25509,7 @@ msgstr ""
msgid "Something went wrong while reopening a requirement."
msgstr ""
msgid "Something went wrong while reopening the %{issuable}. Please try again later"
msgid "Something went wrong while reopening the merge request. Please try again later"
msgstr ""
msgid "Something went wrong while resolving this discussion. Please try again."
......
......@@ -7,6 +7,7 @@ import HeaderActions from '~/issue_show/components/header_actions.vue';
import { IssuableStatus, IssueStateEvent } from '~/issue_show/constants';
import promoteToEpicMutation from '~/issue_show/queries/promote_to_epic.mutation.graphql';
import * as urlUtility from '~/lib/utils/url_utility';
import eventHub from '~/notes/event_hub';
import createStore from '~/notes/stores';
jest.mock('~/flash');
......@@ -82,8 +83,10 @@ describe('HeaderActions component', () => {
} = {}) => {
mutateMock = jest.fn().mockResolvedValue(mutateResponse);
store.getters.getNoteableData.state = issueState;
store.getters.getNoteableData.blocked_by_issues = blockedByIssues;
store.dispatch('setNoteableData', {
blocked_by_issues: blockedByIssues,
state: issueState,
});
return shallowMount(HeaderActions, {
localVue,
......@@ -273,6 +276,26 @@ describe('HeaderActions component', () => {
});
});
describe('when `toggle.issuable.state` event is emitted', () => {
it('invokes a method to toggle the issue state', () => {
wrapper = mountComponent({ mutateResponse: updateIssueMutationResponse });
eventHub.$emit('toggle.issuable.state');
expect(mutateMock).toHaveBeenCalledWith(
expect.objectContaining({
variables: {
input: {
iid: defaultProps.iid,
projectPath: defaultProps.projectPath,
stateEvent: IssueStateEvent.Close,
},
},
}),
);
});
});
describe('modal', () => {
const blockedByIssues = [
{ iid: 13, web_url: 'gitlab-org/gitlab-test/-/issues/13' },
......
......@@ -174,10 +174,10 @@ describe('Actions Notes Store', () => {
axiosMock.onAny().reply(200, {});
});
describe('closeIssue', () => {
describe('closeMergeRequest', () => {
it('sets state as closed', done => {
store
.dispatch('closeIssue', { notesData: { closeIssuePath: '' } })
.dispatch('closeMergeRequest', { notesData: { closeIssuePath: '' } })
.then(() => {
expect(store.state.noteableData.state).toEqual('closed');
expect(store.state.isToggleStateButtonLoading).toEqual(false);
......@@ -187,10 +187,10 @@ describe('Actions Notes Store', () => {
});
});
describe('reopenIssue', () => {
describe('reopenMergeRequest', () => {
it('sets state as reopened', done => {
store
.dispatch('reopenIssue', { notesData: { reopenIssuePath: '' } })
.dispatch('reopenMergeRequest', { notesData: { reopenIssuePath: '' } })
.then(() => {
expect(store.state.noteableData.state).toEqual('reopened');
expect(store.state.isToggleStateButtonLoading).toEqual(false);
......@@ -253,30 +253,6 @@ describe('Actions Notes Store', () => {
});
});
describe('toggleBlockedIssueWarning', () => {
it('should set issue warning as true', done => {
testAction(
actions.toggleBlockedIssueWarning,
true,
{},
[{ type: 'TOGGLE_BLOCKED_ISSUE_WARNING', payload: true }],
[],
done,
);
});
it('should set issue warning as false', done => {
testAction(
actions.toggleBlockedIssueWarning,
false,
{},
[{ type: 'TOGGLE_BLOCKED_ISSUE_WARNING', payload: false }],
[],
done,
);
});
});
describe('fetchData', () => {
describe('given there are no notes', () => {
const lastFetchedAt = '13579';
......
......@@ -687,42 +687,6 @@ describe('Notes Store mutations', () => {
});
});
describe('TOGGLE_BLOCKED_ISSUE_WARNING', () => {
it('should set isToggleBlockedIssueWarning as true', () => {
const state = {
discussions: [],
targetNoteHash: null,
lastFetchedAt: null,
isToggleStateButtonLoading: false,
isToggleBlockedIssueWarning: false,
notesData: {},
userData: {},
noteableData: {},
};
mutations.TOGGLE_BLOCKED_ISSUE_WARNING(state, true);
expect(state.isToggleBlockedIssueWarning).toEqual(true);
});
it('should set isToggleBlockedIssueWarning as false', () => {
const state = {
discussions: [],
targetNoteHash: null,
lastFetchedAt: null,
isToggleStateButtonLoading: false,
isToggleBlockedIssueWarning: true,
notesData: {},
userData: {},
noteableData: {},
};
mutations.TOGGLE_BLOCKED_ISSUE_WARNING(state, false);
expect(state.isToggleBlockedIssueWarning).toEqual(false);
});
});
describe('SET_APPLYING_BATCH_STATE', () => {
const buildDiscussions = suggestionsInfo => {
const suggestions = suggestionsInfo.map(({ suggestionId }) => ({ id: suggestionId }));
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment