Commit b7c0fd4d authored by Coung Ngo's avatar Coung Ngo

Update incidents to use issue header ellipsis dropdown

Since incidents are an issue type and should mirror issue
actions such as New incident and Report abuse, incidents
should be updated to use the new issue header ellipsis
dropdown to keep it in sync with issue functionality
parent 8188bc2d
......@@ -2,8 +2,10 @@
import { GlButton, GlDropdown, GlDropdownItem, GlIcon, GlLink, GlModal } from '@gitlab/ui';
import { mapGetters } from 'vuex';
import createFlash from '~/flash';
import { IssuableType } from '~/issuable_show/constants';
import { IssuableStatus, IssueStateEvent } from '~/issue_show/constants';
import { __ } from '~/locale';
import { capitalizeFirstCharacter } from '~/lib/utils/text_utility';
import { __, sprintf } from '~/locale';
import updateIssueMutation from '../queries/update_issue.mutation.graphql';
export default {
......@@ -22,18 +24,41 @@ export default {
text: __('Yes, close issue'),
attributes: [{ variant: 'warning' }],
},
inject: [
'canCreateIssue',
'canReopenIssue',
'canReportSpam',
'canUpdateIssue',
'iid',
'isIssueAuthor',
'newIssuePath',
'projectPath',
'reportAbusePath',
'submitAsSpamPath',
],
inject: {
canCreateIssue: {
default: false,
},
canReopenIssue: {
default: false,
},
canReportSpam: {
default: false,
},
canUpdateIssue: {
default: false,
},
iid: {
default: '',
},
isIssueAuthor: {
default: false,
},
issueType: {
default: IssuableType.Issue,
},
newIssuePath: {
default: '',
},
projectPath: {
default: '',
},
reportAbusePath: {
default: '',
},
submitAsSpamPath: {
default: '',
},
},
data() {
return {
isUpdatingState: false,
......@@ -45,12 +70,22 @@ export default {
return this.getNoteableData.state === IssuableStatus.Closed;
},
buttonText() {
return this.isClosed ? __('Reopen issue') : __('Close issue');
return this.isClosed
? sprintf(__('Reopen %{issueType}'), { issueType: this.issueType })
: sprintf(__('Close %{issueType}'), { issueType: this.issueType });
},
buttonVariant() {
return this.isClosed ? 'default' : 'warning';
},
showToggleIssueButton() {
dropdownText() {
return sprintf(__('%{issueType} actions'), {
issueType: capitalizeFirstCharacter(this.issueType),
});
},
newIssueTypeText() {
return sprintf(__('New %{issueType}'), { issueType: this.issueType });
},
showToggleIssueStateButton() {
const canClose = !this.isClosed && this.canUpdateIssue;
const canReopen = this.isClosed && this.canReopenIssue;
return canClose || canReopen;
......@@ -106,16 +141,16 @@ export default {
<template>
<div class="detail-page-header-actions">
<gl-dropdown class="gl-display-block gl-display-sm-none!" block :text="__('Issue actions')">
<gl-dropdown class="gl-display-block gl-display-sm-none!" block :text="dropdownText">
<gl-dropdown-item
v-if="showToggleIssueButton"
v-if="showToggleIssueStateButton"
:disabled="isUpdatingState"
@click="toggleIssueState"
>
{{ buttonText }}
</gl-dropdown-item>
<gl-dropdown-item v-if="canCreateIssue" :href="newIssuePath">
{{ __('New issue') }}
{{ newIssueTypeText }}
</gl-dropdown-item>
<gl-dropdown-item v-if="!isIssueAuthor" :href="reportAbusePath">
{{ __('Report abuse') }}
......@@ -131,7 +166,7 @@ export default {
</gl-dropdown>
<gl-button
v-if="showToggleIssueButton"
v-if="showToggleIssueStateButton"
class="gl-display-none gl-display-sm-inline-flex!"
category="secondary"
:loading="isUpdatingState"
......@@ -149,11 +184,11 @@ export default {
>
<template #button-content>
<gl-icon name="ellipsis_v" aria-hidden="true" />
<span class="gl-sr-only">{{ __('Actions') }}</span>
<span class="gl-sr-only">{{ dropdownText }}</span>
</template>
<gl-dropdown-item v-if="canCreateIssue" :href="newIssuePath">
{{ __('New issue') }}
{{ newIssueTypeText }}
</gl-dropdown-item>
<gl-dropdown-item v-if="!isIssueAuthor" :href="reportAbusePath">
{{ __('Report abuse') }}
......
......@@ -48,6 +48,7 @@ export function initIssueHeaderActions(store) {
canUpdateIssue: parseBoolean(el.dataset.canUpdateIssue),
iid: el.dataset.iid,
isIssueAuthor: parseBoolean(el.dataset.isIssueAuthor),
issueType: el.dataset.issueType,
newIssuePath: el.dataset.newIssuePath,
projectPath: el.dataset.projectPath,
reportAbusePath: el.dataset.reportAbusePath,
......
......@@ -153,18 +153,21 @@ module IssuesHelper
}
end
def issue_header_actions_data(project, issue, current_user)
def issue_header_actions_data(project, issuable, current_user)
new_issuable_params = ({ issuable_template: 'incident', issue: { issue_type: 'incident' } } if issuable.incident?)
{
can_create_issue: show_new_issue_link?(project).to_s,
can_reopen_issue: can?(current_user, :reopen_issue, issue).to_s,
can_report_spam: issue.submittable_as_spam_by?(current_user).to_s,
can_update_issue: can?(current_user, :update_issue, issue).to_s,
iid: issue.iid,
is_issue_author: (issue.author == current_user).to_s,
new_issue_path: new_project_issue_path(project),
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_type: issuable_display_type(issuable),
new_issue_path: new_project_issue_path(project, new_issuable_params),
project_path: project.full_path,
report_abuse_path: new_abuse_report_path(user_id: issue.author.id, ref_url: issue_url(issue)),
submit_as_spam_path: mark_as_spam_project_issue_path(project, issue)
report_abuse_path: new_abuse_report_path(user_id: issuable.author.id, ref_url: issue_url(issuable)),
submit_as_spam_path: mark_as_spam_project_issue_path(project, issuable)
}
end
end
......
......@@ -23,8 +23,8 @@
%a.btn.gl-button.btn-default.float-right.gl-display-block.d-sm-none.gutter-toggle.issuable-gutter-toggle.js-sidebar-toggle{ href: "#" }
= sprite_icon('chevron-double-lg-left')
- if Feature.enabled?(:vue_issue_header, @project) && display_issuable_type == 'issue'
.js-issue-header-actions{ data: issue_header_actions_data(@project, @issue, current_user) }
- if Feature.enabled?(:vue_issue_header, @project)
.js-issue-header-actions{ data: issue_header_actions_data(@project, issuable, current_user) }
- else
.detail-page-header-actions.js-issuable-actions.js-issuable-buttons{ data: { "action": "close-reopen" } }
.clearfix.issue-btn-group.dropdown
......
......@@ -526,6 +526,9 @@ msgstr ""
msgid "%{issuableType} will be removed! Are you sure?"
msgstr ""
msgid "%{issueType} actions"
msgstr ""
msgid "%{issuesCount} issues with a limit of %{maxIssueCount}"
msgstr ""
......@@ -5538,13 +5541,13 @@ msgstr ""
msgid "Close %{display_issuable_type}"
msgstr ""
msgid "Close %{tabname}"
msgid "Close %{issueType}"
msgstr ""
msgid "Close epic"
msgid "Close %{tabname}"
msgstr ""
msgid "Close issue"
msgid "Close epic"
msgstr ""
msgid "Close milestone"
......@@ -14855,9 +14858,6 @@ msgstr ""
msgid "Issue Boards"
msgstr ""
msgid "Issue actions"
msgstr ""
msgid "Issue already promoted to epic."
msgstr ""
......@@ -17963,6 +17963,9 @@ msgstr ""
msgid "New %{display_issuable_type}"
msgstr ""
msgid "New %{issueType}"
msgstr ""
msgid "New Application"
msgstr ""
......@@ -22471,10 +22474,10 @@ msgstr ""
msgid "Reopen %{display_issuable_type}"
msgstr ""
msgid "Reopen epic"
msgid "Reopen %{issueType}"
msgstr ""
msgid "Reopen issue"
msgid "Reopen epic"
msgstr ""
msgid "Reopen milestone"
......
import { GlButton, GlDropdown, GlDropdownItem, GlLink, GlModal } from '@gitlab/ui';
import { createLocalVue, shallowMount } from '@vue/test-utils';
import Vuex from 'vuex';
import { IssuableType } from '~/issuable_show/constants';
import HeaderActions from '~/issue_show/components/header_actions.vue';
import { IssuableStatus, IssueStateEvent } from '~/issue_show/constants';
import createStore from '~/notes/stores';
......@@ -20,6 +21,7 @@ describe('HeaderActions component', () => {
canUpdateIssue: true,
iid: '32',
isIssueAuthor: true,
issueType: IssuableType.Issue,
newIssuePath: 'gitlab-org/gitlab-test/-/issues/new',
projectPath: 'gitlab-org/gitlab-test',
reportAbusePath:
......@@ -74,93 +76,100 @@ describe('HeaderActions component', () => {
wrapper.destroy();
});
describe('close/reopen button', () => {
describe.each`
description | issueState | buttonText | newIssueState
${'when the issue is open'} | ${IssuableStatus.Open} | ${'Close issue'} | ${IssueStateEvent.Close}
${'when the issue is closed'} | ${IssuableStatus.Closed} | ${'Reopen issue'} | ${IssueStateEvent.Reopen}
`('$description', ({ issueState, buttonText, newIssueState }) => {
beforeEach(() => {
dispatchEventSpy = jest.spyOn(document, 'dispatchEvent');
wrapper = mountComponent({ issueState });
});
describe.each`
issueType
${IssuableType.Issue}
${IssuableType.Incident}
`('when issue type is $issueType', ({ issueType }) => {
describe('close/reopen button', () => {
describe.each`
description | issueState | buttonText | newIssueState
${`when the ${issueType} is open`} | ${IssuableStatus.Open} | ${`Close ${issueType}`} | ${IssueStateEvent.Close}
${`when the ${issueType} is closed`} | ${IssuableStatus.Closed} | ${`Reopen ${issueType}`} | ${IssueStateEvent.Reopen}
`('$description', ({ issueState, buttonText, newIssueState }) => {
beforeEach(() => {
dispatchEventSpy = jest.spyOn(document, 'dispatchEvent');
it(`has text "${buttonText}"`, () => {
expect(findToggleIssueStateButton().text()).toBe(buttonText);
});
wrapper = mountComponent({ props: { issueType }, issueState });
});
it('calls apollo mutation', () => {
findToggleIssueStateButton().vm.$emit('click');
it(`has text "${buttonText}"`, () => {
expect(findToggleIssueStateButton().text()).toBe(buttonText);
});
expect(mutate).toHaveBeenCalledWith(
expect.objectContaining({
variables: {
input: {
iid: defaultProps.iid.toString(),
projectPath: defaultProps.projectPath,
stateEvent: newIssueState,
it('calls apollo mutation', () => {
findToggleIssueStateButton().vm.$emit('click');
expect(mutate).toHaveBeenCalledWith(
expect.objectContaining({
variables: {
input: {
iid: defaultProps.iid.toString(),
projectPath: defaultProps.projectPath,
stateEvent: newIssueState,
},
},
},
}),
);
});
}),
);
});
it('dispatches a custom event to update the issue page', async () => {
findToggleIssueStateButton().vm.$emit('click');
it('dispatches a custom event to update the issue page', async () => {
findToggleIssueStateButton().vm.$emit('click');
await wrapper.vm.$nextTick();
await wrapper.vm.$nextTick();
expect(dispatchEventSpy).toHaveBeenCalledTimes(1);
expect(dispatchEventSpy).toHaveBeenCalledTimes(1);
});
});
});
});
describe.each`
description | isCloseIssueItemVisible | findDropdownItems
${'mobile dropdown'} | ${true} | ${findMobileDropdownItems}
${'desktop dropdown'} | ${false} | ${findDesktopDropdownItems}
`('$description', ({ isCloseIssueItemVisible, findDropdownItems }) => {
describe.each`
description | itemText | isItemVisible | canUpdateIssue | canCreateIssue | isIssueAuthor | canReportSpam
${'when user can update issue'} | ${'Close issue'} | ${isCloseIssueItemVisible} | ${true} | ${true} | ${true} | ${true}
${'when user cannot update issue'} | ${'Close issue'} | ${false} | ${false} | ${true} | ${true} | ${true}
${'when user can create issue'} | ${'New issue'} | ${true} | ${true} | ${true} | ${true} | ${true}
${'when user cannot create issue'} | ${'New issue'} | ${false} | ${true} | ${false} | ${true} | ${true}
${'when user can report abuse'} | ${'Report abuse'} | ${true} | ${true} | ${true} | ${false} | ${true}
${'when user cannot report abuse'} | ${'Report abuse'} | ${false} | ${true} | ${true} | ${true} | ${true}
${'when user can submit as spam'} | ${'Submit as spam'} | ${true} | ${true} | ${true} | ${true} | ${true}
${'when user cannot submit as spam'} | ${'Submit as spam'} | ${false} | ${true} | ${true} | ${true} | ${false}
`(
'$description',
({
itemText,
isItemVisible,
canUpdateIssue,
canCreateIssue,
isIssueAuthor,
canReportSpam,
}) => {
beforeEach(() => {
wrapper = mountComponent({
props: {
canUpdateIssue,
canCreateIssue,
isIssueAuthor,
canReportSpam,
},
description | isCloseIssueItemVisible | findDropdownItems
${'mobile dropdown'} | ${true} | ${findMobileDropdownItems}
${'desktop dropdown'} | ${false} | ${findDesktopDropdownItems}
`('$description', ({ isCloseIssueItemVisible, findDropdownItems }) => {
describe.each`
description | itemText | isItemVisible | canUpdateIssue | canCreateIssue | isIssueAuthor | canReportSpam
${`when user can update ${issueType}`} | ${`Close ${issueType}`} | ${isCloseIssueItemVisible} | ${true} | ${true} | ${true} | ${true}
${`when user cannot update ${issueType}`} | ${`Close ${issueType}`} | ${false} | ${false} | ${true} | ${true} | ${true}
${`when user can create ${issueType}`} | ${`New ${issueType}`} | ${true} | ${true} | ${true} | ${true} | ${true}
${`when user cannot create ${issueType}`} | ${`New ${issueType}`} | ${false} | ${true} | ${false} | ${true} | ${true}
${'when user can report abuse'} | ${'Report abuse'} | ${true} | ${true} | ${true} | ${false} | ${true}
${'when user cannot report abuse'} | ${'Report abuse'} | ${false} | ${true} | ${true} | ${true} | ${true}
${'when user can submit as spam'} | ${'Submit as spam'} | ${true} | ${true} | ${true} | ${true} | ${true}
${'when user cannot submit as spam'} | ${'Submit as spam'} | ${false} | ${true} | ${true} | ${true} | ${false}
`(
'$description',
({
itemText,
isItemVisible,
canUpdateIssue,
canCreateIssue,
isIssueAuthor,
canReportSpam,
}) => {
beforeEach(() => {
wrapper = mountComponent({
props: {
canUpdateIssue,
canCreateIssue,
isIssueAuthor,
issueType,
canReportSpam,
},
});
});
});
it(`${isItemVisible ? 'shows' : 'hides'} "${itemText}" item`, () => {
expect(
findDropdownItems()
.filter(item => item.text() === itemText)
.exists(),
).toBe(isItemVisible);
});
},
);
it(`${isItemVisible ? 'shows' : 'hides'} "${itemText}" item`, () => {
expect(
findDropdownItems()
.filter(item => item.text() === itemText)
.exists(),
).toBe(isItemVisible);
});
},
);
});
});
describe('modal', () => {
......
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