Commit 7e367474 authored by Jose Ivan Vargas's avatar Jose Ivan Vargas

Merge branch '237938-merge-requests-filter-by-state' into 'master'

Search UI - Implement Merge Request scope results filter by state

See merge request gitlab-org/gitlab!41282
parents 0f206d83 6785cdb4
<script> <script>
import { GlDropdown, GlDropdownItem, GlDropdownDivider } from '@gitlab/ui'; import { GlDropdown, GlDropdownItem, GlDropdownDivider } from '@gitlab/ui';
import { FILTER_STATES, FILTER_HEADER, FILTER_TEXT } from '../constants'; import {
FILTER_STATES,
SCOPES,
FILTER_STATES_BY_SCOPE,
FILTER_HEADER,
FILTER_TEXT,
} from '../constants';
import { setUrlParams, visitUrl } from '~/lib/utils/url_utility'; import { setUrlParams, visitUrl } from '~/lib/utils/url_utility';
const FILTERS_ARRAY = Object.values(FILTER_STATES); const FILTERS_ARRAY = Object.values(FILTER_STATES);
...@@ -26,13 +32,15 @@ export default { ...@@ -26,13 +32,15 @@ export default {
}, },
computed: { computed: {
selectedFilterText() { selectedFilterText() {
let filterText = FILTER_TEXT; const filter = FILTERS_ARRAY.find(({ value }) => value === this.selectedFilter);
if (this.selectedFilter === FILTER_STATES.CLOSED.value) { if (!filter || filter === FILTER_STATES.ANY) {
filterText = FILTER_STATES.CLOSED.label; return FILTER_TEXT;
} else if (this.selectedFilter === FILTER_STATES.OPEN.value) {
filterText = FILTER_STATES.OPEN.label;
} }
return filterText;
return filter.label;
},
showDropdown() {
return Object.values(SCOPES).includes(this.scope);
}, },
selectedFilter: { selectedFilter: {
get() { get() {
...@@ -63,29 +71,24 @@ export default { ...@@ -63,29 +71,24 @@ export default {
}, },
filterStates: FILTER_STATES, filterStates: FILTER_STATES,
filterHeader: FILTER_HEADER, filterHeader: FILTER_HEADER,
filtersArray: FILTERS_ARRAY, filtersByScope: FILTER_STATES_BY_SCOPE,
}; };
</script> </script>
<template> <template>
<gl-dropdown <gl-dropdown v-if="showDropdown" :text="selectedFilterText" class="col-sm-3 gl-pt-4 gl-pl-0">
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"> <header class="gl-text-center gl-font-weight-bold gl-font-lg">
{{ $options.filterHeader }} {{ $options.filterHeader }}
</header> </header>
<gl-dropdown-divider /> <gl-dropdown-divider />
<gl-dropdown-item <gl-dropdown-item
v-for="filter in $options.filtersArray" v-for="filter in $options.filtersByScope[scope]"
:key="filter.value" :key="filter.value"
:is-check-item="true" :is-check-item="true"
:is-checked="isFilterSelected(filter.value)" :is-checked="isFilterSelected(filter.value)"
:class="dropDownItemClass(filter)" :class="dropDownItemClass(filter)"
@click="handleFilterChange(filter.value)" @click="handleFilterChange(filter.value)"
>{{ filter.label }}</gl-dropdown-item
> >
{{ filter.label }}
</gl-dropdown-item>
</gl-dropdown> </gl-dropdown>
</template> </template>
...@@ -17,4 +17,23 @@ export const FILTER_STATES = { ...@@ -17,4 +17,23 @@ export const FILTER_STATES = {
label: __('Closed'), label: __('Closed'),
value: 'closed', value: 'closed',
}, },
MERGED: {
label: __('Merged'),
value: 'merged',
},
};
export const SCOPES = {
ISSUES: 'issues',
MERGE_REQUESTS: 'merge_requests',
};
export const FILTER_STATES_BY_SCOPE = {
[SCOPES.ISSUES]: [FILTER_STATES.ANY, FILTER_STATES.OPEN, FILTER_STATES.CLOSED],
[SCOPES.MERGE_REQUESTS]: [
FILTER_STATES.ANY,
FILTER_STATES.OPEN,
FILTER_STATES.MERGED,
FILTER_STATES.CLOSED,
],
}; };
---
title: Search UI - Implement Merge Request scope results filter by state
merge_request: 41282
author:
type: added
...@@ -3,6 +3,8 @@ ...@@ -3,6 +3,8 @@
module Elastic module Elastic
module Latest module Latest
class IssueClassProxy < ApplicationClassProxy class IssueClassProxy < ApplicationClassProxy
include StateFilter
def elastic_search(query, options: {}) def elastic_search(query, options: {})
query_hash = query_hash =
if query =~ /#(\d+)\z/ if query =~ /#(\d+)\z/
...@@ -28,18 +30,6 @@ module Elastic ...@@ -28,18 +30,6 @@ module Elastic
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]
......
...@@ -3,6 +3,8 @@ ...@@ -3,6 +3,8 @@
module Elastic module Elastic
module Latest module Latest
class MergeRequestClassProxy < ApplicationClassProxy class MergeRequestClassProxy < ApplicationClassProxy
include StateFilter
def elastic_search(query, options: {}) def elastic_search(query, options: {})
query_hash = query_hash =
if query =~ /\!(\d+)\z/ if query =~ /\!(\d+)\z/
...@@ -20,6 +22,7 @@ module Elastic ...@@ -20,6 +22,7 @@ module Elastic
options[:features] = 'merge_requests' options[:features] = 'merge_requests'
query_hash = project_ids_filter(query_hash, options) query_hash = project_ids_filter(query_hash, options)
query_hash = state_filter(query_hash, options)
search(query_hash, options) search(query_hash, options)
end end
......
# frozen_string_literal: true
module Elastic
module Latest
module StateFilter
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 merged).include?(state)
filter = { match: { state: state } }
query_hash[:query][:bool][:filter] << filter
query_hash
end
end
end
end
...@@ -244,7 +244,10 @@ module Gitlab ...@@ -244,7 +244,10 @@ module Gitlab
def merge_requests def merge_requests
strong_memoize(:merge_requests) do strong_memoize(:merge_requests) do
MergeRequest.elastic_search(query, options: base_options) options = base_options
options[:state] = filters[:state] if filters.key?(:state)
MergeRequest.elastic_search(query, options: options)
end end
end end
......
...@@ -17,11 +17,28 @@ RSpec.describe Gitlab::Elastic::GroupSearchResults, :elastic do ...@@ -17,11 +17,28 @@ RSpec.describe Gitlab::Elastic::GroupSearchResults, :elastic do
context 'issues search', :sidekiq_inline do context 'issues search', :sidekiq_inline do
let!(:project) { create(:project, :public, group: group) } let!(:project) { create(:project, :public, group: group) }
let!(:closed_issue) { create(:issue, :closed, project: project, title: 'foo closed') } let!(:closed_result) { create(:issue, :closed, project: project, title: 'foo closed') }
let!(:opened_issue) { create(:issue, :opened, project: project, title: 'foo opened') } let!(:opened_result) { create(:issue, :opened, project: project, title: 'foo opened') }
let(:query) { 'foo' }
let(:scope) { 'issues' }
include_examples 'search results filtered by state' do
before do
ensure_elasticsearch_index!
end
end
end
context 'merge_requests search', :sidekiq_inline do
let!(:project) { create(:project, :public, group: group) }
let!(:opened_result) { create(:merge_request, :opened, source_project: project, title: 'foo opened') }
let!(:closed_result) { create(:merge_request, :closed, source_project: project, title: 'foo closed') }
let(:query) { 'foo' } let(:query) { 'foo' }
let(:scope) { 'merge_requests' }
include_examples 'search issues scope filters by state' do include_examples 'search results filtered by state' do
before do before do
ensure_elasticsearch_index! ensure_elasticsearch_index!
end end
......
...@@ -70,14 +70,31 @@ RSpec.describe Gitlab::Elastic::ProjectSearchResults, :elastic do ...@@ -70,14 +70,31 @@ RSpec.describe Gitlab::Elastic::ProjectSearchResults, :elastic do
end end
context 'filtering' do context 'filtering' do
include_examples 'search issues scope filters by state' do context 'issues' do
let!(:project) { create(:project, :public) } let!(:project) { create(:project, :public) }
let!(:closed_issue) { create(:issue, :closed, project: project, title: 'foo closed') } let!(:closed_result) { create(:issue, :closed, project: project, title: 'foo closed') }
let!(:opened_issue) { create(:issue, :opened, project: project, title: 'foo opened') } let!(:opened_result) { create(:issue, :opened, project: project, title: 'foo opened') }
let(:query) { 'foo' } let(:query) { 'foo' }
let(:scope) { 'issues' }
before do include_examples 'search results filtered by state' do
ensure_elasticsearch_index! before do
ensure_elasticsearch_index!
end
end
end
context 'merge_requests' do
let!(:project) { create(:project, :public) }
let!(:opened_result) { create(:merge_request, :opened, source_project: project, title: 'foo opened') }
let!(:closed_result) { create(:merge_request, :closed, source_project: project, title: 'foo closed') }
let(:query) { 'foo' }
let(:scope) { 'merge_requests' }
include_examples 'search results filtered by state' do
before do
ensure_elasticsearch_index!
end
end end
end end
end end
......
...@@ -174,12 +174,13 @@ RSpec.describe Gitlab::Elastic::SearchResults, :elastic, :sidekiq_might_not_need ...@@ -174,12 +174,13 @@ RSpec.describe Gitlab::Elastic::SearchResults, :elastic, :sidekiq_might_not_need
context 'filtering' do context 'filtering' do
let!(:project) { create(:project, :public) } let!(:project) { create(:project, :public) }
let!(:closed_issue) { create(:issue, :closed, project: project, title: 'foo closed') } let!(:closed_result) { create(:issue, :closed, project: project, title: 'foo closed') }
let!(:opened_issue) { create(:issue, :opened, project: project, title: 'foo opened') } let!(:opened_result) { create(:issue, :opened, project: project, title: 'foo opened') }
let(:scope) { 'issues' }
let(:results) { described_class.new(user, 'foo', [project], filters: filters) } let(:results) { described_class.new(user, 'foo', [project], filters: filters) }
include_examples 'search issues scope filters by state' do include_examples 'search results filtered by state' do
before do before do
ensure_elasticsearch_index! ensure_elasticsearch_index!
end end
...@@ -493,6 +494,21 @@ RSpec.describe Gitlab::Elastic::SearchResults, :elastic, :sidekiq_might_not_need ...@@ -493,6 +494,21 @@ RSpec.describe Gitlab::Elastic::SearchResults, :elastic, :sidekiq_might_not_need
expect(results.objects('merge_requests')).to be_empty expect(results.objects('merge_requests')).to be_empty
expect(results.merge_requests_count).to eq 0 expect(results.merge_requests_count).to eq 0
end end
context 'filtering' do
let!(:project) { create(:project, :public) }
let!(:opened_result) { create(:merge_request, :opened, source_project: project, title: 'foo opened') }
let!(:closed_result) { create(:merge_request, :closed, source_project: project, title: 'foo closed') }
let(:scope) { 'merge_requests' }
let(:results) { described_class.new(user, 'foo', [project], filters: filters) }
include_examples 'search results filtered by state' do
before do
ensure_elasticsearch_index!
end
end
end
end end
describe 'project scoping' do describe 'project scoping' do
......
import { shallowMount } from '@vue/test-utils'; import { shallowMount } from '@vue/test-utils';
import { GlDropdown, GlDropdownItem } from '@gitlab/ui'; import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
import StateFilter from '~/search/state_filter/components/state_filter.vue'; import StateFilter from '~/search/state_filter/components/state_filter.vue';
import { FILTER_STATES } from '~/search/state_filter/constants'; import {
FILTER_STATES,
SCOPES,
FILTER_STATES_BY_SCOPE,
FILTER_TEXT,
} from '~/search/state_filter/constants';
import * as urlUtils from '~/lib/utils/url_utility'; import * as urlUtils from '~/lib/utils/url_utility';
jest.mock('~/lib/utils/url_utility', () => ({ jest.mock('~/lib/utils/url_utility', () => ({
...@@ -38,10 +43,10 @@ describe('StateFilter', () => { ...@@ -38,10 +43,10 @@ describe('StateFilter', () => {
describe.each` describe.each`
scope | showStateDropdown scope | showStateDropdown
${'issues'} | ${true} ${'issues'} | ${true}
${'merge_requests'} | ${true}
${'projects'} | ${false} ${'projects'} | ${false}
${'milestones'} | ${false} ${'milestones'} | ${false}
${'users'} | ${false} ${'users'} | ${false}
${'merge_requests'} | ${false}
${'notes'} | ${false} ${'notes'} | ${false}
${'wiki_blobs'} | ${false} ${'wiki_blobs'} | ${false}
${'blobs'} | ${false} ${'blobs'} | ${false}
...@@ -55,11 +60,29 @@ describe('StateFilter', () => { ...@@ -55,11 +60,29 @@ describe('StateFilter', () => {
}); });
}); });
describe.each`
state | label
${FILTER_STATES.ANY.value} | ${FILTER_TEXT}
${FILTER_STATES.OPEN.value} | ${FILTER_STATES.OPEN.label}
${FILTER_STATES.CLOSED.value} | ${FILTER_STATES.CLOSED.label}
${FILTER_STATES.MERGED.value} | ${FILTER_STATES.MERGED.label}
`(`filter text`, ({ state, label }) => {
describe(`when state is ${state}`, () => {
beforeEach(() => {
wrapper = createComponent({ scope: 'issues', state });
});
it(`sets dropdown label to ${label}`, () => {
expect(findGlDropdown().attributes('text')).toBe(label);
});
});
});
describe('Filter options', () => { describe('Filter options', () => {
it('renders a dropdown item for each filterOption', () => { it('renders a dropdown item for each filterOption', () => {
expect(findDropdownItemsText()).toStrictEqual( expect(findDropdownItemsText()).toStrictEqual(
Object.keys(FILTER_STATES).map(key => { FILTER_STATES_BY_SCOPE[SCOPES.ISSUES].map(v => {
return FILTER_STATES[key].label; return v.label;
}), }),
); );
}); });
......
...@@ -7,6 +7,7 @@ RSpec.describe Gitlab::GroupSearchResults do ...@@ -7,6 +7,7 @@ RSpec.describe Gitlab::GroupSearchResults do
# 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_it_be(:user) { create(:user) } let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project, :public, group: group) }
let(:filters) { {} } let(:filters) { {} }
let(:limit_projects) { Project.all } let(:limit_projects) { Project.all }
let(:query) { 'gob' } let(:query) { 'gob' }
...@@ -14,13 +15,27 @@ RSpec.describe Gitlab::GroupSearchResults do ...@@ -14,13 +15,27 @@ RSpec.describe Gitlab::GroupSearchResults do
subject(:results) { described_class.new(user, query, limit_projects, group: group, filters: filters) } subject(:results) { described_class.new(user, query, limit_projects, group: group, filters: filters) }
describe 'issues search' do describe 'issues search' do
let_it_be(:project) { create(:project, :public, group: group) } let_it_be(:opened_result) { create(:issue, :opened, project: project, title: 'foo opened') }
let_it_be(:opened_issue) { create(:issue, :opened, project: project, title: 'foo opened') } let_it_be(:closed_result) { create(:issue, :closed, project: project, title: 'foo closed') }
let_it_be(:closed_issue) { create(:issue, :closed, project: project, title: 'foo closed') }
let(:query) { 'foo' } let(:query) { 'foo' }
let(:filters) { { state: 'opened' } } let(:scope) { 'issues' }
include_examples 'search issues scope filters by state' include_examples 'search results filtered by state'
end
describe 'merge_requests search' do
let(:opened_result) { create(:merge_request, :opened, source_project: project, title: 'foo opened') }
let(:closed_result) { create(:merge_request, :closed, source_project: project, title: 'foo closed') }
let(:query) { 'foo' }
let(:scope) { 'merge_requests' }
before do
# we're creating those instances in before block because otherwise factory for MRs will fail on after(:build)
opened_result
closed_result
end
include_examples 'search results filtered by state'
end end
describe 'user search' do describe 'user search' do
......
...@@ -249,8 +249,9 @@ RSpec.describe Gitlab::ProjectSearchResults do ...@@ -249,8 +249,9 @@ RSpec.describe Gitlab::ProjectSearchResults do
describe 'issues search' do describe 'issues search' do
let(:issue) { create(:issue, project: project) } let(:issue) { create(:issue, project: project) }
let(:query) { issue.title } let(:query) { issue.title }
let(:scope) { 'issues' }
subject(:objects) { results.objects('issues') } subject(:objects) { results.objects(scope) }
it 'does not list issues on private projects' do it 'does not list issues on private projects' do
expect(objects).not_to include issue expect(objects).not_to include issue
...@@ -262,20 +263,25 @@ RSpec.describe Gitlab::ProjectSearchResults do ...@@ -262,20 +263,25 @@ RSpec.describe Gitlab::ProjectSearchResults do
context 'filtering' do context 'filtering' do
let_it_be(:project) { create(:project, :public) } let_it_be(:project) { create(:project, :public) }
let_it_be(:closed_issue) { create(:issue, :closed, project: project, title: 'foo closed') } let_it_be(:closed_result) { create(:issue, :closed, project: project, title: 'foo closed') }
let_it_be(:opened_issue) { create(:issue, :opened, project: project, title: 'foo opened') } let_it_be(:opened_result) { create(:issue, :opened, project: project, title: 'foo opened') }
let(:query) { 'foo' } let(:query) { 'foo' }
include_examples 'search issues scope filters by state' include_examples 'search results filtered by state'
end end
end
it 'filters issues when state is provided', :aggregate_failures do describe 'merge requests search' do
closed_issue = create(:issue, :closed, project: project, title: "Revert: #{issue.title}") let(:scope) { 'merge_requests' }
let(:project) { create(:project, :public) }
results = described_class.new(project.creator, query, project: project, filters: { state: 'opened' }) context 'filtering' do
let!(:project) { create(:project, :public) }
let!(:opened_result) { create(:merge_request, :opened, source_project: project, title: 'foo opened') }
let!(:closed_result) { create(:merge_request, :closed, source_project: project, title: 'foo closed') }
let(:query) { 'foo' }
expect(results.objects('issues')).not_to include closed_issue include_examples 'search results filtered by state'
expect(results.objects('issues')).to include issue
end end
end end
......
...@@ -150,6 +150,15 @@ RSpec.describe Gitlab::SearchResults do ...@@ -150,6 +150,15 @@ RSpec.describe Gitlab::SearchResults do
results.objects('merge_requests') results.objects('merge_requests')
end end
context 'filtering' do
let!(:opened_result) { create(:merge_request, :opened, source_project: project, title: 'foo opened') }
let!(:closed_result) { create(:merge_request, :closed, source_project: project, title: 'foo closed') }
let(:scope) { 'merge_requests' }
let(:query) { 'foo' }
include_examples 'search results filtered by state'
end
end end
describe '#issues' do describe '#issues' do
...@@ -168,10 +177,12 @@ RSpec.describe Gitlab::SearchResults do ...@@ -168,10 +177,12 @@ RSpec.describe Gitlab::SearchResults do
end end
context 'filtering' do context 'filtering' do
let_it_be(:closed_issue) { create(:issue, :closed, project: project, title: 'foo closed') } let(:scope) { 'issues' }
let_it_be(:opened_issue) { create(:issue, :opened, project: project, title: 'foo open') }
let_it_be(:closed_result) { create(:issue, :closed, project: project, title: 'foo closed') }
let_it_be(:opened_result) { create(:issue, :opened, project: project, title: 'foo open') }
include_examples 'search issues scope filters by state' include_examples 'search results filtered by state'
end end
end end
......
# frozen_string_literal: true # frozen_string_literal: true
RSpec.shared_examples 'search issues scope filters by state' do RSpec.shared_examples 'search results filtered by state' do
context 'state not provided' do context 'state not provided' do
let(:filters) { {} } let(:filters) { {} }
it 'returns opened and closed issues', :aggregate_failures do it 'returns opened and closed results', :aggregate_failures do
expect(results.objects('issues')).to include opened_issue expect(results.objects(scope)).to include opened_result
expect(results.objects('issues')).to include closed_issue expect(results.objects(scope)).to include closed_result
end end
end end
context 'all state' do context 'all state' do
let(:filters) { { state: 'all' } } let(:filters) { { state: 'all' } }
it 'returns opened and closed issues', :aggregate_failures do it 'returns opened and closed results', :aggregate_failures do
expect(results.objects('issues')).to include opened_issue expect(results.objects(scope)).to include opened_result
expect(results.objects('issues')).to include closed_issue expect(results.objects(scope)).to include closed_result
end end
end end
context 'closed state' do context 'closed state' do
let(:filters) { { state: 'closed' } } let(:filters) { { state: 'closed' } }
it 'returns only closed issues', :aggregate_failures do it 'returns only closed results', :aggregate_failures do
expect(results.objects('issues')).not_to include opened_issue expect(results.objects(scope)).not_to include opened_result
expect(results.objects('issues')).to include closed_issue expect(results.objects(scope)).to include closed_result
end end
end end
context 'opened state' do context 'opened state' do
let(:filters) { { state: 'opened' } } let(:filters) { { state: 'opened' } }
it 'returns only opened issues', :aggregate_failures do it 'returns only opened results', :aggregate_failures do
expect(results.objects('issues')).to include opened_issue expect(results.objects(scope)).to include opened_result
expect(results.objects('issues')).not_to include closed_issue expect(results.objects(scope)).not_to include closed_result
end end
end end
context 'unsupported state' do context 'unsupported state' do
let(:filters) { { state: 'hello' } } let(:filters) { { state: 'hello' } }
it 'returns only opened issues', :aggregate_failures do it 'returns only opened results', :aggregate_failures do
expect(results.objects('issues')).to include opened_issue expect(results.objects(scope)).to include opened_result
expect(results.objects('issues')).to include closed_issue expect(results.objects(scope)).to include closed_result
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