Commit 14aae568 authored by Sean McGivern's avatar Sean McGivern

Merge branch 'epic_link_message' into 'master'

Show validation error if link creation fails

See merge request gitlab-org/gitlab!33638
parents c8e6bcf1 0851aada
...@@ -16,9 +16,12 @@ module EpicIssues ...@@ -16,9 +16,12 @@ module EpicIssues
link.epic = issuable link.epic = issuable
link.move_to_start link.move_to_start
link.save!
yield params if link.save
create_notes(referenced_issue, params)
end
link
end end
# rubocop: enable CodeReuse/ActiveRecord # rubocop: enable CodeReuse/ActiveRecord
......
...@@ -23,13 +23,11 @@ module EpicLinks ...@@ -23,13 +23,11 @@ module EpicLinks
def create_single_link def create_single_link
child_epic = referenced_issuables.first child_epic = referenced_issuables.first
if linkable_epic?(child_epic) if linkable_epic?(child_epic) && set_child_epic(child_epic)
set_child_epic!(child_epic) create_notes(child_epic)
create_notes(child_epic, nil)
success success
else else
error(child_epic.errors.messages[:parent].first, 409) error(child_epic.errors.values.flatten.to_sentence, 409)
end end
end end
...@@ -41,19 +39,21 @@ module EpicLinks ...@@ -41,19 +39,21 @@ module EpicLinks
affected_epics = [issuable] affected_epics = [issuable]
affected_epics << referenced_epic if referenced_epic.parent affected_epics << referenced_epic if referenced_epic.parent
set_child_epic!(referenced_epic) if set_child_epic(referenced_epic)
create_notes(referenced_epic)
end
yield referenced_epic
end end
def create_notes(referenced_epic, params) def create_notes(referenced_epic)
SystemNoteService.change_epics_relation(issuable, referenced_epic, current_user, 'relate_epic') SystemNoteService.change_epics_relation(issuable, referenced_epic, current_user, 'relate_epic')
end end
def set_child_epic!(child_epic) def set_child_epic(child_epic)
child_epic.parent = issuable child_epic.parent = issuable
child_epic.move_to_start child_epic.move_to_start
child_epic.save! child_epic.save
end end
def linkable_issuables(epics) def linkable_issuables(epics)
......
...@@ -20,7 +20,13 @@ module IssuableLinks ...@@ -20,7 +20,13 @@ module IssuableLinks
return error(issuables_not_found_message, 404) return error(issuables_not_found_message, 404)
end end
@errors = []
create_links create_links
if @errors.present?
return error(@errors.join('. '), 422)
end
success success
end end
...@@ -36,20 +42,28 @@ module IssuableLinks ...@@ -36,20 +42,28 @@ module IssuableLinks
def create_links def create_links
objects = linkable_issuables(referenced_issuables) objects = linkable_issuables(referenced_issuables)
# it is important that this is not called after relate_issuables, as it relinks epic to the issuable # it is important that this is not called after relate_issuables, as it relinks epic to the issuable
# see EpicLinks::EpicIssues#relate_issuables # see EpicLinks::EpicIssues#relate_issuables
affected_epics = affected_epics(objects) affected_epics = affected_epics(objects)
objects.each do |referenced_object| link_issuables(objects)
relate_issuables(referenced_object) do |params|
create_notes(referenced_object, params)
end
end
Epics::UpdateDatesService.new(affected_epics).execute unless affected_epics.blank? Epics::UpdateDatesService.new(affected_epics).execute unless affected_epics.blank?
end end
def link_issuables(target_issuables)
target_issuables.each do |referenced_object|
link = relate_issuables(referenced_object)
unless link.valid?
@errors << _("%{ref} cannot be added: %{error}") % {
ref: referenced_object.to_reference,
error: link.errors.messages.values.flatten.to_sentence
}
end
end
end
def referenced_issuables def referenced_issuables
@referenced_issuables ||= begin @referenced_issuables ||= begin
target_issuable = params[:target_issuable] target_issuable = params[:target_issuable]
......
...@@ -2,17 +2,21 @@ ...@@ -2,17 +2,21 @@
module IssueLinks module IssueLinks
class CreateService < IssuableLinks::CreateService class CreateService < IssuableLinks::CreateService
# rubocop: disable CodeReuse/ActiveRecord
def relate_issuables(referenced_issue) def relate_issuables(referenced_issue)
attrs = { source: issuable, target: referenced_issue } link = IssueLink.find_or_initialize_by(source: issuable, target: referenced_issue)
if params[:link_type].present? if params[:link_type].present?
attrs[:link_type] = params[:link_type] link.link_type = params[:link_type]
end end
link = IssueLink.create(attrs) if link.changed? && link.save
create_notes(referenced_issue)
end
yield if link.persisted? link
end end
# rubocop: enable CodeReuse/ActiveRecord
def linkable_issuables(issues) def linkable_issuables(issues)
@linkable_issuables ||= begin @linkable_issuables ||= begin
...@@ -20,7 +24,7 @@ module IssueLinks ...@@ -20,7 +24,7 @@ module IssueLinks
end end
end end
def create_notes(referenced_issue, params) def create_notes(referenced_issue)
SystemNoteService.relate_issue(issuable, referenced_issue, current_user) SystemNoteService.relate_issue(issuable, referenced_issue, current_user)
SystemNoteService.relate_issue(referenced_issue, issuable, current_user) SystemNoteService.relate_issue(referenced_issue, issuable, current_user)
end end
......
---
title: Display validation errors when issue link creation fails
merge_request: 33638
author:
type: fixed
...@@ -13,8 +13,6 @@ RSpec.describe EpicIssues::CreateService do ...@@ -13,8 +13,6 @@ RSpec.describe EpicIssues::CreateService do
let(:user) { create(:user) } let(:user) { create(:user) }
let(:valid_reference) { issue.to_reference(full: true) } let(:valid_reference) { issue.to_reference(full: true) }
let!(:existing_link) { create(:epic_issue, epic: epic, issue: issue3) }
def assign_issue(references) def assign_issue(references)
params = { issuable_references: references } params = { issuable_references: references }
...@@ -32,6 +30,8 @@ RSpec.describe EpicIssues::CreateService do ...@@ -32,6 +30,8 @@ RSpec.describe EpicIssues::CreateService do
end end
it 'orders the epic issue to the first place and moves the existing ones down' do it 'orders the epic issue to the first place and moves the existing ones down' do
existing_link = create(:epic_issue, epic: epic, issue: issue3)
subject subject
expect(created_link.relative_position).to be < existing_link.reload.relative_position expect(created_link.relative_position).to be < existing_link.reload.relative_position
...@@ -175,13 +175,15 @@ RSpec.describe EpicIssues::CreateService do ...@@ -175,13 +175,15 @@ RSpec.describe EpicIssues::CreateService do
let(:created_link2) { EpicIssue.find_by!(issue_id: issue2.id) } let(:created_link2) { EpicIssue.find_by!(issue_id: issue2.id) }
it 'creates new relationships' do it 'creates new relationships' do
expect { subject }.to change(EpicIssue, :count).from(1).to(3) expect { subject }.to change { EpicIssue.count }.by(2)
expect(created_link1).to have_attributes(epic: epic) expect(created_link1).to have_attributes(epic: epic)
expect(created_link2).to have_attributes(epic: epic) expect(created_link2).to have_attributes(epic: epic)
end end
it 'orders the epic issues to the first place and moves the existing ones down' do it 'orders the epic issues to the first place and moves the existing ones down' do
existing_link = create(:epic_issue, epic: epic, issue: issue3)
subject subject
expect(created_link2.relative_position).to be < created_link1.relative_position expect(created_link2.relative_position).to be < created_link1.relative_position
...@@ -197,6 +199,32 @@ RSpec.describe EpicIssues::CreateService do ...@@ -197,6 +199,32 @@ RSpec.describe EpicIssues::CreateService do
end end
end end
end end
context 'when there are invalid references' do
let(:epic) { create(:epic, confidential: true, group: group) }
let(:valid_issue) { create(:issue, :confidential, project: project) }
let(:invalid_issue1) { create(:issue, project: project) }
let(:invalid_issue2) { create(:issue, project: project) }
subject do
assign_issue([invalid_issue1.to_reference(full: true),
valid_issue.to_reference(full: true),
invalid_issue2.to_reference(full: true)])
end
it 'creates links only for valid references' do
expect { subject }.to change { EpicIssue.count }.by(1)
end
it 'returns error status' do
expect(subject).to eq(
status: :error,
http_status: 422,
message: "#{invalid_issue1.to_reference} cannot be added: Cannot set confidential epic for not-confidential issue. "\
"#{invalid_issue2.to_reference} cannot be added: Cannot set confidential epic for not-confidential issue"
)
end
end
end end
context 'when user does not have permissions to link the issue' do context 'when user does not have permissions to link the issue' do
...@@ -251,7 +279,7 @@ RSpec.describe EpicIssues::CreateService do ...@@ -251,7 +279,7 @@ RSpec.describe EpicIssues::CreateService do
end end
it 'does not create a new association' do it 'does not create a new association' do
expect { subject }.not_to change(EpicIssue, :count).from(2) expect { subject }.not_to change(EpicIssue, :count)
end end
it 'updates the existing association' do it 'updates the existing association' do
...@@ -263,7 +291,7 @@ RSpec.describe EpicIssues::CreateService do ...@@ -263,7 +291,7 @@ RSpec.describe EpicIssues::CreateService do
end end
it 'creates 3 system notes' do it 'creates 3 system notes' do
expect { subject }.to change { Note.count }.from(0).to(3) expect { subject }.to change { Note.count }.by(3)
end end
it 'updates both old and new epic milestone dates' do it 'updates both old and new epic milestone dates' do
......
...@@ -258,6 +258,34 @@ RSpec.describe EpicLinks::CreateService do ...@@ -258,6 +258,34 @@ RSpec.describe EpicLinks::CreateService do
include_examples 'returns an error' include_examples 'returns an error'
end end
context 'when there are invalid references' do
let(:epic) { create(:epic, confidential: true, group: group) }
let(:invalid_epic1) { create(:epic, group: group) }
let(:valid_epic) { create(:epic, :confidential, group: group) }
let(:invalid_epic2) { create(:epic, group: group) }
subject do
add_epic([invalid_epic1.to_reference(full: true),
valid_epic.to_reference(full: true),
invalid_epic2.to_reference(full: true)])
end
it 'adds only valid references' do
subject
expect(epic.reload.children).to match_array([valid_epic])
end
it 'returns error status' do
expect(subject).to eq(
status: :error,
http_status: 422,
message: "#{invalid_epic1.to_reference} cannot be added: Not-confidential epic cannot be assigned to a confidential parent epic. "\
"#{invalid_epic2.to_reference} cannot be added: Not-confidential epic cannot be assigned to a confidential parent epic"
)
end
end
end end
end end
end end
......
...@@ -125,23 +125,60 @@ RSpec.describe IssueLinks::CreateService do ...@@ -125,23 +125,60 @@ RSpec.describe IssueLinks::CreateService do
context 'when reference of any already related issue is present' do context 'when reference of any already related issue is present' do
let(:issue_a) { create :issue, project: project } let(:issue_a) { create :issue, project: project }
let(:issue_b) { create :issue, project: project } let(:issue_b) { create :issue, project: project }
let(:issue_c) { create :issue, project: project }
before do before do
create :issue_link, source: issue, target: issue_a create :issue_link, source: issue, target: issue_b, link_type: IssueLink::TYPE_RELATES_TO
create :issue_link, source: issue, target: issue_c, link_type: IssueLink::TYPE_IS_BLOCKED_BY
end end
let(:params) do let(:params) do
{ issuable_references: [issue_b.to_reference, issue_a.to_reference] } {
issuable_references: [
issue_a.to_reference,
issue_b.to_reference,
issue_c.to_reference
],
link_type: IssueLink::TYPE_IS_BLOCKED_BY
}
end
it 'creates notes only for new and changed relations' do
expect(SystemNoteService).to receive(:relate_issue).with(issue, issue_a, anything)
expect(SystemNoteService).to receive(:relate_issue).with(issue_a, issue, anything)
expect(SystemNoteService).to receive(:relate_issue).with(issue, issue_b, anything)
expect(SystemNoteService).to receive(:relate_issue).with(issue_b, issue, anything)
expect(SystemNoteService).not_to receive(:relate_issue).with(issue, issue_c, anything)
expect(SystemNoteService).not_to receive(:relate_issue).with(issue_c, issue, anything)
subject
end end
it 'returns success status' do it 'sets the same type of relation for selected references' do
is_expected.to eq(status: :success) expect(subject).to eq(status: :success)
expect(IssueLink.where(target: [issue_a, issue_b, issue_c]).pluck(:link_type))
.to eq([IssueLink::TYPE_IS_BLOCKED_BY, IssueLink::TYPE_IS_BLOCKED_BY, IssueLink::TYPE_IS_BLOCKED_BY])
end
end
context 'when there are invalid references' do
let(:issue_a) { create :issue, project: project }
let(:params) do
{ issuable_references: [issue.to_reference, issue_a.to_reference] }
end end
it 'valid relations are created' do it 'creates links only for valid references' do
expect { subject }.to change(IssueLink, :count).from(1).to(2) expect { subject }.to change { IssueLink.count }.by(1)
end
expect(IssueLink.find_by!(target: issue_b)).to have_attributes(source: issue) it 'returns error status' do
expect(subject).to eq(
status: :error,
http_status: 422,
message: "#{issue.to_reference} cannot be added: cannot be related to itself"
)
end end
end end
end end
......
...@@ -520,6 +520,9 @@ msgstr "" ...@@ -520,6 +520,9 @@ msgstr ""
msgid "%{primary} (%{secondary})" msgid "%{primary} (%{secondary})"
msgstr "" msgstr ""
msgid "%{ref} cannot be added: %{error}"
msgstr ""
msgid "%{releases} release" msgid "%{releases} release"
msgid_plural "%{releases} releases" msgid_plural "%{releases} releases"
msgstr[0] "" msgstr[0] ""
......
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