Commit e7df7f46 authored by Natalia Tepluhina's avatar Natalia Tepluhina

Merge branch '299933-move-delete-option-into-ellipsis-menu-for-issuables' into 'master'

Add `Delete issue` option in issue page ellipsis dropdown

See merge request gitlab-org/gitlab!76455
parents 6568965c b6a019ff
......@@ -289,13 +289,11 @@ export default {
window.addEventListener('beforeunload', this.handleBeforeUnloadEvent);
eventHub.$on('delete.issuable', this.deleteIssuable);
eventHub.$on('update.issuable', this.updateIssuable);
eventHub.$on('close.form', this.closeForm);
eventHub.$on('open.form', this.openForm);
},
beforeDestroy() {
eventHub.$off('delete.issuable', this.deleteIssuable);
eventHub.$off('update.issuable', this.updateIssuable);
eventHub.$off('close.form', this.closeForm);
eventHub.$off('open.form', this.openForm);
......@@ -418,25 +416,6 @@ export default {
});
},
deleteIssuable(payload) {
return this.service
.deleteIssuable(payload)
.then((res) => res.data)
.then((data) => {
// Stop the poll so we don't get 404's with the issuable not existing
this.poll.stop();
visitUrl(data.web_url);
})
.catch(() => {
createFlash({
message: sprintf(__('Error deleting %{issuableType}'), {
issuableType: this.issuableType,
}),
});
});
},
hideStickyHeader() {
this.isStickyHeaderShowing = false;
},
......@@ -475,6 +454,7 @@ export default {
<div>
<div v-if="canUpdate && showForm">
<form-component
:endpoint="endpoint"
:form-state="formState"
:initial-description-text="initialDescriptionText"
:can-destroy="canDestroy"
......
<script>
import { GlModal } from '@gitlab/ui';
import csrf from '~/lib/utils/csrf';
import { capitalizeFirstCharacter } from '~/lib/utils/text_utility';
import { __, sprintf } from '~/locale';
export default {
actionCancel: { text: __('Cancel') },
csrf,
components: {
GlModal,
},
props: {
issuePath: {
type: String,
required: true,
},
issueType: {
type: String,
required: true,
},
modalId: {
type: String,
required: true,
},
title: {
type: String,
required: true,
},
},
computed: {
actionPrimary() {
return {
attributes: { variant: 'danger' },
text: this.title,
};
},
bodyText() {
return this.issueType.toLowerCase() === 'epic'
? __('Delete this epic and all descendants?')
: sprintf(__('%{issuableType} will be removed! Are you sure?'), {
issuableType: capitalizeFirstCharacter(this.issueType),
});
},
},
methods: {
submitForm() {
this.$emit('delete');
this.$refs.form.submit();
},
},
};
</script>
<template>
<gl-modal
:action-cancel="$options.actionCancel"
:action-primary="actionPrimary"
:modal-id="modalId"
size="sm"
:title="title"
@primary="submitForm"
>
<form ref="form" :action="issuePath" method="post">
<input type="hidden" name="_method" value="delete" />
<input type="hidden" name="authenticity_token" :value="$options.csrf.token" />
<input type="hidden" name="destroy_confirm" value="true" />
{{ bodyText }}
</form>
</gl-modal>
</template>
<script>
import { GlButton, GlModal, GlModalDirective } from '@gitlab/ui';
import { GlButton, GlModalDirective } from '@gitlab/ui';
import { uniqueId } from 'lodash';
import { __, sprintf } from '~/locale';
import Tracking from '~/tracking';
import eventHub from '../event_hub';
import updateMixin from '../mixins/update';
import getIssueStateQuery from '../queries/get_issue_state.query.graphql';
import DeleteIssueModal from './delete_issue_modal.vue';
const issuableTypes = {
issue: __('Issue'),
......@@ -12,20 +14,26 @@ const issuableTypes = {
incident: __('Incident'),
};
const trackingMixin = Tracking.mixin({ label: 'delete_issue' });
export default {
components: {
DeleteIssueModal,
GlButton,
GlModal,
},
directives: {
GlModal: GlModalDirective,
},
mixins: [updateMixin],
mixins: [trackingMixin, updateMixin],
props: {
canDestroy: {
type: Boolean,
required: true,
},
endpoint: {
required: true,
type: String,
},
formState: {
type: Object,
required: true,
......@@ -65,27 +73,9 @@ export default {
issuableType: this.typeToShow.toLowerCase(),
});
},
deleteIssuableModalText() {
return this.issuableType === 'epic'
? __('Delete this epic and all descendants?')
: sprintf(__('%{issuableType} will be removed! Are you sure?'), {
issuableType: this.typeToShow,
});
},
isSubmitEnabled() {
return this.formState.title.trim() !== '';
},
modalActionProps() {
return {
primary: {
text: this.deleteIssuableButtonText,
attributes: [{ variant: 'danger' }, { loading: this.deleteLoading }],
},
cancel: {
text: __('Cancel'),
},
};
},
shouldShowDeleteButton() {
return this.canDestroy && this.showDeleteButton;
},
......@@ -101,7 +91,7 @@ export default {
},
deleteIssuable() {
this.deleteLoading = true;
eventHub.$emit('delete.issuable', { destroy_confirm: true });
eventHub.$emit('delete.issuable');
},
},
};
......@@ -135,22 +125,17 @@ export default {
variant="danger"
class="qa-delete-button"
data-testid="issuable-delete-button"
@click="track('click_button')"
>
{{ deleteIssuableButtonText }}
</gl-button>
<gl-modal
ref="removeModal"
<delete-issue-modal
:issue-path="endpoint"
:issue-type="typeToShow"
:modal-id="modalId"
size="sm"
:action-primary="modalActionProps.primary"
:action-cancel="modalActionProps.cancel"
@primary="deleteIssuable"
>
<template #modal-title>{{ deleteIssuableButtonText }}</template>
<div>
<p class="gl-mb-1">{{ deleteIssuableModalText }}</p>
</div>
</gl-modal>
:title="deleteIssuableButtonText"
@delete="deleteIssuable"
/>
</div>
</div>
</template>
......@@ -26,6 +26,10 @@ export default {
type: Boolean,
required: true,
},
endpoint: {
type: String,
required: true,
},
formState: {
type: Object,
required: true,
......@@ -213,6 +217,7 @@ export default {
:enable-autocomplete="enableAutocomplete"
/>
<edit-actions
:endpoint="endpoint"
:form-state="formState"
:can-destroy="canDestroy"
:show-delete-button="showDeleteButton"
......
<script>
import { GlButton, GlDropdown, GlDropdownItem, GlLink, GlModal } from '@gitlab/ui';
import {
GlButton,
GlDropdown,
GlDropdownDivider,
GlDropdownItem,
GlLink,
GlModal,
GlModalDirective,
} from '@gitlab/ui';
import { mapActions, mapGetters, mapState } from 'vuex';
import createFlash, { FLASH_TYPES } from '~/flash';
import { EVENT_ISSUABLE_VUE_APP_CHANGE } from '~/issuable/constants';
......@@ -10,23 +18,21 @@ import { capitalizeFirstCharacter } from '~/lib/utils/text_utility';
import { visitUrl } from '~/lib/utils/url_utility';
import { s__, __, sprintf } from '~/locale';
import eventHub from '~/notes/event_hub';
import Tracking from '~/tracking';
import promoteToEpicMutation from '../queries/promote_to_epic.mutation.graphql';
import updateIssueMutation from '../queries/update_issue.mutation.graphql';
import DeleteIssueModal from './delete_issue_modal.vue';
const trackingMixin = Tracking.mixin({ label: 'delete_issue' });
export default {
components: {
GlButton,
GlDropdown,
GlDropdownItem,
GlLink,
GlModal,
},
actionCancel: {
text: __('Cancel'),
},
actionPrimary: {
text: __('Yes, close issue'),
},
deleteModalId: 'delete-modal-id',
i18n: {
promoteErrorMessage: __(
'Something went wrong while promoting the issue to an epic. Please try again.',
......@@ -35,10 +41,26 @@ export default {
'The issue was successfully promoted to an epic. Redirecting to epic...',
),
},
components: {
DeleteIssueModal,
GlButton,
GlDropdown,
GlDropdownDivider,
GlDropdownItem,
GlLink,
GlModal,
},
directives: {
GlModal: GlModalDirective,
},
mixins: [trackingMixin],
inject: {
canCreateIssue: {
default: false,
},
canDestroyIssue: {
default: false,
},
canPromoteToEpic: {
default: false,
},
......@@ -57,6 +79,9 @@ export default {
isIssueAuthor: {
default: false,
},
issuePath: {
default: '',
},
issueType: {
default: IssuableType.Issue,
},
......@@ -92,6 +117,9 @@ export default {
? sprintf(__('Reopen %{issueType}'), { issueType: this.issueTypeText })
: sprintf(__('Close %{issueType}'), { issueType: this.issueTypeText });
},
deleteButtonText() {
return sprintf(__('Delete %{issuableType}'), { issuableType: this.issueTypeText });
},
qaSelector() {
return this.isClosed ? 'reopen_issue_button' : 'close_issue_button';
},
......@@ -141,8 +169,7 @@ export default {
})
.then(({ data }) => {
if (data.updateIssue.errors.length) {
createFlash({ message: data.updateIssue.errors.join('. ') });
return;
throw new Error();
}
const payload = {
......@@ -175,8 +202,7 @@ export default {
})
.then(({ data }) => {
if (data.promoteToEpic.errors.length) {
createFlash({ message: data.promoteToEpic.errors.join('; ') });
return;
throw new Error();
}
createFlash({
......@@ -228,6 +254,16 @@ export default {
>
{{ __('Submit as spam') }}
</gl-dropdown-item>
<template v-if="canDestroyIssue">
<gl-dropdown-divider />
<gl-dropdown-item
v-gl-modal="$options.deleteModalId"
variant="danger"
@click="track('click_dropdown')"
>
{{ deleteButtonText }}
</gl-dropdown-item>
</template>
</gl-dropdown>
<gl-button
......@@ -271,6 +307,16 @@ export default {
>
{{ __('Submit as spam') }}
</gl-dropdown-item>
<template v-if="canDestroyIssue">
<gl-dropdown-divider />
<gl-dropdown-item
v-gl-modal="$options.deleteModalId"
variant="danger"
@click="track('click_dropdown')"
>
{{ deleteButtonText }}
</gl-dropdown-item>
</template>
</gl-dropdown>
<gl-modal
......@@ -288,5 +334,12 @@ export default {
</li>
</ul>
</gl-modal>
<delete-issue-modal
:issue-path="issuePath"
:issue-type="issueType"
:modal-id="$options.deleteModalId"
:title="deleteButtonText"
/>
</div>
</template>
......@@ -81,12 +81,14 @@ export function initIncidentHeaderActions(store) {
store,
provide: {
canCreateIssue: parseBoolean(el.dataset.canCreateIncident),
canDestroyIssue: parseBoolean(el.dataset.canDestroyIssue),
canPromoteToEpic: parseBoolean(el.dataset.canPromoteToEpic),
canReopenIssue: parseBoolean(el.dataset.canReopenIssue),
canReportSpam: parseBoolean(el.dataset.canReportSpam),
canUpdateIssue: parseBoolean(el.dataset.canUpdateIssue),
iid: el.dataset.iid,
isIssueAuthor: parseBoolean(el.dataset.isIssueAuthor),
issuePath: el.dataset.issuePath,
issueType: el.dataset.issueType,
newIssuePath: el.dataset.newIssuePath,
projectPath: el.dataset.projectPath,
......
......@@ -66,12 +66,14 @@ export function initIssueHeaderActions(store) {
store,
provide: {
canCreateIssue: parseBoolean(el.dataset.canCreateIssue),
canDestroyIssue: parseBoolean(el.dataset.canDestroyIssue),
canPromoteToEpic: parseBoolean(el.dataset.canPromoteToEpic),
canReopenIssue: parseBoolean(el.dataset.canReopenIssue),
canReportSpam: parseBoolean(el.dataset.canReportSpam),
canUpdateIssue: parseBoolean(el.dataset.canUpdateIssue),
iid: el.dataset.iid,
isIssueAuthor: parseBoolean(el.dataset.isIssueAuthor),
issuePath: el.dataset.issuePath,
issueType: el.dataset.issueType,
newIssuePath: el.dataset.newIssuePath,
projectPath: el.dataset.projectPath,
......
......@@ -193,11 +193,13 @@ module IssuesHelper
{
can_create_issue: show_new_issue_link?(project).to_s,
can_create_incident: create_issue_type_allowed?(project, :incident).to_s,
can_destroy_issue: can?(current_user, :"destroy_#{issuable.to_ability_name}", issuable).to_s,
can_reopen_issue: can?(current_user, :reopen_issue, issuable).to_s,
can_report_spam: issuable.submittable_as_spam_by?(current_user).to_s,
can_update_issue: can?(current_user, :update_issue, issuable).to_s,
iid: issuable.iid,
is_issue_author: (issuable.author == current_user).to_s,
issue_path: issuable_path(issuable),
issue_type: issuable_display_type(issuable),
new_issue_path: new_project_issue_path(project, new_issuable_params),
project_path: project.full_path,
......
......@@ -34,7 +34,7 @@ To learn how the GitLab Strategic Marketing department uses GitLab issues with [
- [Edit issues](managing_issues.md#edit-an-issue)
- [Move issues](managing_issues.md#moving-issues)
- [Close issues](managing_issues.md#closing-issues)
- [Delete issues](managing_issues.md#deleting-issues)
- [Delete issues](managing_issues.md#delete-an-issue)
- [Promote issues](managing_issues.md#promote-an-issue-to-an-epic)
- [Set a due date](due_dates.md)
- [Import issues](csv_import.md)
......
......@@ -439,12 +439,23 @@ can change an issue's type. To do this, edit the issue and select an issue type
![Change the issue type](img/issue_type_change_v13_12.png)
## Deleting issues
## Delete an issue
Users with the [Owner role](../../permissions.md) can delete an issue by
editing it and selecting **Delete issue**.
> Deleting from the vertical ellipsis menu [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/299933) in GitLab 14.6.
![delete issue - button](img/delete_issue_v13_11.png)
Prerequisites:
- You must have the [Owner role](../../permissions.md) for a project.
To delete an issue:
1. In an issue, select the vertical ellipsis (**{ellipsis_v}**).
1. Select **Delete issue**.
Alternatively:
1. In an issue, select **Edit title and description** (**{pencil}**).
1. Select **Delete issue**.
## Promote an issue to an epic **(PREMIUM)**
......
......@@ -13779,9 +13779,6 @@ msgstr ""
msgid "Error creating the snippet"
msgstr ""
msgid "Error deleting %{issuableType}"
msgstr ""
msgid "Error deleting project. Check logs for error details."
msgstr ""
......
......@@ -4,7 +4,8 @@ require 'spec_helper'
RSpec.describe 'issue header', :js do
let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project) }
let_it_be(:group) { create(:group) }
let_it_be(:project) { create(:project, group: group) }
let_it_be(:issue) { create(:issue, project: project) }
let_it_be(:closed_issue) { create(:issue, :closed, project: project) }
let_it_be(:closed_locked_issue) { create(:issue, :closed, :locked, project: project) }
......@@ -12,7 +13,7 @@ RSpec.describe 'issue header', :js do
context 'when user has permission to update' do
before do
project.add_maintainer(user)
group.add_owner(user)
sign_in(user)
end
......@@ -24,9 +25,10 @@ RSpec.describe 'issue header', :js do
click_button 'Issue actions'
end
it 'only shows the "New issue" and "Report abuse" items', :aggregate_failures do
it 'shows the "New issue", "Report abuse", and "Delete issue" items', :aggregate_failures do
expect(page).to have_link 'New issue'
expect(page).to have_link 'Report abuse'
expect(page).to have_button 'Delete issue'
expect(page).not_to have_link 'Submit as spam'
end
end
......@@ -116,6 +118,7 @@ RSpec.describe 'issue header', :js do
expect(page).to have_link 'New issue'
expect(page).to have_link 'Report abuse'
expect(page).not_to have_link 'Submit as spam'
expect(page).not_to have_button 'Delete issue'
end
end
......
......@@ -326,44 +326,6 @@ describe('Issuable output', () => {
});
});
describe('deleteIssuable', () => {
it('changes URL when deleted', () => {
jest.spyOn(wrapper.vm.service, 'deleteIssuable').mockResolvedValue({
data: {
web_url: '/test',
},
});
return wrapper.vm.deleteIssuable().then(() => {
expect(visitUrl).toHaveBeenCalledWith('/test');
});
});
it('stops polling when deleting', () => {
const spy = jest.spyOn(wrapper.vm.poll, 'stop');
jest.spyOn(wrapper.vm.service, 'deleteIssuable').mockResolvedValue({
data: {
web_url: '/test',
},
});
return wrapper.vm.deleteIssuable().then(() => {
expect(spy).toHaveBeenCalledWith();
});
});
it('closes form on error', () => {
jest.spyOn(wrapper.vm.service, 'deleteIssuable').mockRejectedValue();
return wrapper.vm.deleteIssuable().then(() => {
expect(eventHub.$emit).not.toHaveBeenCalledWith('close.form');
expect(document.querySelector('.flash-container .flash-text').innerText.trim()).toBe(
'Error deleting issue',
);
});
});
});
describe('updateAndShowForm', () => {
it('shows locked warning if form is open & data is different', () => {
return wrapper.vm
......
import { GlModal } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import DeleteIssueModal from '~/issues/show/components/delete_issue_modal.vue';
jest.mock('~/lib/utils/csrf', () => ({ token: 'mock-csrf-token' }));
describe('DeleteIssueModal component', () => {
let wrapper;
const defaultProps = {
issuePath: 'gitlab-org/gitlab-test/-/issues/1',
issueType: 'issue',
modalId: 'modal-id',
title: 'Delete issue',
};
const findForm = () => wrapper.find('form');
const findModal = () => wrapper.findComponent(GlModal);
const mountComponent = (props = {}) =>
shallowMount(DeleteIssueModal, { propsData: { ...defaultProps, ...props } });
afterEach(() => {
wrapper.destroy();
});
describe('modal', () => {
it('renders', () => {
wrapper = mountComponent();
expect(findModal().props()).toMatchObject({
actionCancel: DeleteIssueModal.actionCancel,
actionPrimary: {
attributes: { variant: 'danger' },
text: defaultProps.title,
},
modalId: defaultProps.modalId,
size: 'sm',
title: defaultProps.title,
});
});
describe('when "primary" event is emitted', () => {
let formSubmitSpy;
beforeEach(() => {
wrapper = mountComponent();
formSubmitSpy = jest.spyOn(wrapper.vm.$refs.form, 'submit');
findModal().vm.$emit('primary');
});
it('"delete" event is emitted by DeleteIssueModal', () => {
expect(wrapper.emitted('delete')).toEqual([[]]);
});
it('submits the form', () => {
expect(formSubmitSpy).toHaveBeenCalled();
});
});
});
describe('form', () => {
beforeEach(() => {
wrapper = mountComponent();
});
it('renders with action and method', () => {
expect(findForm().attributes()).toEqual({
action: defaultProps.issuePath,
method: 'post',
});
});
it('contains form data', () => {
const formData = wrapper.findAll('input').wrappers.reduce(
(acc, input) => ({
...acc,
[input.element.name]: input.element.value,
}),
{},
);
expect(formData).toEqual({
_method: 'delete',
authenticity_token: 'mock-csrf-token',
destroy_confirm: 'true',
});
});
});
describe('body text', () => {
describe('when issue type is not epic', () => {
it('renders', () => {
wrapper = mountComponent();
expect(findForm().text()).toBe('Issue will be removed! Are you sure?');
});
});
describe('when issue type is epic', () => {
it('renders', () => {
wrapper = mountComponent({ issueType: 'epic' });
expect(findForm().text()).toBe('Delete this epic and all descendants?');
});
});
});
});
import { GlButton, GlModal } from '@gitlab/ui';
import { createLocalVue } from '@vue/test-utils';
import { GlButton } from '@gitlab/ui';
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import { mockTracking } from 'helpers/tracking_helper';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
import IssuableEditActions from '~/issues/show/components/edit_actions.vue';
import DeleteIssueModal from '~/issues/show/components/delete_issue_modal.vue';
import eventHub from '~/issues/show/event_hub';
import {
getIssueStateQueryResponse,
updateIssueStateQueryResponse,
} from '../mock_data/apollo_mock';
const localVue = createLocalVue();
localVue.use(VueApollo);
describe('Edit Actions component', () => {
let wrapper;
let fakeApollo;
let mockIssueStateData;
Vue.use(VueApollo);
const mockResolvers = {
Query: {
issueState() {
......@@ -43,6 +43,7 @@ describe('Edit Actions component', () => {
title: 'GitLab Issue',
},
canDestroy: true,
endpoint: 'gitlab-org/gitlab-test/-/issues/1',
issuableType: 'issue',
...props,
},
......@@ -56,11 +57,7 @@ describe('Edit Actions component', () => {
});
};
async function deleteIssuable(localWrapper) {
localWrapper.findComponent(GlModal).vm.$emit('primary');
}
const findModal = () => wrapper.findComponent(GlModal);
const findModal = () => wrapper.findComponent(DeleteIssueModal);
const findEditButtons = () => wrapper.findAllComponents(GlButton);
const findDeleteButton = () => wrapper.findByTestId('issuable-delete-button');
const findSaveButton = () => wrapper.findByTestId('issuable-save-button');
......@@ -123,9 +120,30 @@ describe('Edit Actions component', () => {
});
});
describe('renders create modal with the correct information', () => {
it('renders correct modal id', () => {
expect(findModal().attributes('modalid')).toBe(modalId);
describe('delete issue button', () => {
let trackingSpy;
beforeEach(() => {
trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
});
it('tracks clicking on button', () => {
findDeleteButton().vm.$emit('click');
expect(trackingSpy).toHaveBeenCalledWith(undefined, 'click_button', {
label: 'delete_issue',
});
});
});
describe('delete issue modal', () => {
it('renders', () => {
expect(findModal().props()).toEqual({
issuePath: 'gitlab-org/gitlab-test/-/issues/1',
issueType: 'Issue',
modalId,
title: 'Delete issue',
});
});
});
......@@ -141,8 +159,8 @@ describe('Edit Actions component', () => {
it('sends the `delete.issuable` event when clicking the delete confirm button', async () => {
expect(eventHub.$emit).toHaveBeenCalledTimes(0);
await deleteIssuable(wrapper);
expect(eventHub.$emit).toHaveBeenCalledWith('delete.issuable', { destroy_confirm: true });
findModal().vm.$emit('delete');
expect(eventHub.$emit).toHaveBeenCalledWith('delete.issuable');
expect(eventHub.$emit).toHaveBeenCalledTimes(1);
});
});
......
......@@ -13,6 +13,7 @@ describe('Inline edit form component', () => {
let wrapper;
const defaultProps = {
canDestroy: true,
endpoint: 'gitlab-org/gitlab-test/-/issues/1',
formState: {
title: 'b',
description: 'a',
......
import { GlButton, GlDropdown, GlDropdownItem, GlLink, GlModal } from '@gitlab/ui';
import { createLocalVue, shallowMount } from '@vue/test-utils';
import Vue from 'vue';
import { shallowMount } from '@vue/test-utils';
import Vuex from 'vuex';
import { mockTracking } from 'helpers/tracking_helper';
import createFlash, { FLASH_TYPES } from '~/flash';
import { IssuableType } from '~/vue_shared/issuable/show/constants';
import DeleteIssueModal from '~/issues/show/components/delete_issue_modal.vue';
import HeaderActions from '~/issues/show/components/header_actions.vue';
import { IssuableStatus } from '~/issues/constants';
import { IssueStateEvent } from '~/issues/show/constants';
......@@ -19,18 +22,20 @@ describe('HeaderActions component', () => {
let wrapper;
let visitUrlSpy;
const localVue = createLocalVue();
localVue.use(Vuex);
Vue.use(Vuex);
const store = createStore();
const defaultProps = {
canCreateIssue: true,
canDestroyIssue: true,
canPromoteToEpic: true,
canReopenIssue: true,
canReportSpam: true,
canUpdateIssue: true,
iid: '32',
isIssueAuthor: true,
issuePath: 'gitlab-org/gitlab-test/-/issues/1',
issueType: IssuableType.Issue,
newIssuePath: 'gitlab-org/gitlab-test/-/issues/new',
projectPath: 'gitlab-org/gitlab-test',
......@@ -61,17 +66,12 @@ describe('HeaderActions component', () => {
},
};
const findToggleIssueStateButton = () => wrapper.find(GlButton);
const findDropdownAt = (index) => wrapper.findAll(GlDropdown).at(index);
const findMobileDropdownItems = () => findDropdownAt(0).findAll(GlDropdownItem);
const findDesktopDropdownItems = () => findDropdownAt(1).findAll(GlDropdownItem);
const findModal = () => wrapper.find(GlModal);
const findModalLinkAt = (index) => findModal().findAll(GlLink).at(index);
const findToggleIssueStateButton = () => wrapper.findComponent(GlButton);
const findDropdownAt = (index) => wrapper.findAllComponents(GlDropdown).at(index);
const findMobileDropdownItems = () => findDropdownAt(0).findAllComponents(GlDropdownItem);
const findDesktopDropdownItems = () => findDropdownAt(1).findAllComponents(GlDropdownItem);
const findModal = () => wrapper.findComponent(GlModal);
const findModalLinkAt = (index) => findModal().findAllComponents(GlLink).at(index);
const mountComponent = ({
props = {},
......@@ -87,7 +87,6 @@ describe('HeaderActions component', () => {
});
return shallowMount(HeaderActions, {
localVue,
store,
provide: {
...defaultProps,
......@@ -168,17 +167,19 @@ describe('HeaderActions component', () => {
${'desktop dropdown'} | ${false} | ${findDesktopDropdownItems}
`('$description', ({ isCloseIssueItemVisible, findDropdownItems }) => {
describe.each`
description | itemText | isItemVisible | canUpdateIssue | canCreateIssue | isIssueAuthor | canReportSpam | canPromoteToEpic
${`when user can update ${issueType}`} | ${`Close ${issueType}`} | ${isCloseIssueItemVisible} | ${true} | ${true} | ${true} | ${true} | ${true}
${`when user cannot update ${issueType}`} | ${`Close ${issueType}`} | ${false} | ${false} | ${true} | ${true} | ${true} | ${true}
${`when user can create ${issueType}`} | ${`New ${issueType}`} | ${true} | ${true} | ${true} | ${true} | ${true} | ${true}
${`when user cannot create ${issueType}`} | ${`New ${issueType}`} | ${false} | ${true} | ${false} | ${true} | ${true} | ${true}
${'when user can promote to epic'} | ${'Promote to epic'} | ${true} | ${true} | ${true} | ${true} | ${true} | ${true}
${'when user cannot promote to epic'} | ${'Promote to epic'} | ${false} | ${true} | ${true} | ${true} | ${true} | ${false}
${'when user can report abuse'} | ${'Report abuse'} | ${true} | ${true} | ${true} | ${false} | ${true} | ${true}
${'when user cannot report abuse'} | ${'Report abuse'} | ${false} | ${true} | ${true} | ${true} | ${true} | ${true}
${'when user can submit as spam'} | ${'Submit as spam'} | ${true} | ${true} | ${true} | ${true} | ${true} | ${true}
${'when user cannot submit as spam'} | ${'Submit as spam'} | ${false} | ${true} | ${true} | ${true} | ${false} | ${true}
description | itemText | isItemVisible | canUpdateIssue | canCreateIssue | isIssueAuthor | canReportSpam | canPromoteToEpic | canDestroyIssue
${`when user can update ${issueType}`} | ${`Close ${issueType}`} | ${isCloseIssueItemVisible} | ${true} | ${true} | ${true} | ${true} | ${true} | ${true}
${`when user cannot update ${issueType}`} | ${`Close ${issueType}`} | ${false} | ${false} | ${true} | ${true} | ${true} | ${true} | ${true}
${`when user can create ${issueType}`} | ${`New ${issueType}`} | ${true} | ${true} | ${true} | ${true} | ${true} | ${true} | ${true}
${`when user cannot create ${issueType}`} | ${`New ${issueType}`} | ${false} | ${true} | ${false} | ${true} | ${true} | ${true} | ${true}
${'when user can promote to epic'} | ${'Promote to epic'} | ${true} | ${true} | ${true} | ${true} | ${true} | ${true} | ${true}
${'when user cannot promote to epic'} | ${'Promote to epic'} | ${false} | ${true} | ${true} | ${true} | ${true} | ${false} | ${true}
${'when user can report abuse'} | ${'Report abuse'} | ${true} | ${true} | ${true} | ${false} | ${true} | ${true} | ${true}
${'when user cannot report abuse'} | ${'Report abuse'} | ${false} | ${true} | ${true} | ${true} | ${true} | ${true} | ${true}
${'when user can submit as spam'} | ${'Submit as spam'} | ${true} | ${true} | ${true} | ${true} | ${true} | ${true} | ${true}
${'when user cannot submit as spam'} | ${'Submit as spam'} | ${false} | ${true} | ${true} | ${true} | ${false} | ${true} | ${true}
${`when user can delete ${issueType}`} | ${`Delete ${issueType}`} | ${true} | ${true} | ${true} | ${true} | ${true} | ${true} | ${true}
${`when user cannot delete ${issueType}`} | ${`Delete ${issueType}`} | ${false} | ${true} | ${true} | ${true} | ${true} | ${true} | ${false}
`(
'$description',
({
......@@ -189,6 +190,7 @@ describe('HeaderActions component', () => {
isIssueAuthor,
canReportSpam,
canPromoteToEpic,
canDestroyIssue,
}) => {
beforeEach(() => {
wrapper = mountComponent({
......@@ -199,6 +201,7 @@ describe('HeaderActions component', () => {
issueType,
canReportSpam,
canPromoteToEpic,
canDestroyIssue,
},
});
});
......@@ -215,6 +218,23 @@ describe('HeaderActions component', () => {
});
});
describe('delete issue button', () => {
let trackingSpy;
beforeEach(() => {
wrapper = mountComponent();
trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
});
it('tracks clicking on button', () => {
findDesktopDropdownItems().at(3).vm.$emit('click');
expect(trackingSpy).toHaveBeenCalledWith(undefined, 'click_dropdown', {
label: 'delete_issue',
});
});
});
describe('when "Promote to epic" button is clicked', () => {
describe('when response is successful', () => {
beforeEach(() => {
......@@ -268,7 +288,7 @@ describe('HeaderActions component', () => {
it('shows an error message', () => {
expect(createFlash).toHaveBeenCalledWith({
message: promoteToEpicMutationErrorResponse.data.promoteToEpic.errors.join('; '),
message: HeaderActions.i18n.promoteErrorMessage,
});
});
});
......@@ -294,7 +314,7 @@ describe('HeaderActions component', () => {
});
});
describe('modal', () => {
describe('blocked by issues modal', () => {
const blockedByIssues = [
{ iid: 13, web_url: 'gitlab-org/gitlab-test/-/issues/13' },
{ iid: 79, web_url: 'gitlab-org/gitlab-test/-/issues/79' },
......@@ -346,4 +366,17 @@ describe('HeaderActions component', () => {
});
});
});
describe('delete issue modal', () => {
it('renders', () => {
wrapper = mountComponent();
expect(wrapper.findComponent(DeleteIssueModal).props()).toEqual({
issuePath: defaultProps.issuePath,
issueType: defaultProps.issueType,
modalId: HeaderActions.deleteModalId,
title: 'Delete issue',
});
});
});
});
......@@ -278,11 +278,13 @@ RSpec.describe IssuesHelper do
it 'returns expected result' do
expected = {
can_create_issue: 'true',
can_destroy_issue: 'true',
can_reopen_issue: 'true',
can_report_spam: 'false',
can_update_issue: 'true',
iid: issue.iid,
is_issue_author: 'false',
issue_path: issue_path(issue),
issue_type: 'issue',
new_issue_path: new_project_issue_path(project, { issue: { description: "Related to \##{issue.iid}.\n\n" } }),
project_path: project.full_path,
......
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