Commit 42594901 authored by Bob Van Landuyt's avatar Bob Van Landuyt

Merge branch '197227-graphql-group-milestones' into 'master'

Add issues to graphQL group endpoint

See merge request gitlab-org/gitlab!27789
parents fa582374 9218ed70
...@@ -56,12 +56,17 @@ module Resolvers ...@@ -56,12 +56,17 @@ module Resolvers
# The project could have been loaded in batch by `BatchLoader`. # The project could have been loaded in batch by `BatchLoader`.
# At this point we need the `id` of the project to query for issues, so # At this point we need the `id` of the project to query for issues, so
# make sure it's loaded and not `nil` before continuing. # make sure it's loaded and not `nil` before continuing.
project = object.respond_to?(:sync) ? object.sync : object parent = object.respond_to?(:sync) ? object.sync : object
return Issue.none if project.nil? return Issue.none if parent.nil?
if parent.is_a?(Group)
args[:group_id] = parent.id
else
args[:project_id] = parent.id
end
# Will need to be be made group & namespace aware with # Will need to be be made group & namespace aware with
# https://gitlab.com/gitlab-org/gitlab-foss/issues/54520 # https://gitlab.com/gitlab-org/gitlab-foss/issues/54520
args[:project_id] = project.id
args[:iids] ||= [args[:iid]].compact args[:iids] ||= [args[:iid]].compact
args[:attempt_project_search_optimizations] = args[:search].present? args[:attempt_project_search_optimizations] = args[:search].present?
......
...@@ -43,6 +43,12 @@ module Types ...@@ -43,6 +43,12 @@ module Types
description: 'Parent group', description: 'Parent group',
resolve: -> (obj, _args, _ctx) { Gitlab::Graphql::Loaders::BatchModelLoader.new(Group, obj.parent_id).find } resolve: -> (obj, _args, _ctx) { Gitlab::Graphql::Loaders::BatchModelLoader.new(Group, obj.parent_id).find }
field :issues,
Types::IssueType.connection_type,
null: true,
description: 'Issues of the group',
resolver: Resolvers::IssuesResolver
field :milestones, Types::MilestoneType.connection_type, null: true, field :milestones, Types::MilestoneType.connection_type, null: true,
description: 'Find milestones', description: 'Find milestones',
resolver: Resolvers::MilestoneResolver resolver: Resolvers::MilestoneResolver
......
---
title: Add issues to graphQL group endpoint
merge_request: 27789
author:
type: added
...@@ -3219,6 +3219,106 @@ type Group { ...@@ -3219,6 +3219,106 @@ type Group {
""" """
id: ID! id: ID!
"""
Issues of the group
"""
issues(
"""
Returns the elements in the list that come after the specified cursor.
"""
after: String
"""
ID of a user assigned to the issues, "none" and "any" values supported
"""
assigneeId: String
"""
Username of a user assigned to the issues
"""
assigneeUsername: String
"""
Returns the elements in the list that come before the specified cursor.
"""
before: String
"""
Issues closed after this date
"""
closedAfter: Time
"""
Issues closed before this date
"""
closedBefore: Time
"""
Issues created after this date
"""
createdAfter: Time
"""
Issues created before this date
"""
createdBefore: Time
"""
Returns the first _n_ elements from the list.
"""
first: Int
"""
IID of the issue. For example, "1"
"""
iid: String
"""
List of IIDs of issues. For example, [1, 2]
"""
iids: [String!]
"""
Labels applied to this issue
"""
labelName: [String]
"""
Returns the last _n_ elements from the list.
"""
last: Int
"""
Milestones applied to this issue
"""
milestoneTitle: [String]
"""
Search query for finding issues by title or description
"""
search: String
"""
Sort issues by this criteria
"""
sort: IssueSort = created_desc
"""
Current state of this issue
"""
state: IssuableState
"""
Issues updated after this date
"""
updatedAfter: Time
"""
Issues updated before this date
"""
updatedBefore: Time
): IssueConnection
""" """
Indicates if Large File Storage (LFS) is enabled for namespace Indicates if Large File Storage (LFS) is enabled for namespace
""" """
......
...@@ -9242,6 +9242,225 @@ ...@@ -9242,6 +9242,225 @@
"isDeprecated": false, "isDeprecated": false,
"deprecationReason": null "deprecationReason": null
}, },
{
"name": "issues",
"description": "Issues of the group",
"args": [
{
"name": "iid",
"description": "IID of the issue. For example, \"1\"",
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null
},
{
"name": "iids",
"description": "List of IIDs of issues. For example, [1, 2]",
"type": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
}
},
"defaultValue": null
},
{
"name": "state",
"description": "Current state of this issue",
"type": {
"kind": "ENUM",
"name": "IssuableState",
"ofType": null
},
"defaultValue": null
},
{
"name": "labelName",
"description": "Labels applied to this issue",
"type": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
},
"defaultValue": null
},
{
"name": "milestoneTitle",
"description": "Milestones applied to this issue",
"type": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
},
"defaultValue": null
},
{
"name": "assigneeUsername",
"description": "Username of a user assigned to the issues",
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null
},
{
"name": "assigneeId",
"description": "ID of a user assigned to the issues, \"none\" and \"any\" values supported",
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null
},
{
"name": "createdBefore",
"description": "Issues created before this date",
"type": {
"kind": "SCALAR",
"name": "Time",
"ofType": null
},
"defaultValue": null
},
{
"name": "createdAfter",
"description": "Issues created after this date",
"type": {
"kind": "SCALAR",
"name": "Time",
"ofType": null
},
"defaultValue": null
},
{
"name": "updatedBefore",
"description": "Issues updated before this date",
"type": {
"kind": "SCALAR",
"name": "Time",
"ofType": null
},
"defaultValue": null
},
{
"name": "updatedAfter",
"description": "Issues updated after this date",
"type": {
"kind": "SCALAR",
"name": "Time",
"ofType": null
},
"defaultValue": null
},
{
"name": "closedBefore",
"description": "Issues closed before this date",
"type": {
"kind": "SCALAR",
"name": "Time",
"ofType": null
},
"defaultValue": null
},
{
"name": "closedAfter",
"description": "Issues closed after this date",
"type": {
"kind": "SCALAR",
"name": "Time",
"ofType": null
},
"defaultValue": null
},
{
"name": "search",
"description": "Search query for finding issues by title or description",
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null
},
{
"name": "sort",
"description": "Sort issues by this criteria",
"type": {
"kind": "ENUM",
"name": "IssueSort",
"ofType": null
},
"defaultValue": "created_desc"
},
{
"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": "IssueConnection",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{ {
"name": "lfsEnabled", "name": "lfsEnabled",
"description": "Indicates if Large File Storage (LFS) is enabled for namespace", "description": "Indicates if Large File Storage (LFS) is enabled for namespace",
......
...@@ -7,15 +7,20 @@ describe Resolvers::IssuesResolver do ...@@ -7,15 +7,20 @@ describe Resolvers::IssuesResolver do
let(:current_user) { create(:user) } let(:current_user) { create(:user) }
context "with a project" do let_it_be(:group) { create(:group) }
let_it_be(:project) { create(:project) } let_it_be(:project) { create(:project, group: group) }
let_it_be(:milestone) { create(:milestone, project: project) } let_it_be(:other_project) { create(:project, group: group) }
let_it_be(:assignee) { create(:user) }
let_it_be(:issue1) { create(:issue, project: project, state: :opened, created_at: 3.hours.ago, updated_at: 3.hours.ago, milestone: milestone) } let_it_be(:milestone) { create(:milestone, project: project) }
let_it_be(:issue2) { create(:issue, project: project, state: :closed, title: 'foo', created_at: 1.hour.ago, updated_at: 1.hour.ago, closed_at: 1.hour.ago, assignees: [assignee]) } let_it_be(:assignee) { create(:user) }
let_it_be(:label1) { create(:label, project: project) } let_it_be(:issue1) { create(:issue, project: project, state: :opened, created_at: 3.hours.ago, updated_at: 3.hours.ago, milestone: milestone) }
let_it_be(:label2) { create(:label, project: project) } let_it_be(:issue2) { create(:issue, project: project, state: :closed, title: 'foo', created_at: 1.hour.ago, updated_at: 1.hour.ago, closed_at: 1.hour.ago, assignees: [assignee]) }
let_it_be(:issue3) { create(:issue, project: other_project, state: :closed, title: 'foo', created_at: 1.hour.ago, updated_at: 1.hour.ago, closed_at: 1.hour.ago, assignees: [assignee]) }
let_it_be(:issue4) { create(:issue) }
let_it_be(:label1) { create(:label, project: project) }
let_it_be(:label2) { create(:label, project: project) }
context "with a project" do
before do before do
project.add_developer(current_user) project.add_developer(current_user)
create(:label_link, label: label1, target: issue1) create(:label_link, label: label1, target: issue1)
...@@ -184,6 +189,20 @@ describe Resolvers::IssuesResolver do ...@@ -184,6 +189,20 @@ describe Resolvers::IssuesResolver do
end end
end end
context "with a group" do
before do
group.add_developer(current_user)
end
describe '#resolve' do
it 'finds all group issues' do
result = resolve(described_class, obj: group, ctx: { current_user: current_user })
expect(result).to contain_exactly(issue1, issue2, issue3)
end
end
end
context "when passing a non existent, batch loaded project" do context "when passing a non existent, batch loaded project" do
let(:project) do let(:project) do
BatchLoader::GraphQL.for("non-existent-path").batch do |_fake_paths, loader, _| BatchLoader::GraphQL.for("non-existent-path").batch do |_fake_paths, loader, _|
......
...@@ -51,6 +51,7 @@ describe 'getting group information', :do_not_mock_admin_mode do ...@@ -51,6 +51,7 @@ describe 'getting group information', :do_not_mock_admin_mode do
it "returns one of user1's groups" do it "returns one of user1's groups" do
project = create(:project, namespace: group2, path: 'Foo') project = create(:project, namespace: group2, path: 'Foo')
issue = create(:issue, project: create(:project, group: group1))
create(:project_group_link, project: project, group: group1) create(:project_group_link, project: project, group: group1)
post_graphql(group_query(group1), current_user: user1) post_graphql(group_query(group1), current_user: user1)
...@@ -67,6 +68,8 @@ describe 'getting group information', :do_not_mock_admin_mode do ...@@ -67,6 +68,8 @@ describe 'getting group information', :do_not_mock_admin_mode do
expect(graphql_data['group']['fullName']).to eq(group1.full_name) expect(graphql_data['group']['fullName']).to eq(group1.full_name)
expect(graphql_data['group']['fullPath']).to eq(group1.full_path) expect(graphql_data['group']['fullPath']).to eq(group1.full_path)
expect(graphql_data['group']['parentId']).to eq(group1.parent_id) expect(graphql_data['group']['parentId']).to eq(group1.parent_id)
expect(graphql_data['group']['issues']['nodes'].count).to eq(1)
expect(graphql_data['group']['issues']['nodes'][0]['iid']).to eq(issue.iid.to_s)
end end
it "does not return a non existing group" do it "does not return a non existing group" do
......
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