Commit 86b07736 authored by Jan Provaznik's avatar Jan Provaznik

Merge branch '35757-move-issues-in-boards-pderichs' into 'master'

Add endpoint to move issues in boards

See merge request gitlab-org/gitlab-ce!30216
parents 06b8fe56 69e02904
...@@ -2,16 +2,24 @@ ...@@ -2,16 +2,24 @@
module Boards module Boards
class IssuesController < Boards::ApplicationController class IssuesController < Boards::ApplicationController
# This is the maximum amount of issues which can be moved by one request to
# bulk_move for now. This is temporary and might be removed in future by
# introducing an alternative (async?) approach.
# (related: https://gitlab.com/groups/gitlab-org/-/epics/382)
MAX_MOVE_ISSUES_COUNT = 50
include BoardsResponses include BoardsResponses
include ControllerWithCrossProjectAccessCheck include ControllerWithCrossProjectAccessCheck
requires_cross_project_access if: -> { board&.group_board? } requires_cross_project_access if: -> { board&.group_board? }
before_action :whitelist_query_limiting, only: [:index, :update] before_action :whitelist_query_limiting, only: [:index, :update, :bulk_move]
before_action :authorize_read_issue, only: [:index] before_action :authorize_read_issue, only: [:index]
before_action :authorize_create_issue, only: [:create] before_action :authorize_create_issue, only: [:create]
before_action :authorize_update_issue, only: [:update] before_action :authorize_update_issue, only: [:update]
skip_before_action :authenticate_user!, only: [:index] skip_before_action :authenticate_user!, only: [:index]
before_action :validate_id_list, only: [:bulk_move]
before_action :can_move_issues?, only: [:bulk_move]
# rubocop: disable CodeReuse/ActiveRecord # rubocop: disable CodeReuse/ActiveRecord
def index def index
...@@ -46,6 +54,17 @@ module Boards ...@@ -46,6 +54,17 @@ module Boards
end end
end end
def bulk_move
service = Boards::Issues::MoveService.new(board_parent, current_user, move_params(true))
issues = Issue.find(params[:ids])
if service.execute_multiple(issues)
head :ok
else
head :unprocessable_entity
end
end
def update def update
service = Boards::Issues::MoveService.new(board_parent, current_user, move_params) service = Boards::Issues::MoveService.new(board_parent, current_user, move_params)
...@@ -58,6 +77,10 @@ module Boards ...@@ -58,6 +77,10 @@ module Boards
private private
def can_move_issues?
head(:forbidden) unless can?(current_user, :admin_issue, board)
end
def render_issues(issues, metadata) def render_issues(issues, metadata)
data = { issues: serialize_as_json(issues) } data = { issues: serialize_as_json(issues) }
data.merge!(metadata) data.merge!(metadata)
...@@ -90,8 +113,9 @@ module Boards ...@@ -90,8 +113,9 @@ module Boards
end end
end end
def move_params def move_params(multiple = false)
params.permit(:board_id, :id, :from_list_id, :to_list_id, :move_before_id, :move_after_id) id_param = multiple ? :ids : :id
params.permit(id_param, :board_id, :from_list_id, :to_list_id, :move_before_id, :move_after_id)
end end
def issue_params def issue_params
...@@ -112,5 +136,10 @@ module Boards ...@@ -112,5 +136,10 @@ module Boards
# Also see https://gitlab.com/gitlab-org/gitlab-ce/issues/42439 # Also see https://gitlab.com/gitlab-org/gitlab-ce/issues/42439
Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-ce/issues/42428') Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-ce/issues/42428')
end end
def validate_id_list
head(:bad_request) unless params[:ids].is_a?(Array)
head(:unprocessable_entity) if params[:ids].size > MAX_MOVE_ISSUES_COUNT
end
end end
end end
...@@ -4,14 +4,37 @@ module Boards ...@@ -4,14 +4,37 @@ module Boards
module Issues module Issues
class MoveService < Boards::BaseService class MoveService < Boards::BaseService
def execute(issue) def execute(issue)
return false unless can?(current_user, :update_issue, issue) issue_modification_params = issue_params(issue)
return false if issue_params(issue).empty? return false if issue_modification_params.empty?
update(issue) move_single_issue(issue, issue_modification_params)
end
def execute_multiple(issues)
return false if issues.empty?
last_inserted_issue_id = nil
issues.map do |issue|
issue_modification_params = issue_params(issue)
next if issue_modification_params.empty?
if last_inserted_issue_id
issue_modification_params[:move_between_ids] = move_between_ids({ move_after_id: nil, move_before_id: last_inserted_issue_id })
end
last_inserted_issue_id = issue.id
move_single_issue(issue, issue_modification_params)
end.all?
end end
private private
def move_single_issue(issue, issue_modification_params)
return false unless can?(current_user, :update_issue, issue)
update(issue, issue_modification_params)
end
def board def board
@board ||= parent.boards.find(params[:board_id]) @board ||= parent.boards.find(params[:board_id])
end end
...@@ -33,8 +56,8 @@ module Boards ...@@ -33,8 +56,8 @@ module Boards
end end
# rubocop: enable CodeReuse/ActiveRecord # rubocop: enable CodeReuse/ActiveRecord
def update(issue) def update(issue, issue_modification_params)
::Issues::UpdateService.new(issue.project, current_user, issue_params(issue)).execute(issue) ::Issues::UpdateService.new(issue.project, current_user, issue_modification_params).execute(issue)
end end
def issue_params(issue) def issue_params(issue)
...@@ -48,6 +71,7 @@ module Boards ...@@ -48,6 +71,7 @@ module Boards
) )
end end
move_between_ids = move_between_ids(params)
if move_between_ids if move_between_ids
attrs[:move_between_ids] = move_between_ids attrs[:move_between_ids] = move_between_ids
attrs[:board_group_id] = board.group&.id attrs[:board_group_id] = board.group&.id
...@@ -78,8 +102,8 @@ module Boards ...@@ -78,8 +102,8 @@ module Boards
end end
# rubocop: enable CodeReuse/ActiveRecord # rubocop: enable CodeReuse/ActiveRecord
def move_between_ids def move_between_ids(move_params)
ids = [params[:move_after_id], params[:move_before_id]] ids = [move_params[:move_after_id], move_params[:move_before_id]]
.map(&:to_i) .map(&:to_i)
.map { |m| m.positive? ? m : nil } .map { |m| m.positive? ? m : nil }
......
---
title: Add endpoint to move multiple issues in boards
merge_request: 30216
author:
type: added
...@@ -82,7 +82,11 @@ Rails.application.routes.draw do ...@@ -82,7 +82,11 @@ Rails.application.routes.draw do
resources :issues, only: [:index, :create, :update] resources :issues, only: [:index, :create, :update]
end end
resources :issues, module: :boards, only: [:index, :update] resources :issues, module: :boards, only: [:index, :update] do
collection do
put :bulk_move, format: :json
end
end
Gitlab.ee do Gitlab.ee do
resources :users, module: :boards, only: [:index] resources :users, module: :boards, only: [:index]
......
...@@ -164,6 +164,201 @@ describe Boards::IssuesController do ...@@ -164,6 +164,201 @@ describe Boards::IssuesController do
end end
end end
describe 'PUT move_multiple' do
let(:todo) { create(:group_label, group: group, name: 'Todo') }
let(:development) { create(:group_label, group: group, name: 'Development') }
let(:user) { create(:group_member, :maintainer, user: create(:user), group: group ).user }
let(:guest) { create(:group_member, :guest, user: create(:user), group: group ).user }
let(:project) { create(:project, group: group) }
let(:group) { create(:group) }
let(:board) { create(:board, project: project) }
let(:list1) { create(:list, board: board, label: todo, position: 0) }
let(:list2) { create(:list, board: board, label: development, position: 1) }
let(:issue1) { create(:labeled_issue, project: project, labels: [todo], author: user, relative_position: 10) }
let(:issue2) { create(:labeled_issue, project: project, labels: [todo], author: user, relative_position: 20) }
let(:issue3) { create(:labeled_issue, project: project, labels: [todo], author: user, relative_position: 30) }
let(:issue4) { create(:labeled_issue, project: project, labels: [development], author: user, relative_position: 100) }
let(:move_params) do
{
board_id: board.id,
ids: [issue1.id, issue2.id, issue3.id],
from_list_id: list1.id,
to_list_id: list2.id,
move_before_id: issue4.id,
move_after_id: nil
}
end
before do
project.add_maintainer(user)
project.add_guest(guest)
end
shared_examples 'move issues endpoint provider' do
before do
sign_in(signed_in_user)
end
it 'moves issues as expected' do
put :bulk_move, params: move_issues_params
expect(response).to have_gitlab_http_status(expected_status)
list_issues user: requesting_user, board: board, list: list2
expect(response).to have_gitlab_http_status(200)
expect(response).to match_response_schema('entities/issue_boards')
responded_issues = json_response['issues']
expect(responded_issues.length).to eq expected_issue_count
ids_in_order = responded_issues.pluck('id')
expect(ids_in_order).to eq(expected_issue_ids_in_order)
end
end
context 'when items are moved to another list' do
it_behaves_like 'move issues endpoint provider' do
let(:signed_in_user) { user }
let(:move_issues_params) { move_params }
let(:requesting_user) { user }
let(:expected_status) { 200 }
let(:expected_issue_count) { 4 }
let(:expected_issue_ids_in_order) { [issue4.id, issue1.id, issue2.id, issue3.id] }
end
end
context 'when moving just one issue' do
it_behaves_like 'move issues endpoint provider' do
let(:signed_in_user) { user }
let(:move_issues_params) do
move_params.dup.tap do |hash|
hash[:ids] = [issue2.id]
end
end
let(:requesting_user) { user }
let(:expected_status) { 200 }
let(:expected_issue_count) { 2 }
let(:expected_issue_ids_in_order) { [issue4.id, issue2.id] }
end
end
context 'when user is not allowed to move issue' do
it_behaves_like 'move issues endpoint provider' do
let(:signed_in_user) { guest }
let(:move_issues_params) do
move_params.dup.tap do |hash|
hash[:ids] = [issue2.id]
end
end
let(:requesting_user) { user }
let(:expected_status) { 403 }
let(:expected_issue_count) { 1 }
let(:expected_issue_ids_in_order) { [issue4.id] }
end
end
context 'when issues should be moved visually above existing issue in list' do
it_behaves_like 'move issues endpoint provider' do
let(:signed_in_user) { user }
let(:move_issues_params) do
move_params.dup.tap do |hash|
hash[:move_after_id] = issue4.id
hash[:move_before_id] = nil
end
end
let(:requesting_user) { user }
let(:expected_status) { 200 }
let(:expected_issue_count) { 4 }
let(:expected_issue_ids_in_order) { [issue1.id, issue2.id, issue3.id, issue4.id] }
end
end
context 'when destination list is empty' do
before do
# Remove issue from list
issue4.labels -= [development]
issue4.save!
end
it_behaves_like 'move issues endpoint provider' do
let(:signed_in_user) { user }
let(:move_issues_params) do
move_params.dup.tap do |hash|
hash[:move_before_id] = nil
end
end
let(:requesting_user) { user }
let(:expected_status) { 200 }
let(:expected_issue_count) { 3 }
let(:expected_issue_ids_in_order) { [issue1.id, issue2.id, issue3.id] }
end
end
context 'when no position arguments are given' do
it_behaves_like 'move issues endpoint provider' do
let(:signed_in_user) { user }
let(:move_issues_params) do
move_params.dup.tap do |hash|
hash[:move_before_id] = nil
end
end
let(:requesting_user) { user }
let(:expected_status) { 200 }
let(:expected_issue_count) { 4 }
let(:expected_issue_ids_in_order) { [issue1.id, issue2.id, issue3.id, issue4.id] }
end
end
context 'when move_before_id and move_after_id are given' do
let(:issue5) { create(:labeled_issue, project: project, labels: [development], author: user, relative_position: 90) }
it_behaves_like 'move issues endpoint provider' do
let(:signed_in_user) { user }
let(:move_issues_params) do
move_params.dup.tap do |hash|
hash[:move_before_id] = issue5.id
hash[:move_after_id] = issue4.id
end
end
let(:requesting_user) { user }
let(:expected_status) { 200 }
let(:expected_issue_count) { 5 }
let(:expected_issue_ids_in_order) { [issue5.id, issue1.id, issue2.id, issue3.id, issue4.id] }
end
end
context 'when request contains too many issues' do
it_behaves_like 'move issues endpoint provider' do
let(:signed_in_user) { user }
let(:move_issues_params) do
move_params.dup.tap do |hash|
hash[:ids] = (0..51).to_a
end
end
let(:requesting_user) { user }
let(:expected_status) { 422 }
let(:expected_issue_count) { 1 }
let(:expected_issue_ids_in_order) { [issue4.id] }
end
end
context 'when request is malformed' do
it_behaves_like 'move issues endpoint provider' do
let(:signed_in_user) { user }
let(:move_issues_params) do
move_params.dup.tap do |hash|
hash[:ids] = 'foobar'
end
end
let(:requesting_user) { user }
let(:expected_status) { 400 }
let(:expected_issue_count) { 1 }
let(:expected_issue_ids_in_order) { [issue4.id] }
end
end
end
def list_issues(user:, board:, list: nil) def list_issues(user:, board:, list: nil)
sign_in(user) sign_in(user)
......
...@@ -52,5 +52,91 @@ describe Boards::Issues::MoveService do ...@@ -52,5 +52,91 @@ describe Boards::Issues::MoveService do
it_behaves_like 'issues move service', true it_behaves_like 'issues move service', true
end end
describe '#execute_multiple' do
set(:group) { create(:group) }
set(:user) { create(:user) }
set(:project) { create(:project, namespace: group) }
set(:board1) { create(:board, group: group) }
set(:development) { create(:group_label, group: group, name: 'Development') }
set(:testing) { create(:group_label, group: group, name: 'Testing') }
set(:list1) { create(:list, board: board1, label: development, position: 0) }
set(:list2) { create(:list, board: board1, label: testing, position: 1) }
let(:params) { { board_id: board1.id, from_list_id: list1.id, to_list_id: list2.id } }
before do
project.add_developer(user)
end
it 'returns false if list of issues is empty' do
expect(described_class.new(group, user, params).execute_multiple([])).to eq(false)
end
context 'moving multiple issues' do
let(:issue1) { create(:labeled_issue, project: project, labels: [development]) }
let(:issue2) { create(:labeled_issue, project: project, labels: [development]) }
it 'moves multiple issues from one list to another' do
expect(described_class.new(group, user, params).execute_multiple([issue1, issue2])).to be_truthy
expect(issue1.labels).to eq([testing])
expect(issue2.labels).to eq([testing])
end
end
context 'moving a single issue' do
let(:issue1) { create(:labeled_issue, project: project, labels: [development]) }
it 'moves one issue' do
expect(described_class.new(group, user, params).execute_multiple([issue1])).to be_truthy
expect(issue1.labels).to eq([testing])
end
end
context 'moving issues visually after an existing issue' do
let(:existing_issue) { create(:labeled_issue, project: project, labels: [testing], relative_position: 10) }
let(:issue1) { create(:labeled_issue, project: project, labels: [development]) }
let(:issue2) { create(:labeled_issue, project: project, labels: [development]) }
let(:move_params) do
params.dup.tap do |hash|
hash[:move_before_id] = existing_issue.id
end
end
it 'moves one issue' do
expect(described_class.new(group, user, move_params).execute_multiple([issue1, issue2])).to be_truthy
expect(issue1.labels).to eq([testing])
expect(issue2.labels).to eq([testing])
expect(issue1.relative_position > existing_issue.relative_position).to eq(true)
expect(issue2.relative_position > issue1.relative_position).to eq(true)
end
end
context 'moving issues visually before an existing issue' do
let(:existing_issue) { create(:labeled_issue, project: project, labels: [testing], relative_position: 10) }
let(:issue1) { create(:labeled_issue, project: project, labels: [development]) }
let(:issue2) { create(:labeled_issue, project: project, labels: [development]) }
let(:move_params) do
params.dup.tap do |hash|
hash[:move_after_id] = existing_issue.id
end
end
it 'moves one issue' do
expect(described_class.new(group, user, move_params).execute_multiple([issue1, issue2])).to be_truthy
expect(issue1.labels).to eq([testing])
expect(issue2.labels).to eq([testing])
expect(issue2.relative_position < existing_issue.relative_position).to eq(true)
expect(issue1.relative_position < issue2.relative_position).to eq(true)
end
end
end
end end
end end
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