Commit b95ad734 authored by Jan Provaznik's avatar Jan Provaznik

Merge branch '36309-graphql-mutation-for-updating-epic-associated-with-issue-2' into 'master'

Add GraphQL migration to set epic of an issue

Closes #36309

See merge request gitlab-org/gitlab!25628
parents bac5b68d 2d8bccaf
...@@ -2219,6 +2219,61 @@ type Epic implements Noteable { ...@@ -2219,6 +2219,61 @@ type Epic implements Noteable {
webUrl: String! webUrl: String!
} }
"""
Autogenerated input type of EpicAddIssue
"""
input EpicAddIssueInput {
"""
A unique identifier for the client performing the mutation.
"""
clientMutationId: String
"""
The group the epic to mutate belongs to
"""
groupPath: ID!
"""
The iid of the epic to mutate
"""
iid: ID!
"""
The iid of the issue to be added
"""
issueIid: String!
"""
The project the issue belongs to
"""
projectPath: ID!
}
"""
Autogenerated return type of EpicAddIssue
"""
type EpicAddIssuePayload {
"""
A unique identifier for the client performing the mutation.
"""
clientMutationId: String
"""
The epic after mutation
"""
epic: Epic
"""
The epic-issue relation
"""
epicIssue: EpicIssue
"""
Reasons why the mutation failed.
"""
errors: [String!]!
}
""" """
The connection type for Epic. The connection type for Epic.
""" """
...@@ -2689,7 +2744,7 @@ input EpicSetSubscriptionInput { ...@@ -2689,7 +2744,7 @@ input EpicSetSubscriptionInput {
clientMutationId: String clientMutationId: String
""" """
The group the epic to mutate is in The group the epic to mutate belongs to
""" """
groupPath: ID! groupPath: ID!
...@@ -4872,6 +4927,7 @@ type Mutation { ...@@ -4872,6 +4927,7 @@ type Mutation {
designManagementUpload(input: DesignManagementUploadInput!): DesignManagementUploadPayload designManagementUpload(input: DesignManagementUploadInput!): DesignManagementUploadPayload
destroyNote(input: DestroyNoteInput!): DestroyNotePayload destroyNote(input: DestroyNoteInput!): DestroyNotePayload
destroySnippet(input: DestroySnippetInput!): DestroySnippetPayload destroySnippet(input: DestroySnippetInput!): DestroySnippetPayload
epicAddIssue(input: EpicAddIssueInput!): EpicAddIssuePayload
epicSetSubscription(input: EpicSetSubscriptionInput!): EpicSetSubscriptionPayload epicSetSubscription(input: EpicSetSubscriptionInput!): EpicSetSubscriptionPayload
epicTreeReorder(input: EpicTreeReorderInput!): EpicTreeReorderPayload epicTreeReorder(input: EpicTreeReorderInput!): EpicTreeReorderPayload
issueSetConfidential(input: IssueSetConfidentialInput!): IssueSetConfidentialPayload issueSetConfidential(input: IssueSetConfidentialInput!): IssueSetConfidentialPayload
......
...@@ -19402,6 +19402,33 @@ ...@@ -19402,6 +19402,33 @@
"isDeprecated": false, "isDeprecated": false,
"deprecationReason": null "deprecationReason": null
}, },
{
"name": "epicAddIssue",
"description": null,
"args": [
{
"name": "input",
"description": null,
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "INPUT_OBJECT",
"name": "EpicAddIssueInput",
"ofType": null
}
},
"defaultValue": null
}
],
"type": {
"kind": "OBJECT",
"name": "EpicAddIssuePayload",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{ {
"name": "epicSetSubscription", "name": "epicSetSubscription",
"description": null, "description": null,
...@@ -25120,7 +25147,7 @@ ...@@ -25120,7 +25147,7 @@
}, },
{ {
"name": "groupPath", "name": "groupPath",
"description": "The group the epic to mutate is in", "description": "The group the epic to mutate belongs to",
"type": { "type": {
"kind": "NON_NULL", "kind": "NON_NULL",
"name": null, "name": null,
...@@ -25161,6 +25188,164 @@ ...@@ -25161,6 +25188,164 @@
"enumValues": null, "enumValues": null,
"possibleTypes": null "possibleTypes": null
}, },
{
"kind": "OBJECT",
"name": "EpicAddIssuePayload",
"description": "Autogenerated return type of EpicAddIssue",
"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": "epic",
"description": "The epic after mutation",
"args": [
],
"type": {
"kind": "OBJECT",
"name": "Epic",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "epicIssue",
"description": "The epic-issue relation",
"args": [
],
"type": {
"kind": "OBJECT",
"name": "EpicIssue",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "errors",
"description": "Reasons why the mutation failed.",
"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
}
],
"inputFields": null,
"interfaces": [
],
"enumValues": null,
"possibleTypes": null
},
{
"kind": "INPUT_OBJECT",
"name": "EpicAddIssueInput",
"description": "Autogenerated input type of EpicAddIssue",
"fields": null,
"inputFields": [
{
"name": "iid",
"description": "The iid of the epic to mutate",
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "ID",
"ofType": null
}
},
"defaultValue": null
},
{
"name": "groupPath",
"description": "The group the epic to mutate belongs to",
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "ID",
"ofType": null
}
},
"defaultValue": null
},
{
"name": "projectPath",
"description": "The project the issue belongs to",
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "ID",
"ofType": null
}
},
"defaultValue": null
},
{
"name": "issueIid",
"description": "The iid of the issue to be added",
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"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", "kind": "OBJECT",
"name": "__Schema", "name": "__Schema",
......
...@@ -346,6 +346,17 @@ Represents an epic. ...@@ -346,6 +346,17 @@ Represents an epic.
| `webPath` | String! | Web path of the epic | | `webPath` | String! | Web path of the epic |
| `webUrl` | String! | Web URL of the epic | | `webUrl` | String! | Web URL of the epic |
## EpicAddIssuePayload
Autogenerated return type of EpicAddIssue
| Name | Type | Description |
| --- | ---- | ---------- |
| `clientMutationId` | String | A unique identifier for the client performing the mutation. |
| `epic` | Epic | The epic after mutation |
| `epicIssue` | EpicIssue | The epic-issue relation |
| `errors` | String! => Array | Reasons why the mutation failed. |
## EpicDescendantCount ## EpicDescendantCount
Counts of descendent epics. Counts of descendent epics.
......
...@@ -13,6 +13,7 @@ module EE ...@@ -13,6 +13,7 @@ module EE
mount_mutation ::Mutations::Epics::Update mount_mutation ::Mutations::Epics::Update
mount_mutation ::Mutations::Epics::Create mount_mutation ::Mutations::Epics::Create
mount_mutation ::Mutations::Epics::SetSubscription mount_mutation ::Mutations::Epics::SetSubscription
mount_mutation ::Mutations::Epics::AddIssue
end end
end end
end end
......
# frozen_string_literal: true
module Mutations
module Epics
class AddIssue < Base
include Mutations::ResolvesIssuable
graphql_name 'EpicAddIssue'
authorize :admin_epic
argument :project_path, GraphQL::ID_TYPE,
required: true,
description: 'The project the issue belongs to'
argument :issue_iid, GraphQL::STRING_TYPE,
required: true,
description: 'The iid of the issue to be added'
field :epic_issue,
Types::EpicIssueType,
null: true,
description: 'The epic-issue relation'
def resolve(group_path:, iid:, project_path:, issue_iid:)
epic = authorized_find!(group_path: group_path, iid: iid)
issue = resolve_issuable(type: :issue, parent_path: project_path, iid: issue_iid)
service = create_epic_issue(epic, issue)
epic_issue = service[:status] == :success ? find_epic_issue(epic, issue) : nil
{
epic_issue: epic_issue,
errors: service[:message] || []
}
end
private
def create_epic_issue(epic, issue)
::EpicIssues::CreateService.new(epic, current_user, { target_issuable: issue }).execute
end
def find_epic_issue(epic, issue)
Epic.related_issues(ids: epic.id).find_by_id(issue.id)
end
end
end
end
...@@ -11,7 +11,7 @@ module Mutations ...@@ -11,7 +11,7 @@ module Mutations
argument :group_path, GraphQL::ID_TYPE, argument :group_path, GraphQL::ID_TYPE,
required: true, required: true,
description: 'The group the epic to mutate is in' description: 'The group the epic to mutate belongs to'
field :epic, field :epic,
Types::EpicType, Types::EpicType,
......
---
title: Add GraphQL mutation to set the epic of an issue
merge_request: 25628
author:
type: added
# frozen_string_literal: true
require 'spec_helper'
describe Mutations::Epics::AddIssue do
let_it_be(:group) { create(:group) }
let_it_be(:project) { create(:project, namespace: group) }
let_it_be(:epic) { create(:epic, group: group) }
let(:user) { issue.author }
let(:issue) { create(:issue, project: project) }
subject(:mutation) { described_class.new(object: group, context: { current_user: user }) }
describe '#resolve' do
subject do
mutation.resolve(
group_path: group.full_path,
iid: epic.iid,
issue_iid: issue.iid,
project_path: project.full_path
)
end
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 epic' do
before do
stub_licensed_features(epics: true)
group.add_developer(user)
end
it 'adds the issue to the epic' do
expect(subject[:epic_issue]).to eq(issue)
expect(subject[:epic_issue].epic).to eq(epic)
expect(issue.reload.epic).to eq(epic)
expect(subject[:errors]).to be_empty
end
it 'returns error if the issue is already assigned to the epic' do
issue.update!(epic: epic)
expect(subject[:errors]).to eq('Issue(s) already assigned')
end
it 'returns error if issue is not found' do
issue.update!(project: create(:project))
expect(subject[:errors]).to eq('No Issue found for given params')
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
describe 'Add an issue to an Epic' do
include GraphqlHelpers
let_it_be(:current_user) { create(:user) }
let_it_be(:group) { create(:group) }
let_it_be(:project) { create(:project, namespace: group) }
let(:epic) { create(:epic, group: group) }
let(:issue) { create(:issue, project: project) }
let(:mutation) do
params = { group_path: group.full_path, iid: epic.iid.to_s, issue_iid: issue.iid.to_s, project_path: project.full_path }
graphql_mutation(:epic_add_issue, params)
end
shared_examples 'mutation without access' do
it_behaves_like 'a mutation that returns top-level errors',
errors: ['The resource that you are attempting to access does not exist '\
'or you don\'t have permission to perform this action']
it 'does not add issue to the epic' do
post_graphql_mutation(mutation, current_user: current_user)
expect(issue.epic).to be_nil
end
end
context 'when epics feature is disabled' do
it_behaves_like 'mutation without access'
end
context 'when epics feature is enabled' do
before do
stub_licensed_features(epics: true)
end
context 'when the user is a group member' do
before do
group.add_developer(current_user)
end
it 'adds the issue to the epic' do
post_graphql_mutation(mutation, current_user: current_user)
response = graphql_mutation_response(:epic_add_issue)
expect(response['errors']).to be_empty
expect(response['epicIssue']['iid']).to eq(issue.iid.to_s)
expect(response['epicIssue']['epic']['iid']).to eq(epic.iid.to_s)
expect(issue.epic).to eq(epic)
end
end
context 'when the user is not a group member' do
it_behaves_like 'mutation without access'
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