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
| <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="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="boardissueinputiterationtitle"></a>`iterationTitle` | [`String`](#string) | Filter by iteration title. |
| <a id="boardissueinputiterationwildcardid"></a>`iterationWildcardId` | [`IterationWildcardId`](#iterationwildcardid) | Filter by iteration ID wildcard. |
......@@ -24,7 +24,8 @@ module EE
def filter_items(items)
issues = by_weight(super)
issues = by_epic(issues)
by_iteration(issues)
issues = by_iteration(issues)
by_iteration_cadence(issues)
end
private
......@@ -63,7 +64,7 @@ module EE
elsif params.filter_by_any_iteration?
items.any_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?
items.with_iteration_title(params[:iteration_title])
else
......@@ -71,6 +72,12 @@ module EE
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
def filter_negated_items(items)
items = by_negated_epic(items)
......@@ -108,7 +115,7 @@ module EE
strong_memoize(:current_iteration) do
next unless params.parent
IterationsFinder.new(current_user, iterations_finder_params).execute.first
IterationsFinder.new(current_user, iterations_finder_params).execute
end
end
......
......@@ -42,6 +42,14 @@ module EE
params[:iteration_id].present? || params[:iteration_title].present?
end
def iteration_cadence_id
params[:iteration_cadence_id]
end
def by_iteration_cadence?
iteration_cadence_id.present?
end
def filter_by_no_iteration?
params[:iteration_id].to_s.downcase == ::IssuableFinder::Params::FILTER_NONE
end
......
......@@ -10,6 +10,7 @@ module EE
def set_filter_values(filters)
filter_by_epic(filters)
filter_by_iteration(filters)
filter_by_iteration_cadence(filters)
filter_by_weight(filters)
super
......@@ -54,6 +55,17 @@ module EE
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)
weight = filters[:weight]
weight_wildcard = filters.delete(:weight_wildcard_id)
......
......@@ -16,6 +16,10 @@ module EE
required: false,
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,
required: false,
description: 'Filter by weight ID wildcard. Incompatible with weight.'
......
......@@ -39,6 +39,8 @@ module EE
scope :any_iteration, -> { where.not(sprint_id: nil) }
scope :in_iterations, ->(iterations) { where(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 :without_iteration_title, ->(iteration_title) { left_outer_joins(:iteration).where('sprints.title != ? OR sprints.id IS NULL', iteration_title) }
scope :on_status_page, -> do
......
......@@ -64,7 +64,7 @@ module EE
scope :with_start_date_after, ->(date) { where('start_date > :date', date: date) }
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
scope :start_date_passed, -> { where('start_date <= ?', Date.current).where('due_date >= ?', Date.current) }
......
......@@ -13,8 +13,10 @@ RSpec.describe Resolvers::BoardListIssuesResolver do
let_it_be(:list) { create(:list, board: board, label: label) }
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(:current_iteration) { create(:iteration, group: group, start_date: Date.yesterday, due_date: 1.day.from_now) }
let_it_be(:iteration_cadence1) { create(:iterations_cadence, group: group) }
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(:issue2) { create(:issue, project: project, labels: [label], iteration: iteration) }
......@@ -101,19 +103,30 @@ RSpec.describe Resolvers::BoardListIssuesResolver do
expect(result).to contain_exactly(issue2)
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' } })
expect(result).to contain_exactly(issue1, issue3)
end
it 'accepts iteration iteration id' do
result = resolve_board_list_issues({ filters: { iteration_id: [iteration.to_global_id] } })
it 'filters by iteration current and cadence id' do
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
context 'filterning by negated iteration' do
context 'filtering by negated iteration' do
it 'accepts iteration wildcard id' do
result = resolve_board_list_issues({ filters: { not: { iteration_wildcard_id: 'CURRENT' } } })
......@@ -122,6 +135,14 @@ RSpec.describe Resolvers::BoardListIssuesResolver do
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
it 'filters by iids' do
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