diff --git a/app/graphql/resolvers/issues_resolver.rb b/app/graphql/resolvers/issues_resolver.rb index ae77af32b5b85cb671f0769064f924468f7d23b7..04da54a6bb69fee93a87c67961a8c59f09f46741 100644 --- a/app/graphql/resolvers/issues_resolver.rb +++ b/app/graphql/resolvers/issues_resolver.rb @@ -56,12 +56,17 @@ def resolve(**args) # 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 # make sure it's loaded and not `nil` before continuing. - project = object.respond_to?(:sync) ? object.sync : object - return Issue.none if project.nil? + parent = object.respond_to?(:sync) ? object.sync : object + 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 # https://gitlab.com/gitlab-org/gitlab-foss/issues/54520 - args[:project_id] = project.id args[:iids] ||= [args[:iid]].compact args[:attempt_project_search_optimizations] = args[:search].present? diff --git a/app/graphql/types/group_type.rb b/app/graphql/types/group_type.rb index bd9efef94f8b194253606375e559c778a75c68de..20b4c66ba9510361dec04df85e54fb7c97eb796a 100644 --- a/app/graphql/types/group_type.rb +++ b/app/graphql/types/group_type.rb @@ -43,6 +43,12 @@ class GroupType < NamespaceType description: 'Parent group', 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, description: 'Find milestones', resolver: Resolvers::MilestoneResolver diff --git a/changelogs/unreleased/197227-graphql-group-milestones.yml b/changelogs/unreleased/197227-graphql-group-milestones.yml new file mode 100644 index 0000000000000000000000000000000000000000..8acd40ca2f05b7d05bc6ef0c90bba7abb2479081 --- /dev/null +++ b/changelogs/unreleased/197227-graphql-group-milestones.yml @@ -0,0 +1,5 @@ +--- +title: Add issues to graphQL group endpoint +merge_request: 27789 +author: +type: added diff --git a/doc/api/graphql/reference/gitlab_schema.graphql b/doc/api/graphql/reference/gitlab_schema.graphql index ea11a2039212a8eceb46bfdd572122110417e738..0fbd8c84e5800100d0097e59a51717ed84255825 100644 --- a/doc/api/graphql/reference/gitlab_schema.graphql +++ b/doc/api/graphql/reference/gitlab_schema.graphql @@ -3219,6 +3219,106 @@ type Group { """ 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 """ diff --git a/doc/api/graphql/reference/gitlab_schema.json b/doc/api/graphql/reference/gitlab_schema.json index 9e3460c0b03fd7dcf2b329dba0022c7e8731c44b..bd78b51684f36ef11a838134225ebbc3779f7c27 100644 --- a/doc/api/graphql/reference/gitlab_schema.json +++ b/doc/api/graphql/reference/gitlab_schema.json @@ -9242,6 +9242,225 @@ "isDeprecated": false, "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", "description": "Indicates if Large File Storage (LFS) is enabled for namespace", diff --git a/spec/graphql/resolvers/issues_resolver_spec.rb b/spec/graphql/resolvers/issues_resolver_spec.rb index 7cfef9b4cc7be395436789dfaae426bf2732b052..4467c228e9614d5e708aeb3b5d3224879892e422 100644 --- a/spec/graphql/resolvers/issues_resolver_spec.rb +++ b/spec/graphql/resolvers/issues_resolver_spec.rb @@ -7,15 +7,20 @@ let(:current_user) { create(:user) } - context "with a project" do - let_it_be(:project) { create(:project) } - let_it_be(:milestone) { create(:milestone, project: project) } - 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(: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(:label1) { create(:label, project: project) } - let_it_be(:label2) { create(:label, project: project) } + let_it_be(:group) { create(:group) } + let_it_be(:project) { create(:project, group: group) } + let_it_be(:other_project) { create(:project, group: group) } + + let_it_be(:milestone) { create(:milestone, project: project) } + 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(: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 project.add_developer(current_user) create(:label_link, label: label1, target: issue1) @@ -184,6 +189,20 @@ 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 let(:project) do BatchLoader::GraphQL.for("non-existent-path").batch do |_fake_paths, loader, _| diff --git a/spec/requests/api/graphql/group_query_spec.rb b/spec/requests/api/graphql/group_query_spec.rb index a38d1857076dec608c2502ef01011de8f6b39ea2..c7b537a9923df1c188c9e47a511052aea8377c77 100644 --- a/spec/requests/api/graphql/group_query_spec.rb +++ b/spec/requests/api/graphql/group_query_spec.rb @@ -51,6 +51,7 @@ def group_query(group) it "returns one of user1's groups" do project = create(:project, namespace: group2, path: 'Foo') + issue = create(:issue, project: create(:project, group: group1)) create(:project_group_link, project: project, group: group1) post_graphql(group_query(group1), current_user: user1) @@ -67,6 +68,8 @@ def group_query(group) expect(graphql_data['group']['fullName']).to eq(group1.full_name) expect(graphql_data['group']['fullPath']).to eq(group1.full_path) 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 it "does not return a non existing group" do