Commit ff7c8182 authored by Alexandru Croitor's avatar Alexandru Croitor

Quick actions for adding/removing epic child relations

https://gitlab.com/gitlab-org/gitlab-ee/issues/7330
parent 221f0060
...@@ -283,6 +283,10 @@ module EE ...@@ -283,6 +283,10 @@ module EE
issues.any? || descendants.any? issues.any? || descendants.any?
end end
def child?(id)
children.where(id: id).exists?
end
def hierarchy def hierarchy
::Gitlab::ObjectHierarchy.new(self.class.where(id: id)) ::Gitlab::ObjectHierarchy.new(self.class.where(id: id))
end end
......
...@@ -8,6 +8,7 @@ module EE ...@@ -8,6 +8,7 @@ module EE
# as doing so would clear any existing command definitions. # as doing so would clear any existing command definitions.
prepended do prepended do
# rubocop: disable Cop/InjectEnterpriseEditionModule # rubocop: disable Cop/InjectEnterpriseEditionModule
include EE::Gitlab::QuickActions::EpicActions
include EE::Gitlab::QuickActions::IssueActions include EE::Gitlab::QuickActions::IssueActions
include EE::Gitlab::QuickActions::MergeRequestActions include EE::Gitlab::QuickActions::MergeRequestActions
include EE::Gitlab::QuickActions::IssueAndMergeRequestActions include EE::Gitlab::QuickActions::IssueAndMergeRequestActions
......
---
title: Add quick actions for adding and removing child epic relations to epic
merge_request: 12772
author:
type: added
# frozen_string_literal: true
module EE
module Gitlab
module QuickActions
module EpicActions
extend ActiveSupport::Concern
include ::Gitlab::QuickActions::Dsl
included do
desc _('Add child epic to an epic')
explanation do |epic_param|
child_epic = extract_epic(epic_param)
_("Adds %{epic_ref} as child epic.") % { epic_ref: child_epic.to_reference(quick_action_target) } if child_epic
end
types Epic
condition { action_allowed? }
params '<&epic | group&epic | Epic URL>'
command :child_epic do |epic_param|
child_epic = extract_epic(epic_param)
if child_epic && !quick_action_target.child?(child_epic.id)
EpicLinks::CreateService.new(quick_action_target, current_user, { target_issuable: child_epic }).execute
end
end
desc _('Remove child epic from an epic')
explanation do |epic_param|
child_epic = extract_epic(epic_param)
_("Removes %{epic_ref} from child epics.") % { epic_ref: child_epic.to_reference(quick_action_target) } if child_epic
end
types Epic
condition { action_allowed? }
params '<&epic | group&epic | Epic URL>'
command :remove_child_epic do |epic_param|
child_epic = extract_epic(epic_param)
if child_epic && quick_action_target.child?(child_epic.id)
EpicLinks::DestroyService.new(child_epic, current_user).execute
end
end
private
def extract_epic(params)
return if params.nil?
extract_references(params, :epic).first
end
def action_allowed?
quick_action_target.group&.feature_available?(:epics) &&
current_user.can?(:"admin_#{quick_action_target.to_ability_name}", quick_action_target)
end
end
end
end
end
end
...@@ -103,7 +103,7 @@ describe Groups::AutocompleteService do ...@@ -103,7 +103,7 @@ describe Groups::AutocompleteService do
it 'returns available commands' do it 'returns available commands' do
expect(subject.commands(epic).map { |c| c[:name] }) expect(subject.commands(epic).map { |c| c[:name] })
.to match_array( .to match_array(
[:todo, :unsubscribe, :award, :shrug, :tableflip, :cc, :title, :close] [:todo, :unsubscribe, :award, :shrug, :tableflip, :cc, :title, :close, :child_epic, :remove_child_epic]
) )
end end
end end
......
...@@ -17,6 +17,18 @@ describe QuickActions::InterpretService do ...@@ -17,6 +17,18 @@ describe QuickActions::InterpretService do
project.add_developer(current_user) project.add_developer(current_user)
end end
shared_examples 'quick action is unavailable' do |action|
it 'does not recognize action' do
expect(service.available_commands(target).map { |command| command[:name] }).not_to include(action)
end
end
shared_examples 'quick action is available' do |action|
it 'does recognize action' do
expect(service.available_commands(target).map { |command| command[:name] }).to include(action)
end
end
describe '#execute' do describe '#execute' do
let(:merge_request) { create(:merge_request, source_project: project) } let(:merge_request) { create(:merge_request, source_project: project) }
...@@ -197,7 +209,7 @@ describe QuickActions::InterpretService do ...@@ -197,7 +209,7 @@ describe QuickActions::InterpretService do
end end
context 'epic command' do context 'epic command' do
let(:epic) { create(:epic, group: group)} let(:epic) { create(:epic, group: group) }
let(:content) { "/epic #{epic.to_reference(project)}" } let(:content) { "/epic #{epic.to_reference(project)}" }
context 'when epics are enabled' do context 'when epics are enabled' do
...@@ -286,8 +298,275 @@ describe QuickActions::InterpretService do ...@@ -286,8 +298,275 @@ describe QuickActions::InterpretService do
end end
end end
context 'child_epic command' do
let(:subgroup) { create(:group, parent: group) }
let(:another_group) { create(:group) }
let(:merge_request) { create(:merge_request, source_project: project) }
let(:epic) { create(:epic, group: group) }
let(:child_epic) { create(:epic, group: group) }
let(:content) { "/child_epic #{child_epic&.to_reference(epic)}" }
shared_examples 'epic relation is not added' do
it 'does not add child epic to epic' do
service.execute(content, epic)
child_epic.reload
expect(child_epic.parent).to be_nil
end
end
shared_examples 'epic relation is added' do
it 'adds child epic relation to the epic' do
service.execute(content, epic)
child_epic.reload
expect(child_epic.parent).to eq(epic)
end
end
context 'when epics are enabled' do
before do
stub_licensed_features(epics: true)
end
context 'when a user does not have permissions to add epic relations' do
it_behaves_like 'epic relation is not added'
it_behaves_like 'quick action is unavailable', :child_epic do
let(:target) { epic }
end
end
context 'when a user has permissions to add epic relations' do
before do
group.add_developer(current_user)
another_group.add_developer(current_user)
end
it_behaves_like 'epic relation is added'
it_behaves_like 'quick action is available', :child_epic do
let(:target) { epic }
end
it_behaves_like 'quick action is unavailable', :child_epic do
let(:target) { issue }
end
it_behaves_like 'quick action is unavailable', :child_epic do
let(:target) { merge_request }
end
context 'when passed child epic is nil' do
let(:child_epic) { nil }
it 'does not add child epic to epic' do
expect { service.execute(content, epic) }.not_to change { epic.children.count }
expect { service.execute(content, epic) }.not_to raise_error
end
it 'does not raise error' do
expect { service.execute(content, epic) }.not_to raise_error
end
end
context 'when child_epic is already linked to an epic' do
let(:another_epic) { create(:epic, group: group) }
before do
child_epic.update!(parent: another_epic)
end
it_behaves_like 'epic relation is added'
it_behaves_like 'quick action is available', :child_epic do
let(:target) { epic }
end
end
context 'when child epic is in a subgroup of parent epic' do
let(:child_epic) { create(:epic, group: subgroup) }
it_behaves_like 'epic relation is added'
it_behaves_like 'quick action is available', :child_epic do
let(:target) { epic }
end
end
context 'when child epic is in a parent group of the parent epic' do
let(:child_epic) { create(:epic, group: group) }
before do
epic.update!(group: subgroup)
end
it_behaves_like 'epic relation is not added'
it_behaves_like 'quick action is available', :child_epic do
let(:target) { epic }
end
end
context 'when child epic is in a different group than parent epic' do
let(:child_epic) { create(:epic, group: another_group) }
it_behaves_like 'epic relation is not added'
it_behaves_like 'quick action is available', :child_epic do
let(:target) { epic }
end
end
end
end
context 'when epics are disabled' do
before do
group.add_developer(current_user)
end
it_behaves_like 'epic relation is not added'
it_behaves_like 'quick action is unavailable', :child_epic do
let(:target) { epic }
end
end
end
context 'remove_child_epic command' do
let(:subgroup) { create(:group, parent: group) }
let(:another_group) { create(:group) }
let(:merge_request) { create(:merge_request, source_project: project) }
let(:epic) { create(:epic, group: group) }
let!(:child_epic) { create(:epic, group: group, parent: epic) }
let(:content) { "/remove_child_epic #{child_epic.to_reference(epic)}" }
shared_examples 'epic relation is not removed' do
it 'does not remove child_epic from epic' do
expect(child_epic.parent).to eq(epic)
service.execute(content, target)
child_epic.reload
expect(child_epic.parent).to eq(epic)
end
end
shared_examples 'epic relation is removed' do
it 'does not remove child_epic from epic' do
expect(child_epic.parent).to eq(epic)
service.execute(content, epic)
child_epic.reload
expect(child_epic.parent).to be_nil
end
end
context 'when epics are enabled' do
before do
stub_licensed_features(epics: true)
epic.reload
end
context 'when a user does not have permissions to remove epic relations' do
it 'does not remove child_epic from epic' do
expect(child_epic.parent).to eq(epic)
service.execute(content, epic)
child_epic.reload
expect(child_epic.parent).to eq(epic)
end
it_behaves_like 'epic relation is not removed' do
let(:target) { epic }
end
it_behaves_like 'quick action is unavailable', :remove_child_epic do
let(:target) { epic }
end
end
context 'when a user has permissions to remove epic relations' do
before do
group.add_developer(current_user)
another_group.add_developer(current_user)
end
it_behaves_like 'quick action is available', :remove_child_epic do
let(:target) { epic }
end
it_behaves_like 'quick action is unavailable', :remove_child_epic do
let(:target) { issue }
end
it_behaves_like 'quick action is unavailable', :remove_child_epic do
let(:target) { merge_request }
end
it_behaves_like 'epic relation is removed'
context 'when trying to remove child epic from a different epic' do
let(:another_epic) { create(:epic, group: group) }
it_behaves_like 'epic relation is not removed' do
let(:target) { another_epic }
end
end
context 'when child epic is in a subgroup of parent epic' do
let(:child_epic) { create(:epic, group: subgroup, parent: epic) }
it_behaves_like 'epic relation is removed'
it_behaves_like 'quick action is available', :remove_child_epic do
let(:target) { epic }
end
end
context 'when child and paretn epics are in different groups' do
let(:child_epic) { create(:epic, group: group, parent: epic) }
context 'when child epic is in a parent group of the parent epic' do
before do
epic.update!(group: subgroup)
end
it_behaves_like 'epic relation is removed' do
let(:target) { epic }
end
it_behaves_like 'quick action is available', :remove_child_epic do
let(:target) { epic }
end
end
context 'when child epic is in a different group than parent epic' do
before do
epic.update!(group: another_group)
end
it_behaves_like 'epic relation is removed' do
let(:target) { epic }
end
it_behaves_like 'quick action is available', :remove_child_epic do
let(:target) { epic }
end
end
end
end
end
context 'when epics are disabled' do
before do
group.add_developer(current_user)
end
it_behaves_like 'epic relation is not removed' do
let(:target) { epic }
end
it_behaves_like 'quick action is unavailable', :remove_child_epic do
let(:target) { epic }
end
end
end
context 'label command for epics' do context 'label command for epics' do
let(:epic) { create(:epic, group: group)} let(:epic) { create(:epic, group: group) }
let(:label) { create(:group_label, title: 'bug', group: group) } let(:label) { create(:group_label, title: 'bug', group: group) }
let(:project_label) { create(:label, title: 'project_label') } let(:project_label) { create(:label, title: 'project_label') }
let(:content) { "/label ~#{label.title} ~#{project_label.title}" } let(:content) { "/label ~#{label.title} ~#{project_label.title}" }
...@@ -332,7 +611,7 @@ describe QuickActions::InterpretService do ...@@ -332,7 +611,7 @@ describe QuickActions::InterpretService do
end end
context 'remove_epic command' do context 'remove_epic command' do
let(:epic) { create(:epic, group: group)} let(:epic) { create(:epic, group: group) }
let(:content) { "/remove_epic #{epic.to_reference(project)}" } let(:content) { "/remove_epic #{epic.to_reference(project)}" }
before do before do
...@@ -409,7 +688,7 @@ describe QuickActions::InterpretService do ...@@ -409,7 +688,7 @@ describe QuickActions::InterpretService do
it_behaves_like 'weight command' do it_behaves_like 'weight command' do
let(:weight) { 5 } let(:weight) { 5 }
let(:content) { "/weight #{weight}"} let(:content) { "/weight #{weight}" }
let(:issuable) { issue } let(:issuable) { issue }
end end
...@@ -518,5 +797,55 @@ describe QuickActions::InterpretService do ...@@ -518,5 +797,55 @@ describe QuickActions::InterpretService do
expect(explanations).to eq(['Sets weight to 4.']) expect(explanations).to eq(['Sets weight to 4.'])
end end
end end
context 'epic commands' do
let(:epic) { create(:epic, group: group) }
let(:child_epic) { create(:epic, group: group) }
before do
stub_licensed_features(epics: true)
group.add_developer(current_user)
end
context 'child_epic command' do
context 'when correct epic reference' do
let(:content) { "/child_epic #{child_epic&.to_reference(epic)}" }
it 'returns message with epic reference' do
_, explanations = service.explain(content, epic)
expect(explanations).to eq(["Adds #{child_epic.group.name}&#{child_epic.iid} as child epic."])
end
end
context 'when epic reference is wrong' do
let(:content) { "/child_epic qwe" }
it 'returns empty explain message' do
_, explanations = service.explain(content, epic)
expect(explanations).to eq([])
end
end
end
context 'remove_child_epic command' do
context 'when correct epic reference' do
let(:content) { "/remove_child_epic #{child_epic&.to_reference(epic)}" }
it 'returns message with epic reference' do
_, explanations = service.explain(content, epic)
expect(explanations).to eq(["Removes #{child_epic.group.name}&#{child_epic.iid} from child epics."])
end
end
context 'when epic reference is wrong' do
let(:content) { "/child_epic qwe" }
it 'returns empty explain message' do
_, explanations = service.explain(content, epic)
expect(explanations).to eq([])
end
end
end
end
end end
end end
...@@ -663,6 +663,9 @@ msgstr "" ...@@ -663,6 +663,9 @@ msgstr ""
msgid "Add bold text" msgid "Add bold text"
msgstr "" msgstr ""
msgid "Add child epic to an epic"
msgstr ""
msgid "Add comment now" msgid "Add comment now"
msgstr "" msgstr ""
...@@ -735,6 +738,9 @@ msgstr "" ...@@ -735,6 +738,9 @@ msgstr ""
msgid "Adds" msgid "Adds"
msgstr "" msgstr ""
msgid "Adds %{epic_ref} as child epic."
msgstr ""
msgid "Adds a todo." msgid "Adds a todo."
msgstr "" msgstr ""
...@@ -10403,6 +10409,9 @@ msgstr "" ...@@ -10403,6 +10409,9 @@ msgstr ""
msgid "Remove avatar" msgid "Remove avatar"
msgstr "" msgstr ""
msgid "Remove child epic from an epic"
msgstr ""
msgid "Remove due date" msgid "Remove due date"
msgstr "" msgstr ""
...@@ -10445,6 +10454,9 @@ msgstr "" ...@@ -10445,6 +10454,9 @@ msgstr ""
msgid "Removed projects cannot be restored!" msgid "Removed projects cannot be restored!"
msgstr "" msgstr ""
msgid "Removes %{epic_ref} from child epics."
msgstr ""
msgid "Removes %{milestone_reference} milestone." msgid "Removes %{milestone_reference} milestone."
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