Commit e9aa261b authored by Terri Chu's avatar Terri Chu Committed by Stan Hu

Allow search issues scope to filter by status

Add filter ability to basic and advanced search
issues scope. Results can be filtered by state
(any, opened, or closed). Default is any.
parent 21607734
import Search from './search'; import Search from './search';
import initStateFilter from '~/search/state_filter';
document.addEventListener('DOMContentLoaded', () => new Search()); document.addEventListener('DOMContentLoaded', () => {
initStateFilter();
return new Search();
});
<script>
import { GlDropdown, GlDropdownItem, GlDropdownDivider } from '@gitlab/ui';
import { FILTER_STATES, FILTER_HEADER, FILTER_TEXT } from '../constants';
import { setUrlParams, visitUrl } from '~/lib/utils/url_utility';
const FILTERS_ARRAY = Object.values(FILTER_STATES);
export default {
name: 'StateFilter',
components: {
GlDropdown,
GlDropdownItem,
GlDropdownDivider,
},
props: {
scope: {
type: String,
required: true,
},
state: {
type: String,
required: false,
default: FILTER_STATES.ANY.value,
validator: v => FILTERS_ARRAY.some(({ value }) => value === v),
},
},
computed: {
selectedFilterText() {
let filterText = FILTER_TEXT;
if (this.selectedFilter === FILTER_STATES.CLOSED.value) {
filterText = FILTER_STATES.CLOSED.label;
} else if (this.selectedFilter === FILTER_STATES.OPEN.value) {
filterText = FILTER_STATES.OPEN.label;
}
return filterText;
},
selectedFilter: {
get() {
if (FILTERS_ARRAY.some(({ value }) => value === this.state)) {
return this.state;
}
return FILTER_STATES.ANY.value;
},
set(state) {
visitUrl(setUrlParams({ state }));
},
},
},
methods: {
dropDownItemClass(filter) {
return {
'gl-border-b-solid gl-border-b-gray-100 gl-border-b-1 gl-pb-2! gl-mb-2':
filter === FILTER_STATES.ANY,
};
},
isFilterSelected(filter) {
return filter === this.selectedFilter;
},
handleFilterChange(state) {
this.selectedFilter = state;
},
},
filterStates: FILTER_STATES,
filterHeader: FILTER_HEADER,
filtersArray: FILTERS_ARRAY,
};
</script>
<template>
<gl-dropdown
v-if="scope === 'issues'"
:text="selectedFilterText"
class="col-sm-3 gl-pt-4 gl-pl-0"
>
<header class="gl-text-center gl-font-weight-bold gl-font-lg">
{{ $options.filterHeader }}
</header>
<gl-dropdown-divider />
<gl-dropdown-item
v-for="filter in $options.filtersArray"
:key="filter.value"
:is-check-item="true"
:is-checked="isFilterSelected(filter.value)"
:class="dropDownItemClass(filter)"
@click="handleFilterChange(filter.value)"
>
{{ filter.label }}
</gl-dropdown-item>
</gl-dropdown>
</template>
import { __ } from '~/locale';
export const FILTER_HEADER = __('Status');
export const FILTER_TEXT = __('Any Status');
export const FILTER_STATES = {
ANY: {
label: __('Any'),
value: 'all',
},
OPEN: {
label: __('Open'),
value: 'opened',
},
CLOSED: {
label: __('Closed'),
value: 'closed',
},
};
import Vue from 'vue';
import Translate from '~/vue_shared/translate';
import StateFilter from './components/state_filter.vue';
Vue.use(Translate);
export default () => {
const el = document.getElementById('js-search-filter-by-state');
if (!el) return false;
return new Vue({
el,
components: {
StateFilter,
},
data() {
const { dataset } = this.$options.el;
return {
scope: dataset.scope,
state: dataset.state,
};
},
render(createElement) {
return createElement('state-filter', {
props: {
scope: this.scope,
state: this.state,
},
});
},
});
};
...@@ -8,7 +8,7 @@ ...@@ -8,7 +8,7 @@
# current_user - which user use # current_user - which user use
# params: # params:
# scope: 'created_by_me' or 'assigned_to_me' or 'all' # scope: 'created_by_me' or 'assigned_to_me' or 'all'
# state: 'open' or 'closed' or 'all' # state: 'opened' or 'closed' or 'all'
# group_id: integer # group_id: integer
# project_id: integer # project_id: integer
# milestone_title: string # milestone_title: string
......
# frozen_string_literal: true # frozen_string_literal: true
module SearchHelper module SearchHelper
SEARCH_PERMITTED_PARAMS = [:search, :scope, :project_id, :group_id, :repository_ref, :snippets].freeze SEARCH_PERMITTED_PARAMS = [:search, :scope, :project_id, :group_id, :repository_ref, :snippets, :state].freeze
def search_autocomplete_opts(term) def search_autocomplete_opts(term)
return unless current_user return unless current_user
......
...@@ -13,7 +13,8 @@ module Search ...@@ -13,7 +13,8 @@ module Search
def execute def execute
Gitlab::SearchResults.new(current_user, Gitlab::SearchResults.new(current_user,
params[:search], params[:search],
projects) projects,
filters: { state: params[:state] })
end end
def projects def projects
......
...@@ -15,7 +15,8 @@ module Search ...@@ -15,7 +15,8 @@ module Search
current_user, current_user,
params[:search], params[:search],
projects, projects,
group: group group: group,
filters: { state: params[:state] }
) )
end end
......
...@@ -12,7 +12,8 @@ module Search ...@@ -12,7 +12,8 @@ module Search
Gitlab::ProjectSearchResults.new(current_user, Gitlab::ProjectSearchResults.new(current_user,
params[:search], params[:search],
project: project, project: project,
repository_ref: params[:repository_ref]) repository_ref: params[:repository_ref],
filters: { state: params[:state] })
end end
def scope def scope
......
...@@ -22,6 +22,8 @@ ...@@ -22,6 +22,8 @@
= _("in group %{link_to_group}").html_safe % { link_to_group: link_to_group } = _("in group %{link_to_group}").html_safe % { link_to_group: link_to_group }
= render_if_exists 'shared/promotions/promote_advanced_search' = render_if_exists 'shared/promotions/promote_advanced_search'
#js-search-filter-by-state{ 'v-cloak': true, data: { scope: @scope, state: params[:state] } }
.results.gl-mt-3 .results.gl-mt-3
- if @scope == 'commits' - if @scope == 'commits'
%ul.content-list.commit-list %ul.content-list.commit-list
......
---
title: Search UI Allow issue scope results filtering by state
merge_request: 39881
author:
type: changed
...@@ -15,7 +15,8 @@ module EE ...@@ -15,7 +15,8 @@ module EE
current_user, current_user,
params[:search], params[:search],
projects, projects,
public_and_internal_projects: elastic_global public_and_internal_projects: elastic_global,
filters: { state: params[:state] }
) )
end end
......
...@@ -24,7 +24,8 @@ module EE ...@@ -24,7 +24,8 @@ module EE
params[:search], params[:search],
projects, projects,
group: group, group: group,
public_and_internal_projects: elastic_global public_and_internal_projects: elastic_global,
filters: { state: params[:state] }
) )
end end
end end
......
...@@ -14,7 +14,8 @@ module EE ...@@ -14,7 +14,8 @@ module EE
current_user, current_user,
params[:search], params[:search],
project: project, project: project,
repository_ref: repository_ref repository_ref: repository_ref,
filters: { state: params[:state] }
) )
end end
......
...@@ -21,12 +21,25 @@ module Elastic ...@@ -21,12 +21,25 @@ module Elastic
options[:features] = 'issues' options[:features] = 'issues'
query_hash = project_ids_filter(query_hash, options) query_hash = project_ids_filter(query_hash, options)
query_hash = confidentiality_filter(query_hash, options) query_hash = confidentiality_filter(query_hash, options)
query_hash = state_filter(query_hash, options)
search(query_hash, options) search(query_hash, options)
end end
private private
def state_filter(query_hash, options)
state = options[:state]
return query_hash if state.blank? || state == 'all'
return query_hash unless %w(all opened closed).include?(state)
filter = { match: { state: state } }
query_hash[:query][:bool][:filter] << filter
query_hash
end
def confidentiality_filter(query_hash, options) def confidentiality_filter(query_hash, options)
current_user = options[:current_user] current_user = options[:current_user]
project_ids = options[:project_ids] project_ids = options[:project_ids]
......
...@@ -6,7 +6,7 @@ module Elastic ...@@ -6,7 +6,7 @@ module Elastic
def as_indexed_json(options = {}) def as_indexed_json(options = {})
data = {} data = {}
# We don't use as_json(only: ...) because it calls all virtual and serialized attributtes # We don't use as_json(only: ...) because it calls all virtual and serialized attributes
# https://gitlab.com/gitlab-org/gitlab/issues/349 # https://gitlab.com/gitlab-org/gitlab/issues/349
[:id, :iid, :title, :description, :created_at, :updated_at, :state, :project_id, :author_id, :confidential].each do |attr| [:id, :iid, :title, :description, :created_at, :updated_at, :state, :project_id, :author_id, :confidential].each do |attr|
data[attr.to_s] = safely_read_attribute_for_elasticsearch(attr) data[attr.to_s] = safely_read_attribute_for_elasticsearch(attr)
......
...@@ -9,13 +9,14 @@ module Gitlab ...@@ -9,13 +9,14 @@ module Gitlab
delegate :users, to: :generic_search_results delegate :users, to: :generic_search_results
delegate :limited_users_count, to: :generic_search_results delegate :limited_users_count, to: :generic_search_results
attr_reader :group, :default_project_filter attr_reader :group, :default_project_filter, :filters
def initialize(current_user, query, limit_projects = nil, group:, public_and_internal_projects: false, default_project_filter: false) def initialize(current_user, query, limit_projects = nil, group:, public_and_internal_projects: false, default_project_filter: false, filters: {})
@group = group @group = group
@default_project_filter = default_project_filter @default_project_filter = default_project_filter
@filters = filters
super(current_user, query, limit_projects, public_and_internal_projects: public_and_internal_projects) super(current_user, query, limit_projects, public_and_internal_projects: public_and_internal_projects, filters: filters)
end end
def generic_search_results def generic_search_results
...@@ -23,7 +24,8 @@ module Gitlab ...@@ -23,7 +24,8 @@ module Gitlab
current_user, current_user,
query, query,
limit_projects, limit_projects,
group: group group: group,
filters: filters
) )
end end
end end
......
...@@ -6,16 +6,16 @@ module Gitlab ...@@ -6,16 +6,16 @@ module Gitlab
# superclass inside a module, because autoloading can occur in a # superclass inside a module, because autoloading can occur in a
# different order between execution environments. # different order between execution environments.
class ProjectSearchResults < Gitlab::Elastic::SearchResults class ProjectSearchResults < Gitlab::Elastic::SearchResults
attr_reader :project, :repository_ref attr_reader :project, :repository_ref, :filters
delegate :users, to: :generic_search_results delegate :users, to: :generic_search_results
delegate :limited_users_count, to: :generic_search_results delegate :limited_users_count, to: :generic_search_results
def initialize(current_user, query, project:, repository_ref: nil) def initialize(current_user, query, project:, repository_ref: nil, filters: {})
@project = project @project = project
@repository_ref = repository_ref.presence || project.default_branch @repository_ref = repository_ref.presence || project.default_branch
super(current_user, query, [project], public_and_internal_projects: false) super(current_user, query, [project], public_and_internal_projects: false, filters: filters)
end end
def generic_search_results def generic_search_results
...@@ -23,7 +23,8 @@ module Gitlab ...@@ -23,7 +23,8 @@ module Gitlab
current_user, current_user,
query, query,
project: project, project: project,
repository_ref: repository_ref repository_ref: repository_ref,
filters: filters
) )
end end
......
...@@ -7,7 +7,7 @@ module Gitlab ...@@ -7,7 +7,7 @@ module Gitlab
DEFAULT_PER_PAGE = Gitlab::SearchResults::DEFAULT_PER_PAGE DEFAULT_PER_PAGE = Gitlab::SearchResults::DEFAULT_PER_PAGE
attr_reader :current_user, :query, :public_and_internal_projects attr_reader :current_user, :query, :public_and_internal_projects, :filters
# Limit search results by passed projects # Limit search results by passed projects
# It allows us to search only for projects user has access to # It allows us to search only for projects user has access to
...@@ -16,11 +16,12 @@ module Gitlab ...@@ -16,11 +16,12 @@ module Gitlab
delegate :users, to: :generic_search_results delegate :users, to: :generic_search_results
delegate :limited_users_count, to: :generic_search_results delegate :limited_users_count, to: :generic_search_results
def initialize(current_user, query, limit_projects = nil, public_and_internal_projects: true) def initialize(current_user, query, limit_projects = nil, public_and_internal_projects: true, filters: {})
@current_user = current_user @current_user = current_user
@query = query @query = query
@limit_projects = limit_projects @limit_projects = limit_projects
@public_and_internal_projects = public_and_internal_projects @public_and_internal_projects = public_and_internal_projects
@filters = filters
end end
def objects(scope, page: 1, per_page: DEFAULT_PER_PAGE, preload_method: nil) def objects(scope, page: 1, per_page: DEFAULT_PER_PAGE, preload_method: nil)
...@@ -221,7 +222,10 @@ module Gitlab ...@@ -221,7 +222,10 @@ module Gitlab
def issues def issues
strong_memoize(:issues) do strong_memoize(:issues) do
Issue.elastic_search(query, options: base_options) options = base_options
options[:state] = filters[:state] if filters.key?(:state)
Issue.elastic_search(query, options: options)
end end
end end
......
...@@ -2,17 +2,32 @@ ...@@ -2,17 +2,32 @@
require 'spec_helper' require 'spec_helper'
RSpec.describe Gitlab::Elastic::GroupSearchResults do RSpec.describe Gitlab::Elastic::GroupSearchResults, :elastic do
let_it_be(:user) { create(:user) } let_it_be(:user) { create(:user) }
let_it_be(:group) { create(:group) } let_it_be(:group) { create(:group) }
let_it_be(:guest) { create(:user).tap { |u| group.add_user(u, Gitlab::Access::GUEST) } } let_it_be(:guest) { create(:user).tap { |u| group.add_user(u, Gitlab::Access::GUEST) } }
let(:filters) { {} }
let(:query) { '*' }
subject(:results) { described_class.new(user, query, group: group) } subject(:results) { described_class.new(user, query, Project.all, group: group, filters: filters) }
before do before do
stub_ee_application_setting(elasticsearch_search: true, elasticsearch_indexing: true) stub_ee_application_setting(elasticsearch_search: true, elasticsearch_indexing: true)
end end
context 'issues search', :sidekiq_inline do
let!(:project) { create(:project, :public, group: group) }
let!(:closed_issue) { create(:issue, :closed, project: project, title: 'foo closed') }
let!(:opened_issue) { create(:issue, :opened, project: project, title: 'foo opened') }
let(:query) { 'foo' }
include_examples 'search issues scope filters by state' do
before do
ensure_elasticsearch_index!
end
end
end
context 'user search' do context 'user search' do
let(:query) { guest.username } let(:query) { guest.username }
...@@ -40,8 +55,6 @@ RSpec.describe Gitlab::Elastic::GroupSearchResults do ...@@ -40,8 +55,6 @@ RSpec.describe Gitlab::Elastic::GroupSearchResults do
end end
context 'query performance' do context 'query performance' do
let(:query) { '*' }
include_examples 'does not hit Elasticsearch twice for objects and counts', %w|projects notes blobs wiki_blobs commits issues merge_requests milestones| include_examples 'does not hit Elasticsearch twice for objects and counts', %w|projects notes blobs wiki_blobs commits issues merge_requests milestones|
end end
end end
...@@ -3,12 +3,13 @@ ...@@ -3,12 +3,13 @@
require 'spec_helper' require 'spec_helper'
RSpec.describe Gitlab::Elastic::ProjectSearchResults, :elastic do RSpec.describe Gitlab::Elastic::ProjectSearchResults, :elastic do
let(:user) { create(:user) } let_it_be(:user) { create(:user) }
let(:project) { create(:project, :public, :repository) } let_it_be(:project) { create(:project, :public, :repository) }
let(:query) { 'hello world' } let(:query) { 'hello world' }
let(:repository_ref) { nil } let(:repository_ref) { nil }
let(:filters) { {} }
subject(:results) { described_class.new(user, query, project: project, repository_ref: repository_ref) } subject(:results) { described_class.new(user, query, project: project, repository_ref: repository_ref, filters: filters) }
before do before do
stub_ee_application_setting(elasticsearch_search: true, elasticsearch_indexing: true) stub_ee_application_setting(elasticsearch_search: true, elasticsearch_indexing: true)
...@@ -30,9 +31,9 @@ RSpec.describe Gitlab::Elastic::ProjectSearchResults, :elastic do ...@@ -30,9 +31,9 @@ RSpec.describe Gitlab::Elastic::ProjectSearchResults, :elastic do
it { expect(results.query).to eq('hello world') } it { expect(results.query).to eq('hello world') }
end end
describe "search", :sidekiq_might_not_need_inline do describe "search", :sidekiq_inline do
let(:project) { create(:project, :public, :repository, :wiki_repo) } let_it_be(:project) { create(:project, :public, :repository, :wiki_repo) }
let(:private_project) { create(:project, :repository, :wiki_repo) } let_it_be(:private_project) { create(:project, :repository, :wiki_repo) }
before do before do
[project, private_project].each do |project| [project, private_project].each do |project|
...@@ -56,7 +57,7 @@ RSpec.describe Gitlab::Elastic::ProjectSearchResults, :elastic do ...@@ -56,7 +57,7 @@ RSpec.describe Gitlab::Elastic::ProjectSearchResults, :elastic do
end end
context 'visibility checks' do context 'visibility checks' do
let(:project) { create(:project, :public, :wiki_repo) } let_it_be(:project) { create(:project, :public, :wiki_repo) }
let(:query) { 'term' } let(:query) { 'term' }
before do before do
...@@ -67,6 +68,19 @@ RSpec.describe Gitlab::Elastic::ProjectSearchResults, :elastic do ...@@ -67,6 +68,19 @@ RSpec.describe Gitlab::Elastic::ProjectSearchResults, :elastic do
expect(results.wiki_blobs_count).to eq(1) expect(results.wiki_blobs_count).to eq(1)
end end
end end
context 'filtering' do
include_examples 'search issues scope filters by state' do
let!(:project) { create(:project, :public) }
let!(:closed_issue) { create(:issue, :closed, project: project, title: 'foo closed') }
let!(:opened_issue) { create(:issue, :opened, project: project, title: 'foo opened') }
let(:query) { 'foo' }
before do
ensure_elasticsearch_index!
end
end
end
end end
describe "search for commits in non-default branch" do describe "search for commits in non-default branch" do
...@@ -147,13 +161,14 @@ RSpec.describe Gitlab::Elastic::ProjectSearchResults, :elastic do ...@@ -147,13 +161,14 @@ RSpec.describe Gitlab::Elastic::ProjectSearchResults, :elastic do
it { expect(results.limited_users_count).to eq(1) } it { expect(results.limited_users_count).to eq(1) }
describe 'pagination' do describe 'pagination' do
let(:query) {} let(:query) { }
let!(:user2) { create(:user).tap { |u| project.add_user(u, Gitlab::Access::REPORTER) } } let_it_be(:user2) { create(:user).tap { |u| project.add_user(u, Gitlab::Access::REPORTER) } }
it 'returns the correct page of results' do it 'returns the correct page of results' do
expect(results.objects('users', page: 1, per_page: 1)).to eq([project.owner]) # UsersFinder defaults to order_id_desc, the newer result will be first
expect(results.objects('users', page: 2, per_page: 1)).to eq([user2]) expect(results.objects('users', page: 1, per_page: 1)).to eq([user2])
expect(results.objects('users', page: 2, per_page: 1)).to eq([project.owner])
end end
it 'returns the correct number of results for one page' do it 'returns the correct number of results for one page' do
......
...@@ -171,6 +171,20 @@ RSpec.describe Gitlab::Elastic::SearchResults, :elastic, :sidekiq_might_not_need ...@@ -171,6 +171,20 @@ RSpec.describe Gitlab::Elastic::SearchResults, :elastic, :sidekiq_might_not_need
expect(results.objects('issues')).to be_empty expect(results.objects('issues')).to be_empty
expect(results.issues_count).to eq 0 expect(results.issues_count).to eq 0
end end
context 'filtering' do
let!(:project) { create(:project, :public) }
let!(:closed_issue) { create(:issue, :closed, project: project, title: 'foo closed') }
let!(:opened_issue) { create(:issue, :opened, project: project, title: 'foo opened') }
let(:results) { described_class.new(user, 'foo', [project], filters: filters) }
include_examples 'search issues scope filters by state' do
before do
ensure_elasticsearch_index!
end
end
end
end end
describe 'notes' do describe 'notes' do
......
...@@ -4,10 +4,10 @@ module Gitlab ...@@ -4,10 +4,10 @@ module Gitlab
class GroupSearchResults < SearchResults class GroupSearchResults < SearchResults
attr_reader :group attr_reader :group
def initialize(current_user, query, limit_projects = nil, group:, default_project_filter: false) def initialize(current_user, query, limit_projects = nil, group:, default_project_filter: false, filters: {})
@group = group @group = group
super(current_user, query, limit_projects, default_project_filter: default_project_filter) super(current_user, query, limit_projects, default_project_filter: default_project_filter, filters: filters)
end end
# rubocop:disable CodeReuse/ActiveRecord # rubocop:disable CodeReuse/ActiveRecord
......
...@@ -4,11 +4,11 @@ module Gitlab ...@@ -4,11 +4,11 @@ module Gitlab
class ProjectSearchResults < SearchResults class ProjectSearchResults < SearchResults
attr_reader :project, :repository_ref attr_reader :project, :repository_ref
def initialize(current_user, query, project:, repository_ref: nil) def initialize(current_user, query, project:, repository_ref: nil, filters: {})
@project = project @project = project
@repository_ref = repository_ref.presence @repository_ref = repository_ref.presence
super(current_user, query, [project]) super(current_user, query, [project], filters: filters)
end end
def objects(scope, page: nil, per_page: DEFAULT_PER_PAGE, preload_method: nil) def objects(scope, page: nil, per_page: DEFAULT_PER_PAGE, preload_method: nil)
......
...@@ -7,7 +7,7 @@ module Gitlab ...@@ -7,7 +7,7 @@ module Gitlab
DEFAULT_PAGE = 1 DEFAULT_PAGE = 1
DEFAULT_PER_PAGE = 20 DEFAULT_PER_PAGE = 20
attr_reader :current_user, :query attr_reader :current_user, :query, :filters
# Limit search results by passed projects # Limit search results by passed projects
# It allows us to search only for projects user has access to # It allows us to search only for projects user has access to
...@@ -19,11 +19,12 @@ module Gitlab ...@@ -19,11 +19,12 @@ module Gitlab
# query # query
attr_reader :default_project_filter attr_reader :default_project_filter
def initialize(current_user, query, limit_projects = nil, default_project_filter: false) def initialize(current_user, query, limit_projects = nil, default_project_filter: false, filters: {})
@current_user = current_user @current_user = current_user
@query = query @query = query
@limit_projects = limit_projects || Project.all @limit_projects = limit_projects || Project.all
@default_project_filter = default_project_filter @default_project_filter = default_project_filter
@filters = filters
end end
def objects(scope, page: nil, per_page: DEFAULT_PER_PAGE, without_count: true, preload_method: nil) def objects(scope, page: nil, per_page: DEFAULT_PER_PAGE, without_count: true, preload_method: nil)
...@@ -190,6 +191,8 @@ module Gitlab ...@@ -190,6 +191,8 @@ module Gitlab
else else
params[:search] = query params[:search] = query
end end
params[:state] = filters[:state] if filters.key?(:state)
end end
end end
......
...@@ -2957,6 +2957,9 @@ msgstr "" ...@@ -2957,6 +2957,9 @@ msgstr ""
msgid "Any Author" msgid "Any Author"
msgstr "" msgstr ""
msgid "Any Status"
msgstr ""
msgid "Any branch" msgid "Any branch"
msgstr "" msgstr ""
......
import { shallowMount } from '@vue/test-utils';
import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
import StateFilter from '~/search/state_filter/components/state_filter.vue';
import { FILTER_STATES } from '~/search/state_filter/constants';
import * as urlUtils from '~/lib/utils/url_utility';
jest.mock('~/lib/utils/url_utility', () => ({
visitUrl: jest.fn(),
setUrlParams: jest.fn(),
}));
function createComponent(props = { scope: 'issues' }) {
return shallowMount(StateFilter, {
propsData: {
...props,
},
});
}
describe('StateFilter', () => {
let wrapper;
beforeEach(() => {
wrapper = createComponent();
});
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
const findGlDropdown = () => wrapper.find(GlDropdown);
const findGlDropdownItems = () => findGlDropdown().findAll(GlDropdownItem);
const findDropdownItemsText = () => findGlDropdownItems().wrappers.map(w => w.text());
const firstDropDownItem = () => findGlDropdownItems().at(0);
describe('template', () => {
describe.each`
scope | showStateDropdown
${'issues'} | ${true}
${'projects'} | ${false}
${'milestones'} | ${false}
${'users'} | ${false}
${'merge_requests'} | ${false}
${'notes'} | ${false}
${'wiki_blobs'} | ${false}
${'blobs'} | ${false}
`(`state dropdown`, ({ scope, showStateDropdown }) => {
beforeEach(() => {
wrapper = createComponent({ scope });
});
it(`does${showStateDropdown ? '' : ' not'} render when scope is ${scope}`, () => {
expect(findGlDropdown().exists()).toBe(showStateDropdown);
});
});
describe('Filter options', () => {
it('renders a dropdown item for each filterOption', () => {
expect(findDropdownItemsText()).toStrictEqual(
Object.keys(FILTER_STATES).map(key => {
return FILTER_STATES[key].label;
}),
);
});
it('clicking a dropdown item calls setUrlParams', () => {
const state = FILTER_STATES[Object.keys(FILTER_STATES)[0]].value;
firstDropDownItem().vm.$emit('click');
expect(urlUtils.setUrlParams).toHaveBeenCalledWith({ state });
});
it('clicking a dropdown item calls visitUrl', () => {
firstDropDownItem().vm.$emit('click');
expect(urlUtils.visitUrl).toHaveBeenCalled();
});
});
});
});
...@@ -6,9 +6,22 @@ RSpec.describe Gitlab::GroupSearchResults do ...@@ -6,9 +6,22 @@ RSpec.describe Gitlab::GroupSearchResults do
# group creation calls GroupFinder, so need to create the group # group creation calls GroupFinder, so need to create the group
# before so expect(GroupsFinder) check works # before so expect(GroupsFinder) check works
let_it_be(:group) { create(:group) } let_it_be(:group) { create(:group) }
let(:user) { create(:user) } let_it_be(:user) { create(:user) }
let(:filters) { {} }
let(:limit_projects) { Project.all }
let(:query) { 'gob' }
subject(:results) { described_class.new(user, 'gob', anything, group: group) } subject(:results) { described_class.new(user, query, limit_projects, group: group, filters: filters) }
describe 'issues search' do
let_it_be(:project) { create(:project, :public, group: group) }
let_it_be(:opened_issue) { create(:issue, :opened, project: project, title: 'foo opened') }
let_it_be(:closed_issue) { create(:issue, :closed, project: project, title: 'foo closed') }
let(:query) { 'foo' }
let(:filters) { { state: 'opened' } }
include_examples 'search issues scope filters by state'
end
describe 'user search' do describe 'user search' do
subject(:objects) { results.objects('users') } subject(:objects) { results.objects('users') }
......
...@@ -5,12 +5,13 @@ require 'spec_helper' ...@@ -5,12 +5,13 @@ require 'spec_helper'
RSpec.describe Gitlab::ProjectSearchResults do RSpec.describe Gitlab::ProjectSearchResults do
include SearchHelpers include SearchHelpers
let(:user) { create(:user) } let_it_be(:user) { create(:user) }
let(:project) { create(:project) } let_it_be(:project) { create(:project) }
let(:query) { 'hello world' } let(:query) { 'hello world' }
let(:repository_ref) { nil } let(:repository_ref) { nil }
let(:filters) { {} }
subject(:results) { described_class.new(user, query, project: project, repository_ref: repository_ref) } subject(:results) { described_class.new(user, query, project: project, repository_ref: repository_ref, filters: filters) }
context 'with a repository_ref' do context 'with a repository_ref' do
context 'when empty' do context 'when empty' do
...@@ -258,6 +259,24 @@ RSpec.describe Gitlab::ProjectSearchResults do ...@@ -258,6 +259,24 @@ RSpec.describe Gitlab::ProjectSearchResults do
describe "confidential issues" do describe "confidential issues" do
include_examples "access restricted confidential issues" include_examples "access restricted confidential issues"
end end
context 'filtering' do
let_it_be(:project) { create(:project, :public) }
let_it_be(:closed_issue) { create(:issue, :closed, project: project, title: 'foo closed') }
let_it_be(:opened_issue) { create(:issue, :opened, project: project, title: 'foo opened') }
let(:query) { 'foo' }
include_examples 'search issues scope filters by state'
end
it 'filters issues when state is provided', :aggregate_failures do
closed_issue = create(:issue, :closed, project: project, title: "Revert: #{issue.title}")
results = described_class.new(project.creator, query, project: project, filters: { state: 'opened' })
expect(results.objects('issues')).not_to include closed_issue
expect(results.objects('issues')).to include issue
end
end end
describe 'notes search' do describe 'notes search' do
......
...@@ -6,13 +6,14 @@ RSpec.describe Gitlab::SearchResults do ...@@ -6,13 +6,14 @@ RSpec.describe Gitlab::SearchResults do
include ProjectForksHelper include ProjectForksHelper
include SearchHelpers include SearchHelpers
let(:user) { create(:user) } let_it_be(:user) { create(:user) }
let!(:project) { create(:project, name: 'foo') } let_it_be(:project) { create(:project, name: 'foo') }
let!(:issue) { create(:issue, project: project, title: 'foo') } let_it_be(:issue) { create(:issue, project: project, title: 'foo') }
let!(:merge_request) { create(:merge_request, source_project: project, title: 'foo') } let_it_be(:milestone) { create(:milestone, project: project, title: 'foo') }
let!(:milestone) { create(:milestone, project: project, title: 'foo') } let(:merge_request) { create(:merge_request, source_project: project, title: 'foo') }
let(:filters) { {} }
subject(:results) { described_class.new(user, 'foo', Project.all) } subject(:results) { described_class.new(user, 'foo', Project.all, filters: filters) }
context 'as a user with access' do context 'as a user with access' do
before do before do
...@@ -105,10 +106,10 @@ RSpec.describe Gitlab::SearchResults do ...@@ -105,10 +106,10 @@ RSpec.describe Gitlab::SearchResults do
describe '#limited_issues_count' do describe '#limited_issues_count' do
it 'runs single SQL query to get the limited amount of issues' do it 'runs single SQL query to get the limited amount of issues' do
create(:milestone, project: project, title: 'foo2') create(:issue, project: project, title: 'foo2')
expect(results).to receive(:issues).with(public_only: true).and_call_original expect(results).to receive(:issues).with(public_only: true).and_call_original
expect(results).not_to receive(:issues).with(no_args).and_call_original expect(results).not_to receive(:issues).with(no_args)
expect(results.limited_issues_count).to eq(1) expect(results.limited_issues_count).to eq(1)
end end
...@@ -165,6 +166,13 @@ RSpec.describe Gitlab::SearchResults do ...@@ -165,6 +166,13 @@ RSpec.describe Gitlab::SearchResults do
results.objects('issues') results.objects('issues')
end end
context 'filtering' do
let_it_be(:closed_issue) { create(:issue, :closed, project: project, title: 'foo closed') }
let_it_be(:opened_issue) { create(:issue, :opened, project: project, title: 'foo open') }
include_examples 'search issues scope filters by state'
end
end end
describe '#users' do describe '#users' do
......
# frozen_string_literal: true
RSpec.shared_examples 'search issues scope filters by state' do
context 'state not provided' do
let(:filters) { {} }
it 'returns opened and closed issues', :aggregate_failures do
expect(results.objects('issues')).to include opened_issue
expect(results.objects('issues')).to include closed_issue
end
end
context 'all state' do
let(:filters) { { state: 'all' } }
it 'returns opened and closed issues', :aggregate_failures do
expect(results.objects('issues')).to include opened_issue
expect(results.objects('issues')).to include closed_issue
end
end
context 'closed state' do
let(:filters) { { state: 'closed' } }
it 'returns only closed issues', :aggregate_failures do
expect(results.objects('issues')).not_to include opened_issue
expect(results.objects('issues')).to include closed_issue
end
end
context 'opened state' do
let(:filters) { { state: 'opened' } }
it 'returns only opened issues', :aggregate_failures do
expect(results.objects('issues')).to include opened_issue
expect(results.objects('issues')).not_to include closed_issue
end
end
context 'unsupported state' do
let(:filters) { { state: 'hello' } }
it 'returns only opened issues', :aggregate_failures do
expect(results.objects('issues')).to include opened_issue
expect(results.objects('issues')).to include closed_issue
end
end
end
...@@ -54,6 +54,12 @@ RSpec.describe 'search/_results' do ...@@ -54,6 +54,12 @@ RSpec.describe 'search/_results' do
expect(rendered).to have_selector('[data-track-event=click_text]') expect(rendered).to have_selector('[data-track-event=click_text]')
expect(rendered).to have_selector('[data-track-property=search_result]') expect(rendered).to have_selector('[data-track-property=search_result]')
end end
it 'renders the state filter drop down' do
render
expect(rendered).to have_selector('#js-search-filter-by-state')
end
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