Commit c126034b authored by Paul Slaughter's avatar Paul Slaughter

Merge branch 'revert-aee3acc2' into 'master'

Revert "Convert Search Scope Tabs to Vue Component"

See merge request gitlab-org/gitlab!54041
parents 073c7cf9 8b17d718
import axios from '~/lib/utils/axios_utils';
function showCount(el, count) {
el.textContent = count;
el.classList.remove('hidden');
}
function refreshCount(el) {
const { url } = el.dataset;
return axios
.get(url)
.then(({ data }) => showCount(el, data.count))
.catch((e) => {
// eslint-disable-next-line no-console
console.error(`Failed to fetch search count from '${url}'.`, e);
});
}
export default function refreshCounts() {
const elements = Array.from(document.querySelectorAll('.js-search-count'));
return Promise.all(elements.map(refreshCount));
}
import setHighlightClass from 'ee_else_ce/search/highlight_blob_search_result';
import Project from '~/pages/projects/project';
import refreshCounts from '~/pages/search/show/refresh_counts';
import { queryToObject } from '~/lib/utils/url_utility';
import createStore from './store';
import { initTopbar } from './topbar';
......@@ -19,5 +20,6 @@ export const initSearchApp = () => {
initSearchSort(store);
setHighlightClass(query.search); // Code Highlighting
refreshCounts(); // Other Scope Tab Counts
Project.initRefSwitcher(); // Code Search Branch Picker
};
import axios from '~/lib/utils/axios_utils';
import Api from '~/api';
import createFlash from '~/flash';
import { __ } from '~/locale';
import { visitUrl, setUrlParams } from '~/lib/utils/url_utility';
import * as types from './mutation_types';
/* private */
const getCount = ({ params, state, activeCount }) => {
const globalSearchCountsPath = '/search/count';
const url = Api.buildUrl(globalSearchCountsPath);
// count is known for active tab, so return it and skip the Api call
if (params.scope === state.query?.scope) {
return { scope: params.scope, count: activeCount };
}
return axios
.get(url, { params })
.then(({ data }) => {
return { scope: params.scope, count: data.count };
})
.catch((e) => {
throw e;
});
};
export const fetchGroups = ({ commit }, search) => {
commit(types.REQUEST_GROUPS);
Api.groups(search)
......@@ -59,21 +38,6 @@ export const fetchProjects = ({ commit, state }, search) => {
}
};
export const fetchSearchCounts = ({ commit, state }, { scopeTabs, activeCount }) => {
commit(types.REQUEST_SEARCH_COUNTS, { scopeTabs, activeCount });
const promises = scopeTabs.map((scope) =>
getCount({ params: { ...state.query, scope }, state, activeCount }),
);
Promise.all(promises)
.then((data) => {
commit(types.RECEIVE_SEARCH_COUNTS_SUCCESS, data);
})
.catch(() => {
createFlash({ message: __('There was an error fetching the Search Counts') });
});
};
export const setQuery = ({ commit }, { key, value }) => {
commit(types.SET_QUERY, { key, value });
};
......@@ -82,22 +46,6 @@ export const applyQuery = ({ state }) => {
visitUrl(setUrlParams({ ...state.query, page: null }));
};
export const resetQuery = ({ state }, snippets = false) => {
let defaultQuery = {
page: null,
state: null,
confidential: null,
nav_source: null,
};
if (snippets) {
defaultQuery = {
snippets: true,
group_id: null,
project_id: null,
...defaultQuery,
};
}
visitUrl(setUrlParams({ ...state.query, ...defaultQuery }));
export const resetQuery = ({ state }) => {
visitUrl(setUrlParams({ ...state.query, page: null, state: null, confidential: null }));
};
......@@ -6,7 +6,4 @@ export const REQUEST_PROJECTS = 'REQUEST_PROJECTS';
export const RECEIVE_PROJECTS_SUCCESS = 'RECEIVE_PROJECTS_SUCCESS';
export const RECEIVE_PROJECTS_ERROR = 'RECEIVE_PROJECTS_ERROR';
export const REQUEST_SEARCH_COUNTS = 'REQUEST_SEARCH_COUNTS';
export const RECEIVE_SEARCH_COUNTS_SUCCESS = 'RECEIVE_SEARCH_COUNTS_SUCCESS';
export const SET_QUERY = 'SET_QUERY';
import { ALL_SCOPE_TABS } from '~/search/topbar/constants';
import * as types from './mutation_types';
export default {
......@@ -24,16 +23,6 @@ export default {
state.fetchingProjects = false;
state.projects = [];
},
[types.REQUEST_SEARCH_COUNTS](state, { scopeTabs, activeCount }) {
state.inflatedScopeTabs = scopeTabs.map((tab) => {
return { ...ALL_SCOPE_TABS[tab], count: tab === state.query?.scope ? activeCount : '' };
});
},
[types.RECEIVE_SEARCH_COUNTS_SUCCESS](state, data) {
state.inflatedScopeTabs = data.map((tab) => {
return { ...ALL_SCOPE_TABS[tab.scope], count: tab.count };
});
},
[types.SET_QUERY](state, { key, value }) {
state.query[key] = value;
},
......
......@@ -4,6 +4,5 @@ const createState = ({ query }) => ({
fetchingGroups: false,
projects: [],
fetchingProjects: false,
inflatedScopeTabs: [],
});
export default createState;
......@@ -3,7 +3,6 @@ import { mapState, mapActions } from 'vuex';
import { GlForm, GlSearchBoxByType, GlButton } from '@gitlab/ui';
import GroupFilter from './group_filter.vue';
import ProjectFilter from './project_filter.vue';
import ScopeTabs from './scope_tabs.vue';
export default {
name: 'GlobalSearchTopbar',
......@@ -13,7 +12,6 @@ export default {
GroupFilter,
ProjectFilter,
GlButton,
ScopeTabs,
},
props: {
groupInitialData: {
......@@ -26,16 +24,6 @@ export default {
required: false,
default: () => ({}),
},
scopeTabs: {
type: Array,
required: false,
default: () => [],
},
count: {
type: String,
required: false,
default: '',
},
},
computed: {
...mapState(['query']),
......@@ -50,9 +38,6 @@ export default {
showFilters() {
return !this.query.snippets || this.query.snippets === 'false';
},
showScopeTabs() {
return this.query.search;
},
},
methods: {
...mapActions(['applyQuery', 'setQuery']),
......@@ -61,7 +46,6 @@ export default {
</script>
<template>
<section>
<gl-form class="search-page-form" @submit.prevent="applyQuery">
<section class="gl-lg-display-flex gl-align-items-flex-end">
<div class="gl-flex-fill-1 gl-mb-4 gl-lg-mb-0 gl-lg-mr-2">
......@@ -86,6 +70,4 @@ export default {
}}</gl-button>
</section>
</gl-form>
<scope-tabs v-if="showScopeTabs" :scope-tabs="scopeTabs" :count="count" />
</section>
</template>
<script>
import { GlTabs, GlTab, GlBadge } from '@gitlab/ui';
import { mapState, mapActions } from 'vuex';
export default {
name: 'ScopeTabs',
components: {
GlTabs,
GlTab,
GlBadge,
},
props: {
scopeTabs: {
type: Array,
required: true,
},
count: {
type: String,
required: false,
default: '',
},
},
computed: {
...mapState(['query', 'inflatedScopeTabs']),
},
created() {
this.fetchSearchCounts({ scopeTabs: this.scopeTabs, activeCount: this.count });
},
methods: {
...mapActions(['fetchSearchCounts', 'setQuery', 'resetQuery']),
handleTabChange(scope) {
this.setQuery({ key: 'scope', value: scope });
this.resetQuery(scope === 'snippet_titles');
},
isTabActive(scope) {
return scope === this.query.scope;
},
},
};
</script>
<template>
<div>
<gl-tabs
content-class="gl-p-0"
nav-class="search-filter search-nav-tabs gl-display-flex gl-overflow-x-auto"
>
<gl-tab
v-for="tab in inflatedScopeTabs"
:key="tab.scope"
class="gl-display-flex"
:active="isTabActive(tab.scope)"
:data-testid="`tab-${tab.scope}`"
:title-link-attributes="{ 'data-qa-selector': tab.qaSelector }"
title-link-class="gl-white-space-nowrap"
@click="handleTabChange(tab.scope)"
>
<template #title>
<span data-testid="tab-title"> {{ tab.title }} </span>
<gl-badge
v-show="tab.count"
:data-scope="tab.scope"
:data-testid="`badge-${tab.scope}`"
:variant="isTabActive(tab.scope) ? 'neutral' : 'muted'"
size="sm"
>
{{ tab.count }}
</gl-badge>
</template>
</gl-tab>
</gl-tabs>
</div>
</template>
......@@ -19,17 +19,3 @@ export const PROJECT_DATA = {
selectedDisplayValue: 'name_with_namespace',
itemsDisplayValue: 'name_with_namespace',
};
export const ALL_SCOPE_TABS = {
blobs: { scope: 'blobs', title: __('Code'), qaSelector: 'code_tab' },
issues: { scope: 'issues', title: __('Issues') },
merge_requests: { scope: 'merge_requests', title: __('Merge requests') },
milestones: { scope: 'milestones', title: __('Milestones') },
notes: { scope: 'notes', title: __('Comments') },
wiki_blobs: { scope: 'wiki_blobs', title: __('Wiki') },
commits: { scope: 'commits', title: __('Commits') },
epics: { scope: 'epics', title: __('Epics') },
users: { scope: 'users', title: __('Users') },
snippet_titles: { scope: 'snippet_titles', title: __('Titles and Descriptions') },
projects: { scope: 'projects', title: __('Projects'), qaSelector: 'projects_tab' },
};
......@@ -11,12 +11,10 @@ export const initTopbar = (store) => {
return false;
}
let { groupInitialData, projectInitialData, scopeTabs } = el.dataset;
const { count } = el.dataset;
let { groupInitialData, projectInitialData } = el.dataset;
groupInitialData = JSON.parse(groupInitialData);
projectInitialData = JSON.parse(projectInitialData);
scopeTabs = JSON.parse(scopeTabs);
return new Vue({
el,
......@@ -26,8 +24,6 @@ export const initTopbar = (store) => {
props: {
groupInitialData,
projectInitialData,
scopeTabs,
count,
},
});
},
......
......@@ -2,7 +2,6 @@ $search-dropdown-max-height: 400px;
$search-avatar-size: 16px;
$search-sidebar-min-width: 240px;
$search-sidebar-max-width: 300px;
$search-topbar-min-height: 111px;
.search-results {
.search-result-row {
......@@ -20,12 +19,6 @@ $search-topbar-min-height: 111px;
}
}
.search-topbar {
@include media-breakpoint-up(md) {
min-height: $search-topbar-min-height;
}
}
.search-sidebar {
@include media-breakpoint-up(md) {
min-width: $search-sidebar-min-width;
......@@ -33,11 +26,6 @@ $search-topbar-min-height: 111px;
}
}
.search-nav-tabs {
overflow-y: hidden;
flex-wrap: nowrap;
}
.search form:hover,
.file-finder-input:hover,
.issuable-search-form:hover,
......
......@@ -511,8 +511,7 @@ module ProjectsHelper
commits: :download_code,
merge_requests: :read_merge_request,
notes: [:read_merge_request, :download_code, :read_issue, :read_snippet],
members: :read_project_member,
wiki_blobs: :read_wiki
members: :read_project_member
)
end
......
# frozen_string_literal: true
module SearchHelper
PROJECT_SEARCH_TABS = %i{blobs issues merge_requests milestones notes wiki_blobs commits}.freeze
BASIC_SEARCH_TABS = %i{projects issues merge_requests milestones}.freeze
SEARCH_GENERIC_PARAMS = [
:search,
:scope,
:project_id,
:group_id,
:repository_ref,
:snippets,
:sort,
:force_search_results
].freeze
def search_autocomplete_opts(term)
return unless current_user
......@@ -284,19 +292,27 @@ module SearchHelper
Sanitize.clean(str)
end
def search_nav_tabs
return [:snippet_titles] if !@project && @show_snippets
def search_filter_link(scope, label, data: {}, search: {})
search_params = params
.merge(search)
.merge({ scope: scope })
.permit(SEARCH_GENERIC_PARAMS)
tabs =
if @project
PROJECT_SEARCH_TABS.select { |tab| project_search_tabs?(tab) }
if @scope == scope
li_class = 'active'
count = @search_results.formatted_count(scope)
else
BASIC_SEARCH_TABS.dup
badge_class = 'js-search-count hidden'
badge_data = { url: search_count_path(search_params) }
end
tabs << :users if show_user_search_tab?
tabs
content_tag :li, class: li_class, data: data do
link_to search_path(search_params) do
concat label
concat ' '
concat content_tag(:span, count, class: ['badge badge-pill', badge_class], data: badge_data)
end
end
end
def search_filter_input_options(type, placeholder = _('Search or filter results...'))
......
- users = capture_haml do
- if show_user_search_tab?
= search_filter_link 'users', _("Users")
.scrolling-tabs-container.inner-page-scroll-tabs.is-smaller
.fade-left= sprite_icon('chevron-lg-left', size: 12)
.fade-right= sprite_icon('chevron-lg-right', size: 12)
%ul.nav-links.search-filter.scrolling-tabs.nav.nav-tabs
- if @project
- if project_search_tabs?(:blobs)
= search_filter_link 'blobs', _("Code"), data: { qa_selector: 'code_tab' }
- if project_search_tabs?(:issues)
= search_filter_link 'issues', _("Issues")
- if project_search_tabs?(:merge_requests)
= search_filter_link 'merge_requests', _("Merge requests")
- if project_search_tabs?(:milestones)
= search_filter_link 'milestones', _("Milestones")
- if project_search_tabs?(:notes)
= search_filter_link 'notes', _("Comments")
- if project_search_tabs?(:wiki)
= search_filter_link 'wiki_blobs', _("Wiki")
- if project_search_tabs?(:commits)
= search_filter_link 'commits', _("Commits")
= users
- elsif @show_snippets
= search_filter_link 'snippet_titles', _("Titles and Descriptions"), search: { snippets: true, group_id: nil, project_id: nil }
- else
= search_filter_link 'projects', _("Projects"), data: { qa_selector: 'projects_tab' }
= search_filter_link 'issues', _("Issues")
= search_filter_link 'merge_requests', _("Merge requests")
= search_filter_link 'milestones', _("Milestones")
= render_if_exists 'search/epics_filter_link'
= render_if_exists 'search/category_elasticsearch'
= users
......@@ -16,6 +16,7 @@
= render_if_exists 'search/form_elasticsearch', attrs: { class: 'mb-2 mb-sm-0 align-self-center' }
.gl-mt-3
#js-search-topbar.search-topbar{ data: { "group-initial-data": @group.to_json, "project-initial-data": project_attributes.to_json, "scope-tabs": search_nav_tabs.to_json, count: @search_results&.formatted_count(@scope) } }
#js-search-topbar{ data: { "group-initial-data": @group.to_json, "project-initial-data": project_attributes.to_json } }
- if @search_term
= render 'search/category'
= render 'search/results'
---
title: Change search tab to Vue component
merge_request: 52018
author:
type: changed
......@@ -3,8 +3,8 @@ module EE
module SearchHelper
extend ::Gitlab::Utils::Override
SWITCH_TO_BASIC_SEARCHABLE_TABS = %w[projects issues merge_requests milestones users epics].freeze
PLACEHOLDER = '_PLACEHOLDER_'
ADVANCED_SEARCH_TABS = %i{notes blobs commits wiki_blobs}.freeze
override :search_filter_input_options
def search_filter_input_options(type, placeholder = _('Search or filter results...'))
......@@ -130,20 +130,6 @@ module EE
options + super
end
override :search_nav_tabs
def search_nav_tabs
return super if @project || @show_snippets
tabs = []
tabs << :epics if search_service.show_epics?
tabs.push(*ADVANCED_SEARCH_TABS) if search_service.use_elasticsearch?
super_tabs = super
users_index = super_tabs.index(:users) || -1
super_tabs.insert(users_index, *tabs)
end
private
def recent_epics_autocomplete(term)
......
- if search_service.use_elasticsearch?
= search_filter_link 'notes', _("Comments")
= search_filter_link 'blobs', _("Code"), data: { qa_selector: 'code_tab' }
= search_filter_link 'commits', _("Commits")
= search_filter_link 'wiki_blobs', _("Wiki")
- if search_service.show_epics?
= search_filter_link 'epics', _("Epics")
......@@ -77,7 +77,7 @@ RSpec.describe 'Global elastic search', :elastic, :sidekiq_inline do
end
end
describe 'I search through the issues and I see pagination', :js do
describe 'I search through the issues and I see pagination' do
before do
create_list(:issue, 21, project: project, title: 'initial')
......@@ -94,7 +94,7 @@ RSpec.describe 'Global elastic search', :elastic, :sidekiq_inline do
end
end
describe 'I search through the notes and I see pagination', :js do
describe 'I search through the notes and I see pagination' do
before do
issue = create(:issue, project: project, title: 'initial')
create_list(:note, 21, noteable: issue, project: project, note: 'foo')
......@@ -112,7 +112,7 @@ RSpec.describe 'Global elastic search', :elastic, :sidekiq_inline do
end
end
describe 'I search through the blobs', :js do
describe 'I search through the blobs' do
let(:project_2) { create(:project, :repository, :wiki_repo) }
before do
......@@ -156,7 +156,7 @@ RSpec.describe 'Global elastic search', :elastic, :sidekiq_inline do
end
end
describe 'I search through the wiki blobs', :js do
describe 'I search through the wiki blobs' do
before do
project.wiki.create_page('test.md', '# term')
project.wiki.index_wiki_blobs
......@@ -175,10 +175,9 @@ RSpec.describe 'Global elastic search', :elastic, :sidekiq_inline do
end
end
describe 'I search through the commits', :js do
describe 'I search through the commits' do
before do
project.repository.index_commits_and_blobs
ensure_elasticsearch_index!
end
......@@ -188,7 +187,7 @@ RSpec.describe 'Global elastic search', :elastic, :sidekiq_inline do
submit_search('add')
select_search_scope('Commits')
expect(page).to have_selector('.commit-row-message')
expect(page).to have_selector('.commit-row-description')
expect(page).to have_selector('.project-namespace')
end
......@@ -198,7 +197,7 @@ RSpec.describe 'Global elastic search', :elastic, :sidekiq_inline do
submit_search('add')
select_search_scope('Commits')
expected_message = "Merge branch 'tree_helper_spec' into 'master'"
expected_message = "Add directory structure for tree_helper spec"
expect(page).not_to have_content(expected_message)
......@@ -232,7 +231,7 @@ RSpec.describe 'Global elastic search', :elastic, :sidekiq_inline do
end
end
RSpec.describe 'Global elastic search redactions', :elastic, :js do
RSpec.describe 'Global elastic search redactions', :elastic do
context 'when block_anonymous_global_searches is disabled' do
before do
stub_feature_flags(block_anonymous_global_searches: false)
......
......@@ -83,7 +83,6 @@ RSpec.describe 'Group elastic search', :js, :elastic, :sidekiq_might_not_need_in
describe 'commit search' do
before do
project.repository.index_commits_and_blobs
ensure_elasticsearch_index!
end
......@@ -96,7 +95,7 @@ RSpec.describe 'Group elastic search', :js, :elastic, :sidekiq_might_not_need_in
end
end
RSpec.describe 'Group elastic search redactions', :elastic, :js do
RSpec.describe 'Group elastic search redactions', :elastic do
it_behaves_like 'a redacted search results page' do
let(:search_path) { group_path(public_group) }
end
......
......@@ -10,7 +10,7 @@ RSpec.describe 'Project elastic search', :js, :elastic do
stub_ee_application_setting(elasticsearch_search: true, elasticsearch_indexing: true)
end
describe 'searching', :sidekiq_inline do
describe 'searching' do
before do
project.add_maintainer(user)
sign_in(user)
......@@ -18,7 +18,7 @@ RSpec.describe 'Project elastic search', :js, :elastic do
visit project_path(project)
end
it 'finds issues' do
it 'finds issues', :sidekiq_inline do
create(:issue, project: project, title: 'Test searching for an issue')
ensure_elasticsearch_index!
......@@ -28,7 +28,7 @@ RSpec.describe 'Project elastic search', :js, :elastic do
expect(page).to have_selector('.results', text: 'Test searching for an issue')
end
it 'finds merge requests' do
it 'finds merge requests', :sidekiq_inline do
create(:merge_request, source_project: project, target_project: project, title: 'Test searching for an MR')
ensure_elasticsearch_index!
......@@ -38,7 +38,7 @@ RSpec.describe 'Project elastic search', :js, :elastic do
expect(page).to have_selector('.results', text: 'Test searching for an MR')
end
it 'finds milestones' do
it 'finds milestones', :sidekiq_inline do
create(:milestone, project: project, title: 'Test searching for a milestone')
ensure_elasticsearch_index!
......@@ -48,10 +48,9 @@ RSpec.describe 'Project elastic search', :js, :elastic do
expect(page).to have_selector('.results', text: 'Test searching for a milestone')
end
it 'finds wiki pages' do
it 'finds wiki pages', :sidekiq_inline do
project.wiki.create_page('test.md', 'Test searching for a wiki page')
project.wiki.index_wiki_blobs
ensure_elasticsearch_index!
submit_search('Test')
select_search_scope('Wiki')
......@@ -59,7 +58,7 @@ RSpec.describe 'Project elastic search', :js, :elastic do
expect(page).to have_selector('.results', text: 'Test searching for a wiki page')
end
it 'finds notes' do
it 'finds notes', :sidekiq_inline do
create(:note, project: project, note: 'Test searching for a comment')
ensure_elasticsearch_index!
......@@ -69,9 +68,8 @@ RSpec.describe 'Project elastic search', :js, :elastic do
expect(page).to have_selector('.results', text: 'Test searching for a comment')
end
it 'finds commits' do
it 'finds commits', :sidekiq_inline do
project.repository.index_commits_and_blobs
ensure_elasticsearch_index!
submit_search('initial')
select_search_scope('Commits')
......@@ -79,9 +77,8 @@ RSpec.describe 'Project elastic search', :js, :elastic do
expect(page).to have_selector('.results', text: 'Initial commit')
end
it 'finds blobs' do
it 'finds blobs', :sidekiq_inline do
project.repository.index_commits_and_blobs
ensure_elasticsearch_index!
submit_search('def')
select_search_scope('Code')
......@@ -129,7 +126,7 @@ RSpec.describe 'Project elastic search', :js, :elastic do
end
end
RSpec.describe 'Project elastic search redactions', :elastic, :js do
RSpec.describe 'Project elastic search redactions', :elastic do
it_behaves_like 'a redacted search results page' do
let(:search_path) { project_path(public_restricted_project) }
end
......
......@@ -295,67 +295,4 @@ RSpec.describe SearchHelper do
end
end
end
describe '#search_nav_tabs' do
let(:current_user) { nil }
subject { search_nav_tabs }
context 'when @show_snippets is present' do
before do
@show_snippets = 1
end
it { is_expected.to eq([:snippet_titles]) }
end
context 'when @project is present' do
before do
@project = 1
allow(self).to receive(:project_search_tabs?).with(anything).and_return(true)
end
it { is_expected.to eq([:blobs, :issues, :merge_requests, :milestones, :notes, :wiki_blobs, :commits, :users]) }
end
context 'when @show_snippets and @project are not present' do
context 'when user has access to read users' do
before do
allow(self).to receive(:can?).with(current_user, :read_users_list).and_return(true)
end
context 'when elasticsearch is enabled' do
before do
allow(self.search_service).to receive(:use_elasticsearch?).and_return(true)
end
it { is_expected.to eq([:projects, :issues, :merge_requests, :milestones, :notes, :blobs, :commits, :wiki_blobs, :users]) }
context 'when show_epics? is true' do
before do
allow(self.search_service).to receive(:show_epics?).and_return(true)
end
it { is_expected.to eq([:projects, :issues, :merge_requests, :milestones, :epics, :notes, :blobs, :commits, :wiki_blobs, :users]) }
end
end
context 'when elasticsearch is disabled' do
before do
allow(self.search_service).to receive(:use_elasticsearch?).and_return(false)
end
it { is_expected.to eq([:projects, :issues, :merge_requests, :milestones, :users]) }
context 'when show_epics? is true' do
before do
allow(self.search_service).to receive(:show_epics?).and_return(true)
end
it { is_expected.to eq([:projects, :issues, :merge_requests, :milestones, :epics, :users]) }
end
end
end
end
end
end
......@@ -29563,9 +29563,6 @@ msgstr ""
msgid "There was an error fetching the Node's Groups"
msgstr ""
msgid "There was an error fetching the Search Counts"
msgstr ""
msgid "There was an error fetching the deploy freezes."
msgstr ""
......
......@@ -4,7 +4,7 @@ module QA
module Page
module Search
class Results < QA::Page::Base
view 'app/assets/javascripts/search/topbar/constants.js' do
view 'app/views/search/_category.html.haml' do
element :code_tab
element :projects_tab
end
......
......@@ -28,7 +28,7 @@ RSpec.describe 'Global search' do
create_list(:issue, 2, project: project, title: 'initial')
end
it "has a pagination", :js do
it "has a pagination" do
submit_search('initial')
select_search_scope('Issues')
......
......@@ -2,7 +2,7 @@
require 'spec_helper'
RSpec.describe 'User searches for code', :js do
RSpec.describe 'User searches for code' do
let(:user) { create(:user) }
let(:project) { create(:project, :repository, namespace: user.namespace) }
......@@ -16,7 +16,6 @@ RSpec.describe 'User searches for code', :js do
visit(project_path(project))
submit_search('application.js')
select_search_scope('Code')
expect(page).to have_selector('.results', text: 'application.js')
......@@ -25,7 +24,7 @@ RSpec.describe 'User searches for code', :js do
expect(page).to have_link('application.js', href: /master\/files\/js\/application.js/)
end
context 'when on a project page' do
context 'when on a project page', :js do
before do
visit(search_path)
find('[data-testid="project-filter"]').click
......@@ -49,7 +48,7 @@ RSpec.describe 'User searches for code', :js do
expect(current_url).to match(/master\/.gitignore#L3/)
end
it 'search multiple words with refs switching' do
it 'search mutiple words with refs switching' do
expected_result = 'Use `snake_case` for naming files'
search = 'for naming files'
......@@ -68,7 +67,7 @@ RSpec.describe 'User searches for code', :js do
end
end
context 'search code within refs' do
context 'search code within refs', :js do
let(:ref_name) { 'v1.0.0' }
before do
......@@ -86,9 +85,9 @@ RSpec.describe 'User searches for code', :js do
expect(find('.js-project-refs-dropdown')).to have_text(ref_name)
end
# this example is use to test the design that the refs is not
# only represent the branch as well as the tags.
it 'ref switcher list all the branches and tags' do
# this example is use to test the desgine that the refs is not
# only repersent the branch as well as the tags.
it 'ref swither list all the branchs and tags' do
find('.js-project-refs-dropdown').click
expect(find('.dropdown-page-one .dropdown-content')).to have_link('sha-starting-with-large-number')
expect(find('.dropdown-page-one .dropdown-content')).to have_link('v1.0.0')
......
......@@ -2,7 +2,7 @@
require 'spec_helper'
RSpec.describe 'User searches for comments', :js do
RSpec.describe 'User searches for comments' do
let(:project) { create(:project, :repository) }
let(:user) { create(:user) }
......
......@@ -2,7 +2,7 @@
require 'spec_helper'
RSpec.describe 'User searches for users', :js do
RSpec.describe 'User searches for users' do
let(:user1) { create(:user, username: 'gob_bluth', name: 'Gob Bluth') }
let(:user2) { create(:user, username: 'michael_bluth', name: 'Michael Bluth') }
let(:user3) { create(:user, username: 'gob_2018', name: 'George Oscar Bluth') }
......@@ -12,7 +12,7 @@ RSpec.describe 'User searches for users', :js do
end
context 'when on the dashboard' do
it 'finds the user' do
it 'finds the user', :js do
visit dashboard_projects_path
submit_search('gob')
......
......@@ -2,7 +2,7 @@
require 'spec_helper'
RSpec.describe 'Search Snippets', :js do
RSpec.describe 'Search Snippets' do
it 'user searches for snippets by title' do
public_snippet = create(:personal_snippet, :public, title: 'Beginning and Middle')
private_snippet = create(:personal_snippet, :private, title: 'Middle and End')
......
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`pages/search/show/refresh_counts fetches and displays search counts 1`] = `
"<div class=\\"badge\\">22</div>
<div class=\\"badge js-search-count\\" data-url=\\"http://test.host/search/count?search=lorem+ipsum&amp;project_id=3&amp;scope=issues\\">4</div>
<div class=\\"badge js-search-count\\" data-url=\\"http://test.host/search/count?search=lorem+ipsum&amp;project_id=3&amp;scope=merge_requests\\">5</div>"
`;
import MockAdapter from 'axios-mock-adapter';
import { TEST_HOST } from 'helpers/test_constants';
import axios from '~/lib/utils/axios_utils';
import refreshCounts from '~/pages/search/show/refresh_counts';
const URL = `${TEST_HOST}/search/count?search=lorem+ipsum&project_id=3`;
const urlWithScope = (scope) => `${URL}&scope=${scope}`;
const counts = [
{ scope: 'issues', count: 4 },
{ scope: 'merge_requests', count: 5 },
];
const fixture = `<div class="badge">22</div>
<div class="badge js-search-count hidden" data-url="${urlWithScope('issues')}"></div>
<div class="badge js-search-count hidden" data-url="${urlWithScope('merge_requests')}"></div>`;
describe('pages/search/show/refresh_counts', () => {
let mock;
beforeEach(() => {
mock = new MockAdapter(axios);
setFixtures(fixture);
});
afterEach(() => {
mock.restore();
});
it('fetches and displays search counts', () => {
counts.forEach(({ scope, count }) => {
mock.onGet(urlWithScope(scope)).reply(200, { count });
});
// assert before act behavior
return refreshCounts().then(() => {
expect(document.body.innerHTML).toMatchSnapshot();
});
});
});
......@@ -61,28 +61,3 @@ export const MOCK_SORT_OPTIONS = [
},
},
];
export const MOCK_SEARCH_COUNTS_INPUT = {
scopeTabs: ['issues', 'snippet_titles', 'merge_requests'],
activeCount: '15',
};
export const MOCK_SEARCH_COUNT = { scope: 'issues', count: '15' };
export const MOCK_SEARCH_COUNTS_SUCCESS = [
{ scope: 'issues', count: '15' },
{ scope: 'snippet_titles', count: '15' },
{ scope: 'merge_requests', count: '15' },
];
export const MOCK_SEARCH_COUNTS = [
{ scope: 'issues', count: '15' },
{ scope: 'snippet_titles', count: '5' },
{ scope: 'merge_requests', count: '1' },
];
export const MOCK_SCOPE_TABS = [
{ scope: 'issues', title: 'Issues', count: '15' },
{ scope: 'snippet_titles', title: 'Titles and Descriptions', count: '5' },
{ scope: 'merge_requests', title: 'Merge requests', count: '1' },
];
......@@ -7,15 +7,7 @@ import * as urlUtils from '~/lib/utils/url_utility';
import createState from '~/search/store/state';
import axios from '~/lib/utils/axios_utils';
import createFlash from '~/flash';
import {
MOCK_QUERY,
MOCK_GROUPS,
MOCK_PROJECT,
MOCK_PROJECTS,
MOCK_SEARCH_COUNT,
MOCK_SEARCH_COUNTS_SUCCESS,
MOCK_SEARCH_COUNTS_INPUT,
} from '../mock_data';
import { MOCK_QUERY, MOCK_GROUPS, MOCK_PROJECT, MOCK_PROJECTS } from '../mock_data';
jest.mock('~/flash');
jest.mock('~/lib/utils/url_utility', () => ({
......@@ -45,21 +37,19 @@ describe('Global Search Store Actions', () => {
});
describe.each`
action | axiosMock | payload | type | expectedMutations | callback
${actions.fetchGroups} | ${{ method: 'onGet', code: 200, res: MOCK_GROUPS }} | ${null} | ${'success'} | ${[{ type: types.REQUEST_GROUPS }, { type: types.RECEIVE_GROUPS_SUCCESS, payload: MOCK_GROUPS }]} | ${noCallback}
${actions.fetchGroups} | ${{ method: 'onGet', code: 500, res: null }} | ${null} | ${'error'} | ${[{ type: types.REQUEST_GROUPS }, { type: types.RECEIVE_GROUPS_ERROR }]} | ${flashCallback}
${actions.fetchProjects} | ${{ method: 'onGet', code: 200, res: MOCK_PROJECTS }} | ${null} | ${'success'} | ${[{ type: types.REQUEST_PROJECTS }, { type: types.RECEIVE_PROJECTS_SUCCESS, payload: MOCK_PROJECTS }]} | ${noCallback}
${actions.fetchProjects} | ${{ method: 'onGet', code: 500, res: null }} | ${null} | ${'error'} | ${[{ type: types.REQUEST_PROJECTS }, { type: types.RECEIVE_PROJECTS_ERROR }]} | ${flashCallback}
${actions.fetchSearchCounts} | ${{ method: 'onGet', code: 200, res: MOCK_SEARCH_COUNT }} | ${MOCK_SEARCH_COUNTS_INPUT} | ${'success'} | ${[{ type: types.REQUEST_SEARCH_COUNTS, payload: MOCK_SEARCH_COUNTS_INPUT }, { type: types.RECEIVE_SEARCH_COUNTS_SUCCESS, payload: MOCK_SEARCH_COUNTS_SUCCESS }]} | ${noCallback}
${actions.fetchSearchCounts} | ${{ method: 'onGet', code: 500, res: null }} | ${MOCK_SEARCH_COUNTS_INPUT} | ${'error'} | ${[{ type: types.REQUEST_SEARCH_COUNTS, payload: MOCK_SEARCH_COUNTS_INPUT }]} | ${flashCallback}
`(`axios calls`, ({ action, axiosMock, payload, type, expectedMutations, callback }) => {
action | axiosMock | type | expectedMutations | callback
${actions.fetchGroups} | ${{ method: 'onGet', code: 200, res: MOCK_GROUPS }} | ${'success'} | ${[{ type: types.REQUEST_GROUPS }, { type: types.RECEIVE_GROUPS_SUCCESS, payload: MOCK_GROUPS }]} | ${noCallback}
${actions.fetchGroups} | ${{ method: 'onGet', code: 500, res: null }} | ${'error'} | ${[{ type: types.REQUEST_GROUPS }, { type: types.RECEIVE_GROUPS_ERROR }]} | ${flashCallback}
${actions.fetchProjects} | ${{ method: 'onGet', code: 200, res: MOCK_PROJECTS }} | ${'success'} | ${[{ type: types.REQUEST_PROJECTS }, { type: types.RECEIVE_PROJECTS_SUCCESS, payload: MOCK_PROJECTS }]} | ${noCallback}
${actions.fetchProjects} | ${{ method: 'onGet', code: 500, res: null }} | ${'error'} | ${[{ type: types.REQUEST_PROJECTS }, { type: types.RECEIVE_PROJECTS_ERROR }]} | ${flashCallback}
`(`axios calls`, ({ action, axiosMock, type, expectedMutations, callback }) => {
describe(action.name, () => {
describe(`on ${type}`, () => {
beforeEach(() => {
mock[axiosMock.method]().reply(axiosMock.code, axiosMock.res);
mock[axiosMock.method]().replyOnce(axiosMock.code, axiosMock.res);
});
it(`should dispatch the correct mutations`, () => {
return testAction({ action, payload, state, expectedMutations }).then(() => callback());
return testAction({ action, state, expectedMutations }).then(() => callback());
});
});
});
......@@ -125,25 +115,9 @@ describe('Global Search Store Actions', () => {
page: null,
state: null,
confidential: null,
nav_source: null,
});
expect(urlUtils.visitUrl).toHaveBeenCalled();
});
});
});
it('calls setUrlParams with snippets, group_id, and project_id when snippets param is true', () => {
return testAction(actions.resetQuery, true, state, [], [], () => {
expect(urlUtils.setUrlParams).toHaveBeenCalledWith({
...state.query,
page: null,
state: null,
confidential: null,
nav_source: null,
group_id: null,
project_id: null,
snippets: true,
});
});
});
});
import mutations from '~/search/store/mutations';
import createState from '~/search/store/state';
import * as types from '~/search/store/mutation_types';
import {
MOCK_QUERY,
MOCK_GROUPS,
MOCK_PROJECTS,
MOCK_SEARCH_COUNTS,
MOCK_SCOPE_TABS,
} from '../mock_data';
import { MOCK_QUERY, MOCK_GROUPS, MOCK_PROJECTS } from '../mock_data';
describe('Global Search Store Mutations', () => {
let state;
......@@ -77,32 +71,4 @@ describe('Global Search Store Mutations', () => {
expect(state.query[payload.key]).toBe(payload.value);
});
});
describe('REQUEST_SEARCH_COUNTS', () => {
it('sets the count to for the query.scope activeCount', () => {
const payload = { scopeTabs: ['issues'], activeCount: '22' };
mutations[types.REQUEST_SEARCH_COUNTS](state, payload);
expect(state.inflatedScopeTabs).toStrictEqual([
{ scope: 'issues', title: 'Issues', count: '22' },
]);
});
it('sets other scopes count to empty string', () => {
const payload = { scopeTabs: ['milestones'], activeCount: '22' };
mutations[types.REQUEST_SEARCH_COUNTS](state, payload);
expect(state.inflatedScopeTabs).toStrictEqual([
{ scope: 'milestones', title: 'Milestones', count: '' },
]);
});
});
describe('RECEIVE_SEARCH_COUNTS_SUCCESS', () => {
it('sets the count from the input for all tabs', () => {
mutations[types.RECEIVE_SEARCH_COUNTS_SUCCESS](state, MOCK_SEARCH_COUNTS);
expect(state.inflatedScopeTabs).toStrictEqual(MOCK_SCOPE_TABS);
});
});
});
......@@ -5,7 +5,6 @@ import { MOCK_QUERY } from 'jest/search/mock_data';
import GlobalSearchTopbar from '~/search/topbar/components/app.vue';
import GroupFilter from '~/search/topbar/components/group_filter.vue';
import ProjectFilter from '~/search/topbar/components/project_filter.vue';
import ScopeTabs from '~/search/topbar/components/scope_tabs.vue';
const localVue = createLocalVue();
localVue.use(Vuex);
......@@ -43,7 +42,6 @@ describe('GlobalSearchTopbar', () => {
const findGroupFilter = () => wrapper.find(GroupFilter);
const findProjectFilter = () => wrapper.find(ProjectFilter);
const findSearchButton = () => wrapper.find(GlButton);
const findScopeTabs = () => wrapper.find(ScopeTabs);
describe('template', () => {
beforeEach(() => {
......@@ -54,18 +52,6 @@ describe('GlobalSearchTopbar', () => {
expect(findTopbarForm().exists()).toBe(true);
});
describe('Scope Tabs', () => {
it('renders when search param is set', () => {
createComponent({ query: { search: 'test' } });
expect(findScopeTabs().exists()).toBe(true);
});
it('does not render search param is blank', () => {
createComponent({ query: {} });
expect(findScopeTabs().exists()).toBe(false);
});
});
describe('Search box', () => {
it('renders always', () => {
expect(findGlSearchBox().exists()).toBe(true);
......
import Vuex from 'vuex';
import { createLocalVue, mount } from '@vue/test-utils';
import { GlTabs, GlTab, GlBadge } from '@gitlab/ui';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import { MOCK_QUERY, MOCK_SCOPE_TABS } from 'jest/search/mock_data';
import ScopeTabs from '~/search/topbar/components/scope_tabs.vue';
const localVue = createLocalVue();
localVue.use(Vuex);
describe('ScopeTabs', () => {
let wrapper;
const actionSpies = {
fetchSearchCounts: jest.fn(),
setQuery: jest.fn(),
resetQuery: jest.fn(),
};
const defaultProps = {
scopeTabs: ['issues', 'merge_requests', 'milestones'],
count: '20',
};
const createComponent = (props = {}, initialState = {}) => {
const store = new Vuex.Store({
state: {
query: {
...MOCK_QUERY,
search: 'test',
},
...initialState,
},
actions: actionSpies,
});
wrapper = extendedWrapper(
mount(ScopeTabs, {
localVue,
store,
propsData: {
...defaultProps,
...props,
},
}),
);
};
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
const findScopeTabs = () => wrapper.find(GlTabs);
const findTabs = () => wrapper.findAll(GlTab);
const findBadges = () => wrapper.findAll(GlBadge);
const findTabsTitle = () =>
wrapper.findAll('[data-testid="tab-title"]').wrappers.map((w) => w.text());
const findBadgesTitle = () => findBadges().wrappers.map((w) => w.text());
const findBadgeByScope = (scope) => wrapper.findByTestId(`badge-${scope}`);
const findTabByScope = (scope) => wrapper.findByTestId(`tab-${scope}`);
describe('template', () => {
beforeEach(() => {
createComponent({}, { inflatedScopeTabs: MOCK_SCOPE_TABS });
});
it('always renders Scope Tabs', () => {
expect(findScopeTabs().exists()).toBe(true);
});
describe('findTabs', () => {
it('renders a tab for each scope', () => {
expect(findTabs()).toHaveLength(defaultProps.scopeTabs.length);
expect(findTabsTitle()).toStrictEqual([
'Issues',
'Titles and Descriptions',
'Merge requests',
]);
});
});
describe('findBadges', () => {
it('renders a badge for each scope', () => {
expect(findBadges()).toHaveLength(defaultProps.scopeTabs.length);
expect(findBadgesTitle()).toStrictEqual(['15', '5', '1']);
});
it('sets the variant to neutral for active tab only', () => {
expect(findBadgeByScope('issues').classes()).toContain('badge-neutral');
expect(findBadgeByScope('snippet_titles').classes()).toContain('badge-muted');
expect(findBadgeByScope('merge_requests').classes()).toContain('badge-muted');
});
});
});
describe('methods', () => {
beforeEach(() => {
createComponent({}, { inflatedScopeTabs: MOCK_SCOPE_TABS });
findTabByScope('snippet_titles').vm.$emit('click');
});
describe('handleTabChange', () => {
it('calls setQuery with scope, applies any search params from ALL_SCOPE_TABS, and sends nulls for page, state, confidential, and nav_source', () => {
expect(actionSpies.setQuery).toHaveBeenCalledWith(expect.any(Object), {
key: 'scope',
value: 'snippet_titles',
});
});
it('calls resetQuery and sends true for snippet_titles tab', () => {
expect(actionSpies.resetQuery).toHaveBeenCalledWith(expect.any(Object), true);
});
it('calls resetQuery and does not send true for other tabs', () => {
findTabByScope('issues').vm.$emit('click');
expect(actionSpies.resetQuery).toHaveBeenCalledWith(expect.any(Object), false);
});
});
});
});
......@@ -392,6 +392,63 @@ RSpec.describe SearchHelper do
end
end
describe 'search_filter_link' do
it 'renders a search filter link for the current scope' do
@scope = 'projects'
@search_results = double
expect(@search_results).to receive(:formatted_count).with('projects').and_return('23')
link = search_filter_link('projects', 'Projects')
expect(link).to have_css('li.active')
expect(link).to have_link('Projects', href: search_path(scope: 'projects'))
expect(link).to have_css('span.badge.badge-pill:not(.js-search-count):not(.hidden):not([data-url])', text: '23')
end
it 'renders a search filter link for another scope' do
link = search_filter_link('projects', 'Projects')
count_path = search_count_path(scope: 'projects')
expect(link).to have_css('li:not([class="active"])')
expect(link).to have_link('Projects', href: search_path(scope: 'projects'))
expect(link).to have_css("span.badge.badge-pill.js-search-count.hidden[data-url='#{count_path}']", text: '')
end
it 'merges in the current search params and given params' do
expect(self).to receive(:params).and_return(
ActionController::Parameters.new(
search: 'hello',
scope: 'ignored',
other_param: 'ignored'
)
)
link = search_filter_link('projects', 'Projects', search: { project_id: 23 })
expect(link).to have_link('Projects', href: search_path(scope: 'projects', search: 'hello', project_id: 23))
end
it 'restricts the params' do
expect(self).to receive(:params).and_return(
ActionController::Parameters.new(
search: 'hello',
unknown: 42
)
)
link = search_filter_link('projects', 'Projects')
expect(link).to have_link('Projects', href: search_path(scope: 'projects', search: 'hello'))
end
it 'assigns given data attributes on the list container' do
link = search_filter_link('projects', 'Projects', data: { foo: 'bar' })
expect(link).to have_css('li[data-foo="bar"]')
end
end
describe '#show_user_search_tab?' do
subject { show_user_search_tab? }
......@@ -584,86 +641,4 @@ RSpec.describe SearchHelper do
expect(search_sort_options).to eq(mock_created_sort)
end
end
describe '#search_nav_tabs' do
subject { search_nav_tabs }
let(:current_user) { nil }
before do
allow(self).to receive(:current_user).and_return(current_user)
end
context 'when @show_snippets is present' do
before do
@show_snippets = 1
end
it { is_expected.to eq([:snippet_titles]) }
context 'and @project is present' do
before do
@project = 1
allow(self).to receive(:project_search_tabs?).with(anything).and_return(true)
end
it { is_expected.to eq([:blobs, :issues, :merge_requests, :milestones, :notes, :wiki_blobs, :commits, :users]) }
end
end
context 'when @project is present' do
before do
@project = 1
end
context 'when user has access to project' do
before do
allow(self).to receive(:project_search_tabs?).with(anything).and_return(true)
end
it { is_expected.to eq([:blobs, :issues, :merge_requests, :milestones, :notes, :wiki_blobs, :commits, :users]) }
end
context 'when user does not have access to project' do
before do
allow(self).to receive(:project_search_tabs?).with(anything).and_return(false)
end
it { is_expected.to eq([]) }
end
context 'when user does not have access to read members for project' do
before do
allow(self).to receive(:project_search_tabs?).with(:members).and_return(false)
allow(self).to receive(:project_search_tabs?).with(:merge_requests).and_return(true)
allow(self).to receive(:project_search_tabs?).with(:milestones).and_return(true)
allow(self).to receive(:project_search_tabs?).with(:wiki_blobs).and_return(true)
allow(self).to receive(:project_search_tabs?).with(:issues).and_return(true)
allow(self).to receive(:project_search_tabs?).with(:blobs).and_return(true)
allow(self).to receive(:project_search_tabs?).with(:notes).and_return(true)
allow(self).to receive(:project_search_tabs?).with(:commits).and_return(true)
end
it { is_expected.to eq([:blobs, :issues, :merge_requests, :milestones, :notes, :wiki_blobs, :commits]) }
end
end
context 'when @show_snippets and @project are not present' do
context 'when user has access to read users' do
before do
allow(self).to receive(:can?).with(current_user, :read_users_list).and_return(true)
end
it { is_expected.to eq([:projects, :issues, :merge_requests, :milestones, :users]) }
end
context 'when user does not have access to read users' do
before do
allow(self).to receive(:can?).with(current_user, :read_users_list).and_return(false)
end
it { is_expected.to eq([:projects, :issues, :merge_requests, :milestones]) }
end
end
end
end
......@@ -6,6 +6,7 @@ RSpec.describe 'search/show' do
let(:search_term) { nil }
before do
stub_template "search/_category.html.haml" => 'Category Partial'
stub_template "search/_results.html.haml" => 'Results Partial'
@search_term = search_term
......@@ -20,6 +21,7 @@ RSpec.describe 'search/show' do
end
it 'does not render partials' do
expect(rendered).not_to render_template('search/_category')
expect(rendered).not_to render_template('search/_results')
end
end
......@@ -28,6 +30,7 @@ RSpec.describe 'search/show' do
let(:search_term) { 'Search Foo' }
it 'renders partials' do
expect(rendered).to render_template('search/_category')
expect(rendered).to render_template('search/_results')
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