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