Commit dd43ba1e authored by Sean Arnold's avatar Sean Arnold Committed by Mayra Cabrera

Add issue_type filter for issues in graphql

- Add type
- Add resolver
- Add scope to model
parent 98a5301e
...@@ -25,6 +25,7 @@ ...@@ -25,6 +25,7 @@
# updated_after: datetime # updated_after: datetime
# updated_before: datetime # updated_before: datetime
# confidential: boolean # confidential: boolean
# issue_type: array of strings (one of Issue.issue_types)
# #
class IssuesFinder < IssuableFinder class IssuesFinder < IssuableFinder
CONFIDENTIAL_ACCESS_LEVEL = Gitlab::Access::REPORTER CONFIDENTIAL_ACCESS_LEVEL = Gitlab::Access::REPORTER
...@@ -73,6 +74,7 @@ class IssuesFinder < IssuableFinder ...@@ -73,6 +74,7 @@ class IssuesFinder < IssuableFinder
issues = super issues = super
issues = by_due_date(issues) issues = by_due_date(issues)
issues = by_confidential(issues) issues = by_confidential(issues)
issues = by_issue_types(issues)
issues issues
end end
...@@ -97,6 +99,14 @@ class IssuesFinder < IssuableFinder ...@@ -97,6 +99,14 @@ class IssuesFinder < IssuableFinder
items.due_between(Date.today - 2.weeks, (Date.today + 1.month).end_of_month) items.due_between(Date.today - 2.weeks, (Date.today + 1.month).end_of_month)
end end
end end
def by_issue_types(items)
issue_type_params = Array(params[:issue_types]).map(&:to_s)
return items if issue_type_params.blank?
return Issue.none unless (Issue.issue_types.keys & issue_type_params).sort == issue_type_params.sort
items.with_issue_type(params[:issue_types])
end
end end
IssuesFinder.prepend_if_ee('EE::IssuesFinder') IssuesFinder.prepend_if_ee('EE::IssuesFinder')
...@@ -49,6 +49,10 @@ module Resolvers ...@@ -49,6 +49,10 @@ module Resolvers
description: 'Sort issues by this criteria', description: 'Sort issues by this criteria',
required: false, required: false,
default_value: 'created_desc' default_value: 'created_desc'
argument :types, [Types::IssueTypeEnum],
as: :issue_types,
description: 'Filter issues by the given issue types',
required: false
type Types::IssueType, null: true type Types::IssueType, null: true
......
...@@ -97,6 +97,10 @@ module Types ...@@ -97,6 +97,10 @@ module Types
field :design_collection, Types::DesignManagement::DesignCollectionType, null: true, field :design_collection, Types::DesignManagement::DesignCollectionType, null: true,
description: 'Collection of design images associated with this issue' description: 'Collection of design images associated with this issue'
field :type, Types::IssueTypeEnum, null: true,
method: :issue_type,
description: 'Type of the issue'
end end
end end
......
# frozen_string_literal: true
module Types
class IssueTypeEnum < BaseEnum
graphql_name 'IssueType'
description 'Issue type'
::Issue.issue_types.keys.each do |issue_type|
value issue_type.upcase, value: issue_type, description: "#{issue_type.titleize} issue type"
end
end
end
...@@ -106,6 +106,7 @@ class Issue < ApplicationRecord ...@@ -106,6 +106,7 @@ class Issue < ApplicationRecord
milestone: { project: [:route, { namespace: :route }] }, milestone: { project: [:route, { namespace: :route }] },
project: [:route, { namespace: :route }]) project: [:route, { namespace: :route }])
} }
scope :with_issue_type, ->(types) { where(issue_type: types) }
scope :public_only, -> { where(confidential: false) } scope :public_only, -> { where(confidential: false) }
scope :confidential_only, -> { where(confidential: true) } scope :confidential_only, -> { where(confidential: true) }
......
---
title: Filter Issues in GraphQL by type of Issue
merge_request: 38017
author:
type: added
...@@ -4505,6 +4505,11 @@ type EpicIssue implements Noteable { ...@@ -4505,6 +4505,11 @@ type EpicIssue implements Noteable {
""" """
totalTimeSpent: Int! totalTimeSpent: Int!
"""
Type of the issue
"""
type: IssueType
""" """
Timestamp of when the issue was last updated Timestamp of when the issue was last updated
""" """
...@@ -5263,6 +5268,11 @@ type Group { ...@@ -5263,6 +5268,11 @@ type Group {
""" """
state: IssuableState state: IssuableState
"""
Filter issues by the given issue types
"""
types: [IssueType!]
""" """
Issues updated after this date Issues updated after this date
""" """
...@@ -6138,6 +6148,11 @@ type Issue implements Noteable { ...@@ -6138,6 +6148,11 @@ type Issue implements Noteable {
""" """
totalTimeSpent: Int! totalTimeSpent: Int!
"""
Type of the issue
"""
type: IssueType
""" """
Timestamp of when the issue was last updated Timestamp of when the issue was last updated
""" """
...@@ -6669,6 +6684,21 @@ enum IssueState { ...@@ -6669,6 +6684,21 @@ enum IssueState {
opened opened
} }
"""
Issue type
"""
enum IssueType {
"""
Incident issue type
"""
INCIDENT
"""
Issue issue type
"""
ISSUE
}
""" """
Represents an iteration object. Represents an iteration object.
""" """
...@@ -9593,6 +9623,11 @@ type Project { ...@@ -9593,6 +9623,11 @@ type Project {
""" """
state: IssuableState state: IssuableState
"""
Filter issues by the given issue types
"""
types: [IssueType!]
""" """
Issues updated after this date Issues updated after this date
""" """
...@@ -9698,6 +9733,11 @@ type Project { ...@@ -9698,6 +9733,11 @@ type Project {
""" """
state: IssuableState state: IssuableState
"""
Filter issues by the given issue types
"""
types: [IssueType!]
""" """
Issues updated after this date Issues updated after this date
""" """
......
...@@ -12575,6 +12575,20 @@ ...@@ -12575,6 +12575,20 @@
"isDeprecated": false, "isDeprecated": false,
"deprecationReason": null "deprecationReason": null
}, },
{
"name": "type",
"description": "Type of the issue",
"args": [
],
"type": {
"kind": "ENUM",
"name": "IssueType",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{ {
"name": "updatedAt", "name": "updatedAt",
"description": "Timestamp of when the issue was last updated", "description": "Timestamp of when the issue was last updated",
...@@ -14575,6 +14589,24 @@ ...@@ -14575,6 +14589,24 @@
}, },
"defaultValue": "created_desc" "defaultValue": "created_desc"
}, },
{
"name": "types",
"description": "Filter issues by the given issue types",
"type": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "ENUM",
"name": "IssueType",
"ofType": null
}
}
},
"defaultValue": null
},
{ {
"name": "iterationId", "name": "iterationId",
"description": "Iterations applied to the issue", "description": "Iterations applied to the issue",
...@@ -16931,6 +16963,20 @@ ...@@ -16931,6 +16963,20 @@
"isDeprecated": false, "isDeprecated": false,
"deprecationReason": null "deprecationReason": null
}, },
{
"name": "type",
"description": "Type of the issue",
"args": [
],
"type": {
"kind": "ENUM",
"name": "IssueType",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{ {
"name": "updatedAt", "name": "updatedAt",
"description": "Timestamp of when the issue was last updated", "description": "Timestamp of when the issue was last updated",
...@@ -18412,6 +18458,29 @@ ...@@ -18412,6 +18458,29 @@
], ],
"possibleTypes": null "possibleTypes": null
}, },
{
"kind": "ENUM",
"name": "IssueType",
"description": "Issue type",
"fields": null,
"inputFields": null,
"interfaces": null,
"enumValues": [
{
"name": "ISSUE",
"description": "Issue issue type",
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "INCIDENT",
"description": "Incident issue type",
"isDeprecated": false,
"deprecationReason": null
}
],
"possibleTypes": null
},
{ {
"kind": "OBJECT", "kind": "OBJECT",
"name": "Iteration", "name": "Iteration",
...@@ -28690,6 +28759,24 @@ ...@@ -28690,6 +28759,24 @@
}, },
"defaultValue": "created_desc" "defaultValue": "created_desc"
}, },
{
"name": "types",
"description": "Filter issues by the given issue types",
"type": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "ENUM",
"name": "IssueType",
"ofType": null
}
}
},
"defaultValue": null
},
{ {
"name": "iterationId", "name": "iterationId",
"description": "Iterations applied to the issue", "description": "Iterations applied to the issue",
...@@ -28883,6 +28970,24 @@ ...@@ -28883,6 +28970,24 @@
}, },
"defaultValue": "created_desc" "defaultValue": "created_desc"
}, },
{
"name": "types",
"description": "Filter issues by the given issue types",
"type": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "ENUM",
"name": "IssueType",
"ofType": null
}
}
},
"defaultValue": null
},
{ {
"name": "iterationId", "name": "iterationId",
"description": "Iterations applied to the issue", "description": "Iterations applied to the issue",
...@@ -756,6 +756,7 @@ Relationship between an epic and an issue ...@@ -756,6 +756,7 @@ Relationship between an epic and an issue
| `title` | String! | Title of the issue | | `title` | String! | Title of the issue |
| `titleHtml` | String | The GitLab Flavored Markdown rendering of `title` | | `titleHtml` | String | The GitLab Flavored Markdown rendering of `title` |
| `totalTimeSpent` | Int! | Total time reported as spent on the issue | | `totalTimeSpent` | Int! | Total time reported as spent on the issue |
| `type` | IssueType | Type of the issue |
| `updatedAt` | Time! | Timestamp of when the issue was last updated | | `updatedAt` | Time! | Timestamp of when the issue was last updated |
| `upvotes` | Int! | Number of upvotes the issue has received | | `upvotes` | Int! | Number of upvotes the issue has received |
| `userNotesCount` | Int! | Number of user notes of the issue | | `userNotesCount` | Int! | Number of user notes of the issue |
...@@ -922,6 +923,7 @@ Represents a Group Member ...@@ -922,6 +923,7 @@ Represents a Group Member
| `title` | String! | Title of the issue | | `title` | String! | Title of the issue |
| `titleHtml` | String | The GitLab Flavored Markdown rendering of `title` | | `titleHtml` | String | The GitLab Flavored Markdown rendering of `title` |
| `totalTimeSpent` | Int! | Total time reported as spent on the issue | | `totalTimeSpent` | Int! | Total time reported as spent on the issue |
| `type` | IssueType | Type of the issue |
| `updatedAt` | Time! | Timestamp of when the issue was last updated | | `updatedAt` | Time! | Timestamp of when the issue was last updated |
| `upvotes` | Int! | Number of upvotes the issue has received | | `upvotes` | Int! | Number of upvotes the issue has received |
| `userNotesCount` | Int! | Number of user notes of the issue | | `userNotesCount` | Int! | Number of user notes of the issue |
......
...@@ -668,6 +668,58 @@ RSpec.describe IssuesFinder do ...@@ -668,6 +668,58 @@ RSpec.describe IssuesFinder do
end end
end end
context 'filtering by issue type' do
let_it_be(:incident_issue) { create(:incident, project: project1) }
context 'no type given' do
let(:params) { { issue_types: [] } }
it 'returns all issues' do
expect(issues).to contain_exactly(incident_issue, issue1, issue2, issue3, issue4)
end
end
context 'incident type' do
let(:params) { { issue_types: ['incident'] } }
it 'returns incident issues' do
expect(issues).to contain_exactly(incident_issue)
end
end
context 'issue type' do
let(:params) { { issue_types: ['issue'] } }
it 'returns all issues with type issue' do
expect(issues).to contain_exactly(issue1, issue2, issue3, issue4)
end
end
context 'multiple params' do
let(:params) { { issue_types: %w(issue incident) } }
it 'returns all issues' do
expect(issues).to contain_exactly(incident_issue, issue1, issue2, issue3, issue4)
end
end
context 'without array' do
let(:params) { { issue_types: 'incident' } }
it 'returns incident issues' do
expect(issues).to contain_exactly(incident_issue)
end
end
context 'invalid params' do
let(:params) { { issue_types: ['nonsense'] } }
it 'returns no issues' do
expect(issues).to eq(Issue.none)
end
end
end
context 'when the user is unauthorized' do context 'when the user is unauthorized' do
let(:search_user) { nil } let(:search_user) { nil }
......
...@@ -13,7 +13,7 @@ RSpec.describe Resolvers::IssuesResolver do ...@@ -13,7 +13,7 @@ RSpec.describe Resolvers::IssuesResolver do
let_it_be(:milestone) { create(:milestone, project: project) } let_it_be(:milestone) { create(:milestone, project: project) }
let_it_be(:assignee) { create(:user) } 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(:issue1) { create(:incident, 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(: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(: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(:issue4) { create(:issue) }
...@@ -95,6 +95,20 @@ RSpec.describe Resolvers::IssuesResolver do ...@@ -95,6 +95,20 @@ RSpec.describe Resolvers::IssuesResolver do
end end
end end
describe 'filters by issue_type' do
it 'filters by a single type' do
expect(resolve_issues(issue_types: ['incident'])).to contain_exactly(issue1)
end
it 'filters by more than one type' do
expect(resolve_issues(issue_types: %w(incident issue))).to contain_exactly(issue1, issue2)
end
it 'ignores the filter if none given' do
expect(resolve_issues(issue_types: [])).to contain_exactly(issue1, issue2)
end
end
context 'when searching issues' do context 'when searching issues' do
it 'returns correct issues' do it 'returns correct issues' do
expect(resolve_issues(search: 'foo')).to contain_exactly(issue2) expect(resolve_issues(search: 'foo')).to contain_exactly(issue2)
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Types::IssueTypeEnum do
specify { expect(described_class.graphql_name).to eq('IssueType') }
it 'exposes all the existing issue type values' do
expect(described_class.values.keys).to include(
*%w[ISSUE INCIDENT]
)
end
end
...@@ -128,6 +128,22 @@ RSpec.describe Issue do ...@@ -128,6 +128,22 @@ RSpec.describe Issue do
end end
end end
describe '.with_issue_type' do
let_it_be(:project) { create(:project) }
let_it_be(:issue) { create(:issue, project: project) }
let_it_be(:incident) { create(:incident, project: project) }
it 'gives issues with the given issue type' do
expect(described_class.with_issue_type('issue'))
.to contain_exactly(issue)
end
it 'gives issues with the given issue type' do
expect(described_class.with_issue_type(%w(issue incident)))
.to contain_exactly(issue, incident)
end
end
describe '#order_by_position_and_priority' do describe '#order_by_position_and_priority' do
let(:project) { create :project } let(:project) { create :project }
let(:p1) { create(:label, title: 'P1', project: project, priority: 1) } let(:p1) { create(:label, title: 'P1', project: project, priority: 1) }
......
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