Commit 42202aea authored by Mario Celi's avatar Mario Celi

Allow board issue filtering by iteration cadence ID in GraphQL

GraphQL API adds the iterationCadenceId issue list
filter for boards. iterationCadenceId can
be used together with iterationWildcardID

Changelog: added
EE: true
parent e249f499
...@@ -18263,6 +18263,7 @@ Field that are available while modifying the custom mapping attributes for an HT ...@@ -18263,6 +18263,7 @@ Field that are available while modifying the custom mapping attributes for an HT
| <a id="boardissueinputepicid"></a>`epicId` | [`EpicID`](#epicid) | Filter by epic ID. Incompatible with epicWildcardId. | | <a id="boardissueinputepicid"></a>`epicId` | [`EpicID`](#epicid) | Filter by epic ID. Incompatible with epicWildcardId. |
| <a id="boardissueinputepicwildcardid"></a>`epicWildcardId` | [`EpicWildcardId`](#epicwildcardid) | Filter by epic ID wildcard. Incompatible with epicId. | | <a id="boardissueinputepicwildcardid"></a>`epicWildcardId` | [`EpicWildcardId`](#epicwildcardid) | Filter by epic ID wildcard. Incompatible with epicId. |
| <a id="boardissueinputiids"></a>`iids` | [`[String!]`](#string) | List of IIDs of issues. For example `["1", "2"]`. | | <a id="boardissueinputiids"></a>`iids` | [`[String!]`](#string) | List of IIDs of issues. For example `["1", "2"]`. |
| <a id="boardissueinputiterationcadenceid"></a>`iterationCadenceId` | [`[IterationsCadenceID!]`](#iterationscadenceid) | Filter by a list of iteration cadence IDs. |
| <a id="boardissueinputiterationid"></a>`iterationId` | [`[IterationID!]`](#iterationid) | Filter by a list of iteration IDs. Incompatible with iterationWildcardId. | | <a id="boardissueinputiterationid"></a>`iterationId` | [`[IterationID!]`](#iterationid) | Filter by a list of iteration IDs. Incompatible with iterationWildcardId. |
| <a id="boardissueinputiterationtitle"></a>`iterationTitle` | [`String`](#string) | Filter by iteration title. | | <a id="boardissueinputiterationtitle"></a>`iterationTitle` | [`String`](#string) | Filter by iteration title. |
| <a id="boardissueinputiterationwildcardid"></a>`iterationWildcardId` | [`IterationWildcardId`](#iterationwildcardid) | Filter by iteration ID wildcard. | | <a id="boardissueinputiterationwildcardid"></a>`iterationWildcardId` | [`IterationWildcardId`](#iterationwildcardid) | Filter by iteration ID wildcard. |
...@@ -24,7 +24,8 @@ module EE ...@@ -24,7 +24,8 @@ module EE
def filter_items(items) def filter_items(items)
issues = by_weight(super) issues = by_weight(super)
issues = by_epic(issues) issues = by_epic(issues)
by_iteration(issues) issues = by_iteration(issues)
by_iteration_cadence(issues)
end end
private private
...@@ -63,7 +64,7 @@ module EE ...@@ -63,7 +64,7 @@ module EE
elsif params.filter_by_any_iteration? elsif params.filter_by_any_iteration?
items.any_iteration items.any_iteration
elsif params.filter_by_current_iteration? && get_current_iteration elsif params.filter_by_current_iteration? && get_current_iteration
items.in_iterations(get_current_iteration) items.in_iteration_scope(get_current_iteration)
elsif params.filter_by_iteration_title? elsif params.filter_by_iteration_title?
items.with_iteration_title(params[:iteration_title]) items.with_iteration_title(params[:iteration_title])
else else
...@@ -71,6 +72,12 @@ module EE ...@@ -71,6 +72,12 @@ module EE
end end
end end
def by_iteration_cadence(items)
return items unless params.by_iteration_cadence?
items.in_iteration_cadences(params.iteration_cadence_id)
end
override :filter_negated_items override :filter_negated_items
def filter_negated_items(items) def filter_negated_items(items)
items = by_negated_epic(items) items = by_negated_epic(items)
...@@ -108,7 +115,7 @@ module EE ...@@ -108,7 +115,7 @@ module EE
strong_memoize(:current_iteration) do strong_memoize(:current_iteration) do
next unless params.parent next unless params.parent
IterationsFinder.new(current_user, iterations_finder_params).execute.first IterationsFinder.new(current_user, iterations_finder_params).execute
end end
end end
......
...@@ -42,6 +42,14 @@ module EE ...@@ -42,6 +42,14 @@ module EE
params[:iteration_id].present? || params[:iteration_title].present? params[:iteration_id].present? || params[:iteration_title].present?
end end
def iteration_cadence_id
params[:iteration_cadence_id]
end
def by_iteration_cadence?
iteration_cadence_id.present?
end
def filter_by_no_iteration? def filter_by_no_iteration?
params[:iteration_id].to_s.downcase == ::IssuableFinder::Params::FILTER_NONE params[:iteration_id].to_s.downcase == ::IssuableFinder::Params::FILTER_NONE
end end
......
...@@ -10,6 +10,7 @@ module EE ...@@ -10,6 +10,7 @@ module EE
def set_filter_values(filters) def set_filter_values(filters)
filter_by_epic(filters) filter_by_epic(filters)
filter_by_iteration(filters) filter_by_iteration(filters)
filter_by_iteration_cadence(filters)
filter_by_weight(filters) filter_by_weight(filters)
super super
...@@ -54,6 +55,17 @@ module EE ...@@ -54,6 +55,17 @@ module EE
end end
end end
def filter_by_iteration_cadence(filters)
return if filters[:iteration_cadence_id].blank?
filters[:iteration_cadence_id].map! do |iteration_cadence_id|
# TODO: remove this line when the compatibility layer is removed
# See: https://gitlab.com/gitlab-org/gitlab/-/issues/257883
parsed_id = ::Types::GlobalIDType[::Iterations::Cadence].coerce_isolated_input(iteration_cadence_id)
parsed_id&.model_id
end
end
def filter_by_weight(filters) def filter_by_weight(filters)
weight = filters[:weight] weight = filters[:weight]
weight_wildcard = filters.delete(:weight_wildcard_id) weight_wildcard = filters.delete(:weight_wildcard_id)
......
...@@ -16,6 +16,10 @@ module EE ...@@ -16,6 +16,10 @@ module EE
required: false, required: false,
description: 'Filter by iteration ID wildcard.' description: 'Filter by iteration ID wildcard.'
argument :iteration_cadence_id, [::Types::GlobalIDType[::Iterations::Cadence]],
required: false,
description: 'Filter by a list of iteration cadence IDs.'
argument :weight_wildcard_id, ::Types::Boards::WeightWildcardIdEnum, argument :weight_wildcard_id, ::Types::Boards::WeightWildcardIdEnum,
required: false, required: false,
description: 'Filter by weight ID wildcard. Incompatible with weight.' description: 'Filter by weight ID wildcard. Incompatible with weight.'
......
...@@ -39,6 +39,8 @@ module EE ...@@ -39,6 +39,8 @@ module EE
scope :any_iteration, -> { where.not(sprint_id: nil) } scope :any_iteration, -> { where.not(sprint_id: nil) }
scope :in_iterations, ->(iterations) { where(sprint_id: iterations) } scope :in_iterations, ->(iterations) { where(sprint_id: iterations) }
scope :not_in_iterations, ->(iterations) { where(sprint_id: nil).or(where.not(sprint_id: iterations)) } scope :not_in_iterations, ->(iterations) { where(sprint_id: nil).or(where.not(sprint_id: iterations)) }
scope :in_iteration_scope, ->(iteration_scope) { joins(:iteration).merge(iteration_scope) }
scope :in_iteration_cadences, ->(iteration_cadences) { joins(:iteration).where(sprints: { iterations_cadence_id: iteration_cadences }) }
scope :with_iteration_title, ->(iteration_title) { joins(:iteration).where(sprints: { title: iteration_title }) } scope :with_iteration_title, ->(iteration_title) { joins(:iteration).where(sprints: { title: iteration_title }) }
scope :without_iteration_title, ->(iteration_title) { left_outer_joins(:iteration).where('sprints.title != ? OR sprints.id IS NULL', iteration_title) } scope :without_iteration_title, ->(iteration_title) { left_outer_joins(:iteration).where('sprints.title != ? OR sprints.id IS NULL', iteration_title) }
scope :on_status_page, -> do scope :on_status_page, -> do
......
...@@ -64,7 +64,7 @@ module EE ...@@ -64,7 +64,7 @@ module EE
scope :with_start_date_after, ->(date) { where('start_date > :date', date: date) } scope :with_start_date_after, ->(date) { where('start_date > :date', date: date) }
scope :within_timeframe, -> (start_date, end_date) do scope :within_timeframe, -> (start_date, end_date) do
where('start_date <= ?', end_date).where('due_date >= ?', start_date) where('sprints.start_date <= ?', end_date).where('sprints.due_date >= ?', start_date)
end end
scope :start_date_passed, -> { where('start_date <= ?', Date.current).where('due_date >= ?', Date.current) } scope :start_date_passed, -> { where('start_date <= ?', Date.current).where('due_date >= ?', Date.current) }
......
...@@ -13,8 +13,10 @@ RSpec.describe Resolvers::BoardListIssuesResolver do ...@@ -13,8 +13,10 @@ RSpec.describe Resolvers::BoardListIssuesResolver do
let_it_be(:list) { create(:list, board: board, label: label) } let_it_be(:list) { create(:list, board: board, label: label) }
let_it_be(:epic) { create(:epic, group: group) } let_it_be(:epic) { create(:epic, group: group) }
let_it_be(:iteration) { create(:iteration, group: group, start_date: 1.week.ago, due_date: 2.days.ago) } let_it_be(:iteration_cadence1) { create(:iterations_cadence, group: group) }
let_it_be(:current_iteration) { create(:iteration, group: group, start_date: Date.yesterday, due_date: 1.day.from_now) } let_it_be(:iteration_cadence2) { create(:iterations_cadence, group: group) }
let_it_be(:iteration) { create(:iteration, group: group, start_date: 1.week.ago, due_date: 2.days.ago, iterations_cadence: iteration_cadence1) }
let_it_be(:current_iteration) { create(:iteration, group: group, start_date: Date.yesterday, due_date: 1.day.from_now, iterations_cadence: iteration_cadence2) }
let_it_be(:issue1) { create(:issue, project: project, labels: [label], weight: 3) } let_it_be(:issue1) { create(:issue, project: project, labels: [label], weight: 3) }
let_it_be(:issue2) { create(:issue, project: project, labels: [label], iteration: iteration) } let_it_be(:issue2) { create(:issue, project: project, labels: [label], iteration: iteration) }
...@@ -101,19 +103,30 @@ RSpec.describe Resolvers::BoardListIssuesResolver do ...@@ -101,19 +103,30 @@ RSpec.describe Resolvers::BoardListIssuesResolver do
expect(result).to contain_exactly(issue2) expect(result).to contain_exactly(issue2)
end end
it 'accepts iteration wildcard id' do it 'accepts iteration id' do
result = resolve_board_list_issues({ filters: { iteration_id: [iteration.to_global_id] } })
expect(result).to contain_exactly(issue2)
end
context 'when filtering by wildcard id' do
it 'filters by iteration NONE' do
result = resolve_board_list_issues({ filters: { iteration_wildcard_id: 'NONE' } }) result = resolve_board_list_issues({ filters: { iteration_wildcard_id: 'NONE' } })
expect(result).to contain_exactly(issue1, issue3) expect(result).to contain_exactly(issue1, issue3)
end end
it 'accepts iteration iteration id' do it 'filters by iteration current and cadence id' do
result = resolve_board_list_issues({ filters: { iteration_id: [iteration.to_global_id] } }) another_current_iteration = create(:iteration, group: group, start_date: Date.yesterday, due_date: 1.day.from_now, iterations_cadence: iteration_cadence1)
another_current_iteration_issue = create(:issue, project: project, iteration: another_current_iteration, labels: [label])
expect(result).to contain_exactly(issue2) result = resolve_board_list_issues({ filters: { iteration_wildcard_id: 'CURRENT', iteration_cadence_id: [iteration_cadence1.to_global_id] } })
expect(result).to contain_exactly(another_current_iteration_issue)
end
end end
context 'filterning by negated iteration' do context 'filtering by negated iteration' do
it 'accepts iteration wildcard id' do it 'accepts iteration wildcard id' do
result = resolve_board_list_issues({ filters: { not: { iteration_wildcard_id: 'CURRENT' } } }) result = resolve_board_list_issues({ filters: { not: { iteration_wildcard_id: 'CURRENT' } } })
...@@ -122,6 +135,14 @@ RSpec.describe Resolvers::BoardListIssuesResolver do ...@@ -122,6 +135,14 @@ RSpec.describe Resolvers::BoardListIssuesResolver do
end end
end end
context 'filtering by iteration cadence' do
it 'returns issues associated with an iteration cadence' do
result = resolve_board_list_issues({ filters: { iteration_cadence_id: [iteration.iterations_cadence.to_global_id] } })
expect(result).to contain_exactly(issue2)
end
end
context 'filtering by iids' do context 'filtering by iids' do
it 'filters by iids' do it 'filters by iids' do
result = resolve_board_list_issues({ filters: { iids: [issue1.iid, issue3.iid] } }) result = resolve_board_list_issues({ filters: { iids: [issue1.iid, issue3.iid] } })
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe 'Querying a Board list' do
include GraphqlHelpers
let_it_be(:unknown_user) { create(:user) }
let_it_be(:guest) { create(:user) }
let_it_be(:group) { create(:group) }
let_it_be(:project) { create(:project, group: group) }
let_it_be(:board) { create(:board, resource_parent: project) }
let_it_be(:label) { create(:label, project: project, name: 'foo') }
let_it_be(:list) { create(:list, board: board, label: label) }
let_it_be(:iteration_cadence1) { create(:iterations_cadence, group: group) }
let_it_be(:iteration_cadence2) { create(:iterations_cadence, group: group) }
let_it_be(:current_iteration1) { create(:iteration, group: group, start_date: Date.yesterday, due_date: 1.day.from_now, iterations_cadence: iteration_cadence1) }
let_it_be(:current_iteration2) { create(:iteration, group: group, start_date: Date.yesterday, due_date: 1.day.from_now, iterations_cadence: iteration_cadence2) }
let_it_be(:issue1) { create(:issue, project: project, labels: [label], iteration: current_iteration1) }
let_it_be(:issue2) { create(:issue, project: project, labels: [label], iteration: current_iteration2) }
let(:current_user) { unknown_user }
let(:filters) { {} }
let(:query) do
graphql_query_for(
:board_list,
{ id: list.to_global_id.to_s, issueFilters: filters },
%w[title issuesCount]
)
end
subject { graphql_data['boardList'] }
before_all do
project.add_guest(guest)
end
before do
post_graphql(query, current_user: current_user)
end
context 'when the user has access to the list' do
let(:current_user) { guest }
it_behaves_like 'a working graphql query'
it { is_expected.to include({ 'issuesCount' => 2, 'title' => list.title }) }
describe 'issue filters' do
context 'when filtering by iteration arguments' do
let(:filters) { { iterationWildcardId: :CURRENT, iterationCadenceId: [iteration_cadence2.to_global_id.to_s] } }
it { is_expected.to include({ 'issuesCount' => 1, 'title' => list.title }) }
end
end
end
context 'when the user does not have access to the list' do
it { is_expected.to be_nil }
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