Commit e7f9eb54 authored by Ash McKenzie's avatar Ash McKenzie

Merge branch '31919-graphql-MR-label-mutation' into 'master'

GraphQL: Create MR mutations needed for the sidebar

See merge request gitlab-org/gitlab!19913
parents b400bea6 8ad9f8c6
# 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
# frozen_string_literal: true
module Mutations
module MergeRequests
class SetLocked < Base
graphql_name 'MergeRequestSetLocked'
argument :locked,
GraphQL::BOOLEAN_TYPE,
required: true,
description: <<~DESC
Whether or not to lock the merge request.
DESC
def resolve(project_path:, iid:, locked:)
merge_request = authorized_find!(project_path: project_path, iid: iid)
project = merge_request.project
::MergeRequests::UpdateService.new(project, current_user, discussion_locked: locked)
.execute(merge_request)
{
merge_request: merge_request,
errors: merge_request.errors.full_messages
}
end
end
end
end
# frozen_string_literal: true
module Mutations
module MergeRequests
class SetSubscription < Base
graphql_name 'MergeRequestSetSubscription'
argument :subscribed_state,
GraphQL::BOOLEAN_TYPE,
required: true,
description: 'The desired state of the subscription'
def resolve(project_path:, iid:, subscribed_state:)
merge_request = authorized_find!(project_path: project_path, iid: iid)
project = merge_request.project
merge_request.set_subscription(current_user, subscribed_state, project)
{
merge_request: merge_request,
errors: merge_request.errors.full_messages
}
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,7 +9,10 @@ module Types ...@@ -9,7 +9,10 @@ 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::SetLocked
mount_mutation Mutations::MergeRequests::SetMilestone mount_mutation Mutations::MergeRequests::SetMilestone
mount_mutation Mutations::MergeRequests::SetSubscription
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
mount_mutation Mutations::Notes::Create::Note, calls_gitaly: true mount_mutation Mutations::Notes::Create::Note, calls_gitaly: true
......
---
title: 'GraphQL: Create MR mutations needed for the sidebar'
merge_request: 19913
author:
type: added
...@@ -2814,6 +2814,11 @@ type Label { ...@@ -2814,6 +2814,11 @@ type Label {
""" """
descriptionHtml: String descriptionHtml: String
"""
Label ID
"""
id: ID!
""" """
Text color of the label Text color of the label
""" """
...@@ -3407,6 +3412,101 @@ type MergeRequestSetAssigneesPayload { ...@@ -3407,6 +3412,101 @@ type MergeRequestSetAssigneesPayload {
mergeRequest: MergeRequest mergeRequest: MergeRequest
} }
"""
Autogenerated input type of MergeRequestSetLabels
"""
input MergeRequestSetLabelsInput {
"""
A unique identifier for the client performing the mutation.
"""
clientMutationId: String
"""
The iid of the merge request to mutate
"""
iid: String!
"""
The Label IDs to set. Replaces existing labels by default.
"""
labelIds: [ID!]!
"""
Changes the operation mode. Defaults to REPLACE.
"""
operationMode: MutationOperationMode
"""
The project the merge request to mutate is in
"""
projectPath: ID!
}
"""
Autogenerated return type of MergeRequestSetLabels
"""
type MergeRequestSetLabelsPayload {
"""
A unique identifier for the client performing the mutation.
"""
clientMutationId: String
"""
Reasons why the mutation failed.
"""
errors: [String!]!
"""
The merge request after mutation
"""
mergeRequest: MergeRequest
}
"""
Autogenerated input type of MergeRequestSetLocked
"""
input MergeRequestSetLockedInput {
"""
A unique identifier for the client performing the mutation.
"""
clientMutationId: String
"""
The iid of the merge request to mutate
"""
iid: String!
"""
Whether or not to lock the merge request.
"""
locked: Boolean!
"""
The project the merge request to mutate is in
"""
projectPath: ID!
}
"""
Autogenerated return type of MergeRequestSetLocked
"""
type MergeRequestSetLockedPayload {
"""
A unique identifier for the client performing the mutation.
"""
clientMutationId: String
"""
Reasons why the mutation failed.
"""
errors: [String!]!
"""
The merge request after mutation
"""
mergeRequest: MergeRequest
}
""" """
Autogenerated input type of MergeRequestSetMilestone Autogenerated input type of MergeRequestSetMilestone
""" """
...@@ -3452,6 +3552,51 @@ type MergeRequestSetMilestonePayload { ...@@ -3452,6 +3552,51 @@ type MergeRequestSetMilestonePayload {
mergeRequest: MergeRequest mergeRequest: MergeRequest
} }
"""
Autogenerated input type of MergeRequestSetSubscription
"""
input MergeRequestSetSubscriptionInput {
"""
A unique identifier for the client performing the mutation.
"""
clientMutationId: String
"""
The iid of the merge request to mutate
"""
iid: String!
"""
The project the merge request to mutate is in
"""
projectPath: ID!
"""
The desired state of the subscription
"""
subscribedState: Boolean!
}
"""
Autogenerated return type of MergeRequestSetSubscription
"""
type MergeRequestSetSubscriptionPayload {
"""
A unique identifier for the client performing the mutation.
"""
clientMutationId: String
"""
Reasons why the mutation failed.
"""
errors: [String!]!
"""
The merge request after mutation
"""
mergeRequest: MergeRequest
}
""" """
Autogenerated input type of MergeRequestSetWip Autogenerated input type of MergeRequestSetWip
""" """
...@@ -3588,7 +3733,10 @@ type Mutation { ...@@ -3588,7 +3733,10 @@ type Mutation {
epicSetSubscription(input: EpicSetSubscriptionInput!): EpicSetSubscriptionPayload epicSetSubscription(input: EpicSetSubscriptionInput!): EpicSetSubscriptionPayload
epicTreeReorder(input: EpicTreeReorderInput!): EpicTreeReorderPayload epicTreeReorder(input: EpicTreeReorderInput!): EpicTreeReorderPayload
mergeRequestSetAssignees(input: MergeRequestSetAssigneesInput!): MergeRequestSetAssigneesPayload mergeRequestSetAssignees(input: MergeRequestSetAssigneesInput!): MergeRequestSetAssigneesPayload
mergeRequestSetLabels(input: MergeRequestSetLabelsInput!): MergeRequestSetLabelsPayload
mergeRequestSetLocked(input: MergeRequestSetLockedInput!): MergeRequestSetLockedPayload
mergeRequestSetMilestone(input: MergeRequestSetMilestoneInput!): MergeRequestSetMilestonePayload mergeRequestSetMilestone(input: MergeRequestSetMilestoneInput!): MergeRequestSetMilestonePayload
mergeRequestSetSubscription(input: MergeRequestSetSubscriptionInput!): MergeRequestSetSubscriptionPayload
mergeRequestSetWip(input: MergeRequestSetWipInput!): MergeRequestSetWipPayload mergeRequestSetWip(input: MergeRequestSetWipInput!): MergeRequestSetWipPayload
removeAwardEmoji(input: RemoveAwardEmojiInput!): RemoveAwardEmojiPayload removeAwardEmoji(input: RemoveAwardEmojiInput!): RemoveAwardEmojiPayload
todoMarkDone(input: TodoMarkDoneInput!): TodoMarkDonePayload todoMarkDone(input: TodoMarkDoneInput!): TodoMarkDonePayload
......
...@@ -410,6 +410,7 @@ The API can be explored interactively using the [GraphiQL IDE](../index.md#graph ...@@ -410,6 +410,7 @@ The API can be explored interactively using the [GraphiQL IDE](../index.md#graph
| Name | Type | Description | | Name | Type | Description |
| --- | ---- | ---------- | | --- | ---- | ---------- |
| `id` | ID! | Label ID |
| `description` | String | Description of the label (markdown rendered as HTML for caching) | | `description` | String | Description of the label (markdown rendered as HTML for caching) |
| `descriptionHtml` | String | The GitLab Flavored Markdown rendering of `description` | | `descriptionHtml` | String | The GitLab Flavored Markdown rendering of `description` |
| `title` | String! | Content of the label | | `title` | String! | Content of the label |
...@@ -491,6 +492,22 @@ The API can be explored interactively using the [GraphiQL IDE](../index.md#graph ...@@ -491,6 +492,22 @@ The API can be explored interactively using the [GraphiQL IDE](../index.md#graph
| `errors` | String! => Array | Reasons why the mutation failed. | | `errors` | String! => Array | Reasons why the mutation failed. |
| `mergeRequest` | MergeRequest | The merge request after mutation | | `mergeRequest` | MergeRequest | The merge request after mutation |
### MergeRequestSetLabelsPayload
| Name | Type | Description |
| --- | ---- | ---------- |
| `clientMutationId` | String | A unique identifier for the client performing the mutation. |
| `errors` | String! => Array | Reasons why the mutation failed. |
| `mergeRequest` | MergeRequest | The merge request after mutation |
### MergeRequestSetLockedPayload
| Name | Type | Description |
| --- | ---- | ---------- |
| `clientMutationId` | String | A unique identifier for the client performing the mutation. |
| `errors` | String! => Array | Reasons why the mutation failed. |
| `mergeRequest` | MergeRequest | The merge request after mutation |
### MergeRequestSetMilestonePayload ### MergeRequestSetMilestonePayload
| Name | Type | Description | | Name | Type | Description |
...@@ -499,6 +516,14 @@ The API can be explored interactively using the [GraphiQL IDE](../index.md#graph ...@@ -499,6 +516,14 @@ The API can be explored interactively using the [GraphiQL IDE](../index.md#graph
| `errors` | String! => Array | Reasons why the mutation failed. | | `errors` | String! => Array | Reasons why the mutation failed. |
| `mergeRequest` | MergeRequest | The merge request after mutation | | `mergeRequest` | MergeRequest | The merge request after mutation |
### MergeRequestSetSubscriptionPayload
| Name | Type | Description |
| --- | ---- | ---------- |
| `clientMutationId` | String | A unique identifier for the client performing the mutation. |
| `errors` | String! => Array | Reasons why the mutation failed. |
| `mergeRequest` | MergeRequest | The merge request after mutation |
### MergeRequestSetWipPayload ### MergeRequestSetWipPayload
| Name | Type | Description | | Name | Type | Description |
......
# 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
# frozen_string_literal: true
require 'spec_helper'
describe Mutations::MergeRequests::SetLocked 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(:locked) { true }
let(:mutated_merge_request) { subject[:merge_request] }
subject { mutation.resolve(project_path: merge_request.project.full_path, iid: merge_request.iid, locked: locked) }
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 'returns the merge request as discussion locked' do
expect(mutated_merge_request).to eq(merge_request)
expect(mutated_merge_request).to be_discussion_locked
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 locked as false' do
let(:locked) { false }
it 'unlocks the discussion' do
merge_request.update(discussion_locked: true)
expect(mutated_merge_request).not_to be_discussion_locked
end
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
describe Mutations::MergeRequests::SetSubscription do
let(:merge_request) { create(:merge_request) }
let(:project) { merge_request.project }
let(:user) { create(:user) }
subject(:mutation) { described_class.new(object: nil, context: { current_user: user }) }
describe '#resolve' do
let(:subscribe) { true }
let(:mutated_merge_request) { subject[:merge_request] }
subject { mutation.resolve(project_path: merge_request.project.full_path, iid: merge_request.iid, subscribed_state: subscribe) }
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 'returns the merge request as discussion locked' do
expect(mutated_merge_request).to eq(merge_request)
expect(mutated_merge_request.subscribed?(user, project)).to eq(true)
expect(subject[:errors]).to be_empty
end
context 'when passing subscribe as false' do
let(:subscribe) { false }
it 'unsubscribes from the discussion' do
merge_request.subscribe(user, project)
expect(mutated_merge_request.subscribed?(user, project)).to eq(false)
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
# frozen_string_literal: true
require 'spec_helper'
describe 'Setting locked status of a merge request' do
include GraphqlHelpers
let(:current_user) { create(:user) }
let(:merge_request) { create(:merge_request) }
let(:project) { merge_request.project }
let(:input) { { locked: true } }
let(:mutation) do
variables = {
project_path: project.full_path,
iid: merge_request.iid.to_s
}
graphql_mutation(:merge_request_set_locked, variables.merge(input),
<<-QL.strip_heredoc
clientMutationId
errors
mergeRequest {
id
discussionLocked
}
QL
)
end
def mutation_response
graphql_mutation_response(:merge_request_set_locked)['mergeRequest']['discussionLocked']
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 'marks the merge request as WIP' do
post_graphql_mutation(mutation, current_user: current_user)
expect(response).to have_gitlab_http_status(:success)
expect(mutation_response).to eq(true)
end
it 'does not do anything if the merge request was already locked' do
merge_request.update!(discussion_locked: true)
post_graphql_mutation(mutation, current_user: current_user)
expect(response).to have_gitlab_http_status(:success)
expect(mutation_response).to eq(true)
end
context 'when passing locked false as input' do
let(:input) { { locked: false } }
it 'does not do anything if the merge request was not marked locked' do
post_graphql_mutation(mutation, current_user: current_user)
expect(response).to have_gitlab_http_status(:success)
expect(mutation_response).to eq(false)
end
it 'unmarks the merge request as locked' do
merge_request.update!(discussion_locked: true)
post_graphql_mutation(mutation, current_user: current_user)
expect(response).to have_gitlab_http_status(:success)
expect(mutation_response).to eq(false)
end
end
end
# frozen_string_literal: true
require 'spec_helper'
describe 'Setting subscribed status of a merge request' do
include GraphqlHelpers
let(:current_user) { create(:user) }
let(:merge_request) { create(:merge_request) }
let(:project) { merge_request.project }
let(:input) { { subscribed_state: true } }
let(:mutation) do
variables = {
project_path: project.full_path,
iid: merge_request.iid.to_s
}
graphql_mutation(:merge_request_set_subscription, variables.merge(input),
<<-QL.strip_heredoc
clientMutationId
errors
mergeRequest {
id
subscribed
}
QL
)
end
def mutation_response
graphql_mutation_response(:merge_request_set_subscription)['mergeRequest']['subscribed']
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 'marks the merge request as WIP' do
post_graphql_mutation(mutation, current_user: current_user)
expect(response).to have_gitlab_http_status(:success)
expect(mutation_response).to eq(true)
end
context 'when passing subscribe false as input' do
let(:input) { { subscribed_state: false } }
it 'unmarks the merge request as subscribed' do
merge_request.subscribe(current_user, project)
post_graphql_mutation(mutation, current_user: current_user)
expect(response).to have_gitlab_http_status(:success)
expect(mutation_response).to eq(false)
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