Commit ce30cfef authored by Mario de la Ossa's avatar Mario de la Ossa

GraphQL: Add MRSetLabels mutation

Add a mutation to set labels on a Merge Request.

If `append` is true, only appends labels
If `remove` is true, only removes labels

Otherwise the list sent becomes the only labels applied
parent 04813972
# frozen_string_literal: true
module Mutations
module MergeRequests
class SetLabels < Base
graphql_name 'MergeRequestSetLabels'
argument :label_ids,
[GraphQL::ID_TYPE],
required: true,
description: <<~DESC
The Label IDs to set. Replaces existing labels by default.
DESC
argument :operation_mode,
Types::MutationOperationModeEnum,
required: false,
description: <<~DESC
Changes the operation mode. Defaults to REPLACE.
DESC
def resolve(project_path:, iid:, label_ids:, operation_mode: Types::MutationOperationModeEnum.enum[:replace])
merge_request = authorized_find!(project_path: project_path, iid: iid)
project = merge_request.project
label_ids = label_ids
.select(&method(:label_descendant?))
.map { |gid| GlobalID.parse(gid).model_id } # MergeRequests::UpdateService expects integers
attribute_name = case operation_mode
when Types::MutationOperationModeEnum.enum[:append]
:add_label_ids
when Types::MutationOperationModeEnum.enum[:remove]
:remove_label_ids
else
:label_ids
end
::MergeRequests::UpdateService.new(project, current_user, attribute_name => label_ids)
.execute(merge_request)
{
merge_request: merge_request,
errors: merge_request.errors.full_messages
}
end
def label_descendant?(gid)
GlobalID.parse(gid)&.model_class&.ancestors&.include?(Label)
end
end
end
end
...@@ -6,6 +6,8 @@ module Types ...@@ -6,6 +6,8 @@ module Types
authorize :read_label authorize :read_label
field :id, GraphQL::ID_TYPE, null: false,
description: 'Label ID'
field :description, GraphQL::STRING_TYPE, null: true, field :description, GraphQL::STRING_TYPE, null: true,
description: 'Description of the label (markdown rendered as HTML for caching)' description: 'Description of the label (markdown rendered as HTML for caching)'
markdown_field :description_html, null: true markdown_field :description_html, null: true
......
...@@ -9,6 +9,7 @@ module Types ...@@ -9,6 +9,7 @@ module Types
mount_mutation Mutations::AwardEmojis::Add mount_mutation Mutations::AwardEmojis::Add
mount_mutation Mutations::AwardEmojis::Remove mount_mutation Mutations::AwardEmojis::Remove
mount_mutation Mutations::AwardEmojis::Toggle mount_mutation Mutations::AwardEmojis::Toggle
mount_mutation Mutations::MergeRequests::SetLabels
mount_mutation Mutations::MergeRequests::SetMilestone mount_mutation Mutations::MergeRequests::SetMilestone
mount_mutation Mutations::MergeRequests::SetWip, calls_gitaly: true mount_mutation Mutations::MergeRequests::SetWip, calls_gitaly: true
mount_mutation Mutations::MergeRequests::SetAssignees mount_mutation Mutations::MergeRequests::SetAssignees
......
# frozen_string_literal: true
require 'spec_helper'
describe Mutations::MergeRequests::SetLabels do
let(:merge_request) { create(:merge_request) }
let(:user) { create(:user) }
subject(:mutation) { described_class.new(object: nil, context: { current_user: user }) }
describe '#resolve' do
let(:label) { create(:label, project: merge_request.project) }
let(:label2) { create(:label, project: merge_request.project) }
let(:label_ids) { [label.to_global_id] }
let(:mutated_merge_request) { subject[:merge_request] }
subject { mutation.resolve(project_path: merge_request.project.full_path, iid: merge_request.iid, label_ids: label_ids) }
it 'raises an error if the resource is not accessible to the user' do
expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
end
context 'when the user can update the merge request' do
before do
merge_request.project.add_developer(user)
end
it 'sets the labels, removing all others' do
merge_request.update!(labels: [label2])
expect(mutated_merge_request).to eq(merge_request)
expect(mutated_merge_request.labels).to contain_exactly(label)
expect(subject[:errors]).to be_empty
end
it 'returns errors merge request could not be updated' do
# Make the merge request invalid
merge_request.allow_broken = true
merge_request.update!(source_project: nil)
expect(subject[:errors]).not_to be_empty
end
context 'when passing an empty array' do
let(:label_ids) { [] }
it 'removes all labels' do
merge_request.update!(labels: [label])
expect(mutated_merge_request.labels).to be_empty
end
end
context 'when passing operation_mode as APPEND' do
subject { mutation.resolve(project_path: merge_request.project.full_path, iid: merge_request.iid, label_ids: label_ids, operation_mode: Types::MutationOperationModeEnum.enum[:append]) }
it 'sets the labels, without removing others' do
merge_request.update!(labels: [label2])
expect(mutated_merge_request).to eq(merge_request)
expect(mutated_merge_request.labels).to contain_exactly(label, label2)
expect(subject[:errors]).to be_empty
end
end
context 'when passing operation_mode as REMOVE' do
subject { mutation.resolve(project_path: merge_request.project.full_path, iid: merge_request.iid, label_ids: label_ids, operation_mode: Types::MutationOperationModeEnum.enum[:remove])}
it 'removes the labels, without removing others' do
merge_request.update!(labels: [label, label2])
expect(mutated_merge_request).to eq(merge_request)
expect(mutated_merge_request.labels).to contain_exactly(label2)
expect(subject[:errors]).to be_empty
end
end
end
end
end
...@@ -3,7 +3,7 @@ require 'spec_helper' ...@@ -3,7 +3,7 @@ require 'spec_helper'
describe GitlabSchema.types['Label'] do describe GitlabSchema.types['Label'] do
it 'has the correct fields' do it 'has the correct fields' do
expected_fields = [:description, :description_html, :title, :color, :text_color] expected_fields = [:id, :description, :description_html, :title, :color, :text_color]
is_expected.to have_graphql_fields(*expected_fields) is_expected.to have_graphql_fields(*expected_fields)
end end
......
# frozen_string_literal: true
require 'spec_helper'
describe 'Setting labels of a merge request' do
include GraphqlHelpers
let(:current_user) { create(:user) }
let(:merge_request) { create(:merge_request) }
let(:project) { merge_request.project }
let(:label) { create(:label, project: project) }
let(:label2) { create(:label, project: project) }
let(:input) { { label_ids: [GitlabSchema.id_from_object(label).to_s] } }
let(:mutation) do
variables = {
project_path: project.full_path,
iid: merge_request.iid.to_s
}
graphql_mutation(:merge_request_set_labels, variables.merge(input),
<<-QL.strip_heredoc
clientMutationId
errors
mergeRequest {
id
labels {
nodes {
id
}
}
}
QL
)
end
def mutation_response
graphql_mutation_response(:merge_request_set_labels)
end
def mutation_label_nodes
mutation_response['mergeRequest']['labels']['nodes']
end
before do
project.add_developer(current_user)
end
it 'returns an error if the user is not allowed to update the merge request' do
post_graphql_mutation(mutation, current_user: create(:user))
expect(graphql_errors).not_to be_empty
end
it 'sets the merge request labels, removing existing ones' do
merge_request.update(labels: [label2])
post_graphql_mutation(mutation, current_user: current_user)
expect(response).to have_gitlab_http_status(:success)
expect(mutation_label_nodes.count).to eq(1)
expect(mutation_label_nodes[0]['id']).to eq(label.to_global_id.to_s)
end
context 'when passing label_ids empty array as input' do
let(:input) { { label_ids: [] } }
it 'removes the merge request labels' do
merge_request.update!(labels: [label])
post_graphql_mutation(mutation, current_user: current_user)
expect(response).to have_gitlab_http_status(:success)
expect(mutation_label_nodes.count).to eq(0)
end
end
context 'when passing operation_mode as APPEND' do
let(:input) { { operation_mode: Types::MutationOperationModeEnum.enum[:append], label_ids: [GitlabSchema.id_from_object(label).to_s] } }
before do
merge_request.update!(labels: [label2])
end
it 'sets the labels, without removing others' do
post_graphql_mutation(mutation, current_user: current_user)
expect(response).to have_gitlab_http_status(:success)
expect(mutation_label_nodes.count).to eq(2)
expect(mutation_label_nodes).to contain_exactly({ 'id' => label.to_global_id.to_s }, { 'id' => label2.to_global_id.to_s })
end
end
context 'when passing operation_mode as REMOVE' do
let(:input) { { operation_mode: Types::MutationOperationModeEnum.enum[:remove], label_ids: [GitlabSchema.id_from_object(label).to_s] } }
before do
merge_request.update!(labels: [label, label2])
end
it 'removes the labels, without removing others' do
post_graphql_mutation(mutation, current_user: current_user)
expect(response).to have_gitlab_http_status(:success)
expect(mutation_label_nodes.count).to eq(1)
expect(mutation_label_nodes[0]['id']).to eq(label2.to_global_id.to_s)
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