Commit c3abd803 authored by Andreas Brandl's avatar Andreas Brandl

Merge branch '32632-release-search-filter-be' into 'master'

Added queries for release search filter

Closes #32632

See merge request gitlab-org/gitlab!19806
parents cdccbb1c 95cf4b0c
...@@ -44,7 +44,7 @@ class Projects::IssuesController < Projects::ApplicationController ...@@ -44,7 +44,7 @@ class Projects::IssuesController < Projects::ApplicationController
before_action do before_action do
push_frontend_feature_flag(:vue_issuable_sidebar, project.group) push_frontend_feature_flag(:vue_issuable_sidebar, project.group)
push_frontend_feature_flag(:release_search_filter, project) push_frontend_feature_flag(:release_search_filter, project, default_enabled: true)
end end
respond_to :html respond_to :html
......
...@@ -24,7 +24,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo ...@@ -24,7 +24,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
before_action do before_action do
push_frontend_feature_flag(:vue_issuable_sidebar, @project.group) push_frontend_feature_flag(:vue_issuable_sidebar, @project.group)
push_frontend_feature_flag(:release_search_filter, @project) push_frontend_feature_flag(:release_search_filter, @project, default_enabled: true)
end end
around_action :allow_gitaly_ref_name_caching, only: [:index, :show, :discussions] around_action :allow_gitaly_ref_name_caching, only: [:index, :show, :discussions]
......
...@@ -13,6 +13,7 @@ ...@@ -13,6 +13,7 @@
# group_id: integer # group_id: integer
# project_id: integer # project_id: integer
# milestone_title: string # milestone_title: string
# release_tag: string
# author_id: integer # author_id: integer
# author_username: string # author_username: string
# assignee_id: integer or 'None' or 'Any' # assignee_id: integer or 'None' or 'Any'
...@@ -59,6 +60,7 @@ class IssuableFinder ...@@ -59,6 +60,7 @@ class IssuableFinder
author_username author_username
label_name label_name
milestone_title milestone_title
release_tag
my_reaction_emoji my_reaction_emoji
search search
in in
...@@ -126,6 +128,7 @@ class IssuableFinder ...@@ -126,6 +128,7 @@ class IssuableFinder
items = by_non_archived(items) items = by_non_archived(items)
items = by_iids(items) items = by_iids(items)
items = by_milestone(items) items = by_milestone(items)
items = by_release(items)
items = by_label(items) items = by_label(items)
by_my_reaction_emoji(items) by_my_reaction_emoji(items)
end end
...@@ -364,6 +367,10 @@ class IssuableFinder ...@@ -364,6 +367,10 @@ class IssuableFinder
end end
end end
def releases?
params[:release_tag].present?
end
private private
def force_cte? def force_cte?
...@@ -570,6 +577,18 @@ class IssuableFinder ...@@ -570,6 +577,18 @@ class IssuableFinder
end end
# rubocop: enable CodeReuse/ActiveRecord # rubocop: enable CodeReuse/ActiveRecord
def by_release(items)
return items unless releases?
if filter_by_no_release?
items.without_release
elsif filter_by_any_release?
items.any_release
else
items.with_release(params[:release_tag], params[:project_id])
end
end
def filter_by_no_milestone? def filter_by_no_milestone?
# Accepts `No Milestone` for compatibility # Accepts `No Milestone` for compatibility
params[:milestone_title].to_s.downcase == FILTER_NONE || params[:milestone_title] == Milestone::None.title params[:milestone_title].to_s.downcase == FILTER_NONE || params[:milestone_title] == Milestone::None.title
...@@ -588,6 +607,14 @@ class IssuableFinder ...@@ -588,6 +607,14 @@ class IssuableFinder
params[:milestone_title] == Milestone::Started.name params[:milestone_title] == Milestone::Started.name
end end
def filter_by_no_release?
params[:release_tag].to_s.downcase == FILTER_NONE
end
def filter_by_any_release?
params[:release_tag].to_s.downcase == FILTER_ANY
end
def by_label(items) def by_label(items)
return items unless labels? return items unless labels?
......
...@@ -12,6 +12,7 @@ ...@@ -12,6 +12,7 @@
# group_id: integer # group_id: integer
# project_id: integer # project_id: integer
# milestone_title: string # milestone_title: string
# release_tag: string
# author_id: integer # author_id: integer
# assignee_id: integer # assignee_id: integer
# search: string # search: string
......
...@@ -99,6 +99,8 @@ module Issuable ...@@ -99,6 +99,8 @@ module Issuable
scope :of_milestones, ->(ids) { where(milestone_id: ids) } scope :of_milestones, ->(ids) { where(milestone_id: ids) }
scope :any_milestone, -> { where('milestone_id IS NOT NULL') } scope :any_milestone, -> { where('milestone_id IS NOT NULL') }
scope :with_milestone, ->(title) { left_joins_milestones.where(milestones: { title: title }) } scope :with_milestone, ->(title) { left_joins_milestones.where(milestones: { title: title }) }
scope :any_release, -> { joins_milestone_releases }
scope :with_release, -> (tag, project_id) { joins_milestone_releases.where( milestones: { releases: { tag: tag, project_id: project_id } } ) }
scope :opened, -> { with_state(:opened) } scope :opened, -> { with_state(:opened) }
scope :only_opened, -> { with_state(:opened) } scope :only_opened, -> { with_state(:opened) }
scope :closed, -> { with_state(:closed) } scope :closed, -> { with_state(:closed) }
...@@ -120,6 +122,16 @@ module Issuable ...@@ -120,6 +122,16 @@ module Issuable
scope :order_milestone_due_desc, -> { left_joins_milestones.reorder(Arel.sql('milestones.due_date IS NULL, milestones.id IS NULL, milestones.due_date DESC')) } scope :order_milestone_due_desc, -> { left_joins_milestones.reorder(Arel.sql('milestones.due_date IS NULL, milestones.id IS NULL, milestones.due_date DESC')) }
scope :order_milestone_due_asc, -> { left_joins_milestones.reorder(Arel.sql('milestones.due_date IS NULL, milestones.id IS NULL, milestones.due_date ASC')) } scope :order_milestone_due_asc, -> { left_joins_milestones.reorder(Arel.sql('milestones.due_date IS NULL, milestones.id IS NULL, milestones.due_date ASC')) }
scope :without_release, -> do
joins("LEFT OUTER JOIN milestone_releases ON #{table_name}.milestone_id = milestone_releases.milestone_id")
.where('milestone_releases.release_id IS NULL')
end
scope :joins_milestone_releases, -> do
joins("JOIN milestone_releases ON issues.milestone_id = milestone_releases.milestone_id
JOIN releases ON milestone_releases.release_id = releases.id").distinct
end
scope :without_label, -> { joins("LEFT OUTER JOIN label_links ON label_links.target_type = '#{name}' AND label_links.target_id = #{table_name}.id").where(label_links: { id: nil }) } scope :without_label, -> { joins("LEFT OUTER JOIN label_links ON label_links.target_type = '#{name}' AND label_links.target_id = #{table_name}.id").where(label_links: { id: nil }) }
scope :any_label, -> { joins(:label_links).group(:id) } scope :any_label, -> { joins(:label_links).group(:id) }
scope :join_project, -> { joins(:project) } scope :join_project, -> { joins(:project) }
......
...@@ -852,4 +852,77 @@ describe Issuable do ...@@ -852,4 +852,77 @@ describe Issuable do
it_behaves_like 'matches_cross_reference_regex? fails fast' it_behaves_like 'matches_cross_reference_regex? fails fast'
end end
end end
describe 'release scopes' do
let_it_be(:project) { create(:project) }
let_it_be(:release_1) { create(:release, tag: 'v1.0', project: project) }
let_it_be(:release_2) { create(:release, tag: 'v2.0', project: project) }
let_it_be(:release_3) { create(:release, tag: 'v3.0', project: project) }
let_it_be(:release_4) { create(:release, tag: 'v4.0', project: project) }
let_it_be(:milestone_1) { create(:milestone, releases: [release_1], title: 'm1', project: project) }
let_it_be(:milestone_2) { create(:milestone, releases: [release_1, release_2], title: 'm2', project: project) }
let_it_be(:milestone_3) { create(:milestone, releases: [release_2, release_4], title: 'm3', project: project) }
let_it_be(:milestone_4) { create(:milestone, releases: [release_3], title: 'm4', project: project) }
let_it_be(:milestone_5) { create(:milestone, releases: [release_3], title: 'm5', project: project) }
let_it_be(:milestone_6) { create(:milestone, title: 'm6', project: project) }
let_it_be(:issue_1) { create(:issue, milestone: milestone_1, project: project) }
let_it_be(:issue_2) { create(:issue, milestone: milestone_1, project: project) }
let_it_be(:issue_3) { create(:issue, milestone: milestone_2, project: project) }
let_it_be(:issue_4) { create(:issue, milestone: milestone_5, project: project) }
let_it_be(:issue_5) { create(:issue, milestone: milestone_6, project: project) }
let_it_be(:issue_6) { create(:issue, project: project) }
let_it_be(:items) { Issue.all }
describe '#without_release' do
it 'returns the issues not tied to any milestone and the ones tied to milestone with no release' do
expect(items.without_release).to contain_exactly(issue_5, issue_6)
end
end
describe '#any_release' do
it 'returns all issues tied to a release' do
expect(items.any_release).to contain_exactly(issue_1, issue_2, issue_3, issue_4)
end
end
describe '#with_release' do
it 'returns the issues tied a specfic release' do
expect(items.with_release('v1.0', project.id)).to contain_exactly(issue_1, issue_2, issue_3)
end
context 'when a release has a milestone with one issue and another one with no issue' do
it 'returns that one issue' do
expect(items.with_release('v2.0', project.id)).to contain_exactly(issue_3)
end
context 'when the milestone with no issue is added as a filter' do
it 'returns an empty list' do
expect(items.with_release('v2.0', project.id).with_milestone('m3')).to be_empty
end
end
context 'when the milestone with the issue is added as a filter' do
it 'returns this issue' do
expect(items.with_release('v2.0', project.id).with_milestone('m2')).to contain_exactly(issue_3)
end
end
end
context 'when there is no issue under a specific release' do
it 'returns no issue' do
expect(items.with_release('v4.0', project.id)).to be_empty
end
end
context 'when a non-existent release tag is passed in' do
it 'returns no issue' do
expect(items.with_release('v999.0', project.id)).to be_empty
end
end
end
end
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