Commit f3c8cc36 authored by Robert Speicher's avatar Robert Speicher

Merge branch '229506-graphql-mutation-for-adding-epic-to-issue' into 'master'

Add GraphQL mutation to set the epic of an issue

See merge request gitlab-org/gitlab!38494
parents c25257f4 89f2d959
...@@ -6802,6 +6802,51 @@ type IssueSetDueDatePayload { ...@@ -6802,6 +6802,51 @@ type IssueSetDueDatePayload {
issue: Issue issue: Issue
} }
"""
Autogenerated input type of IssueSetEpic
"""
input IssueSetEpicInput {
"""
A unique identifier for the client performing the mutation.
"""
clientMutationId: String
"""
Global ID of the epic to be assigned to the issue
"""
epicId: ID!
"""
The IID of the issue to mutate
"""
iid: String!
"""
The project the issue to mutate is in
"""
projectPath: ID!
}
"""
Autogenerated return type of IssueSetEpic
"""
type IssueSetEpicPayload {
"""
A unique identifier for the client performing the mutation.
"""
clientMutationId: String
"""
Errors encountered during execution of the mutation.
"""
errors: [String!]!
"""
The issue after mutation
"""
issue: Issue
}
""" """
Autogenerated input type of IssueSetIteration Autogenerated input type of IssueSetIteration
""" """
...@@ -8878,6 +8923,7 @@ type Mutation { ...@@ -8878,6 +8923,7 @@ type Mutation {
issueSetAssignees(input: IssueSetAssigneesInput!): IssueSetAssigneesPayload issueSetAssignees(input: IssueSetAssigneesInput!): IssueSetAssigneesPayload
issueSetConfidential(input: IssueSetConfidentialInput!): IssueSetConfidentialPayload issueSetConfidential(input: IssueSetConfidentialInput!): IssueSetConfidentialPayload
issueSetDueDate(input: IssueSetDueDateInput!): IssueSetDueDatePayload issueSetDueDate(input: IssueSetDueDateInput!): IssueSetDueDatePayload
issueSetEpic(input: IssueSetEpicInput!): IssueSetEpicPayload
issueSetIteration(input: IssueSetIterationInput!): IssueSetIterationPayload issueSetIteration(input: IssueSetIterationInput!): IssueSetIterationPayload
issueSetLocked(input: IssueSetLockedInput!): IssueSetLockedPayload issueSetLocked(input: IssueSetLockedInput!): IssueSetLockedPayload
issueSetSubscription(input: IssueSetSubscriptionInput!): IssueSetSubscriptionPayload issueSetSubscription(input: IssueSetSubscriptionInput!): IssueSetSubscriptionPayload
......
...@@ -18861,6 +18861,136 @@ ...@@ -18861,6 +18861,136 @@
"enumValues": null, "enumValues": null,
"possibleTypes": null "possibleTypes": null
}, },
{
"kind": "INPUT_OBJECT",
"name": "IssueSetEpicInput",
"description": "Autogenerated input type of IssueSetEpic",
"fields": null,
"inputFields": [
{
"name": "projectPath",
"description": "The project the issue to mutate is in",
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "ID",
"ofType": null
}
},
"defaultValue": null
},
{
"name": "iid",
"description": "The IID of the issue to mutate",
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
},
"defaultValue": null
},
{
"name": "epicId",
"description": "Global ID of the epic to be assigned to the issue",
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "ID",
"ofType": null
}
},
"defaultValue": null
},
{
"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": "IssueSetEpicPayload",
"description": "Autogenerated return type of IssueSetEpic",
"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": "issue",
"description": "The issue after mutation",
"args": [
],
"type": {
"kind": "OBJECT",
"name": "Issue",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [
],
"enumValues": null,
"possibleTypes": null
},
{ {
"kind": "INPUT_OBJECT", "kind": "INPUT_OBJECT",
"name": "IssueSetIterationInput", "name": "IssueSetIterationInput",
...@@ -25832,6 +25962,33 @@ ...@@ -25832,6 +25962,33 @@
"isDeprecated": false, "isDeprecated": false,
"deprecationReason": null "deprecationReason": null
}, },
{
"name": "issueSetEpic",
"description": null,
"args": [
{
"name": "input",
"description": null,
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "INPUT_OBJECT",
"name": "IssueSetEpicInput",
"ofType": null
}
},
"defaultValue": null
}
],
"type": {
"kind": "OBJECT",
"name": "IssueSetEpicPayload",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{ {
"name": "issueSetIteration", "name": "issueSetIteration",
"description": null, "description": null,
...@@ -1030,6 +1030,16 @@ Autogenerated return type of IssueSetDueDate ...@@ -1030,6 +1030,16 @@ Autogenerated return type of IssueSetDueDate
| `errors` | String! => Array | Errors encountered during execution of the mutation. | | `errors` | String! => Array | Errors encountered during execution of the mutation. |
| `issue` | Issue | The issue after mutation | | `issue` | Issue | The issue after mutation |
## IssueSetEpicPayload
Autogenerated return type of IssueSetEpic
| Name | Type | Description |
| --- | ---- | ---------- |
| `clientMutationId` | String | A unique identifier for the client performing the mutation. |
| `errors` | String! => Array | Errors encountered during execution of the mutation. |
| `issue` | Issue | The issue after mutation |
## IssueSetIterationPayload ## IssueSetIterationPayload
Autogenerated return type of IssueSetIteration Autogenerated return type of IssueSetIteration
......
...@@ -8,6 +8,7 @@ module EE ...@@ -8,6 +8,7 @@ module EE
prepended do prepended do
mount_mutation ::Mutations::Issues::SetIteration mount_mutation ::Mutations::Issues::SetIteration
mount_mutation ::Mutations::Issues::SetWeight mount_mutation ::Mutations::Issues::SetWeight
mount_mutation ::Mutations::Issues::SetEpic
mount_mutation ::Mutations::EpicTree::Reorder mount_mutation ::Mutations::EpicTree::Reorder
mount_mutation ::Mutations::Epics::Update mount_mutation ::Mutations::Epics::Update
mount_mutation ::Mutations::Epics::Create mount_mutation ::Mutations::Epics::Create
......
# frozen_string_literal: true
module Mutations
module Issues
class SetEpic < Base
graphql_name 'IssueSetEpic'
argument :epic_id,
GraphQL::ID_TYPE,
required: true,
loads: Types::EpicType,
description: 'Global ID of the epic to be assigned to the issue'
def resolve(project_path:, iid:, epic: nil)
issue = authorized_find!(project_path: project_path, iid: iid)
project = issue.project
authorize_admin_rights!(epic)
::Issues::UpdateService.new(project, current_user, epic: epic)
.execute(issue)
{
issue: issue,
errors: issue.errors.full_messages
}
end
private
def authorize_admin_rights!(epic)
return unless epic.present?
raise_resource_not_available_error! unless Ability.allowed?(current_user, :admin_epic, epic.group)
end
end
end
end
---
title: Add GraphQL mutation to set the epic of an issue
merge_request: 38494
author:
type: added
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Mutations::Issues::SetEpic do
let_it_be(:group) { create(:group) }
let_it_be(:project) { create(:project, group: group) }
let_it_be(:user) { create(:user) }
let_it_be_with_reload(:issue) { create(:issue, project: project) }
subject(:mutation) { described_class.new(object: nil, context: { current_user: user }, field: nil) }
describe '#resolve' do
let_it_be_with_reload(:epic) { create(:epic, group: group) }
let(:mutated_issue) { subject[:issue] }
subject { mutation.resolve(project_path: issue.project.full_path, iid: issue.iid, epic: epic) }
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 issue' do
before do
stub_licensed_features(epics: true)
project.add_developer(user)
end
it 'raises an error if the epic is not accessible to the user' do
expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
end
context 'when user can admin epic' do
before do
group.add_owner(user)
end
it 'returns the issue with the epic' do
expect(mutated_issue).to eq(issue)
expect(mutated_issue.epic).to eq(epic)
expect(subject[:errors]).to be_empty
end
it 'returns errors if issue could not be updated' do
issue.update_column(:author_id, nil)
expect(subject[:errors]).to eq(["Author can't be blank"])
end
context 'when passing epic_id as nil' do
let(:epic) { nil }
it 'removes the epic' do
issue.update!(epic: create(:epic, group: group))
expect(mutated_issue.epic).to eq(nil)
end
it 'does not do anything if the issue already does not have a epic' do
expect(mutated_issue.epic).to eq(nil)
end
end
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe 'Setting the epic of an issue' do
include GraphqlHelpers
let(:current_user) { create(:user) }
let(:group) { create(:group) }
let(:epic) { create(:epic, group: group) }
let(:project) { create(:project, group: group) }
let(:issue) { create(:issue, project: project) }
let(:input) { { epic_id: global_id_of(epic) } }
let(:mutation) do
graphql_mutation(
:issue_set_epic,
{ project_path: project.full_path, iid: issue.iid.to_s }.merge(input),
<<~GRAPHQL
clientMutationId
errors
issue {
iid
epic {
iid
title
}
}
GRAPHQL
)
end
def mutation_response
graphql_mutation_response(:issue_set_epic)
end
before do
project.add_developer(current_user)
group.add_developer(current_user)
stub_licensed_features(epics: true)
end
it 'returns an error if the user is not allowed to update the issue' do
error = "The resource that you are attempting to access does not exist or you "\
"don't have permission to perform this action"
post_graphql_mutation(mutation, current_user: create(:user))
expect(graphql_errors).to include(a_hash_including('message' => error))
end
it 'return an error if issue can not be updated' do
issue.update_column(:author_id, nil)
post_graphql_mutation(mutation, current_user: current_user)
expect(mutation_response["errors"]).to eq(["Author can't be blank"])
end
it 'sets given epic to the issue' do
post_graphql_mutation(mutation, current_user: current_user)
expect(response).to have_gitlab_http_status(:success)
expect(mutation_response['errors']).to be_empty
expect(mutation_response['issue']['epic']['iid']).to eq(epic.iid.to_s)
expect(mutation_response['issue']['epic']['title']).to eq(epic.title)
expect(issue.reload.epic).to eq(epic)
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