Commit a4e7575d authored by Jan Provaznik's avatar Jan Provaznik Committed by Luke Duncalfe

Add graphql API for creating labels

Allows creating project and group labels using graphql API.
parent f8b87202
......@@ -3,8 +3,7 @@
module Mutations
module Boards
class Create < ::Mutations::BaseMutation
include Mutations::ResolvesGroup
include ResolvesProject
include Mutations::ResolvesResourceParent
graphql_name 'CreateBoard'
......@@ -13,12 +12,6 @@ module Mutations
null: true,
description: 'The board after mutation.'
argument :project_path, GraphQL::ID_TYPE,
required: false,
description: 'The project full path the board is associated with.'
argument :group_path, GraphQL::ID_TYPE,
required: false,
description: 'The group full path the board is associated with.'
argument :name,
GraphQL::STRING_TYPE,
required: false,
......@@ -43,10 +36,7 @@ module Mutations
authorize :admin_board
def resolve(args)
group_path = args.delete(:group_path)
project_path = args.delete(:project_path)
board_parent = authorized_find!(group_path: group_path, project_path: project_path)
board_parent = authorized_resource_parent_find!(args)
response = ::Boards::CreateService.new(board_parent, current_user, args).execute
{
......@@ -54,25 +44,6 @@ module Mutations
errors: response.errors
}
end
def ready?(**args)
if args.values_at(:project_path, :group_path).compact.blank?
raise Gitlab::Graphql::Errors::ArgumentError,
'group_path or project_path arguments are required'
end
super
end
private
def find_object(group_path: nil, project_path: nil)
if group_path
resolve_group(full_path: group_path)
else
resolve_project(full_path: project_path)
end
end
end
end
end
# frozen_string_literal: true
module Mutations
module ResolvesResourceParent
extend ActiveSupport::Concern
include Mutations::ResolvesGroup
include ResolvesProject
included do
argument :project_path, GraphQL::ID_TYPE,
required: false,
description: 'The project full path the resource is associated with'
argument :group_path, GraphQL::ID_TYPE,
required: false,
description: 'The group full path the resource is associated with'
end
def ready?(**args)
unless args[:project_path].present? ^ args[:group_path].present?
raise Gitlab::Graphql::Errors::ArgumentError,
'Exactly one of group_path or project_path arguments is required'
end
super
end
private
def authorized_resource_parent_find!(args)
authorized_find!(project_path: args.delete(:project_path),
group_path: args.delete(:group_path))
end
def find_object(project_path: nil, group_path: nil)
if group_path.present?
resolve_group(full_path: group_path)
else
resolve_project(full_path: project_path)
end
end
end
end
# frozen_string_literal: true
module Mutations
module Labels
class Create < BaseMutation
include Mutations::ResolvesResourceParent
graphql_name 'LabelCreate'
field :label,
Types::LabelType,
null: true,
description: 'The label after mutation'
argument :title, GraphQL::STRING_TYPE,
required: true,
description: 'Title of the label'
argument :description, GraphQL::STRING_TYPE,
required: false,
description: 'Description of the label'
argument :color, GraphQL::STRING_TYPE,
required: false,
default_value: Label::DEFAULT_COLOR,
description: "The color of the label given in 6-digit hex notation with leading '#' sign (e.g. #FFAABB) or one of the CSS color names in https://developer.mozilla.org/en-US/docs/Web/CSS/color_value#Color_keywords"
authorize :admin_label
def resolve(args)
parent = authorized_resource_parent_find!(args)
parent_key = parent.is_a?(Project) ? :project : :group
label = ::Labels::CreateService.new(args).execute(parent_key => parent)
{
label: label.persisted? ? label : nil,
errors: errors_on_object(label)
}
end
end
end
end
......@@ -39,6 +39,7 @@ module Types
mount_mutation Mutations::Issues::SetSubscription
mount_mutation Mutations::Issues::Update
mount_mutation Mutations::Issues::Move
mount_mutation Mutations::Labels::Create
mount_mutation Mutations::MergeRequests::Create
mount_mutation Mutations::MergeRequests::Update
mount_mutation Mutations::MergeRequests::SetLabels
......
---
title: Added GraphQL mutation for creating project and group labels
merge_request: 46534
author:
type: added
......@@ -3499,7 +3499,7 @@ input CreateBoardInput {
clientMutationId: String
"""
The group full path the board is associated with.
The group full path the resource is associated with
"""
groupPath: ID
......@@ -3519,7 +3519,7 @@ input CreateBoardInput {
name: String
"""
The project full path the board is associated with.
The project full path the resource is associated with
"""
projectPath: ID
......@@ -11303,6 +11303,63 @@ type LabelConnection {
pageInfo: PageInfo!
}
"""
Autogenerated input type of LabelCreate
"""
input LabelCreateInput {
"""
A unique identifier for the client performing the mutation.
"""
clientMutationId: String
"""
The color of the label given in 6-digit hex notation with leading '#' sign
(e.g. #FFAABB) or one of the CSS color names in
https://developer.mozilla.org/en-US/docs/Web/CSS/color_value#Color_keywords
"""
color: String = "#428BCA"
"""
Description of the label
"""
description: String
"""
The group full path the resource is associated with
"""
groupPath: ID
"""
The project full path the resource is associated with
"""
projectPath: ID
"""
Title of the label
"""
title: String!
}
"""
Autogenerated return type of LabelCreate
"""
type LabelCreatePayload {
"""
A unique identifier for the client performing the mutation.
"""
clientMutationId: String
"""
Errors encountered during execution of the mutation.
"""
errors: [String!]!
"""
The label after mutation
"""
label: Label
}
"""
An edge in a connection.
"""
......@@ -13046,6 +13103,7 @@ type Mutation {
issueSetWeight(input: IssueSetWeightInput!): IssueSetWeightPayload
jiraImportStart(input: JiraImportStartInput!): JiraImportStartPayload
jiraImportUsers(input: JiraImportUsersInput!): JiraImportUsersPayload
labelCreate(input: LabelCreateInput!): LabelCreatePayload
markAsSpamSnippet(input: MarkAsSpamSnippetInput!): MarkAsSpamSnippetPayload
mergeRequestCreate(input: MergeRequestCreateInput!): MergeRequestCreatePayload
mergeRequestSetAssignees(input: MergeRequestSetAssigneesInput!): MergeRequestSetAssigneesPayload
......
......@@ -9454,7 +9454,7 @@
"inputFields": [
{
"name": "projectPath",
"description": "The project full path the board is associated with.",
"description": "The project full path the resource is associated with",
"type": {
"kind": "SCALAR",
"name": "ID",
......@@ -9464,7 +9464,7 @@
},
{
"name": "groupPath",
"description": "The group full path the board is associated with.",
"description": "The group full path the resource is associated with",
"type": {
"kind": "SCALAR",
"name": "ID",
......@@ -30964,6 +30964,148 @@
"enumValues": null,
"possibleTypes": null
},
{
"kind": "INPUT_OBJECT",
"name": "LabelCreateInput",
"description": "Autogenerated input type of LabelCreate",
"fields": null,
"inputFields": [
{
"name": "projectPath",
"description": "The project full path the resource is associated with",
"type": {
"kind": "SCALAR",
"name": "ID",
"ofType": null
},
"defaultValue": null
},
{
"name": "groupPath",
"description": "The group full path the resource is associated with",
"type": {
"kind": "SCALAR",
"name": "ID",
"ofType": null
},
"defaultValue": null
},
{
"name": "title",
"description": "Title of the label",
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
},
"defaultValue": null
},
{
"name": "description",
"description": "Description of the label",
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null
},
{
"name": "color",
"description": "The color of the label given in 6-digit hex notation with leading '#' sign (e.g. #FFAABB) or one of the CSS color names in https://developer.mozilla.org/en-US/docs/Web/CSS/color_value#Color_keywords",
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": "\"#428BCA\""
},
{
"name": "clientMutationId",
"description": "A unique identifier for the client performing the mutation.",
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null
}
],
"interfaces": null,
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "LabelCreatePayload",
"description": "Autogenerated return type of LabelCreate",
"fields": [
{
"name": "clientMutationId",
"description": "A unique identifier for the client performing the mutation.",
"args": [
],
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "errors",
"description": "Errors encountered during execution of the mutation.",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
}
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "label",
"description": "The label after mutation",
"args": [
],
"type": {
"kind": "OBJECT",
"name": "Label",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [
],
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "LabelEdge",
......@@ -37488,6 +37630,33 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "labelCreate",
"description": null,
"args": [
{
"name": "input",
"description": null,
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "INPUT_OBJECT",
"name": "LabelCreateInput",
"ofType": null
}
},
"defaultValue": null
}
],
"type": {
"kind": "OBJECT",
"name": "LabelCreatePayload",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "markAsSpamSnippet",
"description": null,
......@@ -1731,6 +1731,16 @@ Autogenerated return type of JiraImportUsers.
| `textColor` | String! | Text color of the label |
| `title` | String! | Content of the label |
### LabelCreatePayload
Autogenerated return type of LabelCreate.
| Field | Type | Description |
| ----- | ---- | ----------- |
| `clientMutationId` | String | A unique identifier for the client performing the mutation. |
| `errors` | String! => Array | Errors encountered during execution of the mutation. |
| `label` | Label | The label after mutation |
### MarkAsSpamSnippetPayload
Autogenerated return type of MarkAsSpamSnippet.
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Mutations::Labels::Create do
let_it_be(:user) { create(:user) }
let(:attributes) do
{
title: 'new title',
description: 'A new label'
}
end
let(:mutation) { described_class.new(object: nil, context: { current_user: user }, field: nil) }
let(:mutated_label) { subject[:label] }
shared_examples 'create labels mutation' do
describe '#resolve' do
subject { mutation.resolve(attributes.merge(extra_params)) }
context 'when the user does not have permission to create a label' do
before do
parent.add_guest(user)
end
it 'raises an error' do
expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
end
end
context 'when the user can create a label' do
before do
parent.add_developer(user)
end
it 'creates label with correct values' do
expect(mutated_label).to have_attributes(attributes)
end
end
end
end
specify { expect(described_class).to require_graphql_authorizations(:admin_label) }
context 'when creating a project label' do
let_it_be(:parent) { create(:project) }
let(:extra_params) { { project_path: parent.full_path } }
it_behaves_like 'create labels mutation'
end
context 'when creating a group label' do
let_it_be(:parent) { create(:group) }
let(:extra_params) { { group_path: parent.full_path } }
it_behaves_like 'create labels mutation'
end
describe '#ready?' do
subject { mutation.ready?(attributes.merge(extra_params)) }
context 'when passing both project_path and group_path' do
let(:extra_params) { { project_path: 'foo', group_path: 'bar' } }
it 'raises an argument error' do
expect { subject }
.to raise_error(Gitlab::Graphql::Errors::ArgumentError, /Exactly one of/)
end
end
context 'when passing only project_path or group_path' do
let(:extra_params) { { project_path: 'foo' } }
it 'does not raise an error' do
expect { subject }.not_to raise_error
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Mutations::Labels::Create do
include GraphqlHelpers
let_it_be(:current_user) { create(:user) }
let(:params) do
{
'title' => 'foo',
'description' => 'some description',
'color' => '#FF0000'
}
end
let(:mutation) { graphql_mutation(:label_create, params.merge(extra_params)) }
subject { post_graphql_mutation(mutation, current_user: current_user) }
def mutation_response
graphql_mutation_response(:label_create)
end
shared_examples_for 'labels create mutation' do
context 'when the user does not have permission to create a label' do
it_behaves_like 'a mutation that returns a top-level access error'
it 'does not create the label' do
expect { subject }.not_to change { Label.count }
end
end
context 'when the user has permission to create a label' do
before do
parent.add_developer(current_user)
end
context 'when the parent (project_path or group_path) param is given' do
it 'creates the label' do
expect { subject }.to change { Label.count }.to(1)
expect(mutation_response).to include(
'label' => a_hash_including(params))
end
it 'does not create a label when there are errors' do
label_factory = parent.is_a?(Group) ? :group_label : :label
create(label_factory, title: 'foo', parent.class.name.underscore.to_sym => parent)
expect { subject }.not_to change { Label.count }
expect(mutation_response).to have_key('label')
expect(mutation_response['label']).to be_nil
expect(mutation_response['errors'].first).to eq('Title has already been taken')
end
end
end
end
context 'when creating a project label' do
let_it_be(:parent) { create(:project) }
let(:extra_params) { { project_path: parent.full_path } }
it_behaves_like 'labels create mutation'
end
context 'when creating a group label' do
let_it_be(:parent) { create(:group) }
let(:extra_params) { { group_path: parent.full_path } }
it_behaves_like 'labels create mutation'
end
context 'when neither project_path nor group_path param is given' do
let(:mutation) { graphql_mutation(:label_create, params) }
it_behaves_like 'a mutation that returns top-level errors',
errors: ['Exactly one of group_path or project_path arguments is required']
it 'does not create the label' do
expect { subject }.not_to change { Label.count }
end
end
end
......@@ -65,7 +65,7 @@ RSpec.shared_examples 'boards create mutation' do
let(:params) { { name: name } }
it_behaves_like 'a mutation that returns top-level errors',
errors: ['group_path or project_path arguments are required']
errors: ['Exactly one of group_path or project_path arguments is required']
it 'does not create the board' do
expect { subject }.not_to change { Board.count }
......
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