diff --git a/app/services/boards/issues/move_service.rb b/app/services/boards/issues/move_service.rb new file mode 100644 index 00000000000..d893a52cf9b --- /dev/null +++ b/app/services/boards/issues/move_service.rb @@ -0,0 +1,58 @@ +module Boards + module Issues + class MoveService + def initialize(project, user, params = {}) + @project = project + @user = user + @params = params.dup + end + + def execute + return false unless issue.present? + + update_service.execute(issue) + reopen_service.execute(issue) if moving_from.done? + close_service.execute(issue) if moving_to.done? + + true + end + + private + + attr_reader :project, :user, :params + + delegate :board, to: :project + + def issue + @issue ||= project.issues.visible_to_user(user).find_by!(iid: params[:id]) + end + + def moving_from + @moving_from ||= board.lists.find(params[:from]) + end + + def moving_to + @moving_to ||= board.lists.find(params[:to]) + end + + def close_service + ::Issues::CloseService.new(project, user) + end + + def reopen_service + ::Issues::ReopenService.new(project, user) + end + + def update_service + ::Issues::UpdateService.new(project, user, issue_params) + end + + def issue_params + { + add_label_ids: [moving_to.label_id].compact, + remove_label_ids: [moving_from.label_id].compact + } + end + end + end +end diff --git a/spec/services/boards/issues/move_service_spec.rb b/spec/services/boards/issues/move_service_spec.rb new file mode 100644 index 00000000000..a0f72dddc09 --- /dev/null +++ b/spec/services/boards/issues/move_service_spec.rb @@ -0,0 +1,125 @@ +require 'spec_helper' + +describe Boards::Issues::MoveService, services: true do + describe '#execute' do + let(:user) { create(:user) } + let(:project) { create(:project_with_board) } + let(:board) { project.board } + + let(:bug) { create(:label, project: project, name: 'Bug') } + let(:development) { create(:label, project: project, name: 'Development') } + let(:testing) { create(:label, project: project, name: 'Testing') } + + let(:backlog) { create(:backlog_list, board: board) } + let(:list1) { create(:label_list, board: board, label: development) } + let(:list2) { create(:label_list, board: board, label: testing) } + let(:done) { create(:done_list, board: board) } + + before do + project.team << [user, :developer] + end + + context 'when moving from backlog' do + it 'adds the label of the list it goes to' do + issue = create(:labeled_issue, project: project, labels: [bug]) + params = { id: issue.iid, from: backlog.id, to: list1.id } + + described_class.new(project, user, params).execute + + expect(issue.reload.labels).to contain_exactly(bug, development) + end + end + + context 'when moving to backlog' do + it 'remove the label of the list it came from' do + issue = create(:labeled_issue, project: project, labels: [bug, development]) + params = { id: issue.iid, from: list1.id, to: backlog.id } + + described_class.new(project, user, params).execute + + expect(issue.reload.labels).to contain_exactly(bug) + end + end + + context 'when moving from backlog to done' do + it 'closes the issue' do + issue = create(:labeled_issue, project: project, labels: [bug]) + params = { id: issue.iid, from: backlog.id, to: done.id } + + described_class.new(project, user, params).execute + issue.reload + + expect(issue.labels).to contain_exactly(bug) + expect(issue).to be_closed + end + end + + context 'when moving an issue between lists' do + let(:issue) { create(:labeled_issue, project: project, labels: [bug, development]) } + let(:params) { { id: issue.iid, from: list1.id, to: list2.id } } + + it 'delegates the label changes to Issues::UpdateService' do + expect_any_instance_of(Issues::UpdateService).to receive(:execute).with(issue).once + + described_class.new(project, user, params).execute + end + + it 'removes the label from the list it came from and adds the label of the list it goes to' do + described_class.new(project, user, params).execute + + expect(issue.reload.labels).to contain_exactly(bug, testing) + end + end + + context 'when moving to done' do + let(:issue) { create(:labeled_issue, project: project, labels: [bug, testing]) } + let(:params) { { id: issue.iid, from: list2.id, to: done.id } } + + it 'delegates the close proceedings to Issues::CloseService' do + expect_any_instance_of(Issues::CloseService).to receive(:execute).with(issue).once + + described_class.new(project, user, params).execute + end + + it 'remove the label of the list it came from and close the issue' do + described_class.new(project, user, params).execute + issue.reload + + expect(issue.labels).to contain_exactly(bug) + expect(issue).to be_closed + end + end + + context 'when moving from done' do + let(:issue) { create(:labeled_issue, :closed, project: project, labels: [bug]) } + let(:params) { { id: issue.iid, from: done.id, to: list2.id } } + + it 'delegates the re-open proceedings to Issues::ReopenService' do + expect_any_instance_of(Issues::ReopenService).to receive(:execute).with(issue).once + + described_class.new(project, user, params).execute + end + + it 'adds the label of the list it goes to and reopen the issue' do + described_class.new(project, user, params).execute + issue.reload + + expect(issue.labels).to contain_exactly(bug, testing) + expect(issue).to be_reopened + end + end + + context 'when moving from done to backlog' do + it 'reopens the issue' do + issue = create(:labeled_issue, :closed, project: project, labels: [bug]) + params = { id: issue.iid, from: done.id, to: backlog.id } + + described_class.new(project, user, params).execute + issue.reload + + expect(issue.labels).to contain_exactly(bug) + expect(issue).to be_reopened + end + end + end +end