From ab9c8dbc2d2de013ee448b0191d460114619d04a Mon Sep 17 00:00:00 2001 From: Heinrich Lee Yu <heinrich@gitlab.com> Date: Mon, 22 Aug 2022 16:57:48 +0800 Subject: [PATCH] Add mutation to move to start / end of board lists Updates the GraphQL mutation so that we can move to the start or end of a board list. Changelog: added --- .../boards/issues/issue_move_list.rb | 28 ++++++++++- app/services/boards/base_item_move_service.rb | 22 ++++++++- app/services/boards/issues/move_service.rb | 4 ++ doc/api/graphql/reference/index.md | 1 + .../boards/issues/issue_move_list_spec.rb | 46 ++++++++++++++++++- .../boards/issues/issue_move_list_spec.rb | 14 ++++++ .../issues_move_service_shared_examples.rb | 34 ++++++++++++++ 7 files changed, 145 insertions(+), 4 deletions(-) diff --git a/app/graphql/mutations/boards/issues/issue_move_list.rb b/app/graphql/mutations/boards/issues/issue_move_list.rb index 14fe9714f99a..e9cae80e5f91 100644 --- a/app/graphql/mutations/boards/issues/issue_move_list.rb +++ b/app/graphql/mutations/boards/issues/issue_move_list.rb @@ -38,10 +38,16 @@ class IssueMoveList < Mutations::Issues::Base required: false, description: 'ID of issue that should be placed after the current issue.' + argument :position_in_list, GraphQL::Types::Int, + required: false, + description: "Position of issue within the board list. Positions start at 0. "\ + "Use #{::Boards::Issues::MoveService::LIST_END_POSITION} to move to the end of the list." + def ready?(**args) if move_arguments(args).blank? raise Gitlab::Graphql::Errors::ArgumentError, - 'At least one of the arguments fromListId, toListId, afterId or beforeId is required' + 'At least one of the arguments ' \ + 'fromListId, toListId, positionInList, moveAfterId, or moveBeforeId is required' end if move_list_arguments(args).one? @@ -49,6 +55,24 @@ def ready?(**args) 'Both fromListId and toListId must be present' end + if args[:position_in_list].present? + if move_list_arguments(args).empty? + raise Gitlab::Graphql::Errors::ArgumentError, + 'Both fromListId and toListId are required when positionInList is given' + end + + if args[:move_before_id].present? || args[:move_after_id].present? + raise Gitlab::Graphql::Errors::ArgumentError, + 'positionInList is mutually exclusive with any of moveBeforeId or moveAfterId' + end + + if args[:position_in_list] != ::Boards::Issues::MoveService::LIST_END_POSITION && + args[:position_in_list] < 0 + raise Gitlab::Graphql::Errors::ArgumentError, + "positionInList must be >= 0 or #{::Boards::Issues::MoveService::LIST_END_POSITION}" + end + end + super end @@ -77,7 +101,7 @@ def move_list_arguments(args) end def move_arguments(args) - args.slice(:from_list_id, :to_list_id, :move_after_id, :move_before_id) + args.slice(:from_list_id, :to_list_id, :position_in_list, :move_after_id, :move_before_id) end def error_for(result) diff --git a/app/services/boards/base_item_move_service.rb b/app/services/boards/base_item_move_service.rb index 9d711d83fd27..c9da889c5365 100644 --- a/app/services/boards/base_item_move_service.rb +++ b/app/services/boards/base_item_move_service.rb @@ -2,6 +2,8 @@ module Boards class BaseItemMoveService < Boards::BaseService + LIST_END_POSITION = -1 + def execute(issuable) issuable_modification_params = issuable_params(issuable) return if issuable_modification_params.empty? @@ -32,7 +34,13 @@ def issuable_params(issuable) ) end - reposition_ids = move_between_ids(params) + move_params = if params[:position_in_list].present? + move_params_from_list_position(params[:position_in_list]) + else + params + end + + reposition_ids = move_between_ids(move_params) attrs.merge!(reposition_params(reposition_ids)) if reposition_ids attrs @@ -90,6 +98,18 @@ def board_label_ids ::Label.ids_on_board(board.id) end + def move_params_from_list_position(position) + if position == LIST_END_POSITION + { move_before_id: moving_to_list_items_relation.reverse_order.pick(:id), move_after_id: nil } + else + item_at_position = moving_to_list_items_relation.offset(position).pick(:id) # rubocop: disable CodeReuse/ActiveRecord + + return move_params_from_list_position(LIST_END_POSITION) if item_at_position.nil? + + { move_before_id: nil, move_after_id: item_at_position } + end + end + def move_between_ids(move_params) ids = [move_params[:move_before_id], move_params[:move_after_id]] .map(&:to_i) diff --git a/app/services/boards/issues/move_service.rb b/app/services/boards/issues/move_service.rb index 90226b9d4e0b..4de4d7c8f69a 100644 --- a/app/services/boards/issues/move_service.rb +++ b/app/services/boards/issues/move_service.rb @@ -54,6 +54,10 @@ def board def update(issue, issue_modification_params) ::Issues::UpdateService.new(project: issue.project, current_user: current_user, params: issue_modification_params).execute(issue) end + + def moving_to_list_items_relation + Boards::Issues::ListService.new(board.resource_parent, current_user, board_id: board.id, id: moving_to_list.id).execute + end end end end diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md index 2e77b196068a..089c6e0d766c 100644 --- a/doc/api/graphql/reference/index.md +++ b/doc/api/graphql/reference/index.md @@ -3038,6 +3038,7 @@ Input type: `IssueMoveListInput` | <a id="mutationissuemovelistiid"></a>`iid` | [`String!`](#string) | IID of the issue to mutate. | | <a id="mutationissuemovelistmoveafterid"></a>`moveAfterId` | [`ID`](#id) | ID of issue that should be placed after the current issue. | | <a id="mutationissuemovelistmovebeforeid"></a>`moveBeforeId` | [`ID`](#id) | ID of issue that should be placed before the current issue. | +| <a id="mutationissuemovelistpositioninlist"></a>`positionInList` | [`Int`](#int) | Position of issue within the board list. Positions start at 0. Use -1 to move to the end of the list. | | <a id="mutationissuemovelistprojectpath"></a>`projectPath` | [`ID!`](#id) | Project the issue to mutate is in. | | <a id="mutationissuemovelisttolistid"></a>`toListId` | [`ID`](#id) | ID of the board list that the issue will be moved to. | diff --git a/spec/graphql/mutations/boards/issues/issue_move_list_spec.rb b/spec/graphql/mutations/boards/issues/issue_move_list_spec.rb index 10aed8a1f009..8e9a567f6149 100644 --- a/spec/graphql/mutations/boards/issues/issue_move_list_spec.rb +++ b/spec/graphql/mutations/boards/issues/issue_move_list_spec.rb @@ -55,7 +55,7 @@ let(:move_params) { {} } it 'generates an error' do - expect_graphql_error_to_be_created(Gitlab::Graphql::Errors::ArgumentError, 'At least one of the arguments fromListId, toListId, afterId or beforeId is required') do + expect_graphql_error_to_be_created(Gitlab::Graphql::Errors::ArgumentError, 'At least one of the arguments fromListId, toListId, positionInList, moveAfterId, or moveBeforeId is required') do subject end end @@ -71,6 +71,50 @@ end end + context 'when positionInList is given' do + let(:move_params) { { from_list_id: list1.id, to_list_id: list2.id, position_in_list: 0 } } + + context 'when fromListId and toListId are missing' do + let(:move_params) { { position_in_list: 0 } } + + it 'generates an error' do + expect_graphql_error_to_be_created(Gitlab::Graphql::Errors::ArgumentError, 'Both fromListId and toListId are required when positionInList is given') do + subject + end + end + end + + context 'when move_before_id is also given' do + let(:move_params) { { from_list_id: list1.id, to_list_id: list2.id, position_in_list: 0, move_before_id: 1 } } + + it 'generates an error' do + expect_graphql_error_to_be_created(Gitlab::Graphql::Errors::ArgumentError, 'positionInList is mutually exclusive with any of moveBeforeId or moveAfterId') do + subject + end + end + end + + context 'when move_after_id is also given' do + let(:move_params) { { from_list_id: list1.id, to_list_id: list2.id, position_in_list: 0, move_after_id: 1 } } + + it 'generates an error' do + expect_graphql_error_to_be_created(Gitlab::Graphql::Errors::ArgumentError, 'positionInList is mutually exclusive with any of moveBeforeId or moveAfterId') do + subject + end + end + end + + context 'when position_in_list is invalid' do + let(:move_params) { { from_list_id: list1.id, to_list_id: list2.id, position_in_list: -5 } } + + it 'generates an error' do + expect_graphql_error_to_be_created(Gitlab::Graphql::Errors::ArgumentError, "positionInList must be >= 0 or #{Boards::Issues::MoveService::LIST_END_POSITION}") do + subject + end + end + end + end + context 'when user have access to resources' do it 'moves and repositions issue' do subject diff --git a/spec/requests/api/graphql/mutations/boards/issues/issue_move_list_spec.rb b/spec/requests/api/graphql/mutations/boards/issues/issue_move_list_spec.rb index 46ec22e7ef84..06093e9f7c2e 100644 --- a/spec/requests/api/graphql/mutations/boards/issues/issue_move_list_spec.rb +++ b/spec/requests/api/graphql/mutations/boards/issues/issue_move_list_spec.rb @@ -100,6 +100,20 @@ expect(response_issue['labels']['edges'][0]['node']['title']).to eq(testing.title) end end + + context 'when moving an issue using position_in_list' do + let(:issue_move_params) { { from_list_id: list1.id, to_list_id: list2.id, position_in_list: 0 } } + + it 'repositions an issue' do + post_graphql_mutation(mutation(params), current_user: current_user) + + expect(response).to have_gitlab_http_status(:success) + response_issue = json_response['data'][mutation_result_identifier]['issue'] + expect(response_issue['iid']).to eq(issue1.iid.to_s) + expect(response_issue['labels']['edges'][0]['node']['title']).to eq(testing.title) + expect(response_issue['relativePosition']).to be < existing_issue1.relative_position + end + end end context 'when user has no access to resources' do diff --git a/spec/support/shared_examples/services/boards/issues_move_service_shared_examples.rb b/spec/support/shared_examples/services/boards/issues_move_service_shared_examples.rb index 1638e2fa86ad..162be24fe8f7 100644 --- a/spec/support/shared_examples/services/boards/issues_move_service_shared_examples.rb +++ b/spec/support/shared_examples/services/boards/issues_move_service_shared_examples.rb @@ -140,6 +140,40 @@ expect(issue2.reload.updated_at.change(usec: 0)).to eq updated_at2.change(usec: 0) end + context 'when moving to a specific list position' do + before do + [issue1, issue2, issue].each do |issue| + issue.move_to_end && issue.save! + end + end + + it 'moves issue to the top of the list' do + described_class.new(parent, user, params.merge({ position_in_list: 0 })).execute(issue) + + expect(issue.relative_position).to be < issue1.relative_position + end + + it 'moves issue to a position in the middle of the list' do + described_class.new(parent, user, params.merge({ position_in_list: 1 })).execute(issue) + + expect(issue.relative_position).to be_between(issue1.relative_position, issue2.relative_position) + end + + it 'moves issue to the bottom of the list' do + described_class.new(parent, user, params.merge({ position_in_list: -1 })).execute(issue1) + + expect(issue1.relative_position).to be > issue.relative_position + end + + context 'when given position is greater than number of issues in the list' do + it 'moves the issue to the bottom of the list' do + described_class.new(parent, user, params.merge({ position_in_list: 5 })).execute(issue1) + + expect(issue1.relative_position).to be > issue.relative_position + end + end + end + def reorder_issues(params, issues: []) issues.each do |issue| issue.move_to_end && issue.save! -- GitLab