Commit 4d5e175e authored by Coung Ngo's avatar Coung Ngo Committed by Alexandru Croitor

Add "Promote to epic" issue actions dropdown item

Add new "Promote to epic" button for issues so that
users can more easily promote an issue to an epic.
Currently, a user can only promote an issue through
the /promote quick action.
parent fea3a72b
<script> <script>
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, { FLASH_TYPES } from '~/flash';
import { IssuableType } from '~/issuable_show/constants'; import { IssuableType } from '~/issuable_show/constants';
import { IssuableStatus, IssueStateEvent } from '~/issue_show/constants'; import { IssuableStatus, IssueStateEvent } from '~/issue_show/constants';
import { capitalizeFirstCharacter } from '~/lib/utils/text_utility'; import { capitalizeFirstCharacter } from '~/lib/utils/text_utility';
import { visitUrl } from '~/lib/utils/url_utility';
import { __, sprintf } from '~/locale'; import { __, sprintf } from '~/locale';
import promoteToEpicMutation from '../queries/promote_to_epic.mutation.graphql';
import updateIssueMutation from '../queries/update_issue.mutation.graphql'; import updateIssueMutation from '../queries/update_issue.mutation.graphql';
export default { export default {
...@@ -24,10 +26,21 @@ export default { ...@@ -24,10 +26,21 @@ export default {
text: __('Yes, close issue'), text: __('Yes, close issue'),
attributes: [{ variant: 'warning' }], attributes: [{ variant: 'warning' }],
}, },
i18n: {
promoteErrorMessage: __(
'Something went wrong while promoting the issue to an epic. Please try again.',
),
promoteSuccessMessage: __(
'The issue was successfully promoted to an epic. Redirecting to epic...',
),
},
inject: { inject: {
canCreateIssue: { canCreateIssue: {
default: false, default: false,
}, },
canPromoteToEpic: {
default: false,
},
canReopenIssue: { canReopenIssue: {
default: false, default: false,
}, },
...@@ -135,6 +148,37 @@ export default { ...@@ -135,6 +148,37 @@ export default {
this.isUpdatingState = false; this.isUpdatingState = false;
}); });
}, },
promoteToEpic() {
this.isUpdatingState = true;
this.$apollo
.mutate({
mutation: promoteToEpicMutation,
variables: {
input: {
iid: this.iid,
projectPath: this.projectPath,
},
},
})
.then(({ data }) => {
if (data.promoteToEpic.errors.length) {
createFlash({ message: data.promoteToEpic.errors.join('; ') });
return;
}
createFlash({
message: this.$options.i18n.promoteSuccessMessage,
type: FLASH_TYPES.SUCCESS,
});
visitUrl(data.promoteToEpic.epic.webPath);
})
.catch(() => createFlash({ message: this.$options.i18n.promoteErrorMessage }))
.finally(() => {
this.isUpdatingState = false;
});
},
}, },
}; };
</script> </script>
...@@ -152,6 +196,9 @@ export default { ...@@ -152,6 +196,9 @@ export default {
<gl-dropdown-item v-if="canCreateIssue" :href="newIssuePath"> <gl-dropdown-item v-if="canCreateIssue" :href="newIssuePath">
{{ newIssueTypeText }} {{ newIssueTypeText }}
</gl-dropdown-item> </gl-dropdown-item>
<gl-dropdown-item v-if="canPromoteToEpic" :disabled="isUpdatingState" @click="promoteToEpic">
{{ __('Promote to epic') }}
</gl-dropdown-item>
<gl-dropdown-item v-if="!isIssueAuthor" :href="reportAbusePath"> <gl-dropdown-item v-if="!isIssueAuthor" :href="reportAbusePath">
{{ __('Report abuse') }} {{ __('Report abuse') }}
</gl-dropdown-item> </gl-dropdown-item>
...@@ -190,6 +237,14 @@ export default { ...@@ -190,6 +237,14 @@ export default {
<gl-dropdown-item v-if="canCreateIssue" :href="newIssuePath"> <gl-dropdown-item v-if="canCreateIssue" :href="newIssuePath">
{{ newIssueTypeText }} {{ newIssueTypeText }}
</gl-dropdown-item> </gl-dropdown-item>
<gl-dropdown-item
v-if="canPromoteToEpic"
:disabled="isUpdatingState"
data-testid="promote-button"
@click="promoteToEpic"
>
{{ __('Promote to epic') }}
</gl-dropdown-item>
<gl-dropdown-item v-if="!isIssueAuthor" :href="reportAbusePath"> <gl-dropdown-item v-if="!isIssueAuthor" :href="reportAbusePath">
{{ __('Report abuse') }} {{ __('Report abuse') }}
</gl-dropdown-item> </gl-dropdown-item>
......
...@@ -45,6 +45,7 @@ export function initIssueHeaderActions(store) { ...@@ -45,6 +45,7 @@ export function initIssueHeaderActions(store) {
store, store,
provide: { provide: {
canCreateIssue: parseBoolean(el.dataset.canCreateIssue), canCreateIssue: parseBoolean(el.dataset.canCreateIssue),
canPromoteToEpic: parseBoolean(el.dataset.canPromoteToEpic),
canReopenIssue: parseBoolean(el.dataset.canReopenIssue), canReopenIssue: parseBoolean(el.dataset.canReopenIssue),
canReportSpam: parseBoolean(el.dataset.canReportSpam), canReportSpam: parseBoolean(el.dataset.canReportSpam),
canUpdateIssue: parseBoolean(el.dataset.canUpdateIssue), canUpdateIssue: parseBoolean(el.dataset.canUpdateIssue),
......
mutation promoteToEpic($input: PromoteToEpicInput!) {
promoteToEpic(input: $input) {
epic {
webPath
}
errors
}
}
...@@ -95,12 +95,13 @@ While you can view and manage the full details of an issue on the [issue page](# ...@@ -95,12 +95,13 @@ While you can view and manage the full details of an issue on the [issue page](#
you can also work with multiple issues at a time using the [Issues List](#issues-list), you can also work with multiple issues at a time using the [Issues List](#issues-list),
[Issue Boards](#issue-boards), Issue references, and [Epics](#epics)**(PREMIUM)**. [Issue Boards](#issue-boards), Issue references, and [Epics](#epics)**(PREMIUM)**.
Key actions for Issues include: Key actions for issues include:
- [Creating issues](managing_issues.md#create-a-new-issue) - [Creating issues](managing_issues.md#create-a-new-issue)
- [Moving issues](managing_issues.md#moving-issues) - [Moving issues](managing_issues.md#moving-issues)
- [Closing issues](managing_issues.md#closing-issues) - [Closing issues](managing_issues.md#closing-issues)
- [Deleting issues](managing_issues.md#deleting-issues) - [Deleting issues](managing_issues.md#deleting-issues)
- [Promoting issues](managing_issues.md#promote-an-issue-to-an-epic) **(PREMIUM)**
### Issue page ### Issue page
......
...@@ -7,9 +7,15 @@ info: To determine the technical writer assigned to the Stage/Group associated w ...@@ -7,9 +7,15 @@ info: To determine the technical writer assigned to the Stage/Group associated w
# Managing issues # Managing issues
[GitLab Issues](index.md) are the fundamental medium for collaborating on ideas and [GitLab Issues](index.md) are the fundamental medium for collaborating on ideas and
planning work in GitLab. [Creating](#create-a-new-issue), [moving](#moving-issues), planning work in GitLab.
[closing](#closing-issues), and [deleting](#deleting-issues) are key actions that
you can do with issues. Key actions for issues include:
- [Creating issues](#create-a-new-issue)
- [Moving issues](#moving-issues)
- [Closing issues](#closing-issues)
- [Deleting issues](#deleting-issues)
- [Promoting issues](#promote-an-issue-to-an-epic) **(PREMIUM)**
## Create a new issue ## Create a new issue
...@@ -280,6 +286,23 @@ editing it and clicking on the delete button. ...@@ -280,6 +286,23 @@ editing it and clicking on the delete button.
![delete issue - button](img/delete_issue.png) ![delete issue - button](img/delete_issue.png)
## Promote an issue to an epic **(PREMIUM)**
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/3777) in [GitLab Ultimate](https://about.gitlab.com/pricing/) 11.6.
> - [Moved](https://gitlab.com/gitlab-org/gitlab/-/issues/37081) to [GitLab Premium](https://about.gitlab.com/pricing/) in 12.8.
> - Promoting issues to epics via the UI [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/233974) in [GitLab Premium](https://about.gitlab.com/pricing/) 13.6.
You can promote an issue to an epic in the immediate parent group.
To promote an issue to an epic:
1. In an issue, select the vertical ellipsis (**{ellipsis_v}**) button.
1. Select **Promote to epic**.
Alternatively, you can use the `/promote` [quick action](../quick_actions.md#quick-actions-for-issues-merge-requests-and-epics).
Read more about promoting an issue to an epic on the [Manage epics page](../../group/epics/manage_epics.md#promote-an-issue-to-an-epic).
## Add an issue to an iteration **(STARTER)** ## Add an issue to an iteration **(STARTER)**
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/216158) in [GitLab Starter](https://about.gitlab.com/pricing/) 13.2. > [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/216158) in [GitLab Starter](https://about.gitlab.com/pricing/) 13.2.
......
...@@ -31,15 +31,6 @@ module EE ...@@ -31,15 +31,6 @@ module EE
end end
end end
override :issue_closed_link
def issue_closed_link(issue, current_user, css_class: '')
if issue.promoted? && can?(current_user, :read_epic, issue.promoted_to_epic)
link_to(s_('IssuableStatus|promoted'), issue.promoted_to_epic, class: css_class)
else
super
end
end
def issue_in_subepic?(issue, epic_id) def issue_in_subepic?(issue, epic_id)
# This helper is used if a list of issues are filtered by epic id # This helper is used if a list of issues are filtered by epic id
return false if epic_id.blank? return false if epic_id.blank?
...@@ -55,9 +46,27 @@ module EE ...@@ -55,9 +46,27 @@ module EE
issue.incident? && issue.project.feature_available?(:incident_timeline_view) issue.incident? && issue.project.feature_available?(:incident_timeline_view)
end end
# OVERRIDES
override :scoped_labels_available? override :scoped_labels_available?
def scoped_labels_available?(parent) def scoped_labels_available?(parent)
parent.feature_available?(:scoped_labels) parent.feature_available?(:scoped_labels)
end end
override :issue_closed_link
def issue_closed_link(issue, current_user, css_class: '')
if issue.promoted? && can?(current_user, :read_epic, issue.promoted_to_epic)
link_to(s_('IssuableStatus|promoted'), issue.promoted_to_epic, class: css_class)
else
super
end
end
override :issue_header_actions_data
def issue_header_actions_data(project, issuable, current_user)
actions = super
actions[:can_promote_to_epic] = issuable.can_be_promoted_to_epic?(current_user).to_s
actions
end
end end
end end
...@@ -163,6 +163,16 @@ module EE ...@@ -163,6 +163,16 @@ module EE
user&.can?(:admin_epic, project.group) user&.can?(:admin_epic, project.group)
end end
def can_be_promoted_to_epic?(user, group = nil)
group ||= project.group
return false unless user
return false unless group
persisted? && supports_epic? && !promoted? &&
user.can?(:admin_issue, project) && user.can?(:create_epic, group)
end
# Issue position on boards list should be relative to all group projects # Issue position on boards list should be relative to all group projects
def parent_ids def parent_ids
return super unless has_group_boards? return super unless has_group_boards?
......
...@@ -15,6 +15,10 @@ module EE ...@@ -15,6 +15,10 @@ module EE
prevent :create_design prevent :create_design
prevent :create_note prevent :create_note
end end
rule { can_be_promoted_to_epic }.policy do
enable :promote_to_epic
end
end end
end end
end end
---
title: Promote an Issue to an Epic via the UI
merge_request: 47306
author:
type: added
...@@ -70,11 +70,7 @@ module EE ...@@ -70,11 +70,7 @@ module EE
icon 'confidential' icon 'confidential'
types Issue types Issue
condition do condition do
quick_action_target.persisted? && quick_action_target.can_be_promoted_to_epic?(current_user)
quick_action_target.supports_epic? &&
!quick_action_target.promoted? &&
current_user.can?(:admin_issue, project) &&
current_user.can?(:create_epic, project.group)
end end
command :promote do command :promote do
@updates[:promote_to_epic] = true @updates[:promote_to_epic] = true
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe 'Issue actions', :js do
let(:group) { create(:group) }
let(:project) { create(:project, group: group) }
let(:issue) { create(:issue, project: project) }
let(:user) { create(:user) }
before do
stub_licensed_features(epics: true)
sign_in(user)
end
describe 'promote issue to epic action' do
context 'when user is unauthorized' do
before do
group.add_guest(user)
visit project_issue_path(project, issue)
end
it 'does not show "Promote to epic" item in issue actions dropdown' do
page.within '.detail-page-header' do
# Click on ellipsis dropdown button
click_button 'Issue actions'
expect(page).not_to have_button('Promote to epic')
end
end
end
context 'when user is authorized' do
before do
group.add_owner(user)
visit project_issue_path(project, issue)
end
it 'clicking "Promote to epic" creates and redirects user to epic' do
page.within '.detail-page-header' do
# Click on ellipsis dropdown button
click_button 'Issue actions'
click_button 'Promote to epic'
end
wait_for_requests
expect(page).to have_current_path(group_epic_path(group, 1))
end
end
end
end
...@@ -880,6 +880,76 @@ RSpec.describe Issue do ...@@ -880,6 +880,76 @@ RSpec.describe Issue do
end end
end end
describe '#can_be_promoted_to_epic?' do
before do
stub_licensed_features(epics: true)
end
let_it_be(:user) { create(:user) }
let(:group) { nil }
subject { issue.can_be_promoted_to_epic?(user, group) }
context 'when project on the issue does not have a parent group' do
let(:project) { create(:project) }
let(:issue) { create(:issue, project: project) }
before do
project.add_developer(user)
end
it { is_expected.to be_falsey }
end
context 'when project on the issue is in a subgroup' do
let(:parent_group) { create(:group) }
let(:group) { create(:group, parent: parent_group) }
let(:project) { create(:project, group: group) }
let(:issue) { create(:issue, project: project) }
before do
group.add_developer(user)
project.add_developer(user)
end
it { is_expected.to be_truthy }
end
context 'when project has a parent group' do
let_it_be(:group) { create(:group) }
let_it_be(:project) { create(:project, group: group) }
let_it_be(:issue) { create(:issue, project: project) }
context 'when a user is not a project member' do
it { is_expected.to be_falsey }
end
context 'when a user is a project member' do
before do
project.add_developer(user)
end
it { is_expected.to be_falsey }
end
context 'when a user is a group member' do
before do
group.add_developer(user)
end
it { is_expected.to be_truthy }
context 'when issue is an incident' do
before do
issue.update!(issue_type: :incident)
end
it { is_expected.to be_falsey }
end
end
end
end
describe '#supports_iterations?' do describe '#supports_iterations?' do
let(:group) { build_stubbed(:group) } let(:group) { build_stubbed(:group) }
let(:project_with_group) { build_stubbed(:project, group: group) } let(:project_with_group) { build_stubbed(:project, group: group) }
......
...@@ -3,14 +3,15 @@ ...@@ -3,14 +3,15 @@
require 'spec_helper' require 'spec_helper'
RSpec.describe IssuePolicy do RSpec.describe IssuePolicy do
let(:owner) { build_stubbed(:user) } let_it_be(:owner) { create(:user) }
let(:namespace) { build_stubbed(:namespace, owner: owner) } let_it_be(:namespace) { create(:group) }
let(:project) { build_stubbed(:project, namespace: namespace) } let_it_be(:project) { create(:project, group: namespace) }
let(:issue) { build_stubbed(:issue, project: project) } let_it_be(:issue) { create(:issue, project: project) }
subject { described_class.new(owner, issue) } subject { described_class.new(owner, issue) }
before do before do
namespace.add_owner(owner)
allow(issue).to receive(:namespace).and_return namespace allow(issue).to receive(:namespace).and_return namespace
allow(project).to receive(:design_management_enabled?).and_return true allow(project).to receive(:design_management_enabled?).and_return true
end end
......
...@@ -21789,6 +21789,9 @@ msgstr "" ...@@ -21789,6 +21789,9 @@ msgstr ""
msgid "Promote issue to an epic" msgid "Promote issue to an epic"
msgstr "" msgstr ""
msgid "Promote to epic"
msgstr ""
msgid "Promote to group label" msgid "Promote to group label"
msgstr "" msgstr ""
...@@ -25320,6 +25323,9 @@ msgstr "" ...@@ -25320,6 +25323,9 @@ msgstr ""
msgid "Something went wrong while performing the action." msgid "Something went wrong while performing the action."
msgstr "" msgstr ""
msgid "Something went wrong while promoting the issue to an epic. Please try again."
msgstr ""
msgid "Something went wrong while reopening a requirement." msgid "Something went wrong while reopening a requirement."
msgstr "" msgstr ""
...@@ -26935,6 +26941,9 @@ msgstr "" ...@@ -26935,6 +26941,9 @@ msgstr ""
msgid "The issue stage shows the time it takes from creating an issue to assigning the issue to a milestone, or add the issue to a list on your Issue Board. Begin creating issues to see data for this stage." msgid "The issue stage shows the time it takes from creating an issue to assigning the issue to a milestone, or add the issue to a list on your Issue Board. Begin creating issues to see data for this stage."
msgstr "" msgstr ""
msgid "The issue was successfully promoted to an epic. Redirecting to epic..."
msgstr ""
msgid "The license for Deploy Board is required to use this feature." msgid "The license for Deploy Board is required to use this feature."
msgstr "" msgstr ""
......
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