Commit 8b8c552e authored by Jarka Kadlecová's avatar Jarka Kadlecová Committed by Jarka Košanová

Quick action for adding/removing epic to issues

parent f401f3b6
...@@ -47,6 +47,8 @@ do. ...@@ -47,6 +47,8 @@ do.
| `/shrug` | Append the comment with `¯\_(ツ)_/¯` | | `/shrug` | Append the comment with `¯\_(ツ)_/¯` |
| <code>/copy_metadata #issue &#124; !merge_request</code> | Copy labels and milestone from other issue or merge request | | <code>/copy_metadata #issue &#124; !merge_request</code> | Copy labels and milestone from other issue or merge request |
| `/confidential` | Makes the issue confidential | | `/confidential` | Makes the issue confidential |
| `/epic <group&epic | Epic URL>` | Adds an issue to an epic |
| `/remove_epic` | Removes an issue from an epic |
Note: In GitLab Starter every issue can have more than one assignee, so commands `/assign`, `/unassign` and `/reassign` Note: In GitLab Starter every issue can have more than one assignee, so commands `/assign`, `/unassign` and `/reassign`
support multiple assignees. support multiple assignees.
...@@ -7,6 +7,7 @@ module EE ...@@ -7,6 +7,7 @@ module EE
override :execute override :execute
def execute(issue) def execute(issue)
handle_epic(issue)
result = super result = super
if issue.previous_changes.include?(:milestone_id) && issue.epic if issue.previous_changes.include?(:milestone_id) && issue.epic
...@@ -15,6 +16,24 @@ module EE ...@@ -15,6 +16,24 @@ module EE
result result
end end
private
def handle_epic(issue)
return unless params.key?(:epic)
epic_param = params.delete(:epic)
if epic_param
EpicIssues::CreateService.new(epic_param, current_user, { target_issue: issue }).execute
else
link = EpicIssue.find_by(issue_id: issue.id)
return unless link
EpicIssues::DestroyService.new(link, current_user).execute
end
end
end end
end end
end end
...@@ -47,6 +47,36 @@ module EE ...@@ -47,6 +47,36 @@ module EE
command :clear_weight do command :clear_weight do
@updates[:weight] = nil @updates[:weight] = nil
end end
desc 'Add to epic'
explanation 'Adds an issue to an epic.'
condition do
issuable.is_a?(::Issue) &&
issuable.project.group&.feature_available?(:epics) &&
current_user.can?(:"admin_#{issuable.to_ability_name}", issuable)
end
params '<group&epic | Epic URL>'
command :epic do |epic_param|
@updates[:epic] = extract_epic(epic_param)
end
desc 'Remove from epic'
explanation 'Removes an issue from an epic.'
condition do
issuable.is_a?(::Issue) &&
issuable.persisted? &&
issuable.project.group&.feature_available?(:epics) &&
current_user.can?(:"admin_#{issuable.to_ability_name}", issuable)
end
command :remove_epic do
@updates[:epic] = nil
end
def extract_epic(params)
return nil if params.nil?
extract_references(params, :epic).first
end
end end
end end
end end
---
title: Quick action for adding/removing epic to issues
merge_request: 6934
author:
type: added
# frozen_string_literal: true
require 'spec_helper'
describe Issues::CreateService do
let(:group) { create(:group) }
let(:project) { create(:project, group: group) }
let(:user) { create(:user) }
let(:issue) { described_class.new(project, user, opts).execute }
before do
stub_licensed_features(epics: true)
project.add_maintainer(user)
end
context 'quick actions' do
context '/epic action' do
let(:epic) { create(:epic, group: group) }
let(:opts) do
{
title: 'New issue',
description: "/epic #{epic.to_reference(project)}"
}
end
it 'adds an issue to the passed epic' do
expect(issue).to be_persisted
expect(issue.epic).to eq(epic)
end
end
end
end
...@@ -2,9 +2,10 @@ ...@@ -2,9 +2,10 @@
require 'spec_helper' require 'spec_helper'
describe Issues::UpdateService do describe Issues::UpdateService do
let(:issue) { create(:issue) } let(:group) { create(:group) }
let(:project) { create(:project, group: group) }
let(:issue) { create(:issue, project: project) }
let(:user) { issue.author } let(:user) { issue.author }
let(:project) { issue.project }
describe 'execute' do describe 'execute' do
def update_issue(opts) def update_issue(opts)
...@@ -33,5 +34,89 @@ describe Issues::UpdateService do ...@@ -33,5 +34,89 @@ describe Issues::UpdateService do
end end
end end
end end
context 'assigning epic' do
before do
stub_licensed_features(epics: true)
group.add_maintainer(user)
end
let(:epic) { create(:epic, group: group) }
context 'when issue does not belong to an epic yet' do
it 'assigns an issue to the provided epic' do
expect { update_issue(epic: epic) }.to change { issue.reload.epic }.from(nil).to(epic)
end
it 'creates system notes for the epic and the issue' do
expect { update_issue(epic: epic) }.to change { Note.count }.from(0).to(2)
epic_note = Note.find_by(noteable_id: epic.id, noteable_type: 'Epic')
issue_note = Note.find_by(noteable_id: issue.id, noteable_type: 'Issue')
expect(epic_note.system_note_metadata.action).to eq('epic_issue_added')
expect(issue_note.system_note_metadata.action).to eq('issue_added_to_epic')
end
end
context 'when issue does belongs to another epic' do
let(:epic2) { create(:epic, group: group) }
before do
issue.update!(epic: epic2)
end
it 'assigns the issue passed to the provided epic' do
expect { update_issue(epic: epic) }.to change { issue.reload.epic }.from(epic2).to(epic)
end
it 'creates system notes for the epic and the issue' do
expect { update_issue(epic: epic) }.to change { Note.count }.from(0).to(3)
epic_note = Note.find_by(noteable_id: epic.id, noteable_type: 'Epic')
epic2_note = Note.find_by(noteable_id: epic2.id, noteable_type: 'Epic')
issue_note = Note.find_by(noteable_id: issue.id, noteable_type: 'Issue')
expect(epic_note.system_note_metadata.action).to eq('epic_issue_moved')
expect(epic2_note.system_note_metadata.action).to eq('epic_issue_moved')
expect(issue_note.system_note_metadata.action).to eq('issue_changed_epic')
end
end
end
context 'removing epic' do
before do
stub_licensed_features(epics: true)
group.add_maintainer(user)
end
let(:epic) { create(:epic, group: group) }
context 'when issue does not belong to an epic yet' do
it 'does not do anything' do
expect { update_issue(epic: nil) }.not_to change { issue.reload.epic }
end
end
context 'when issue does belongs to an epic' do
before do
issue.update!(epic: epic)
end
it 'assigns a new issue to the provided epic' do
expect { update_issue(epic: nil) }.to change { issue.reload.epic }.from(epic).to(nil)
end
it 'creates system notes for the epic and the issue' do
expect { update_issue(epic: nil) }.to change { Note.count }.from(0).to(2)
epic_note = Note.find_by(noteable_id: epic.id, noteable_type: 'Epic')
issue_note = Note.find_by(noteable_id: issue.id, noteable_type: 'Issue')
expect(epic_note.system_note_metadata.action).to eq('epic_issue_removed')
expect(issue_note.system_note_metadata.action).to eq('issue_removed_from_epic')
end
end
end
end end
end end
# frozen_string_literal: true
require 'spec_helper'
describe Notes::QuickActionsService do
let(:group) { create(:group) }
let(:project) { create(:project, group: group) }
let(:user) { create(:user) }
let(:issue) { create(:issue, project: project) }
let(:epic) { create(:epic, group: group)}
let(:service) { described_class.new(project, user) }
before do
project.add_maintainer(user)
end
def execute(note)
content, command_params = service.extract_commands(note)
service.execute(command_params, note)
content
end
describe '/epic' do
let(:note_text) { "/epic #{epic.to_reference(project)}" }
let(:note) { create(:note_on_issue, noteable: issue, project: project, note: note_text) }
before do
group.add_developer(user)
end
context 'when epics are not enabled' do
it 'does not assign the epic' do
expect(execute(note)).to be_empty
expect(issue.epic).to be_nil
end
end
context 'when epics are enabled' do
before do
stub_licensed_features(epics: true)
end
context 'on an issue' do
it 'assigns the issue to the epic' do
expect { execute(note) }.to change { issue.reload.epic }.from(nil).to(epic)
end
it 'leaves the note empty' do
expect(execute(note)).to eq('')
end
it 'creates a system note' do
expect { execute(note) }.to change { Note.system.count }.from(0).to(2)
end
end
context 'on a merge request' do
let(:note_mr) { create(:note_on_merge_request, project: project, note: note_text) }
it 'leaves the note empty' do
expect(execute(note_mr)).to be_empty
end
end
end
end
describe '/remove_epic' do
let(:note_text) { "/remove_epic" }
let(:note) { create(:note_on_issue, noteable: issue, project: project, note: note_text) }
before do
issue.update!(epic: epic)
group.add_developer(user)
end
context 'when epics are not enabled' do
it 'does not remove the epic' do
expect(execute(note)).to be_empty
expect(issue.epic).to eq(epic)
end
end
context 'when epics are enabled' do
before do
stub_licensed_features(epics: true)
end
context 'on an issue' do
it 'removes the epic' do
expect { execute(note) }.to change { issue.reload.epic }.from(epic).to(nil)
end
it 'leaves the note empty' do
expect(execute(note)).to eq('')
end
it 'creates a system note' do
expect { execute(note) }.to change { Note.system.count }.from(0).to(2)
end
end
context 'on a merge request' do
let(:note_mr) { create(:note_on_merge_request, project: project, note: note_text) }
it 'leaves the note empty' do
expect(execute(note_mr)).to be_empty
end
end
end
end
end
...@@ -5,7 +5,8 @@ describe QuickActions::InterpretService do ...@@ -5,7 +5,8 @@ describe QuickActions::InterpretService do
let(:developer) { create(:user) } let(:developer) { create(:user) }
let(:developer2) { create(:user) } let(:developer2) { create(:user) }
let(:developer3) { create(:user) } let(:developer3) { create(:user) }
let(:project) { create(:project, :public) } let(:group) { create(:group) }
let(:project) { create(:project, :public, group: group) }
let(:issue) { create(:issue, project: project) } let(:issue) { create(:issue, project: project) }
let(:service) { described_class.new(project, developer) } let(:service) { described_class.new(project, developer) }
...@@ -115,6 +116,75 @@ describe QuickActions::InterpretService do ...@@ -115,6 +116,75 @@ describe QuickActions::InterpretService do
end end
end end
end end
context 'epic command' do
let(:epic) { create(:epic, group: group)}
let(:content) { "/epic #{epic.to_reference(project)}" }
context 'when epics are enabled' do
before do
stub_licensed_features(epics: true)
end
it 'assigns an issue to an epic' do
_, updates = service.execute(content, issue)
expect(updates).to eq(epic: epic)
end
context 'when an issue belongs to a project without group' do
let(:user_project) { create(:project) }
let(:issue) { create(:issue, project: user_project) }
before do
user_project.add_developer(user)
end
it 'does not assign an issue to an epic' do
_, updates = service.execute(content, issue)
expect(updates).to be_empty
end
end
end
context 'when epics are disabled' do
it 'does not recognize /epic' do
_, updates = service.execute(content, issue)
expect(updates).to be_empty
end
end
end
context 'remove_epic command' do
let(:epic) { create(:epic, group: group)}
let(:content) { "/remove_epic #{epic.to_reference(project)}" }
before do
issue.update!(epic: epic)
end
context 'when epics are disabled' do
it 'does not recognize /remove_epic' do
_, updates = service.execute(content, issue)
expect(updates).to be_empty
end
end
context 'when epics are enabled' do
before do
stub_licensed_features(epics: true)
end
it 'unassigns an issue from an epic' do
_, updates = service.execute(content, issue)
expect(updates).to eq(epic: nil)
end
end
end
end end
describe '#explain' do describe '#explain' do
......
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