Commit 4ce3e71e authored by Heinrich Lee Yu's avatar Heinrich Lee Yu

Merge branch 'project_iterations' into 'master'

Support project-level iteration creation

See merge request gitlab-org/gitlab!38345
parents 210f7e9b 8f5ed601
...@@ -1964,7 +1964,12 @@ input CreateIterationInput { ...@@ -1964,7 +1964,12 @@ input CreateIterationInput {
""" """
The target group for the iteration The target group for the iteration
""" """
groupPath: ID! groupPath: ID
"""
The target project for the iteration
"""
projectPath: ID
""" """
The start date of the iteration The start date of the iteration
...@@ -9714,6 +9719,68 @@ type Project { ...@@ -9714,6 +9719,68 @@ type Project {
""" """
issuesEnabled: Boolean issuesEnabled: Boolean
"""
Find iterations
"""
iterations(
"""
Returns the elements in the list that come after the specified cursor.
"""
after: String
"""
Returns the elements in the list that come before the specified cursor.
"""
before: String
"""
List items within a time frame where items.end_date is between startDate and
endDate parameters (startDate parameter must be present)
"""
endDate: Time
"""
Returns the first _n_ elements from the list.
"""
first: Int
"""
The ID of the Iteration to look up
"""
id: ID
"""
The internal ID of the Iteration to look up
"""
iid: ID
"""
Whether to include ancestor iterations. Defaults to true
"""
includeAncestors: Boolean
"""
Returns the last _n_ elements from the list.
"""
last: Int
"""
List items within a time frame where items.start_date is between startDate
and endDate parameters (endDate parameter must be present)
"""
startDate: Time
"""
Filter iterations by state
"""
state: IterationState
"""
Fuzzy search by title
"""
title: String
): IterationConnection
""" """
Status of Jira import background job of the project Status of Jira import background job of the project
""" """
......
...@@ -5202,13 +5202,19 @@ ...@@ -5202,13 +5202,19 @@
"name": "groupPath", "name": "groupPath",
"description": "The target group for the iteration", "description": "The target group for the iteration",
"type": { "type": {
"kind": "NON_NULL", "kind": "SCALAR",
"name": null, "name": "ID",
"ofType": { "ofType": null
"kind": "SCALAR", },
"name": "ID", "defaultValue": null
"ofType": null },
} {
"name": "projectPath",
"description": "The target project for the iteration",
"type": {
"kind": "SCALAR",
"name": "ID",
"ofType": null
}, },
"defaultValue": null "defaultValue": null
}, },
...@@ -28960,6 +28966,129 @@ ...@@ -28960,6 +28966,129 @@
"isDeprecated": false, "isDeprecated": false,
"deprecationReason": null "deprecationReason": null
}, },
{
"name": "iterations",
"description": "Find iterations",
"args": [
{
"name": "startDate",
"description": "List items within a time frame where items.start_date is between startDate and endDate parameters (endDate parameter must be present)",
"type": {
"kind": "SCALAR",
"name": "Time",
"ofType": null
},
"defaultValue": null
},
{
"name": "endDate",
"description": "List items within a time frame where items.end_date is between startDate and endDate parameters (startDate parameter must be present)",
"type": {
"kind": "SCALAR",
"name": "Time",
"ofType": null
},
"defaultValue": null
},
{
"name": "state",
"description": "Filter iterations by state",
"type": {
"kind": "ENUM",
"name": "IterationState",
"ofType": null
},
"defaultValue": null
},
{
"name": "title",
"description": "Fuzzy search by title",
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null
},
{
"name": "id",
"description": "The ID of the Iteration to look up",
"type": {
"kind": "SCALAR",
"name": "ID",
"ofType": null
},
"defaultValue": null
},
{
"name": "iid",
"description": "The internal ID of the Iteration to look up",
"type": {
"kind": "SCALAR",
"name": "ID",
"ofType": null
},
"defaultValue": null
},
{
"name": "includeAncestors",
"description": "Whether to include ancestor iterations. Defaults to true",
"type": {
"kind": "SCALAR",
"name": "Boolean",
"ofType": null
},
"defaultValue": null
},
{
"name": "after",
"description": "Returns the elements in the list that come after the specified cursor.",
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null
},
{
"name": "before",
"description": "Returns the elements in the list that come before the specified cursor.",
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null
},
{
"name": "first",
"description": "Returns the first _n_ elements from the list.",
"type": {
"kind": "SCALAR",
"name": "Int",
"ofType": null
},
"defaultValue": null
},
{
"name": "last",
"description": "Returns the last _n_ elements from the list.",
"type": {
"kind": "SCALAR",
"name": "Int",
"ofType": null
},
"defaultValue": null
}
],
"type": {
"kind": "OBJECT",
"name": "IterationConnection",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{ {
"name": "jiraImportStatus", "name": "jiraImportStatus",
"description": "Status of Jira import background job of the project", "description": "Status of Jira import background job of the project",
...@@ -60,6 +60,10 @@ module EE ...@@ -60,6 +60,10 @@ module EE
Rails.application.routes.url_helpers.project_security_dashboard_index_path(project) Rails.application.routes.url_helpers.project_security_dashboard_index_path(project)
end end
field :iterations, ::Types::IterationType.connection_type, null: true,
description: 'Find iterations',
resolver: ::Resolvers::IterationsResolver
def self.requirements_available?(project, user) def self.requirements_available?(project, user)
::Feature.enabled?(:requirements_management, project, default_enabled: true) && Ability.allowed?(user, :read_requirement, project) ::Feature.enabled?(:requirements_management, project, default_enabled: true) && Ability.allowed?(user, :read_requirement, project)
end end
......
...@@ -4,6 +4,7 @@ module Mutations ...@@ -4,6 +4,7 @@ module Mutations
module Iterations module Iterations
class Create < BaseMutation class Create < BaseMutation
include Mutations::ResolvesGroup include Mutations::ResolvesGroup
include ResolvesProject
graphql_name 'CreateIteration' graphql_name 'CreateIteration'
...@@ -15,9 +16,13 @@ module Mutations ...@@ -15,9 +16,13 @@ module Mutations
description: 'The created iteration' description: 'The created iteration'
argument :group_path, GraphQL::ID_TYPE, argument :group_path, GraphQL::ID_TYPE,
required: true, required: false,
description: "The target group for the iteration" description: "The target group for the iteration"
argument :project_path, GraphQL::ID_TYPE,
required: false,
description: "The target project for the iteration"
argument :title, argument :title,
GraphQL::STRING_TYPE, GraphQL::STRING_TYPE,
required: false, required: false,
...@@ -39,12 +44,11 @@ module Mutations ...@@ -39,12 +44,11 @@ module Mutations
description: 'The end date of the iteration' description: 'The end date of the iteration'
def resolve(args) def resolve(args)
group_path = args.delete(:group_path)
validate_arguments!(args) validate_arguments!(args)
group = authorized_find!(group_path: group_path) parent = find_parent(args)
response = ::Iterations::CreateService.new(group, current_user, args).execute
response = ::Iterations::CreateService.new(parent, current_user, args).execute
response_object = response.payload[:iteration] if response.success? response_object = response.payload[:iteration] if response.success?
response_errors = response.error? ? response.payload[:errors].full_messages : [] response_errors = response.error? ? response.payload[:errors].full_messages : []
...@@ -57,15 +61,40 @@ module Mutations ...@@ -57,15 +61,40 @@ module Mutations
private private
def find_object(group_path:) def find_object(group_path: nil, project_path: nil)
resolve_group(full_path: group_path) if group_path
resolve_group(full_path: group_path)
elsif project_path
resolve_project(full_path: project_path)
end
end
def find_parent(args)
group_path = args.delete(:group_path)
project_path = args.delete(:project_path)
if group_path
authorized_find!(group_path: group_path)
elsif project_path
authorized_find!(project_path: project_path)
end
end end
def validate_arguments!(args) def validate_arguments!(args)
if args.empty? if args.except(:group_path, :project_path).empty?
raise Gitlab::Graphql::Errors::ArgumentError, raise Gitlab::Graphql::Errors::ArgumentError,
'The list of iteration attributes is empty' 'The list of iteration attributes is empty'
end end
if args[:group_path].present? && args[:project_path].present?
raise Gitlab::Graphql::Errors::ArgumentError,
'Only one of group_path or project_path can be provided'
end
if args[:group_path].nil? && args[:project_path].nil?
raise Gitlab::Graphql::Errors::ArgumentError,
'Either group_path or project_path is required'
end
end end
end end
end end
......
---
title: Support project-level iteration creation via GraphQL
merge_request: 38345
author:
type: added
...@@ -14,7 +14,7 @@ module EE ...@@ -14,7 +14,7 @@ module EE
when Epic when Epic
instance.group_epic_url(object.group, object, **options) instance.group_epic_url(object.group, object, **options)
when Iteration when Iteration
instance.group_iteration_url(object.group, object, **options) instance.iteration_url(object, **options)
when Vulnerability when Vulnerability
instance.project_security_vulnerability_url(object.project, object, **options) instance.project_security_vulnerability_url(object.project, object, **options)
else else
......
...@@ -17,7 +17,7 @@ RSpec.describe GitlabSchema.types['Project'] do ...@@ -17,7 +17,7 @@ RSpec.describe GitlabSchema.types['Project'] do
expected_fields = %w[ expected_fields = %w[
vulnerabilities vulnerability_scanners requirement_states_count vulnerabilities vulnerability_scanners requirement_states_count
vulnerability_severities_count packages compliance_frameworks vulnerability_severities_count packages compliance_frameworks
security_dashboard_path security_dashboard_path iterations
] ]
expect(described_class).to include_graphql_fields(*expected_fields) expect(described_class).to include_graphql_fields(*expected_fields)
......
...@@ -19,10 +19,14 @@ RSpec.describe 'Creating an Iteration' do ...@@ -19,10 +19,14 @@ RSpec.describe 'Creating an Iteration' do
} }
end end
let(:mutation) do let(:params) do
params = { group_path: group.full_path }.merge(attributes) {
group_path: group.full_path
}
end
graphql_mutation(:create_iteration, params) let(:mutation) do
graphql_mutation(:create_iteration, params.merge(attributes))
end end
def mutation_response def mutation_response
...@@ -63,7 +67,7 @@ RSpec.describe 'Creating an Iteration' do ...@@ -63,7 +67,7 @@ RSpec.describe 'Creating an Iteration' do
stub_licensed_features(iterations: true) stub_licensed_features(iterations: true)
end end
it 'creates the iteration' do it 'creates the iteration for a group' do
post_graphql_mutation(mutation, current_user: current_user) post_graphql_mutation(mutation, current_user: current_user)
iteration_hash = mutation_response['iteration'] iteration_hash = mutation_response['iteration']
...@@ -75,6 +79,27 @@ RSpec.describe 'Creating an Iteration' do ...@@ -75,6 +79,27 @@ RSpec.describe 'Creating an Iteration' do
end end
end end
context 'when a project_path is given' do
let_it_be(:project) { create(:project, namespace: group) }
let(:params) { { project_path: project.full_path } }
before do
project.add_developer(current_user)
end
it 'creates the iteration for a project' do
post_graphql_mutation(mutation, current_user: current_user)
iteration_hash = mutation_response['iteration']
aggregate_failures do
expect(iteration_hash['title']).to eq('title')
expect(iteration_hash['description']).to eq('some description')
expect(iteration_hash['startDate']).to eq(start_date)
expect(iteration_hash['dueDate']).to eq(end_date)
end
end
end
context 'when there are ActiveRecord validation errors' do context 'when there are ActiveRecord validation errors' do
let(:attributes) { { title: '' } } let(:attributes) { { title: '' } }
...@@ -96,6 +121,28 @@ RSpec.describe 'Creating an Iteration' do ...@@ -96,6 +121,28 @@ RSpec.describe 'Creating an Iteration' do
expect { post_graphql_mutation(mutation, current_user: current_user) }.not_to change(Iteration, :count) expect { post_graphql_mutation(mutation, current_user: current_user) }.not_to change(Iteration, :count)
end end
end end
context 'when the params contains neither group nor project path' do
let(:params) { {} }
it_behaves_like 'a mutation that returns top-level errors',
errors: ['Either group_path or project_path is required']
it 'does not create the iteration' do
expect { post_graphql_mutation(mutation, current_user: current_user) }.not_to change(Iteration, :count)
end
end
context 'when the params contains both group and project path' do
let(:params) { { group_path: group.full_path, project_path: 'doesnotreallymatter' } }
it_behaves_like 'a mutation that returns top-level errors',
errors: ['Only one of group_path or project_path can be provided']
it 'does not create the iteration' do
expect { post_graphql_mutation(mutation, current_user: current_user) }.not_to change(Iteration, :count)
end
end
end end
end 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