From 38ac3939a813fe43756438d4544cb29797b12e85 Mon Sep 17 00:00:00 2001 From: Matt D'Angelo Date: Mon, 16 Jun 2025 15:58:14 +0930 Subject: [PATCH 01/12] Add GraphQL mutation to reorder work items Related to: https://gitlab.com/gitlab-org/gitlab/-/issues/536882 Changelog: added --- app/graphql/mutations/work_items/reorder.rb | 63 +++++++++++++++++ app/graphql/types/mutation_type.rb | 1 + app/services/work_items/reorder_service.rb | 26 +++++++ doc/api/graphql/reference/_index.md | 29 ++++++++ .../ee/work_items/reorder_service_spec.rb | 38 +++++++++++ .../mutations/work_items/reorder_spec.rb | 67 +++++++++++++++++++ spec/services/issues/reorder_service_spec.rb | 59 +++------------- .../work_items/reorder_service_spec.rb | 61 +++++++++++++++++ .../reorder_service_shared_examples.rb | 40 +++++++++++ 9 files changed, 335 insertions(+), 49 deletions(-) create mode 100644 app/graphql/mutations/work_items/reorder.rb create mode 100644 app/services/work_items/reorder_service.rb create mode 100644 ee/spec/services/ee/work_items/reorder_service_spec.rb create mode 100644 spec/graphql/mutations/work_items/reorder_spec.rb create mode 100644 spec/services/work_items/reorder_service_spec.rb create mode 100644 spec/support/shared_examples/reorder_service_shared_examples.rb diff --git a/app/graphql/mutations/work_items/reorder.rb b/app/graphql/mutations/work_items/reorder.rb new file mode 100644 index 00000000000000..bcb168bb88fd3b --- /dev/null +++ b/app/graphql/mutations/work_items/reorder.rb @@ -0,0 +1,63 @@ +# frozen_string_literal: true + +module Mutations + module WorkItems + class Reorder < BaseMutation + graphql_name 'workItemsReorder' + description 'Reorders a project level work item.' + + argument :id, + ::Types::GlobalIDType[::WorkItem], + required: true, + description: 'Global ID of the work item to be reordered.' + + argument :project_id, + ::Types::GlobalIDType[Project], + required: true, + description: 'Global ID of the project.' + + argument :move_before_id, + ::Types::GlobalIDType[::WorkItem], + required: false, + description: 'Global ID of a project’s issue that should be placed before the issue.' + + argument :move_after_id, + ::Types::GlobalIDType[::WorkItem], + required: false, + description: 'Global ID of a project’s issue that should be placed after the issue.' + + field :work_item, + ::Types::WorkItemType, + null: true, + description: 'Work item after mutation.' + + authorize :read_work_item + + def ready?(**args) + return super if args.slice(:move_after_id, :move_before_id).compact.present? + + raise Gitlab::Graphql::Errors::ArgumentError, + 'At least one of move_before_id and move_after_id are required' + end + + def resolve(**args) + project = authorized_find!(id: args[:project_id]) + work_item = authorized_find!(id: args[:id]) + + reorder_service_params = args + .slice(:move_before_id, :move_after_id) + .transform_values { |v| v&.model_id } + + service = ::WorkItems::ReorderService.new( + container: project, + current_user: current_user, + params: reorder_service_params + ) + + return { work_item: work_item, errors: [] } if service.execute(work_item) + + { work_item: work_item, errors: Array.wrap('Unable to reorder the work item.') } + end + end + end +end diff --git a/app/graphql/types/mutation_type.rb b/app/graphql/types/mutation_type.rb index 35a9b9ff6a06ca..bc5d314f733849 100644 --- a/app/graphql/types/mutation_type.rb +++ b/app/graphql/types/mutation_type.rb @@ -251,6 +251,7 @@ class MutationType < BaseObject mount_mutation Mutations::WorkItems::Hierarchy::AddChildrenItems, experiment: { milestone: '18.2' } mount_mutation Mutations::WorkItems::BulkUpdate, experiment: { milestone: '17.4' } mount_mutation Mutations::WorkItems::BulkMove, experiment: { milestone: '18.2' } + mount_mutation Mutations::WorkItems::Reorder, experiment: { milestone: '18.3' } mount_mutation Mutations::WorkItems::UserPreference::Update, experiment: { milestone: '17.10' } mount_mutation Mutations::Users::SavedReplies::Create mount_mutation Mutations::Users::SavedReplies::Update diff --git a/app/services/work_items/reorder_service.rb b/app/services/work_items/reorder_service.rb new file mode 100644 index 00000000000000..622f5dd0ebcc9f --- /dev/null +++ b/app/services/work_items/reorder_service.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +module WorkItems + class ReorderService < ::Issues::ReorderService + override :execute + def execute(work_item) + # Currently only supports project-level work items + return false unless work_item.project + return false unless can?(current_user, :update_work_item, work_item) + return false unless move_between_ids + + update(work_item, { move_between_ids: move_between_ids }) + end + + private + + override :update + def update(work_item, attrs) + ::WorkItems::UpdateService + .new(container: project, current_user: current_user, params: attrs) + .execute(work_item) + rescue ActiveRecord::RecordNotFound + false + end + end +end diff --git a/doc/api/graphql/reference/_index.md b/doc/api/graphql/reference/_index.md index 757644740e064f..d9a43abd827c0d 100644 --- a/doc/api/graphql/reference/_index.md +++ b/doc/api/graphql/reference/_index.md @@ -13767,6 +13767,35 @@ Input type: `workItemsHierarchyReorderInput` | `parentWorkItem` | [`WorkItem`](#workitem) | Work item's parent after mutation. | | `workItem` | [`WorkItem`](#workitem) | Work item after mutation. | +### `Mutation.workItemsReorder` + +Reorders a project level work item. + +{{< details >}} +**Introduced** in GitLab 18.2. +**Status**: Experiment. +{{< /details >}} + +Input type: `workItemsReorderInput` + +#### Arguments + +| Name | Type | Description | +| ---- | ---- | ----------- | +| `clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. | +| `id` | [`WorkItemID!`](#workitemid) | Global ID of the work item to be reordered. | +| `moveAfterId` | [`WorkItemID`](#workitemid) | Global ID of a project’s issue that should be placed after the issue. | +| `moveBeforeId` | [`WorkItemID`](#workitemid) | Global ID of a project’s issue that should be placed before the issue. | +| `projectId` | [`ProjectID!`](#projectid) | Global ID of the project. | + +#### Fields + +| Name | Type | Description | +| ---- | ---- | ----------- | +| `clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. | +| `errors` | [`[String!]!`](#string) | Errors encountered during the mutation. | +| `workItem` | [`WorkItem`](#workitem) | Work item after mutation. | + ### `Mutation.workspaceCreate` Input type: `WorkspaceCreateInput` diff --git a/ee/spec/services/ee/work_items/reorder_service_spec.rb b/ee/spec/services/ee/work_items/reorder_service_spec.rb new file mode 100644 index 00000000000000..0f6e557c1fa991 --- /dev/null +++ b/ee/spec/services/ee/work_items/reorder_service_spec.rb @@ -0,0 +1,38 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe WorkItems::ReorderService, feature_category: :team_planning do + let_it_be(:group) { create(:group) } + let_it_be(:user) { create_default(:user, developer_of: group) } + let_it_be(:project) { create(:project, namespace: group) } + + describe '#execute' do + let_it_be(:item1) { create(:work_item, :issue, project: project, relative_position: 10) } + let_it_be(:item2) { create(:work_item, :issue, project: project, relative_position: 20) } + + context 'with epics enabled' do + before do + stub_licensed_features(epics: true) + end + + context 'when ordering work items in a group' do + context 'with a non-project level work item' do + let_it_be(:non_project_work_item) do + create(:work_item, :group_level, namespace: group, relative_position: 10) + end + + it 'returns false' do + params = { move_after_id: item1.id, move_before_id: item2.id } + + expect(service(params).execute(non_project_work_item)).to be_falsey + end + end + end + end + end + + def service(params) + described_class.new(container: project, current_user: user, params: params) + end +end diff --git a/spec/graphql/mutations/work_items/reorder_spec.rb b/spec/graphql/mutations/work_items/reorder_spec.rb new file mode 100644 index 00000000000000..d147704df5e948 --- /dev/null +++ b/spec/graphql/mutations/work_items/reorder_spec.rb @@ -0,0 +1,67 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Mutations::WorkItems::Reorder, feature_category: :team_planning do + include GraphqlHelpers + using RSpec::Parameterized::TableSyntax + + let_it_be(:project) { create(:project) } + let_it_be(:current_user) { create(:user, developer_of: project) } + let_it_be(:item1, reload: true) { create(:work_item, :issue, project: project, relative_position: 10) } + let_it_be(:item2, reload: true) { create(:work_item, :issue, project: project, relative_position: 20) } + + subject(:mutation) { described_class.new(object: nil, context: query_context, field: nil) } + + describe '#ready?' do + context 'with invalid arguments' do + it 'raises an error' do + expect { mutation.ready? } + .to raise_error('At least one of move_before_id and move_after_id are required') + end + end + + context 'with valid arguments' do + where(:move_before, :move_after, :expected_result) do + nil | ref(:item2) | true + ref(:item2) | nil | true + end + + with_them do + specify do + ready_result = mutation.ready?(move_before_id: move_before&.to_gid, move_after_id: move_after&.to_gid) + + expect(ready_result).to be(expected_result) + end + end + end + end + + describe '#resolve' do + context 'when moving it before other item' do + specify do + result = mutation.resolve( + project_id: project.to_gid, + id: item1.to_gid, + move_before_id: item2.to_gid + ) + + expect(result[:errors]).to be_empty + expect(result[:work_item]&.relative_position).to be > item2.relative_position + end + end + + context 'when moving it after other item' do + specify do + result = mutation.resolve( + project_id: project.to_gid, + id: item1.to_gid, + move_after_id: item2.to_gid + ) + + expect(result[:errors]).to be_empty + expect(result[:work_item]&.relative_position).to be < item2.relative_position + end + end + end +end diff --git a/spec/services/issues/reorder_service_spec.rb b/spec/services/issues/reorder_service_spec.rb index b98d23e0f7f95c..b02abc84125ca6 100644 --- a/spec/services/issues/reorder_service_spec.rb +++ b/spec/services/issues/reorder_service_spec.rb @@ -7,56 +7,17 @@ let_it_be(:group) { create(:group) } let_it_be(:project, reload: true) { create(:project, namespace: group) } - shared_examples 'issues reorder service' do - context 'when reordering issues' do - it 'returns false with no params' do - expect(service({}).execute(issue1)).to be_falsey - end - - it 'returns false with both invalid params' do - params = { move_after_id: nil, move_before_id: non_existing_record_id } - - expect(service(params).execute(issue1)).to be_falsey - end - - it 'sorts issues' do - params = { move_after_id: issue2.id, move_before_id: issue3.id } - - service(params).execute(issue1) - - expect(issue1.relative_position) - .to be_between(issue2.relative_position, issue3.relative_position) - end - - it 'sorts issues if only given one neighbour, on the left' do - params = { move_before_id: issue3.id } - - service(params).execute(issue1) - - expect(issue1.relative_position).to be > issue3.relative_position - end - - it 'sorts issues if only given one neighbour, on the right' do - params = { move_after_id: issue1.id } - - service(params).execute(issue3) - - expect(issue3.relative_position).to be < issue1.relative_position - end - end - end - describe '#execute' do - let_it_be(:issue1, reload: true) { create(:issue, project: project, relative_position: 10) } - let_it_be(:issue2) { create(:issue, project: project, relative_position: 20) } - let_it_be(:issue3, reload: true) { create(:issue, project: project, relative_position: 30) } + let_it_be(:item1, reload: true) { create(:issue, project: project, relative_position: 10) } + let_it_be(:item2) { create(:issue, project: project, relative_position: 20) } + let_it_be(:item3, reload: true) { create(:issue, project: project, relative_position: 30) } context 'when ordering issues in a project' do before do project.add_developer(user) end - it_behaves_like 'issues reorder service' + it_behaves_like 'reorder service' end context 'when ordering issues in a group' do @@ -64,21 +25,21 @@ group.add_developer(user) end - it_behaves_like 'issues reorder service' + it_behaves_like 'reorder service' context 'when ordering in a group issue list' do - let(:params) { { move_after_id: issue2.id, move_before_id: issue3.id } } + let(:params) { { move_after_id: item2.id, move_before_id: item3.id } } subject { service(params) } it 'sorts issues' do project2 = create(:project, namespace: group) - issue4 = create(:issue, project: project2) + item4 = create(:issue, project: project2) - subject.execute(issue4) + subject.execute(item4) - expect(issue4.relative_position) - .to be_between(issue2.relative_position, issue3.relative_position) + expect(item4.relative_position) + .to be_between(item2.relative_position, item3.relative_position) end end end diff --git a/spec/services/work_items/reorder_service_spec.rb b/spec/services/work_items/reorder_service_spec.rb new file mode 100644 index 00000000000000..3f9e4ec10d52c1 --- /dev/null +++ b/spec/services/work_items/reorder_service_spec.rb @@ -0,0 +1,61 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe WorkItems::ReorderService, feature_category: :team_planning do + let_it_be(:user) { create_default(:user) } + let_it_be(:group) { create(:group) } + let_it_be(:project, reload: true) { create(:project, namespace: group) } + + describe '#execute' do + let_it_be(:item1, reload: true) { create(:work_item, :issue, project: project, relative_position: 10) } + let_it_be(:item2) { create(:work_item, :issue, project: project, relative_position: 20) } + let_it_be(:item3, reload: true) { create(:work_item, :issue, project: project, relative_position: 30) } + + context 'when ordering work items in a project' do + before do + project.add_developer(user) + end + + it_behaves_like 'reorder service' + end + + context 'when ordering work items in a group' do + before_all do + group.add_developer(user) + end + + it_behaves_like 'reorder service' + + context 'when ordering in a group work items list' do + let(:params) { { move_after_id: item2.id, move_before_id: item3.id } } + + it 'sorts work items' do + project2 = create(:project, namespace: group) + item4 = create(:work_item, :issue, project: project2) + + service(params).execute(item4) + + expect(item4.relative_position) + .to be_between(item2.relative_position, item3.relative_position) + end + end + + context 'with a non-project level work item' do + let_it_be(:non_project_work_item) do + create(:work_item, :group_level, namespace: group, relative_position: 10) + end + + it 'returns false' do + params = { move_after_id: item2.id, move_before_id: item3.id } + + expect(service(params).execute(non_project_work_item)).to be_falsey + end + end + end + end + + def service(params) + described_class.new(container: project, current_user: user, params: params) + end +end diff --git a/spec/support/shared_examples/reorder_service_shared_examples.rb b/spec/support/shared_examples/reorder_service_shared_examples.rb new file mode 100644 index 00000000000000..396fcec837a1de --- /dev/null +++ b/spec/support/shared_examples/reorder_service_shared_examples.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'reorder service' do + context 'when reordering items' do + it 'returns false with no params' do + expect(service({}).execute(item1)).to be_falsey + end + + it 'returns false with both invalid params' do + params = { move_after_id: nil, move_before_id: non_existing_record_id } + + expect(service(params).execute(item1)).to be_falsey + end + + it 'sorts items' do + params = { move_after_id: item2.id, move_before_id: item3.id } + + service(params).execute(item1) + + expect(item1.relative_position) + .to be_between(item2.relative_position, item3.relative_position) + end + + it 'sorts items if only given one neighbour, on the left' do + params = { move_before_id: item3.id } + + service(params).execute(item1) + + expect(item1.relative_position).to be > item3.relative_position + end + + it 'sorts items if only given one neighbour, on the right' do + params = { move_after_id: item1.id } + + service(params).execute(item3) + + expect(item3.relative_position).to be < item1.relative_position + end + end +end -- GitLab From 7fed84244a9510cfb5c111cb615910cd23e2096c Mon Sep 17 00:00:00 2001 From: Kassio Borges Date: Thu, 3 Jul 2025 12:30:35 +0200 Subject: [PATCH 02/12] Update docs and improve a test --- app/graphql/mutations/work_items/reorder.rb | 5 +++-- spec/graphql/mutations/work_items/reorder_spec.rb | 6 +++--- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/app/graphql/mutations/work_items/reorder.rb b/app/graphql/mutations/work_items/reorder.rb index bcb168bb88fd3b..b183c96ae827a9 100644 --- a/app/graphql/mutations/work_items/reorder.rb +++ b/app/graphql/mutations/work_items/reorder.rb @@ -19,18 +19,19 @@ class Reorder < BaseMutation argument :move_before_id, ::Types::GlobalIDType[::WorkItem], required: false, - description: 'Global ID of a project’s issue that should be placed before the issue.' + description: 'Global ID of a project’s work item that should be placed before the work item.' argument :move_after_id, ::Types::GlobalIDType[::WorkItem], required: false, - description: 'Global ID of a project’s issue that should be placed after the issue.' + description: 'Global ID of a project’s work item that should be placed after the work item.' field :work_item, ::Types::WorkItemType, null: true, description: 'Work item after mutation.' + # authorize :update_work_item authorize :read_work_item def ready?(**args) diff --git a/spec/graphql/mutations/work_items/reorder_spec.rb b/spec/graphql/mutations/work_items/reorder_spec.rb index d147704df5e948..1c782d10ff462d 100644 --- a/spec/graphql/mutations/work_items/reorder_spec.rb +++ b/spec/graphql/mutations/work_items/reorder_spec.rb @@ -42,12 +42,12 @@ specify do result = mutation.resolve( project_id: project.to_gid, - id: item1.to_gid, - move_before_id: item2.to_gid + id: item2.to_gid, + move_before_id: item1.to_gid ) expect(result[:errors]).to be_empty - expect(result[:work_item]&.relative_position).to be > item2.relative_position + expect(result[:work_item]&.relative_position).to be > item1.relative_position end end -- GitLab From 17ff95938bd9b8aa394e536cb4e7aa88cbd35743 Mon Sep 17 00:00:00 2001 From: Kassio Borges Date: Thu, 3 Jul 2025 12:46:30 +0200 Subject: [PATCH 03/12] Use better authorization --- app/graphql/mutations/work_items/reorder.rb | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/app/graphql/mutations/work_items/reorder.rb b/app/graphql/mutations/work_items/reorder.rb index b183c96ae827a9..c3ee23b6b7aaed 100644 --- a/app/graphql/mutations/work_items/reorder.rb +++ b/app/graphql/mutations/work_items/reorder.rb @@ -31,8 +31,7 @@ class Reorder < BaseMutation null: true, description: 'Work item after mutation.' - # authorize :update_work_item - authorize :read_work_item + authorize :update_work_item def ready?(**args) return super if args.slice(:move_after_id, :move_before_id).compact.present? @@ -42,7 +41,7 @@ def ready?(**args) end def resolve(**args) - project = authorized_find!(id: args[:project_id]) + project = find_object(id: args[:project_id]) work_item = authorized_find!(id: args[:id]) reorder_service_params = args -- GitLab From ae970e9811fc719a4e9ad7e126a3a43c898e4891 Mon Sep 17 00:00:00 2001 From: Kassio Borges Date: Thu, 3 Jul 2025 13:09:06 +0200 Subject: [PATCH 04/12] Update graphql docs --- doc/api/graphql/reference/_index.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/api/graphql/reference/_index.md b/doc/api/graphql/reference/_index.md index d9a43abd827c0d..ad3bd7fd4a3678 100644 --- a/doc/api/graphql/reference/_index.md +++ b/doc/api/graphql/reference/_index.md @@ -13784,8 +13784,8 @@ Input type: `workItemsReorderInput` | ---- | ---- | ----------- | | `clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. | | `id` | [`WorkItemID!`](#workitemid) | Global ID of the work item to be reordered. | -| `moveAfterId` | [`WorkItemID`](#workitemid) | Global ID of a project’s issue that should be placed after the issue. | -| `moveBeforeId` | [`WorkItemID`](#workitemid) | Global ID of a project’s issue that should be placed before the issue. | +| `moveAfterId` | [`WorkItemID`](#workitemid) | Global ID of a project’s work item that should be placed after the work item. | +| `moveBeforeId` | [`WorkItemID`](#workitemid) | Global ID of a project’s work item that should be placed before the work item. | | `projectId` | [`ProjectID!`](#projectid) | Global ID of the project. | #### Fields -- GitLab From 32ea2bea2ea1249d419644d75f1005364080d631 Mon Sep 17 00:00:00 2001 From: Kassio Borges Date: Fri, 4 Jul 2025 10:13:58 +0200 Subject: [PATCH 05/12] Add missing test --- spec/graphql/mutations/work_items/reorder_spec.rb | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/spec/graphql/mutations/work_items/reorder_spec.rb b/spec/graphql/mutations/work_items/reorder_spec.rb index 1c782d10ff462d..94841050609833 100644 --- a/spec/graphql/mutations/work_items/reorder_spec.rb +++ b/spec/graphql/mutations/work_items/reorder_spec.rb @@ -38,6 +38,19 @@ end describe '#resolve' do + context 'when it fails to move' do + specify do + result = mutation.resolve( + project_id: project.to_gid, + id: item2.to_gid, + move_before_id: GlobalID.parse("gid://gitlab/WorkItem/#{non_existing_record_id}") + ) + + expect(result[:errors]).to eq(["Unable to reorder the work item."]) + expect(result[:work_item]&.relative_position).to be(item2.relative_position) + end + end + context 'when moving it before other item' do specify do result = mutation.resolve( -- GitLab From df8e1d8fac8e3928ecf6edd54ff68d2b1fffee0d Mon Sep 17 00:00:00 2001 From: Kassio Borges Date: Fri, 4 Jul 2025 10:15:51 +0200 Subject: [PATCH 06/12] Apply suggestions --- doc/api/graphql/reference/_index.md | 2 +- spec/graphql/mutations/work_items/reorder_spec.rb | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/doc/api/graphql/reference/_index.md b/doc/api/graphql/reference/_index.md index ad3bd7fd4a3678..4ede9191025999 100644 --- a/doc/api/graphql/reference/_index.md +++ b/doc/api/graphql/reference/_index.md @@ -13772,7 +13772,7 @@ Input type: `workItemsHierarchyReorderInput` Reorders a project level work item. {{< details >}} -**Introduced** in GitLab 18.2. +**Introduced** in GitLab 18.3. **Status**: Experiment. {{< /details >}} diff --git a/spec/graphql/mutations/work_items/reorder_spec.rb b/spec/graphql/mutations/work_items/reorder_spec.rb index 94841050609833..61f046817ecb94 100644 --- a/spec/graphql/mutations/work_items/reorder_spec.rb +++ b/spec/graphql/mutations/work_items/reorder_spec.rb @@ -55,12 +55,12 @@ specify do result = mutation.resolve( project_id: project.to_gid, - id: item2.to_gid, - move_before_id: item1.to_gid + id: item1.to_gid, + move_before_id: item2.to_gid ) expect(result[:errors]).to be_empty - expect(result[:work_item]&.relative_position).to be > item1.relative_position + expect(result[:work_item].relative_position).to be > item2.relative_position end end @@ -68,12 +68,12 @@ specify do result = mutation.resolve( project_id: project.to_gid, - id: item1.to_gid, - move_after_id: item2.to_gid + id: item2.to_gid, + move_after_id: item1.to_gid ) expect(result[:errors]).to be_empty - expect(result[:work_item]&.relative_position).to be < item2.relative_position + expect(result[:work_item].relative_position).to be < item1.relative_position end end end -- GitLab From 375f4d2547ad08aabe2f6433db66c812142fa6d0 Mon Sep 17 00:00:00 2001 From: Kassio Borges Date: Tue, 15 Jul 2025 16:49:56 +0200 Subject: [PATCH 07/12] Remove unused inheritance --- app/graphql/mutations/work_items/reorder.rb | 8 +--- app/services/work_items/reorder_service.rb | 47 ++++++++++++++----- .../work_items/reorder_service_spec.rb | 20 ++------ .../reorder_service_shared_examples.rb | 23 ++++----- 4 files changed, 52 insertions(+), 46 deletions(-) diff --git a/app/graphql/mutations/work_items/reorder.rb b/app/graphql/mutations/work_items/reorder.rb index c3ee23b6b7aaed..6823f623b1e1f0 100644 --- a/app/graphql/mutations/work_items/reorder.rb +++ b/app/graphql/mutations/work_items/reorder.rb @@ -48,15 +48,11 @@ def resolve(**args) .slice(:move_before_id, :move_after_id) .transform_values { |v| v&.model_id } - service = ::WorkItems::ReorderService.new( + ::WorkItems::ReorderService.new( container: project, current_user: current_user, params: reorder_service_params - ) - - return { work_item: work_item, errors: [] } if service.execute(work_item) - - { work_item: work_item, errors: Array.wrap('Unable to reorder the work item.') } + ).execute(work_item) end end end diff --git a/app/services/work_items/reorder_service.rb b/app/services/work_items/reorder_service.rb index 622f5dd0ebcc9f..117077545ff12e 100644 --- a/app/services/work_items/reorder_service.rb +++ b/app/services/work_items/reorder_service.rb @@ -1,26 +1,47 @@ # frozen_string_literal: true module WorkItems - class ReorderService < ::Issues::ReorderService - override :execute + class ReorderService + include Gitlab::Utils::StrongMemoize + + def initialize(container:, current_user:, params:) + @container = container + @current_user = current_user + @params = params + end + def execute(work_item) - # Currently only supports project-level work items - return false unless work_item.project - return false unless can?(current_user, :update_work_item, work_item) + return false unless current_user.can?(:update_work_item, work_item) return false unless move_between_ids - update(work_item, { move_between_ids: move_between_ids }) + ::WorkItems::UpdateService.new( + container: container, + current_user: current_user, + params: { move_between_ids: move_between_ids } + ).execute(work_item) + + ServiceResponse.success(payload: { + work_item: work_item, + errors: [] + }) + rescue ActiveRecord::RecordNotFound + ServiceResponse.new(status: :error, payload: { + work_item: work_item, + errors: ['Unable to reorder the work item.'] + }) end private - override :update - def update(work_item, attrs) - ::WorkItems::UpdateService - .new(container: project, current_user: current_user, params: attrs) - .execute(work_item) - rescue ActiveRecord::RecordNotFound - false + attr_reader :container, :current_user, :params + + def move_between_ids + params + .values_at(:move_before_id, :move_after_id) + .map(&:to_i) + .map { |id| id > 0 ? id : nil } + .then { |ids| ids.any? ? ids : nil } end + strong_memoize_attr :move_between_ids end end diff --git a/spec/services/work_items/reorder_service_spec.rb b/spec/services/work_items/reorder_service_spec.rb index 3f9e4ec10d52c1..005dce303761d2 100644 --- a/spec/services/work_items/reorder_service_spec.rb +++ b/spec/services/work_items/reorder_service_spec.rb @@ -34,22 +34,10 @@ project2 = create(:project, namespace: group) item4 = create(:work_item, :issue, project: project2) - service(params).execute(item4) - - expect(item4.relative_position) - .to be_between(item2.relative_position, item3.relative_position) - end - end - - context 'with a non-project level work item' do - let_it_be(:non_project_work_item) do - create(:work_item, :group_level, namespace: group, relative_position: 10) - end - - it 'returns false' do - params = { move_after_id: item2.id, move_before_id: item3.id } - - expect(service(params).execute(non_project_work_item)).to be_falsey + expect { service(params).execute(item4) } + .to change { item4.relative_position }.to( + be_between(item2.relative_position, item3.relative_position) + ) end end end diff --git a/spec/support/shared_examples/reorder_service_shared_examples.rb b/spec/support/shared_examples/reorder_service_shared_examples.rb index 396fcec837a1de..ff284af2889727 100644 --- a/spec/support/shared_examples/reorder_service_shared_examples.rb +++ b/spec/support/shared_examples/reorder_service_shared_examples.rb @@ -3,38 +3,39 @@ RSpec.shared_examples 'reorder service' do context 'when reordering items' do it 'returns false with no params' do - expect(service({}).execute(item1)).to be_falsey + expect { service({}).execute(item1) } + .not_to change { item1.relative_position } end it 'returns false with both invalid params' do params = { move_after_id: nil, move_before_id: non_existing_record_id } - expect(service(params).execute(item1)).to be_falsey + expect { service(params).execute(item1) } + .not_to change { item1.relative_position } end it 'sorts items' do params = { move_after_id: item2.id, move_before_id: item3.id } - service(params).execute(item1) - - expect(item1.relative_position) + expect { service(params).execute(item1) } + .to change { item1.relative_position } .to be_between(item2.relative_position, item3.relative_position) end it 'sorts items if only given one neighbour, on the left' do params = { move_before_id: item3.id } - service(params).execute(item1) - - expect(item1.relative_position).to be > item3.relative_position + expect { service(params).execute(item1) } + .to change { item1.relative_position } + .to be > item3.relative_position end it 'sorts items if only given one neighbour, on the right' do params = { move_after_id: item1.id } - service(params).execute(item3) - - expect(item3.relative_position).to be < item1.relative_position + expect { service(params).execute(item3) } + .to change { item3.relative_position } + .to be < item1.relative_position end end end -- GitLab From 2983d9a882950606222ff2ca071b976ac20cb7d1 Mon Sep 17 00:00:00 2001 From: Kassio Borges Date: Tue, 15 Jul 2025 16:52:32 +0200 Subject: [PATCH 08/12] Simplify mutation code --- app/graphql/mutations/work_items/reorder.rb | 12 +++++------- spec/graphql/mutations/work_items/reorder_spec.rb | 11 +++++++---- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/app/graphql/mutations/work_items/reorder.rb b/app/graphql/mutations/work_items/reorder.rb index 6823f623b1e1f0..8b164108750733 100644 --- a/app/graphql/mutations/work_items/reorder.rb +++ b/app/graphql/mutations/work_items/reorder.rb @@ -19,12 +19,14 @@ class Reorder < BaseMutation argument :move_before_id, ::Types::GlobalIDType[::WorkItem], required: false, - description: 'Global ID of a project’s work item that should be placed before the work item.' + description: 'Global ID of a project’s work item that should be placed before the work item.', + prepare: ->(id, _ctx) { id.map(&:model_id) } argument :move_after_id, ::Types::GlobalIDType[::WorkItem], required: false, - description: 'Global ID of a project’s work item that should be placed after the work item.' + description: 'Global ID of a project’s work item that should be placed after the work item.', + prepare: ->(id, _ctx) { id.map(&:model_id) } field :work_item, ::Types::WorkItemType, @@ -44,14 +46,10 @@ def resolve(**args) project = find_object(id: args[:project_id]) work_item = authorized_find!(id: args[:id]) - reorder_service_params = args - .slice(:move_before_id, :move_after_id) - .transform_values { |v| v&.model_id } - ::WorkItems::ReorderService.new( container: project, current_user: current_user, - params: reorder_service_params + params: args.slice(:move_before_id, :move_after_id) ).execute(work_item) end end diff --git a/spec/graphql/mutations/work_items/reorder_spec.rb b/spec/graphql/mutations/work_items/reorder_spec.rb index 61f046817ecb94..6920a502047736 100644 --- a/spec/graphql/mutations/work_items/reorder_spec.rb +++ b/spec/graphql/mutations/work_items/reorder_spec.rb @@ -29,7 +29,10 @@ with_them do specify do - ready_result = mutation.ready?(move_before_id: move_before&.to_gid, move_after_id: move_after&.to_gid) + ready_result = mutation.ready?( + move_before_id: move_before&.id, + move_after_id: move_after&.id + ) expect(ready_result).to be(expected_result) end @@ -43,7 +46,7 @@ result = mutation.resolve( project_id: project.to_gid, id: item2.to_gid, - move_before_id: GlobalID.parse("gid://gitlab/WorkItem/#{non_existing_record_id}") + move_before_id: non_existing_record_id ) expect(result[:errors]).to eq(["Unable to reorder the work item."]) @@ -56,7 +59,7 @@ result = mutation.resolve( project_id: project.to_gid, id: item1.to_gid, - move_before_id: item2.to_gid + move_before_id: item2.id ) expect(result[:errors]).to be_empty @@ -69,7 +72,7 @@ result = mutation.resolve( project_id: project.to_gid, id: item2.to_gid, - move_after_id: item1.to_gid + move_after_id: item1.id ) expect(result[:errors]).to be_empty -- GitLab From d69c3392a9354c0a438bda6fb6678cdd087d0596 Mon Sep 17 00:00:00 2001 From: Kassio Borges Date: Wed, 16 Jul 2025 14:35:34 +0200 Subject: [PATCH 09/12] Use the work item project instead of passing the container --- app/graphql/mutations/work_items/reorder.rb | 7 -- app/services/work_items/reorder_service.rb | 48 ++++++++++--- doc/api/graphql/reference/_index.md | 1 - .../ee/work_items/reorder_service_spec.rb | 28 +++++--- .../mutations/work_items/reorder_spec.rb | 2 +- spec/services/issues/reorder_service_spec.rb | 59 +++++++++++++--- .../work_items/reorder_service_spec.rb | 31 +++++---- .../reorder_service_shared_examples.rb | 68 +++++++++++++------ 8 files changed, 169 insertions(+), 75 deletions(-) diff --git a/app/graphql/mutations/work_items/reorder.rb b/app/graphql/mutations/work_items/reorder.rb index 8b164108750733..d64218243f0414 100644 --- a/app/graphql/mutations/work_items/reorder.rb +++ b/app/graphql/mutations/work_items/reorder.rb @@ -11,11 +11,6 @@ class Reorder < BaseMutation required: true, description: 'Global ID of the work item to be reordered.' - argument :project_id, - ::Types::GlobalIDType[Project], - required: true, - description: 'Global ID of the project.' - argument :move_before_id, ::Types::GlobalIDType[::WorkItem], required: false, @@ -43,11 +38,9 @@ def ready?(**args) end def resolve(**args) - project = find_object(id: args[:project_id]) work_item = authorized_find!(id: args[:id]) ::WorkItems::ReorderService.new( - container: project, current_user: current_user, params: args.slice(:move_before_id, :move_after_id) ).execute(work_item) diff --git a/app/services/work_items/reorder_service.rb b/app/services/work_items/reorder_service.rb index 117077545ff12e..0a7d92247aadc8 100644 --- a/app/services/work_items/reorder_service.rb +++ b/app/services/work_items/reorder_service.rb @@ -4,37 +4,63 @@ module WorkItems class ReorderService include Gitlab::Utils::StrongMemoize - def initialize(container:, current_user:, params:) - @container = container + def initialize(current_user:, params:) @current_user = current_user @params = params end def execute(work_item) - return false unless current_user.can?(:update_work_item, work_item) - return false unless move_between_ids + return unauthorized_error(work_item) unless current_user.can?(:update_work_item, work_item) + return non_project_work_item_error(work_item) if work_item.project.blank? + return missing_arguments_error(work_item) unless move_between_ids ::WorkItems::UpdateService.new( - container: container, + container: work_item.project, current_user: current_user, params: { move_between_ids: move_between_ids } ).execute(work_item) + success(work_item) + rescue ActiveRecord::RecordNotFound + error(work_item, message: "Work item not found") + end + + private + + attr_reader :current_user, :params + + def success(work_item) ServiceResponse.success(payload: { work_item: work_item, errors: [] }) - rescue ActiveRecord::RecordNotFound + end + + def non_project_work_item_error(work_item) + error( + work_item, + message: 'Only project work items are supported at this moment' + ) + end + + def unauthorized_error(work_item) + error( + work_item, + message: "You don't have permissions to update this work item" + ) + end + + def missing_arguments_error(work_item) + error(work_item, message: 'At least one of move_before_id or move_after_id is required') + end + + def error(work_item, message: nil) ServiceResponse.new(status: :error, payload: { work_item: work_item, - errors: ['Unable to reorder the work item.'] + errors: Array.wrap(message) }) end - private - - attr_reader :container, :current_user, :params - def move_between_ids params .values_at(:move_before_id, :move_after_id) diff --git a/doc/api/graphql/reference/_index.md b/doc/api/graphql/reference/_index.md index 4ede9191025999..d7ae9cf2c7e09a 100644 --- a/doc/api/graphql/reference/_index.md +++ b/doc/api/graphql/reference/_index.md @@ -13786,7 +13786,6 @@ Input type: `workItemsReorderInput` | `id` | [`WorkItemID!`](#workitemid) | Global ID of the work item to be reordered. | | `moveAfterId` | [`WorkItemID`](#workitemid) | Global ID of a project’s work item that should be placed after the work item. | | `moveBeforeId` | [`WorkItemID`](#workitemid) | Global ID of a project’s work item that should be placed before the work item. | -| `projectId` | [`ProjectID!`](#projectid) | Global ID of the project. | #### Fields diff --git a/ee/spec/services/ee/work_items/reorder_service_spec.rb b/ee/spec/services/ee/work_items/reorder_service_spec.rb index 0f6e557c1fa991..069b2b89ef224f 100644 --- a/ee/spec/services/ee/work_items/reorder_service_spec.rb +++ b/ee/spec/services/ee/work_items/reorder_service_spec.rb @@ -10,6 +10,18 @@ describe '#execute' do let_it_be(:item1) { create(:work_item, :issue, project: project, relative_position: 10) } let_it_be(:item2) { create(:work_item, :issue, project: project, relative_position: 20) } + let_it_be(:non_project_work_item) do + create(:work_item, :group_level, namespace: group, relative_position: 10) + end + + let(:work_item) { non_project_work_item } + let(:params) { {} } + + subject(:service_result) do + described_class + .new(current_user: user, params: params) + .execute(work_item) + end context 'with epics enabled' do before do @@ -18,21 +30,17 @@ context 'when ordering work items in a group' do context 'with a non-project level work item' do - let_it_be(:non_project_work_item) do - create(:work_item, :group_level, namespace: group, relative_position: 10) - end + let(:params) { { move_after_id: item1.id, move_before_id: item2.id } } - it 'returns false' do - params = { move_after_id: item1.id, move_before_id: item2.id } + it 'does not update the position' do + expect { service_result } + .not_to change { non_project_work_item.relative_position } - expect(service(params).execute(non_project_work_item)).to be_falsey + expect(service_result[:errors]) + .to eq(["Only project work items are supported at this moment"]) end end end end end - - def service(params) - described_class.new(container: project, current_user: user, params: params) - end end diff --git a/spec/graphql/mutations/work_items/reorder_spec.rb b/spec/graphql/mutations/work_items/reorder_spec.rb index 6920a502047736..147227d8efa173 100644 --- a/spec/graphql/mutations/work_items/reorder_spec.rb +++ b/spec/graphql/mutations/work_items/reorder_spec.rb @@ -49,7 +49,7 @@ move_before_id: non_existing_record_id ) - expect(result[:errors]).to eq(["Unable to reorder the work item."]) + expect(result[:errors]).to eq(["Work item not found"]) expect(result[:work_item]&.relative_position).to be(item2.relative_position) end end diff --git a/spec/services/issues/reorder_service_spec.rb b/spec/services/issues/reorder_service_spec.rb index b02abc84125ca6..b98d23e0f7f95c 100644 --- a/spec/services/issues/reorder_service_spec.rb +++ b/spec/services/issues/reorder_service_spec.rb @@ -7,17 +7,56 @@ let_it_be(:group) { create(:group) } let_it_be(:project, reload: true) { create(:project, namespace: group) } + shared_examples 'issues reorder service' do + context 'when reordering issues' do + it 'returns false with no params' do + expect(service({}).execute(issue1)).to be_falsey + end + + it 'returns false with both invalid params' do + params = { move_after_id: nil, move_before_id: non_existing_record_id } + + expect(service(params).execute(issue1)).to be_falsey + end + + it 'sorts issues' do + params = { move_after_id: issue2.id, move_before_id: issue3.id } + + service(params).execute(issue1) + + expect(issue1.relative_position) + .to be_between(issue2.relative_position, issue3.relative_position) + end + + it 'sorts issues if only given one neighbour, on the left' do + params = { move_before_id: issue3.id } + + service(params).execute(issue1) + + expect(issue1.relative_position).to be > issue3.relative_position + end + + it 'sorts issues if only given one neighbour, on the right' do + params = { move_after_id: issue1.id } + + service(params).execute(issue3) + + expect(issue3.relative_position).to be < issue1.relative_position + end + end + end + describe '#execute' do - let_it_be(:item1, reload: true) { create(:issue, project: project, relative_position: 10) } - let_it_be(:item2) { create(:issue, project: project, relative_position: 20) } - let_it_be(:item3, reload: true) { create(:issue, project: project, relative_position: 30) } + let_it_be(:issue1, reload: true) { create(:issue, project: project, relative_position: 10) } + let_it_be(:issue2) { create(:issue, project: project, relative_position: 20) } + let_it_be(:issue3, reload: true) { create(:issue, project: project, relative_position: 30) } context 'when ordering issues in a project' do before do project.add_developer(user) end - it_behaves_like 'reorder service' + it_behaves_like 'issues reorder service' end context 'when ordering issues in a group' do @@ -25,21 +64,21 @@ group.add_developer(user) end - it_behaves_like 'reorder service' + it_behaves_like 'issues reorder service' context 'when ordering in a group issue list' do - let(:params) { { move_after_id: item2.id, move_before_id: item3.id } } + let(:params) { { move_after_id: issue2.id, move_before_id: issue3.id } } subject { service(params) } it 'sorts issues' do project2 = create(:project, namespace: group) - item4 = create(:issue, project: project2) + issue4 = create(:issue, project: project2) - subject.execute(item4) + subject.execute(issue4) - expect(item4.relative_position) - .to be_between(item2.relative_position, item3.relative_position) + expect(issue4.relative_position) + .to be_between(issue2.relative_position, issue3.relative_position) end end end diff --git a/spec/services/work_items/reorder_service_spec.rb b/spec/services/work_items/reorder_service_spec.rb index 005dce303761d2..9dd74a5aeff812 100644 --- a/spec/services/work_items/reorder_service_spec.rb +++ b/spec/services/work_items/reorder_service_spec.rb @@ -9,9 +9,18 @@ describe '#execute' do let_it_be(:item1, reload: true) { create(:work_item, :issue, project: project, relative_position: 10) } - let_it_be(:item2) { create(:work_item, :issue, project: project, relative_position: 20) } + let_it_be(:item2, reload: true) { create(:work_item, :issue, project: project, relative_position: 20) } let_it_be(:item3, reload: true) { create(:work_item, :issue, project: project, relative_position: 30) } + let(:work_item) { item1 } + let(:params) { {} } + + subject(:service_result) do + described_class + .new(current_user: user, params: params) + .execute(work_item) + end + context 'when ordering work items in a project' do before do project.add_developer(user) @@ -27,23 +36,19 @@ it_behaves_like 'reorder service' - context 'when ordering in a group work items list' do + context 'when ordering work items from other namespaces' do + let(:project2) { create(:project) } + let(:work_item) { create(:work_item, :issue, project: project2) } let(:params) { { move_after_id: item2.id, move_before_id: item3.id } } - it 'sorts work items' do - project2 = create(:project, namespace: group) - item4 = create(:work_item, :issue, project: project2) + it 'does not reorder' do + expect { service_result } + .not_to change { work_item.relative_position } - expect { service(params).execute(item4) } - .to change { item4.relative_position }.to( - be_between(item2.relative_position, item3.relative_position) - ) + expect(service_result[:errors]) + .to eq(["You don't have permissions to update this work item"]) end end end end - - def service(params) - described_class.new(container: project, current_user: user, params: params) - end end diff --git a/spec/support/shared_examples/reorder_service_shared_examples.rb b/spec/support/shared_examples/reorder_service_shared_examples.rb index ff284af2889727..e9a15c3af2fc33 100644 --- a/spec/support/shared_examples/reorder_service_shared_examples.rb +++ b/spec/support/shared_examples/reorder_service_shared_examples.rb @@ -2,40 +2,64 @@ RSpec.shared_examples 'reorder service' do context 'when reordering items' do - it 'returns false with no params' do - expect { service({}).execute(item1) } - .not_to change { item1.relative_position } + context 'when no params are passed' do + let(:params) { {} } + + it 'does not reorder' do + expect { service_result } + .not_to change { work_item.relative_position } + + expect(service_result[:errors]) + .to eq(["At least one of move_before_id or move_after_id is required"]) + end end - it 'returns false with both invalid params' do - params = { move_after_id: nil, move_before_id: non_existing_record_id } + context 'with both invalid params' do + let(:params) { { move_after_id: nil, move_before_id: non_existing_record_id } } + + it 'does not reorder' do + expect { service_result } + .not_to change { work_item.relative_position } - expect { service(params).execute(item1) } - .not_to change { item1.relative_position } + expect(service_result[:errors]).to eq(["Work item not found"]) + end end - it 'sorts items' do - params = { move_after_id: item2.id, move_before_id: item3.id } + context 'with valid params' do + let(:params) { { move_after_id: item2.id, move_before_id: item3.id } } - expect { service(params).execute(item1) } - .to change { item1.relative_position } - .to be_between(item2.relative_position, item3.relative_position) + it 'sorts items' do + expect { service_result } + .to change { work_item.relative_position }.to be_between( + item2.relative_position, + item3.relative_position + ) + + expect(service_result[:errors]).to be_empty + end end - it 'sorts items if only given one neighbour, on the left' do - params = { move_before_id: item3.id } + context 'when only move_before_id is given' do + let(:params) { { move_before_id: item3.id } } + + it 'sorts items if only given one neighbour, on the left' do + expect { service_result } + .to change { work_item.relative_position }.to be > item3.relative_position - expect { service(params).execute(item1) } - .to change { item1.relative_position } - .to be > item3.relative_position + expect(service_result[:errors]).to be_empty + end end - it 'sorts items if only given one neighbour, on the right' do - params = { move_after_id: item1.id } + context 'when only move_after_id is given' do + let(:work_item) { item3 } + let(:params) { { move_after_id: item1.id } } + + it 'sorts items if only given one neighbour, on the right' do + expect { service_result } + .to change { work_item.relative_position }.to be < item1.relative_position - expect { service(params).execute(item3) } - .to change { item3.relative_position } - .to be < item1.relative_position + expect(service_result[:errors]).to be_empty + end end end end -- GitLab From 6cd40f9c8dff34e6705591eddd1b67ee00147013 Mon Sep 17 00:00:00 2001 From: Kassio Borges Date: Wed, 16 Jul 2025 16:04:56 +0200 Subject: [PATCH 10/12] Apply 1 suggestion(s) to 1 file(s) Co-authored-by: DANGER_GITLAB_API_TOKEN --- spec/services/work_items/reorder_service_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/services/work_items/reorder_service_spec.rb b/spec/services/work_items/reorder_service_spec.rb index 9dd74a5aeff812..6fdcae3eba63d9 100644 --- a/spec/services/work_items/reorder_service_spec.rb +++ b/spec/services/work_items/reorder_service_spec.rb @@ -37,7 +37,7 @@ it_behaves_like 'reorder service' context 'when ordering work items from other namespaces' do - let(:project2) { create(:project) } + let_it_be(:project2) { create(:project) } let(:work_item) { create(:work_item, :issue, project: project2) } let(:params) { { move_after_id: item2.id, move_before_id: item3.id } } -- GitLab From c15f676720b6afb1d49590c779b75d868b5a44dd Mon Sep 17 00:00:00 2001 From: Kassio Borges Date: Thu, 17 Jul 2025 13:15:18 +0200 Subject: [PATCH 11/12] Update docs --- doc/api/graphql/reference/_index.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/api/graphql/reference/_index.md b/doc/api/graphql/reference/_index.md index d7ae9cf2c7e09a..7a208516e83e51 100644 --- a/doc/api/graphql/reference/_index.md +++ b/doc/api/graphql/reference/_index.md @@ -13769,13 +13769,13 @@ Input type: `workItemsHierarchyReorderInput` ### `Mutation.workItemsReorder` -Reorders a project level work item. - {{< details >}} **Introduced** in GitLab 18.3. **Status**: Experiment. {{< /details >}} +Reorders a project level work item. + Input type: `workItemsReorderInput` #### Arguments -- GitLab From 07b1cafe5d09d075e0fce61b0122284881c59067 Mon Sep 17 00:00:00 2001 From: Kassio Borges Date: Fri, 18 Jul 2025 17:11:00 +0200 Subject: [PATCH 12/12] Fix undercov --- app/graphql/mutations/work_items/reorder.rb | 6 +- .../mutations/work_items/reorder_spec.rb | 3 - .../mutations/work_items/reorder_spec.rb | 75 +++++++++++++++++++ 3 files changed, 78 insertions(+), 6 deletions(-) create mode 100644 spec/requests/api/graphql/mutations/work_items/reorder_spec.rb diff --git a/app/graphql/mutations/work_items/reorder.rb b/app/graphql/mutations/work_items/reorder.rb index d64218243f0414..4de05fe70cf0e9 100644 --- a/app/graphql/mutations/work_items/reorder.rb +++ b/app/graphql/mutations/work_items/reorder.rb @@ -15,13 +15,13 @@ class Reorder < BaseMutation ::Types::GlobalIDType[::WorkItem], required: false, description: 'Global ID of a project’s work item that should be placed before the work item.', - prepare: ->(id, _ctx) { id.map(&:model_id) } + prepare: ->(id, _ctx) { GitlabSchema.parse_gid(id)&.model_id } argument :move_after_id, ::Types::GlobalIDType[::WorkItem], required: false, description: 'Global ID of a project’s work item that should be placed after the work item.', - prepare: ->(id, _ctx) { id.map(&:model_id) } + prepare: ->(id, _ctx) { GitlabSchema.parse_gid(id)&.model_id } field :work_item, ::Types::WorkItemType, @@ -43,7 +43,7 @@ def resolve(**args) ::WorkItems::ReorderService.new( current_user: current_user, params: args.slice(:move_before_id, :move_after_id) - ).execute(work_item) + ).execute(work_item).payload end end end diff --git a/spec/graphql/mutations/work_items/reorder_spec.rb b/spec/graphql/mutations/work_items/reorder_spec.rb index 147227d8efa173..88b3055a627098 100644 --- a/spec/graphql/mutations/work_items/reorder_spec.rb +++ b/spec/graphql/mutations/work_items/reorder_spec.rb @@ -44,7 +44,6 @@ context 'when it fails to move' do specify do result = mutation.resolve( - project_id: project.to_gid, id: item2.to_gid, move_before_id: non_existing_record_id ) @@ -57,7 +56,6 @@ context 'when moving it before other item' do specify do result = mutation.resolve( - project_id: project.to_gid, id: item1.to_gid, move_before_id: item2.id ) @@ -70,7 +68,6 @@ context 'when moving it after other item' do specify do result = mutation.resolve( - project_id: project.to_gid, id: item2.to_gid, move_after_id: item1.id ) diff --git a/spec/requests/api/graphql/mutations/work_items/reorder_spec.rb b/spec/requests/api/graphql/mutations/work_items/reorder_spec.rb new file mode 100644 index 00000000000000..3b510d7b0ea952 --- /dev/null +++ b/spec/requests/api/graphql/mutations/work_items/reorder_spec.rb @@ -0,0 +1,75 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'Reorder work items', feature_category: :team_planning do + include GraphqlHelpers + + let_it_be(:project) { create(:project) } + let_it_be(:current_user) { create(:user, developer_of: project) } + let_it_be(:item1, reload: true) { create(:work_item, :issue, project: project, relative_position: 10) } + let_it_be(:item2, reload: true) { create(:work_item, :issue, project: project, relative_position: 20) } + let_it_be(:item3, reload: true) { create(:work_item, :issue, project: project, relative_position: 20) } + + let(:input) do + { + id: item1.to_gid.to_s + } + end + + let(:mutation) { graphql_mutation(:work_items_reorder, input) } + let(:mutation_response) { graphql_mutation_response(:work_items_reorder) } + + context 'when missing arguments' do + let(:input) do + { + id: item1.to_gid.to_s + } + end + + it 'returns an error' do + expect { post_graphql_mutation(mutation, current_user: current_user) } + .not_to change { item1.relative_position } + + expect_graphql_errors_to_include( + 'At least one of move_before_id and move_after_id are required' + ) + end + end + + context 'when all arguments are given' do + context 'when moving it before other item' do + let(:input) do + { + id: item1.to_gid.to_s, + move_before_id: item2.to_gid.to_s + } + end + + specify do + expect { post_graphql_mutation(mutation, current_user: current_user) } + .to change { item1.reload.relative_position }.to be > item2.relative_position + + expect(graphql_errors).to be_blank + expect(mutation_response['errors']).to be_blank + end + end + + context 'when moving it after other item' do + let(:input) do + { + id: item2.to_gid.to_s, + move_after_id: item1.to_gid.to_s + } + end + + specify do + expect { post_graphql_mutation(mutation, current_user: current_user) } + .to change { item2.reload.relative_position }.to be < item1.relative_position + + expect(graphql_errors).to be_blank + expect(mutation_response['errors']).to be_blank + end + end + end +end -- GitLab