Commit e229a4ed authored by Luke Duncalfe's avatar Luke Duncalfe

Merge branch 'graphql_create_label' into 'master'

Add graphql API for creating labels

See merge request gitlab-org/gitlab!46534
parents a86581ac a4e7575d
......@@ -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
......@@ -11423,6 +11423,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.
"""
......@@ -13171,6 +13228,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",
......@@ -31314,6 +31314,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",
......@@ -37852,6 +37994,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,
......@@ -1742,6 +1742,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