Commit c7da246b authored by Kushal Pandya's avatar Kushal Pandya

Merge branch '233974-update-incident-to-use-issue-header-ellipsis-dropdown' into 'master'

Update incidents to use issue header ellipsis dropdown

See merge request gitlab-org/gitlab!46977
parents 2e3ed169 b7c0fd4d
......@@ -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') }}
......
......@@ -50,6 +50,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 ""
......@@ -5585,13 +5588,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"
......@@ -14947,9 +14950,6 @@ msgstr ""
msgid "Issue Boards"
msgstr ""
msgid "Issue actions"
msgstr ""
msgid "Issue already promoted to epic."
msgstr ""
......@@ -18055,6 +18055,9 @@ msgstr ""
msgid "New %{display_issuable_type}"
msgstr ""
msgid "New %{issueType}"
msgstr ""
msgid "New Application"
msgstr ""
......@@ -22563,10 +22566,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