Commit 8df4fa8e authored by Etienne Baqué's avatar Etienne Baqué

Merge branch '336611-search-iterations-by-cadence-title' into 'master'

Add ability to search iterations by cadence title

See merge request gitlab-org/gitlab!76231
parents 481527e8 75e3079e
...@@ -125,17 +125,6 @@ module Timebox ...@@ -125,17 +125,6 @@ module Timebox
fuzzy_search(query, [:title, :description]) fuzzy_search(query, [:title, :description])
end end
# Searches for timeboxes with a matching title.
#
# This method uses ILIKE on PostgreSQL
#
# query - The search query as a String
#
# Returns an ActiveRecord::Relation.
def search_title(query)
fuzzy_search(query, [:title])
end
def filter_by_state(timeboxes, state) def filter_by_state(timeboxes, state)
case state case state
when 'closed' then timeboxes.closed when 'closed' then timeboxes.closed
......
...@@ -52,6 +52,17 @@ class Milestone < ApplicationRecord ...@@ -52,6 +52,17 @@ class Milestone < ApplicationRecord
state :active state :active
end end
# Searches for timeboxes with a matching title.
#
# This method uses ILIKE on PostgreSQL
#
# query - The search query as a String
#
# Returns an ActiveRecord::Relation.
def self.search_title(query)
fuzzy_search(query, [:title])
end
def self.min_chars_for_partial_matching def self.min_chars_for_partial_matching
2 2
end end
......
...@@ -11064,12 +11064,15 @@ four standard [pagination arguments](#connection-pagination-arguments): ...@@ -11064,12 +11064,15 @@ four standard [pagination arguments](#connection-pagination-arguments):
| <a id="groupiterationsenddate"></a>`endDate` **{warning-solid}** | [`Time`](#time) | **Deprecated** in 13.5. Use timeframe.end. | | <a id="groupiterationsenddate"></a>`endDate` **{warning-solid}** | [`Time`](#time) | **Deprecated** in 13.5. Use timeframe.end. |
| <a id="groupiterationsid"></a>`id` | [`ID`](#id) | Global ID of the Iteration to look up. | | <a id="groupiterationsid"></a>`id` | [`ID`](#id) | Global ID of the Iteration to look up. |
| <a id="groupiterationsiid"></a>`iid` | [`ID`](#id) | Internal ID of the Iteration to look up. | | <a id="groupiterationsiid"></a>`iid` | [`ID`](#id) | Internal ID of the Iteration to look up. |
| <a id="groupiterationsin"></a>`in` | [`[IterationSearchableField!]`](#iterationsearchablefield) | Fields in which the fuzzy-search should be performed with the query given in the argument `search`. Defaults to `[title]`. |
| <a id="groupiterationsincludeancestors"></a>`includeAncestors` | [`Boolean`](#boolean) | Whether to include ancestor iterations. Defaults to true. | | <a id="groupiterationsincludeancestors"></a>`includeAncestors` | [`Boolean`](#boolean) | Whether to include ancestor iterations. Defaults to true. |
| <a id="groupiterationsiterationcadenceids"></a>`iterationCadenceIds` | [`[IterationsCadenceID!]`](#iterationscadenceid) | Global iteration cadence IDs by which to look up the iterations. | | <a id="groupiterationsiterationcadenceids"></a>`iterationCadenceIds` | [`[IterationsCadenceID!]`](#iterationscadenceid) | Global iteration cadence IDs by which to look up the iterations. |
| <a id="groupiterationssearch"></a>`search` | [`String`](#string) | Query used for fuzzy-searching in the fields selected in the argument `in`. |
| <a id="groupiterationssort"></a>`sort` | [`IterationSort`](#iterationsort) | List iterations by sort order. If unspecified, an arbitrary order (subject to change) is used. |
| <a id="groupiterationsstartdate"></a>`startDate` **{warning-solid}** | [`Time`](#time) | **Deprecated** in 13.5. Use timeframe.start. | | <a id="groupiterationsstartdate"></a>`startDate` **{warning-solid}** | [`Time`](#time) | **Deprecated** in 13.5. Use timeframe.start. |
| <a id="groupiterationsstate"></a>`state` | [`IterationState`](#iterationstate) | Filter iterations by state. | | <a id="groupiterationsstate"></a>`state` | [`IterationState`](#iterationstate) | Filter iterations by state. |
| <a id="groupiterationstimeframe"></a>`timeframe` | [`Timeframe`](#timeframe) | List items overlapping the given timeframe. | | <a id="groupiterationstimeframe"></a>`timeframe` | [`Timeframe`](#timeframe) | List items overlapping the given timeframe. |
| <a id="groupiterationstitle"></a>`title` | [`String`](#string) | Fuzzy search by title. | | <a id="groupiterationstitle"></a>`title` **{warning-solid}** | [`String`](#string) | **Deprecated** in 15.4. The argument will be removed in 15.4. Please use `search` and `in` fields instead. |
##### `Group.label` ##### `Group.label`
...@@ -13762,12 +13765,15 @@ four standard [pagination arguments](#connection-pagination-arguments): ...@@ -13762,12 +13765,15 @@ four standard [pagination arguments](#connection-pagination-arguments):
| <a id="projectiterationsenddate"></a>`endDate` **{warning-solid}** | [`Time`](#time) | **Deprecated** in 13.5. Use timeframe.end. | | <a id="projectiterationsenddate"></a>`endDate` **{warning-solid}** | [`Time`](#time) | **Deprecated** in 13.5. Use timeframe.end. |
| <a id="projectiterationsid"></a>`id` | [`ID`](#id) | Global ID of the Iteration to look up. | | <a id="projectiterationsid"></a>`id` | [`ID`](#id) | Global ID of the Iteration to look up. |
| <a id="projectiterationsiid"></a>`iid` | [`ID`](#id) | Internal ID of the Iteration to look up. | | <a id="projectiterationsiid"></a>`iid` | [`ID`](#id) | Internal ID of the Iteration to look up. |
| <a id="projectiterationsin"></a>`in` | [`[IterationSearchableField!]`](#iterationsearchablefield) | Fields in which the fuzzy-search should be performed with the query given in the argument `search`. Defaults to `[title]`. |
| <a id="projectiterationsincludeancestors"></a>`includeAncestors` | [`Boolean`](#boolean) | Whether to include ancestor iterations. Defaults to true. | | <a id="projectiterationsincludeancestors"></a>`includeAncestors` | [`Boolean`](#boolean) | Whether to include ancestor iterations. Defaults to true. |
| <a id="projectiterationsiterationcadenceids"></a>`iterationCadenceIds` | [`[IterationsCadenceID!]`](#iterationscadenceid) | Global iteration cadence IDs by which to look up the iterations. | | <a id="projectiterationsiterationcadenceids"></a>`iterationCadenceIds` | [`[IterationsCadenceID!]`](#iterationscadenceid) | Global iteration cadence IDs by which to look up the iterations. |
| <a id="projectiterationssearch"></a>`search` | [`String`](#string) | Query used for fuzzy-searching in the fields selected in the argument `in`. |
| <a id="projectiterationssort"></a>`sort` | [`IterationSort`](#iterationsort) | List iterations by sort order. If unspecified, an arbitrary order (subject to change) is used. |
| <a id="projectiterationsstartdate"></a>`startDate` **{warning-solid}** | [`Time`](#time) | **Deprecated** in 13.5. Use timeframe.start. | | <a id="projectiterationsstartdate"></a>`startDate` **{warning-solid}** | [`Time`](#time) | **Deprecated** in 13.5. Use timeframe.start. |
| <a id="projectiterationsstate"></a>`state` | [`IterationState`](#iterationstate) | Filter iterations by state. | | <a id="projectiterationsstate"></a>`state` | [`IterationState`](#iterationstate) | Filter iterations by state. |
| <a id="projectiterationstimeframe"></a>`timeframe` | [`Timeframe`](#timeframe) | List items overlapping the given timeframe. | | <a id="projectiterationstimeframe"></a>`timeframe` | [`Timeframe`](#timeframe) | List items overlapping the given timeframe. |
| <a id="projectiterationstitle"></a>`title` | [`String`](#string) | Fuzzy search by title. | | <a id="projectiterationstitle"></a>`title` **{warning-solid}** | [`String`](#string) | **Deprecated** in 15.4. The argument will be removed in 15.4. Please use `search` and `in` fields instead. |
##### `Project.jobs` ##### `Project.jobs`
...@@ -17000,6 +17006,23 @@ Issue type. ...@@ -17000,6 +17006,23 @@ Issue type.
| <a id="issuetyperequirement"></a>`REQUIREMENT` | Requirement issue type. | | <a id="issuetyperequirement"></a>`REQUIREMENT` | Requirement issue type. |
| <a id="issuetypetest_case"></a>`TEST_CASE` | Test Case issue type. | | <a id="issuetypetest_case"></a>`TEST_CASE` | Test Case issue type. |
### `IterationSearchableField`
Fields to perform the search in.
| Value | Description |
| ----- | ----------- |
| <a id="iterationsearchablefieldcadence_title"></a>`CADENCE_TITLE` | Search in cadence_title field. |
| <a id="iterationsearchablefieldtitle"></a>`TITLE` | Search in title field. |
### `IterationSort`
Iteration sort values.
| Value | Description |
| ----- | ----------- |
| <a id="iterationsortcadence_and_due_date_asc"></a>`CADENCE_AND_DUE_DATE_ASC` | Sort by cadence id and due date in ascending order. |
### `IterationState` ### `IterationState`
State of a GitLab iteration. State of a GitLab iteration.
...@@ -5,14 +5,18 @@ ...@@ -5,14 +5,18 @@
# params - Hash # params - Hash
# parent - The group in which to look-up iterations. # parent - The group in which to look-up iterations.
# include_ancestors - whether to look-up iterations in group ancestors. # include_ancestors - whether to look-up iterations in group ancestors.
# order - Orders by field default due date asc.
# title - Filter by title. # title - Filter by title.
# search - Filter by fuzzy searching the given query in the selected fields.
# in - Array of searchable fields used with search param.
# state - Filters by state. # state - Filters by state.
# sort - Items are sorted by due_date and title with id as a tie breaker if unspecified.
class IterationsFinder class IterationsFinder
include FinderMethods include FinderMethods
include TimeFrameFilter include TimeFrameFilter
SEARCHABLE_FIELDS = %i(title cadence_title).freeze
attr_reader :params, :current_user attr_reader :params, :current_user
def initialize(current_user, params = {}) def initialize(current_user, params = {})
...@@ -30,7 +34,7 @@ class IterationsFinder ...@@ -30,7 +34,7 @@ class IterationsFinder
items = by_iid(items) items = by_iid(items)
items = by_groups(items) items = by_groups(items)
items = by_title(items) items = by_title(items)
items = by_search_title(items) items = by_search(items)
items = by_state(items) items = by_state(items)
items = by_timeframe(items) items = by_timeframe(items)
items = by_iteration_cadences(items) items = by_iteration_cadences(items)
...@@ -72,10 +76,20 @@ class IterationsFinder ...@@ -72,10 +76,20 @@ class IterationsFinder
items.with_title(params[:title]) items.with_title(params[:title])
end end
def by_search_title(items) def by_search(items)
return items unless params[:search_title].present? return items unless params[:search].present? && params[:in].present?
query = params[:search]
in_title = params[:in].include?(:title)
in_cadence_title = params[:in].include?(:cadence_title)
items.search_title(params[:search_title]) if in_title && in_cadence_title
items.search_title_or_cadence_title(query)
elsif in_title
items.search_title(query)
elsif in_cadence_title
items.search_cadence_title(query)
end
end end
def by_state(items) def by_state(items)
...@@ -96,6 +110,8 @@ class IterationsFinder ...@@ -96,6 +110,8 @@ class IterationsFinder
# rubocop: disable CodeReuse/ActiveRecord # rubocop: disable CodeReuse/ActiveRecord
def order(items) def order(items)
return items.sort_by_cadence_id_and_due_date_asc if params[:sort].present? && params[:sort] == :cadence_and_due_date_asc
items.reorder(:due_date).order(:title, { id: :asc }) items.reorder(:due_date).order(:title, { id: :asc })
end end
# rubocop: enable CodeReuse/ActiveRecord # rubocop: enable CodeReuse/ActiveRecord
......
...@@ -5,12 +5,23 @@ module Resolvers ...@@ -5,12 +5,23 @@ module Resolvers
include Gitlab::Graphql::Authorize::AuthorizeResource include Gitlab::Graphql::Authorize::AuthorizeResource
include TimeFrameArguments include TimeFrameArguments
DEFAULT_IN_FIELD = :title
argument :state, Types::IterationStateEnum, argument :state, Types::IterationStateEnum,
required: false, required: false,
description: 'Filter iterations by state.' description: 'Filter iterations by state.'
argument :title, GraphQL::Types::String, argument :title, GraphQL::Types::String,
required: false, required: false,
description: 'Fuzzy search by title.' description: 'Fuzzy search by title.',
deprecated: { reason: 'The argument will be removed in 15.4. Please use `search` and `in` fields instead', milestone: '15.4' }
argument :search, GraphQL::Types::String,
required: false,
description: 'Query used for fuzzy-searching in the fields selected in the argument `in`.'
argument :in, [Types::IterationSearchableFieldEnum],
required: false,
description: "Fields in which the fuzzy-search should be performed with the query given in the argument `search`. Defaults to `[#{DEFAULT_IN_FIELD}]`."
# rubocop:disable Graphql/IDType # rubocop:disable Graphql/IDType
argument :id, GraphQL::Types::ID, argument :id, GraphQL::Types::ID,
...@@ -29,10 +40,15 @@ module Resolvers ...@@ -29,10 +40,15 @@ module Resolvers
required: false, required: false,
description: 'Global iteration cadence IDs by which to look up the iterations.' description: 'Global iteration cadence IDs by which to look up the iterations.'
argument :sort, Types::IterationSortEnum,
required: false,
description: 'List iterations by sort order. If unspecified, an arbitrary order (subject to change) is used.'
type Types::IterationType.connection_type, null: true type Types::IterationType.connection_type, null: true
def resolve(**args) def resolve(**args)
validate_timeframe_params!(args) validate_timeframe_params!(args)
validate_search_params!(args)
authorize! authorize!
...@@ -40,6 +56,8 @@ module Resolvers ...@@ -40,6 +56,8 @@ module Resolvers
args[:iteration_cadence_ids] = parse_iteration_cadence_ids(args[:iteration_cadence_ids]) args[:iteration_cadence_ids] = parse_iteration_cadence_ids(args[:iteration_cadence_ids])
args[:include_ancestors] = true if args[:include_ancestors].nil? && args[:iid].nil? args[:include_ancestors] = true if args[:include_ancestors].nil? && args[:iid].nil?
handle_search_params!(args)
iterations = IterationsFinder.new(context[:current_user], iterations_finder_params(args)).execute iterations = IterationsFinder.new(context[:current_user], iterations_finder_params(args)).execute
# Necessary for scopedPath computation in IterationPresenter # Necessary for scopedPath computation in IterationPresenter
...@@ -50,6 +68,23 @@ module Resolvers ...@@ -50,6 +68,23 @@ module Resolvers
private private
def validate_search_params!(args)
if args[:title].present? && (args[:search].present? || args[:in].present?)
raise Gitlab::Graphql::Errors::ArgumentError, "'title' is deprecated in favor of 'search'. Please use 'search'."
end
if !args[:search].present? && args[:in].present?
raise Gitlab::Graphql::Errors::ArgumentError, "'search' must be specified when using 'in' argument."
end
end
def handle_search_params!(args)
return unless args[:search] || args[:title]
args[:in] = [DEFAULT_IN_FIELD] if args[:in].nil? || args[:in].empty?
args[:search] = args[:title] if args[:title]
end
def iterations_finder_params(args) def iterations_finder_params(args)
{ {
parent: parent, parent: parent,
...@@ -58,7 +93,9 @@ module Resolvers ...@@ -58,7 +93,9 @@ module Resolvers
iid: args[:iid], iid: args[:iid],
iteration_cadence_ids: args[:iteration_cadence_ids], iteration_cadence_ids: args[:iteration_cadence_ids],
state: args[:state] || 'all', state: args[:state] || 'all',
search_title: args[:title] search: args[:search],
in: args[:in],
sort: args[:sort]
}.merge(transform_timeframe_parameters(args)) }.merge(transform_timeframe_parameters(args))
end end
......
# frozen_string_literal: true
module Types
class IterationSearchableFieldEnum < BaseEnum
graphql_name 'IterationSearchableField'
description 'Fields to perform the search in'
IterationsFinder::SEARCHABLE_FIELDS.each do |field|
value field.to_s.upcase, value: field, description: "Search in #{field} field."
end
end
end
# frozen_string_literal: true
module Types
class IterationSortEnum < BaseEnum
graphql_name 'IterationSort'
description 'Iteration sort values'
value 'CADENCE_AND_DUE_DATE_ASC', 'Sort by cadence id and due date in ascending order.', value: :cadence_and_due_date_asc
end
end
...@@ -60,6 +60,7 @@ module EE ...@@ -60,6 +60,7 @@ module EE
after_commit :reset, on: [:update, :create], if: :saved_change_to_start_or_due_date? after_commit :reset, on: [:update, :create], if: :saved_change_to_start_or_due_date?
scope :due_date_order_asc, -> { order(:due_date) } scope :due_date_order_asc, -> { order(:due_date) }
scope :sort_by_cadence_id_and_due_date_asc, -> { reorder(iterations_cadence_id: :asc).due_date_order_asc.order(id: :asc) }
scope :upcoming, -> { with_state(:upcoming) } scope :upcoming, -> { with_state(:upcoming) }
scope :current, -> { with_state(:current) } scope :current, -> { with_state(:current) }
scope :closed, -> { with_state(:closed) } scope :closed, -> { with_state(:closed) }
...@@ -147,6 +148,28 @@ module EE ...@@ -147,6 +148,28 @@ module EE
else raise ArgumentError, "Unknown state filter: #{state}" else raise ArgumentError, "Unknown state filter: #{state}"
end end
end end
def search_title(query)
fuzzy_search(query, [::Resolvers::IterationsResolver::DEFAULT_IN_FIELD], use_minimum_char_limit: contains_digits?(query))
end
def search_cadence_title(query)
cadence_ids = Iterations::Cadence.search_title(query).pluck(:id)
where(iterations_cadence_id: cadence_ids)
end
def search_title_or_cadence_title(query)
union_sql = ::Gitlab::SQL::Union.new([search_title(query), search_cadence_title(query)]).to_sql
::Iteration.from("(#{union_sql}) #{table_name}")
end
private
def contains_digits?(query)
!(query =~ / \d+ /).nil?
end
end end
def display_text def display_text
......
...@@ -36,8 +36,16 @@ module Iterations ...@@ -36,8 +36,16 @@ module Iterations
.where("DATE ((COALESCE(iterations_cadences.last_run_date, DATE('01-01-1970')) + iterations_cadences.duration_in_weeks * INTERVAL '1 week')) <= CURRENT_DATE") .where("DATE ((COALESCE(iterations_cadences.last_run_date, DATE('01-01-1970')) + iterations_cadences.duration_in_weeks * INTERVAL '1 week')) <= CURRENT_DATE")
end end
def self.search_title(query) class << self
fuzzy_search(query, [:title]) def search_title(query)
fuzzy_search(query, [::Resolvers::IterationsResolver::DEFAULT_IN_FIELD], use_minimum_char_limit: contains_digit?(query))
end
private
def contains_digit?(query)
!(query =~ / \d+ /).nil?
end
end end
def next_open_iteration(date) def next_open_iteration(date)
......
...@@ -23,11 +23,23 @@ module API ...@@ -23,11 +23,23 @@ module API
end end
def iterations_finder_params(parent) def iterations_finder_params(parent)
{ finder_params = {
parent: parent, parent: parent,
include_ancestors: params[:include_ancestors], include_ancestors: params[:include_ancestors],
state: params[:state], state: params[:state],
search_title: params[:search] search: nil,
in: nil
}
finder_params.merge!(search_params) if params[:search]
finder_params
end
def search_params
{
search: params[:search],
in: [::Resolvers::IterationsResolver::DEFAULT_IN_FIELD]
} }
end end
end end
......
...@@ -10,11 +10,11 @@ RSpec.describe IterationsFinder do ...@@ -10,11 +10,11 @@ RSpec.describe IterationsFinder do
let_it_be(:iteration_cadence1) { create(:iterations_cadence, group: group, active: true, duration_in_weeks: 1, title: 'one week iterations') } let_it_be(:iteration_cadence1) { create(:iterations_cadence, group: group, active: true, duration_in_weeks: 1, title: 'one week iterations') }
let_it_be(:iteration_cadence2) { create(:iterations_cadence, group: group, active: true, duration_in_weeks: 2, title: 'two week iterations') } let_it_be(:iteration_cadence2) { create(:iterations_cadence, group: group, active: true, duration_in_weeks: 2, title: 'two week iterations') }
let_it_be(:iteration_cadence3) { create(:iterations_cadence, group: root, active: true, duration_in_weeks: 3, title: 'three week iterations') } let_it_be(:iteration_cadence3) { create(:iterations_cadence, group: root, active: true, duration_in_weeks: 3, title: 'three week iterations') }
let_it_be(:closed_iteration) { create(:closed_iteration, :skip_future_date_validation, iterations_cadence: iteration_cadence2, group: iteration_cadence2.group, start_date: 7.days.ago, due_date: 2.days.ago) } let_it_be(:closed_iteration) { create(:closed_iteration, :skip_future_date_validation, iterations_cadence: iteration_cadence2, start_date: 7.days.ago, due_date: 2.days.ago) }
let_it_be(:started_group_iteration) { create(:current_iteration, :skip_future_date_validation, iterations_cadence: iteration_cadence2, group: iteration_cadence2.group, title: 'one test', start_date: 1.day.ago, due_date: Date.today) } let_it_be(:started_group_iteration) { create(:current_iteration, :skip_future_date_validation, iterations_cadence: iteration_cadence2, title: 'one test', start_date: 1.day.ago, due_date: Date.today) }
let_it_be(:upcoming_group_iteration) { create(:iteration, iterations_cadence: iteration_cadence1, group: iteration_cadence1.group, start_date: 1.day.from_now, due_date: 3.days.from_now) } let_it_be(:upcoming_group_iteration) { create(:iteration, iterations_cadence: iteration_cadence1, title: 'Iteration 1', start_date: 1.day.from_now, due_date: 3.days.from_now) }
let_it_be(:root_group_iteration) { create(:current_iteration, iterations_cadence: iteration_cadence3, group: iteration_cadence3.group, start_date: 1.day.ago, due_date: 2.days.from_now) } let_it_be(:root_group_iteration) { create(:current_iteration, iterations_cadence: iteration_cadence3, start_date: 1.day.ago, due_date: 2.days.from_now) }
let_it_be(:root_closed_iteration) { create(:closed_iteration, iterations_cadence: iteration_cadence3, group: iteration_cadence3.group, start_date: 1.week.ago, due_date: 2.days.ago) } let_it_be(:root_closed_iteration) { create(:closed_iteration, iterations_cadence: iteration_cadence3, start_date: 1.week.ago, due_date: 2.days.ago) }
let(:parent) { project_1 } let(:parent) { project_1 }
let(:params) { { parent: parent, include_ancestors: true } } let(:params) { { parent: parent, include_ancestors: true } }
...@@ -132,10 +132,41 @@ RSpec.describe IterationsFinder do ...@@ -132,10 +132,41 @@ RSpec.describe IterationsFinder do
expect(subject.to_a).to contain_exactly(started_group_iteration) expect(subject.to_a).to contain_exactly(started_group_iteration)
end end
it 'filters by search_title' do context "with search params" do
params[:search_title] = 'one t' using RSpec::Parameterized::TableSyntax
expect(subject.to_a).to contain_exactly(started_group_iteration) shared_examples "search returns correct items" do
before do
params.merge!({ search: search, in: fields_to_search })
end
it { is_expected.to contain_exactly(*expected_iterations) }
end
context 'filters by title' do
let(:all_iterations) { [closed_iteration, started_group_iteration, upcoming_group_iteration, root_group_iteration, root_closed_iteration] }
where(:search, :fields_to_search, :expected_iterations) do
'' | [] | lazy { all_iterations }
'iteration' | [] | lazy { all_iterations }
'iteration' | [:title] | lazy { [upcoming_group_iteration] }
'iteration' | [:title] | lazy { [upcoming_group_iteration] }
'iter 1' | [:title] | lazy { [upcoming_group_iteration] }
'iteration 1' | [:title] | lazy { [upcoming_group_iteration] }
'iteration test' | [:title] | lazy { [] }
'one week iter' | [:cadence_title] | lazy { [upcoming_group_iteration] }
'iteration' | [:cadence_title] | lazy { all_iterations }
'two week' | [:cadence_title] | lazy { [closed_iteration, started_group_iteration] }
'iteration test' | [:cadence_title] | lazy { [] }
'one week' | [:title, :cadence_title] | lazy { [upcoming_group_iteration] }
'iteration' | [:title, :cadence_title] | lazy { all_iterations }
'iteration 1' | [:title, :cadence_title] | lazy { [upcoming_group_iteration] }
end
with_them do
it_behaves_like "search returns correct items"
end
end
end end
it 'filters by ID' do it 'filters by ID' do
...@@ -205,6 +236,22 @@ RSpec.describe IterationsFinder do ...@@ -205,6 +236,22 @@ RSpec.describe IterationsFinder do
end end
end end
end end
context 'sorting' do
it 'sorts by the default order (due_date, title, id asc) when no param is given' do
expect(subject).to eq([closed_iteration, root_closed_iteration, started_group_iteration, root_group_iteration, upcoming_group_iteration])
end
it 'sorts correctly when supported sorting param provided' do
params[:sort] = :cadence_and_due_date_asc
cadence1_iterations = [upcoming_group_iteration]
cadence2_iterations = [closed_iteration, started_group_iteration]
cadence3_iterations = [root_closed_iteration, root_group_iteration]
expect(subject).to eq([*cadence1_iterations, *cadence2_iterations, *cadence3_iterations])
end
end
end end
describe '#find_by' do describe '#find_by' do
......
...@@ -15,7 +15,9 @@ RSpec.describe Resolvers::IterationsResolver do ...@@ -15,7 +15,9 @@ RSpec.describe Resolvers::IterationsResolver do
iteration_cadence_ids: nil, iteration_cadence_ids: nil,
parent: nil, parent: nil,
state: nil, state: nil,
search_title: nil search: nil,
in: nil,
sort: nil
} }
end end
...@@ -50,6 +52,60 @@ RSpec.describe Resolvers::IterationsResolver do ...@@ -50,6 +52,60 @@ RSpec.describe Resolvers::IterationsResolver do
end end
context 'with parameters' do context 'with parameters' do
context 'search' do
using RSpec::Parameterized::TableSyntax
let_it_be(:plan_cadence) { create(:iterations_cadence, title: 'plan cadence', group: group) }
let_it_be(:product_cadence) { create(:iterations_cadence, title: 'product management', group: group) }
let_it_be(:plan_iteration1) { create(:iteration, :with_due_date, title: "Iteration 1", iterations_cadence: plan_cadence, start_date: 1.week.ago)}
let_it_be(:plan_iteration2) { create(:iteration, :with_due_date, title: "My iteration", iterations_cadence: plan_cadence, start_date: 2.weeks.ago)}
let_it_be(:product_iteration) { create(:iteration, :with_due_date, iterations_cadence: product_cadence, start_date: 1.week.from_now)}
let(:all_iterations) { group.iterations }
context 'with search and in parameters' do
where(:search, :fields_to_search, :expected_iterations) do
'' | [] | lazy { all_iterations }
'iteration' | nil | lazy { plan_cadence.iterations }
'iteration' | [] | lazy { plan_cadence.iterations }
'iteration' | [:title] | lazy { plan_cadence.iterations }
'iteration' | [:title, :cadence_title] | lazy { plan_cadence.iterations }
'plan' | [] | lazy { [] }
'plan' | [:cadence_title] | lazy { plan_cadence.iterations }
end
with_them do
it "returns correct items" do
expect(resolve_group_iterations({ search: search, in: fields_to_search }).items).to contain_exactly(*expected_iterations)
end
end
end
context "with the deprecated argument 'title' (to be deprecated in 15.4)" do
[
{ search: "foo" },
{ in: [:title] },
{ in: [:cadence_title] }
].each do |params|
it "raises an error when 'title' is used with #{params}" do
expect do
resolve_group_iterations({ title: "foo", **params })
end.to raise_error(Gitlab::Graphql::Errors::ArgumentError, "'title' is deprecated in favor of 'search'. Please use 'search'.")
end
end
it "raises an error when 'in' is specified but 'search' is not" do
expect do
resolve_group_iterations({ in: [:title] })
end.to raise_error(Gitlab::Graphql::Errors::ArgumentError, "'search' must be specified when using 'in' argument.")
end
it "uses 'search' and 'in' arguments to search title" do
expect(resolve_group_iterations({ title: 'iteration' }).items).to contain_exactly(*plan_cadence.iterations)
end
end
end
it 'calls IterationsFinder with correct parameters, using timeframe' do it 'calls IterationsFinder with correct parameters, using timeframe' do
start_date = now start_date = now
end_date = start_date + 1.hour end_date = start_date + 1.hour
...@@ -58,11 +114,11 @@ RSpec.describe Resolvers::IterationsResolver do ...@@ -58,11 +114,11 @@ RSpec.describe Resolvers::IterationsResolver do
iid = 2 iid = 2
iteration_cadence_ids = ['5'] iteration_cadence_ids = ['5']
params = params_list.merge(id: id, iid: iid, iteration_cadence_ids: iteration_cadence_ids, parent: group, include_ancestors: nil, state: 'closed', start_date: start_date, end_date: end_date, search_title: search) params = params_list.merge(id: id, iid: iid, iteration_cadence_ids: iteration_cadence_ids, parent: group, include_ancestors: nil, state: 'closed', start_date: start_date, end_date: end_date, search: search, in: [:title])
expect(IterationsFinder).to receive(:new).with(current_user, params).and_call_original expect(IterationsFinder).to receive(:new).with(current_user, params).and_call_original
resolve_group_iterations(timeframe: { start: start_date, end: end_date }, state: 'closed', title: search, id: 'gid://gitlab/Iteration/1', iteration_cadence_ids: ['gid://gitlab/Iterations::Cadence/5'], iid: iid) resolve_group_iterations(timeframe: { start: start_date, end: end_date }, state: 'closed', search: search, id: 'gid://gitlab/Iteration/1', iteration_cadence_ids: ['gid://gitlab/Iterations::Cadence/5'], iid: iid)
end end
it 'calls IterationsFinder with correct parameters, using start and end date' do it 'calls IterationsFinder with correct parameters, using start and end date' do
...@@ -73,11 +129,11 @@ RSpec.describe Resolvers::IterationsResolver do ...@@ -73,11 +129,11 @@ RSpec.describe Resolvers::IterationsResolver do
iid = 2 iid = 2
iteration_cadence_ids = ['5'] iteration_cadence_ids = ['5']
params = params_list.merge(id: id, iid: iid, iteration_cadence_ids: iteration_cadence_ids, parent: group, include_ancestors: nil, state: 'closed', start_date: start_date, end_date: end_date, search_title: search) params = params_list.merge(id: id, iid: iid, iteration_cadence_ids: iteration_cadence_ids, parent: group, include_ancestors: nil, state: 'closed', start_date: start_date, end_date: end_date, search: search, in: [:title])
expect(IterationsFinder).to receive(:new).with(current_user, params).and_call_original expect(IterationsFinder).to receive(:new).with(current_user, params).and_call_original
resolve_group_iterations(start_date: start_date, end_date: end_date, state: 'closed', title: search, id: 'gid://gitlab/Iteration/1', iteration_cadence_ids: ['gid://gitlab/Iterations::Cadence/5'], iid: iid) resolve_group_iterations(start_date: start_date, end_date: end_date, state: 'closed', search: search, id: 'gid://gitlab/Iteration/1', iteration_cadence_ids: ['gid://gitlab/Iterations::Cadence/5'], iid: iid)
end end
it 'accepts a raw model id for backward compatibility' do it 'accepts a raw model id for backward compatibility' do
......
...@@ -538,6 +538,82 @@ RSpec.describe Iteration do ...@@ -538,6 +538,82 @@ RSpec.describe Iteration do
end end
end end
context 'search and sorting scopes' do
let_it_be(:group1) { create(:group) }
let_it_be(:group2) { create(:group) }
let_it_be(:subgroup) { create(:group, parent: group1) }
let_it_be(:plan_cadence) { create(:iterations_cadence, title: 'plan cadence', group: group1) }
let_it_be(:product_cadence) { create(:iterations_cadence, title: 'product management', group: subgroup) }
let_it_be(:cadence) { create(:iterations_cadence, title: 'cadence', group: group2) }
let_it_be(:plan_iteration1) { create(:iteration, :with_due_date, title: "Iteration 1", iterations_cadence: plan_cadence, start_date: 1.week.ago)}
let_it_be(:plan_iteration2) { create(:iteration, :with_due_date, title: "My iteration", iterations_cadence: plan_cadence, start_date: 2.weeks.ago)}
let_it_be(:product_iteration) { create(:iteration, :with_due_date, title: "Iteration 2", iterations_cadence: product_cadence, start_date: 1.week.from_now)}
let_it_be(:cadence_iteration) { create(:iteration, :with_due_date, iterations_cadence: cadence, start_date: Date.today)}
shared_examples "search returns correct records" do
it { is_expected.to contain_exactly(*expected_iterations) }
end
describe '.search_title' do
where(:query, :expected_iterations) do
'iter 1' | lazy { [plan_iteration1] }
'iteration' | lazy { [plan_iteration1, plan_iteration2, product_iteration] }
'iteration 1' | lazy { [plan_iteration1] }
'my iteration 1' | lazy { [] }
end
with_them do
subject { described_class.search_title(query) }
it_behaves_like "search returns correct records"
end
end
describe '.search_cadence_title' do
where(:query, :expected_iterations) do
'plan' | lazy { [plan_iteration1, plan_iteration2] }
'plan cadence' | lazy { [plan_iteration1, plan_iteration2] }
'product cadence' | lazy { [] }
'cadence' | lazy { [plan_iteration1, plan_iteration2, cadence_iteration] }
end
with_them do
subject { described_class.search_cadence_title(query) }
it_behaves_like "search returns correct records"
end
end
describe '.search_title_or_cadence_title' do
where(:query, :expected_iterations) do
# The same test cases used for .search_title
'iter 1' | lazy { [plan_iteration1] }
'iteration' | lazy { [plan_iteration1, plan_iteration2, product_iteration] }
'iteration 1' | lazy { [plan_iteration1] }
'my iteration 1' | lazy { [] }
# The same test cases used for .search_cadence_title
'plan' | lazy { [plan_iteration1, plan_iteration2] }
'plan cadence' | lazy { [plan_iteration1, plan_iteration2] }
'product cadence' | lazy { [] }
'cadence' | lazy { [plan_iteration1, plan_iteration2, cadence_iteration] }
# At least one of cadence title or iteration title should contain all of the terms
'plan iteration' | lazy { [] }
end
with_them do
subject { described_class.search_title_or_cadence_title(query) }
it_behaves_like "search returns correct records"
end
end
describe '.sort_by_cadence_id_and_due_date_asc' do
subject { described_class.all.sort_by_cadence_id_and_due_date_asc }
it { is_expected.to eq([plan_iteration2, plan_iteration1, product_iteration, cadence_iteration]) }
end
end
context 'time scopes' do context 'time scopes' do
let_it_be(:group) { create(:group) } let_it_be(:group) { create(:group) }
let_it_be(:iteration_cadence) { create(:iterations_cadence, group: group) } let_it_be(:iteration_cadence) { create(:iterations_cadence, group: group) }
......
...@@ -11,9 +11,9 @@ RSpec.describe 'getting iterations' do ...@@ -11,9 +11,9 @@ RSpec.describe 'getting iterations' do
let_it_be(:iteration_cadence1) { create(:iterations_cadence, group: group, active: true, duration_in_weeks: 1, title: 'one week iterations') } let_it_be(:iteration_cadence1) { create(:iterations_cadence, group: group, active: true, duration_in_weeks: 1, title: 'one week iterations') }
let_it_be(:iteration_cadence2) { create(:iterations_cadence, group: group, active: true, duration_in_weeks: 2, title: 'two week iterations') } let_it_be(:iteration_cadence2) { create(:iterations_cadence, group: group, active: true, duration_in_weeks: 2, title: 'two week iterations') }
let_it_be(:current_group_iteration) { create(:iteration, :skip_future_date_validation, iterations_cadence: iteration_cadence1, group: iteration_cadence1.group, title: 'one test', start_date: 1.day.ago, due_date: 1.week.from_now) } let_it_be(:current_group_iteration) { create(:iteration, :skip_future_date_validation, iterations_cadence: iteration_cadence1, title: 'one test', start_date: 1.day.ago, due_date: 1.week.from_now) }
let_it_be(:upcoming_group_iteration) { create(:iteration, iterations_cadence: iteration_cadence2, group: iteration_cadence2.group, start_date: 1.day.from_now, due_date: 2.days.from_now) } let_it_be(:upcoming_group_iteration) { create(:iteration, iterations_cadence: iteration_cadence2, start_date: 1.day.from_now, due_date: 2.days.from_now) }
let_it_be(:closed_group_iteration) { create(:iteration, :skip_project_validation, iterations_cadence: iteration_cadence1, group: iteration_cadence1.group, start_date: 3.weeks.ago, due_date: 1.week.ago) } let_it_be(:closed_group_iteration) { create(:iteration, :skip_project_validation, iterations_cadence: iteration_cadence1, start_date: 3.weeks.ago, due_date: 1.week.ago) }
before do before do
group.add_maintainer(user) group.add_maintainer(user)
...@@ -47,6 +47,28 @@ RSpec.describe 'getting iterations' do ...@@ -47,6 +47,28 @@ RSpec.describe 'getting iterations' do
describe 'query for iterations by cadence' do describe 'query for iterations by cadence' do
context 'with multiple cadences' do context 'with multiple cadences' do
context 'searching by cadence title or iteration title and sorting by cadence and due date ASC' do
using RSpec::Parameterized::TableSyntax
let_it_be(:past_iteration1) { create(:iteration, :with_due_date, iterations_cadence: iteration_cadence2, start_date: 4.weeks.ago) }
let_it_be(:past_iteration2) { create(:iteration, :with_due_date, iterations_cadence: iteration_cadence2, start_date: 2.weeks.ago) }
where(:search, :ordered_expected_iterations) do
'two' | lazy { [past_iteration1, past_iteration2, upcoming_group_iteration] }
'iteration' | lazy { [closed_group_iteration, current_group_iteration, past_iteration1, past_iteration2, upcoming_group_iteration] }
end
with_them do
let(:field_queries) { "search: \"#{search}\", in: [TITLE, CADENCE_TITLE], sort: CADENCE_AND_DUE_DATE_ASC" }
it 'correctly returns ordered items' do
post_graphql(iterations_query(group, field_queries), current_user: user)
expect(actual_iterations).to eq(expected_iterations(ordered_expected_iterations))
end
end
end
it 'returns iterations' do it 'returns iterations' do
post_graphql(iteration_cadence_query(group, [iteration_cadence1.to_global_id, iteration_cadence2.to_global_id]), current_user: user) post_graphql(iteration_cadence_query(group, [iteration_cadence1.to_global_id, iteration_cadence2.to_global_id]), current_user: user)
...@@ -103,11 +125,16 @@ RSpec.describe 'getting iterations' do ...@@ -103,11 +125,16 @@ RSpec.describe 'getting iterations' do
QUERY QUERY
end end
def expect_iterations_response(*iterations) def actual_iterations
actual_iterations = graphql_data['group']['iterations']['nodes'].map { |iteration| iteration['id'] } graphql_data['group']['iterations']['nodes'].map { |iteration| iteration['id'] }
expected_iterations = iterations.map { |iteration| iteration.to_global_id.to_s } end
expect(actual_iterations).to contain_exactly(*expected_iterations) def expected_iterations(iterations)
iterations.map { |iteration| iteration.to_global_id.to_s }
end
def expect_iterations_response(*iterations)
expect(actual_iterations).to contain_exactly(*expected_iterations(iterations))
expect(graphql_errors).to be_nil expect(graphql_errors).to be_nil
end end
end end
...@@ -12,8 +12,8 @@ RSpec.describe IterationsUpdateStatusWorker do ...@@ -12,8 +12,8 @@ RSpec.describe IterationsUpdateStatusWorker do
describe '#perform' do describe '#perform' do
before do before do
current_iteration1.update_column(:state_enum, 2) current_iteration1.update_column(:state_enum, Iteration::STATE_ENUM_MAP[:current])
closed_iteration1.update_column(:state_enum, 1) closed_iteration1.update_column(:state_enum, Iteration::STATE_ENUM_MAP[:upcoming])
end end
it 'schedules an issues roll-over job' do it 'schedules an issues roll-over job' 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