Commit 26f6d1e8 authored by Aleksei Lipniagov's avatar Aleksei Lipniagov

Merge branch '345204-board-issue-filter-cadence-id' into 'master'

Allow board issue filtering by iteration cadence ID in GraphQL

See merge request gitlab-org/gitlab!74451
parents 7c455246 42202aea
...@@ -18291,6 +18291,7 @@ Field that are available while modifying the custom mapping attributes for an HT ...@@ -18291,6 +18291,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
...@@ -53,6 +54,17 @@ module EE ...@@ -53,6 +54,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_wildcard_id: 'NONE' } }) result = resolve_board_list_issues({ filters: { iteration_id: [iteration.to_global_id] } })
expect(result).to contain_exactly(issue1, issue3) expect(result).to contain_exactly(issue2)
end end
it 'accepts iteration iteration id' do context 'when filtering by wildcard id' do
result = resolve_board_list_issues({ filters: { iteration_id: [iteration.to_global_id] } }) it 'filters by iteration NONE' do
result = resolve_board_list_issues({ filters: { iteration_wildcard_id: 'NONE' } })
expect(result).to contain_exactly(issue2) expect(result).to contain_exactly(issue1, issue3)
end
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])
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