Commit e5845760 authored by Mario Celi's avatar Mario Celi

Support negated filters on IssueResolver

Support assignee_usernames in IssuableFinder

Add CE negated params to issue resolver

- Add all negated arguments supported by IssueFinder
- Add resolver specs

Add EE IssueResolver GQL filter arguments

- Adds missing filter EE arguments to resolver
- Adds negated EE filter arguments supported by issue finder

Add EE changelog entry

Refactor EE issue_resolver_spec. Create less DB records

Add FOSS changelog entry
parent 9c00e4f2
...@@ -163,6 +163,8 @@ class IssuableFinder ...@@ -163,6 +163,8 @@ class IssuableFinder
User.where(id: params[:assignee_id]) User.where(id: params[:assignee_id])
elsif assignee_username? elsif assignee_username?
User.where(username: params[:assignee_username]) User.where(username: params[:assignee_username])
elsif assignee_usernames?
User.where(username: params[:assignee_usernames])
else else
User.none User.none
end end
......
...@@ -55,6 +55,10 @@ module IssueResolverArguments ...@@ -55,6 +55,10 @@ module IssueResolverArguments
as: :issue_types, as: :issue_types,
description: 'Filter issues by the given issue types.', description: 'Filter issues by the given issue types.',
required: false required: false
argument :not, Types::Issues::NegatedIssueFilterInputType,
description: 'List of negated params.',
prepare: ->(negated_args, ctx) { negated_args.to_h },
required: false
end end
def resolve_with_lookahead(**args) def resolve_with_lookahead(**args)
......
# frozen_string_literal: true
module Types
module Issues
class NegatedIssueFilterInputType < BaseInputObject
graphql_name 'NegatedIssueFilterInput'
argument :iids, [GraphQL::STRING_TYPE],
required: false,
description: 'List of IIDs of issues to exclude. For example, [1, 2].'
argument :label_name, GraphQL::STRING_TYPE.to_list_type,
required: false,
description: 'Labels not applied to this issue.'
argument :milestone_title, GraphQL::STRING_TYPE.to_list_type,
required: false,
description: 'Milestone not applied to this issue.'
argument :assignee_username, GraphQL::STRING_TYPE,
required: false,
description: 'Username of a user not assigned to the issue.'
argument :assignee_usernames, [GraphQL::STRING_TYPE],
required: false,
description: 'Usernames of users not assigned to the issue.'
argument :assignee_id, GraphQL::STRING_TYPE,
required: false,
description: 'ID of a user not assigned to the issues.'
end
end
end
Types::Issues::NegatedIssueFilterInputType.prepend_if_ee('::EE::Types::Issues::NegatedIssueFilterInputType')
---
title: Support negated filtering by iids, label_name, milestone_title, assignee_username, assignee_usernames and assignee_id in IssueResolvers, GQL API
merge_request: 58154
author:
type: added
...@@ -19,7 +19,7 @@ module EE ...@@ -19,7 +19,7 @@ module EE
end end
def weights? def weights?
params[:weight].present? && params[:weight] != ::Issue::WEIGHT_ALL params[:weight].present? && params[:weight].to_s.casecmp(::Issue::WEIGHT_ALL) != 0
end end
def filter_by_no_weight? def filter_by_no_weight?
......
...@@ -10,10 +10,12 @@ module EE ...@@ -10,10 +10,12 @@ module EE
argument :iteration_id, ::GraphQL::ID_TYPE.to_list_type, argument :iteration_id, ::GraphQL::ID_TYPE.to_list_type,
required: false, required: false,
description: 'Iterations applied to the issue.' description: 'Iterations applied to the issue.'
argument :epic_id, GraphQL::STRING_TYPE, argument :epic_id, GraphQL::STRING_TYPE,
required: false, required: false,
description: 'ID of an epic associated with the issues, "none" and "any" values are supported.' description: 'ID of an epic associated with the issues, "none" and "any" values are supported.'
argument :weight, GraphQL::STRING_TYPE,
required: false,
description: 'Weight applied to the issue, "none" and "any" values are supported.'
end end
private private
......
# frozen_string_literal: true
module EE
module Types
module Issues
module NegatedIssueFilterInputType
extend ActiveSupport::Concern
prepended do
argument :epic_id, GraphQL::STRING_TYPE,
required: false,
description: 'ID of an epic not associated with the issues.'
argument :weight, GraphQL::STRING_TYPE,
required: false,
description: 'Weight not applied to the issue.'
end
end
end
end
end
---
title: Support negated filtering by epic_id and weight in IssueResolvers, GQL API
merge_request: 58154
author:
type: added
...@@ -11,36 +11,39 @@ RSpec.describe Resolvers::IssuesResolver do ...@@ -11,36 +11,39 @@ RSpec.describe Resolvers::IssuesResolver do
context "with a project" do context "with a project" do
describe '#resolve' do describe '#resolve' do
let_it_be(:epic1) { create :epic, group: group }
let_it_be(:epic2) { create :epic, group: group }
let_it_be(:iteration1) { create(:iteration, group: group, start_date: 2.weeks.ago, due_date: 1.week.ago) }
let_it_be(:current_iteration) { create(:iteration, :started, group: group, start_date: Date.today, due_date: 1.day.from_now) }
let_it_be(:issue1) { create :issue, project: project, epic: epic1, iteration: iteration1 }
let_it_be(:issue2) { create :issue, project: project, epic: epic2, weight: 1 }
let_it_be(:issue3) { create :issue, project: project, weight: 3, iteration: current_iteration }
let_it_be(:issue4) { create :issue, :published, project: project }
before do before do
project.add_developer(current_user) project.add_developer(current_user)
end end
describe 'sorting' do describe 'sorting' do
context 'when sorting by weight' do context 'when sorting by weight' do
let_it_be(:weight_issue1) { create(:issue, project: project, weight: 5) }
let_it_be(:weight_issue2) { create(:issue, project: project, weight: nil) }
let_it_be(:weight_issue3) { create(:issue, project: project, weight: 1) }
let_it_be(:weight_issue4) { create(:issue, project: project, weight: nil) }
it 'sorts issues ascending' do it 'sorts issues ascending' do
expect(resolve_issues(sort: :weight_asc).to_a).to eq [weight_issue3, weight_issue1, weight_issue4, weight_issue2] expect(resolve_issues(sort: :weight_asc).to_a).to eq [issue2, issue3, issue4, issue1]
end end
it 'sorts issues descending' do it 'sorts issues descending' do
expect(resolve_issues(sort: :weight_desc).to_a).to eq [weight_issue1, weight_issue3, weight_issue4, weight_issue2] expect(resolve_issues(sort: :weight_desc).to_a).to eq [issue3, issue2, issue4, issue1]
end end
end end
context 'when sorting by published' do context 'when sorting by published' do
let_it_be(:not_published) { create(:issue, project: project) }
let_it_be(:published) { create(:issue, :published, project: project) }
it 'sorts issues ascending' do it 'sorts issues ascending' do
expect(resolve_issues(sort: :published_asc).to_a).to eq [not_published, published] expect(resolve_issues(sort: :published_asc).to_a).to eq [issue3, issue2, issue1, issue4]
end end
it 'sorts issues descending' do it 'sorts issues descending' do
expect(resolve_issues(sort: :published_desc).to_a).to eq [published, not_published] expect(resolve_issues(sort: :published_desc).to_a).to eq [issue4, issue3, issue2, issue1]
end end
end end
...@@ -64,32 +67,56 @@ RSpec.describe Resolvers::IssuesResolver do ...@@ -64,32 +67,56 @@ RSpec.describe Resolvers::IssuesResolver do
end end
describe 'filtering by iteration' do describe 'filtering by iteration' do
let_it_be(:iteration1) { create(:iteration, group: group) }
let_it_be(:issue_with_iteration) { create(:issue, project: project, iteration: iteration1) }
let_it_be(:issue_without_iteration) { create(:issue, project: project) }
it 'returns issues with iteration' do it 'returns issues with iteration' do
expect(resolve_issues(iteration_id: [iteration1.id])).to contain_exactly(issue_with_iteration) expect(resolve_issues(iteration_id: [iteration1.id.to_s])).to contain_exactly(issue1)
end end
end end
describe 'filter by epic' do describe 'filter by epic' do
let_it_be(:epic) { create :epic, group: group }
let_it_be(:epic2) { create :epic, group: group }
let_it_be(:issue1) { create :issue, project: project, epic: epic }
let_it_be(:issue2) { create :issue, project: project, epic: epic2 }
let_it_be(:issue3) { create :issue, project: project }
it 'returns issues without epic when epic_id is "none"' do it 'returns issues without epic when epic_id is "none"' do
expect(resolve_issues(epic_id: 'none')).to match_array([issue3]) expect(resolve_issues(epic_id: 'none')).to contain_exactly(issue4, issue3)
end end
it 'returns issues with any epic when epic_id is "any"' do it 'returns issues with any epic when epic_id is "any"' do
expect(resolve_issues(epic_id: 'any')).to match_array([issue1, issue2]) expect(resolve_issues(epic_id: 'any')).to contain_exactly(issue1, issue2)
end end
it 'returns issues with any epic when epic_id is specific' do it 'returns issues with any epic when epic_id is specific' do
expect(resolve_issues(epic_id: epic.id)).to match_array([issue1]) expect(resolve_issues(epic_id: epic1.id.to_s)).to contain_exactly(issue1)
end
end
describe 'filter by weight' do
context 'when filtering by any weight' do
it 'only returns issues that have a weight assigned' do
expect(resolve_issues(weight: 'any')).to contain_exactly(issue2, issue3)
end
end
context 'when filtering by no weight' do
it 'only returns issues that have no weight assigned' do
expect(resolve_issues(weight: 'none')).to contain_exactly(issue1, issue4)
end
end
context 'when filtering by specific weight' do
it 'only returns issues that have the specified weight assigned' do
expect(resolve_issues(weight: '3')).to contain_exactly(issue3)
end
end
end
describe 'filtering by negated params' do
describe 'filter by negated epic' do
it 'returns issues without the specified epic_id' do
expect(resolve_issues(not: { epic_id: epic2.id.to_s })).to contain_exactly(issue1, issue3, issue4)
end
end
describe 'filtering by negated weight' do
it 'only returns issues that do not have the specified weight assigned' do
expect(resolve_issues(not: { weight: '3' })).to contain_exactly(issue1, issue2, issue4)
end
end end
end end
end end
......
...@@ -63,6 +63,11 @@ RSpec.describe IssuesFinder do ...@@ -63,6 +63,11 @@ RSpec.describe IssuesFinder do
let(:expected_issuables) { [issue2] } let(:expected_issuables) { [issue2] }
end end
it_behaves_like 'assignee username filter' do
let(:params) { { assignee_usernames: [user2.username] } }
let(:expected_issuables) { [issue2] }
end
it_behaves_like 'assignee NOT username filter' do it_behaves_like 'assignee NOT username filter' do
before do before do
issue2.assignees = [user2] issue2.assignees = [user2]
......
...@@ -50,6 +50,10 @@ RSpec.describe Resolvers::IssuesResolver do ...@@ -50,6 +50,10 @@ RSpec.describe Resolvers::IssuesResolver do
expect(resolve_issues(assignee_username: [assignee.username])).to contain_exactly(issue2) expect(resolve_issues(assignee_username: [assignee.username])).to contain_exactly(issue2)
end end
it 'filters by assignee_usernames' do
expect(resolve_issues(assignee_usernames: [assignee.username])).to contain_exactly(issue2)
end
it 'filters by two assignees' do it 'filters by two assignees' do
assignee2 = create(:user) assignee2 = create(:user)
issue2.update!(assignees: [assignee, assignee2]) issue2.update!(assignees: [assignee, assignee2])
...@@ -144,6 +148,33 @@ RSpec.describe Resolvers::IssuesResolver do ...@@ -144,6 +148,33 @@ RSpec.describe Resolvers::IssuesResolver do
end end
end end
describe 'filters by negated params' do
it 'returns issues without the specified iids' do
expect(resolve_issues(not: { iids: [issue1.iid] })).to contain_exactly(issue2)
end
it 'returns issues without the specified label names' do
expect(resolve_issues(not: { label_name: [label1.title] })).to be_empty
expect(resolve_issues(not: { label_name: [label2.title] })).to contain_exactly(issue1)
end
it 'returns issues without the specified milestone' do
expect(resolve_issues(not: { milestone_title: [milestone.title] })).to contain_exactly(issue2)
end
it 'returns issues without the specified assignee_username' do
expect(resolve_issues(not: { assignee_username: assignee.username })).to contain_exactly(issue1)
end
it 'returns issues without the specified assignee_usernames' do
expect(resolve_issues(not: { assignee_usernames: [assignee.username] })).to contain_exactly(issue1)
end
it 'returns issues without the specified assignee_id' do
expect(resolve_issues(not: { assignee_id: [assignee.id] })).to contain_exactly(issue1)
end
end
describe 'sorting' do describe 'sorting' do
context 'when sorting by created' do context 'when sorting by created' do
it 'sorts issues ascending' do it 'sorts issues ascending' 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